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 type { ConnectorConfigFormItem } from '@logto/connector-kit';
|
||||||
import { ConnectorConfigFormItemType } from '@logto/connector-kit';
|
import { ConnectorConfigFormItemType } from '@logto/connector-kit';
|
||||||
|
import { conditional } from '@silverhand/essentials';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { Controller, useFormContext } from 'react-hook-form';
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
@ -25,11 +26,13 @@ function ConfigFormFields({ formItems }: Props) {
|
||||||
watch,
|
watch,
|
||||||
register,
|
register,
|
||||||
control,
|
control,
|
||||||
formState: { errors },
|
formState: {
|
||||||
|
errors: { formConfig: formConfigErrors },
|
||||||
|
},
|
||||||
} = useFormContext<ConnectorFormType>();
|
} = useFormContext<ConnectorFormType>();
|
||||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||||
|
|
||||||
const values = watch();
|
const values = watch('formConfig');
|
||||||
|
|
||||||
const filteredFormItems = useMemo(() => {
|
const filteredFormItems = useMemo(() => {
|
||||||
return formItems.filter((item) => {
|
return formItems.filter((item) => {
|
||||||
|
@ -46,10 +49,13 @@ function ConfigFormFields({ formItems }: Props) {
|
||||||
}, [formItems, values]);
|
}, [formItems, values]);
|
||||||
|
|
||||||
const renderFormItem = (item: ConnectorConfigFormItem) => {
|
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 = () => ({
|
const buildCommonProperties = () => ({
|
||||||
...register(item.key, {
|
...register(`formConfig.${item.key}`, {
|
||||||
required: item.required,
|
required: item.required,
|
||||||
valueAsNumber: item.type === ConnectorConfigFormItemType.Number,
|
valueAsNumber: item.type === ConnectorConfigFormItemType.Number,
|
||||||
}),
|
}),
|
||||||
|
@ -77,7 +83,7 @@ function ConfigFormFields({ formItems }: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Controller
|
<Controller
|
||||||
name={item.key}
|
name={`formConfig.${item.key}`}
|
||||||
control={control}
|
control={control}
|
||||||
rules={{
|
rules={{
|
||||||
// For switch, "false" will be treated as an empty value, so we need to set required to false.
|
// 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">
|
<FormField title="connectors.guide.config">
|
||||||
<Controller
|
<Controller
|
||||||
name="config"
|
name="jsonConfig"
|
||||||
control={control}
|
control={control}
|
||||||
rules={{
|
rules={{
|
||||||
validate: (value) => jsonValidator(value) || t('errors.invalid_json_format'),
|
validate: (value) => jsonValidator(value) || t('errors.invalid_json_format'),
|
||||||
}}
|
}}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
error={errors.config?.message ?? Boolean(errors.config)}
|
error={errors.jsonConfig?.message ?? Boolean(errors.jsonConfig)}
|
||||||
language="json"
|
language="json"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
|
|
@ -32,6 +32,8 @@ export const useConnectorFormConfigParser = () => {
|
||||||
const parseJsonConfig = useJsonStringConfigParser();
|
const parseJsonConfig = useJsonStringConfigParser();
|
||||||
|
|
||||||
return (data: ConnectorFormType, formItems: ConnectorResponse['formItems']) => {
|
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 { ConnectorResponse } from '@logto/schemas';
|
||||||
import type { Optional } from '@silverhand/essentials';
|
import type { Optional } from '@silverhand/essentials';
|
||||||
import { conditional } 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 { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
@ -39,6 +39,12 @@ function ConnectorContent({ isDeleted, connectorData, onConnectorUpdated }: Prop
|
||||||
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 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: {
|
||||||
|
@ -46,28 +52,35 @@ function ConnectorContent({ isDeleted, connectorData, onConnectorUpdated }: Prop
|
||||||
target: getConnectorTarget(connectorData),
|
target: getConnectorTarget(connectorData),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
formState: { isSubmitting, isDirty },
|
formState: { isSubmitting, isDirty },
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
watch,
|
watch,
|
||||||
reset,
|
reset,
|
||||||
|
setValue,
|
||||||
} = methods;
|
} = methods;
|
||||||
const isSocialConnector = connectorData.type === ConnectorType.Social;
|
const isSocialConnector = connectorData.type === ConnectorType.Social;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { formItems, metadata, config, syncProfile } = connectorData;
|
const { metadata, config, syncProfile } = connectorData;
|
||||||
const { name, logo, logoDark, target } = metadata;
|
const { name, logo, logoDark } = metadata;
|
||||||
|
|
||||||
reset({
|
reset({
|
||||||
...(formItems ? initFormData(formItems, config) : {}),
|
target: getConnectorTarget(connectorData),
|
||||||
target: getConnectorTarget(connectorData) ?? target,
|
|
||||||
logo,
|
logo,
|
||||||
logoDark: logoDark ?? '',
|
logoDark: logoDark ?? '',
|
||||||
name: name?.en,
|
name: name?.en,
|
||||||
config: JSON.stringify(config, null, 2),
|
jsonConfig: JSON.stringify(config, null, 2),
|
||||||
syncProfile: syncProfile ? SyncProfileMode.EachSignIn : SyncProfileMode.OnlyAtRegister,
|
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();
|
const configParser = useConnectorFormConfigParser();
|
||||||
|
|
||||||
|
@ -95,7 +108,6 @@ function ConnectorContent({ isDeleted, connectorData, onConnectorUpdated }: Prop
|
||||||
json: body,
|
json: body,
|
||||||
})
|
})
|
||||||
.json<ConnectorResponse>();
|
.json<ConnectorResponse>();
|
||||||
|
|
||||||
onConnectorUpdated(updatedConnector);
|
onConnectorUpdated(updatedConnector);
|
||||||
toast.success(t('general.saved'));
|
toast.success(t('general.saved'));
|
||||||
})
|
})
|
||||||
|
@ -107,7 +119,15 @@ function ConnectorContent({ isDeleted, connectorData, onConnectorUpdated }: Prop
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
isDirty={isDirty}
|
isDirty={isDirty}
|
||||||
isSubmitting={isSubmitting}
|
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}
|
onSubmit={onSubmit}
|
||||||
>
|
>
|
||||||
{isSocialConnector && (
|
{isSocialConnector && (
|
||||||
|
|
|
@ -64,16 +64,23 @@ function Guide({ connector, onClose }: Props) {
|
||||||
watch,
|
watch,
|
||||||
setError,
|
setError,
|
||||||
reset,
|
reset,
|
||||||
|
setValue,
|
||||||
} = methods;
|
} = methods;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const formConfig = conditional(formItems && initFormData(formItems));
|
||||||
reset({
|
reset({
|
||||||
...(formItems ? initFormData(formItems) : {}),
|
...(configTemplate ? { jsonConfig: configTemplate } : {}),
|
||||||
...(configTemplate ? { config: configTemplate } : {}),
|
|
||||||
...(isSocialConnector && !isStandard && target ? { target } : {}),
|
...(isSocialConnector && !isStandard && target ? { target } : {}),
|
||||||
syncProfile: SyncProfileMode.OnlyAtRegister,
|
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();
|
const configParser = useConnectorFormConfigParser();
|
||||||
|
|
||||||
|
|
|
@ -14,10 +14,11 @@ export enum SyncProfileMode {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ConnectorFormType = {
|
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;
|
name: string;
|
||||||
logo: string;
|
logo: string;
|
||||||
logoDark: string;
|
logoDark: string;
|
||||||
target: string;
|
target: string;
|
||||||
syncProfile: SyncProfileMode;
|
syncProfile: SyncProfileMode;
|
||||||
} & Record<string, unknown>; // Extend custom connector config form
|
};
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import type { ConnectorConfigFormItem } from '@logto/connector-kit';
|
import type { ConnectorConfigFormItem } from '@logto/connector-kit';
|
||||||
import { ConnectorConfigFormItemType } from '@logto/connector-kit';
|
import { ConnectorConfigFormItemType } from '@logto/connector-kit';
|
||||||
|
|
||||||
import type { ConnectorFormType } from '@/types/connector';
|
|
||||||
import { safeParseJson } from '@/utils/json';
|
import { safeParseJson } from '@/utils/json';
|
||||||
|
|
||||||
export const initFormData = (
|
export const initFormData = (
|
||||||
|
@ -21,9 +20,12 @@ export const initFormData = (
|
||||||
return Object.fromEntries(data);
|
return Object.fromEntries(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const parseFormConfig = (data: ConnectorFormType, formItems: ConnectorConfigFormItem[]) => {
|
export const parseFormConfig = (
|
||||||
|
config: Record<string, unknown>,
|
||||||
|
formItems: ConnectorConfigFormItem[]
|
||||||
|
) => {
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
Object.entries(data)
|
Object.entries(config)
|
||||||
.map(([key, value]) => {
|
.map(([key, value]) => {
|
||||||
// Filter out empty input
|
// Filter out empty input
|
||||||
if (value === '') {
|
if (value === '') {
|
||||||
|
|
Loading…
Reference in a new issue