mirror of
https://github.com/logto-io/logto.git
synced 2024-12-30 20:33:54 -05:00
refactor(console): refactor custom connector form initialization (#4110)
This commit is contained in:
parent
bbf0551273
commit
97fc519ac1
4 changed files with 71 additions and 68 deletions
|
@ -1,7 +1,6 @@
|
||||||
import { ServiceConnector } from '@logto/connector-kit';
|
import { ServiceConnector } from '@logto/connector-kit';
|
||||||
import { ConnectorType } from '@logto/schemas';
|
import { ConnectorType } from '@logto/schemas';
|
||||||
import type { ConnectorResponse } from '@logto/schemas';
|
import type { ConnectorResponse } from '@logto/schemas';
|
||||||
import type { Optional } from '@silverhand/essentials';
|
|
||||||
import { conditional } from '@silverhand/essentials';
|
import { conditional } from '@silverhand/essentials';
|
||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
|
@ -19,7 +18,7 @@ import { useConnectorFormConfigParser } from '@/hooks/use-connector-form-config-
|
||||||
import useDocumentationUrl from '@/hooks/use-documentation-url';
|
import useDocumentationUrl from '@/hooks/use-documentation-url';
|
||||||
import { SyncProfileMode } from '@/types/connector';
|
import { SyncProfileMode } from '@/types/connector';
|
||||||
import type { ConnectorFormType } from '@/types/connector';
|
import type { ConnectorFormType } from '@/types/connector';
|
||||||
import { initFormData } from '@/utils/connector-form';
|
import { convertResponseToForm } from '@/utils/connector-form';
|
||||||
import { trySubmitSafe } from '@/utils/form';
|
import { trySubmitSafe } from '@/utils/form';
|
||||||
|
|
||||||
import EmailServiceConnectorForm from './EmailServiceConnectorForm';
|
import EmailServiceConnectorForm from './EmailServiceConnectorForm';
|
||||||
|
@ -30,29 +29,22 @@ type Props = {
|
||||||
onConnectorUpdated: (connector: ConnectorResponse) => void;
|
onConnectorUpdated: (connector: ConnectorResponse) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getConnectorTarget = (connectorData: ConnectorResponse): Optional<string> => {
|
|
||||||
return conditional(
|
|
||||||
connectorData.type === ConnectorType.Social &&
|
|
||||||
!connectorData.isStandard &&
|
|
||||||
(connectorData.metadata.target ?? connectorData.target)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function ConnectorContent({ isDeleted, connectorData, onConnectorUpdated }: Props) {
|
function ConnectorContent({ isDeleted, connectorData, onConnectorUpdated }: Props) {
|
||||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||||
const { getDocumentationUrl } = useDocumentationUrl();
|
const { getDocumentationUrl } = useDocumentationUrl();
|
||||||
const api = useApi();
|
const api = useApi();
|
||||||
|
const formData = useMemo(() => convertResponseToForm(connectorData), [connectorData]);
|
||||||
const formConfig = useMemo(() => {
|
|
||||||
const { formItems, config } = connectorData;
|
|
||||||
return conditional(formItems && initFormData(formItems, config)) ?? {};
|
|
||||||
}, [connectorData]);
|
|
||||||
|
|
||||||
const methods = useForm<ConnectorFormType>({
|
const methods = useForm<ConnectorFormType>({
|
||||||
reValidateMode: 'onBlur',
|
reValidateMode: 'onBlur',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
syncProfile: SyncProfileMode.OnlyAtRegister,
|
...formData,
|
||||||
target: getConnectorTarget(connectorData),
|
/**
|
||||||
|
* Note:
|
||||||
|
* The `formConfig` will be setup in the `useEffect` hook since react-hook-form's `useForm` hook infers `Record<string, unknown>` to `{ [x: string]: {} | undefined }` incorrectly,
|
||||||
|
* this causes we cannot apply the default value of `formConfig` to the form.
|
||||||
|
*/
|
||||||
|
formConfig: {},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -61,7 +53,6 @@ function ConnectorContent({ isDeleted, connectorData, onConnectorUpdated }: Prop
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
watch,
|
watch,
|
||||||
reset,
|
reset,
|
||||||
setValue,
|
|
||||||
} = methods;
|
} = methods;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -72,27 +63,13 @@ function ConnectorContent({ isDeleted, connectorData, onConnectorUpdated }: Prop
|
||||||
isStandard: isStandardConnector,
|
isStandard: isStandardConnector,
|
||||||
metadata: { logoDark },
|
metadata: { logoDark },
|
||||||
} = connectorData;
|
} = connectorData;
|
||||||
|
|
||||||
const isSocialConnector = connectorType === ConnectorType.Social;
|
const isSocialConnector = connectorType === ConnectorType.Social;
|
||||||
const isEmailServiceConnector = connectorId === ServiceConnector.Email;
|
const isEmailServiceConnector = connectorId === ServiceConnector.Email;
|
||||||
useEffect(() => {
|
|
||||||
const { metadata, config, syncProfile } = connectorData;
|
|
||||||
const { name, logo, logoDark } = metadata;
|
|
||||||
|
|
||||||
reset({
|
useEffect(() => {
|
||||||
target: getConnectorTarget(connectorData),
|
reset(formData);
|
||||||
logo,
|
}, [formData, reset]);
|
||||||
logoDark: logoDark ?? '',
|
|
||||||
name: name?.en,
|
|
||||||
jsonConfig: JSON.stringify(config, null, 2),
|
|
||||||
syncProfile: syncProfile ? SyncProfileMode.EachSignIn : SyncProfileMode.OnlyAtRegister,
|
|
||||||
});
|
|
||||||
/**
|
|
||||||
* Note:
|
|
||||||
* Set `formConfig` independently.
|
|
||||||
* Since react-hook-form's reset function infers `Record<string, unknown>` to `{ [x: string]: {} | undefined }` incorrectly.
|
|
||||||
*/
|
|
||||||
setValue('formConfig', formConfig, { shouldDirty: false });
|
|
||||||
}, [connectorData, formConfig, reset, setValue]);
|
|
||||||
|
|
||||||
const configParser = useConnectorFormConfigParser();
|
const configParser = useConnectorFormConfigParser();
|
||||||
|
|
||||||
|
@ -133,12 +110,6 @@ function ConnectorContent({ isDeleted, connectorData, onConnectorUpdated }: Prop
|
||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
onDiscard={() => {
|
onDiscard={() => {
|
||||||
reset();
|
reset();
|
||||||
/**
|
|
||||||
* Note:
|
|
||||||
* Reset `formConfig` manually since react-hook-form's `useForm` hook infers `Record<string, unknown>` to `{ [x: string]: {} | undefined }` incorrectly,
|
|
||||||
* this causes we cannot apply the default value of `formConfig` to the form.
|
|
||||||
*/
|
|
||||||
setValue('formConfig', formConfig, { shouldDirty: false });
|
|
||||||
}}
|
}}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
>
|
>
|
||||||
|
|
|
@ -29,7 +29,7 @@ import { useConnectorFormConfigParser } from '@/hooks/use-connector-form-config-
|
||||||
import * as modalStyles from '@/scss/modal.module.scss';
|
import * as modalStyles from '@/scss/modal.module.scss';
|
||||||
import type { ConnectorFormType } from '@/types/connector';
|
import type { ConnectorFormType } from '@/types/connector';
|
||||||
import { SyncProfileMode } from '@/types/connector';
|
import { SyncProfileMode } from '@/types/connector';
|
||||||
import { initFormData } from '@/utils/connector-form';
|
import { convertFactoryResponseToForm } from '@/utils/connector-form';
|
||||||
import { trySubmitSafe } from '@/utils/form';
|
import { trySubmitSafe } from '@/utils/form';
|
||||||
|
|
||||||
import { splitMarkdownByTitle } from '../utils';
|
import { splitMarkdownByTitle } from '../utils';
|
||||||
|
@ -50,37 +50,35 @@ function Guide({ connector, onClose }: Props) {
|
||||||
const { updateConfigs } = useConfigs();
|
const { updateConfigs } = useConfigs();
|
||||||
const [conflictConnectorName, setConflictConnectorName] = useState<Record<string, string>>();
|
const [conflictConnectorName, setConflictConnectorName] = useState<Record<string, string>>();
|
||||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||||
const { type: connectorType, formItems, target, isStandard, configTemplate } = connector ?? {};
|
const { type: connectorType, formItems, isStandard } = connector ?? {};
|
||||||
const { language } = i18next;
|
const { language } = i18next;
|
||||||
|
|
||||||
const isSocialConnector =
|
const isSocialConnector =
|
||||||
connectorType !== ConnectorType.Sms && connectorType !== ConnectorType.Email;
|
connectorType !== ConnectorType.Sms && connectorType !== ConnectorType.Email;
|
||||||
|
|
||||||
const methods = useForm<ConnectorFormType>({
|
const methods = useForm<ConnectorFormType>({
|
||||||
reValidateMode: 'onBlur',
|
reValidateMode: 'onBlur',
|
||||||
|
defaultValues: {
|
||||||
|
jsonConfig: '{}',
|
||||||
|
formConfig: {},
|
||||||
|
syncProfile: SyncProfileMode.OnlyAtRegister,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
formState: { isSubmitting },
|
formState: { isSubmitting },
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
watch,
|
watch,
|
||||||
setError,
|
setError,
|
||||||
reset,
|
reset,
|
||||||
setValue,
|
|
||||||
} = methods;
|
} = methods;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const formConfig = conditional(formItems && initFormData(formItems));
|
if (!connector) {
|
||||||
reset({
|
return;
|
||||||
...(configTemplate ? { jsonConfig: configTemplate } : {}),
|
}
|
||||||
...(isSocialConnector && !isStandard && target ? { target } : {}),
|
reset(convertFactoryResponseToForm(connector));
|
||||||
syncProfile: SyncProfileMode.OnlyAtRegister,
|
}, [reset, connector]);
|
||||||
});
|
|
||||||
/**
|
|
||||||
* Note:
|
|
||||||
* Set `formConfig` independently.
|
|
||||||
* Since react-hook-form's reset function infers `Record<string, unknown>` to `{ [x: string]: {} | undefined }` incorrectly.
|
|
||||||
*/
|
|
||||||
setValue('formConfig', formConfig ?? {}, { shouldDirty: false });
|
|
||||||
}, [formItems, reset, configTemplate, target, isSocialConnector, isStandard, setValue]);
|
|
||||||
|
|
||||||
const configParser = useConnectorFormConfigParser();
|
const configParser = useConnectorFormConfigParser();
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import type { ConnectorResponse } from '@logto/schemas';
|
import type { ConnectorResponse } from '@logto/schemas';
|
||||||
|
import { type Nullable } from '@silverhand/essentials';
|
||||||
|
|
||||||
export type ConnectorGroup<T = ConnectorResponse> = Pick<
|
export type ConnectorGroup<T = ConnectorResponse> = Pick<
|
||||||
ConnectorResponse,
|
ConnectorResponse,
|
||||||
|
@ -14,11 +15,11 @@ export enum SyncProfileMode {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ConnectorFormType = {
|
export type ConnectorFormType = {
|
||||||
|
name?: string;
|
||||||
|
logo?: string;
|
||||||
|
logoDark?: Nullable<string>;
|
||||||
|
target?: string;
|
||||||
|
syncProfile: SyncProfileMode;
|
||||||
jsonConfig: string; // Support editing configs by the code editor
|
jsonConfig: string; // Support editing configs by the code editor
|
||||||
formConfig: Record<string, unknown>; // Support custom connector config form
|
formConfig: Record<string, unknown>; // Support custom connector config form
|
||||||
name: string;
|
|
||||||
logo: string;
|
|
||||||
logoDark: string;
|
|
||||||
target: string;
|
|
||||||
syncProfile: SyncProfileMode;
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import type { ConnectorConfigFormItem } from '@logto/connector-kit';
|
import type { ConnectorConfigFormItem } from '@logto/connector-kit';
|
||||||
import { ConnectorConfigFormItemType } from '@logto/connector-kit';
|
import { ConnectorConfigFormItemType, ConnectorType } from '@logto/connector-kit';
|
||||||
|
import { type ConnectorFactoryResponse, type ConnectorResponse } from '@logto/schemas';
|
||||||
|
import { conditional } from '@silverhand/essentials';
|
||||||
|
|
||||||
|
import { SyncProfileMode, type ConnectorFormType } from '@/types/connector';
|
||||||
import { safeParseJson } from '@/utils/json';
|
import { safeParseJson } from '@/utils/json';
|
||||||
|
|
||||||
export const initFormData = (
|
const initFormData = (formItems: ConnectorConfigFormItem[], config?: Record<string, unknown>) => {
|
||||||
formItems: ConnectorConfigFormItem[],
|
|
||||||
config?: Record<string, unknown>
|
|
||||||
) => {
|
|
||||||
const data: Array<[string, unknown]> = formItems.map((item) => {
|
const data: Array<[string, unknown]> = formItems.map((item) => {
|
||||||
const value = config?.[item.key] ?? item.defaultValue;
|
const value = config?.[item.key] ?? item.defaultValue;
|
||||||
|
|
||||||
|
@ -60,3 +60,36 @@ export const parseFormConfig = (
|
||||||
.filter((item): item is [string, unknown] => Array.isArray(item))
|
.filter((item): item is [string, unknown] => Array.isArray(item))
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const convertResponseToForm = (connector: ConnectorResponse): ConnectorFormType => {
|
||||||
|
const { metadata, type, config, syncProfile, isStandard, formItems, target } = connector;
|
||||||
|
const { name, logo, logoDark } = metadata;
|
||||||
|
const formConfig = conditional(formItems && initFormData(formItems, config)) ?? {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: name?.en,
|
||||||
|
logo,
|
||||||
|
logoDark,
|
||||||
|
target: conditional(
|
||||||
|
type === ConnectorType.Social && !isStandard && (metadata.target ?? target)
|
||||||
|
),
|
||||||
|
syncProfile: syncProfile ? SyncProfileMode.EachSignIn : SyncProfileMode.OnlyAtRegister,
|
||||||
|
jsonConfig: JSON.stringify(config, null, 2),
|
||||||
|
formConfig,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const convertFactoryResponseToForm = (
|
||||||
|
connectorFactory: ConnectorFactoryResponse
|
||||||
|
): ConnectorFormType => {
|
||||||
|
const { formItems, configTemplate, type, isStandard, target } = connectorFactory;
|
||||||
|
const jsonConfig = configTemplate ?? '{}';
|
||||||
|
const formConfig = conditional(formItems && initFormData(formItems)) ?? {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
target: conditional(type === ConnectorType.Social && !isStandard && target),
|
||||||
|
syncProfile: SyncProfileMode.OnlyAtRegister,
|
||||||
|
jsonConfig,
|
||||||
|
formConfig,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in a new issue