feat: support for url based icons (#840)

Co-authored-by: Elias Schneider <login@eliasschneider.com>
This commit is contained in:
Kyle Mendell
2025-09-29 10:07:55 -05:00
committed by GitHub
parent 47bd5ba1ba
commit 6bdf5fa37a
19 changed files with 650 additions and 442 deletions

View File

@@ -41,7 +41,7 @@
accentColor: z.string()
});
let { inputs, ...form } = $derived(createForm(formSchema, appConfig));
let { inputs, ...form } = $derived(createForm(formSchema, updatedAppConfig));
async function onSubmit() {
const data = form.validate();
@@ -69,7 +69,6 @@
description={m.whether_the_users_should_be_able_to_edit_their_own_account_details()}
bind:checked={$inputs.allowOwnAccountEdit.value}
/>
<SwitchWithLabel
id="emails-verified"
label={m.emails_verified()}

View File

@@ -1,10 +1,7 @@
<script lang="ts">
import FileInput from '$lib/components/form/file-input.svelte';
import FormInput from '$lib/components/form/form-input.svelte';
import SwitchWithLabel from '$lib/components/form/switch-with-label.svelte';
import ImageBox from '$lib/components/image-box.svelte';
import { Button } from '$lib/components/ui/button';
import Label from '$lib/components/ui/label/label.svelte';
import { m } from '$lib/paraglide/messages';
import type {
OidcClient,
@@ -21,6 +18,7 @@
import { z } from 'zod/v4';
import FederatedIdentitiesInput from './federated-identities-input.svelte';
import OidcCallbackUrlInput from './oidc-callback-url-input.svelte';
import OidcClientImageInput from './oidc-client-image-input.svelte';
let {
callback,
@@ -31,7 +29,6 @@
callback: (client: OidcClientCreateWithLogo | OidcClientUpdateWithLogo) => Promise<boolean>;
mode: 'create' | 'update';
} = $props();
let isLoading = $state(false);
let showAdvancedOptions = $state(false);
let logo = $state<File | null | undefined>();
@@ -50,7 +47,8 @@
launchURL: existingClient?.launchURL || '',
credentials: {
federatedIdentities: existingClient?.credentials?.federatedIdentities || []
}
},
logoUrl: ''
};
const formSchema = z.object({
@@ -71,6 +69,7 @@
pkceEnabled: z.boolean(),
requiresReauthentication: z.boolean(),
launchURL: optionalUrl,
logoUrl: optionalUrl,
credentials: z.object({
federatedIdentities: z.array(
z.object({
@@ -90,30 +89,42 @@
const data = form.validate();
if (!data) return;
isLoading = true;
const success = await callback({
...data,
logo
logo: $inputs.logoUrl?.value ? null : logo,
logoUrl: $inputs.logoUrl?.value
});
// Reset form if client was successfully created
const hasLogo = logo != null || !!$inputs.logoUrl?.value;
if (success && existingClient && hasLogo) {
logoDataURL = cachedOidcClientLogo.getUrl(existingClient.id);
}
if (success && !existingClient) form.reset();
isLoading = false;
}
function onLogoChange(e: Event) {
const file = (e.target as HTMLInputElement).files?.[0] || null;
if (file) {
logo = file;
function onLogoChange(input: File | string | null) {
if (input == null) return;
if (typeof input === 'string') {
logo = null;
logoDataURL = input || null;
$inputs.logoUrl!.value = input;
} else {
logo = input;
$inputs.logoUrl && ($inputs.logoUrl.value = '');
const reader = new FileReader();
reader.onload = (event) => {
logoDataURL = event.target?.result as string;
};
reader.readAsDataURL(file);
reader.onload = (event) => (logoDataURL = event.target?.result as string);
reader.readAsDataURL(input);
}
}
function resetLogo() {
logo = null;
logoDataURL = null;
$inputs.logoUrl && ($inputs.logoUrl.value = '');
}
function getFederatedIdentityErrors(errors: z.ZodError<any> | undefined) {
@@ -173,32 +184,13 @@
bind:checked={$inputs.requiresReauthentication.value}
/>
</div>
<div class="mt-8">
<Label for="logo">{m.logo()}</Label>
<div class="mt-2 flex items-end gap-3">
{#if logoDataURL}
<ImageBox
class="size-24"
src={logoDataURL}
alt={m.name_logo({ name: $inputs.name.value })}
/>
{/if}
<div class="flex flex-col gap-2">
<FileInput
id="logo"
variant="secondary"
accept="image/png, image/jpeg, image/svg+xml, image/webp, image/avif, image/heic"
onchange={onLogoChange}
>
<Button variant="secondary">
{logoDataURL ? m.change_logo() : m.upload_logo()}
</Button>
</FileInput>
{#if logoDataURL}
<Button variant="outline" onclick={resetLogo}>{m.remove_logo()}</Button>
{/if}
</div>
</div>
<div class="mt-7">
<OidcClientImageInput
{logoDataURL}
{resetLogo}
clientName={$inputs.name.value}
{onLogoChange}
/>
</div>
{#if showAdvancedOptions}

View File

@@ -0,0 +1,44 @@
<script lang="ts">
import UrlFileInput from '$lib/components/form/url-file-input.svelte';
import ImageBox from '$lib/components/image-box.svelte';
import { Button } from '$lib/components/ui/button';
import { Label } from '$lib/components/ui/label';
import { m } from '$lib/paraglide/messages';
import { LucideX } from '@lucide/svelte';
let {
logoDataURL,
clientName,
resetLogo,
onLogoChange
}: {
logoDataURL: string | null;
clientName: string;
resetLogo: () => void;
onLogoChange: (file: File | string | null) => void;
} = $props();
</script>
<Label for="logo">{m.logo()}</Label>
<div class="flex items-end gap-4">
{#if logoDataURL}
<div class="flex items-start gap-4">
<div class="relative shrink-0">
<ImageBox class="size-24" src={logoDataURL} alt={m.name_logo({ name: clientName })} />
<Button
variant="destructive"
size="icon"
onclick={resetLogo}
class="absolute -top-2 -right-2 size-6 rounded-full shadow-md"
>
<LucideX class="size-3" />
</Button>
</div>
</div>
{/if}
<div class="flex flex-col gap-3">
<div class="flex flex-wrap items-center gap-2">
<UrlFileInput label={m.upload_logo()} accept="image/*" onchange={onLogoChange} />
</div>
</div>
</div>

View File

@@ -71,16 +71,21 @@
? item.allowedUserGroupsCount
: m.unrestricted()}</Table.Cell
>
<Table.Cell class="flex justify-end gap-1">
<Button
href="/settings/admin/oidc-clients/{item.id}"
size="sm"
variant="outline"
aria-label={m.edit()}><LucidePencil class="size-3 " /></Button
>
<Button onclick={() => deleteClient(item)} size="sm" variant="outline" aria-label={m.delete()}
><LucideTrash class="size-3 text-red-500" /></Button
>
<Table.Cell class="align-middle">
<div class="flex justify-end gap-1">
<Button
href="/settings/admin/oidc-clients/{item.id}"
size="sm"
variant="outline"
aria-label={m.edit()}><LucidePencil class="size-3 " /></Button
>
<Button
onclick={() => deleteClient(item)}
size="sm"
variant="outline"
aria-label={m.delete()}><LucideTrash class="size-3 text-red-500" /></Button
>
</div>
</Table.Cell>
{/snippet}
</AdvancedTable>