0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-30 20:33:54 -05:00

Merge pull request #6020 from logto-io/gao-add-profile-in-user-settings

refactor(console): allow view and update `user.profile` in settings
This commit is contained in:
Gao Sun 2024-06-17 10:45:14 +08:00 committed by GitHub
commit e541716012
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 49 additions and 12 deletions

View file

@ -0,0 +1,6 @@
---
"@logto/console": patch
"@logto/phrases": patch
---
view and update user's `profile` property in the user settings page

View file

@ -26,3 +26,4 @@ export const organizationRoleLink =
'/docs/recipes/organizations/understand-how-it-works/#organization-role'; '/docs/recipes/organizations/understand-how-it-works/#organization-role';
export const organizationPermissionLink = export const organizationPermissionLink =
'/docs/recipes/organizations/understand-how-it-works/#organization-permission'; '/docs/recipes/organizations/understand-how-it-works/#organization-permission';
export const profilePropertyReferenceLink = '/docs/references/users/#profile-1';

View file

@ -5,15 +5,17 @@ import { conditionalString, trySafe } from '@silverhand/essentials';
import { parsePhoneNumberWithError } from 'libphonenumber-js'; import { parsePhoneNumberWithError } from 'libphonenumber-js';
import { useForm, useController } from 'react-hook-form'; import { useForm, useController } from 'react-hook-form';
import { toast } from 'react-hot-toast'; import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next'; import { Trans, useTranslation } from 'react-i18next';
import { useOutletContext } from 'react-router-dom'; import { useOutletContext } from 'react-router-dom';
import DetailsForm from '@/components/DetailsForm'; import DetailsForm from '@/components/DetailsForm';
import FormCard from '@/components/FormCard'; import FormCard from '@/components/FormCard';
import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal'; import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
import { profilePropertyReferenceLink } from '@/consts';
import CodeEditor from '@/ds-components/CodeEditor'; import CodeEditor from '@/ds-components/CodeEditor';
import FormField from '@/ds-components/FormField'; import FormField from '@/ds-components/FormField';
import TextInput from '@/ds-components/TextInput'; import TextInput from '@/ds-components/TextInput';
import TextLink from '@/ds-components/TextLink';
import useApi from '@/hooks/use-api'; import useApi from '@/hooks/use-api';
import { useConfirmModal } from '@/hooks/use-confirm-modal'; import { useConfirmModal } from '@/hooks/use-confirm-modal';
import useDocumentationUrl from '@/hooks/use-documentation-url'; import useDocumentationUrl from '@/hooks/use-documentation-url';
@ -44,9 +46,8 @@ function UserSettings() {
formState: { isSubmitting, errors, isDirty }, formState: { isSubmitting, errors, isDirty },
} = useForm<UserDetailsForm>({ defaultValues: userFormData }); } = useForm<UserDetailsForm>({ defaultValues: userFormData });
const { const { field: customData } = useController({ name: 'customData', control });
field: { onChange, value }, const { field: profile } = useController({ name: 'profile', control });
} = useController({ name: 'customData', control });
const api = useApi(); const api = useApi();
@ -56,7 +57,7 @@ function UserSettings() {
return; return;
} }
const { identities, id: userId } = user; const { identities, id: userId } = user;
const { customData: inputCustomData, username, primaryEmail, primaryPhone } = formData; const { customData, profile, username, primaryEmail, primaryPhone } = formData;
if (!username && !primaryEmail && !primaryPhone && Object.keys(identities).length === 0) { if (!username && !primaryEmail && !primaryPhone && Object.keys(identities).length === 0) {
const [result] = await show({ const [result] = await show({
@ -69,18 +70,23 @@ function UserSettings() {
} }
} }
const parseResult = safeParseJsonObject(inputCustomData); const parsedCustomData = safeParseJsonObject(customData);
if (!parsedCustomData.success) {
if (!parseResult.success) {
toast.error(t('user_details.custom_data_invalid')); toast.error(t('user_details.custom_data_invalid'));
return;
}
const parsedProfile = safeParseJsonObject(profile);
if (!parsedProfile.success) {
toast.error(t('user_details.profile_invalid'));
return; return;
} }
const payload: Partial<User> = { const payload: Partial<User> = {
...formData, ...formData,
primaryPhone: conditionalString(primaryPhone && parsePhoneNumber(primaryPhone)), primaryPhone: conditionalString(primaryPhone && parsePhoneNumber(primaryPhone)),
customData: parseResult.data, customData: parsedCustomData.data,
profile: parsedProfile.data,
}; };
const updatedUser = await api.patch(`api/users/${userId}`, { json: payload }).json<User>(); const updatedUser = await api.patch(`api/users/${userId}`, { json: payload }).json<User>();
@ -174,11 +180,29 @@ function UserSettings() {
/> />
</FormField> </FormField>
<FormField <FormField
isRequired
title="user_details.field_custom_data" title="user_details.field_custom_data"
tip={t('user_details.field_custom_data_tip')} tip={t('user_details.field_custom_data_tip')}
> >
<CodeEditor language="json" value={value} onChange={onChange} /> <CodeEditor language="json" value={customData.value} onChange={customData.onChange} />
</FormField>
<FormField
title="user_details.field_profile"
tip={
<Trans
components={{
a: (
<TextLink
href={getDocumentationUrl(profilePropertyReferenceLink)}
targetBlank="noopener"
/>
),
}}
>
{t('user_details.field_profile_tip')}
</Trans>
}
>
<CodeEditor language="json" value={profile.value} onChange={profile.onChange} />
</FormField> </FormField>
</FormCard> </FormCard>
</DetailsForm> </DetailsForm>

View file

@ -7,6 +7,7 @@ export type UserDetailsForm = {
name: string; name: string;
avatar: string; avatar: string;
customData: string; customData: string;
profile: string;
}; };
export type UserDetailsOutletContext = { export type UserDetailsOutletContext = {

View file

@ -6,7 +6,7 @@ import type { UserDetailsForm } from './types';
export const userDetailsParser = { export const userDetailsParser = {
toLocalForm: (data: UserProfileResponse): UserDetailsForm => { toLocalForm: (data: UserProfileResponse): UserDetailsForm => {
const { primaryEmail, primaryPhone, username, name, avatar, customData } = data; const { primaryEmail, primaryPhone, username, name, avatar, customData, profile } = data;
const parsedPhoneNumber = conditional( const parsedPhoneNumber = conditional(
primaryPhone && formatToInternationalPhoneNumber(primaryPhone) primaryPhone && formatToInternationalPhoneNumber(primaryPhone)
); );
@ -18,6 +18,7 @@ export const userDetailsParser = {
name: name ?? '', name: name ?? '',
avatar: avatar ?? '', avatar: avatar ?? '',
customData: JSON.stringify(customData, null, 2), customData: JSON.stringify(customData, null, 2),
profile: JSON.stringify(profile, null, 2),
}; };
}, },
}; };

View file

@ -34,9 +34,13 @@ const user_details = {
field_custom_data: 'Custom data', field_custom_data: 'Custom data',
field_custom_data_tip: field_custom_data_tip:
'Additional user info not listed in the pre-defined user properties, such as user-preferred color and language.', 'Additional user info not listed in the pre-defined user properties, such as user-preferred color and language.',
field_profile: 'Profile',
field_profile_tip:
"Additional OpenID Connect standard claims that are not included in user's properties. Note that all unknown properties will be stripped. Please refer to <a>profile property reference</a> for more information.",
field_connectors: 'Social connections', field_connectors: 'Social connections',
field_sso_connectors: 'Enterprise connections', field_sso_connectors: 'Enterprise connections',
custom_data_invalid: 'Custom data must be a valid JSON object', custom_data_invalid: 'Custom data must be a valid JSON object',
profile_invalid: 'Profile must be a valid JSON object',
connectors: { connectors: {
connectors: 'Connectors', connectors: 'Connectors',
user_id: 'User ID', user_id: 'User ID',