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": {
|
||||
"@fontsource/roboto-mono": "^4.5.7",
|
||||
"@logto/connector-kit": "workspace:*",
|
||||
"@logto/core-kit": "workspace:*",
|
||||
"@logto/language-kit": "workspace:*",
|
||||
"@logto/phrases": "workspace:*",
|
||||
|
|
|
@ -11,6 +11,14 @@
|
|||
outline-color: var(--color-focused-variant);
|
||||
}
|
||||
|
||||
&.error {
|
||||
border-color: var(--color-error);
|
||||
|
||||
&:focus-within {
|
||||
outline-color: var(--color-danger-focused);
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
@ -23,7 +31,7 @@
|
|||
padding: 0;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--color-caption);
|
||||
color: var(--color-placeholder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,11 +6,15 @@ import * as styles from './index.module.scss';
|
|||
|
||||
type Props = HTMLProps<HTMLTextAreaElement> & {
|
||||
className?: string;
|
||||
hasError?: boolean;
|
||||
};
|
||||
|
||||
const Textarea = ({ className, ...rest }: Props, reference: ForwardedRef<HTMLTextAreaElement>) => {
|
||||
const Textarea = (
|
||||
{ className, hasError, ...rest }: Props,
|
||||
reference: ForwardedRef<HTMLTextAreaElement>
|
||||
) => {
|
||||
return (
|
||||
<div className={classNames(styles.container, className)}>
|
||||
<div className={classNames(styles.container, hasError && styles.error, className)}>
|
||||
<textarea {...rest} ref={reference} />
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -11,9 +11,10 @@ import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
|
|||
import useApi from '@/hooks/use-api';
|
||||
import useDocumentationUrl from '@/hooks/use-documentation-url';
|
||||
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 { SyncProfileMode } from '@/pages/Connectors/types';
|
||||
import { safeParseJson } from '@/utilities/json';
|
||||
|
||||
import * as styles from '../index.module.scss';
|
||||
import SenderTester from './SenderTester';
|
||||
|
@ -27,6 +28,7 @@ type Props = {
|
|||
const ConnectorContent = ({ isDeleted, connectorData, onConnectorUpdated }: Props) => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { getDocumentationUrl } = useDocumentationUrl();
|
||||
const parseJsonConfig = useConfigParser();
|
||||
const api = useApi();
|
||||
const methods = useForm<ConnectorFormType>({
|
||||
reValidateMode: 'onBlur',
|
||||
|
@ -42,9 +44,11 @@ const ConnectorContent = ({ isDeleted, connectorData, onConnectorUpdated }: Prop
|
|||
} = methods;
|
||||
|
||||
useEffect(() => {
|
||||
const { name, logo, logoDark, target } = connectorData.metadata;
|
||||
const { config, syncProfile } = connectorData;
|
||||
const { formItems, metadata, config, syncProfile } = connectorData;
|
||||
const { name, logo, logoDark, target } = metadata;
|
||||
|
||||
reset({
|
||||
...(formItems ? initFormData(formItems, config) : {}),
|
||||
target,
|
||||
logo,
|
||||
logoDark: logoDark ?? '',
|
||||
|
@ -54,36 +58,26 @@ const ConnectorContent = ({ isDeleted, connectorData, onConnectorUpdated }: Prop
|
|||
});
|
||||
}, [connectorData, reset]);
|
||||
|
||||
const onSubmit = handleSubmit(async ({ config, syncProfile, ...metadata }) => {
|
||||
if (!config) {
|
||||
toast.error(t('connector_details.save_error_empty_config'));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const result = safeParseJson(config);
|
||||
|
||||
if (!result.success) {
|
||||
toast.error(result.error);
|
||||
|
||||
return;
|
||||
}
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
const { formItems, isStandard, id } = connectorData;
|
||||
const config = formItems ? parseFormConfig(data, formItems) : parseJsonConfig(data.config);
|
||||
const { syncProfile, name, logo, logoDark, target } = data;
|
||||
|
||||
const payload =
|
||||
connectorData.type === ConnectorType.Social
|
||||
? {
|
||||
config: result.data,
|
||||
config,
|
||||
syncProfile: syncProfile === SyncProfileMode.EachSignIn,
|
||||
}
|
||||
: { config: result.data };
|
||||
: { config };
|
||||
const standardConnectorPayload = {
|
||||
...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
|
||||
.patch(`/api/connectors/${connectorData.id}`, {
|
||||
.patch(`/api/connectors/${id}`, {
|
||||
json: body,
|
||||
})
|
||||
.json<ConnectorResponse>();
|
||||
|
@ -109,6 +103,7 @@ const ConnectorContent = ({ isDeleted, connectorData, onConnectorUpdated }: Prop
|
|||
connectorType={connectorData.type}
|
||||
isStandard={connectorData.isStandard}
|
||||
isDarkDefaultVisible={Boolean(connectorData.metadata.logoDark)}
|
||||
formItems={connectorData.formItems}
|
||||
/>
|
||||
{connectorData.type !== ConnectorType.Social && (
|
||||
<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 { ConnectorType } from '@logto/schemas';
|
||||
import { useState } from 'react';
|
||||
|
@ -17,6 +18,7 @@ import { uriValidator, jsonValidator } from '@/utilities/validator';
|
|||
|
||||
import type { ConnectorFormType } from '../../types';
|
||||
import { SyncProfileMode } from '../../types';
|
||||
import ConfigForm from '../ConfigForm';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
|
@ -25,6 +27,7 @@ type Props = {
|
|||
configTemplate?: ConnectorFactoryResponse['configTemplate'];
|
||||
isAllowEditTarget?: boolean;
|
||||
isDarkDefaultVisible?: boolean;
|
||||
formItems?: ConnectorConfigFormItem[];
|
||||
};
|
||||
|
||||
const ConnectorForm = ({
|
||||
|
@ -33,6 +36,7 @@ const ConnectorForm = ({
|
|||
isAllowEditTarget,
|
||||
isDarkDefaultVisible,
|
||||
connectorType,
|
||||
formItems,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { getDocumentationUrl } = useDocumentationUrl();
|
||||
|
@ -132,25 +136,29 @@ const ConnectorForm = ({
|
|||
</FormField>
|
||||
</>
|
||||
)}
|
||||
<FormField title="connectors.guide.config">
|
||||
<Controller
|
||||
name="config"
|
||||
control={control}
|
||||
defaultValue={configTemplate}
|
||||
rules={{
|
||||
validate: (value) => jsonValidator(value) || t('errors.invalid_json_format'),
|
||||
}}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<CodeEditor
|
||||
hasError={Boolean(errors.config)}
|
||||
errorMessage={errors.config?.message}
|
||||
language="json"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
{formItems ? (
|
||||
<ConfigForm formItems={formItems} />
|
||||
) : (
|
||||
<FormField title="connectors.guide.config">
|
||||
<Controller
|
||||
name="config"
|
||||
control={control}
|
||||
defaultValue={configTemplate}
|
||||
rules={{
|
||||
validate: (value) => jsonValidator(value) || t('errors.invalid_json_format'),
|
||||
}}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<CodeEditor
|
||||
hasError={Boolean(errors.config)}
|
||||
errorMessage={errors.config?.message}
|
||||
language="json"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
)}
|
||||
{connectorType === ConnectorType.Social && (
|
||||
<FormField title="connectors.guide.sync_profile">
|
||||
<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 useConfigs from '@/hooks/use-configs';
|
||||
import SenderTester from '@/pages/ConnectorDetails/components/SenderTester';
|
||||
import { safeParseJson } from '@/utilities/json';
|
||||
|
||||
import type { ConnectorFormType } from '../../types';
|
||||
import { SyncProfileMode } from '../../types';
|
||||
import ConnectorForm from '../ConnectorForm';
|
||||
import { useConfigParser } from '../ConnectorForm/hooks';
|
||||
import { initFormData, parseFormConfig } from '../ConnectorForm/utils';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
|
@ -34,8 +35,9 @@ const Guide = ({ connector, onClose }: Props) => {
|
|||
const api = useApi();
|
||||
const navigate = useNavigate();
|
||||
const { updateConfigs } = useConfigs();
|
||||
const parseJsonConfig = useConfigParser();
|
||||
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 connectorName = conditional(isLanguageTag(language) && name[language]) ?? name.en;
|
||||
const isSocialConnector =
|
||||
|
@ -43,6 +45,7 @@ const Guide = ({ connector, onClose }: Props) => {
|
|||
const methods = useForm<ConnectorFormType>({
|
||||
reValidateMode: 'onBlur',
|
||||
defaultValues: {
|
||||
...(formItems ? initFormData(formItems) : {}),
|
||||
syncProfile: SyncProfileMode.OnlyAtRegister,
|
||||
},
|
||||
});
|
||||
|
@ -57,23 +60,18 @@ const Guide = ({ connector, onClose }: Props) => {
|
|||
return;
|
||||
}
|
||||
|
||||
const { config, name, syncProfile, ...otherData } = data;
|
||||
const result = safeParseJson(config);
|
||||
|
||||
if (!result.success) {
|
||||
toast.error(result.error);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { id: connectorId } = connector;
|
||||
const { formItems, isStandard, id: connectorId } = connector;
|
||||
const config = formItems ? parseFormConfig(data, formItems) : parseJsonConfig(data.config);
|
||||
const { syncProfile, name, logo, logoDark, target } = data;
|
||||
|
||||
const basePayload = {
|
||||
config: result.data,
|
||||
config,
|
||||
connectorId,
|
||||
metadata: conditional(
|
||||
isStandard && {
|
||||
...otherData,
|
||||
logo,
|
||||
logoDark,
|
||||
target,
|
||||
name: { en: name },
|
||||
}
|
||||
),
|
||||
|
@ -127,6 +125,7 @@ const Guide = ({ connector, onClose }: Props) => {
|
|||
connectorType={connector.type}
|
||||
configTemplate={connector.configTemplate}
|
||||
isStandard={connector.isStandard}
|
||||
formItems={connector.formItems}
|
||||
/>
|
||||
{!isSocialConnector && (
|
||||
<SenderTester
|
||||
|
|
|
@ -5,7 +5,7 @@ export type ConnectorFormType = {
|
|||
logoDark: string;
|
||||
target: string;
|
||||
syncProfile: SyncProfileMode;
|
||||
};
|
||||
} & Record<string, unknown>; // Extend custom connector config form
|
||||
|
||||
export enum SyncProfileMode {
|
||||
OnlyAtRegister = 'OnlyAtRegister',
|
||||
|
|
|
@ -59,7 +59,8 @@ export const parseMetadata = async (
|
|||
logo: await readUrl(metadata.logo, packagePath, 'svg'),
|
||||
logoDark: metadata.logoDark && (await readUrl(metadata.logoDark, packagePath, 'svg')),
|
||||
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. */
|
||||
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({
|
||||
id: z.string(),
|
||||
target: z.string(),
|
||||
|
@ -103,7 +143,8 @@ const connectorMetadataGuard = z.object({
|
|||
description: i18nPhrasesGuard,
|
||||
isStandard: z.boolean().optional(),
|
||||
readme: z.string(),
|
||||
configTemplate: z.string(),
|
||||
configTemplate: z.string().optional(),
|
||||
formItems: connectorConfigFormItemGuard.array().optional(),
|
||||
});
|
||||
|
||||
export const configurableConnectorMetadataGuard = connectorMetadataGuard
|
||||
|
|
|
@ -107,6 +107,7 @@ importers:
|
|||
packages/console:
|
||||
specifiers:
|
||||
'@fontsource/roboto-mono': ^4.5.7
|
||||
'@logto/connector-kit': workspace:*
|
||||
'@logto/core-kit': workspace:*
|
||||
'@logto/language-kit': workspace:*
|
||||
'@logto/phrases': workspace:*
|
||||
|
@ -178,6 +179,7 @@ importers:
|
|||
zod: ^3.20.2
|
||||
devDependencies:
|
||||
'@fontsource/roboto-mono': 4.5.7
|
||||
'@logto/connector-kit': link:../toolkit/connector-kit
|
||||
'@logto/core-kit': link:../toolkit/core-kit
|
||||
'@logto/language-kit': link:../toolkit/language-kit
|
||||
'@logto/phrases': link:../phrases
|
||||
|
|
Loading…
Reference in a new issue