446 lines
12 KiB
TypeScript
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>
|
|
</>
|
|
);
|
|
}
|