0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

feat(console,phrases): add support to update sign-in identifiers in user details form (#3828)

* feat(console,phrases): add support to update sign-in identifiers in user details form

* chore: add changeset
This commit is contained in:
Charles Zhao 2023-05-17 16:19:51 +08:00 committed by GitHub
parent 1b6bb9b1cf
commit 497d5b5262
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 239 additions and 96 deletions

View file

@ -0,0 +1,9 @@
---
"@logto/console": minor
"@logto/phrases": minor
"@logto/schemas": patch
---
Support updating sign-in identifiers in user details form
- Admin can now update user sign-in identifiers (username, email, phone number) in the user details form in user management.
- Other trivial improvements and fixes, e.g. input field placeholder, error handling, etc.

View file

@ -81,6 +81,7 @@
"jest-transformer-svg": "^2.0.0",
"just-kebab-case": "^4.2.0",
"ky": "^0.33.0",
"libphonenumber-js": "^1.9.49",
"lint-staged": "^13.0.0",
"nanoid": "^4.0.0",
"overlayscrollbars": "^2.0.2",

View file

@ -1,5 +1,7 @@
import { jsonObjectGuard } from '@logto/schemas';
import { emailRegEx, usernameRegEx } from '@logto/core-kit';
import type { User } from '@logto/schemas';
import { trySafe } from '@silverhand/essentials';
import { parsePhoneNumberWithError } from 'libphonenumber-js';
import { useForm, useController } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
@ -12,8 +14,10 @@ import FormField from '@/components/FormField';
import TextInput from '@/components/TextInput';
import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
import useApi from '@/hooks/use-api';
import { useConfirmModal } from '@/hooks/use-confirm-modal';
import useDocumentationUrl from '@/hooks/use-documentation-url';
import { safeParseJson } from '@/utils/json';
import { safeParseJsonObject } from '@/utils/json';
import { parsePhoneNumber } from '@/utils/phone';
import { uriValidator } from '@/utils/validator';
import type { UserDetailsForm, UserDetailsOutletContext } from '../types';
@ -24,7 +28,7 @@ import UserSocialIdentities from './components/UserSocialIdentities';
function UserSettings() {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { getDocumentationUrl } = useDocumentationUrl();
const { show } = useConfirmModal();
const { user, isDeleting, onUserUpdated } = useOutletContext<UserDetailsOutletContext>();
const userFormData = userDetailsParser.toLocalForm(user);
@ -35,7 +39,6 @@ function UserSettings() {
control,
reset,
formState: { isSubmitting, errors, isDirty },
getValues,
} = useForm<UserDetailsForm>({ defaultValues: userFormData });
const {
@ -48,35 +51,42 @@ function UserSettings() {
if (isSubmitting) {
return;
}
const { identities, id: userId } = user;
const { customData: inputCustomData, username, primaryEmail, primaryPhone } = formData;
const { customData: inputCustomData, name, avatar } = formData;
if (!username && !primaryEmail && !primaryPhone && Object.keys(identities).length === 0) {
const [result] = await show({
ModalContent: t('user_details.warning_no_sign_in_identifier'),
type: 'confirm',
});
const parseResult = safeParseJson(inputCustomData);
if (!parseResult.success) {
toast.error(parseResult.error);
return;
if (!result) {
return;
}
}
const guardResult = jsonObjectGuard.safeParse(parseResult.data);
const parseResult = safeParseJsonObject(inputCustomData);
if (!guardResult.success) {
if (!parseResult.success) {
toast.error(t('user_details.custom_data_invalid'));
return;
}
const payload: Partial<User> = {
name,
avatar,
customData: guardResult.data,
...formData,
primaryPhone: parsePhoneNumber(primaryPhone),
customData: parseResult.data,
};
const updatedUser = await api.patch(`api/users/${user.id}`, { json: payload }).json<User>();
reset(userDetailsParser.toLocalForm(updatedUser));
onUserUpdated(updatedUser);
toast.success(t('general.saved'));
try {
const updatedUser = await api.patch(`api/users/${userId}`, { json: payload }).json<User>();
reset(userDetailsParser.toLocalForm(updatedUser));
onUserUpdated(updatedUser);
toast.success(t('general.saved'));
} catch {
// Do nothing since we only show error toasts, which is handled in the useApi hook
}
});
return (
@ -92,23 +102,45 @@ function UserSettings() {
description="user_details.settings_description"
learnMoreLink={getDocumentationUrl('/docs/references/users')}
>
{getValues('primaryEmail') && (
<FormField title="user_details.field_email">
<TextInput readOnly {...register('primaryEmail')} />
</FormField>
)}
{getValues('primaryPhone') && (
<FormField title="user_details.field_phone">
<TextInput readOnly {...register('primaryPhone')} />
</FormField>
)}
{getValues('username') && (
<FormField title="user_details.field_username">
<TextInput readOnly {...register('username')} />
</FormField>
)}
<FormField title="user_details.field_name">
<TextInput {...register('name')} />
<TextInput {...register('name')} placeholder={t('users.placeholder_name')} />
</FormField>
<FormField title="user_details.field_email">
<TextInput
{...register('primaryEmail', {
pattern: { value: emailRegEx, message: t('errors.email_pattern_error') },
})}
error={errors.primaryEmail?.message}
placeholder={t('users.placeholder_email')}
/>
</FormField>
<FormField title="user_details.field_phone">
<TextInput
{...register('primaryPhone', {
validate: (value) => {
if (!value) {
return true;
}
const parsed = trySafe(() => parsePhoneNumberWithError(value));
return (
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
parsed?.isValid() || t('errors.phone_pattern_error')
);
},
})}
error={errors.primaryPhone?.message}
placeholder={t('users.placeholder_phone')}
/>
</FormField>
<FormField title="user_details.field_username">
<TextInput
{...register('username', {
pattern: { value: usernameRegEx, message: t('errors.username_pattern_error') },
})}
error={errors.username?.message}
placeholder={t('users.placeholder_username')}
/>
</FormField>
<FormField title="user_details.field_avatar">
<TextInput

View file

@ -1,14 +1,20 @@
import type { User } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import { formatToInternationalPhoneNumber } from '@/utils/phone';
import type { UserDetailsForm } from './types';
export const userDetailsParser = {
toLocalForm: (data: User): UserDetailsForm => {
const { primaryEmail, primaryPhone, username, name, avatar, customData } = data;
const parsedPhoneNumber = conditional(
primaryPhone && formatToInternationalPhoneNumber(primaryPhone)
);
return {
primaryEmail: primaryEmail ?? '',
primaryPhone: primaryPhone ?? '',
primaryPhone: parsedPhoneNumber ?? primaryPhone ?? '',
username: username ?? '',
name: name ?? '',
avatar: avatar ?? '',

View file

@ -89,14 +89,18 @@ function CreateForm({ onClose, onCreate }: Props) {
Object.entries(userData).filter(([, value]) => Boolean(value))
);
const createdUser = await api.post('api/users', { json: payload }).json<User>();
try {
const createdUser = await api.post('api/users', { json: payload }).json<User>();
setCreatedUserInfo({
user: createdUser,
password,
});
setCreatedUserInfo({
user: createdUser,
password,
});
onCreate();
onCreate();
} catch {
// Do nothing since we only show error toasts, which is handled in the useApi hook
}
});
return createdUserInfo ? (

View file

@ -1,11 +1,23 @@
import { jsonGuard, type Json, jsonObjectGuard, type JsonObject } from '@logto/schemas';
import { t } from 'i18next';
export const safeParseJson = (
jsonString: string
): { success: true; data: unknown } | { success: false; error: string } => {
): { success: true; data: Json } | { success: false; error: string } => {
try {
// eslint-disable-next-line no-restricted-syntax
const data = JSON.parse(jsonString) as unknown;
const data = jsonGuard.parse(JSON.parse(jsonString));
return { success: true, data };
} catch {
return { success: false, error: t('admin_console.errors.invalid_json_format') };
}
};
export const safeParseJsonObject = (
jsonString: string
): { success: true; data: JsonObject } | { success: false; error: string } => {
try {
const data = jsonObjectGuard.parse(JSON.parse(jsonString));
return { success: true, data };
} catch {

View file

@ -1,2 +1,28 @@
export const parsePhoneNumber = (phone: string) =>
phone.replace(/[ ()-]/g, '').replace(/\+/g, '00');
import { parsePhoneNumberWithError } from 'libphonenumber-js';
/**
* Parse phone number to number string.
* E.g. +1 (650) 253-0000 -> 16502530000
*/
export const parsePhoneNumber = (phone: string) => {
try {
return parsePhoneNumberWithError(phone).number.slice(1);
} catch {
console.error(`Invalid phone number: ${phone}`);
return phone;
}
};
/**
* Parse phone number to readable international format.
* E.g. 16502530000 -> +1 650 253 0000
*/
export const formatToInternationalPhoneNumber = (phone: string) => {
try {
const phoneNumber = phone.startsWith('+') ? phone : `+${phone}`;
return parsePhoneNumberWithError(phoneNumber).formatInternational();
} catch {
console.error(`Invalid phone number: ${phone}`);
return phone;
}
};

View file

@ -1,9 +1,14 @@
import type { User } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import { formatToInternationalPhoneNumber } from './phone';
const getUserIdentity = (user: User) => {
const { primaryEmail, primaryPhone, username } = user;
return primaryEmail ?? primaryPhone ?? username;
const formattedPhoneNumber = conditional(
primaryPhone && formatToInternationalPhoneNumber(primaryPhone)
);
return primaryEmail ?? formattedPhoneNumber ?? username;
};
export const getUserTitle = (user: User) => user.name ?? getUserIdentity(user);

View file

@ -73,6 +73,8 @@ const user_details = {
search: 'Nach Rollennamen, Beschreibung oder ID suchen',
empty: 'Keine Rolle verfügbar',
},
warning_no_sign_in_identifier:
'Der Benutzer muss mindestens einen der Anmelde-Identifikatoren (Benutzername, E-Mail, Telefonnummer oder soziales Konto) haben, um sich anzumelden. Sind Sie sicher, dass Sie fortfahren möchten?',
};
export default user_details;

View file

@ -13,9 +13,10 @@ const users = {
create_form_username: 'Benutzername',
create_form_password: 'Passwort',
create_form_name: 'Name',
placeholder_email: 'ihremail@domain.com',
placeholder_username: 'Ihr Benutzername',
placeholder_phone: '+1 555-123-4567',
placeholder_name: 'Max Mustermann',
placeholder_email: 'max@example.com',
placeholder_username: 'user123',
placeholder_phone: '+49 (0)123 456789',
unnamed: 'Unbenannt',
search: 'Suche nach Name, E-Mail, Telefon oder Benutzername',
check_user_detail: 'Benutzerdetails überprüfen',

View file

@ -71,6 +71,8 @@ const user_details = {
search: 'Search by role name, description or ID',
empty: 'No role available',
},
warning_no_sign_in_identifier:
'User needs to have at least one of the sign-in identifiers (username, email, phone number or social) to sign in. Are you sure you want to continue?',
};
export default user_details;

View file

@ -12,9 +12,10 @@ const users = {
create_form_username: 'Username',
create_form_password: 'Password',
create_form_name: 'Full name',
placeholder_email: 'youremail@domain.com',
placeholder_username: 'Your username',
placeholder_phone: '+1 555-123-4567',
placeholder_name: 'John/Jane Doe',
placeholder_email: 'jdoe@example.com',
placeholder_username: 'user123',
placeholder_phone: '+1 (555) 555-5555',
unnamed: 'Unnamed',
search: 'Search by name, email, phone or username',
check_user_detail: 'Check user detail',

View file

@ -73,6 +73,8 @@ const user_details = {
search: 'Buscar por nombre de rol, descripción o ID',
empty: 'No hay roles disponibles',
},
warning_no_sign_in_identifier:
'El usuario necesita tener al menos uno de los identificadores de inicio de sesión (nombre de usuario, correo electrónico, número de teléfono o red social) para iniciar sesión. ¿Estás seguro/a de que quieres continuar?',
};
export default user_details;

View file

@ -12,9 +12,10 @@ const users = {
create_form_username: 'Nombre de usuario',
create_form_password: 'Contraseña',
create_form_name: 'Nombre completo',
placeholder_email: 'tucorreo@dominio.com',
placeholder_username: 'Tu nombre de usuario',
placeholder_phone: '+51 912 345 678',
placeholder_name: 'Fulano de Tal',
placeholder_email: 'fulano@example.com',
placeholder_username: 'user123',
placeholder_phone: '+34 123 456 789',
unnamed: 'Sin nombre',
search: 'Buscar por nombre, correo electrónico, teléfono o nombre de usuario',
check_user_detail: 'Ver detalles del usuario',

View file

@ -73,6 +73,8 @@ const user_details = {
search: 'Recherche par nom de rôle, description ou ID',
empty: 'Aucun rôle disponible',
},
warning_no_sign_in_identifier:
"L'utilisateur doit avoir au moins l'un des identifiants de connexion (nom d'utilisateur, e-mail, numéro de téléphone ou compte social) pour se connecter. Êtes-vous sûr(e) de vouloir continuer?",
};
export default user_details;

View file

@ -12,9 +12,10 @@ const users = {
create_form_username: "Nom d'utilisateur",
create_form_password: 'Mot de passe',
create_form_name: 'Nom complet',
placeholder_email: 'votreemail@domaine.com',
placeholder_username: "Votre nom d'utilisateur",
placeholder_phone: '+1 555-123-4567',
placeholder_name: 'Monsieur/Madame Dupont',
placeholder_email: 'mdupont@example.com',
placeholder_username: 'user123',
placeholder_phone: '+33 1 23 45 67 89',
unnamed: 'Sans nom',
search: "Rechercher par nom, email, téléphone ou nom d'utilisateur",
check_user_detail: "Vérifier les détails de l'utilisateur",

View file

@ -73,6 +73,8 @@ const user_details = {
search: 'Cerca per nome ruolo, descrizione o ID',
empty: 'Nessun ruolo disponibile',
},
warning_no_sign_in_identifier:
"L'utente deve avere almeno uno degli identificatori di accesso (nome utente, email, numero di telefono, o social) per accedere. Sei sicuro di voler continuare?",
};
export default user_details;

View file

@ -12,9 +12,10 @@ const users = {
create_form_username: 'Nome utente',
create_form_password: 'Password',
create_form_name: 'Nome completo',
placeholder_email: 'tuaemail@dominio.com',
placeholder_username: 'Il tuo nome utente',
placeholder_phone: '+39 123-456-7890',
placeholder_name: 'Mario Rossi',
placeholder_email: 'mario@example.com',
placeholder_username: 'user123',
placeholder_phone: '+39 02 1234567',
unnamed: 'Senza nome',
search: 'Cerca per nome, email, telefono o nome utente',
check_user_detail: "Controlla i dettagli dell'utente",

View file

@ -71,6 +71,8 @@ const user_details = {
search: 'ロール名、説明、またはIDで検索',
empty: '利用可能な役割はありません',
},
warning_no_sign_in_identifier:
'ユーザーは、サインインに少なくとも1つの識別子ユーザー名、メールアドレス、電話番号、またはソーシャルを持っている必要があります。続行してよろしいですか',
};
export default user_details;

View file

@ -13,9 +13,10 @@ const users = {
create_form_username: 'ユーザー名',
create_form_password: 'パスワード',
create_form_name: 'フルネーム',
placeholder_email: 'youremail@domain.com',
placeholder_username: 'あなたのユーザー名',
placeholder_phone: '+1 555-123-4567',
placeholder_name: '山田 太郎',
placeholder_email: 'taroyamada@example.com',
placeholder_username: 'user123',
placeholder_phone: '+81 3-1234-5678',
unnamed: '名前がありません',
search: '名前、メール、電話、またはユーザー名で検索',
check_user_detail: 'ユーザー詳細を確認する',

View file

@ -70,6 +70,8 @@ const user_details = {
search: '역할 이름, 설명, ID로 검색',
empty: '역할 없음',
},
warning_no_sign_in_identifier:
'사용자는 로그인 식별자(사용자 이름, 이메일, 전화 번호 또는 소셜) 중 적어도 하나를 갖고 로그인해야 합니다. 계속 하시겠습니까?',
};
export default user_details;

View file

@ -11,9 +11,10 @@ const users = {
create_form_username: '사용자 이름',
create_form_password: '비밀번호',
create_form_name: '이름',
placeholder_email: 'youremail@domain.com',
placeholder_username: '사용자 이름',
placeholder_phone: '+1 555-123-4567',
placeholder_name: '홍길동',
placeholder_email: 'honggildong@example.com',
placeholder_username: 'user123',
placeholder_phone: '+82 2-1234-5678',
unnamed: '이름없음',
search: '이름, 이메일, 전화번호, ID로 검색',
check_user_detail: '사용자 상세정보 확인',

View file

@ -70,6 +70,8 @@ const user_details = {
search: 'Szukaj po nazwie roli, opisie lub ID',
empty: 'Brak dostępnej roli',
},
warning_no_sign_in_identifier:
'Aby się zalogować, użytkownik musi mieć co najmniej jeden z identyfikatorów logowania (nazwa użytkownika, e-mail, numer telefonu lub konto społecznościowe). Czy na pewno chcesz kontynuować?',
};
export default user_details;

View file

@ -13,9 +13,10 @@ const users = {
create_form_username: 'Nazwa użytkownika',
create_form_password: 'Hasło',
create_form_name: 'Imię i nazwisko',
placeholder_email: 'twojemail@domena.com',
placeholder_username: 'Twoja nazwa użytkownika',
placeholder_phone: '+48 555-123-4567',
placeholder_name: 'Jan Kowalski',
placeholder_email: 'jkowalski@example.com',
placeholder_username: 'user123',
placeholder_phone: '+48 123 456 789',
unnamed: 'Bez nazwy',
search: 'Wyszukaj według nazwy, e-maila, numeru telefonu lub nazwy użytkownika',
check_user_detail: 'Sprawdź szczegóły użytkownika',

View file

@ -71,6 +71,8 @@ const user_details = {
search: 'Pesquisar por nome de função, descrição ou ID',
empty: 'Nenhuma função disponível',
},
warning_no_sign_in_identifier:
'O usuário precisa ter pelo menos um dos identificadores de login (nome de usuário, e-mail, número de telefone ou social) para fazer login. Tem certeza de que deseja continuar?',
};
export default user_details;

View file

@ -12,9 +12,10 @@ const users = {
create_form_username: 'Nome de usuário',
create_form_password: 'Senha',
create_form_name: 'Nome completo',
placeholder_email: 'seuemail@dominio.com',
placeholder_username: 'Seu nome de usuário',
placeholder_phone: '+55 11 1234-5678',
placeholder_name: 'João da Silva',
placeholder_email: 'jsilva@example.com',
placeholder_username: 'user123',
placeholder_phone: '+55 (11) 1234-5678',
unnamed: 'Sem nome',
search: 'Busca por nome, e-mail, telefone ou nome de usuário',
check_user_detail: 'Detalhes do usuário',

View file

@ -73,6 +73,8 @@ const user_details = {
search: 'Pesquisar pelo nome, descrição ou ID da função',
empty: 'Nenhuma função disponível',
},
warning_no_sign_in_identifier:
'O utilizador precisa de ter pelo menos um dos identificadores de início de sessão (nome de utilizador, e-mail, número de telefone, ou social) para iniciar sessão. Tem a certeza de que quer continuar?',
};
export default user_details;

View file

@ -12,9 +12,10 @@ const users = {
create_form_username: 'Utilizador',
create_form_password: 'Palavra-passe',
create_form_name: 'Nome completo',
placeholder_email: 'seuemail@dominio.com',
placeholder_username: 'Seu nome de utilizador',
placeholder_phone: '+1 555-123-4567',
placeholder_name: 'Zé Ninguém',
placeholder_email: 'zninguem@example.com',
placeholder_username: 'user123',
placeholder_phone: '+351 21 234 5678',
unnamed: 'Sem nome',
search: 'Procurar por nome, email, telefone ou nome de utilizador',
check_user_detail: 'Ver detalhes do utilizador',

View file

@ -71,6 +71,8 @@ const user_details = {
search: 'Поиск по названию роли, описанию или ID',
empty: 'Нет доступных ролей',
},
warning_no_sign_in_identifier:
'Пользователь должен иметь хотя бы один из идентификаторов входа (имя пользователя, электронная почта, номер телефона или социальная сеть), чтобы войти. Вы уверены, что хотите продолжить?',
};
export default user_details;

View file

@ -13,9 +13,10 @@ const users = {
create_form_username: 'Имя пользователя',
create_form_password: 'Пароль',
create_form_name: 'Полное имя',
placeholder_email: 'youremail@domain.com',
placeholder_username: 'Ваше имя пользователя',
placeholder_phone: '+1 555-123-4567',
placeholder_name: 'Иван Иванов',
placeholder_email: 'ivan@example.com',
placeholder_username: 'user123',
placeholder_phone: '+7 (123) 456-78-90',
unnamed: 'Без имени',
search: 'Поиск по имени, электронной почте, телефону или имени пользователя',
check_user_detail: 'Просмотреть информацию о пользователе',

View file

@ -71,6 +71,8 @@ const user_details = {
search: 'Rol adına, açıklamasına veya Kimliğine göre arama yapın',
empty: 'Uygun rol yok',
},
warning_no_sign_in_identifier:
'Kullanıcının giriş yapmak için en az bir oturum açma kimliği (kullanıcı adı, e-posta, telefon numarası, veya sosyal) olması gerekiyor. Devam etmek istediğinizden emin misiniz?',
};
export default user_details;

View file

@ -13,9 +13,10 @@ const users = {
create_form_username: 'Kullanıcı Adı',
create_form_password: 'Şifre',
create_form_name: 'Ad Soyad',
placeholder_email: 'youremail@domain.com',
placeholder_username: 'Kullanıcı adınız',
placeholder_phone: '+1 555-123-4567',
placeholder_name: 'Ali Veli',
placeholder_email: 'aveli@example.com',
placeholder_username: 'user123',
placeholder_phone: '+90 212 345 67 89',
unnamed: 'İsimsiz',
search: 'İsim, email, telefon veya kullanıcı adına göre arama',
check_user_detail: 'Kullanıcı detaylarını kontrol et',

View file

@ -67,6 +67,8 @@ const user_details = {
search: '按角色名称、描述或 ID 搜索',
empty: '无可用角色',
},
warning_no_sign_in_identifier:
'用户需要至少拥有一个登录标识(用户名、邮箱、手机号或社交账户)才能登录。确定要继续吗?',
};
export default user_details;

View file

@ -11,9 +11,10 @@ const users = {
create_form_username: '用户名',
create_form_password: '密码',
create_form_name: '姓名',
placeholder_email: 'youremail@domain.com',
placeholder_username: '你的用户名',
placeholder_phone: '+1 555-123-4567',
placeholder_name: '李雷',
placeholder_email: 'lilei@example.com',
placeholder_username: 'user123',
placeholder_phone: '+86 123 4567 8901',
unnamed: '未命名',
search: '按姓名、电子邮件、电话或用户名搜索',
check_user_detail: '查看用户详情',

View file

@ -67,6 +67,8 @@ const user_details = {
search: '按角色名稱、描述或 ID 搜索',
empty: '無可用角色',
},
warning_no_sign_in_identifier:
'用戶需要至少擁有一個登錄標識(用戶名、電子郵件、電話號碼或社交帳號)才能登錄。確定要繼續嗎?',
};
export default user_details;

View file

@ -11,9 +11,10 @@ const users = {
create_form_username: '用戶名',
create_form_password: '密碼',
create_form_name: '姓名',
placeholder_email: 'youremail@domain.com',
placeholder_username: '你的用戶名',
placeholder_phone: '+1 555-123-4567',
placeholder_name: '無名氏',
placeholder_email: 'moumingsi@example.com',
placeholder_username: 'user123',
placeholder_phone: '+852 9123 4567',
unnamed: '未命名',
search: '按姓名、電子郵件、電話或用戶名搜索',
check_user_detail: '查看用戶詳情',

View file

@ -67,6 +67,8 @@ const user_details = {
search: '按角色名稱、描述或 ID 搜索',
empty: '無可用角色',
},
warning_no_sign_in_identifier:
'使用者需要至少擁有一個登入標識(使用者名稱、電子郵件、電話號碼或社交帳號)才能登入。確定要繼續嗎?',
};
export default user_details;

View file

@ -11,9 +11,10 @@ const users = {
create_form_username: '用戶名',
create_form_password: '密碼',
create_form_name: '姓名',
placeholder_email: 'youremail@domain.com',
placeholder_username: '你的用戶名',
placeholder_phone: '+1 555-123-4567',
placeholder_name: '張三',
placeholder_email: 'example@example.com',
placeholder_username: 'user123',
placeholder_phone: '+886 2 1234 5678',
unnamed: '未命名',
search: '按姓名、電子郵件、電話或用戶名搜索',
check_user_detail: '查看用戶詳情',

View file

@ -7,14 +7,14 @@ export {
configurableConnectorMetadataGuard,
type ConfigurableConnectorMetadata,
} from '@logto/connector-kit';
export type { JsonObject } from '@withtyped/server';
export type { Json, JsonObject } from '@withtyped/server';
/* === Commonly Used === */
// Copied from https://github.com/colinhacks/zod#json-type
const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
const jsonGuard: z.ZodType<Json> = z.lazy(() =>
export const jsonGuard: z.ZodType<Json> = z.lazy(() =>
z.union([literalSchema, z.array(jsonGuard), z.record(jsonGuard)])
);

View file

@ -2936,6 +2936,9 @@ importers:
ky:
specifier: ^0.33.0
version: 0.33.0
libphonenumber-js:
specifier: ^1.9.49
version: 1.9.49
lint-staged:
specifier: ^13.0.0
version: 13.0.0