chore: sync GovOPlaN module split state

This commit is contained in:
2026-07-10 12:51:18 +02:00
parent 57f6066bf7
commit 52e6ed49c5
66 changed files with 8868 additions and 4072 deletions

View File

@@ -8,7 +8,7 @@ import DataGrid, { DataGridEmptyAction, DataGridRowActions, type DataGridColumn
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 } from "@govoplan/core-webui";
import { insertAfter, moveArrayItem, i18nMessage } from "@govoplan/core-webui";
import { asRecord } from "../utils/campaignView";
import { renderTemplatePreviewText } from "../utils/templatePlaceholders";
@@ -57,7 +57,7 @@ export default function AttachmentRulesOverlay({
campaignId,
disabled = false,
buttonLabel,
emptyText = "No attachment files or matching rules configured yet.",
emptyText = "i18n:govoplan-campaign.no_attachment_files_or_matching_rules_configured.c6948b49",
basePaths = [],
zipConfig = { enabled: false, archives: [] },
filesModuleInstalled = false,
@@ -91,13 +91,13 @@ export default function AttachmentRulesOverlay({
className="attachment-rules-modal"
bodyClassName="attachment-rules-body"
onClose={cancelOverlay}
footer={(
<>
<Button onClick={cancelOverlay}>Cancel</Button>
<Button variant="primary" onClick={saveOverlay} disabled={disabled}>Save</Button>
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}
@@ -109,20 +109,20 @@ export default function AttachmentRulesOverlay({
filesModuleInstalled={filesModuleInstalled}
previewContext={previewContext}
activeChooserRuleIndex={null}
onChange={setDraftRules}
/>
onChange={setDraftRules} />
</Dialog>,
document.body
) : null;
return (
<>
<Button className="attachment-summary-button" onClick={openOverlay} disabled={disabled && rules.length === 0} title={`${summary.direct} direct file(s), ${summary.rules} rule(s) / pattern(s)`}>
<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[] {
@@ -141,8 +141,8 @@ export function AttachmentRulesTable({
<div className="attachment-rules-main">
<AttachmentRulesDataGrid {...tableProps} onChange={onChange} />
</div>
</div>
);
</div>);
}
export function AttachmentRulesDataGrid({
@@ -150,7 +150,7 @@ export function AttachmentRulesDataGrid({
settings,
campaignId,
disabled = false,
emptyText = "No attachment files or matching rules configured yet.",
emptyText = "i18n:govoplan-campaign.no_attachment_files_or_matching_rules_configured.c6948b49",
basePaths = [],
id = "attachment-rules",
activeChooserRuleIndex = null,
@@ -196,10 +196,10 @@ export function AttachmentRulesDataGrid({
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;
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 });
}
@@ -225,30 +225,30 @@ export function AttachmentRulesDataGrid({
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 ? "No attachment source is enabled for individual attachments." : emptyText}
emptyAction={<DataGridEmptyAction onAdd={() => addRule(-1)} disabled={disabled || basePaths.length === 0} label="Add first attachment" />}
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}
/>
)}
</>
);
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 = {
@@ -267,122 +267,154 @@ type AttachmentRuleColumnContext = {
function attachmentRuleColumns({ disabled, rules, basePaths, zipConfig, filesModuleInstalled, activeChooserRuleIndex: _activeChooserRuleIndex, patchRule, addRule, moveRule, 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,
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 ? "No individual attachment source" : "Unavailable attachment source"}</option>
)}
{ 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 ?? "")
</select>);
},
{
id: "file_filter",
header: "File / pattern",
width: "minmax(260px, 1fr)",
resizable: true,
sortable: true,
filterable: true,
render: (rule, index) => (
<div className="field-with-action split-field-action">
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 ? "Choose a managed file or pattern" : "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}>Choose</Button>}
</div>
),
value: (rule) => getText(rule, "file_filter")
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 }))]
},
...(zipConfig.enabled ? [{
id: "zip",
header: "ZIP archive",
width: 240,
sortable: true,
filterable: true,
columnType: "from-list",
list: {
options: [
{ value: "exclude", label: "Exclude from ZIP" },
{ value: "inherit", label: "Campaign standard" },
...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={`ZIP archive for ${getText(rule, "label") || `attachment ${index + 1}`}`}
onChange={(event) => patchRule(index, { zip: { ...asRecord(rule.zip), archive_id: event.target.value } })}
>
<option value="exclude">Exclude from ZIP</option>
<option value="inherit">Campaign standard</option>
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: "Required", width: 175, sortable: true, filterable: true, columnType: "from-list", list: { options: [{ value: "required", label: "Required" }, { value: "optional", label: "Optional" }] }, 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: "actions",
header: "Actions",
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="Add attachment below"
removeLabel="Remove attachment"
moveUpLabel="Move attachment up"
moveDownLabel="Move attachment down"
/>
)
}
];
</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" />
}];
}