mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
fix(console): avoid connector custom config fields conflict with reserved fields (#4088)
This commit is contained in:
parent
166ccf93ae
commit
3a8f016d6a
7 changed files with 63 additions and 25 deletions
|
@ -1,5 +1,6 @@
|
|||
import type { ConnectorConfigFormItem } from '@logto/connector-kit';
|
||||
import { ConnectorConfigFormItemType } from '@logto/connector-kit';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import { useMemo } from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
@ -25,11 +26,13 @@ function ConfigFormFields({ formItems }: Props) {
|
|||
watch,
|
||||
register,
|
||||
control,
|
||||
formState: { errors },
|
||||
formState: {
|
||||
errors: { formConfig: formConfigErrors },
|
||||
},
|
||||
} = useFormContext<ConnectorFormType>();
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
const values = watch();
|
||||
const values = watch('formConfig');
|
||||
|
||||
const filteredFormItems = useMemo(() => {
|
||||
return formItems.filter((item) => {
|
||||
|
@ -46,10 +49,13 @@ function ConfigFormFields({ formItems }: Props) {
|
|||
}, [formItems, values]);
|
||||
|
||||
const renderFormItem = (item: ConnectorConfigFormItem) => {
|
||||
const error = errors[item.key]?.message ?? Boolean(errors[item.key]);
|
||||
const error = conditional(
|
||||
formConfigErrors &&
|
||||
(formConfigErrors[item.key]?.message ?? Boolean(formConfigErrors[item.key]))
|
||||
);
|
||||
|
||||
const buildCommonProperties = () => ({
|
||||
...register(item.key, {
|
||||
...register(`formConfig.${item.key}`, {
|
||||
required: item.required,
|
||||
valueAsNumber: item.type === ConnectorConfigFormItemType.Number,
|
||||
}),
|
||||
|
@ -77,7 +83,7 @@ function ConfigFormFields({ formItems }: Props) {
|
|||
|
||||
return (
|
||||
<Controller
|
||||
name={item.key}
|
||||
name={`formConfig.${item.key}`}
|
||||
control={control}
|
||||
rules={{
|
||||
// For switch, "false" will be treated as an empty value, so we need to set required to false.
|
||||
|
|
|
@ -69,14 +69,14 @@ function ConfigForm({ formItems, className, connectorId, connectorType }: Props)
|
|||
) : (
|
||||
<FormField title="connectors.guide.config">
|
||||
<Controller
|
||||
name="config"
|
||||
name="jsonConfig"
|
||||
control={control}
|
||||
rules={{
|
||||
validate: (value) => jsonValidator(value) || t('errors.invalid_json_format'),
|
||||
}}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<CodeEditor
|
||||
error={errors.config?.message ?? Boolean(errors.config)}
|
||||
error={errors.jsonConfig?.message ?? Boolean(errors.jsonConfig)}
|
||||
language="json"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
|
|
|
@ -32,6 +32,8 @@ export const useConnectorFormConfigParser = () => {
|
|||
const parseJsonConfig = useJsonStringConfigParser();
|
||||
|
||||
return (data: ConnectorFormType, formItems: ConnectorResponse['formItems']) => {
|
||||
return formItems ? parseFormConfig(data, formItems) : parseJsonConfig(data.config);
|
||||
return formItems
|
||||
? parseFormConfig(data.formConfig, formItems)
|
||||
: parseJsonConfig(data.jsonConfig);
|
||||
};
|
||||
};
|
||||
|
|
|
@ -2,7 +2,7 @@ import { ConnectorType } from '@logto/schemas';
|
|||
import type { ConnectorResponse } from '@logto/schemas';
|
||||
import type { Optional } from '@silverhand/essentials';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
@ -39,6 +39,12 @@ function ConnectorContent({ isDeleted, connectorData, onConnectorUpdated }: Prop
|
|||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { getDocumentationUrl } = useDocumentationUrl();
|
||||
const api = useApi();
|
||||
|
||||
const formConfig = useMemo(() => {
|
||||
const { formItems, config } = connectorData;
|
||||
return conditional(formItems && initFormData(formItems, config)) ?? {};
|
||||
}, [connectorData]);
|
||||
|
||||
const methods = useForm<ConnectorFormType>({
|
||||
reValidateMode: 'onBlur',
|
||||
defaultValues: {
|
||||
|
@ -46,28 +52,35 @@ function ConnectorContent({ isDeleted, connectorData, onConnectorUpdated }: Prop
|
|||
target: getConnectorTarget(connectorData),
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
formState: { isSubmitting, isDirty },
|
||||
handleSubmit,
|
||||
watch,
|
||||
reset,
|
||||
setValue,
|
||||
} = methods;
|
||||
const isSocialConnector = connectorData.type === ConnectorType.Social;
|
||||
|
||||
useEffect(() => {
|
||||
const { formItems, metadata, config, syncProfile } = connectorData;
|
||||
const { name, logo, logoDark, target } = metadata;
|
||||
const { metadata, config, syncProfile } = connectorData;
|
||||
const { name, logo, logoDark } = metadata;
|
||||
|
||||
reset({
|
||||
...(formItems ? initFormData(formItems, config) : {}),
|
||||
target: getConnectorTarget(connectorData) ?? target,
|
||||
target: getConnectorTarget(connectorData),
|
||||
logo,
|
||||
logoDark: logoDark ?? '',
|
||||
name: name?.en,
|
||||
config: JSON.stringify(config, null, 2),
|
||||
jsonConfig: JSON.stringify(config, null, 2),
|
||||
syncProfile: syncProfile ? SyncProfileMode.EachSignIn : SyncProfileMode.OnlyAtRegister,
|
||||
});
|
||||
}, [connectorData, reset]);
|
||||
/**
|
||||
* 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();
|
||||
|
||||
|
@ -95,7 +108,6 @@ function ConnectorContent({ isDeleted, connectorData, onConnectorUpdated }: Prop
|
|||
json: body,
|
||||
})
|
||||
.json<ConnectorResponse>();
|
||||
|
||||
onConnectorUpdated(updatedConnector);
|
||||
toast.success(t('general.saved'));
|
||||
})
|
||||
|
@ -107,7 +119,15 @@ function ConnectorContent({ isDeleted, connectorData, onConnectorUpdated }: Prop
|
|||
autoComplete="off"
|
||||
isDirty={isDirty}
|
||||
isSubmitting={isSubmitting}
|
||||
onDiscard={reset}
|
||||
onDiscard={() => {
|
||||
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}
|
||||
>
|
||||
{isSocialConnector && (
|
||||
|
|
|
@ -64,16 +64,23 @@ function Guide({ connector, onClose }: Props) {
|
|||
watch,
|
||||
setError,
|
||||
reset,
|
||||
setValue,
|
||||
} = methods;
|
||||
|
||||
useEffect(() => {
|
||||
const formConfig = conditional(formItems && initFormData(formItems));
|
||||
reset({
|
||||
...(formItems ? initFormData(formItems) : {}),
|
||||
...(configTemplate ? { config: configTemplate } : {}),
|
||||
...(configTemplate ? { jsonConfig: configTemplate } : {}),
|
||||
...(isSocialConnector && !isStandard && target ? { target } : {}),
|
||||
syncProfile: SyncProfileMode.OnlyAtRegister,
|
||||
});
|
||||
}, [formItems, reset, configTemplate, target, isSocialConnector, isStandard]);
|
||||
/**
|
||||
* 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();
|
||||
|
||||
|
|
|
@ -14,10 +14,11 @@ export enum SyncProfileMode {
|
|||
}
|
||||
|
||||
export type ConnectorFormType = {
|
||||
config: string;
|
||||
jsonConfig: string; // Support editing configs by the code editor
|
||||
formConfig: Record<string, unknown>; // Support custom connector config form
|
||||
name: string;
|
||||
logo: string;
|
||||
logoDark: string;
|
||||
target: string;
|
||||
syncProfile: SyncProfileMode;
|
||||
} & Record<string, unknown>; // Extend custom connector config form
|
||||
};
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import type { ConnectorConfigFormItem } from '@logto/connector-kit';
|
||||
import { ConnectorConfigFormItemType } from '@logto/connector-kit';
|
||||
|
||||
import type { ConnectorFormType } from '@/types/connector';
|
||||
import { safeParseJson } from '@/utils/json';
|
||||
|
||||
export const initFormData = (
|
||||
|
@ -21,9 +20,12 @@ export const initFormData = (
|
|||
return Object.fromEntries(data);
|
||||
};
|
||||
|
||||
export const parseFormConfig = (data: ConnectorFormType, formItems: ConnectorConfigFormItem[]) => {
|
||||
export const parseFormConfig = (
|
||||
config: Record<string, unknown>,
|
||||
formItems: ConnectorConfigFormItem[]
|
||||
) => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(data)
|
||||
Object.entries(config)
|
||||
.map(([key, value]) => {
|
||||
// Filter out empty input
|
||||
if (value === '') {
|
||||
|
|
Loading…
Reference in a new issue