0
Fork 0
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:
simeng-li 2024-08-02 12:02:07 +08:00 committed by GitHub
parent 08e7c52365
commit ce63bba018
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 73 additions and 77 deletions

View file

@ -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 } }) => (
<FormField
title="application_details.field_custom_data"
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>
)}
/>

View file

@ -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,
})
.json<ApplicationResponse>();
reset(applicationFormDataParser.fromResponse(updatedData));
onApplicationUpdated();
toast.success(t('general.saved'));
return;
}
if (error) {
toast.error(String(t(error)));
}
})
);

View file

@ -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<Exclude<ProtectedAppMetadataType, null>, '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<AdminConsoleKey>, DeepPartial<ApplicationResponse>?] => {
toRequestPayload: (data: ApplicationForm): DeepPartial<ApplicationResponse> => {
const {
name,
description,
@ -73,15 +70,7 @@ export const applicationFormDataParser = {
customData,
} = data;
const parsedCustomData = safeParseJsonObject(customData);
if (!parsedCustomData.success) {
return ['application_details.custom_data_invalid'];
}
return [
null,
{
return {
name,
...cond(
!protectedAppMetadata && {
@ -101,7 +90,11 @@ export const applicationFormDataParser = {
customClientMetadata?.corsAllowedOrigins
),
},
customData: parsedCustomData.data,
...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,
}
),
@ -113,7 +106,6 @@ export const applicationFormDataParser = {
},
}
),
},
];
};
},
};

View file

@ -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<OrganizationDetailsOutletContext>();

View file

@ -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<Omit<Organization, 'customData'> & { customData:
jitSsoConnectorIds: string[];
};
export const isJsonObject = (value: string) => {
const parsed = trySafe<unknown>(() => JSON.parse(value));
return Boolean(parsed && typeof parsed === 'object');
};
export const normalizeData = (
data: Organization,
jit: { emailDomains: string[]; roles: Array<Option<string>>; ssoConnectorIds: string[] }

View file

@ -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<unknown>(() => JSON.parse(value));
return Boolean(parsed && typeof parsed === 'object');
};

View file

@ -96,7 +96,7 @@ const application_details = {
no_organization_placeholder: 'No organization found. <a>Go to organizations</a>',
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',