feat(campaign-ui): configure attachment delivery behavior
This commit is contained in:
@@ -6,6 +6,7 @@ import { Button } from "@govoplan/core-webui";
|
|||||||
import { Card } from "@govoplan/core-webui";
|
import { Card } from "@govoplan/core-webui";
|
||||||
import { PageTitle } from "@govoplan/core-webui";
|
import { PageTitle } from "@govoplan/core-webui";
|
||||||
import { LoadingFrame } from "@govoplan/core-webui";
|
import { LoadingFrame } from "@govoplan/core-webui";
|
||||||
|
import { MetricCard } from "@govoplan/core-webui";
|
||||||
import LockedVersionNotice from "./components/LockedVersionNotice";
|
import LockedVersionNotice from "./components/LockedVersionNotice";
|
||||||
import VersionLine from "./components/VersionLine";
|
import VersionLine from "./components/VersionLine";
|
||||||
import { ToggleSwitch } from "@govoplan/core-webui";
|
import { ToggleSwitch } from "@govoplan/core-webui";
|
||||||
@@ -40,7 +41,7 @@ export default function AttachmentsDataPage({ settings, campaignId }: {settings:
|
|||||||
const [zipNameEditorIndex, setZipNameEditorIndex] = useState<number | null>(null);
|
const [zipNameEditorIndex, setZipNameEditorIndex] = useState<number | null>(null);
|
||||||
const version = data.currentVersion;
|
const version = data.currentVersion;
|
||||||
const locked = isAuditLockedVersion(version, data.campaign?.current_version_id);
|
const locked = isAuditLockedVersion(version, data.campaign?.current_version_id);
|
||||||
const { draft, setDraft, displayDraft, dirty, saveState, localError, patch, markDirty, saveDraft } = useCampaignDraftEditor({
|
const { draft, setDraft, displayDraft, dirty, saveState, localError, patch, markDirty, discardDraft, saveDraft } = useCampaignDraftEditor({
|
||||||
settings,
|
settings,
|
||||||
campaignId,
|
campaignId,
|
||||||
version,
|
version,
|
||||||
@@ -238,7 +239,7 @@ export default function AttachmentsDataPage({ settings, campaignId }: {settings:
|
|||||||
</div>
|
</div>
|
||||||
<div className="button-row compact-actions">
|
<div className="button-row compact-actions">
|
||||||
{filesModuleInstalled && <Button onClick={() => navigate("/files")}>i18n:govoplan-campaign.manage_files.90a419f7</Button>}
|
{filesModuleInstalled && <Button onClick={() => navigate("/files")}>i18n:govoplan-campaign.manage_files.90a419f7</Button>}
|
||||||
<Button onClick={reload} disabled={loading}>i18n:govoplan-campaign.reload.cce71553</Button>
|
<Button onClick={() => void discardDraft()} disabled={loading}>Discard</Button>
|
||||||
<Button variant="primary" onClick={() => saveDraft("manual")} disabled={!canSave}>i18n:govoplan-campaign.save.efc007a3</Button>
|
<Button variant="primary" onClick={() => saveDraft("manual")} disabled={!canSave}>i18n:govoplan-campaign.save.efc007a3</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -249,6 +250,13 @@ export default function AttachmentsDataPage({ settings, campaignId }: {settings:
|
|||||||
|
|
||||||
<LoadingFrame loading={loading || !draft} label="i18n:govoplan-campaign.loading_campaign_draft.1cf47e50">
|
<LoadingFrame loading={loading || !draft} label="i18n:govoplan-campaign.loading_campaign_draft.1cf47e50">
|
||||||
<>
|
<>
|
||||||
|
<div className="metric-grid attachment-statistics-grid">
|
||||||
|
<MetricCard label="i18n:govoplan-campaign.base_paths.edf35a97" value={basePaths.length} tone="neutral" />
|
||||||
|
<MetricCard label="i18n:govoplan-campaign.global_attachments.438263a1" value={`${globalSummary.direct} / ${globalSummary.rules}`} tone="info" detail="direct / rules" />
|
||||||
|
<MetricCard label="i18n:govoplan-campaign.per_recipient_patterns.53da30da" value={individualRulesCount} tone="neutral" />
|
||||||
|
<MetricCard label="i18n:govoplan-campaign.upload_support.1c54931c" value={managedFilesAvailable ? "Files" : "Manual"} tone={managedFilesAvailable ? "good" : filesModuleInstalled ? "warning" : "neutral"} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<Card title="i18n:govoplan-campaign.attachment_sources.8ef0a6ce">
|
<Card title="i18n:govoplan-campaign.attachment_sources.8ef0a6ce">
|
||||||
<div className="admin-table-surface attachment-sources-table-surface">
|
<div className="admin-table-surface attachment-sources-table-surface">
|
||||||
<DataGrid
|
<DataGrid
|
||||||
@@ -300,7 +308,6 @@ export default function AttachmentsDataPage({ settings, campaignId }: {settings:
|
|||||||
{zipArchiveNameValidation.message &&
|
{zipArchiveNameValidation.message &&
|
||||||
<DismissibleAlert tone="danger" compact resetKey={zipArchiveNameValidation.message} className="attachment-zip-name-error">{zipArchiveNameValidation.message}</DismissibleAlert>
|
<DismissibleAlert tone="danger" compact resetKey={zipArchiveNameValidation.message} className="attachment-zip-name-error">{zipArchiveNameValidation.message}</DismissibleAlert>
|
||||||
}
|
}
|
||||||
<p className="muted small-note">i18n:govoplan-campaign.archive_names_support_recipient_and_campaign_fie.d5b1b2d1</p>
|
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card title="i18n:govoplan-campaign.global_attachments.492bd841" collapsible>
|
<Card title="i18n:govoplan-campaign.global_attachments.492bd841" collapsible>
|
||||||
@@ -319,20 +326,6 @@ export default function AttachmentsDataPage({ settings, campaignId }: {settings:
|
|||||||
|
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card title="i18n:govoplan-campaign.statistics.2086b21f" collapsible>
|
|
||||||
<dl className="detail-list">
|
|
||||||
<div><dt>i18n:govoplan-campaign.base_paths.edf35a97</dt><dd>{basePaths.length}</dd></div>
|
|
||||||
<div><dt>i18n:govoplan-campaign.global_attachments.438263a1</dt><dd>direct: {globalSummary.direct} i18n:govoplan-campaign.rules.e5d36074 {globalSummary.rules}</dd></div>
|
|
||||||
<div><dt>i18n:govoplan-campaign.per_recipient_patterns.53da30da</dt><dd>{individualRulesCount}</dd></div>
|
|
||||||
<div><dt>i18n:govoplan-campaign.upload_support.1c54931c</dt><dd>{managedFilesAvailable ? "i18n:govoplan-campaign.connected_through_files.95007112" : "i18n:govoplan-campaign.manual_paths.8e20627a"}</dd></div>
|
|
||||||
</dl>
|
|
||||||
<p className="muted small-note">{managedFilesAvailable ?
|
|
||||||
"i18n:govoplan-campaign.files_are_managed_in_the_top_level_files_module_.4b370222" :
|
|
||||||
filesModuleInstalled ?
|
|
||||||
"i18n:govoplan-campaign.managed_file_browsing_is_unavailable_use_manual_.782867fe" :
|
|
||||||
"i18n:govoplan-campaign.the_files_module_is_not_installed_use_manual_pat.49abc725"}</p>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
</>
|
</>
|
||||||
</LoadingFrame>
|
</LoadingFrame>
|
||||||
|
|
||||||
@@ -450,6 +443,19 @@ function zipArchiveColumns({ disabled, archives, invalidNameIndexes, onEditName,
|
|||||||
render: (archive, index) => <ToggleSwitch label="i18n:govoplan-campaign.protected.28531336" checked={archive.password_enabled} disabled={disabled} onChange={(checked) => patchArchive(index, { password_enabled: checked })} />,
|
render: (archive, index) => <ToggleSwitch label="i18n:govoplan-campaign.protected.28531336" checked={archive.password_enabled} disabled={disabled} onChange={(checked) => patchArchive(index, { password_enabled: checked })} />,
|
||||||
value: (archive) => archive.password_enabled ? "protected" : "none"
|
value: (archive) => archive.password_enabled ? "protected" : "none"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "method", header: "ZIP mode", width: 280, sortable: true, filterable: true,
|
||||||
|
columnType: "from-list", list: { options: [{ value: "aes", label: "AES" }, { value: "zip_standard", label: "Win-compatible" }] },
|
||||||
|
render: (archive, index) =>
|
||||||
|
<ToggleSwitch
|
||||||
|
label="Win-compatible"
|
||||||
|
checked={archive.method === "zip_standard"}
|
||||||
|
disabled={disabled}
|
||||||
|
help="Win-compatible ZIP uses the legacy ZipCrypto format so password-protected archives can be opened with Windows Explorer. Use AES when recipients can use 7-Zip, NanaZip, WinRAR, or another AES-capable ZIP tool."
|
||||||
|
onChange={(checked) => patchArchive(index, { method: checked ? "zip_standard" : "aes" })} />,
|
||||||
|
|
||||||
|
value: (archive) => archive.method
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "password_field", header: "i18n:govoplan-campaign.password_field.a1fc8a1c", width: 230, sortable: true, filterable: true,
|
id: "password_field", header: "i18n:govoplan-campaign.password_field.a1fc8a1c", width: 230, sortable: true, filterable: true,
|
||||||
columnType: "from-list", list: { options: [{ value: "", label: "i18n:govoplan-campaign.no_field.1fe00ed4" }, ...passwordFields.map((field) => ({ value: field.name, label: field.label || field.name }))] },
|
columnType: "from-list", list: { options: [{ value: "", label: "i18n:govoplan-campaign.no_field.1fe00ed4" }, ...passwordFields.map((field) => ({ value: field.name, label: field.label || field.name }))] },
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export default function GlobalSettingsPage({ settings, auth, campaignId, view =
|
|||||||
|
|
||||||
const version = data.currentVersion;
|
const version = data.currentVersion;
|
||||||
const locked = isAuditLockedVersion(version, data.campaign?.current_version_id);
|
const locked = isAuditLockedVersion(version, data.campaign?.current_version_id);
|
||||||
const { draft, displayDraft, dirty, saveState, localError, patch, markDirty, saveDraft } = useCampaignDraftEditor({
|
const { draft, displayDraft, dirty, saveState, localError, patch, markDirty, discardDraft, saveDraft } = useCampaignDraftEditor({
|
||||||
settings,
|
settings,
|
||||||
campaignId,
|
campaignId,
|
||||||
version,
|
version,
|
||||||
@@ -71,6 +71,11 @@ export default function GlobalSettingsPage({ settings, auth, campaignId, view =
|
|||||||
markDirty();
|
markDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function patchSendWithoutAttachmentsBehavior(value: string) {
|
||||||
|
patch(["attachments", "send_without_attachments_behavior"], value);
|
||||||
|
patch(["attachments", "send_without_attachments"], value !== "block");
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="content-pad workspace-data-page">
|
<div className="content-pad workspace-data-page">
|
||||||
<div className="page-heading split workspace-heading">
|
<div className="page-heading split workspace-heading">
|
||||||
@@ -79,7 +84,7 @@ export default function GlobalSettingsPage({ settings, auth, campaignId, view =
|
|||||||
<VersionLine version={version} versions={data.versions} status={saveState} />
|
<VersionLine version={version} versions={data.versions} status={saveState} />
|
||||||
</div>
|
</div>
|
||||||
<div className="button-row compact-actions">
|
<div className="button-row compact-actions">
|
||||||
<Button onClick={reload} disabled={loading}>i18n:govoplan-campaign.reload.cce71553</Button>
|
<Button onClick={() => void discardDraft()} disabled={loading}>Discard</Button>
|
||||||
<Button variant="primary" onClick={() => saveDraft("manual")} disabled={!dirty || locked || !draft}>{dirty ? "i18n:govoplan-campaign.save_now.3989b7c0" : "i18n:govoplan-campaign.saved.c0ae8f6e"}</Button>
|
<Button variant="primary" onClick={() => saveDraft("manual")} disabled={!dirty || locked || !draft}>{dirty ? "i18n:govoplan-campaign.save_now.3989b7c0" : "i18n:govoplan-campaign.saved.c0ae8f6e"}</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -208,8 +213,8 @@ export default function GlobalSettingsPage({ settings, auth, campaignId, view =
|
|||||||
effectiveClassName="campaign-policy-effective-note"
|
effectiveClassName="campaign-policy-effective-note"
|
||||||
label="i18n:govoplan-campaign.send_without_attachments.ead6d030"
|
label="i18n:govoplan-campaign.send_without_attachments.ead6d030"
|
||||||
note="i18n:govoplan-campaign.campaign_wide_fallback_when_no_attachment_is_ava.84ffa0bc"
|
note="i18n:govoplan-campaign.campaign_wide_fallback_when_no_attachment_is_ava.84ffa0bc"
|
||||||
control={<ToggleSwitch label="i18n:govoplan-campaign.allowed.77c7b490" checked={getBool(attachments, "send_without_attachments", true)} disabled={locked} onChange={(checked) => patch(["attachments", "send_without_attachments"], checked)} />}
|
control={<PolicySelectControl value={sendWithoutAttachmentsBehavior(attachments)} disabled={locked} onChange={patchSendWithoutAttachmentsBehavior} />}
|
||||||
effective={getBool(attachments, "send_without_attachments", true) ? "i18n:govoplan-campaign.messages_may_be_sent_when_attachment_rules_allow.e6bf1aed" : "i18n:govoplan-campaign.missing_attachment_coverage_blocks_sending.05ff02a1"} />
|
effective={behaviorSummary(sendWithoutAttachmentsBehavior(attachments))} />
|
||||||
|
|
||||||
</PolicyTable>
|
</PolicyTable>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -261,3 +266,9 @@ function behaviorSummary(value: string): string {
|
|||||||
if (value === "warn") return "i18n:govoplan-campaign.keeps_sending_possible_with_a_warning.66c84a54";
|
if (value === "warn") return "i18n:govoplan-campaign.keeps_sending_possible_with_a_warning.66c84a54";
|
||||||
return "i18n:govoplan-campaign.uses_the_configured_behavior.ea6e82a3";
|
return "i18n:govoplan-campaign.uses_the_configured_behavior.ea6e82a3";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sendWithoutAttachmentsBehavior(attachments: Record<string, unknown>): string {
|
||||||
|
const configured = getText(attachments, "send_without_attachments_behavior");
|
||||||
|
if (behaviorOptions.includes(configured)) return configured;
|
||||||
|
return getBool(attachments, "send_without_attachments", true) ? "continue" : "block";
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,15 +22,21 @@ export function ensureCampaignDraft(version: CampaignVersionDetail | null): Reco
|
|||||||
raw.server = isRecord(raw.server) ? raw.server : {};
|
raw.server = isRecord(raw.server) ? raw.server : {};
|
||||||
raw.recipients = isRecord(raw.recipients) ? raw.recipients : {};
|
raw.recipients = isRecord(raw.recipients) ? raw.recipients : {};
|
||||||
raw.template = isRecord(raw.template) ? raw.template : { subject: "", text: "" };
|
raw.template = isRecord(raw.template) ? raw.template : { subject: "", text: "" };
|
||||||
|
const sourceAttachments = asRecord(raw.attachments);
|
||||||
raw.attachments = {
|
raw.attachments = {
|
||||||
base_path: ".",
|
base_path: ".",
|
||||||
allow_individual: false,
|
allow_individual: false,
|
||||||
send_without_attachments: true,
|
send_without_attachments: true,
|
||||||
|
send_without_attachments_behavior: "continue",
|
||||||
global: [],
|
global: [],
|
||||||
missing_behavior: "ask",
|
missing_behavior: "ask",
|
||||||
ambiguous_behavior: "ask",
|
ambiguous_behavior: "ask",
|
||||||
...asRecord(raw.attachments)
|
...sourceAttachments
|
||||||
};
|
};
|
||||||
|
const normalizedAttachments = asRecord(raw.attachments);
|
||||||
|
if (sourceAttachments.send_without_attachments_behavior === undefined) {
|
||||||
|
normalizedAttachments.send_without_attachments_behavior = getBool(normalizedAttachments, "send_without_attachments", true) ? "continue" : "block";
|
||||||
|
}
|
||||||
raw.entries = isRecord(raw.entries) ? raw.entries : { inline: [] };
|
raw.entries = isRecord(raw.entries) ? raw.entries : { inline: [] };
|
||||||
raw.validation_policy = {
|
raw.validation_policy = {
|
||||||
unsent_attachment_files: "warn",
|
unsent_attachment_files: "warn",
|
||||||
|
|||||||
Reference in New Issue
Block a user