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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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