From ce63bba018f7483dc66953050ddb599ea82248e6 Mon Sep 17 00:00:00 2001 From: simeng-li Date: Fri, 2 Aug 2024 12:02:07 +0800 Subject: [PATCH] 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 --- .../ApplicationDetailsContent/Settings.tsx | 12 ++- .../ApplicationDetailsContent/index.tsx | 25 ++--- .../ApplicationDetailsContent/utils.ts | 92 +++++++++---------- .../OrganizationDetails/Settings/index.tsx | 5 +- .../OrganizationDetails/Settings/utils.ts | 6 -- packages/console/src/utils/json.ts | 8 +- .../admin-console/application-details.ts | 2 +- 7 files changed, 73 insertions(+), 77 deletions(-) diff --git a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Settings.tsx b/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Settings.tsx index 09f316265..880d74a03 100644 --- a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Settings.tsx +++ b/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Settings.tsx @@ -16,6 +16,7 @@ import { import TextInput from '@/ds-components/TextInput'; import TextLink from '@/ds-components/TextLink'; import useDocumentationUrl from '@/hooks/use-documentation-url'; +import { isJsonObject } from '@/utils/json'; import ProtectedAppSettings from './ProtectedAppSettings'; import { type ApplicationForm } from './utils'; @@ -167,12 +168,21 @@ function Settings({ data }: Props) { name="customData" control={control} defaultValue="{}" + rules={{ + validate: (value) => + isJsonObject(value ?? '') ? true : t('application_details.custom_data_invalid'), + }} render={({ field: { value, onChange } }) => ( - + )} /> diff --git a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/index.tsx b/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/index.tsx index 81ac8561e..102910dce 100644 --- a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/index.tsx +++ b/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/index.tsx @@ -84,24 +84,17 @@ function ApplicationDetailsContent({ data, secrets, oidcConfig, onApplicationUpd return; } - const [error, result] = applicationFormDataParser.toRequestPayload(formData); + const json = applicationFormDataParser.toRequestPayload(formData); - if (result) { - const updatedData = await api - .patch(`api/applications/${data.id}`, { - json: result, - }) - .json(); + const updatedData = await api + .patch(`api/applications/${data.id}`, { + json, + }) + .json(); - reset(applicationFormDataParser.fromResponse(updatedData)); - onApplicationUpdated(); - toast.success(t('general.saved')); - return; - } - - if (error) { - toast.error(String(t(error))); - } + reset(applicationFormDataParser.fromResponse(updatedData)); + onApplicationUpdated(); + toast.success(t('general.saved')); }) ); diff --git a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/utils.ts b/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/utils.ts index 97501b0e9..e2cc0213c 100644 --- a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/utils.ts +++ b/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/utils.ts @@ -1,8 +1,7 @@ -import { type AdminConsoleKey } from '@logto/phrases'; 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']; @@ -14,7 +13,7 @@ export type ApplicationForm = { isAdmin?: ApplicationResponse['isAdmin']; // eslint-disable-next-line @typescript-eslint/ban-types protectedAppMetadata?: Omit, 'customDomains'>; // Custom domains are handled separately - customData: string; + customData?: string; }; const mapToUriFormatArrays = (value?: string[]) => @@ -46,6 +45,7 @@ export const applicationFormDataParser = { ...customClientMetadataDefault, ...customClientMetadata, }, + customData: JSON.stringify(customData, null, 2), isAdmin, } ), @@ -57,12 +57,9 @@ export const applicationFormDataParser = { }, } ), - customData: JSON.stringify(customData, null, 2), }; }, - toRequestPayload: ( - data: ApplicationForm - ): [Nullable, DeepPartial?] => { + toRequestPayload: (data: ApplicationForm): DeepPartial => { const { name, description, @@ -73,47 +70,42 @@ export const applicationFormDataParser = { customData, } = data; - const parsedCustomData = safeParseJsonObject(customData); - - if (!parsedCustomData.success) { - return ['application_details.custom_data_invalid']; - } - - return [ - null, - { - name, - ...cond( - !protectedAppMetadata && { - description, - oidcClientMetadata: { - ...oidcClientMetadata, - redirectUris: mapToUriFormatArrays(oidcClientMetadata?.redirectUris), - postLogoutRedirectUris: mapToUriFormatArrays( - oidcClientMetadata?.postLogoutRedirectUris - ), - // Empty string is not a valid URL - backchannelLogoutUri: cond(oidcClientMetadata?.backchannelLogoutUri), - }, - customClientMetadata: { - ...customClientMetadata, - corsAllowedOrigins: mapToUriOriginFormatArrays( - customClientMetadata?.corsAllowedOrigins - ), - }, - customData: parsedCustomData.data, - isAdmin, - } - ), - ...cond( - protectedAppMetadata && { - protectedAppMetadata: { - ...protectedAppMetadata, - sessionDuration: protectedAppMetadata.sessionDuration * 3600 * 24, - }, - } - ), - }, - ]; + return { + name, + ...cond( + !protectedAppMetadata && { + description, + oidcClientMetadata: { + ...oidcClientMetadata, + redirectUris: mapToUriFormatArrays(oidcClientMetadata?.redirectUris), + postLogoutRedirectUris: mapToUriFormatArrays( + oidcClientMetadata?.postLogoutRedirectUris + ), + // Empty string is not a valid URL + backchannelLogoutUri: cond(oidcClientMetadata?.backchannelLogoutUri), + }, + customClientMetadata: { + ...customClientMetadata, + corsAllowedOrigins: mapToUriOriginFormatArrays( + customClientMetadata?.corsAllowedOrigins + ), + }, + ...conditional( + // Invalid JSON string will be guarded by the form field validation + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + customData && isJsonObject(customData) && { customData: JSON.parse(customData) } + ), + isAdmin, + } + ), + ...cond( + protectedAppMetadata && { + protectedAppMetadata: { + ...protectedAppMetadata, + sessionDuration: protectedAppMetadata.sessionDuration * 3600 * 24, + }, + } + ), + }; }, }; diff --git a/packages/console/src/pages/OrganizationDetails/Settings/index.tsx b/packages/console/src/pages/OrganizationDetails/Settings/index.tsx index deccae6d6..286a48af0 100644 --- a/packages/console/src/pages/OrganizationDetails/Settings/index.tsx +++ b/packages/console/src/pages/OrganizationDetails/Settings/index.tsx @@ -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 { toast } from 'react-hot-toast'; 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 useDocumentationUrl from '@/hooks/use-documentation-url'; import { trySubmitSafe } from '@/utils/form'; +import { isJsonObject } from '@/utils/json'; import { type OrganizationDetailsOutletContext } from '../types'; import JitSettings from './JitSettings'; import styles from './index.module.scss'; -import { assembleData, isJsonObject, normalizeData, type FormData } from './utils'; +import { assembleData, normalizeData, type FormData } from './utils'; function Settings() { const { isDeleting, data, jit, onUpdated } = useOutletContext(); diff --git a/packages/console/src/pages/OrganizationDetails/Settings/utils.ts b/packages/console/src/pages/OrganizationDetails/Settings/utils.ts index 0ad7767a6..4f82d7ac5 100644 --- a/packages/console/src/pages/OrganizationDetails/Settings/utils.ts +++ b/packages/console/src/pages/OrganizationDetails/Settings/utils.ts @@ -1,5 +1,4 @@ import { type Organization } from '@logto/schemas'; -import { trySafe } from '@silverhand/essentials'; import { type Option } from '@/ds-components/Select/MultiSelect'; import { emptyBranding } from '@/types/sign-in-experience'; @@ -11,11 +10,6 @@ export type FormData = Partial & { customData: jitSsoConnectorIds: string[]; }; -export const isJsonObject = (value: string) => { - const parsed = trySafe(() => JSON.parse(value)); - return Boolean(parsed && typeof parsed === 'object'); -}; - export const normalizeData = ( data: Organization, jit: { emailDomains: string[]; roles: Array>; ssoConnectorIds: string[] } diff --git a/packages/console/src/utils/json.ts b/packages/console/src/utils/json.ts index d3b390b0f..97ec910b1 100644 --- a/packages/console/src/utils/json.ts +++ b/packages/console/src/utils/json.ts @@ -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'; export const safeParseJson = ( @@ -24,3 +25,8 @@ export const safeParseJsonObject = ( return { success: false, error: t('admin_console.errors.invalid_json_format') }; } }; + +export const isJsonObject = (value: string) => { + const parsed = trySafe(() => JSON.parse(value)); + return Boolean(parsed && typeof parsed === 'object'); +}; diff --git a/packages/phrases/src/locales/en/translation/admin-console/application-details.ts b/packages/phrases/src/locales/en/translation/admin-console/application-details.ts index 242a7a75d..daa5b00b8 100644 --- a/packages/phrases/src/locales/en/translation/admin-console/application-details.ts +++ b/packages/phrases/src/locales/en/translation/admin-console/application-details.ts @@ -96,7 +96,7 @@ const application_details = { no_organization_placeholder: 'No organization found. Go to organizations', field_custom_data: 'Custom data', 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', branding: { name: 'Branding',