CE-WP-0007 T10-T12: field add/edit dialog, pencil edit, integration tests

- FieldDefinitionForm shared component (label + text/textarea/date)
- Add field opens inline form; per-field pencil edit with stable ids
- forms-field-edit.dom.test.tsx covers add, edit, and link-to-new-field
- Workplan T10-T12 and README marked done
This commit is contained in:
tegwick 2026-06-08 00:46:06 +02:00
parent 2fd085b65e
commit 48a53df9fc
6 changed files with 540 additions and 54 deletions

View file

@ -34,7 +34,7 @@ import {
useScrollToAnnotation,
} from "@work/index";
import { FormRenderer } from "@binder/FormRenderer";
import { FormRenderer, type FieldDefinitionPatch } from "@binder/FormRenderer";
import { DEMO_SCHEMA } from "./demo-schema";
import { HighlightRectBridge } from "./HighlightRectBridge";
@ -64,24 +64,76 @@ export function FormsApp() {
[schema],
);
const addField = useCallback(() => {
setSchema((prev) => {
const n = prev.fields.length + 1;
const field: FormFieldSchema = {
type: "text",
id: `field_${n}`,
label: `New field ${n}`,
};
return { ...prev, fields: [...prev.fields, field] };
});
const [showAddFieldForm, setShowAddFieldForm] = useState(false);
const [editingFieldId, setEditingFieldId] = useState<string | null>(null);
const nextFieldId = useCallback((fields: readonly FormFieldSchema[]): string => {
let max = 0;
for (const f of fields) {
const m = /^field_(\d+)$/.exec(f.id);
if (m) max = Math.max(max, Number(m[1]));
}
return `field_${max + 1}`;
}, []);
const handleConfirmAddField = useCallback(
(patch: FieldDefinitionPatch) => {
setSchema((prev) => {
const id = nextFieldId(prev.fields);
const n = prev.fields.length + 1;
const field: FormFieldSchema = {
id,
type: patch.type,
label: patch.label.length > 0 ? patch.label : `New field ${n}`,
};
return { ...prev, fields: [...prev.fields, field] };
});
setShowAddFieldForm(false);
},
[nextFieldId],
);
const handleSaveFieldEdit = useCallback(
(fieldId: string, patch: FieldDefinitionPatch) => {
setSchema((prev) => ({
...prev,
fields: prev.fields.map((f) =>
f.id === fieldId
? {
...f,
type: patch.type,
label: patch.label.length > 0 ? patch.label : f.label,
}
: f,
),
}));
setEditingFieldId(null);
},
[],
);
return (
<div style={{ display: "flex", flexDirection: "column", height: "100%" }}>
<div style={{ display: "flex", flex: 1, minHeight: 0 }}>
<CollectionList />
<ViewerShell />
<FormPane schema={schema} onAddField={addField} />
<FormPane
schema={schema}
showAddFieldForm={showAddFieldForm}
editingFieldId={editingFieldId}
onRequestAddField={() => {
setEditingFieldId(null);
setShowAddFieldForm(true);
}}
onConfirmAddField={handleConfirmAddField}
onCancelAddField={() => setShowAddFieldForm(false)}
onBeginEditField={(fieldId) => {
setShowAddFieldForm(false);
setEditingFieldId(fieldId);
}}
onSaveFieldEdit={handleSaveFieldEdit}
onCancelFieldEdit={() => setEditingFieldId(null)}
/>
</div>
<EvidenceStrip fieldLabels={fieldLabels} />
<ScrollBridge />
@ -104,10 +156,24 @@ function ScrollBridge() {
function FormPane({
schema,
onAddField,
showAddFieldForm,
editingFieldId,
onRequestAddField,
onConfirmAddField,
onCancelAddField,
onBeginEditField,
onSaveFieldEdit,
onCancelFieldEdit,
}: {
schema: FormSchema;
onAddField: () => void;
showAddFieldForm: boolean;
editingFieldId: string | null;
onRequestAddField: () => void;
onConfirmAddField: (patch: FieldDefinitionPatch) => void;
onCancelAddField: () => void;
onBeginEditField: (fieldId: string) => void;
onSaveFieldEdit: (fieldId: string, patch: FieldDefinitionPatch) => void;
onCancelFieldEdit: () => void;
}) {
const { document } = useActiveDocument();
const { bindings } = useBinder();
@ -189,23 +255,14 @@ function FormPane({
schema={schema}
linkCounts={linkCounts}
linkHints={linkHints}
headerAction={
<button
type="button"
data-testid="add-field-button"
onClick={onAddField}
style={{
fontSize: 11,
padding: "4px 10px",
border: "1px solid #888",
borderRadius: 4,
background: "white",
cursor: "pointer",
}}
>
Add field
</button>
}
showAddFieldForm={showAddFieldForm}
onRequestAddField={onRequestAddField}
onConfirmAddField={onConfirmAddField}
onCancelAddField={onCancelAddField}
editingFieldId={editingFieldId}
onBeginEditField={onBeginEditField}
onSaveFieldEdit={onSaveFieldEdit}
onCancelFieldEdit={onCancelFieldEdit}
/>
) : (
<EmptyHint />