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 (
)}
- {create.isError && (
+ {failed && (
{t("form.rejected")}
)}
-