mirror of
https://github.com/logto-io/logto.git
synced 2024-12-30 20:33:54 -05:00
fix(console): add in-line error message (#6386)
* fix(console): add in-line error message add in-line error message * refactor(console): remove old validation logic remove old validation logic
This commit is contained in:
parent
08e7c52365
commit
ce63bba018
7 changed files with 73 additions and 77 deletions
|
@ -16,6 +16,7 @@ import {
|
||||||
import TextInput from '@/ds-components/TextInput';
|
import TextInput from '@/ds-components/TextInput';
|
||||||
import TextLink from '@/ds-components/TextLink';
|
import TextLink from '@/ds-components/TextLink';
|
||||||
import useDocumentationUrl from '@/hooks/use-documentation-url';
|
import useDocumentationUrl from '@/hooks/use-documentation-url';
|
||||||
|
import { isJsonObject } from '@/utils/json';
|
||||||
|
|
||||||
import ProtectedAppSettings from './ProtectedAppSettings';
|
import ProtectedAppSettings from './ProtectedAppSettings';
|
||||||
import { type ApplicationForm } from './utils';
|
import { type ApplicationForm } from './utils';
|
||||||
|
@ -167,12 +168,21 @@ function Settings({ data }: Props) {
|
||||||
name="customData"
|
name="customData"
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue="{}"
|
defaultValue="{}"
|
||||||
|
rules={{
|
||||||
|
validate: (value) =>
|
||||||
|
isJsonObject(value ?? '') ? true : t('application_details.custom_data_invalid'),
|
||||||
|
}}
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<FormField
|
<FormField
|
||||||
title="application_details.field_custom_data"
|
title="application_details.field_custom_data"
|
||||||
tip={t('application_details.field_custom_data_tip')}
|
tip={t('application_details.field_custom_data_tip')}
|
||||||
>
|
>
|
||||||
<CodeEditor language="json" value={value} onChange={onChange} />
|
<CodeEditor
|
||||||
|
language="json"
|
||||||
|
value={value}
|
||||||
|
error={errors.customData?.message}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -84,24 +84,17 @@ function ApplicationDetailsContent({ data, secrets, oidcConfig, onApplicationUpd
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [error, result] = applicationFormDataParser.toRequestPayload(formData);
|
const json = applicationFormDataParser.toRequestPayload(formData);
|
||||||
|
|
||||||
if (result) {
|
const updatedData = await api
|
||||||
const updatedData = await api
|
.patch(`api/applications/${data.id}`, {
|
||||||
.patch(`api/applications/${data.id}`, {
|
json,
|
||||||
json: result,
|
})
|
||||||
})
|
.json<ApplicationResponse>();
|
||||||
.json<ApplicationResponse>();
|
|
||||||
|
|
||||||
reset(applicationFormDataParser.fromResponse(updatedData));
|
reset(applicationFormDataParser.fromResponse(updatedData));
|
||||||
onApplicationUpdated();
|
onApplicationUpdated();
|
||||||
toast.success(t('general.saved'));
|
toast.success(t('general.saved'));
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
toast.error(String(t(error)));
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import { type AdminConsoleKey } from '@logto/phrases';
|
|
||||||
import { customClientMetadataDefault, type ApplicationResponse } from '@logto/schemas';
|
import { customClientMetadataDefault, type ApplicationResponse } from '@logto/schemas';
|
||||||
import { cond, type DeepPartial, type Nullable } from '@silverhand/essentials';
|
import { cond, conditional, type DeepPartial } from '@silverhand/essentials';
|
||||||
|
|
||||||
import { safeParseJsonObject } from '@/utils/json';
|
import { isJsonObject } from '@/utils/json';
|
||||||
|
|
||||||
type ProtectedAppMetadataType = ApplicationResponse['protectedAppMetadata'];
|
type ProtectedAppMetadataType = ApplicationResponse['protectedAppMetadata'];
|
||||||
|
|
||||||
|
@ -14,7 +13,7 @@ export type ApplicationForm = {
|
||||||
isAdmin?: ApplicationResponse['isAdmin'];
|
isAdmin?: ApplicationResponse['isAdmin'];
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
protectedAppMetadata?: Omit<Exclude<ProtectedAppMetadataType, null>, 'customDomains'>; // Custom domains are handled separately
|
protectedAppMetadata?: Omit<Exclude<ProtectedAppMetadataType, null>, 'customDomains'>; // Custom domains are handled separately
|
||||||
customData: string;
|
customData?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapToUriFormatArrays = (value?: string[]) =>
|
const mapToUriFormatArrays = (value?: string[]) =>
|
||||||
|
@ -46,6 +45,7 @@ export const applicationFormDataParser = {
|
||||||
...customClientMetadataDefault,
|
...customClientMetadataDefault,
|
||||||
...customClientMetadata,
|
...customClientMetadata,
|
||||||
},
|
},
|
||||||
|
customData: JSON.stringify(customData, null, 2),
|
||||||
isAdmin,
|
isAdmin,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
@ -57,12 +57,9 @@ export const applicationFormDataParser = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
customData: JSON.stringify(customData, null, 2),
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
toRequestPayload: (
|
toRequestPayload: (data: ApplicationForm): DeepPartial<ApplicationResponse> => {
|
||||||
data: ApplicationForm
|
|
||||||
): [Nullable<AdminConsoleKey>, DeepPartial<ApplicationResponse>?] => {
|
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
|
@ -73,47 +70,42 @@ export const applicationFormDataParser = {
|
||||||
customData,
|
customData,
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
const parsedCustomData = safeParseJsonObject(customData);
|
return {
|
||||||
|
name,
|
||||||
if (!parsedCustomData.success) {
|
...cond(
|
||||||
return ['application_details.custom_data_invalid'];
|
!protectedAppMetadata && {
|
||||||
}
|
description,
|
||||||
|
oidcClientMetadata: {
|
||||||
return [
|
...oidcClientMetadata,
|
||||||
null,
|
redirectUris: mapToUriFormatArrays(oidcClientMetadata?.redirectUris),
|
||||||
{
|
postLogoutRedirectUris: mapToUriFormatArrays(
|
||||||
name,
|
oidcClientMetadata?.postLogoutRedirectUris
|
||||||
...cond(
|
),
|
||||||
!protectedAppMetadata && {
|
// Empty string is not a valid URL
|
||||||
description,
|
backchannelLogoutUri: cond(oidcClientMetadata?.backchannelLogoutUri),
|
||||||
oidcClientMetadata: {
|
},
|
||||||
...oidcClientMetadata,
|
customClientMetadata: {
|
||||||
redirectUris: mapToUriFormatArrays(oidcClientMetadata?.redirectUris),
|
...customClientMetadata,
|
||||||
postLogoutRedirectUris: mapToUriFormatArrays(
|
corsAllowedOrigins: mapToUriOriginFormatArrays(
|
||||||
oidcClientMetadata?.postLogoutRedirectUris
|
customClientMetadata?.corsAllowedOrigins
|
||||||
),
|
),
|
||||||
// Empty string is not a valid URL
|
},
|
||||||
backchannelLogoutUri: cond(oidcClientMetadata?.backchannelLogoutUri),
|
...conditional(
|
||||||
},
|
// Invalid JSON string will be guarded by the form field validation
|
||||||
customClientMetadata: {
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
...customClientMetadata,
|
customData && isJsonObject(customData) && { customData: JSON.parse(customData) }
|
||||||
corsAllowedOrigins: mapToUriOriginFormatArrays(
|
),
|
||||||
customClientMetadata?.corsAllowedOrigins
|
isAdmin,
|
||||||
),
|
}
|
||||||
},
|
),
|
||||||
customData: parsedCustomData.data,
|
...cond(
|
||||||
isAdmin,
|
protectedAppMetadata && {
|
||||||
}
|
protectedAppMetadata: {
|
||||||
),
|
...protectedAppMetadata,
|
||||||
...cond(
|
sessionDuration: protectedAppMetadata.sessionDuration * 3600 * 24,
|
||||||
protectedAppMetadata && {
|
},
|
||||||
protectedAppMetadata: {
|
}
|
||||||
...protectedAppMetadata,
|
),
|
||||||
sessionDuration: protectedAppMetadata.sessionDuration * 3600 * 24,
|
};
|
||||||
},
|
|
||||||
}
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { type SignInExperience, type Organization, Theme } from '@logto/schemas';
|
import { Theme, type Organization, type SignInExperience } from '@logto/schemas';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
|
@ -20,12 +20,13 @@ import useApi, { type RequestError } from '@/hooks/use-api';
|
||||||
import { mfa } from '@/hooks/use-console-routes/routes/mfa';
|
import { mfa } from '@/hooks/use-console-routes/routes/mfa';
|
||||||
import useDocumentationUrl from '@/hooks/use-documentation-url';
|
import useDocumentationUrl from '@/hooks/use-documentation-url';
|
||||||
import { trySubmitSafe } from '@/utils/form';
|
import { trySubmitSafe } from '@/utils/form';
|
||||||
|
import { isJsonObject } from '@/utils/json';
|
||||||
|
|
||||||
import { type OrganizationDetailsOutletContext } from '../types';
|
import { type OrganizationDetailsOutletContext } from '../types';
|
||||||
|
|
||||||
import JitSettings from './JitSettings';
|
import JitSettings from './JitSettings';
|
||||||
import styles from './index.module.scss';
|
import styles from './index.module.scss';
|
||||||
import { assembleData, isJsonObject, normalizeData, type FormData } from './utils';
|
import { assembleData, normalizeData, type FormData } from './utils';
|
||||||
|
|
||||||
function Settings() {
|
function Settings() {
|
||||||
const { isDeleting, data, jit, onUpdated } = useOutletContext<OrganizationDetailsOutletContext>();
|
const { isDeleting, data, jit, onUpdated } = useOutletContext<OrganizationDetailsOutletContext>();
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { type Organization } from '@logto/schemas';
|
import { type Organization } from '@logto/schemas';
|
||||||
import { trySafe } from '@silverhand/essentials';
|
|
||||||
|
|
||||||
import { type Option } from '@/ds-components/Select/MultiSelect';
|
import { type Option } from '@/ds-components/Select/MultiSelect';
|
||||||
import { emptyBranding } from '@/types/sign-in-experience';
|
import { emptyBranding } from '@/types/sign-in-experience';
|
||||||
|
@ -11,11 +10,6 @@ export type FormData = Partial<Omit<Organization, 'customData'> & { customData:
|
||||||
jitSsoConnectorIds: string[];
|
jitSsoConnectorIds: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isJsonObject = (value: string) => {
|
|
||||||
const parsed = trySafe<unknown>(() => JSON.parse(value));
|
|
||||||
return Boolean(parsed && typeof parsed === 'object');
|
|
||||||
};
|
|
||||||
|
|
||||||
export const normalizeData = (
|
export const normalizeData = (
|
||||||
data: Organization,
|
data: Organization,
|
||||||
jit: { emailDomains: string[]; roles: Array<Option<string>>; ssoConnectorIds: string[] }
|
jit: { emailDomains: string[]; roles: Array<Option<string>>; ssoConnectorIds: string[] }
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { jsonGuard, type Json, jsonObjectGuard, type JsonObject } from '@logto/schemas';
|
import { jsonGuard, jsonObjectGuard, type Json, type JsonObject } from '@logto/schemas';
|
||||||
|
import { trySafe } from '@silverhand/essentials';
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
|
|
||||||
export const safeParseJson = (
|
export const safeParseJson = (
|
||||||
|
@ -24,3 +25,8 @@ export const safeParseJsonObject = (
|
||||||
return { success: false, error: t('admin_console.errors.invalid_json_format') };
|
return { success: false, error: t('admin_console.errors.invalid_json_format') };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isJsonObject = (value: string) => {
|
||||||
|
const parsed = trySafe<unknown>(() => JSON.parse(value));
|
||||||
|
return Boolean(parsed && typeof parsed === 'object');
|
||||||
|
};
|
||||||
|
|
|
@ -96,7 +96,7 @@ const application_details = {
|
||||||
no_organization_placeholder: 'No organization found. <a>Go to organizations</a>',
|
no_organization_placeholder: 'No organization found. <a>Go to organizations</a>',
|
||||||
field_custom_data: 'Custom data',
|
field_custom_data: 'Custom data',
|
||||||
field_custom_data_tip:
|
field_custom_data_tip:
|
||||||
'Additional custom application metadata not listed in the pre-defined application properties, ',
|
'Additional custom application info not listed in the pre-defined application properties, such as business-specific settings and configurations.',
|
||||||
custom_data_invalid: 'Custom data must be a valid JSON object',
|
custom_data_invalid: 'Custom data must be a valid JSON object',
|
||||||
branding: {
|
branding: {
|
||||||
name: 'Branding',
|
name: 'Branding',
|
||||||
|
|
Loading…
Reference in a new issue