DataGrid list type; Review/Send refinment; User lock; Table actions

This commit is contained in:
2026-06-13 19:28:48 +02:00
parent 5937dfe97e
commit c72df498e7
15 changed files with 1318 additions and 218 deletions

View File

@@ -13,8 +13,9 @@ import { asRecord, isAuditLockedVersion, isRecord } from "./utils/campaignView";
import { getBool, getText, updateNested } from "./utils/draftEditor";
import FieldValueInput from "./components/FieldValueInput";
import DismissibleAlert from "../../components/DismissibleAlert";
import DataGrid, { type DataGridColumn } from "../../components/table/DataGrid";
import DataGrid, { DataGridRowActions, type DataGridColumn } from "../../components/table/DataGrid";
import { fieldTypeOptions, humanizeFieldName, normalizeFieldType, type CampaignFieldDefinition } from "./utils/fieldDefinitions";
import { insertAfter, moveArrayItem } from "../../utils/arrayOrder";
export default function CampaignFieldsPage({ settings, campaignId }: { settings: ApiSettings; campaignId: string }) {
@@ -97,10 +98,11 @@ export default function CampaignFieldsPage({ settings, campaignId }: { settings:
markDirty();
}
function addField() {
function addField(afterIndex = fields.length - 1) {
const name = uniqueFieldName(fields);
const nextFields = [...fields, { name, label: humanizeFieldName(name), type: "string", required: false, can_override: true }];
fieldValueKeys.current = [...fieldValueKeys.current, name];
const nextField = { name, label: humanizeFieldName(name), type: "string", required: false, can_override: true };
const nextFields = insertAfter(fields, afterIndex, nextField);
fieldValueKeys.current = insertAfter(fieldValueKeys.current, afterIndex, name);
const nextGlobalValues = { ...globalValues, [name]: "" };
setDraft((current) => {
const nextDraft = updateNested(current ?? {}, ["fields"], nextFields);
@@ -109,6 +111,12 @@ export default function CampaignFieldsPage({ settings, campaignId }: { settings:
markDirty();
}
function moveField(index: number, targetIndex: number) {
if (locked || index === targetIndex) return;
fieldValueKeys.current = moveArrayItem(fieldValueKeys.current, index, targetIndex);
patchFields(moveArrayItem(fields, index, targetIndex));
}
function deleteField(index: number) {
const field = fields[index];
const nextFields = fields.filter((_, currentIndex) => currentIndex !== index);
@@ -163,16 +171,13 @@ export default function CampaignFieldsPage({ settings, campaignId }: { settings:
<LoadingFrame loading={loading || !draft} label="Loading campaign draft…">
<>
<Card
title="Fields and global values"
actions={<Button variant="primary" onClick={addField} disabled={locked}>Add field</Button>}
>
<Card title="Fields and global values">
<DataGrid
id={`campaign-${campaignId}-fields`}
rows={fields}
columns={fieldColumns({ locked, globalValues, renameField, setField, setGlobalValue, setOverrideAllowed, deleteField })}
columns={fieldColumns({ locked, fields, globalValues, renameField, setField, setGlobalValue, setOverrideAllowed, addField, moveField, deleteField })}
getRowKey={(_field, index) => `field-row-${index}`}
emptyText="No campaign fields configured yet."
emptyText={<div className="data-grid-empty-action"><span>No campaign fields configured yet.</span><Button variant="primary" onClick={() => addField(-1)} disabled={locked}>Add field</Button></div>}
className="field-editor-table-wrap field-editor-table"
/>
</Card>
@@ -188,15 +193,18 @@ export default function CampaignFieldsPage({ settings, campaignId }: { settings:
type FieldColumnContext = {
locked: boolean;
fields: CampaignFieldDefinition[];
globalValues: Record<string, unknown>;
renameField: (index: number, nextName: string) => void;
setField: (index: number, patchValue: Partial<CampaignFieldDefinition>) => void;
setGlobalValue: (key: string, value: unknown) => void;
setOverrideAllowed: (index: number, allowed: boolean) => void;
addField: (afterIndex?: number) => void;
moveField: (index: number, targetIndex: number) => void;
deleteField: (index: number) => void;
};
function fieldColumns({ locked, globalValues, renameField, setField, setGlobalValue, setOverrideAllowed, deleteField }: FieldColumnContext): DataGridColumn<CampaignFieldDefinition>[] {
function fieldColumns({ locked, fields, globalValues, renameField, setField, setGlobalValue, setOverrideAllowed, addField, moveField, deleteField }: FieldColumnContext): DataGridColumn<CampaignFieldDefinition>[] {
return [
{ id: "name", header: "Field ID", width: 190, resizable: true, sortable: true, filterable: true, sticky: "start", render: (field, index) => <input value={field.name} disabled={locked} placeholder="field_name" onChange={(event) => renameField(index, event.target.value)} />, value: (field) => field.name },
{ id: "label", header: "Label", width: 210, resizable: true, sortable: true, filterable: true, render: (field, index) => <input value={field.label} disabled={locked} placeholder="Display label" onChange={(event) => setField(index, { label: event.target.value })} />, value: (field) => field.label },
@@ -204,7 +212,25 @@ function fieldColumns({ locked, globalValues, renameField, setField, setGlobalVa
{ id: "required", header: "Required", width: 140, sortable: true, filterable: true, render: (field, index) => <ToggleSwitch label="Required" checked={field.required} disabled={locked} onChange={(checked) => setField(index, { required: checked })} />, value: (field) => field.required ? "required" : "optional" },
{ id: "global_value", header: "Global value", width: 220, resizable: true, filterable: true, render: (field) => <FieldValueInput fieldType={field.type} value={globalValues[field.name]} disabled={locked || !field.name} placeholder="Optional default" onChange={(value) => setGlobalValue(field.name, value)} />, value: (field) => String(globalValues[field.name] ?? "") },
{ id: "override", header: "Recipient override", width: 170, sortable: true, filterable: true, render: (field, index) => <ToggleSwitch label="Can override" checked={field.can_override} disabled={locked || !field.name} onChange={(checked) => setOverrideAllowed(index, checked)} />, value: (field) => field.can_override ? "can override" : "locked" },
{ id: "actions", header: "Actions", width: 120, sticky: "end", render: (_field, index) => <Button variant="danger" disabled={locked} onClick={() => deleteField(index)}>Remove</Button> }
{
id: "actions",
header: "Actions",
width: 150,
sticky: "end",
render: (_field, index) => (
<DataGridRowActions
disabled={locked}
onAddBelow={() => addField(index)}
onRemove={() => deleteField(index)}
onMoveUp={index > 0 ? () => moveField(index, index - 1) : undefined}
onMoveDown={index < fields.length - 1 ? () => moveField(index, index + 1) : undefined}
addLabel="Add field below"
removeLabel="Remove field"
moveUpLabel="Move field up"
moveDownLabel="Move field down"
/>
)
}
];
}