DataGrid - initial commit

This commit is contained in:
2026-06-11 18:21:15 +02:00
parent fdab7cd362
commit 2fc4648515
27 changed files with 1813 additions and 648 deletions

View File

@@ -12,6 +12,8 @@ import { useCampaignDraftEditor } from "./hooks/useCampaignDraftEditor";
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 { fieldTypeOptions, humanizeFieldName, normalizeFieldType, type CampaignFieldDefinition } from "./utils/fieldDefinitions";
@@ -154,9 +156,9 @@ export default function CampaignFieldsPage({ settings, campaignId }: { settings:
</div>
</div>
{error && <div className="alert danger">{error}</div>}
{localError && <div className="alert danger">{localError}</div>}
{fieldNameWarning && <div className="alert warning">{fieldNameWarning}</div>}
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
{localError && <DismissibleAlert tone="danger" resetKey={localError}>{localError}</DismissibleAlert>}
{fieldNameWarning && <DismissibleAlert tone="warning" resetKey={fieldNameWarning}>{fieldNameWarning}</DismissibleAlert>}
{locked && <LockedVersionNotice settings={settings} campaignId={campaignId} version={version} reload={reload} message="Create an editable copy before changing fields." />}
<LoadingFrame loading={loading || !draft} label="Loading campaign draft…">
@@ -165,42 +167,14 @@ export default function CampaignFieldsPage({ settings, campaignId }: { settings:
title="Fields and global values"
actions={<Button variant="primary" onClick={addField} disabled={locked}>Add field</Button>}
>
<div className="app-table-wrap field-editor-table-wrap">
<table className="app-table field-editor-table">
<thead>
<tr>
<th>Field ID</th>
<th>Label</th>
<th>Type</th>
<th>Required</th>
<th>Global value</th>
<th>Recipient override</th>
<th></th>
</tr>
</thead>
<tbody>
{fields.length === 0 ? (
<tr>
<td colSpan={7} className="empty-table-cell">No campaign fields configured yet.</td>
</tr>
) : fields.map((field, index) => (
<tr key={`field-row-${index}`}>
<td><input value={field.name} disabled={locked} placeholder="field_name" onChange={(event) => renameField(index, event.target.value)} /></td>
<td><input value={field.label} disabled={locked} placeholder="Display label" onChange={(event) => setField(index, { label: event.target.value })} /></td>
<td>
<select value={field.type} disabled={locked} onChange={(event) => setField(index, { type: normalizeFieldType(event.target.value) })}>
{fieldTypeOptions.map((option) => <option key={option} value={option}>{option}</option>)}
</select>
</td>
<td><ToggleSwitch label="Required" checked={field.required} disabled={locked} onChange={(checked) => setField(index, { required: checked })} /></td>
<td><FieldValueInput fieldType={field.type} value={globalValues[field.name]} disabled={locked || !field.name} placeholder="Optional default" onChange={(value) => setGlobalValue(field.name, value)} /></td>
<td><ToggleSwitch label="Can override" checked={field.can_override} disabled={locked || !field.name} onChange={(checked) => setOverrideAllowed(index, checked)} /></td>
<td className="table-action-cell"><Button variant="danger" disabled={locked} onClick={() => deleteField(index)}>Remove</Button></td>
</tr>
))}
</tbody>
</table>
</div>
<DataGrid
id={`campaign-${campaignId}-fields`}
rows={fields}
columns={fieldColumns({ locked, globalValues, renameField, setField, setGlobalValue, setOverrideAllowed, deleteField })}
getRowKey={(_field, index) => `field-row-${index}`}
emptyText="No campaign fields configured yet."
className="field-editor-table-wrap field-editor-table"
/>
</Card>
<div className="button-row page-bottom-actions">
@@ -212,6 +186,28 @@ export default function CampaignFieldsPage({ settings, campaignId }: { settings:
);
}
type FieldColumnContext = {
locked: boolean;
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;
deleteField: (index: number) => void;
};
function fieldColumns({ locked, globalValues, renameField, setField, setGlobalValue, setOverrideAllowed, 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 },
{ id: "type", header: "Type", width: 140, sortable: true, filterable: true, render: (field, index) => <select value={field.type} disabled={locked} onChange={(event) => setField(index, { type: normalizeFieldType(event.target.value) })}>{fieldTypeOptions.map((option) => <option key={option} value={option}>{option}</option>)}</select>, value: (field) => field.type },
{ 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> }
];
}
function normalizeFields(value: unknown): CampaignFieldDefinition[] {
if (!Array.isArray(value)) return [];
return value.filter(isRecord).map((field) => ({