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

@@ -1,6 +1,7 @@
import { useMemo, useState } from "react";
import { createPortal } from "react-dom";
import Button from "../../../components/Button";
import DataGrid, { type DataGridColumn } from "../../../components/table/DataGrid";
import ToggleSwitch from "../../../components/ToggleSwitch";
import { getBool, getText } from "../utils/draftEditor";
import { createAttachmentRule, mockAttachmentFiles, summarizeAttachmentRules, type AttachmentBasePath, type AttachmentRule } from "../utils/attachments";
@@ -22,6 +23,7 @@ type AttachmentRulesTableProps = {
disabled?: boolean;
emptyText?: string;
basePaths?: AttachmentBasePath[];
id?: string;
showAddButton?: boolean;
activeChooserRuleIndex?: number | null;
onOpenFileChooser?: (ruleIndex: number) => void;
@@ -124,25 +126,44 @@ export default function AttachmentRulesOverlay({
}
export function AttachmentRulesTable({
showAddButton = true,
onChange,
...tableProps
}: AttachmentRulesTableProps) {
function addRule() {
onChange([...tableProps.rules, createAttachmentRule(tableProps.basePaths?.[0]?.path ?? "")]);
}
return (
<div className="attachment-rules-editor">
<div className="attachment-rules-main">
<AttachmentRulesDataGrid {...tableProps} onChange={onChange} />
{showAddButton && (
<div className="button-row compact-actions attachment-rules-footer-actions">
<Button onClick={addRule} disabled={tableProps.disabled}>Add file</Button>
</div>
)}
</div>
</div>
);
}
export function AttachmentRulesDataGrid({
rules,
disabled = false,
emptyText = "No attachment files or matching rules configured yet.",
basePaths = [],
showAddButton = true,
id = "attachment-rules",
activeChooserRuleIndex = null,
onOpenFileChooser,
onChange
}: AttachmentRulesTableProps) {
}: Omit<AttachmentRulesTableProps, "showAddButton">) {
const [fileChooser, setFileChooser] = useState<FileChooserState | null>(null);
function patchRule(index: number, patch: Partial<AttachmentRule>) {
onChange(rules.map((rule, currentIndex) => currentIndex === index ? { ...rule, ...patch } : rule));
}
function addRule() {
onChange([...rules, createAttachmentRule(basePaths[0]?.path ?? "")]);
}
function removeRule(index: number) {
setFileChooser(null);
onChange(rules.filter((_, currentIndex) => currentIndex !== index));
@@ -169,75 +190,20 @@ export function AttachmentRulesTable({
: "";
return (
<div className="attachment-rules-editor">
<div className="attachment-rules-main">
{rules.length === 0 ? (
<p className="muted attachment-rules-empty">{emptyText}</p>
) : (
<div className="app-table-wrap attachment-rules-table-wrap">
<table className="app-table attachment-rules-table">
<thead>
<tr>
<th>Label</th>
<th>Base path</th>
<th>File / pattern</th>
<th>Required</th>
<th>Subdirs</th>
<th></th>
</tr>
</thead>
<tbody>
{rules.map((rule, index) => {
const currentBasePath = getText(rule, "base_dir", basePaths[0]?.path ?? "");
const isChoosingFile = (activeChooserRuleIndex ?? fileChooser?.ruleIndex) === index;
return (
<tr key={String(rule.id ?? index)} className={isChoosingFile ? "is-choosing-file" : undefined}>
<td><input value={getText(rule, "label")} disabled={disabled} placeholder="Attachment label" onChange={(event) => patchRule(index, { label: event.target.value })} /></td>
<td>
{basePaths.length > 0 ? (
<select value={currentBasePath} disabled={disabled} onChange={(event) => patchRule(index, { base_dir: event.target.value })}>
{basePaths.map((basePath) => <option key={basePath.id} value={basePath.path}>{basePath.name || basePath.path}</option>)}
</select>
) : (
<input value={currentBasePath} disabled={disabled} readOnly placeholder="optional/folder" />
)}
</td>
<td>
<div className="field-with-action">
<input
className="chooser-display-input"
value={getText(rule, "file_filter")}
disabled={disabled}
readOnly
tabIndex={-1}
placeholder="file.pdf or {{local:id}}.pdf"
onClick={() => !disabled && openFileChooser(index)}
onKeyDown={(event) => {
if (!disabled && (event.key === "Enter" || event.key === " ")) {
event.preventDefault();
openFileChooser(index);
}
}}
/>
<Button onClick={() => openFileChooser(index)} disabled={disabled}>Choose</Button>
</div>
</td>
<td><ToggleSwitch label="Required" checked={getBool(rule, "required", true)} disabled={disabled} onChange={(checked) => patchRule(index, { required: checked })} /></td>
<td><ToggleSwitch label="Subdirs" checked={getBool(rule, "include_subdirs")} disabled={disabled} onChange={(checked) => patchRule(index, { include_subdirs: checked })} /></td>
<td className="table-action-cell"><Button variant="danger" onClick={() => removeRule(index)} disabled={disabled}>Remove</Button></td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
{showAddButton && (
<div className="button-row compact-actions attachment-rules-footer-actions">
<Button onClick={addRule} disabled={disabled}>Add file</Button>
</div>
)}
</div>
<>
{rules.length === 0 ? (
<p className="muted attachment-rules-empty">{emptyText}</p>
) : (
<DataGrid
id={id}
rows={rules}
columns={attachmentRuleColumns({ disabled, basePaths, activeChooserRuleIndex: activeChooserRuleIndex ?? fileChooser?.ruleIndex ?? null, patchRule, openFileChooser, removeRule })}
getRowKey={(rule, index) => String(rule.id ?? index)}
emptyText={emptyText}
className="attachment-rules-table-wrap attachment-rules-table"
rowClassName={(_rule, index) => (activeChooserRuleIndex ?? fileChooser?.ruleIndex) === index ? "is-choosing-file" : undefined}
/>
)}
{fileChooser && (
<MockFileChooserOverlay
basePath={activeBasePath}
@@ -245,10 +211,75 @@ export function AttachmentRulesTable({
onClose={() => setFileChooser(null)}
/>
)}
</div>
</>
);
}
type AttachmentRuleColumnContext = {
disabled: boolean;
basePaths: AttachmentBasePath[];
activeChooserRuleIndex: number | null;
patchRule: (index: number, patch: Partial<AttachmentRule>) => void;
openFileChooser: (ruleIndex: number) => void;
removeRule: (index: number) => void;
};
function attachmentRuleColumns({ disabled, basePaths, activeChooserRuleIndex: _activeChooserRuleIndex, patchRule, openFileChooser, removeRule }: AttachmentRuleColumnContext): DataGridColumn<AttachmentRule>[] {
return [
{ id: "label", header: "Label", width: 190, resizable: true, sortable: true, filterable: true, sticky: "start", render: (rule, index) => <input value={getText(rule, "label")} disabled={disabled} placeholder="Attachment label" onChange={(event) => patchRule(index, { label: event.target.value })} />, value: (rule) => getText(rule, "label") },
{
id: "base_path",
header: "Base path",
width: 250,
sortable: true,
filterable: true,
render: (rule, index) => {
const currentBasePath = getText(rule, "base_dir", basePaths[0]?.path ?? "");
return basePaths.length > 0 ? (
<select value={currentBasePath} disabled={disabled} onChange={(event) => patchRule(index, { base_dir: event.target.value })}>
{basePaths.map((basePath) => <option key={basePath.id} value={basePath.path}>{basePath.name || basePath.path}</option>)}
</select>
) : (
<input value={currentBasePath} disabled={disabled} readOnly placeholder="optional/folder" />
);
},
value: (rule) => getText(rule, "base_dir", basePaths[0]?.path ?? "")
},
{
id: "file_filter",
header: "File / pattern",
width: "minmax(260px, 1fr)",
resizable: true,
sortable: true,
filterable: true,
render: (rule, index) => (
<div className="field-with-action">
<input
className="chooser-display-input"
value={getText(rule, "file_filter")}
disabled={disabled}
readOnly
tabIndex={-1}
placeholder="file.pdf or {{local:id}}.pdf"
onClick={() => !disabled && openFileChooser(index)}
onKeyDown={(event) => {
if (!disabled && (event.key === "Enter" || event.key === " ")) {
event.preventDefault();
openFileChooser(index);
}
}}
/>
<Button onClick={() => openFileChooser(index)} disabled={disabled}>Choose</Button>
</div>
),
value: (rule) => getText(rule, "file_filter")
},
{ id: "required", header: "Required", width: 175, sortable: true, filterable: true, render: (rule, index) => <ToggleSwitch label="Required" checked={getBool(rule, "required", true)} disabled={disabled} onChange={(checked) => patchRule(index, { required: checked })} />, value: (rule) => getBool(rule, "required", true) ? "required" : "optional" },
{ id: "subdirs", header: "Subdirs", width: 165, sortable: true, filterable: true, render: (rule, index) => <ToggleSwitch label="Subdirs" checked={getBool(rule, "include_subdirs")} disabled={disabled} onChange={(checked) => patchRule(index, { include_subdirs: checked })} />, value: (rule) => getBool(rule, "include_subdirs") ? "subdirs" : "current" },
{ id: "actions", header: "", width: 120, sticky: "end", render: (_rule, index) => <Button variant="danger" onClick={() => removeRule(index)} disabled={disabled}>Remove</Button> }
];
}
function MockFileChooserOverlay({ basePath, onSelect, onClose }: { basePath: string; onSelect: (fileFilter: string) => void; onClose: () => void }) {
return createPortal(
<div className="overlay-backdrop" role="dialog" aria-modal="true" aria-labelledby="mock-file-chooser-title">