Sprechende Kurz-Links: selbst vergeben, aendern, frueher gueltig halten
Schliesst Issue #1. Der Wunsch betraf den Kurz-Link, nicht das Ziel: Er soll sich von Anfang an bestimmen und spaeter aendern lassen. Beim Anlegen gibt es dafuer ein Feld, im Bearbeiten-Dialog laesst er sich aendern. Erlaubt sind Kleinbuchstaben, Ziffern, Bindestriche und „/" als Trenner — damit sind Adressen wie kunde-a/sommer moeglich. Umlaute bleiben draussen, weil sie in Adressen unleserlich kodiert werden; Gross- und Kleinschreibung ist beim Aufrufen egal. Beim Aendern bleibt die vorherige Adresse gueltig. Das ist der Kern: Auf gedruckten Codes steht die alte, und genau deren Bestand ist der Sinn eines dynamischen Codes. Frueher vergebene Adressen stehen im Dialog und lassen sich einzeln freigeben — fuer den Tippfehler, mit dem noch nichts gedruckt ist. Solange eine Adresse gehalten wird, kann sie kein anderer Code uebernehmen; sonst liessen sich fremde gedruckte Codes umlenken. Vergeben und Aendern ist standardmaessig Administratoren vorbehalten. Ein Kurz-Link ist eine oeffentliche Adresse, und wer ihn aendert, kann Aushaenge entwerten. Unter Verwaltung -> Einstellungen laesst sich das fuer Bearbeiter freigeben; ohne das Recht erscheint das Feld gar nicht, Codes anlegen und deren Ziel aendern geht weiterhin. Eigene Aktion in der Policy statt Teil von codes:update, weil sie mehr wiegt. Technisch: /r und /s sind Sammelrouten, weil ein Kurz-Link „/" enthalten darf. Die Eindeutigkeit gilt zusaetzlich auf der kleingeschriebenen Form; die Migration bricht ab, falls der Bestand das nicht zulaesst, statt still Daten zu verlieren. Der bestehende exakte Index bleibt — er traegt den haeufigen Fall, den gescannten Code.
This commit is contained in:
@@ -22,6 +22,7 @@ import type { QrCodeDto } from "@/lib/dto";
|
||||
import { defaultDesign, designSchema, type QrDesign } from "@/lib/qr/design";
|
||||
import { qrTypes } from "@/lib/qr/registry";
|
||||
import type { QrFormData, QrTypeId } from "@/lib/qr/type-module";
|
||||
import { describeSlugProblem } from "@/lib/slug";
|
||||
|
||||
type PreviewState = { dataUrl: string; content: string };
|
||||
|
||||
@@ -42,7 +43,14 @@ function sameData(a: unknown, b: unknown): boolean {
|
||||
return stableStringify(a) === stableStringify(b);
|
||||
}
|
||||
|
||||
export function QrGenerator({ typeId }: { typeId: QrTypeId }) {
|
||||
export function QrGenerator({
|
||||
typeId,
|
||||
/** Darf der Kurz-Link selbst vergeben werden? Sonst erscheint das Feld nicht. */
|
||||
maySetSlug = true,
|
||||
}: {
|
||||
typeId: QrTypeId;
|
||||
maySetSlug?: boolean;
|
||||
}) {
|
||||
const qrType = qrTypes[typeId];
|
||||
const Form = qrForms[typeId];
|
||||
|
||||
@@ -51,6 +59,8 @@ export function QrGenerator({ typeId }: { typeId: QrTypeId }) {
|
||||
// Typen ohne statische Variante — etwa der App-Link — starten dynamisch.
|
||||
const [isDynamic, setIsDynamic] = useState(!qrType.supportsStatic);
|
||||
const [label, setLabel] = useState("");
|
||||
/** Selbst gewählter Kurz-Link. Leer bedeutet: einen erzeugen lassen. */
|
||||
const [wunschSlug, setWunschSlug] = useState("");
|
||||
|
||||
const [saved, setSaved] = useState<QrCodeDto | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
@@ -164,6 +174,11 @@ export function QrGenerator({ typeId }: { typeId: QrTypeId }) {
|
||||
|
||||
/* ----------------------------------------------- Dynamischen Code speichern */
|
||||
|
||||
// Sofortige Rückmeldung, damit man nicht erst beim Speichern erfährt, dass
|
||||
// ein Zeichen nicht erlaubt ist.
|
||||
const slugProblem =
|
||||
wunschSlug.trim().length > 0 ? describeSlugProblem(wunschSlug) : null;
|
||||
|
||||
const hasUnsavedChanges =
|
||||
saved !== null &&
|
||||
(!sameData(saved.data, validation.ok ? validation.data : data) ||
|
||||
@@ -171,7 +186,7 @@ export function QrGenerator({ typeId }: { typeId: QrTypeId }) {
|
||||
(saved.label ?? "") !== label.trim());
|
||||
|
||||
const handleSave = useCallback(async () => {
|
||||
if (!validation.ok || !designValid) return;
|
||||
if (!validation.ok || !designValid || slugProblem) return;
|
||||
setSaving(true);
|
||||
setSaveError(null);
|
||||
try {
|
||||
@@ -180,6 +195,7 @@ export function QrGenerator({ typeId }: { typeId: QrTypeId }) {
|
||||
data: validation.data,
|
||||
design,
|
||||
label: label.trim() || undefined,
|
||||
slug: wunschSlug.trim() || undefined,
|
||||
};
|
||||
const result = saved
|
||||
? await apiJson<{ code: QrCodeDto }>(`/api/codes/${saved.id}`, {
|
||||
@@ -200,7 +216,7 @@ export function QrGenerator({ typeId }: { typeId: QrTypeId }) {
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}, [validation, designValid, typeId, design, label, saved]);
|
||||
}, [validation, designValid, slugProblem, typeId, design, label, wunschSlug, saved]);
|
||||
|
||||
const handleCopy = useCallback(async () => {
|
||||
if (!saved) return;
|
||||
@@ -347,6 +363,27 @@ export function QrGenerator({ typeId }: { typeId: QrTypeId }) {
|
||||
/>
|
||||
</Field>
|
||||
|
||||
{/* Nur beim Anlegen: Danach steht der Kurz-Link im
|
||||
Bearbeiten-Dialog, wo auch die früheren Adressen liegen. */}
|
||||
{saved || !maySetSlug ? null : (
|
||||
<Field
|
||||
label="Kurz-Link (optional)"
|
||||
htmlFor="code-slug"
|
||||
error={slugProblem}
|
||||
hint="Leer lassen, dann wird einer erzeugt. Kleinbuchstaben, Ziffern, Bindestriche und „/“ als Trenner."
|
||||
>
|
||||
<TextInput
|
||||
id="code-slug"
|
||||
value={wunschSlug}
|
||||
maxLength={100}
|
||||
spellCheck={false}
|
||||
invalid={Boolean(slugProblem)}
|
||||
placeholder="z. B. sommeraktion oder kunde-a/flyer"
|
||||
onChange={(event) => setWunschSlug(event.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
|
||||
{saved ? (
|
||||
<div className="space-y-3">
|
||||
<Notice tone="success">
|
||||
@@ -408,7 +445,7 @@ export function QrGenerator({ typeId }: { typeId: QrTypeId }) {
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleSave}
|
||||
disabled={saving || !ready}
|
||||
disabled={saving || !ready || Boolean(slugProblem)}
|
||||
>
|
||||
{saving ? "Speichert …" : "Dynamischen Code speichern"}
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user