0
Fork 0
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:
Xiao Yijun 2023-07-03 11:55:50 +08:00 committed by GitHub
parent 166ccf93ae
commit 3a8f016d6a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 63 additions and 25 deletions

View file

@ -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.

View file

@ -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}

View file

@ -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);
}; };
}; };

View file

@ -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 && (

View file

@ -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();

View file

@ -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 };

View file

@ -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 === '') {