Added edit tenant form

This commit is contained in:
Karl Ludwig Weise
2025-09-09 21:11:16 +02:00
parent 7ada2bea9b
commit 7a283cd060
9 changed files with 178 additions and 14 deletions
+10 -2
View File
@@ -46,7 +46,7 @@
"noPassAtAll": "Either passphrase or passkey is required",
"bothPassSet": "Nur Passphrase oder Passkey darf gesetzt sein",
"name": "Muss mindestens 2 Zeichen lang sein",
"shortname": "Muss zwischen 4 und 20 Zeichen lang sein",
"shortname": "Muss zwischen 4 und 15 Zeichen lang sein",
"deleteConfirmation": "Muss exakt »{expectedValue}« sein"
},
"passkey": "Passkey",
@@ -127,7 +127,8 @@
"name": {
"description": "Muss einmalig auf diesem Server sein. Wird als URL verwendet {domain}",
"errors": {
"urlFormat": "Nur Kleinbuchstaben, Zahlen und Bindestriche sind erlaubt"
"urlFormat": "Nur Kleinbuchstaben, Zahlen und Bindestriche sind erlaubt",
"startEndDash": "Darf nicht mit einem Bindestrich beginnen oder enden"
}
},
"invite": {
@@ -167,6 +168,13 @@
"title": "Mandanten kann nicht gelöscht werden",
"description": "Du kannst den Mandanten, den Du gerade konfigurierst, nicht löschen. Bitte wechsle zu einem anderen Mandanten, bevor Du diesen löschst."
}
},
"edit": {
"title": "Mandant bearbeiten",
"description": "Aktualisiere den Namen des Mandanten.",
"success": "Mandant aktualisiert",
"error": "Mandant konnte nicht aktualisiert werden.",
"action": "Name ändern"
}
},
"components": {
+11 -3
View File
@@ -47,7 +47,7 @@
"noPassAtAll": "Either passphrase or passkey is required",
"bothPassSet": "Only one of passphrase or passkey should be provided",
"name": "Must be at least 2 characters",
"shortname": "Must be between 4 and 20 characters",
"shortname": "Must be between 4 and 15 characters",
"deleteConfirmation": "Must be exactly »{expectedValue}«"
},
"noPassAtAll": "Either passphrase or passkey is required",
@@ -136,7 +136,8 @@
"name": {
"description": "Must be unique on this server. Used as a subdomain {domain}",
"errors": {
"urlFormat": "Only letters, numbers, and hyphens are allowed"
"urlFormat": "Only letters, numbers, and hyphens are allowed",
"startEndDash": "Must not start or end with a dash"
}
},
"invite": {
@@ -175,7 +176,14 @@
"loading": "Loading tenants",
"allTenants": "All tenants",
"unselectTenant": "Unselect tenant",
"failedToSwitch": "Failed to switch tenant"
"failedToSwitch": "Failed to switch tenant",
"edit": {
"title": "Edit Tenant",
"description": "Update the tenant's short name.",
"success": "Tenant updated",
"error": "Could not update tenant.",
"action": "Edit Tenant"
}
},
"components": {
"openMenu": "Open menu"
@@ -42,19 +42,19 @@
</script>
<Form.Root {enhance} action="?/add">
<Form.Field {form} name="shortname">
<Form.Field {form} name="shortName">
<Form.Control>
{#snippet children({ props })}
<Form.Label>{m["form.name"]()}</Form.Label>
<Input {...props} bind:value={$formData.shortname} type="text" autocomplete="off" />
<Input {...props} bind:value={$formData.shortName} type="text" autocomplete="off" />
{/snippet}
</Form.Control>
<Form.Description>
{m["tenants.add.name.description"]({
domain:
$formData.shortname.length < 2
$formData.shortName.length < 2
? ""
: `${$formData.shortname}.${window.location.hostname}`,
: `${$formData.shortName}.${window.location.hostname}`,
})}
</Form.Description>
<Form.FieldErrors />
@@ -2,15 +2,15 @@ import { m } from "$i18n/messages";
import { z } from "zod";
export const formSchema = z.object({
shortname: z
shortName: z
.string()
.min(4, m["form.errors.shortname"]())
.max(20, m["form.errors.shortname"]())
.max(15, m["form.errors.shortname"]())
.regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, {
message: m["tenants.add.name.errors.urlFormat"](),
})
.refine((val) => !val.startsWith("-") && !val.endsWith("-"), {
message: "Cannot start or end with a dash",
message: m["tenants.add.name.errors.startEndDash"](),
}),
inviteAdmin: z.boolean(),
email: z.string().email(m["form.errors.email"]()).optional().or(z.literal("")),
@@ -0,0 +1,67 @@
<script lang="ts">
import { m } from "$i18n/messages.js";
import * as Form from "$lib/components/ui/form";
import { Input } from "$lib/components/ui/input";
import type { TTenant } from "$lib/types/tenant";
import { toast } from "svelte-sonner";
import { superForm } from "sveltekit-superforms";
import { zodClient } from "sveltekit-superforms/adapters";
import { formSchema } from ".";
let { entity, done }: { entity: TTenant; done: () => void } = $props();
const form = superForm(
{ tenantId: entity.id, shortName: entity.shortName },
{
validators: zodClient(formSchema),
onResult: async (event) => {
if (event.result.type === "success") {
toast.success(m["tenants.edit.success"]());
done();
} else if (event.result.type === "failure") {
toast.error(m["tenants.edit.error"]());
}
isSubmitting = false;
},
onSubmit: () => (isSubmitting = true),
},
);
let isSubmitting = $state(false);
const { form: formData, enhance } = form;
</script>
<Form.Root {enhance} action="?/edit">
<Form.Field {form} name="shortName">
<Form.Control>
{#snippet children({ props })}
<Form.Label>{m["form.name"]()}</Form.Label>
<Input {...props} bind:value={$formData.shortName} type="text" autocomplete="off" />
{/snippet}
</Form.Control>
<Form.Description>
{m["tenants.add.name.description"]({
domain:
$formData.shortName.length < 2
? ""
: `${$formData.shortName}.${window.location.hostname}`,
})}
</Form.Description>
<Form.FieldErrors />
</Form.Field>
<Form.Field {form} name="tenantId" class="hidden">
<Form.Control>
{#snippet children({ props })}
<Form.Label>{m["form.name"]()}</Form.Label>
<Input {...props} bind:value={$formData.tenantId} type="hidden" />
{/snippet}
</Form.Control>
</Form.Field>
<div class="mt-6 flex flex-col gap-4">
<Form.Button size="lg" type="submit" isLoading={isSubmitting} disabled={isSubmitting}>
{m["tenants.edit.action"]()}
</Form.Button>
</div>
</Form.Root>
@@ -0,0 +1,5 @@
import EditTenantForm from "./edit-tenant-form.svelte";
export { EditTenantForm };
export { formSchema } from "./schema";
export type { FormSchema } from "./schema";
@@ -0,0 +1,18 @@
import { m } from "$i18n/messages";
import { z } from "zod";
export const formSchema = z.object({
shortName: z
.string()
.min(4, m["form.errors.shortname"]())
.max(15, m["form.errors.shortname"]())
.regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, {
message: m["tenants.add.name.errors.urlFormat"](),
})
.refine((val) => !val.startsWith("-") && !val.endsWith("-"), {
message: m["tenants.add.name.errors.startEndDash"](),
}),
tenantId: z.string(),
});
export type FormSchema = typeof formSchema;
@@ -4,6 +4,7 @@ import { fail, redirect, type Actions } from "@sveltejs/kit";
import { superValidate } from "sveltekit-superforms";
import { zod } from "sveltekit-superforms/adapters";
import { formSchema as addFormSchema } from "./(components)/add-tenant-form";
import { formSchema as editFormSchema } from "./(components)/edit-tenant-form";
import { formSchema as deleteFormSchema } from "./(components)/delete-tenant-form";
import type { TTenant } from "$lib/types/tenant";
@@ -59,7 +60,7 @@ export const actions: Actions = {
},
credentials: "same-origin",
body: JSON.stringify({
shortName: form.data.shortname,
shortName: form.data.shortName,
inviteAdmin: form.data.email,
}),
});
@@ -80,6 +81,42 @@ export const actions: Actions = {
});
}
},
edit: async (event) => {
const form = await superValidate(event, zod(editFormSchema));
if (!form.valid) {
log.error("Edit tenant form is not valid", { errors: form.errors });
return fail(400, {
form: { ...form, data: { ...form.data } },
error: "Form is not valid",
});
}
const resp = await event.fetch(`/api/tenants/${form.data.tenantId}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
credentials: "same-origin",
body: JSON.stringify({ shortName: form.data.shortName }),
});
if (resp.status < 400) {
return { form };
} else {
let error = "Unknown error";
try {
const body = await resp.json();
error = body.error;
} catch (e) {
log.error("Failed to parse edit tenant error response", { error: e });
}
return fail(400, {
form: { ...form, data: { ...form.data } },
error,
});
}
},
delete: async (event) => {
const form = await superValidate(event, zod(deleteFormSchema));
@@ -18,6 +18,7 @@
import { AddTenantForm } from "./(components)/add-tenant-form";
import { tenants as tenantsStore } from "$lib/stores/tenants";
import DeleteTenantForm from "./(components)/delete-tenant-form/delete-tenant-form.svelte";
import EditTenantForm from "./(components)/edit-tenant-form/edit-tenant-form.svelte";
const { data } = $props();
let curTenant: TTenant | null = $state(null);
@@ -75,7 +76,10 @@
type: "action",
icon: EditIcon,
label: m["edit"](),
onClick: () => {},
onClick: () => {
curTenant = tenant;
openDialog("edit-tenant");
},
},
{
type: "action",
@@ -102,6 +106,23 @@
</ListItem>
{/each}
</List>
<ResponsiveDialog
id="edit-tenant"
title={m["tenants.edit.title"]()}
description={m["tenants.edit.description"]()}
triggerHidden={true}
>
{#if curTenant}
<EditTenantForm
entity={curTenant}
done={() => {
closeDialog("edit-tenant");
curTenant = null;
invalidate(ROUTES.DASHBOARD.TENANTS);
}}
/>
{/if}
</ResponsiveDialog>
<ResponsiveDialog
id="delete-tenant"
title={m["tenants.delete.title"]()}