mirror of
https://github.com/pocket-id/pocket-id.git
synced 2025-12-10 15:12:58 +03:00
feat: add user display name field (#898)
Co-authored-by: Elias Schneider <login@eliasschneider.com>
This commit is contained in:
@@ -443,7 +443,10 @@
|
||||
"custom_client_id_description": "Set a custom client ID if this is required by your application. Otherwise, leave it blank to generate a random one.",
|
||||
"generated": "Generated",
|
||||
"administration": "Administration",
|
||||
"group_rdn_attribute_description": "The attribute used in the groups distinguished name (DN). Recommended value: `cn`",
|
||||
"group_rdn_attribute_description": "The attribute used in the groups distinguished name (DN).",
|
||||
"display_name_attribute": "Display Name Attribute",
|
||||
"display_name": "Display Name",
|
||||
"configure_application_images": "Configure Application Images",
|
||||
"ui_config_disabled_info_title": "UI Configuration Disabled",
|
||||
"ui_config_disabled_info_description": "The UI configuration is disabled because the application configuration settings are managed through environment variables. Some settings may not be editable."
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { preventDefault } from '$lib/utils/event-util';
|
||||
import { createForm } from '$lib/utils/form-util';
|
||||
import { tryCatch } from '$lib/utils/try-catch-util';
|
||||
import { emptyToUndefined } from '$lib/utils/zod-util';
|
||||
import { z } from 'zod/v4';
|
||||
|
||||
let {
|
||||
@@ -24,7 +25,7 @@
|
||||
|
||||
const formSchema = z.object({
|
||||
firstName: z.string().min(1).max(50),
|
||||
lastName: z.string().max(50).optional(),
|
||||
lastName: emptyToUndefined(z.string().max(50).optional()),
|
||||
username: z
|
||||
.string()
|
||||
.min(2)
|
||||
|
||||
@@ -41,6 +41,7 @@ export type AllAppConfig = AppConfig & {
|
||||
ldapAttributeUserEmail: string;
|
||||
ldapAttributeUserFirstName: string;
|
||||
ldapAttributeUserLastName: string;
|
||||
ldapAttributeUserDisplayName: string;
|
||||
ldapAttributeUserProfilePicture: string;
|
||||
ldapAttributeGroupMember: string;
|
||||
ldapAttributeGroupUniqueIdentifier: string;
|
||||
|
||||
@@ -8,6 +8,7 @@ export type User = {
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName?: string;
|
||||
displayName: string;
|
||||
isAdmin: boolean;
|
||||
userGroups: UserGroup[];
|
||||
customClaims: CustomClaim[];
|
||||
@@ -18,6 +19,6 @@ export type User = {
|
||||
|
||||
export type UserCreate = Omit<User, 'id' | 'customClaims' | 'ldapId' | 'userGroups'>;
|
||||
|
||||
export type UserSignUp = Omit<UserCreate, 'isAdmin' | 'disabled'> & {
|
||||
export type UserSignUp = Omit<UserCreate, 'isAdmin' | 'disabled' | 'displayName'> & {
|
||||
token?: string;
|
||||
};
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
import { setLocale as setParaglideLocale, type Locale } from '$lib/paraglide/runtime';
|
||||
import {
|
||||
extractLocaleFromCookie,
|
||||
setLocale as setParaglideLocale,
|
||||
type Locale
|
||||
} from '$lib/paraglide/runtime';
|
||||
import { setDefaultOptions } from 'date-fns';
|
||||
import { z } from 'zod/v4';
|
||||
|
||||
export async function setLocale(locale: Locale, reload = true) {
|
||||
await setLocaleForLibraries(locale);
|
||||
setParaglideLocale(locale, { reload });
|
||||
}
|
||||
|
||||
export async function setLocaleForLibraries(
|
||||
locale: Locale = (extractLocaleFromCookie() as Locale) || 'en'
|
||||
) {
|
||||
const [zodResult, dateFnsResult] = await Promise.allSettled([
|
||||
import(`../../../node_modules/zod/v4/locales/${locale}.js`),
|
||||
import(`../../../node_modules/date-fns/locale/${locale}.js`)
|
||||
@@ -14,8 +25,6 @@ export async function setLocale(locale: Locale, reload = true) {
|
||||
console.warn(`Failed to load zod locale for ${locale}:`, zodResult.reason);
|
||||
}
|
||||
|
||||
setParaglideLocale(locale, { reload });
|
||||
|
||||
if (dateFnsResult.status === 'fulfilled') {
|
||||
setDefaultOptions({
|
||||
locale: dateFnsResult.value.default
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import z from 'zod/v4';
|
||||
import { z } from 'zod/v4';
|
||||
|
||||
export const emptyToUndefined = <T>(validation: z.ZodType<T>) =>
|
||||
z.preprocess((v) => (v === '' ? undefined : v), validation);
|
||||
z.preprocess((v) => (v === '' ? undefined : v), validation.optional());
|
||||
|
||||
export const optionalUrl = z
|
||||
.url()
|
||||
|
||||
@@ -2,6 +2,7 @@ import AppConfigService from '$lib/services/app-config-service';
|
||||
import UserService from '$lib/services/user-service';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||
import userStore from '$lib/stores/user-store';
|
||||
import { setLocaleForLibraries } from '$lib/utils/locale.util';
|
||||
import type { LayoutLoad } from './$types';
|
||||
|
||||
export const ssr = false;
|
||||
@@ -29,6 +30,8 @@ export const load: LayoutLoad = async () => {
|
||||
appConfigStore.set(appConfig);
|
||||
}
|
||||
|
||||
await setLocaleForLibraries();
|
||||
|
||||
return {
|
||||
user,
|
||||
appConfig
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||
import { preventDefault } from '$lib/utils/event-util';
|
||||
import { createForm } from '$lib/utils/form-util';
|
||||
import { emptyToUndefined } from '$lib/utils/zod-util';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { z } from 'zod/v4';
|
||||
|
||||
@@ -26,12 +27,14 @@
|
||||
} = $props();
|
||||
|
||||
let isLoading = $state(false);
|
||||
let hasManualDisplayNameEdit = $state(!!account.displayName);
|
||||
|
||||
const userService = new UserService();
|
||||
|
||||
const formSchema = z.object({
|
||||
firstName: z.string().min(1).max(50),
|
||||
lastName: z.string().max(50).optional(),
|
||||
lastName: emptyToUndefined(z.string().max(50).optional()),
|
||||
displayName: z.string().max(100),
|
||||
username: z
|
||||
.string()
|
||||
.min(2)
|
||||
@@ -44,6 +47,14 @@
|
||||
|
||||
const { inputs, ...form } = createForm<FormSchema>(formSchema, account);
|
||||
|
||||
function onNameInput() {
|
||||
if (!hasManualDisplayNameEdit) {
|
||||
$inputs.displayName.value = `${$inputs.firstName.value}${
|
||||
$inputs.lastName?.value ? ' ' + $inputs.lastName.value : ''
|
||||
}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
const data = form.validate();
|
||||
if (!data) return;
|
||||
@@ -68,7 +79,6 @@
|
||||
</script>
|
||||
|
||||
<form onsubmit={preventDefault(onSubmit)} class="space-y-6">
|
||||
<!-- Profile Picture Section -->
|
||||
<ProfilePictureSettings
|
||||
{userId}
|
||||
{isLdapUser}
|
||||
@@ -76,31 +86,32 @@
|
||||
resetCallback={resetProfilePicture}
|
||||
/>
|
||||
|
||||
<!-- Divider -->
|
||||
<hr class="border-border" />
|
||||
|
||||
<!-- User Information -->
|
||||
<fieldset disabled={userInfoInputDisabled}>
|
||||
<div>
|
||||
<div class="flex flex-col gap-3 sm:flex-row">
|
||||
<div class="w-full">
|
||||
<FormInput label={m.first_name()} bind:input={$inputs.firstName} />
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<FormInput label={m.last_name()} bind:input={$inputs.lastName} />
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<div>
|
||||
<FormInput label={m.first_name()} bind:input={$inputs.firstName} onInput={onNameInput} />
|
||||
</div>
|
||||
<div class="mt-3 flex flex-col gap-3 sm:flex-row">
|
||||
<div class="w-full">
|
||||
<FormInput label={m.email()} bind:input={$inputs.email} />
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<FormInput label={m.username()} bind:input={$inputs.username} />
|
||||
</div>
|
||||
<div>
|
||||
<FormInput label={m.last_name()} bind:input={$inputs.lastName} onInput={onNameInput} />
|
||||
</div>
|
||||
<div>
|
||||
<FormInput
|
||||
label={m.display_name()}
|
||||
bind:input={$inputs.displayName}
|
||||
onInput={() => (hasManualDisplayNameEdit = true)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<FormInput label={m.username()} bind:input={$inputs.username} />
|
||||
</div>
|
||||
<div>
|
||||
<FormInput label={m.email()} bind:input={$inputs.email} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-2">
|
||||
<div class="flex justify-end pt-4">
|
||||
<Button {isLoading} type="submit">{m.save()}</Button>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
@@ -120,7 +120,12 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<CollapsibleCard id="application-configuration-images" icon={LucideImage} title={m.images()}>
|
||||
<CollapsibleCard
|
||||
id="application-configuration-images"
|
||||
icon={LucideImage}
|
||||
title={m.images()}
|
||||
description={m.configure_application_images()}
|
||||
>
|
||||
<UpdateApplicationImages callback={updateImages} />
|
||||
</CollapsibleCard>
|
||||
</div>
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
ldapAttributeUserEmail: z.string().min(1),
|
||||
ldapAttributeUserFirstName: z.string().min(1),
|
||||
ldapAttributeUserLastName: z.string().min(1),
|
||||
ldapAttributeUserDisplayName: z.string().min(1),
|
||||
ldapAttributeUserProfilePicture: z.string(),
|
||||
ldapAttributeGroupMember: z.string(),
|
||||
ldapAttributeGroupUniqueIdentifier: z.string().min(1),
|
||||
@@ -159,6 +160,11 @@
|
||||
placeholder="sn"
|
||||
bind:input={$inputs.ldapAttributeUserLastName}
|
||||
/>
|
||||
<FormInput
|
||||
label={m.display_name_attribute()}
|
||||
placeholder="displayName"
|
||||
bind:input={$inputs.ldapAttributeUserDisplayName}
|
||||
/>
|
||||
<FormInput
|
||||
label={m.user_profile_picture_attribute()}
|
||||
description={m.the_value_of_this_attribute_can_either_be_a_url_binary_or_base64_encoded_image()}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<script lang="ts">
|
||||
import SwitchWithLabel from '$lib/components/form/switch-with-label.svelte';
|
||||
import FormInput from '$lib/components/form/form-input.svelte';
|
||||
import SwitchWithLabel from '$lib/components/form/switch-with-label.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||
import type { User, UserCreate } from '$lib/types/user.type';
|
||||
import { preventDefault } from '$lib/utils/event-util';
|
||||
import { createForm } from '$lib/utils/form-util';
|
||||
import { emptyToUndefined } from '$lib/utils/zod-util';
|
||||
import { z } from 'zod/v4';
|
||||
|
||||
let {
|
||||
@@ -19,10 +20,12 @@
|
||||
|
||||
let isLoading = $state(false);
|
||||
let inputDisabled = $derived(!!existingUser?.ldapId && $appConfigStore.ldapEnabled);
|
||||
let hasManualDisplayNameEdit = $state(!!existingUser?.displayName);
|
||||
|
||||
const user = {
|
||||
firstName: existingUser?.firstName || '',
|
||||
lastName: existingUser?.lastName || '',
|
||||
displayName: existingUser?.displayName || '',
|
||||
email: existingUser?.email || '',
|
||||
username: existingUser?.username || '',
|
||||
isAdmin: existingUser?.isAdmin || false,
|
||||
@@ -31,7 +34,8 @@
|
||||
|
||||
const formSchema = z.object({
|
||||
firstName: z.string().min(1).max(50),
|
||||
lastName: z.string().max(50),
|
||||
lastName: emptyToUndefined(z.string().max(50).optional()),
|
||||
displayName: z.string().max(100),
|
||||
username: z
|
||||
.string()
|
||||
.min(2)
|
||||
@@ -53,15 +57,29 @@
|
||||
if (success && !existingUser) form.reset();
|
||||
isLoading = false;
|
||||
}
|
||||
function onNameInput() {
|
||||
if (!hasManualDisplayNameEdit) {
|
||||
$inputs.displayName.value = `${$inputs.firstName.value}${
|
||||
$inputs.lastName?.value ? ' ' + $inputs.lastName.value : ''
|
||||
}`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<form onsubmit={preventDefault(onSubmit)}>
|
||||
<fieldset disabled={inputDisabled}>
|
||||
<div class="grid grid-cols-1 items-start gap-5 md:grid-cols-2">
|
||||
<FormInput label={m.first_name()} bind:input={$inputs.firstName} />
|
||||
<FormInput label={m.last_name()} bind:input={$inputs.lastName} />
|
||||
<FormInput label={m.first_name()} oninput={onNameInput} bind:input={$inputs.firstName} />
|
||||
<FormInput label={m.last_name()} oninput={onNameInput} bind:input={$inputs.lastName} />
|
||||
<FormInput
|
||||
label={m.display_name()}
|
||||
oninput={() => (hasManualDisplayNameEdit = true)}
|
||||
bind:input={$inputs.displayName}
|
||||
/>
|
||||
<FormInput label={m.username()} bind:input={$inputs.username} />
|
||||
<FormInput label={m.email()} bind:input={$inputs.email} />
|
||||
</div>
|
||||
<div class="mt-5 grid grid-cols-1 items-start gap-5 md:grid-cols-2">
|
||||
<SwitchWithLabel
|
||||
id="admin-privileges"
|
||||
label={m.admin_privileges()}
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
columns={[
|
||||
{ label: m.first_name(), sortColumn: 'firstName' },
|
||||
{ label: m.last_name(), sortColumn: 'lastName' },
|
||||
{ label: m.display_name(), sortColumn: 'displayName' },
|
||||
{ label: m.email(), sortColumn: 'email' },
|
||||
{ label: m.username(), sortColumn: 'username' },
|
||||
{ label: m.role(), sortColumn: 'isAdmin' },
|
||||
@@ -114,6 +115,7 @@
|
||||
{#snippet rows({ item })}
|
||||
<Table.Cell>{item.firstName}</Table.Cell>
|
||||
<Table.Cell>{item.lastName}</Table.Cell>
|
||||
<Table.Cell>{item.displayName}</Table.Cell>
|
||||
<Table.Cell>{item.email}</Table.Cell>
|
||||
<Table.Cell>{item.username}</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
Reference in New Issue
Block a user