From 65ca79f2bd98f3cc9a8129c8340d09ce964b9820 Mon Sep 17 00:00:00 2001 From: Anders Olsson Date: Fri, 5 Jun 2026 20:19:13 +0200 Subject: [PATCH] feat(web): edit/delete field definitions on /fields (in-place edit pane) (#36) --- web/src/fields/field-form.stories.tsx | 38 ++++++++ web/src/fields/field-form.tsx | 130 +++++++++++++++++--------- web/src/fields/field-list.tsx | 56 +++++++---- web/src/fields/fields-page.tsx | 15 ++- 4 files changed, 179 insertions(+), 60 deletions(-) create mode 100644 web/src/fields/field-form.stories.tsx diff --git a/web/src/fields/field-form.stories.tsx b/web/src/fields/field-form.stories.tsx new file mode 100644 index 0000000..662983d --- /dev/null +++ b/web/src/fields/field-form.stories.tsx @@ -0,0 +1,38 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import { expect, fn } from 'storybook/test' + +import { FieldForm } from './field-form' + +const meta = { + component: FieldForm, + tags: ['ai-generated'], +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Create: Story = { + args: { editing: null, onDone: fn() }, + play: async ({ canvas }) => { + await expect(canvas.getByLabelText('Key')).toBeEnabled() + }, +} + +export const Edit: Story = { + args: { + editing: { + key: 'material', + data_type: 'text', + vocabulary_id: null, + authority_kind: null, + required: true, + group: 'Identification', + labels: [{ lang: 'en', label: 'Material' }], + }, + onDone: fn(), + }, + play: async ({ canvas }) => { + await expect(canvas.getByLabelText('Key')).toBeDisabled() + await expect(canvas.getByRole('button', { name: 'Save' })).toBeVisible() + }, +} diff --git a/web/src/fields/field-form.tsx b/web/src/fields/field-form.tsx index 8177fd7..c153128 100644 --- a/web/src/fields/field-form.tsx +++ b/web/src/fields/field-form.tsx @@ -2,7 +2,11 @@ import { useState, type FormEvent } from "react"; import { useTranslation } from "react-i18next"; import type { components } from "../api/schema"; -import { useCreateFieldDefinition, useVocabularies } from "../api/queries"; +import { + useCreateFieldDefinition, + useUpdateFieldDefinition, + useVocabularies, +} from "../api/queries"; import { LabelEditor } from "../components/label-editor"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -10,68 +14,103 @@ import { Label } from "@/components/ui/label"; import { Checkbox } from "@/components/ui/checkbox"; type LabelInput = components["schemas"]["LabelInput"]; +type FieldDefinitionView = components["schemas"]["FieldDefinitionView"]; const TYPES = ["text", "localized_text", "integer", "date", "boolean", "term", "authority"] as const; const KINDS = ["person", "organisation", "place"] as const; -export function FieldForm() { +export function FieldForm({ + editing, + onDone, +}: { + editing: FieldDefinitionView | null; + onDone: () => void; +}) { const { t } = useTranslation(); const create = useCreateFieldDefinition(); + const update = useUpdateFieldDefinition(); const { data: vocabularies } = useVocabularies(); - const [key, setKey] = useState(""); - const [labels, setLabels] = useState([]); - const [dataType, setDataType] = useState("text"); - const [vocabularyId, setVocabularyId] = useState(""); - const [authorityKind, setAuthorityKind] = useState(""); - const [group, setGroup] = useState(""); - const [required, setRequired] = useState(false); - const [error, setError] = useState(false); + const isEdit = editing !== null; - const reset = () => { - setKey(""); - setLabels([]); - setDataType("text"); - setVocabularyId(""); - setAuthorityKind(""); - setGroup(""); - setRequired(false); - setError(false); - }; + const [key, setKey] = useState(editing?.key ?? ""); + const [labels, setLabels] = useState((editing?.labels as LabelInput[]) ?? []); + const [dataType, setDataType] = useState(editing?.data_type ?? "text"); + const [vocabularyId, setVocabularyId] = useState(editing?.vocabulary_id ?? ""); + const [authorityKind, setAuthorityKind] = useState(editing?.authority_kind ?? ""); + const [group, setGroup] = useState(editing?.group ?? ""); + const [required, setRequired] = useState(editing?.required ?? false); + const [error, setError] = useState(false); const onSubmit = (event: FormEvent) => { event.preventDefault(); const hasLabel = labels.some((l) => l.label); - const termNeedsVocab = dataType === "term" && !vocabularyId; - if (!key.trim() || !hasLabel || termNeedsVocab) { + if (!hasLabel || (!isEdit && !key.trim()) || (!isEdit && dataType === "term" && !vocabularyId)) { setError(true); return; } setError(false); - create.mutate( - { - key: key.trim(), - data_type: dataType, - vocabulary_id: dataType === "term" ? vocabularyId : null, - authority_kind: dataType === "authority" ? authorityKind || null : null, - required, - group: group.trim() || null, - labels, - }, - { onSuccess: reset }, - ); + + if (isEdit) { + update.mutate( + { key: editing.key, required, group: group.trim() || null, labels }, + { onSuccess: onDone }, + ); + } else { + create.mutate( + { + key: key.trim(), + data_type: dataType, + vocabulary_id: dataType === "term" ? vocabularyId : null, + authority_kind: dataType === "authority" ? authorityKind || null : null, + required, + group: group.trim() || null, + labels, + }, + { + onSuccess: () => { + setKey(""); + setLabels([]); + setDataType("text"); + setVocabularyId(""); + setAuthorityKind(""); + setGroup(""); + setRequired(false); + setError(false); + onDone(); + }, + }, + ); + } }; + const pending = isEdit ? update.isPending : create.isPending; + const failed = isEdit ? update.isError : create.isError; + return (
-
{t("fields.newField")}
+
+
+ {isEdit ? labelTextOrKey(editing) : t("fields.newField")} +
+ {isEdit && ( + + )} +
- setKey(e.target.value)} /> + setKey(e.target.value)} + />
@@ -81,8 +120,9 @@ export function FieldForm() { setVocabularyId(e.target.value)} - className="w-full rounded border px-2 py-1 text-sm" + className="w-full rounded border px-2 py-1 text-sm disabled:opacity-60" > {vocabularies?.map((vocab) => ( @@ -117,8 +158,9 @@ export function FieldForm() {