0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-31 22:51:25 -05:00

feat(console,core,phrases): add display name field, fix error display (#4983)

This commit is contained in:
Darcy Ye 2023-11-28 15:49:46 +08:00 committed by GitHub
parent c47e0192ff
commit 14855cde2c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 113 additions and 30 deletions

View file

@ -1,7 +1,9 @@
import {
type SsoConnectorFactoriesResponse,
type SsoConnectorWithProviderConfig,
type RequestErrorBody,
} from '@logto/schemas';
import { HTTPError } from 'ky';
import { useMemo, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
@ -32,6 +34,8 @@ type FormType = {
connectorName: string;
};
const duplicateConnectorNameErrorCode = 'single_sign_on.duplicate_connector_name';
function SsoCreationModal({ isOpen, onClose: rawOnClose }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const [selectedProviderName, setSelectedProviderName] = useState<string>();
@ -43,8 +47,9 @@ function SsoCreationModal({ isOpen, onClose: rawOnClose }: Props) {
register,
handleSubmit,
formState: { isSubmitting, errors },
setError,
} = useForm<FormType>();
const api = useApi();
const api = useApi({ hideErrorToast: true });
const isLoading = !data && !error;
@ -80,11 +85,22 @@ function SsoCreationModal({ isOpen, onClose: rawOnClose }: Props) {
return;
}
const createdSsoConnector = await api
.post(`api/sso-connectors`, { json: { ...formData, providerName: selectedProviderName } })
.json<SsoConnectorWithProviderConfig>();
try {
const createdSsoConnector = await api
.post(`api/sso-connectors`, { json: { ...formData, providerName: selectedProviderName } })
.json<SsoConnectorWithProviderConfig>();
onClose(createdSsoConnector);
onClose(createdSsoConnector);
} catch (error: unknown) {
if (error instanceof HTTPError) {
const { response } = error;
const metadata = await response.clone().json<RequestErrorBody>();
if (metadata.code === duplicateConnectorNameErrorCode) {
setError('connectorName', { type: 'custom', message: metadata.message });
}
}
}
})
);
@ -142,7 +158,7 @@ function SsoCreationModal({ isOpen, onClose: rawOnClose }: Props) {
<TextInput
{...register('connectorName', { required: true })}
placeholder={t('enterprise_sso.create_modal.connector_name_field_placeholder')}
error={Boolean(errors.connectorName)}
error={errors.connectorName?.message}
/>
</FormField>
</ModalLayout>

View file

@ -49,6 +49,8 @@ const duplicatedDomainsErrorCode = 'single_sign_on.duplicated_domains';
const forbiddenDomainsErrorCode = 'single_sign_on.forbidden_domains';
const invalidDomainFormatErrorCode = 'single_sign_on.invalid_domain_format';
const duplicateConnectorNameErrorCode = 'single_sign_on.duplicate_connector_name';
const dataToFormParser = (data: DataType) => {
const { branding, connectorName, domains, syncProfile } = data;
return {
@ -71,7 +73,7 @@ const formDataToSsoConnectorParser = (
};
};
function Settings({ data, isDeleted, onUpdated }: Props) {
function Experience({ data, isDeleted, onUpdated }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { isReady: isUserAssetsServiceReady } = useUserAssetsService();
const api = useApi({ hideErrorToast: true });
@ -118,26 +120,38 @@ function Settings({ data, isDeleted, onUpdated }: Props) {
toast.success(t('general.saved'));
onUpdated(updatedSsoConnector);
} catch (error: unknown) {
/**
* Should render invalid domains:
* - show `error` tag for forbidden domains
* - show `info` tag for duplicated domains
*
* Also manually passed the returned error message to `setError` to show the error message in-place.
*/
if (error instanceof HTTPError) {
const { response } = error;
const metadata = await response.clone().json<RequestErrorBody<{ data: string[] }>>();
setValue(
'domains',
watch('domains').map((domain) => ({
...domain,
...conditional(metadata.data.data.includes(domain.value) && { status: 'info' }),
})),
{ shouldDirty: true }
);
setError('domains', { type: 'custom', message: metadata.message });
/**
* Should render invalid domains:
* - show `error` tag for forbidden domains
* - show `info` tag for duplicated domains
*
* Also manually passed the returned error message to `setError` to show the error message in-place.
*/
if (
[
duplicatedDomainsErrorCode,
forbiddenDomainsErrorCode,
invalidDomainFormatErrorCode,
].includes(metadata.code)
) {
setValue(
'domains',
watch('domains').map((domain) => ({
...domain,
...conditional(metadata.data.data.includes(domain.value) && { status: 'info' }),
})),
{ shouldDirty: true }
);
setError('domains', { type: 'custom', message: metadata.message });
}
if (duplicateConnectorNameErrorCode === metadata.code) {
setError('connectorName', { type: 'custom', message: metadata.message });
}
}
}
})
@ -152,6 +166,12 @@ function Settings({ data, isDeleted, onUpdated }: Props) {
onSubmit={onSubmit}
>
<FormCard title="enterprise_sso_details.general_settings_title">
<FormField isRequired title="enterprise_sso_details.connector_name_field_name">
<TextInput
{...register('connectorName', { required: true })}
error={errors.connectorName?.message}
/>
</FormField>
<FormField title="enterprise_sso_details.email_domain_field_name">
<div className={styles.description}>
{t('enterprise_sso_details.email_domain_field_description')}
@ -239,10 +259,10 @@ function Settings({ data, isDeleted, onUpdated }: Props) {
title="enterprise_sso_details.custom_branding_title"
description="enterprise_sso_details.custom_branding_description"
>
<FormField isRequired title="enterprise_sso_details.connector_name_field_name">
<FormField title="enterprise_sso_details.display_name_field_name">
<TextInput
{...register('connectorName', { required: true })}
error={errors.connectorName?.message}
{...register('branding.displayName')}
error={errors.branding?.displayName?.message}
/>
</FormField>
{isUserAssetsServiceReady ? (
@ -283,4 +303,4 @@ function Settings({ data, isDeleted, onUpdated }: Props) {
);
}
export default Settings;
export default Experience;

View file

@ -28,7 +28,7 @@ import SsoConnectorLogo from '../EnterpriseSso/SsoConnectorLogo';
import { type SsoConnectorWithProviderConfigWithGeneric } from '../EnterpriseSso/types';
import Connection from './Connection';
import Settings from './Settings';
import Experience from './Experience';
import * as styles from './index.module.scss';
const enterpriseSsoPathname = '/enterprise-sso';
@ -167,7 +167,7 @@ function EnterpriseSsoConnectorDetails<T extends SsoProviderName>() {
</TabNavItem>
</TabNav>
{tab === EnterpriseSsoDetailsTabs.Experience && (
<Settings
<Experience
data={ssoConnector}
isDeleted={isDeleted}
onUpdated={() => {

View file

@ -250,7 +250,11 @@ export default function singleSignOnRoutes<T extends AuthedRouter>(...args: Rout
// Validate the connector name is unique
if (rest.connectorName) {
const duplicateConnector = await ssoConnectors.findByConnectorName(rest.connectorName);
assertThat(!duplicateConnector, 'single_sign_on.duplicate_connector_name');
// Should not block the update of the current connector.
assertThat(
!duplicateConnector || duplicateConnector.id === id,
'single_sign_on.duplicate_connector_name'
);
}
// Validate the connector config if it's provided

View file

@ -214,6 +214,20 @@ describe('patch sso-connector by id', () => {
await deleteSsoConnectorById(id2);
});
it('should not block the update of current connector', async () => {
const { id } = await createSsoConnector({
providerName: 'OIDC',
connectorName: 'test connector name',
});
const updatedSsoConnector = await patchSsoConnectorById(id, {
connectorName: 'test connector name',
});
expect(updatedSsoConnector).toHaveProperty('connectorName', 'test connector name');
await deleteSsoConnectorById(id);
});
it.each(providerNames)('should patch sso connector without config', async (providerName) => {
const { id } = await createSsoConnector({
providerName,

View file

@ -36,6 +36,8 @@ const enterprise_sso_details = {
/** UNTRANSLATED */
connector_name_field_name: 'Connector name',
/** UNTRANSLATED */
display_name_field_name: 'Display name',
/** UNTRANSLATED */
connector_logo_field_name: 'Connector logo',
/** UNTRANSLATED */
branding_logo_context: 'Upload logo',

View file

@ -19,6 +19,7 @@ const enterprise_sso_details = {
each_sign_in: 'Always sync at each sign-in',
},
connector_name_field_name: 'Connector name',
display_name_field_name: 'Display name',
connector_logo_field_name: 'Connector logo',
branding_logo_context: 'Upload logo',
branding_logo_error: 'Upload logo error: {{error}}',

View file

@ -36,6 +36,8 @@ const enterprise_sso_details = {
/** UNTRANSLATED */
connector_name_field_name: 'Connector name',
/** UNTRANSLATED */
display_name_field_name: 'Display name',
/** UNTRANSLATED */
connector_logo_field_name: 'Connector logo',
/** UNTRANSLATED */
branding_logo_context: 'Upload logo',

View file

@ -36,6 +36,8 @@ const enterprise_sso_details = {
/** UNTRANSLATED */
connector_name_field_name: 'Connector name',
/** UNTRANSLATED */
display_name_field_name: 'Display name',
/** UNTRANSLATED */
connector_logo_field_name: 'Connector logo',
/** UNTRANSLATED */
branding_logo_context: 'Upload logo',

View file

@ -36,6 +36,8 @@ const enterprise_sso_details = {
/** UNTRANSLATED */
connector_name_field_name: 'Connector name',
/** UNTRANSLATED */
display_name_field_name: 'Display name',
/** UNTRANSLATED */
connector_logo_field_name: 'Connector logo',
/** UNTRANSLATED */
branding_logo_context: 'Upload logo',

View file

@ -36,6 +36,8 @@ const enterprise_sso_details = {
/** UNTRANSLATED */
connector_name_field_name: 'Connector name',
/** UNTRANSLATED */
display_name_field_name: 'Display name',
/** UNTRANSLATED */
connector_logo_field_name: 'Connector logo',
/** UNTRANSLATED */
branding_logo_context: 'Upload logo',

View file

@ -36,6 +36,8 @@ const enterprise_sso_details = {
/** UNTRANSLATED */
connector_name_field_name: 'Connector name',
/** UNTRANSLATED */
display_name_field_name: 'Display name',
/** UNTRANSLATED */
connector_logo_field_name: 'Connector logo',
/** UNTRANSLATED */
branding_logo_context: 'Upload logo',

View file

@ -36,6 +36,8 @@ const enterprise_sso_details = {
/** UNTRANSLATED */
connector_name_field_name: 'Connector name',
/** UNTRANSLATED */
display_name_field_name: 'Display name',
/** UNTRANSLATED */
connector_logo_field_name: 'Connector logo',
/** UNTRANSLATED */
branding_logo_context: 'Upload logo',

View file

@ -36,6 +36,8 @@ const enterprise_sso_details = {
/** UNTRANSLATED */
connector_name_field_name: 'Connector name',
/** UNTRANSLATED */
display_name_field_name: 'Display name',
/** UNTRANSLATED */
connector_logo_field_name: 'Connector logo',
/** UNTRANSLATED */
branding_logo_context: 'Upload logo',

View file

@ -36,6 +36,8 @@ const enterprise_sso_details = {
/** UNTRANSLATED */
connector_name_field_name: 'Connector name',
/** UNTRANSLATED */
display_name_field_name: 'Display name',
/** UNTRANSLATED */
connector_logo_field_name: 'Connector logo',
/** UNTRANSLATED */
branding_logo_context: 'Upload logo',

View file

@ -36,6 +36,8 @@ const enterprise_sso_details = {
/** UNTRANSLATED */
connector_name_field_name: 'Connector name',
/** UNTRANSLATED */
display_name_field_name: 'Display name',
/** UNTRANSLATED */
connector_logo_field_name: 'Connector logo',
/** UNTRANSLATED */
branding_logo_context: 'Upload logo',

View file

@ -36,6 +36,8 @@ const enterprise_sso_details = {
/** UNTRANSLATED */
connector_name_field_name: 'Connector name',
/** UNTRANSLATED */
display_name_field_name: 'Display name',
/** UNTRANSLATED */
connector_logo_field_name: 'Connector logo',
/** UNTRANSLATED */
branding_logo_context: 'Upload logo',

View file

@ -36,6 +36,8 @@ const enterprise_sso_details = {
/** UNTRANSLATED */
connector_name_field_name: 'Connector name',
/** UNTRANSLATED */
display_name_field_name: 'Display name',
/** UNTRANSLATED */
connector_logo_field_name: 'Connector logo',
/** UNTRANSLATED */
branding_logo_context: 'Upload logo',

View file

@ -36,6 +36,8 @@ const enterprise_sso_details = {
/** UNTRANSLATED */
connector_name_field_name: 'Connector name',
/** UNTRANSLATED */
display_name_field_name: 'Display name',
/** UNTRANSLATED */
connector_logo_field_name: 'Connector logo',
/** UNTRANSLATED */
branding_logo_context: 'Upload logo',

View file

@ -36,6 +36,8 @@ const enterprise_sso_details = {
/** UNTRANSLATED */
connector_name_field_name: 'Connector name',
/** UNTRANSLATED */
display_name_field_name: 'Display name',
/** UNTRANSLATED */
connector_logo_field_name: 'Connector logo',
/** UNTRANSLATED */
branding_logo_context: 'Upload logo',