0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

feat(console): implement email service connector details page (#4089)

This commit is contained in:
Xiao Yijun 2023-07-03 16:35:36 +08:00 committed by GitHub
parent 909591f1c5
commit bbf0551273
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 178 additions and 24 deletions

View file

@ -80,6 +80,21 @@ export const defaultMetadata: ConnectorMetadata = {
type: ConnectorConfigFormItemType.Text, type: ConnectorConfigFormItemType.Text,
required: true, required: true,
}, },
{
key: 'fromName',
label: 'From Name',
type: ConnectorConfigFormItemType.Text,
},
{
key: 'companyAddress',
label: 'Company Address',
type: ConnectorConfigFormItemType.Text,
},
{
key: 'appLogo',
label: 'App Logo',
type: ConnectorConfigFormItemType.Text,
},
], ],
}; };

View file

@ -1,3 +1,4 @@
import { ServiceConnector } from '@logto/connector-kit';
import { emailRegEx, phoneInputRegEx } from '@logto/core-kit'; import { emailRegEx, phoneInputRegEx } from '@logto/core-kit';
import { ConnectorType } from '@logto/schemas'; import { ConnectorType } from '@logto/schemas';
import { conditional } from '@silverhand/essentials'; import { conditional } from '@silverhand/essentials';
@ -40,6 +41,7 @@ function ConnectorTester({ connectorFactoryId, connectorType, className, parse }
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const api = useApi(); const api = useApi();
const isSms = connectorType === ConnectorType.Sms; const isSms = connectorType === ConnectorType.Sms;
const isEmailServiceConnector = connectorFactoryId === ServiceConnector.Email;
useEffect(() => { useEffect(() => {
if (!showTooltip) { if (!showTooltip) {
@ -110,7 +112,13 @@ function ConnectorTester({ connectorFactoryId, connectorType, className, parse }
/> />
</Tooltip> </Tooltip>
</div> </div>
<div className={styles.description}>{t('connector_details.test_sender_description')}</div> <div className={styles.description}>
{t(
isEmailServiceConnector
? 'connector_details.logto_email.test_notes'
: 'connector_details.test_sender_description'
)}
</div>
<div className={styles.error}>{inputError?.message}</div> <div className={styles.error}>{inputError?.message}</div>
</div> </div>
); );

View file

@ -0,0 +1,5 @@
@use '@/scss/underscore' as _;
.imageFieldHeadline {
margin-bottom: _.unit(2);
}

View file

@ -0,0 +1,116 @@
import { urlRegEx } from '@logto/connector-kit';
import { conditional, conditionalString } from '@silverhand/essentials';
import { Controller, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import FormCard from '@/components/FormCard';
import DynamicT from '@/ds-components/DynamicT';
import FormField from '@/ds-components/FormField';
import TextInput from '@/ds-components/TextInput';
import ImageUploaderField from '@/ds-components/Uploader/ImageUploaderField';
import useUserAssetsService from '@/hooks/use-user-assets-service';
import { type ConnectorFormType } from '@/types/connector';
import { uriValidator } from '@/utils/validator';
import * as styles from './index.module.scss';
type Props = {
extraInfo?: Record<string, unknown>;
};
const extraInfoGuard = z.object({
fromEmail: z.string(),
});
/**
* Note:
* This `EmailServiceConnectorForm` is hard-coded since the custom connector config form does not support i18n and we need i18n for our built-in connectors.
*
* TODO: @xiaoyijun @darcyYe remove this hard-coded form once the custom connector config form supports i18n.
*/
function EmailServiceConnectorForm({ extraInfo }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { isReady: isUserAssetsServiceReady } = useUserAssetsService();
const parsedExtraInfo = extraInfoGuard.safeParse(extraInfo ?? {});
const {
control,
register,
formState: {
errors: { formConfig: fromConfigErrors },
},
} = useFormContext<ConnectorFormType>();
const validateInput = (value: string) => {
const containUrl = urlRegEx.test(value);
return containUrl ? t('connector_details.logto_email.urls_not_allowed') : true;
};
return (
<FormCard
title="connector_details.logto_email.email_template_title"
description="connector_details.logto_email.template_description"
>
<FormField title="connector_details.logto_email.from_email_field">
<TextInput
readOnly
value={conditionalString(parsedExtraInfo.success && parsedExtraInfo.data.fromEmail)}
/>
</FormField>
<FormField
title="connector_details.logto_email.from_name_field"
tip={<DynamicT forKey="connector_details.logto_email.from_name_tip" />}
>
<TextInput
{...register('formConfig.fromName', {
validate: (value) => validateInput(conditionalString(value)),
})}
error={fromConfigErrors?.fromName?.message}
placeholder={t('connector_details.logto_email.from_name_placeholder')}
/>
</FormField>
<FormField
title="connector_details.logto_email.company_address_field"
tip={<DynamicT forKey="connector_details.logto_email.company_address_tip" />}
>
<TextInput
{...register('formConfig.companyAddress', {
validate: (value) => validateInput(conditionalString(value)),
})}
error={fromConfigErrors?.companyAddress?.message}
placeholder={t('connector_details.logto_email.company_address_placeholder')}
/>
</FormField>
<FormField
title="connector_details.logto_email.app_logo_field"
tip={<DynamicT forKey="connector_details.logto_email.app_logo_tip" />}
headlineClassName={conditional(isUserAssetsServiceReady && styles.imageFieldHeadline)}
>
{isUserAssetsServiceReady ? (
<Controller
name="formConfig.appLogo"
control={control}
render={({ field: { onChange, value, name } }) => (
<ImageUploaderField
name={name}
value={conditionalString(value)}
onChange={onChange}
/>
)}
/>
) : (
<TextInput
{...register('formConfig.appLogo', {
validate: (value) =>
!value || uriValidator(conditionalString(value)) || t('errors.invalid_uri_format'),
})}
error={fromConfigErrors?.appLogo?.message}
/>
)}
</FormField>
</FormCard>
);
}
export default EmailServiceConnectorForm;

View file

@ -1,3 +1,4 @@
import { ServiceConnector } from '@logto/connector-kit';
import { ConnectorType } from '@logto/schemas'; 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';
@ -21,6 +22,8 @@ import type { ConnectorFormType } from '@/types/connector';
import { initFormData } from '@/utils/connector-form'; import { initFormData } from '@/utils/connector-form';
import { trySubmitSafe } from '@/utils/form'; import { trySubmitSafe } from '@/utils/form';
import EmailServiceConnectorForm from './EmailServiceConnectorForm';
type Props = { type Props = {
isDeleted: boolean; isDeleted: boolean;
connectorData: ConnectorResponse; connectorData: ConnectorResponse;
@ -60,8 +63,17 @@ function ConnectorContent({ isDeleted, connectorData, onConnectorUpdated }: Prop
reset, reset,
setValue, setValue,
} = methods; } = methods;
const isSocialConnector = connectorData.type === ConnectorType.Social;
const {
id,
connectorId,
type: connectorType,
formItems,
isStandard: isStandardConnector,
metadata: { logoDark },
} = connectorData;
const isSocialConnector = connectorType === ConnectorType.Social;
const isEmailServiceConnector = connectorId === ServiceConnector.Email;
useEffect(() => { useEffect(() => {
const { metadata, config, syncProfile } = connectorData; const { metadata, config, syncProfile } = connectorData;
const { name, logo, logoDark } = metadata; const { name, logo, logoDark } = metadata;
@ -136,32 +148,30 @@ function ConnectorContent({ isDeleted, connectorData, onConnectorUpdated }: Prop
description="connector_details.settings_description" description="connector_details.settings_description"
learnMoreLink={getDocumentationUrl('/docs/references/connectors')} learnMoreLink={getDocumentationUrl('/docs/references/connectors')}
> >
<BasicForm <BasicForm isStandard={isStandardConnector} isDarkDefaultVisible={Boolean(logoDark)} />
isStandard={connectorData.isStandard}
isDarkDefaultVisible={Boolean(connectorData.metadata.logoDark)}
/>
</FormCard> </FormCard>
)} )}
{isEmailServiceConnector ? (
<EmailServiceConnectorForm extraInfo={connectorData.extraInfo} />
) : (
<FormCard <FormCard
title="connector_details.parameter_configuration" title="connector_details.parameter_configuration"
description={conditional(!isSocialConnector && 'connector_details.settings_description')} description={conditional(
!isSocialConnector && 'connector_details.settings_description'
)}
learnMoreLink={conditional( learnMoreLink={conditional(
!isSocialConnector && getDocumentationUrl('/docs/references/connectors') !isSocialConnector && getDocumentationUrl('/docs/references/connectors')
)} )}
> >
<ConfigForm <ConfigForm formItems={formItems} connectorId={id} connectorType={connectorType} />
formItems={connectorData.formItems}
connectorId={connectorData.id}
connectorType={connectorData.type}
/>
</FormCard> </FormCard>
{/* Tell typescript that the connectorType is Email or Sms */} )}
{connectorData.type !== ConnectorType.Social && ( {!isSocialConnector && (
<FormCard title="connector_details.test_connection"> <FormCard title="connector_details.test_connection">
<ConnectorTester <ConnectorTester
connectorFactoryId={connectorData.connectorId} connectorFactoryId={connectorId}
connectorType={connectorData.type} connectorType={connectorType}
parse={() => configParser(watch(), connectorData.formItems)} parse={() => configParser(watch(), formItems)}
/> />
</FormCard> </FormCard>
)} )}