421 lines
17 KiB
TypeScript
421 lines
17 KiB
TypeScript
import { useMemo, useState } from "react";
|
|
import { createPortal } from "react-dom";
|
|
import type { ApiSettings } from "../../../types";
|
|
import { Button } from "@govoplan/core-webui";
|
|
import { Dialog } from "@govoplan/core-webui";
|
|
import { usePlatformUiCapability, type FilesFileExplorerUiCapability, type FilesManagedAttachmentSelection } from "@govoplan/core-webui";
|
|
import DataGrid, { DataGridEmptyAction, DataGridRowActions, type DataGridColumn } from "../../../components/table/DataGrid";
|
|
import { ToggleSwitch } from "@govoplan/core-webui";
|
|
import { getBool, getText } from "../utils/draftEditor";
|
|
import { attachmentRuleZipSelection, createAttachmentRule, nextAttachmentLabel, summarizeAttachmentRules, type AttachmentBasePath, type AttachmentRule, type AttachmentZipCollection } from "../utils/attachments";
|
|
import { insertAfter, moveArrayItem, i18nMessage } from "@govoplan/core-webui";
|
|
import { asRecord } from "../utils/campaignView";
|
|
import { renderTemplatePreviewText } from "../utils/templatePlaceholders";
|
|
|
|
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[];
|
|
zipConfig?: AttachmentZipCollection;
|
|
filesModuleInstalled?: boolean;
|
|
previewContext?: Record<string, string>;
|
|
onChange: (rules: AttachmentRule[]) => void;
|
|
};
|
|
|
|
type AttachmentRulesTableProps = {
|
|
rules: AttachmentRule[];
|
|
settings: ApiSettings;
|
|
campaignId: string;
|
|
disabled?: boolean;
|
|
emptyText?: string;
|
|
basePaths?: AttachmentBasePath[];
|
|
id?: string;
|
|
activeChooserRuleIndex?: number | null;
|
|
zipConfig?: AttachmentZipCollection;
|
|
filesModuleInstalled?: boolean;
|
|
previewContext?: Record<string, string>;
|
|
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 = "i18n:govoplan-campaign.no_attachment_files_or_matching_rules_configured.c6948b49",
|
|
basePaths = [],
|
|
zipConfig = { enabled: false, archives: [] },
|
|
filesModuleInstalled = false,
|
|
previewContext,
|
|
onChange
|
|
}: AttachmentRulesOverlayProps) {
|
|
const [open, setOpen] = useState(false);
|
|
const [draftRules, setDraftRules] = useState<AttachmentRule[]>([]);
|
|
const summary = useMemo(() => summarizeAttachmentRules(rules), [rules]);
|
|
const label = buttonLabel ?? `direct: ${summary.direct} / rules: ${summary.rules}`;
|
|
|
|
function openOverlay() {
|
|
setDraftRules(cloneAttachmentRules(rules));
|
|
setOpen(true);
|
|
}
|
|
|
|
function cancelOverlay() {
|
|
setOpen(false);
|
|
}
|
|
|
|
function saveOverlay() {
|
|
if (disabled) return;
|
|
onChange(draftRules);
|
|
setOpen(false);
|
|
}
|
|
|
|
const dialog = open ? createPortal(
|
|
<Dialog
|
|
open
|
|
title={title}
|
|
className="attachment-rules-modal"
|
|
bodyClassName="attachment-rules-body"
|
|
onClose={cancelOverlay}
|
|
footer={
|
|
<>
|
|
<Button onClick={cancelOverlay}>i18n:govoplan-campaign.cancel.77dfd213</Button>
|
|
<Button variant="primary" onClick={saveOverlay} disabled={disabled}>i18n:govoplan-campaign.save.efc007a3</Button>
|
|
</>
|
|
}>
|
|
|
|
<AttachmentRulesTable
|
|
rules={draftRules}
|
|
settings={settings}
|
|
campaignId={campaignId}
|
|
disabled={disabled}
|
|
emptyText={emptyText}
|
|
basePaths={basePaths}
|
|
zipConfig={zipConfig}
|
|
filesModuleInstalled={filesModuleInstalled}
|
|
previewContext={previewContext}
|
|
activeChooserRuleIndex={null}
|
|
onChange={setDraftRules} />
|
|
|
|
</Dialog>,
|
|
document.body
|
|
) : null;
|
|
|
|
return (
|
|
<>
|
|
<Button className="attachment-summary-button" onClick={openOverlay} disabled={disabled && rules.length === 0} title={i18nMessage("i18n:govoplan-campaign.value_direct_file_s_value_rule_s_pattern_s.df9b46d9", { value0: summary.direct, value1: summary.rules })}>
|
|
{label}
|
|
</Button>
|
|
{dialog}
|
|
</>);
|
|
|
|
}
|
|
|
|
function cloneAttachmentRules(rules: AttachmentRule[]): AttachmentRule[] {
|
|
return rules.map((rule) => ({
|
|
...rule,
|
|
...(rule.zip && typeof rule.zip === "object" ? { zip: { ...asRecord(rule.zip) } } : {})
|
|
}));
|
|
}
|
|
|
|
export function AttachmentRulesTable({
|
|
onChange,
|
|
...tableProps
|
|
}: AttachmentRulesTableProps) {
|
|
return (
|
|
<div className="attachment-rules-editor">
|
|
<div className="attachment-rules-main">
|
|
<AttachmentRulesDataGrid {...tableProps} onChange={onChange} />
|
|
</div>
|
|
</div>);
|
|
|
|
}
|
|
|
|
export function AttachmentRulesDataGrid({
|
|
rules,
|
|
settings,
|
|
campaignId,
|
|
disabled = false,
|
|
emptyText = "i18n:govoplan-campaign.no_attachment_files_or_matching_rules_configured.c6948b49",
|
|
basePaths = [],
|
|
id = "attachment-rules",
|
|
activeChooserRuleIndex = null,
|
|
zipConfig = { enabled: false, archives: [] },
|
|
filesModuleInstalled = false,
|
|
previewContext,
|
|
onOpenFileChooser,
|
|
onChange
|
|
}: AttachmentRulesTableProps) {
|
|
const [fileChooser, setFileChooser] = useState<FileChooserState | null>(null);
|
|
const filesFileExplorer = usePlatformUiCapability<FilesFileExplorerUiCapability>("files.fileExplorer");
|
|
const ManagedFileChooser = filesModuleInstalled ? filesFileExplorer?.ManagedFileChooser : null;
|
|
const managedFilesAvailable = Boolean(ManagedFileChooser);
|
|
|
|
function patchRule(index: number, patch: Partial<AttachmentRule>) {
|
|
onChange(rules.map((rule, currentIndex) => currentIndex === index ? { ...rule, ...patch } : rule));
|
|
}
|
|
|
|
function addRule(afterIndex = rules.length - 1) {
|
|
if (disabled || basePaths.length === 0) return;
|
|
const nextRule = createAttachmentRule(basePaths[0].path, nextAttachmentLabel(rules), basePaths[0].id);
|
|
onChange(insertAfter(rules, afterIndex, nextRule));
|
|
}
|
|
|
|
function removeRule(index: number) {
|
|
setFileChooser(null);
|
|
onChange(rules.filter((_, currentIndex) => currentIndex !== index));
|
|
}
|
|
|
|
function moveRule(index: number, targetIndex: number) {
|
|
if (disabled || index === targetIndex) return;
|
|
setFileChooser(null);
|
|
onChange(moveArrayItem(rules, index, targetIndex));
|
|
}
|
|
|
|
function openFileChooser(ruleIndex: number) {
|
|
if (!managedFilesAvailable) return;
|
|
if (onOpenFileChooser) {
|
|
onOpenFileChooser(ruleIndex);
|
|
return;
|
|
}
|
|
const rule = rules[ruleIndex] ?? {};
|
|
const currentPath = getText(rule, "base_dir");
|
|
const currentBasePathId = getText(rule, "base_path_id");
|
|
const explicitlyReferenced = Boolean(currentBasePathId || currentPath);
|
|
const basePath = basePaths.find((item) => item.id === currentBasePathId) ??
|
|
basePaths.find((item) => item.path === currentPath) ?? (
|
|
!explicitlyReferenced ? basePaths[0] : undefined) ??
|
|
null;
|
|
if (!basePath) return;
|
|
setFileChooser({ ruleIndex, basePath });
|
|
}
|
|
|
|
function selectAttachment(selection: FilesManagedAttachmentSelection) {
|
|
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 (
|
|
<>
|
|
<DataGrid
|
|
id={id}
|
|
rows={rules}
|
|
columns={attachmentRuleColumns({ disabled, rules, basePaths, zipConfig, filesModuleInstalled: managedFilesAvailable, activeChooserRuleIndex: activeChooserRuleIndex ?? fileChooser?.ruleIndex ?? null, patchRule, addRule, moveRule, openFileChooser, removeRule })}
|
|
getRowKey={(rule, index) => String(rule.id ?? index)}
|
|
emptyText={basePaths.length === 0 ? "i18n:govoplan-campaign.no_attachment_source_is_enabled_for_individual_a.818a2820" : emptyText}
|
|
emptyAction={<DataGridEmptyAction onAdd={() => addRule(-1)} disabled={disabled || basePaths.length === 0} label="i18n:govoplan-campaign.add_first_attachment.025fbf31" />}
|
|
className="attachment-rules-table-wrap attachment-rules-table"
|
|
rowClassName={(_rule, index) => (activeChooserRuleIndex ?? fileChooser?.ruleIndex) === index ? "is-choosing-file" : undefined} />
|
|
|
|
{fileChooser && ManagedFileChooser &&
|
|
<ManagedFileChooser
|
|
open
|
|
settings={settings}
|
|
storageScope={campaignId}
|
|
linkTarget={{ type: "campaign", id: campaignId, label: "campaign" }}
|
|
mode="attachment"
|
|
source={fileChooser.basePath?.source}
|
|
basePath={fileChooser.basePath?.path ?? "."}
|
|
initialPattern={getText(rules[fileChooser.ruleIndex], "file_filter")}
|
|
rememberKey={`${id}:${String(rules[fileChooser.ruleIndex]?.id ?? fileChooser.ruleIndex)}`}
|
|
previewContext={previewContext}
|
|
renderPatternPreview={(pattern, context) => renderTemplatePreviewText(pattern, context, false)}
|
|
onClose={() => setFileChooser(null)}
|
|
onSelectAttachment={selectAttachment} />
|
|
|
|
}
|
|
</>);
|
|
|
|
}
|
|
|
|
type AttachmentRuleColumnContext = {
|
|
disabled: boolean;
|
|
rules: AttachmentRule[];
|
|
basePaths: AttachmentBasePath[];
|
|
zipConfig: AttachmentZipCollection;
|
|
filesModuleInstalled: boolean;
|
|
activeChooserRuleIndex: number | null;
|
|
patchRule: (index: number, patch: Partial<AttachmentRule>) => void;
|
|
addRule: (afterIndex?: number) => void;
|
|
moveRule: (index: number, targetIndex: number) => void;
|
|
openFileChooser: (ruleIndex: number) => void;
|
|
removeRule: (index: number) => void;
|
|
};
|
|
|
|
function attachmentRuleColumns({ disabled, rules, basePaths, zipConfig, filesModuleInstalled, activeChooserRuleIndex: _activeChooserRuleIndex, patchRule, addRule, moveRule, openFileChooser, removeRule }: AttachmentRuleColumnContext): DataGridColumn<AttachmentRule>[] {
|
|
return [
|
|
{ id: "label", header: "i18n:govoplan-campaign.label.74341e3c", width: 190, resizable: true, sortable: true, filterable: true, sticky: "start", render: (rule, index) => <input value={getText(rule, "label")} disabled={disabled} placeholder="i18n:govoplan-campaign.attachment_label.a340f70e" onChange={(event) => patchRule(index, { label: event.target.value })} />, value: (rule) => getText(rule, "label") },
|
|
{
|
|
id: "base_path",
|
|
header: "i18n:govoplan-campaign.base_path.6a4867ca",
|
|
width: 250,
|
|
sortable: true,
|
|
filterable: true,
|
|
columnType: "from-list",
|
|
list: { options: basePaths.map((basePath) => ({ value: basePath.path, label: basePath.name || basePath.path })) },
|
|
render: (rule, index) => {
|
|
const currentBasePathValue = getText(rule, "base_dir");
|
|
const currentBasePathId = getText(rule, "base_path_id");
|
|
const explicitlyReferenced = Boolean(currentBasePathId || currentBasePathValue);
|
|
const selectedBasePath = basePaths.find((basePath) => basePath.id === currentBasePathId) ??
|
|
basePaths.find((basePath) => basePath.path === currentBasePathValue) ?? (
|
|
!explicitlyReferenced ? basePaths[0] : undefined);
|
|
return (
|
|
<select
|
|
value={selectedBasePath?.id ?? ""}
|
|
disabled={disabled || basePaths.length === 0}
|
|
onChange={(event) => {
|
|
const nextBasePath = basePaths.find((basePath) => basePath.id === event.target.value);
|
|
if (nextBasePath) patchRule(index, { base_path_id: nextBasePath.id, base_dir: nextBasePath.path });
|
|
}}>
|
|
|
|
{!selectedBasePath &&
|
|
<option value="" disabled>{basePaths.length === 0 ? "i18n:govoplan-campaign.no_individual_attachment_source.6b54176a" : "i18n:govoplan-campaign.unavailable_attachment_source.7ff1096e"}</option>
|
|
}
|
|
{basePaths.map((basePath) => <option key={basePath.id} value={basePath.id}>{basePath.name || basePath.path}</option>)}
|
|
</select>);
|
|
|
|
},
|
|
value: (rule) => getText(rule, "base_dir", basePaths[0]?.path ?? "")
|
|
},
|
|
{
|
|
id: "file_filter",
|
|
header: "i18n:govoplan-campaign.file_pattern.86320b07",
|
|
width: "minmax(260px, 1fr)",
|
|
resizable: true,
|
|
sortable: true,
|
|
filterable: true,
|
|
render: (rule, index) =>
|
|
<div className="field-with-action split-field-action">
|
|
<input
|
|
className="chooser-display-input"
|
|
value={getText(rule, "file_filter")}
|
|
disabled={disabled || basePaths.length === 0}
|
|
readOnly={filesModuleInstalled}
|
|
tabIndex={filesModuleInstalled ? -1 : undefined}
|
|
placeholder={filesModuleInstalled ? "i18n:govoplan-campaign.choose_a_managed_file_or_pattern.96bb3bfb" : "file.pdf or **/*.pdf"}
|
|
onChange={(event) => {
|
|
if (!filesModuleInstalled) patchRule(index, { file_filter: event.target.value });
|
|
}}
|
|
onClick={() => filesModuleInstalled && !disabled && openFileChooser(index)}
|
|
onKeyDown={(event) => {
|
|
if (filesModuleInstalled && !disabled && (event.key === "Enter" || event.key === " ")) {
|
|
event.preventDefault();
|
|
openFileChooser(index);
|
|
}
|
|
}} />
|
|
|
|
{filesModuleInstalled && <Button onClick={() => openFileChooser(index)} disabled={disabled || basePaths.length === 0}>i18n:govoplan-campaign.choose.78b7c9f6</Button>}
|
|
</div>,
|
|
|
|
value: (rule) => getText(rule, "file_filter")
|
|
},
|
|
{
|
|
id: "message_filename_template",
|
|
header: "i18n:govoplan-campaign.message_filename_template.0b7ac934",
|
|
width: 230,
|
|
resizable: true,
|
|
sortable: true,
|
|
filterable: true,
|
|
render: (rule, index) =>
|
|
<input
|
|
value={getText(rule, "message_filename_template")}
|
|
disabled={disabled}
|
|
placeholder="i18n:govoplan-campaign.keep_original_filename.9b805045"
|
|
onChange={(event) => patchRule(index, { message_filename_template: event.target.value })} />,
|
|
|
|
value: (rule) => getText(rule, "message_filename_template")
|
|
},
|
|
{
|
|
id: "zip_entry_name_template",
|
|
header: "i18n:govoplan-campaign.zip_entry_name_template.83772a73",
|
|
width: 230,
|
|
resizable: true,
|
|
sortable: true,
|
|
filterable: true,
|
|
render: (rule, index) =>
|
|
<input
|
|
value={getText(rule, "zip_entry_name_template")}
|
|
disabled={disabled}
|
|
placeholder="i18n:govoplan-campaign.keep_original_zip_entry_name.d51558c4"
|
|
onChange={(event) => patchRule(index, { zip_entry_name_template: event.target.value })} />,
|
|
|
|
value: (rule) => getText(rule, "zip_entry_name_template")
|
|
},
|
|
...(zipConfig.enabled ? [{
|
|
id: "zip",
|
|
header: "i18n:govoplan-campaign.zip_archive.5a2430dd",
|
|
width: 240,
|
|
sortable: true,
|
|
filterable: true,
|
|
columnType: "from-list",
|
|
list: {
|
|
options: [
|
|
{ value: "exclude", label: "i18n:govoplan-campaign.exclude_from_zip.a6422da1" },
|
|
{ value: "inherit", label: "i18n:govoplan-campaign.campaign_standard.c88ce44c" },
|
|
...zipConfig.archives.map((archive) => ({ value: archive.id, label: archive.name }))]
|
|
|
|
},
|
|
render: (rule: AttachmentRule, index: number) => {
|
|
const selection = attachmentRuleZipSelection(rule);
|
|
return (
|
|
<select
|
|
value={selection}
|
|
disabled={disabled || zipConfig.archives.length === 0}
|
|
aria-label={i18nMessage("i18n:govoplan-campaign.zip_archive_for_value.3dfaf812", { value0: getText(rule, "label") || `attachment ${index + 1}` })}
|
|
onChange={(event) => patchRule(index, { zip: { ...asRecord(rule.zip), archive_id: event.target.value } })}>
|
|
|
|
<option value="exclude">i18n:govoplan-campaign.exclude_from_zip.a6422da1</option>
|
|
<option value="inherit">i18n:govoplan-campaign.campaign_standard.c88ce44c</option>
|
|
{zipConfig.archives.map((archive) => <option key={archive.id} value={archive.id}>{archive.name}</option>)}
|
|
</select>);
|
|
|
|
},
|
|
value: (rule: AttachmentRule) => attachmentRuleZipSelection(rule)
|
|
} satisfies DataGridColumn<AttachmentRule>] : []),
|
|
{ id: "required", header: "i18n:govoplan-campaign.required.eed6bfb4", width: 175, sortable: true, filterable: true, columnType: "from-list", list: { options: [{ value: "required", label: "i18n:govoplan-campaign.required.eed6bfb4" }, { value: "optional", label: "i18n:govoplan-campaign.optional.0c6c4102" }] }, render: (rule, index) => <ToggleSwitch label="i18n:govoplan-campaign.required.eed6bfb4" checked={getBool(rule, "required", true)} disabled={disabled} onChange={(checked) => patchRule(index, { required: checked })} />, value: (rule) => getBool(rule, "required", true) ? "required" : "optional" },
|
|
{
|
|
id: "actions",
|
|
header: "i18n:govoplan-campaign.actions.c3cd636a",
|
|
width: 180,
|
|
sticky: "end",
|
|
render: (_rule, index) =>
|
|
<DataGridRowActions
|
|
disabled={disabled}
|
|
onAddBelow={() => addRule(index)}
|
|
onRemove={() => removeRule(index)}
|
|
onMoveUp={index > 0 ? () => moveRule(index, index - 1) : undefined}
|
|
onMoveDown={index < rules.length - 1 ? () => moveRule(index, index + 1) : undefined}
|
|
addLabel="i18n:govoplan-campaign.add_attachment_below.c7b66ffd"
|
|
removeLabel="i18n:govoplan-campaign.remove_attachment.0fcc6594"
|
|
moveUpLabel="i18n:govoplan-campaign.move_attachment_up.28614804"
|
|
moveDownLabel="i18n:govoplan-campaign.move_attachment_down.a4d6604f" />
|
|
|
|
|
|
}];
|
|
|
|
}
|