mirror of
https://github.com/logto-io/logto.git
synced 2024-12-30 20:33:54 -05:00
feat(console): connector config form (#3074)
This commit is contained in:
parent
24f2cd20e7
commit
111e2973c2
13 changed files with 353 additions and 61 deletions
|
@ -19,6 +19,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@fontsource/roboto-mono": "^4.5.7",
|
"@fontsource/roboto-mono": "^4.5.7",
|
||||||
|
"@logto/connector-kit": "workspace:*",
|
||||||
"@logto/core-kit": "workspace:*",
|
"@logto/core-kit": "workspace:*",
|
||||||
"@logto/language-kit": "workspace:*",
|
"@logto/language-kit": "workspace:*",
|
||||||
"@logto/phrases": "workspace:*",
|
"@logto/phrases": "workspace:*",
|
||||||
|
|
|
@ -11,6 +11,14 @@
|
||||||
outline-color: var(--color-focused-variant);
|
outline-color: var(--color-focused-variant);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
border-color: var(--color-error);
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
outline-color: var(--color-danger-focused);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -23,7 +31,7 @@
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
color: var(--color-caption);
|
color: var(--color-placeholder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,11 +6,15 @@ import * as styles from './index.module.scss';
|
||||||
|
|
||||||
type Props = HTMLProps<HTMLTextAreaElement> & {
|
type Props = HTMLProps<HTMLTextAreaElement> & {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
hasError?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Textarea = ({ className, ...rest }: Props, reference: ForwardedRef<HTMLTextAreaElement>) => {
|
const Textarea = (
|
||||||
|
{ className, hasError, ...rest }: Props,
|
||||||
|
reference: ForwardedRef<HTMLTextAreaElement>
|
||||||
|
) => {
|
||||||
return (
|
return (
|
||||||
<div className={classNames(styles.container, className)}>
|
<div className={classNames(styles.container, hasError && styles.error, className)}>
|
||||||
<textarea {...rest} ref={reference} />
|
<textarea {...rest} ref={reference} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -11,9 +11,10 @@ import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
|
||||||
import useApi from '@/hooks/use-api';
|
import useApi from '@/hooks/use-api';
|
||||||
import useDocumentationUrl from '@/hooks/use-documentation-url';
|
import useDocumentationUrl from '@/hooks/use-documentation-url';
|
||||||
import ConnectorForm from '@/pages/Connectors/components/ConnectorForm';
|
import ConnectorForm from '@/pages/Connectors/components/ConnectorForm';
|
||||||
|
import { useConfigParser } from '@/pages/Connectors/components/ConnectorForm/hooks';
|
||||||
|
import { initFormData, parseFormConfig } from '@/pages/Connectors/components/ConnectorForm/utils';
|
||||||
import type { ConnectorFormType } from '@/pages/Connectors/types';
|
import type { ConnectorFormType } from '@/pages/Connectors/types';
|
||||||
import { SyncProfileMode } from '@/pages/Connectors/types';
|
import { SyncProfileMode } from '@/pages/Connectors/types';
|
||||||
import { safeParseJson } from '@/utilities/json';
|
|
||||||
|
|
||||||
import * as styles from '../index.module.scss';
|
import * as styles from '../index.module.scss';
|
||||||
import SenderTester from './SenderTester';
|
import SenderTester from './SenderTester';
|
||||||
|
@ -27,6 +28,7 @@ type Props = {
|
||||||
const ConnectorContent = ({ isDeleted, connectorData, onConnectorUpdated }: Props) => {
|
const 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 parseJsonConfig = useConfigParser();
|
||||||
const api = useApi();
|
const api = useApi();
|
||||||
const methods = useForm<ConnectorFormType>({
|
const methods = useForm<ConnectorFormType>({
|
||||||
reValidateMode: 'onBlur',
|
reValidateMode: 'onBlur',
|
||||||
|
@ -42,9 +44,11 @@ const ConnectorContent = ({ isDeleted, connectorData, onConnectorUpdated }: Prop
|
||||||
} = methods;
|
} = methods;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { name, logo, logoDark, target } = connectorData.metadata;
|
const { formItems, metadata, config, syncProfile } = connectorData;
|
||||||
const { config, syncProfile } = connectorData;
|
const { name, logo, logoDark, target } = metadata;
|
||||||
|
|
||||||
reset({
|
reset({
|
||||||
|
...(formItems ? initFormData(formItems, config) : {}),
|
||||||
target,
|
target,
|
||||||
logo,
|
logo,
|
||||||
logoDark: logoDark ?? '',
|
logoDark: logoDark ?? '',
|
||||||
|
@ -54,36 +58,26 @@ const ConnectorContent = ({ isDeleted, connectorData, onConnectorUpdated }: Prop
|
||||||
});
|
});
|
||||||
}, [connectorData, reset]);
|
}, [connectorData, reset]);
|
||||||
|
|
||||||
const onSubmit = handleSubmit(async ({ config, syncProfile, ...metadata }) => {
|
const onSubmit = handleSubmit(async (data) => {
|
||||||
if (!config) {
|
const { formItems, isStandard, id } = connectorData;
|
||||||
toast.error(t('connector_details.save_error_empty_config'));
|
const config = formItems ? parseFormConfig(data, formItems) : parseJsonConfig(data.config);
|
||||||
|
const { syncProfile, name, logo, logoDark, target } = data;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = safeParseJson(config);
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
toast.error(result.error);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload =
|
const payload =
|
||||||
connectorData.type === ConnectorType.Social
|
connectorData.type === ConnectorType.Social
|
||||||
? {
|
? {
|
||||||
config: result.data,
|
config,
|
||||||
syncProfile: syncProfile === SyncProfileMode.EachSignIn,
|
syncProfile: syncProfile === SyncProfileMode.EachSignIn,
|
||||||
}
|
}
|
||||||
: { config: result.data };
|
: { config };
|
||||||
const standardConnectorPayload = {
|
const standardConnectorPayload = {
|
||||||
...payload,
|
...payload,
|
||||||
metadata: { ...metadata, name: { en: metadata.name } },
|
metadata: { name: { en: name }, logo, logoDark, target },
|
||||||
};
|
};
|
||||||
const body = connectorData.isStandard ? standardConnectorPayload : payload;
|
const body = isStandard ? standardConnectorPayload : payload;
|
||||||
|
|
||||||
const updatedConnector = await api
|
const updatedConnector = await api
|
||||||
.patch(`/api/connectors/${connectorData.id}`, {
|
.patch(`/api/connectors/${id}`, {
|
||||||
json: body,
|
json: body,
|
||||||
})
|
})
|
||||||
.json<ConnectorResponse>();
|
.json<ConnectorResponse>();
|
||||||
|
@ -109,6 +103,7 @@ const ConnectorContent = ({ isDeleted, connectorData, onConnectorUpdated }: Prop
|
||||||
connectorType={connectorData.type}
|
connectorType={connectorData.type}
|
||||||
isStandard={connectorData.isStandard}
|
isStandard={connectorData.isStandard}
|
||||||
isDarkDefaultVisible={Boolean(connectorData.metadata.logoDark)}
|
isDarkDefaultVisible={Boolean(connectorData.metadata.logoDark)}
|
||||||
|
formItems={connectorData.formItems}
|
||||||
/>
|
/>
|
||||||
{connectorData.type !== ConnectorType.Social && (
|
{connectorData.type !== ConnectorType.Social && (
|
||||||
<SenderTester
|
<SenderTester
|
||||||
|
|
|
@ -0,0 +1,151 @@
|
||||||
|
import type { ConnectorConfigFormItem } from '@logto/connector-kit';
|
||||||
|
import { ConnectorConfigFormItemType } from '@logto/connector-kit';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import CodeEditor from '@/components/CodeEditor';
|
||||||
|
import DangerousRaw from '@/components/DangerousRaw';
|
||||||
|
import FormField from '@/components/FormField';
|
||||||
|
import Select from '@/components/Select';
|
||||||
|
import Switch from '@/components/Switch';
|
||||||
|
import TextInput from '@/components/TextInput';
|
||||||
|
import Textarea from '@/components/Textarea';
|
||||||
|
import { jsonValidator } from '@/utilities/validator';
|
||||||
|
|
||||||
|
import type { ConnectorFormType } from '../../types';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
formItems: ConnectorConfigFormItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const ConfigForm = ({ formItems }: Props) => {
|
||||||
|
const {
|
||||||
|
watch,
|
||||||
|
register,
|
||||||
|
control,
|
||||||
|
formState: { errors },
|
||||||
|
} = useFormContext<ConnectorFormType>();
|
||||||
|
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||||
|
|
||||||
|
const values = watch();
|
||||||
|
|
||||||
|
const filteredFormItems = useMemo(() => {
|
||||||
|
return formItems.filter((item) => {
|
||||||
|
if (!item.showConditions) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return item.showConditions.every(({ expectValue, targetKey }) => {
|
||||||
|
const targetValue = values[targetKey];
|
||||||
|
|
||||||
|
return targetValue === expectValue;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [formItems, values]);
|
||||||
|
|
||||||
|
const renderFormItem = (item: ConnectorConfigFormItem) => {
|
||||||
|
const hasError = Boolean(errors[item.key]);
|
||||||
|
const errorMessage = errors[item.key]?.message;
|
||||||
|
|
||||||
|
const commonProperties = {
|
||||||
|
...register(item.key, { required: item.required }),
|
||||||
|
placeholder: item.placeholder,
|
||||||
|
hasError,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (item.type === ConnectorConfigFormItemType.Text) {
|
||||||
|
return <TextInput {...commonProperties} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.type === ConnectorConfigFormItemType.MultilineText) {
|
||||||
|
return <Textarea rows={5} {...commonProperties} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.type === ConnectorConfigFormItemType.Number) {
|
||||||
|
return <TextInput type="number" {...commonProperties} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
name={item.key}
|
||||||
|
control={control}
|
||||||
|
rules={
|
||||||
|
item.type === ConnectorConfigFormItemType.Json
|
||||||
|
? {
|
||||||
|
validate: (value) =>
|
||||||
|
(typeof value === 'string' && jsonValidator(value)) ||
|
||||||
|
t('errors.invalid_json_format'),
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
render={({ field: { onChange, value } }) => {
|
||||||
|
if (item.type === ConnectorConfigFormItemType.Switch) {
|
||||||
|
return (
|
||||||
|
<Switch
|
||||||
|
label={item.label}
|
||||||
|
checked={typeof value === 'boolean' ? value : false}
|
||||||
|
onChange={({ currentTarget: { checked } }) => {
|
||||||
|
onChange(checked);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.type === ConnectorConfigFormItemType.Select) {
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
options={item.selectItems}
|
||||||
|
value={typeof value === 'string' ? value : undefined}
|
||||||
|
hasError={hasError}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.type === ConnectorConfigFormItemType.Json) {
|
||||||
|
return (
|
||||||
|
<CodeEditor
|
||||||
|
language="json"
|
||||||
|
hasError={hasError}
|
||||||
|
errorMessage={errorMessage}
|
||||||
|
value={typeof value === 'string' ? value : '{}'}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default (unknown) type is "Text"
|
||||||
|
// This will happen when connector's version is ahead of AC
|
||||||
|
return (
|
||||||
|
<TextInput
|
||||||
|
hasError={hasError}
|
||||||
|
value={typeof value === 'string' ? value : ''}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{filteredFormItems.map((item) => (
|
||||||
|
<FormField
|
||||||
|
key={item.key}
|
||||||
|
isRequired={item.required}
|
||||||
|
title={
|
||||||
|
<DangerousRaw>
|
||||||
|
{item.type !== ConnectorConfigFormItemType.Switch && item.label}
|
||||||
|
</DangerousRaw>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{renderFormItem(item)}
|
||||||
|
</FormField>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConfigForm;
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { toast } from 'react-hot-toast';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { safeParseJson } from '@/utilities/json';
|
||||||
|
|
||||||
|
export const useConfigParser = () => {
|
||||||
|
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||||
|
|
||||||
|
return (config: string) => {
|
||||||
|
if (!config) {
|
||||||
|
toast.error(t('connector_details.save_error_empty_config'));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = safeParseJson(config);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
toast.error(result.error);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
};
|
|
@ -1,3 +1,4 @@
|
||||||
|
import type { ConnectorConfigFormItem } from '@logto/connector-kit';
|
||||||
import type { ConnectorFactoryResponse } from '@logto/schemas';
|
import type { ConnectorFactoryResponse } from '@logto/schemas';
|
||||||
import { ConnectorType } from '@logto/schemas';
|
import { ConnectorType } from '@logto/schemas';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
@ -17,6 +18,7 @@ import { uriValidator, jsonValidator } from '@/utilities/validator';
|
||||||
|
|
||||||
import type { ConnectorFormType } from '../../types';
|
import type { ConnectorFormType } from '../../types';
|
||||||
import { SyncProfileMode } from '../../types';
|
import { SyncProfileMode } from '../../types';
|
||||||
|
import ConfigForm from '../ConfigForm';
|
||||||
import * as styles from './index.module.scss';
|
import * as styles from './index.module.scss';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -25,6 +27,7 @@ type Props = {
|
||||||
configTemplate?: ConnectorFactoryResponse['configTemplate'];
|
configTemplate?: ConnectorFactoryResponse['configTemplate'];
|
||||||
isAllowEditTarget?: boolean;
|
isAllowEditTarget?: boolean;
|
||||||
isDarkDefaultVisible?: boolean;
|
isDarkDefaultVisible?: boolean;
|
||||||
|
formItems?: ConnectorConfigFormItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const ConnectorForm = ({
|
const ConnectorForm = ({
|
||||||
|
@ -33,6 +36,7 @@ const ConnectorForm = ({
|
||||||
isAllowEditTarget,
|
isAllowEditTarget,
|
||||||
isDarkDefaultVisible,
|
isDarkDefaultVisible,
|
||||||
connectorType,
|
connectorType,
|
||||||
|
formItems,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||||
const { getDocumentationUrl } = useDocumentationUrl();
|
const { getDocumentationUrl } = useDocumentationUrl();
|
||||||
|
@ -132,25 +136,29 @@ const ConnectorForm = ({
|
||||||
</FormField>
|
</FormField>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<FormField title="connectors.guide.config">
|
{formItems ? (
|
||||||
<Controller
|
<ConfigForm formItems={formItems} />
|
||||||
name="config"
|
) : (
|
||||||
control={control}
|
<FormField title="connectors.guide.config">
|
||||||
defaultValue={configTemplate}
|
<Controller
|
||||||
rules={{
|
name="config"
|
||||||
validate: (value) => jsonValidator(value) || t('errors.invalid_json_format'),
|
control={control}
|
||||||
}}
|
defaultValue={configTemplate}
|
||||||
render={({ field: { onChange, value } }) => (
|
rules={{
|
||||||
<CodeEditor
|
validate: (value) => jsonValidator(value) || t('errors.invalid_json_format'),
|
||||||
hasError={Boolean(errors.config)}
|
}}
|
||||||
errorMessage={errors.config?.message}
|
render={({ field: { onChange, value } }) => (
|
||||||
language="json"
|
<CodeEditor
|
||||||
value={value}
|
hasError={Boolean(errors.config)}
|
||||||
onChange={onChange}
|
errorMessage={errors.config?.message}
|
||||||
/>
|
language="json"
|
||||||
)}
|
value={value}
|
||||||
/>
|
onChange={onChange}
|
||||||
</FormField>
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
)}
|
||||||
{connectorType === ConnectorType.Social && (
|
{connectorType === ConnectorType.Social && (
|
||||||
<FormField title="connectors.guide.sync_profile">
|
<FormField title="connectors.guide.sync_profile">
|
||||||
<Controller
|
<Controller
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
import type { ConnectorConfigFormItem } from '@logto/connector-kit';
|
||||||
|
import { ConnectorConfigFormItemType } from '@logto/connector-kit';
|
||||||
|
|
||||||
|
import { safeParseJson } from '@/utilities/json';
|
||||||
|
|
||||||
|
import type { ConnectorFormType } from '../../types';
|
||||||
|
|
||||||
|
export const initFormData = (
|
||||||
|
formItems: ConnectorConfigFormItem[],
|
||||||
|
config?: Record<string, unknown>
|
||||||
|
) => {
|
||||||
|
const data: Array<[string, unknown]> = formItems.map((item) => {
|
||||||
|
const value = config?.[item.key] ?? item.defaultValue;
|
||||||
|
|
||||||
|
if (item.type === ConnectorConfigFormItemType.Json) {
|
||||||
|
return [item.key, JSON.stringify(value, null, 2)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [item.key, value];
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.fromEntries(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseFormConfig = (data: ConnectorFormType, formItems: ConnectorConfigFormItem[]) => {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(data)
|
||||||
|
.map(([key, value]) => {
|
||||||
|
// Filter out empty input
|
||||||
|
if (value === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formItem = formItems.find((item) => item.key === key);
|
||||||
|
|
||||||
|
if (!formItem) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formItem.type === ConnectorConfigFormItemType.Number) {
|
||||||
|
// The number input my return string value.
|
||||||
|
return [key, Number(value)];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formItem.type === ConnectorConfigFormItemType.Json) {
|
||||||
|
// The JSON validation is done in the form
|
||||||
|
const result = safeParseJson(typeof value === 'string' ? value : '');
|
||||||
|
|
||||||
|
return [key, result.success ? result.data : {}];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [key, value];
|
||||||
|
})
|
||||||
|
.filter((item): item is [string, unknown] => Array.isArray(item))
|
||||||
|
);
|
||||||
|
};
|
|
@ -18,11 +18,12 @@ import { ConnectorsTabs } from '@/consts/page-tabs';
|
||||||
import useApi from '@/hooks/use-api';
|
import useApi from '@/hooks/use-api';
|
||||||
import useConfigs from '@/hooks/use-configs';
|
import useConfigs from '@/hooks/use-configs';
|
||||||
import SenderTester from '@/pages/ConnectorDetails/components/SenderTester';
|
import SenderTester from '@/pages/ConnectorDetails/components/SenderTester';
|
||||||
import { safeParseJson } from '@/utilities/json';
|
|
||||||
|
|
||||||
import type { ConnectorFormType } from '../../types';
|
import type { ConnectorFormType } from '../../types';
|
||||||
import { SyncProfileMode } from '../../types';
|
import { SyncProfileMode } from '../../types';
|
||||||
import ConnectorForm from '../ConnectorForm';
|
import ConnectorForm from '../ConnectorForm';
|
||||||
|
import { useConfigParser } from '../ConnectorForm/hooks';
|
||||||
|
import { initFormData, parseFormConfig } from '../ConnectorForm/utils';
|
||||||
import * as styles from './index.module.scss';
|
import * as styles from './index.module.scss';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -34,8 +35,9 @@ const Guide = ({ connector, onClose }: Props) => {
|
||||||
const api = useApi();
|
const api = useApi();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { updateConfigs } = useConfigs();
|
const { updateConfigs } = useConfigs();
|
||||||
|
const parseJsonConfig = useConfigParser();
|
||||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||||
const { id: connectorId, type: connectorType, name, readme, isStandard } = connector;
|
const { id: connectorId, type: connectorType, name, readme, isStandard, formItems } = connector;
|
||||||
const { language } = i18next;
|
const { language } = i18next;
|
||||||
const connectorName = conditional(isLanguageTag(language) && name[language]) ?? name.en;
|
const connectorName = conditional(isLanguageTag(language) && name[language]) ?? name.en;
|
||||||
const isSocialConnector =
|
const isSocialConnector =
|
||||||
|
@ -43,6 +45,7 @@ const Guide = ({ connector, onClose }: Props) => {
|
||||||
const methods = useForm<ConnectorFormType>({
|
const methods = useForm<ConnectorFormType>({
|
||||||
reValidateMode: 'onBlur',
|
reValidateMode: 'onBlur',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
|
...(formItems ? initFormData(formItems) : {}),
|
||||||
syncProfile: SyncProfileMode.OnlyAtRegister,
|
syncProfile: SyncProfileMode.OnlyAtRegister,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -57,23 +60,18 @@ const Guide = ({ connector, onClose }: Props) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { config, name, syncProfile, ...otherData } = data;
|
const { formItems, isStandard, id: connectorId } = connector;
|
||||||
const result = safeParseJson(config);
|
const config = formItems ? parseFormConfig(data, formItems) : parseJsonConfig(data.config);
|
||||||
|
const { syncProfile, name, logo, logoDark, target } = data;
|
||||||
if (!result.success) {
|
|
||||||
toast.error(result.error);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id: connectorId } = connector;
|
|
||||||
|
|
||||||
const basePayload = {
|
const basePayload = {
|
||||||
config: result.data,
|
config,
|
||||||
connectorId,
|
connectorId,
|
||||||
metadata: conditional(
|
metadata: conditional(
|
||||||
isStandard && {
|
isStandard && {
|
||||||
...otherData,
|
logo,
|
||||||
|
logoDark,
|
||||||
|
target,
|
||||||
name: { en: name },
|
name: { en: name },
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
@ -127,6 +125,7 @@ const Guide = ({ connector, onClose }: Props) => {
|
||||||
connectorType={connector.type}
|
connectorType={connector.type}
|
||||||
configTemplate={connector.configTemplate}
|
configTemplate={connector.configTemplate}
|
||||||
isStandard={connector.isStandard}
|
isStandard={connector.isStandard}
|
||||||
|
formItems={connector.formItems}
|
||||||
/>
|
/>
|
||||||
{!isSocialConnector && (
|
{!isSocialConnector && (
|
||||||
<SenderTester
|
<SenderTester
|
||||||
|
|
|
@ -5,7 +5,7 @@ export type ConnectorFormType = {
|
||||||
logoDark: string;
|
logoDark: string;
|
||||||
target: string;
|
target: string;
|
||||||
syncProfile: SyncProfileMode;
|
syncProfile: SyncProfileMode;
|
||||||
};
|
} & Record<string, unknown>; // Extend custom connector config form
|
||||||
|
|
||||||
export enum SyncProfileMode {
|
export enum SyncProfileMode {
|
||||||
OnlyAtRegister = 'OnlyAtRegister',
|
OnlyAtRegister = 'OnlyAtRegister',
|
||||||
|
|
|
@ -59,7 +59,8 @@ export const parseMetadata = async (
|
||||||
logo: await readUrl(metadata.logo, packagePath, 'svg'),
|
logo: await readUrl(metadata.logo, packagePath, 'svg'),
|
||||||
logoDark: metadata.logoDark && (await readUrl(metadata.logoDark, packagePath, 'svg')),
|
logoDark: metadata.logoDark && (await readUrl(metadata.logoDark, packagePath, 'svg')),
|
||||||
readme: await readUrl(metadata.readme, packagePath, 'text'),
|
readme: await readUrl(metadata.readme, packagePath, 'text'),
|
||||||
configTemplate: await readUrl(metadata.configTemplate, packagePath, 'text'),
|
configTemplate:
|
||||||
|
metadata.configTemplate && (await readUrl(metadata.configTemplate, packagePath, 'text')),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -93,6 +93,46 @@ export enum MessageType {
|
||||||
/** @deprecated Use `verificationCodeTypeGuard` instead. */
|
/** @deprecated Use `verificationCodeTypeGuard` instead. */
|
||||||
export const messageTypesGuard = verificationCodeTypeGuard;
|
export const messageTypesGuard = verificationCodeTypeGuard;
|
||||||
|
|
||||||
|
export enum ConnectorConfigFormItemType {
|
||||||
|
Text = 'Text',
|
||||||
|
Number = 'Number',
|
||||||
|
MultilineText = 'MultilineText',
|
||||||
|
Switch = 'Switch',
|
||||||
|
Select = 'Select',
|
||||||
|
Json = 'Json',
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseConfigFormItem = {
|
||||||
|
key: z.string(),
|
||||||
|
label: z.string(),
|
||||||
|
placeholder: z.string().optional(),
|
||||||
|
required: z.boolean().optional(),
|
||||||
|
defaultValue: z.unknown().optional(),
|
||||||
|
showConditions: z
|
||||||
|
.array(z.object({ targetKey: z.string(), expectValue: z.unknown().optional() }))
|
||||||
|
.optional(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const connectorConfigFormItemGuard = z.discriminatedUnion('type', [
|
||||||
|
z.object({
|
||||||
|
type: z.literal(ConnectorConfigFormItemType.Select),
|
||||||
|
selectItems: z.array(z.object({ value: z.string(), title: z.string() })),
|
||||||
|
...baseConfigFormItem,
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
type: z.enum([
|
||||||
|
ConnectorConfigFormItemType.Text,
|
||||||
|
ConnectorConfigFormItemType.Number,
|
||||||
|
ConnectorConfigFormItemType.MultilineText,
|
||||||
|
ConnectorConfigFormItemType.Switch,
|
||||||
|
ConnectorConfigFormItemType.Json,
|
||||||
|
]),
|
||||||
|
...baseConfigFormItem,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export type ConnectorConfigFormItem = z.infer<typeof connectorConfigFormItemGuard>;
|
||||||
|
|
||||||
const connectorMetadataGuard = z.object({
|
const connectorMetadataGuard = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
target: z.string(),
|
target: z.string(),
|
||||||
|
@ -103,7 +143,8 @@ const connectorMetadataGuard = z.object({
|
||||||
description: i18nPhrasesGuard,
|
description: i18nPhrasesGuard,
|
||||||
isStandard: z.boolean().optional(),
|
isStandard: z.boolean().optional(),
|
||||||
readme: z.string(),
|
readme: z.string(),
|
||||||
configTemplate: z.string(),
|
configTemplate: z.string().optional(),
|
||||||
|
formItems: connectorConfigFormItemGuard.array().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const configurableConnectorMetadataGuard = connectorMetadataGuard
|
export const configurableConnectorMetadataGuard = connectorMetadataGuard
|
||||||
|
|
|
@ -107,6 +107,7 @@ importers:
|
||||||
packages/console:
|
packages/console:
|
||||||
specifiers:
|
specifiers:
|
||||||
'@fontsource/roboto-mono': ^4.5.7
|
'@fontsource/roboto-mono': ^4.5.7
|
||||||
|
'@logto/connector-kit': workspace:*
|
||||||
'@logto/core-kit': workspace:*
|
'@logto/core-kit': workspace:*
|
||||||
'@logto/language-kit': workspace:*
|
'@logto/language-kit': workspace:*
|
||||||
'@logto/phrases': workspace:*
|
'@logto/phrases': workspace:*
|
||||||
|
@ -178,6 +179,7 @@ importers:
|
||||||
zod: ^3.20.2
|
zod: ^3.20.2
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@fontsource/roboto-mono': 4.5.7
|
'@fontsource/roboto-mono': 4.5.7
|
||||||
|
'@logto/connector-kit': link:../toolkit/connector-kit
|
||||||
'@logto/core-kit': link:../toolkit/core-kit
|
'@logto/core-kit': link:../toolkit/core-kit
|
||||||
'@logto/language-kit': link:../toolkit/language-kit
|
'@logto/language-kit': link:../toolkit/language-kit
|
||||||
'@logto/phrases': link:../phrases
|
'@logto/phrases': link:../phrases
|
||||||
|
|
Loading…
Reference in a new issue