import { useMemo, useState } from "react";
import { createPortal } from "react-dom";
import type { ApiSettings } from "../../../types";
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, nextAttachmentLabel, summarizeAttachmentRules, type AttachmentBasePath, type AttachmentRule } from "../utils/attachments";
import ManagedFileChooser, { type ManagedAttachmentSelection } from "./ManagedFileChooser";
export type { AttachmentBasePath, AttachmentRule } from "../utils/attachments";
type AttachmentRulesOverlayProps = {
title: string;
rules: AttachmentRule[];
settings: ApiSettings;
campaignId: string;
disabled?: boolean;
buttonLabel?: string;
emptyText?: string;
basePaths?: AttachmentBasePath[];
onChange: (rules: AttachmentRule[]) => void;
};
type AttachmentRulesTableProps = {
rules: AttachmentRule[];
settings: ApiSettings;
campaignId: string;
disabled?: boolean;
emptyText?: string;
basePaths?: AttachmentBasePath[];
id?: string;
showAddButton?: boolean;
activeChooserRuleIndex?: number | null;
onOpenFileChooser?: (ruleIndex: number) => void;
onChange: (rules: AttachmentRule[]) => void;
};
type FileChooserState = {
ruleIndex: number;
basePath: AttachmentBasePath | null;
};
export default function AttachmentRulesOverlay({
title,
rules,
settings,
campaignId,
disabled = false,
buttonLabel,
emptyText = "No attachment files or matching rules configured yet.",
basePaths = [],
onChange
}: AttachmentRulesOverlayProps) {
const [open, setOpen] = useState(false);
const summary = useMemo(() => summarizeAttachmentRules(rules), [rules]);
const label = buttonLabel ?? `direct: ${summary.direct} / rules: ${summary.rules}`;
function closeOverlay() {
setOpen(false);
}
function addOverlayRule() {
onChange([...rules, createAttachmentRule(basePaths[0]?.path ?? "", nextAttachmentLabel(rules), basePaths[0]?.id ?? "")]);
}
const dialog = open ? createPortal(
,
document.body
) : null;
return (
<>
{dialog}
>
);
}
export function AttachmentRulesTable({
showAddButton = true,
onChange,
...tableProps
}: AttachmentRulesTableProps) {
function addRule() {
onChange([
...tableProps.rules,
createAttachmentRule(tableProps.basePaths?.[0]?.path ?? "", nextAttachmentLabel(tableProps.rules), tableProps.basePaths?.[0]?.id ?? "")
]);
}
return (
{showAddButton && (
)}
);
}
export function AttachmentRulesDataGrid({
rules,
settings,
campaignId,
disabled = false,
emptyText = "No attachment files or matching rules configured yet.",
basePaths = [],
id = "attachment-rules",
activeChooserRuleIndex = null,
onOpenFileChooser,
onChange
}: AttachmentRulesTableProps) {
const [fileChooser, setFileChooser] = useState(null);
function patchRule(index: number, patch: Partial) {
onChange(rules.map((rule, currentIndex) => currentIndex === index ? { ...rule, ...patch } : rule));
}
function removeRule(index: number) {
setFileChooser(null);
onChange(rules.filter((_, currentIndex) => currentIndex !== index));
}
function openFileChooser(ruleIndex: number) {
if (onOpenFileChooser) {
onOpenFileChooser(ruleIndex);
return;
}
const rule = rules[ruleIndex] ?? {};
const currentPath = getText(rule, "base_dir", basePaths[0]?.path ?? "");
const currentBasePathId = getText(rule, "base_path_id");
const basePath = basePaths.find((item) => item.id === currentBasePathId) ?? basePaths.find((item) => item.path === currentPath) ?? basePaths[0] ?? null;
setFileChooser({ ruleIndex, basePath });
}
function selectAttachment(selection: ManagedAttachmentSelection) {
if (!fileChooser) return;
const currentRule = rules[fileChooser.ruleIndex] ?? {};
patchRule(fileChooser.ruleIndex, {
base_path_id: fileChooser.basePath?.id ?? "",
base_dir: fileChooser.basePath?.path ?? (selection.folderPath || "."),
file_filter: selection.fileFilter,
type: selection.selectionType === "file" ? "direct" : "pattern",
include_subdirs: false,
label: getText(currentRule, "label") || `Attachment ${fileChooser.ruleIndex + 1}`
});
setFileChooser(null);
}
return (
<>
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 && (
setFileChooser(null)}
onSelectAttachment={selectAttachment}
/>
)}
>
);
}
type AttachmentRuleColumnContext = {
disabled: boolean;
basePaths: AttachmentBasePath[];
activeChooserRuleIndex: number | null;
patchRule: (index: number, patch: Partial) => void;
openFileChooser: (ruleIndex: number) => void;
removeRule: (index: number) => void;
};
function attachmentRuleColumns({ disabled, basePaths, activeChooserRuleIndex: _activeChooserRuleIndex, patchRule, openFileChooser, removeRule }: AttachmentRuleColumnContext): DataGridColumn[] {
return [
{ id: "label", header: "Label", width: 190, resizable: true, sortable: true, filterable: true, sticky: "start", render: (rule, index) => 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 currentBasePathValue = getText(rule, "base_dir", basePaths[0]?.path ?? "");
const currentBasePathId = getText(rule, "base_path_id");
const selectedBasePath = basePaths.find((basePath) => basePath.id === currentBasePathId) ?? basePaths.find((basePath) => basePath.path === currentBasePathValue) ?? basePaths[0];
return basePaths.length > 0 ? (
) : (
);
},
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) => (
!disabled && openFileChooser(index)}
onKeyDown={(event) => {
if (!disabled && (event.key === "Enter" || event.key === " ")) {
event.preventDefault();
openFileChooser(index);
}
}}
/>
),
value: (rule) => getText(rule, "file_filter")
},
{ id: "required", header: "Required", width: 175, sortable: true, filterable: true, render: (rule, index) => patchRule(index, { required: checked })} />, value: (rule) => getBool(rule, "required", true) ? "required" : "optional" },
{
id: "actions",
header: "",
width: 145,
sticky: "end",
render: (_rule, index) =>
}
];
}