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:
parent
c47e0192ff
commit
14855cde2c
25 changed files with 113 additions and 30 deletions
|
@ -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>
|
||||
|
|
|
@ -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;
|
|
@ -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={() => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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}}',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Add table
Reference in a new issue