diff --git a/project.inlang/messages/de.json b/project.inlang/messages/de.json index 3a6f1f3..d40dc63 100644 --- a/project.inlang/messages/de.json +++ b/project.inlang/messages/de.json @@ -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": { diff --git a/project.inlang/messages/en.json b/project.inlang/messages/en.json index 8c3efb1..a4f3d83 100644 --- a/project.inlang/messages/en.json +++ b/project.inlang/messages/en.json @@ -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" diff --git a/src/routes/(pages)/dashboard/tenants/(components)/add-tenant-form/add-tenant-form.svelte b/src/routes/(pages)/dashboard/tenants/(components)/add-tenant-form/add-tenant-form.svelte index ada0aaf..86f3fc3 100644 --- a/src/routes/(pages)/dashboard/tenants/(components)/add-tenant-form/add-tenant-form.svelte +++ b/src/routes/(pages)/dashboard/tenants/(components)/add-tenant-form/add-tenant-form.svelte @@ -42,19 +42,19 @@ - + {#snippet children({ props })} {m["form.name"]()} - + {/snippet} {m["tenants.add.name.description"]({ domain: - $formData.shortname.length < 2 + $formData.shortName.length < 2 ? "" - : `${$formData.shortname}.${window.location.hostname}`, + : `${$formData.shortName}.${window.location.hostname}`, })} diff --git a/src/routes/(pages)/dashboard/tenants/(components)/add-tenant-form/schema.ts b/src/routes/(pages)/dashboard/tenants/(components)/add-tenant-form/schema.ts index 7fd4089..3238d4f 100644 --- a/src/routes/(pages)/dashboard/tenants/(components)/add-tenant-form/schema.ts +++ b/src/routes/(pages)/dashboard/tenants/(components)/add-tenant-form/schema.ts @@ -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("")), diff --git a/src/routes/(pages)/dashboard/tenants/(components)/edit-tenant-form/edit-tenant-form.svelte b/src/routes/(pages)/dashboard/tenants/(components)/edit-tenant-form/edit-tenant-form.svelte new file mode 100644 index 0000000..345d79f --- /dev/null +++ b/src/routes/(pages)/dashboard/tenants/(components)/edit-tenant-form/edit-tenant-form.svelte @@ -0,0 +1,67 @@ + + + + + + {#snippet children({ props })} + {m["form.name"]()} + + {/snippet} + + + {m["tenants.add.name.description"]({ + domain: + $formData.shortName.length < 2 + ? "" + : `${$formData.shortName}.${window.location.hostname}`, + })} + + + + + +
+ + {m["tenants.edit.action"]()} + +
+
diff --git a/src/routes/(pages)/dashboard/tenants/(components)/edit-tenant-form/index.ts b/src/routes/(pages)/dashboard/tenants/(components)/edit-tenant-form/index.ts new file mode 100644 index 0000000..5b272a1 --- /dev/null +++ b/src/routes/(pages)/dashboard/tenants/(components)/edit-tenant-form/index.ts @@ -0,0 +1,5 @@ +import EditTenantForm from "./edit-tenant-form.svelte"; + +export { EditTenantForm }; +export { formSchema } from "./schema"; +export type { FormSchema } from "./schema"; diff --git a/src/routes/(pages)/dashboard/tenants/(components)/edit-tenant-form/schema.ts b/src/routes/(pages)/dashboard/tenants/(components)/edit-tenant-form/schema.ts new file mode 100644 index 0000000..e5d3693 --- /dev/null +++ b/src/routes/(pages)/dashboard/tenants/(components)/edit-tenant-form/schema.ts @@ -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; diff --git a/src/routes/(pages)/dashboard/tenants/+page.server.ts b/src/routes/(pages)/dashboard/tenants/+page.server.ts index 6bae972..345aa40 100644 --- a/src/routes/(pages)/dashboard/tenants/+page.server.ts +++ b/src/routes/(pages)/dashboard/tenants/+page.server.ts @@ -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)); diff --git a/src/routes/(pages)/dashboard/tenants/+page.svelte b/src/routes/(pages)/dashboard/tenants/+page.svelte index 247300f..84d3d43 100644 --- a/src/routes/(pages)/dashboard/tenants/+page.svelte +++ b/src/routes/(pages)/dashboard/tenants/+page.svelte @@ -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 @@ {/each} + + {#if curTenant} + { + closeDialog("edit-tenant"); + curTenant = null; + invalidate(ROUTES.DASHBOARD.TENANTS); + }} + /> + {/if} +