lms-frontend/src/features/Settings/index.tsx

446 lines
12 KiB
TypeScript

import { Button, Flex, Form, Input, Modal } from "antd";
import HeaderBar from "core/components/Header";
import MyBanner from "shared/components/MyBanner";
import { SaveOutlined } from "@ant-design/icons";
import MyUpload from "shared/components/MyUpload";
import { Constants } from "core/utils/utils";
import ColorPicker from "antd/es/color-picker";
import {
useGetOrganizationSettingsQuery,
useIsSubdomainAvailableMutation,
useUpdateOrganizationSettingsMutation,
useUpdateSubdomainMutation,
} from "core/services/organization";
import MyErrorResult from "shared/components/MyResult";
import { useForm } from "antd/es/form/Form";
import { useEffect, useRef, useState } from "react";
import { AggregationColor } from "antd/es/color-picker/color";
import { useDispatch, useSelector } from "react-redux";
import {
bannerUrl,
logoUrl,
setBannerUrl,
setLogoUrl,
setPrimaryColor,
} from "core/reducers/appSlice";
import MyMiddleCard from "shared/components/MyMiddleCard";
import { OrganizationSettings } from "core/types/organization";
import { useMessage } from "core/context/MessageContext";
import {
addWebSocketReconnectListener,
removeWebSocketReconnectListener,
} from "core/services/websocketService";
import { useTranslation } from "react-i18next";
type GeneralFieldType = {
primaryColor: string | AggregationColor;
companyName: string;
};
export default function Settings() {
const { t } = useTranslation();
const { data, error, isLoading, refetch } = useGetOrganizationSettingsQuery(
undefined,
{
refetchOnMountOrArgChange: true,
}
);
useEffect(() => {
addWebSocketReconnectListener(refetch);
return () => removeWebSocketReconnectListener(refetch);
}, []);
if (error) return <MyErrorResult />;
return (
<>
<MyBanner
title={t("organizationSettings.bannerTitle")}
subtitle={t("common.bannerSubtitle")}
headerBar={<HeaderBar />}
/>
<GeneralCard data={data} isLoading={isLoading} />
<MediaCard isLoading={isLoading} />
<SubdomainCard data={data} isLoading={isLoading} />
</>
);
}
function GeneralCard({
data,
isLoading,
}: { data?: OrganizationSettings; isLoading?: boolean } = {}) {
const { t } = useTranslation();
const [form] = useForm<GeneralFieldType>();
const { success, error: errorMessage } = useMessage();
const dispatch = useDispatch();
const debounceRef = useRef<null | NodeJS.Timeout>(null);
const currentPrimaryColor = useRef<string>();
const [updateOrganizationSettings, { isLoading: isUpdateSettingsLoading }] =
useUpdateOrganizationSettingsMutation();
const handleSave = async (values: GeneralFieldType) => {
const hexColor =
typeof values.primaryColor === "string"
? values.primaryColor
: values.primaryColor.toHexString().split("#")[1];
try {
await updateOrganizationSettings({
primaryColor: hexColor,
companyName: values.companyName,
}).unwrap();
currentPrimaryColor.current = hexColor;
success(
t("organizationSettings.generalCard.messageSettingsSuccessfullyUpdated")
);
} catch (error) {
console.error(error);
errorMessage(t("common.messageRequestFailed"));
}
};
useEffect(() => {
if (data) {
form.setFieldsValue({
primaryColor: data.PrimaryColor,
companyName: data.CompanyName,
});
currentPrimaryColor.current = data.PrimaryColor;
}
}, [data]);
useEffect(() => {
return () => {
dispatch(setPrimaryColor(currentPrimaryColor.current));
if (debounceRef.current) {
clearTimeout(debounceRef.current);
}
};
}, []);
return (
<Form form={form} onFinish={handleSave} layout="vertical">
<MyMiddleCard
styles={{
body: {
padding: 16,
},
}}
title={t("organizationSettings.generalCard.title")}
loading={isLoading}
extra={
<Button
icon={<SaveOutlined />}
type="text"
shape="circle"
size="large"
htmlType="submit"
loading={isUpdateSettingsLoading}
/>
}
>
<Flex wrap gap={12}>
<Form.Item<GeneralFieldType>
name="primaryColor"
label={t("organizationSettings.generalCard.primaryColor")}
>
<ColorPicker
size="small"
showText
onChange={(color: AggregationColor) => {
if (debounceRef.current) {
clearTimeout(debounceRef.current);
}
debounceRef.current = setTimeout(() => {
dispatch(setPrimaryColor(color.toHexString().split("#")[1]));
}, 600);
}}
/>
</Form.Item>
<Form.Item<GeneralFieldType>
name="companyName"
label={t("organizationSettings.generalCard.companyName")}
>
<Input />
</Form.Item>
</Flex>
</MyMiddleCard>
</Form>
);
}
function MediaCard({
isLoading,
}: { data?: OrganizationSettings; isLoading?: boolean } = {}) {
const { t } = useTranslation();
const { success } = useMessage();
const dispatch = useDispatch();
const appLogoUrl = useSelector(logoUrl);
const appBannerUrl = useSelector(bannerUrl);
return (
<Form layout="vertical">
<MyMiddleCard
title={t("organizationSettings.mediaCard.title")}
loading={isLoading}
>
<Form.Item label={t("organizationSettings.mediaCard.logo")}>
<MyUpload
action="/organization/file/logo"
onChange={(info) => {
if (info.file.status === "done" && info.file.response.Data) {
dispatch(setLogoUrl(info.file.response.Data));
success(
t(
"organizationSettings.mediaCard.messageLogoSuccessfullyUpdated"
)
);
}
}}
imgCropProps={{
aspect: 1 / 1,
children: <></>,
}}
>
<img
src={`${Constants.STATIC_CONTENT_ADDRESS}/${
appLogoUrl || Constants.DEMO_LOGO_URL
}`}
alt={t("organizationSettings.mediaCard.logo")}
style={{
width: 128,
maxHeight: 128,
padding: 4,
borderRadius: 4,
border: "1px solid #ddd",
}}
/>
</MyUpload>
</Form.Item>
<Form.Item label={t("organizationSettings.mediaCard.banner")}>
<MyUpload
action="/organization/file/banner"
onChange={(info) => {
if (info.file.status === "done" && info.file.response.Data) {
dispatch(setBannerUrl(info.file.response.Data));
success(
t(
"organizationSettings.mediaCard.messageBannerSuccessfullyUpdated"
)
);
}
}}
imgCropProps={{
aspect: 22 / 9,
children: <></>,
}}
>
<img
src={`${Constants.STATIC_CONTENT_ADDRESS}/${
appBannerUrl || Constants.DEMO_BANNER_URL
}`}
alt={t("organizationSettings.mediaCard.banner")}
style={{
width: "100%",
height: 228,
objectFit: "cover",
padding: 4,
borderRadius: 4,
border: "1px solid #ddd",
}}
/>
</MyUpload>
</Form.Item>
</MyMiddleCard>
</Form>
);
}
const subdomainPattern = /^[a-zA-Z0-9-]+$/;
function SubdomainCard({
data,
isLoading,
}: { data?: OrganizationSettings; isLoading?: boolean } = {}) {
const { t } = useTranslation();
const [form] = useForm();
const { success, info } = useMessage();
const [isModalOpen, setIsModalOpen] = useState(false);
const [reqIsSubdomainAvailable] = useIsSubdomainAvailableMutation();
const [reqUpdateSubdomain] = useUpdateSubdomainMutation();
const validateSubdomain = async (rule: any, value: string) => {
if (value) {
if (
value.length < Constants.GLOBALS.MIN_SUBDOMAIN_LENGTH ||
value.length > Constants.GLOBALS.MAX_SUBDOMAIN_LENGTH ||
!subdomainPattern.test(value)
) {
return Promise.reject();
}
// Check if subdomain is current subdomain
if (value === window.location.hostname.split(".")[0]) {
return Promise.resolve();
}
try {
const { data } = await reqIsSubdomainAvailable(value);
if (!data.Available) {
return Promise.reject(
t("organizationSettings.subdomainCard.subdomainAlreadyTaken")
);
}
return Promise.resolve();
} catch (error) {
return Promise.reject(
t("organizationSettings.subdomainCard.subdomainAlreadyTaken")
);
}
}
return Promise.resolve();
};
useEffect(() => {
if (data) {
form.setFieldsValue({
subdomain: data.Subdomain,
});
}
}, [data]);
return (
<>
<Modal
title={t(
"organizationSettings.subdomainCard.modalChangeSubdomain.title"
)}
open={isModalOpen}
centered
onCancel={() => setIsModalOpen(false)}
okText={t("common.change")}
onOk={async () => {
try {
await reqUpdateSubdomain(form.getFieldValue("subdomain"));
success(
t(
"organizationSettings.subdomainCard.modalChangeSubdomain.messageSubdomainSuccessfullyUpdated"
)
);
info(
t(
"organizationSettings.subdomainCard.modalChangeSubdomain.messageRedirect"
)
);
/*
window.location.href = `https://${form.getFieldValue(
"subdomain"
)}.${window.location.hostname.split(".").slice(1).join(".")}`; */
} catch (error) {
console.error(error);
}
}}
>
<p>
{t("organizationSettings.subdomainCard.modalChangeSubdomain.message")}
</p>
</Modal>
<Form form={form} layout="vertical" requiredMark={false}>
<MyMiddleCard
title={t("organizationSettings.subdomainCard.middleCardTitle")}
loading={isLoading}
extra={
<Button
icon={<SaveOutlined />}
type="text"
shape="circle"
size="large"
onClick={() => {
form
.validateFields()
.then(() => setIsModalOpen(true))
.catch(() => {});
}}
/>
}
>
<Flex>
<Form.Item
name="subdomain"
label={t("organizationSettings.subdomainCard.subdomain")}
hasFeedback
validateDebounce={300}
rules={[
{
required: true,
message: t(
"organizationSettings.subdomainCard.rules.required"
),
},
{
pattern: subdomainPattern,
message: t("organizationSettings.subdomainCard.rules.valid"),
},
{
min: Constants.GLOBALS.MIN_SUBDOMAIN_LENGTH,
message: t(
"organizationSettings.subdomainCard.rules.minLength",
{
minLength: Constants.GLOBALS.MIN_SUBDOMAIN_LENGTH,
}
),
},
{
max: Constants.GLOBALS.MAX_SUBDOMAIN_LENGTH,
message: t(
"organizationSettings.subdomainCard.rules.maxLength",
{
maxLength: Constants.GLOBALS.MAX_SUBDOMAIN_LENGTH,
}
),
},
{
required: true,
validator: validateSubdomain,
},
]}
>
<Input
addonBefore="https://"
addonAfter=". xx.de"
maxLength={Constants.GLOBALS.MAX_SUBDOMAIN_LENGTH}
/>
</Form.Item>
</Flex>
</MyMiddleCard>
</Form>
</>
);
}