mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-02-14 18:12:31 +00:00
feat(signup): add default user groups and claims for new users (#812)
Co-authored-by: Kyle Mendell <kmendell@ofkm.us> Co-authored-by: Elias Schneider <login@eliasschneider.com>
This commit is contained in:
140
frontend/src/lib/components/form/searchable-multi-select.svelte
Normal file
140
frontend/src/lib/components/form/searchable-multi-select.svelte
Normal file
@@ -0,0 +1,140 @@
|
||||
<script lang="ts">
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Command from '$lib/components/ui/command';
|
||||
import * as Popover from '$lib/components/ui/popover';
|
||||
import { cn } from '$lib/utils/style';
|
||||
import { LoaderCircle, LucideCheck, LucideChevronDown } from '@lucide/svelte';
|
||||
import type { FormEventHandler } from 'svelte/elements';
|
||||
|
||||
type Item = {
|
||||
value: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
let {
|
||||
items,
|
||||
selectedItems = $bindable(),
|
||||
onSelect,
|
||||
oninput,
|
||||
isLoading = false,
|
||||
placeholder = 'Select items...',
|
||||
searchText = 'Search...',
|
||||
noItemsText = 'No items found.',
|
||||
disableInternalSearch = false,
|
||||
id
|
||||
}: {
|
||||
items: Item[];
|
||||
selectedItems: string[];
|
||||
onSelect?: (value: string[]) => void;
|
||||
oninput?: FormEventHandler<HTMLInputElement>;
|
||||
isLoading?: boolean;
|
||||
placeholder?: string;
|
||||
searchText?: string;
|
||||
noItemsText?: string;
|
||||
disableInternalSearch?: boolean;
|
||||
id?: string;
|
||||
} = $props();
|
||||
|
||||
let open = $state(false);
|
||||
let searchValue = $state('');
|
||||
let filteredItems = $state(items);
|
||||
|
||||
const selectedLabels = $derived(
|
||||
items.filter((item) => selectedItems.includes(item.value)).map((item) => item.label)
|
||||
);
|
||||
|
||||
function handleItemSelect(value: string) {
|
||||
let newSelectedItems: string[];
|
||||
if (selectedItems.includes(value)) {
|
||||
newSelectedItems = selectedItems.filter((item) => item !== value);
|
||||
} else {
|
||||
newSelectedItems = [...selectedItems, value];
|
||||
}
|
||||
selectedItems = newSelectedItems;
|
||||
onSelect?.(newSelectedItems);
|
||||
}
|
||||
|
||||
function filterItems(search: string) {
|
||||
if (disableInternalSearch) return;
|
||||
searchValue = search;
|
||||
if (!search) {
|
||||
filteredItems = items;
|
||||
} else {
|
||||
filteredItems = items.filter((item) =>
|
||||
item.label.toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset search value when the popover is closed
|
||||
$effect(() => {
|
||||
if (!open) {
|
||||
filterItems('');
|
||||
}
|
||||
|
||||
filteredItems = items;
|
||||
});
|
||||
</script>
|
||||
|
||||
<Popover.Root bind:open>
|
||||
<Popover.Trigger {id}>
|
||||
{#snippet child({ props })}
|
||||
<Button
|
||||
{...props}
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
class="h-auto min-h-10 w-full justify-between"
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-1">
|
||||
{#if selectedItems.length > 0}
|
||||
{#each selectedLabels as label}
|
||||
<Badge variant="secondary">{label}</Badge>
|
||||
{/each}
|
||||
{:else}
|
||||
<span class="text-muted-foreground font-normal">{placeholder}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<LucideChevronDown class="ml-2 size-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
{/snippet}
|
||||
</Popover.Trigger>
|
||||
<Popover.Content class="p-0" sameWidth>
|
||||
<Command.Root shouldFilter={false}>
|
||||
<Command.Input
|
||||
placeholder={searchText}
|
||||
value={searchValue}
|
||||
oninput={(e) => {
|
||||
filterItems(e.currentTarget.value);
|
||||
oninput?.(e);
|
||||
}}
|
||||
/>
|
||||
<Command.Empty>
|
||||
{#if isLoading}
|
||||
<div class="flex w-full items-center justify-center py-2">
|
||||
<LoaderCircle class="size-4 animate-spin" />
|
||||
</div>
|
||||
{:else}
|
||||
{noItemsText}
|
||||
{/if}
|
||||
</Command.Empty>
|
||||
<Command.Group class="max-h-60 overflow-y-auto">
|
||||
{#each filteredItems as item}
|
||||
<Command.Item
|
||||
aria-checked={selectedItems.includes(item.value)}
|
||||
value={item.value}
|
||||
onSelect={() => {
|
||||
handleItemSelect(item.value);
|
||||
}}
|
||||
>
|
||||
<LucideCheck
|
||||
class={cn('mr-2 size-4', !selectedItems.includes(item.value) && 'text-transparent')}
|
||||
/>
|
||||
{item.label}
|
||||
</Command.Item>
|
||||
{/each}
|
||||
</Command.Group>
|
||||
</Command.Root>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
@@ -14,10 +14,15 @@ export default class AppConfigService extends APIService {
|
||||
}
|
||||
|
||||
async update(appConfig: AllAppConfig) {
|
||||
// Convert all values to string
|
||||
const appConfigConvertedToString = {};
|
||||
// Convert all values to string, stringifying JSON where needed
|
||||
const appConfigConvertedToString: Record<string, string> = {};
|
||||
for (const key in appConfig) {
|
||||
(appConfigConvertedToString as any)[key] = (appConfig as any)[key].toString();
|
||||
const value = (appConfig as any)[key];
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
appConfigConvertedToString[key] = JSON.stringify(value);
|
||||
} else {
|
||||
appConfigConvertedToString[key] = String(value);
|
||||
}
|
||||
}
|
||||
const res = await this.api.put('/application-configuration', appConfigConvertedToString);
|
||||
return this.parseConfigList(res.data);
|
||||
@@ -66,6 +71,16 @@ export default class AppConfigService extends APIService {
|
||||
}
|
||||
|
||||
private parseValue(value: string) {
|
||||
// Try to parse JSON first
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
if (typeof parsed === 'object' && parsed !== null) {
|
||||
return parsed;
|
||||
}
|
||||
value = String(parsed);
|
||||
} catch {}
|
||||
|
||||
// Handle rest of the types
|
||||
if (value === 'true') {
|
||||
return true;
|
||||
} else if (value === 'false') {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { CustomClaim } from './custom-claim.type';
|
||||
|
||||
export type AppConfig = {
|
||||
appName: string;
|
||||
allowOwnAccountEdit: boolean;
|
||||
@@ -14,6 +16,8 @@ export type AllAppConfig = AppConfig & {
|
||||
// General
|
||||
sessionDuration: number;
|
||||
emailsVerified: boolean;
|
||||
signupDefaultUserGroupIDs: string[];
|
||||
signupDefaultCustomClaims: CustomClaim[];
|
||||
// Email
|
||||
smtpHost: string;
|
||||
smtpPort: number;
|
||||
|
||||
Reference in New Issue
Block a user