mirror of
https://github.com/pocket-id/pocket-id.git
synced 2025-12-20 17:25:43 +03:00
feat: add required indicator for required inputs (#993)
This commit is contained in:
@@ -27,7 +27,7 @@ type UserCreateDto struct {
|
|||||||
Email string `json:"email" binding:"required,email" unorm:"nfc"`
|
Email string `json:"email" binding:"required,email" unorm:"nfc"`
|
||||||
FirstName string `json:"firstName" binding:"required,min=1,max=50" unorm:"nfc"`
|
FirstName string `json:"firstName" binding:"required,min=1,max=50" unorm:"nfc"`
|
||||||
LastName string `json:"lastName" binding:"max=50" unorm:"nfc"`
|
LastName string `json:"lastName" binding:"max=50" unorm:"nfc"`
|
||||||
DisplayName string `json:"displayName" binding:"required,max=100" unorm:"nfc"`
|
DisplayName string `json:"displayName" binding:"required,min=1,max=100" unorm:"nfc"`
|
||||||
IsAdmin bool `json:"isAdmin"`
|
IsAdmin bool `json:"isAdmin"`
|
||||||
Locale *string `json:"locale"`
|
Locale *string `json:"locale"`
|
||||||
Disabled bool `json:"disabled"`
|
Disabled bool `json:"disabled"`
|
||||||
|
|||||||
@@ -355,6 +355,11 @@ func (s *LdapService) SyncUsers(ctx context.Context, tx *gorm.DB, client *ldap.C
|
|||||||
IsAdmin: isAdmin,
|
IsAdmin: isAdmin,
|
||||||
LdapID: ldapId,
|
LdapID: ldapId,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if newUser.DisplayName == "" {
|
||||||
|
newUser.DisplayName = strings.TrimSpace(newUser.FirstName + " " + newUser.LastName)
|
||||||
|
}
|
||||||
|
|
||||||
dto.Normalize(newUser)
|
dto.Normalize(newUser)
|
||||||
|
|
||||||
err = newUser.Validate()
|
err = newUser.Validate()
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
|
|
||||||
<div {...restProps}>
|
<div {...restProps}>
|
||||||
{#if label}
|
{#if label}
|
||||||
<Label class="mb-0" for={id}>{label}</Label>
|
<Label required={input?.required} class="mb-0" for={id}>{label}</Label>
|
||||||
{/if}
|
{/if}
|
||||||
{#if description}
|
{#if description}
|
||||||
<p class="text-muted-foreground mt-1 text-xs">
|
<p class="text-muted-foreground mt-1 text-xs">
|
||||||
|
|||||||
@@ -26,8 +26,7 @@
|
|||||||
<DropdownMenu.Label class="font-normal">
|
<DropdownMenu.Label class="font-normal">
|
||||||
<div class="flex flex-col space-y-1">
|
<div class="flex flex-col space-y-1">
|
||||||
<p class="text-sm leading-none font-medium">
|
<p class="text-sm leading-none font-medium">
|
||||||
{$userStore?.firstName}
|
{$userStore?.displayName}
|
||||||
{$userStore?.lastName}
|
|
||||||
</p>
|
</p>
|
||||||
<p class="text-muted-foreground text-xs leading-none">{$userStore?.email}</p>
|
<p class="text-muted-foreground text-xs leading-none">{$userStore?.email}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,16 +5,25 @@
|
|||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
|
required = false,
|
||||||
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: LabelPrimitive.RootProps = $props();
|
}: LabelPrimitive.RootProps & {
|
||||||
|
required?: boolean;
|
||||||
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<LabelPrimitive.Root
|
<LabelPrimitive.Root
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="label"
|
data-slot="label"
|
||||||
class={cn(
|
class={cn(
|
||||||
'mb-3 flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
|
'mb-3 flex items-center gap-1.5 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
|
||||||
|
required && "after:text-[14px] after:text-red-500 after:content-['*']",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
>
|
||||||
|
{#if children}
|
||||||
|
{@render children()}
|
||||||
|
{/if}
|
||||||
|
</LabelPrimitive.Root>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { z } from 'zod/v4';
|
|||||||
export type FormInput<T> = {
|
export type FormInput<T> = {
|
||||||
value: T;
|
value: T;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
|
required: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type FormInputs<T> = {
|
type FormInputs<T> = {
|
||||||
@@ -17,11 +18,18 @@ export function createForm<T extends z.ZodType<any, any>>(schema: T, initialValu
|
|||||||
|
|
||||||
function initializeInputs(initialValues: z.infer<T>): FormInputs<z.infer<T>> {
|
function initializeInputs(initialValues: z.infer<T>): FormInputs<z.infer<T>> {
|
||||||
const inputs: FormInputs<z.infer<T>> = {} as FormInputs<z.infer<T>>;
|
const inputs: FormInputs<z.infer<T>> = {} as FormInputs<z.infer<T>>;
|
||||||
|
|
||||||
|
const shape =
|
||||||
|
schema instanceof z.ZodObject ? (schema.shape as Record<string, z.ZodTypeAny>) : {};
|
||||||
|
|
||||||
for (const key in initialValues) {
|
for (const key in initialValues) {
|
||||||
if (Object.prototype.hasOwnProperty.call(initialValues, key)) {
|
if (Object.prototype.hasOwnProperty.call(initialValues, key)) {
|
||||||
|
const fieldSchema = shape[key];
|
||||||
|
|
||||||
inputs[key as keyof z.infer<T>] = {
|
inputs[key as keyof z.infer<T>] = {
|
||||||
value: initialValues[key as keyof z.infer<T>],
|
value: initialValues[key as keyof z.infer<T>],
|
||||||
error: null
|
error: null,
|
||||||
|
required: fieldSchema ? isRequired(fieldSchema) : false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -31,7 +39,6 @@ export function createForm<T extends z.ZodType<any, any>>(schema: T, initialValu
|
|||||||
function validate() {
|
function validate() {
|
||||||
let success = true;
|
let success = true;
|
||||||
inputsStore.update((inputs) => {
|
inputsStore.update((inputs) => {
|
||||||
// Extract values from inputs to validate against the schema
|
|
||||||
const values = Object.fromEntries(
|
const values = Object.fromEntries(
|
||||||
Object.entries(inputs).map(([key, input]) => [key, input.value])
|
Object.entries(inputs).map(([key, input]) => [key, input.value])
|
||||||
);
|
);
|
||||||
@@ -54,7 +61,7 @@ export function createForm<T extends z.ZodType<any, any>>(schema: T, initialValu
|
|||||||
inputs[input as keyof z.infer<T>].error = null;
|
inputs[input as keyof z.infer<T>].error = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Update the input values with the parsed data
|
|
||||||
for (const key in result.data) {
|
for (const key in result.data) {
|
||||||
if (Object.prototype.hasOwnProperty.call(inputs, key)) {
|
if (Object.prototype.hasOwnProperty.call(inputs, key)) {
|
||||||
inputs[key as keyof z.infer<T>].value = result.data[key];
|
inputs[key as keyof z.infer<T>].value = result.data[key];
|
||||||
@@ -82,7 +89,9 @@ export function createForm<T extends z.ZodType<any, any>>(schema: T, initialValu
|
|||||||
function reset() {
|
function reset() {
|
||||||
inputsStore.update((inputs) => {
|
inputsStore.update((inputs) => {
|
||||||
for (const input of Object.keys(inputs)) {
|
for (const input of Object.keys(inputs)) {
|
||||||
|
const current = inputs[input as keyof z.infer<T>];
|
||||||
inputs[input as keyof z.infer<T>] = {
|
inputs[input as keyof z.infer<T>] = {
|
||||||
|
...current,
|
||||||
value: initialValues[input as keyof z.infer<T>],
|
value: initialValues[input as keyof z.infer<T>],
|
||||||
error: null
|
error: null
|
||||||
};
|
};
|
||||||
@@ -98,7 +107,6 @@ export function createForm<T extends z.ZodType<any, any>>(schema: T, initialValu
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trims whitespace from string values and arrays of strings
|
|
||||||
function trimValue(value: any) {
|
function trimValue(value: any) {
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
value = value.trim();
|
value = value.trim();
|
||||||
@@ -113,6 +121,26 @@ export function createForm<T extends z.ZodType<any, any>>(schema: T, initialValu
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isRequired(fieldSchema: z.ZodTypeAny): boolean {
|
||||||
|
// Handle unions like callbackUrlSchema
|
||||||
|
if (fieldSchema instanceof z.ZodUnion) {
|
||||||
|
return !fieldSchema.def.options.some((o: any) => {
|
||||||
|
return o.def.type == 'optional';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle pipes like emptyToUndefined
|
||||||
|
if (fieldSchema instanceof z.ZodPipe) {
|
||||||
|
return isRequired(fieldSchema.def.out as z.ZodTypeAny);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle the normal cases
|
||||||
|
if (fieldSchema instanceof z.ZodOptional || fieldSchema instanceof z.ZodDefault) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
schema,
|
schema,
|
||||||
inputs: inputsStore,
|
inputs: inputsStore,
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
firstName: z.string().min(1).max(50),
|
firstName: z.string().min(1).max(50),
|
||||||
lastName: emptyToUndefined(z.string().max(50).optional()),
|
lastName: emptyToUndefined(z.string().max(50).optional()),
|
||||||
displayName: z.string().max(100),
|
displayName: z.string().min(1).max(100),
|
||||||
username: usernameSchema,
|
username: usernameSchema,
|
||||||
email: z.email(),
|
email: z.email(),
|
||||||
isAdmin: z.boolean()
|
isAdmin: z.boolean()
|
||||||
|
|||||||
@@ -37,13 +37,13 @@
|
|||||||
ldapAttributeUserUsername: z.string().min(1),
|
ldapAttributeUserUsername: z.string().min(1),
|
||||||
ldapAttributeUserEmail: z.string().min(1),
|
ldapAttributeUserEmail: z.string().min(1),
|
||||||
ldapAttributeUserFirstName: z.string().min(1),
|
ldapAttributeUserFirstName: z.string().min(1),
|
||||||
ldapAttributeUserLastName: z.string().min(1),
|
ldapAttributeUserLastName: z.string().optional(),
|
||||||
ldapAttributeUserDisplayName: z.string().min(1),
|
ldapAttributeUserDisplayName: z.string().optional(),
|
||||||
ldapAttributeUserProfilePicture: z.string(),
|
ldapAttributeUserProfilePicture: z.string().optional(),
|
||||||
ldapAttributeGroupMember: z.string(),
|
ldapAttributeGroupMember: z.string().optional(),
|
||||||
ldapAttributeGroupUniqueIdentifier: z.string().min(1),
|
ldapAttributeGroupUniqueIdentifier: z.string().min(1),
|
||||||
ldapAttributeGroupName: z.string().min(1),
|
ldapAttributeGroupName: z.string().min(1),
|
||||||
ldapAttributeAdminGroup: z.string(),
|
ldapAttributeAdminGroup: z.string().optional(),
|
||||||
ldapSoftDeleteUsers: z.boolean()
|
ldapSoftDeleteUsers: z.boolean()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,7 @@
|
|||||||
|
|
||||||
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
|
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
<Label for="issuer-{i}" class="text-xs">Issuer (Required)</Label>
|
<Label required for="issuer-{i}" class="text-xs">Issuer</Label>
|
||||||
<Input
|
<Input
|
||||||
id="issuer-{i}"
|
id="issuer-{i}"
|
||||||
placeholder="https://example.com/"
|
placeholder="https://example.com/"
|
||||||
@@ -96,10 +96,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label for="subject-{i}" class="text-xs">Subject (Optional)</Label>
|
<Label for="subject-{i}" class="text-xs">Subject</Label>
|
||||||
<Input
|
<Input
|
||||||
id="subject-{i}"
|
id="subject-{i}"
|
||||||
placeholder="Defaults to the client ID: {client?.id}"
|
placeholder="Defaults to the client ID"
|
||||||
value={identity.subject || ''}
|
value={identity.subject || ''}
|
||||||
oninput={(e) => updateFederatedIdentity(i, 'subject', e.currentTarget.value)}
|
oninput={(e) => updateFederatedIdentity(i, 'subject', e.currentTarget.value)}
|
||||||
aria-invalid={!!getFieldError(i, 'subject')}
|
aria-invalid={!!getFieldError(i, 'subject')}
|
||||||
@@ -110,7 +110,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label for="audience-{i}" class="text-xs">Audience (Optional)</Label>
|
<Label for="audience-{i}" class="text-xs">Audience</Label>
|
||||||
<Input
|
<Input
|
||||||
id="audience-{i}"
|
id="audience-{i}"
|
||||||
placeholder="Defaults to the Pocket ID URL"
|
placeholder="Defaults to the Pocket ID URL"
|
||||||
@@ -124,7 +124,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label for="jwks-{i}" class="text-xs">JWKS URL (Optional)</Label>
|
<Label for="jwks-{i}" class="text-xs">JWKS URL</Label>
|
||||||
<Input
|
<Input
|
||||||
id="jwks-{i}"
|
id="jwks-{i}"
|
||||||
placeholder="Defaults to {identity.issuer || '<issuer>'}/.well-known/jwks.json"
|
placeholder="Defaults to {identity.issuer || '<issuer>'}/.well-known/jwks.json"
|
||||||
|
|||||||
@@ -100,7 +100,7 @@
|
|||||||
<Dialog.Title>{m.oidc_data_preview()}</Dialog.Title>
|
<Dialog.Title>{m.oidc_data_preview()}</Dialog.Title>
|
||||||
<Dialog.Description>
|
<Dialog.Description>
|
||||||
{#if user}
|
{#if user}
|
||||||
{m.preview_for_user({ name: user.firstName + ' ' + user.lastName, email: user.email })}
|
{m.preview_for_user({ name: user.displayName, email: user.email })}
|
||||||
{:else}
|
{:else}
|
||||||
{m.preview_the_oidc_data_that_would_be_sent_for_this_user()}
|
{m.preview_the_oidc_data_that_would_be_sent_for_this_user()}
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
{selectionDisabled}
|
{selectionDisabled}
|
||||||
>
|
>
|
||||||
{#snippet rows({ item })}
|
{#snippet rows({ item })}
|
||||||
<Table.Cell>{item.firstName} {item.lastName}</Table.Cell>
|
<Table.Cell>{item.displayName}</Table.Cell>
|
||||||
<Table.Cell>{item.email}</Table.Cell>
|
<Table.Cell>{item.email}</Table.Cell>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</AdvancedTable>
|
</AdvancedTable>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
firstName: z.string().min(1).max(50),
|
firstName: z.string().min(1).max(50),
|
||||||
lastName: emptyToUndefined(z.string().max(50).optional()),
|
lastName: emptyToUndefined(z.string().max(50).optional()),
|
||||||
displayName: z.string().max(100),
|
displayName: z.string().min(1).max(100),
|
||||||
username: usernameSchema,
|
username: usernameSchema,
|
||||||
email: z.email(),
|
email: z.email(),
|
||||||
isAdmin: z.boolean(),
|
isAdmin: z.boolean(),
|
||||||
|
|||||||
Reference in New Issue
Block a user