Attachment preview
This commit is contained in:
@@ -131,6 +131,51 @@ export type CampaignSendNowPayload = {
|
|||||||
enqueue_imap_task?: boolean;
|
enqueue_imap_task?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type CampaignAttachmentPreviewFile = {
|
||||||
|
id: string;
|
||||||
|
version_id?: string;
|
||||||
|
blob_id?: string;
|
||||||
|
display_path: string;
|
||||||
|
filename: string;
|
||||||
|
owner_type: string;
|
||||||
|
owner_id: string;
|
||||||
|
checksum_sha256?: string;
|
||||||
|
size_bytes?: number;
|
||||||
|
content_type?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CampaignAttachmentPreviewRule = {
|
||||||
|
source: "global" | "entry";
|
||||||
|
entry_index: number;
|
||||||
|
entry_id?: string | null;
|
||||||
|
index: number;
|
||||||
|
attachment_id?: string | null;
|
||||||
|
label?: string | null;
|
||||||
|
required: boolean;
|
||||||
|
pattern: string;
|
||||||
|
base_path_name?: string | null;
|
||||||
|
base_path?: string | null;
|
||||||
|
status: "ok" | "missing" | "ambiguous";
|
||||||
|
behavior?: string | null;
|
||||||
|
matches: CampaignAttachmentPreviewFile[];
|
||||||
|
match_count: number;
|
||||||
|
issues: Record<string, unknown>[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CampaignAttachmentPreviewResponse = {
|
||||||
|
campaign_id: string;
|
||||||
|
version_id: string;
|
||||||
|
shared_file_count: number;
|
||||||
|
rules: CampaignAttachmentPreviewRule[];
|
||||||
|
unused_shared_files: CampaignAttachmentPreviewFile[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CampaignAttachmentPreviewPayload = {
|
||||||
|
include_unmatched?: boolean;
|
||||||
|
campaign_json?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
export type CampaignMockSendPayload = {
|
export type CampaignMockSendPayload = {
|
||||||
version_id?: string | null;
|
version_id?: string | null;
|
||||||
send?: boolean;
|
send?: boolean;
|
||||||
@@ -309,6 +354,20 @@ export async function buildVersion(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function previewCampaignAttachments(
|
||||||
|
settings: ApiSettings,
|
||||||
|
campaignId: string,
|
||||||
|
versionId: string,
|
||||||
|
payload: CampaignAttachmentPreviewPayload = {}
|
||||||
|
): Promise<CampaignAttachmentPreviewResponse> {
|
||||||
|
return apiFetch<CampaignAttachmentPreviewResponse>(
|
||||||
|
settings,
|
||||||
|
`/api/v1/campaigns/${campaignId}/versions/${versionId}/attachments/preview`,
|
||||||
|
{ method: "POST", body: JSON.stringify(payload) }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export async function getCampaignSummary(settings: ApiSettings, campaignId: string): Promise<CampaignSummary> {
|
export async function getCampaignSummary(settings: ApiSettings, campaignId: string): Promise<CampaignSummary> {
|
||||||
return apiFetch<CampaignSummary>(settings, `/api/v1/campaigns/${campaignId}/summary`);
|
return apiFetch<CampaignSummary>(settings, `/api/v1/campaigns/${campaignId}/summary`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export default function AttachmentsDataPage({ settings, campaignId }: { settings
|
|||||||
|
|
||||||
function addGlobalAttachmentRule() {
|
function addGlobalAttachmentRule() {
|
||||||
if (locked) return;
|
if (locked) return;
|
||||||
patch(["attachments", "global"], [...globalRules, createAttachmentRule(basePaths[0]?.path ?? "", nextAttachmentLabel(globalRules))]);
|
patch(["attachments", "global"], [...globalRules, createAttachmentRule(basePaths[0]?.path ?? "", nextAttachmentLabel(globalRules), basePaths[0]?.id ?? "")]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import type { ApiSettings } from "../../types";
|
import type { ApiSettings } from "../../types";
|
||||||
|
import { previewCampaignAttachments, type CampaignAttachmentPreviewRule } from "../../api/campaigns";
|
||||||
import Button from "../../components/Button";
|
import Button from "../../components/Button";
|
||||||
import Card from "../../components/Card";
|
import Card from "../../components/Card";
|
||||||
import FormField from "../../components/FormField";
|
import FormField from "../../components/FormField";
|
||||||
@@ -13,7 +14,6 @@ import { useCampaignWorkspaceData } from "./hooks/useCampaignWorkspaceData";
|
|||||||
import { useCampaignDraftEditor } from "./hooks/useCampaignDraftEditor";
|
import { useCampaignDraftEditor } from "./hooks/useCampaignDraftEditor";
|
||||||
import { asArray, asRecord, isAuditLockedVersion } from "./utils/campaignView";
|
import { asArray, asRecord, isAuditLockedVersion } from "./utils/campaignView";
|
||||||
import { cloneJson, getBool, getText } from "./utils/draftEditor";
|
import { cloneJson, getBool, getText } from "./utils/draftEditor";
|
||||||
import { buildEffectiveAttachmentPreviews, type EffectiveAttachmentPreview } from "./utils/attachments";
|
|
||||||
import { humanizeFieldName } from "./utils/fieldDefinitions";
|
import { humanizeFieldName } from "./utils/fieldDefinitions";
|
||||||
import { buildTemplatePreviewContext, buildUndefinedPlaceholders, extractTemplatePlaceholders, removePlaceholderFromText, renderTemplatePreviewText, uniquePlaceholders, valueToPreview, type TemplateNamespace, type TemplatePlaceholder, type UndefinedPlaceholder } from "./utils/templatePlaceholders";
|
import { buildTemplatePreviewContext, buildUndefinedPlaceholders, extractTemplatePlaceholders, removePlaceholderFromText, renderTemplatePreviewText, uniquePlaceholders, valueToPreview, type TemplateNamespace, type TemplatePlaceholder, type UndefinedPlaceholder } from "./utils/templatePlaceholders";
|
||||||
|
|
||||||
@@ -27,6 +27,9 @@ export default function TemplateDataPage({ settings, campaignId }: { settings: A
|
|||||||
const [previewOpen, setPreviewOpen] = useState(false);
|
const [previewOpen, setPreviewOpen] = useState(false);
|
||||||
const [previewIndex, setPreviewIndex] = useState(0);
|
const [previewIndex, setPreviewIndex] = useState(0);
|
||||||
const [undefinedDialog, setUndefinedDialog] = useState<UndefinedPlaceholder | null>(null);
|
const [undefinedDialog, setUndefinedDialog] = useState<UndefinedPlaceholder | null>(null);
|
||||||
|
const [attachmentPreviewRules, setAttachmentPreviewRules] = useState<CampaignAttachmentPreviewRule[]>([]);
|
||||||
|
const [attachmentPreviewLoading, setAttachmentPreviewLoading] = useState(false);
|
||||||
|
const [attachmentPreviewError, setAttachmentPreviewError] = useState("");
|
||||||
const subjectRef = useRef<HTMLInputElement | null>(null);
|
const subjectRef = useRef<HTMLInputElement | null>(null);
|
||||||
const textRef = useRef<HTMLTextAreaElement | null>(null);
|
const textRef = useRef<HTMLTextAreaElement | null>(null);
|
||||||
const htmlRef = useRef<HTMLTextAreaElement | null>(null);
|
const htmlRef = useRef<HTMLTextAreaElement | null>(null);
|
||||||
@@ -64,13 +67,50 @@ export default function TemplateDataPage({ settings, campaignId }: { settings: A
|
|||||||
const previewSubject = renderTemplatePreviewText(getText(template, "subject"), previewContext, ignoreEmptyFields);
|
const previewSubject = renderTemplatePreviewText(getText(template, "subject"), previewContext, ignoreEmptyFields);
|
||||||
const previewText = renderTemplatePreviewText(getText(template, "text"), previewContext, ignoreEmptyFields);
|
const previewText = renderTemplatePreviewText(getText(template, "text"), previewContext, ignoreEmptyFields);
|
||||||
const previewHtml = renderTemplatePreviewText(getText(template, "html"), previewContext, ignoreEmptyFields);
|
const previewHtml = renderTemplatePreviewText(getText(template, "html"), previewContext, ignoreEmptyFields);
|
||||||
const previewAttachments = useMemo(() => buildEffectiveAttachmentPreviews(displayDraft, previewEntry), [displayDraft, previewEntry]);
|
const selectedPreviewEntryIndex = Math.min(previewIndex, previewEntries.length - 1) + 1;
|
||||||
|
const previewAttachmentRules = useMemo(
|
||||||
|
() => attachmentPreviewRules.filter((rule) => rule.entry_index === selectedPreviewEntryIndex),
|
||||||
|
[attachmentPreviewRules, selectedPreviewEntryIndex]
|
||||||
|
);
|
||||||
|
const previewAttachments = useMemo(
|
||||||
|
() => mapResolvedAttachmentsToPreviewBoxes(previewAttachmentRules, attachmentPreviewLoading, attachmentPreviewError),
|
||||||
|
[attachmentPreviewError, attachmentPreviewLoading, previewAttachmentRules]
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (previewIndex >= previewEntries.length) setPreviewIndex(Math.max(0, previewEntries.length - 1));
|
if (previewIndex >= previewEntries.length) setPreviewIndex(Math.max(0, previewEntries.length - 1));
|
||||||
}, [previewIndex, previewEntries.length]);
|
}, [previewIndex, previewEntries.length]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!previewOpen || !version?.id || !draft) return;
|
||||||
|
let cancelled = false;
|
||||||
|
setAttachmentPreviewLoading(true);
|
||||||
|
setAttachmentPreviewError("");
|
||||||
|
const handle = window.setTimeout(() => {
|
||||||
|
void previewCampaignAttachments(settings, campaignId, version.id, {
|
||||||
|
include_unmatched: false,
|
||||||
|
campaign_json: displayDraft
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (!cancelled) setAttachmentPreviewRules(response.rules);
|
||||||
|
})
|
||||||
|
.catch((reason: unknown) => {
|
||||||
|
if (!cancelled) {
|
||||||
|
setAttachmentPreviewRules([]);
|
||||||
|
setAttachmentPreviewError(reason instanceof Error ? reason.message : String(reason));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (!cancelled) setAttachmentPreviewLoading(false);
|
||||||
|
});
|
||||||
|
}, 120);
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
window.clearTimeout(handle);
|
||||||
|
};
|
||||||
|
}, [campaignId, displayDraft, draft, previewOpen, settings.apiBaseUrl, settings.apiKey, settings.accessToken, version?.id]);
|
||||||
|
|
||||||
|
|
||||||
function patchTemplateText(target: EditorTarget, value: string) {
|
function patchTemplateText(target: EditorTarget, value: string) {
|
||||||
patch(["template", target], value);
|
patch(["template", target], value);
|
||||||
@@ -242,7 +282,7 @@ export default function TemplateDataPage({ settings, campaignId }: { settings: A
|
|||||||
html={previewHtml}
|
html={previewHtml}
|
||||||
recipientLabel={inlineEntries.length > 0 ? recipientLabel(previewEntry, Math.min(previewIndex, previewEntries.length - 1)) : "Global preview"}
|
recipientLabel={inlineEntries.length > 0 ? recipientLabel(previewEntry, Math.min(previewIndex, previewEntries.length - 1)) : "Global preview"}
|
||||||
recipientNote={inlineEntries.length > 0 ? `${Math.min(previewIndex, previewEntries.length - 1) + 1} of ${previewEntries.length}` : "No inline recipients are available yet."}
|
recipientNote={inlineEntries.length > 0 ? `${Math.min(previewIndex, previewEntries.length - 1) + 1} of ${previewEntries.length}` : "No inline recipients are available yet."}
|
||||||
attachments={mapEffectiveAttachmentsToPreviewBoxes(previewAttachments)}
|
attachments={previewAttachments}
|
||||||
navigation={{
|
navigation={{
|
||||||
index: Math.min(previewIndex, previewEntries.length - 1),
|
index: Math.min(previewIndex, previewEntries.length - 1),
|
||||||
total: previewEntries.length,
|
total: previewEntries.length,
|
||||||
@@ -320,31 +360,44 @@ function UndefinedPlaceholderList({ items, onSelect }: { items: UndefinedPlaceho
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapEffectiveAttachmentsToPreviewBoxes(attachments: EffectiveAttachmentPreview[]): MessagePreviewAttachment[] {
|
function mapResolvedAttachmentsToPreviewBoxes(
|
||||||
return attachments.flatMap((attachment) => {
|
rules: CampaignAttachmentPreviewRule[],
|
||||||
|
loading: boolean,
|
||||||
|
error: string
|
||||||
|
): MessagePreviewAttachment[] {
|
||||||
|
if (loading) {
|
||||||
|
return [{ filename: "Resolving attachment patterns…", detail: "Managed files are being checked for this recipient." }];
|
||||||
|
}
|
||||||
|
if (error) {
|
||||||
|
return [{ filename: "Attachment preview unavailable", detail: error }];
|
||||||
|
}
|
||||||
|
return rules.flatMap((rule) => {
|
||||||
const detailParts = [
|
const detailParts = [
|
||||||
attachment.scope === "global" ? "Global" : "Recipient",
|
rule.source === "global" ? "Global" : "Recipient",
|
||||||
attachment.label,
|
rule.label,
|
||||||
attachment.required ? "required" : "optional",
|
rule.required ? "required" : "optional",
|
||||||
attachment.includeSubdirs ? "subdirs" : ""
|
rule.pattern
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
const detail = detailParts.join(" · ");
|
const detail = detailParts.join(" · ");
|
||||||
if (attachment.matches.length > 0) {
|
if (rule.matches.length > 0) {
|
||||||
return attachment.matches.map((match) => ({
|
return rule.matches.map((match) => ({
|
||||||
filename: match,
|
filename: match.filename || match.display_path,
|
||||||
label: attachment.label,
|
label: rule.label,
|
||||||
detail
|
detail: `${detail}${match.display_path ? ` · ${match.display_path}` : ""}`,
|
||||||
|
contentType: match.content_type,
|
||||||
|
sizeBytes: match.size_bytes
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
return [{
|
return [{
|
||||||
filename: attachment.fileFilter || "No matched file",
|
filename: `No file matched ${rule.pattern || "attachment pattern"}`,
|
||||||
label: attachment.label,
|
label: rule.label,
|
||||||
detail: `${detail}${detail ? " · " : ""}${attachment.basePathName || attachment.basePath || "attachment source"}`
|
detail: `${detail}${rule.status !== "ok" ? ` · ${rule.status}` : ""}`
|
||||||
}];
|
}];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function recipientLabel(entry: Record<string, unknown>, index: number): string {
|
function recipientLabel(entry: Record<string, unknown>, index: number): string {
|
||||||
const name = valueToPreview(entry.name).trim();
|
const name = valueToPreview(entry.name).trim();
|
||||||
const email = valueToPreview(entry.email).trim();
|
const email = valueToPreview(entry.email).trim();
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export default function AttachmentRulesOverlay({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addOverlayRule() {
|
function addOverlayRule() {
|
||||||
onChange([...rules, createAttachmentRule(basePaths[0]?.path ?? "", nextAttachmentLabel(rules))]);
|
onChange([...rules, createAttachmentRule(basePaths[0]?.path ?? "", nextAttachmentLabel(rules), basePaths[0]?.id ?? "")]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const dialog = open ? createPortal(
|
const dialog = open ? createPortal(
|
||||||
@@ -110,7 +110,7 @@ export function AttachmentRulesTable({
|
|||||||
function addRule() {
|
function addRule() {
|
||||||
onChange([
|
onChange([
|
||||||
...tableProps.rules,
|
...tableProps.rules,
|
||||||
createAttachmentRule(tableProps.basePaths?.[0]?.path ?? "", nextAttachmentLabel(tableProps.rules))
|
createAttachmentRule(tableProps.basePaths?.[0]?.path ?? "", nextAttachmentLabel(tableProps.rules), tableProps.basePaths?.[0]?.id ?? "")
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +158,8 @@ export function AttachmentRulesDataGrid({
|
|||||||
}
|
}
|
||||||
const rule = rules[ruleIndex] ?? {};
|
const rule = rules[ruleIndex] ?? {};
|
||||||
const currentPath = getText(rule, "base_dir", basePaths[0]?.path ?? "");
|
const currentPath = getText(rule, "base_dir", basePaths[0]?.path ?? "");
|
||||||
const basePath = basePaths.find((item) => item.path === currentPath) ?? basePaths[0] ?? null;
|
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 });
|
setFileChooser({ ruleIndex, basePath });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,6 +167,7 @@ export function AttachmentRulesDataGrid({
|
|||||||
if (!fileChooser) return;
|
if (!fileChooser) return;
|
||||||
const currentRule = rules[fileChooser.ruleIndex] ?? {};
|
const currentRule = rules[fileChooser.ruleIndex] ?? {};
|
||||||
patchRule(fileChooser.ruleIndex, {
|
patchRule(fileChooser.ruleIndex, {
|
||||||
|
base_path_id: fileChooser.basePath?.id ?? "",
|
||||||
base_dir: fileChooser.basePath?.path ?? (selection.folderPath || "."),
|
base_dir: fileChooser.basePath?.path ?? (selection.folderPath || "."),
|
||||||
file_filter: selection.fileFilter,
|
file_filter: selection.fileFilter,
|
||||||
type: selection.selectionType === "file" ? "direct" : "pattern",
|
type: selection.selectionType === "file" ? "direct" : "pattern",
|
||||||
@@ -223,13 +225,22 @@ function attachmentRuleColumns({ disabled, basePaths, activeChooserRuleIndex: _a
|
|||||||
sortable: true,
|
sortable: true,
|
||||||
filterable: true,
|
filterable: true,
|
||||||
render: (rule, index) => {
|
render: (rule, index) => {
|
||||||
const currentBasePath = getText(rule, "base_dir", basePaths[0]?.path ?? "");
|
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 ? (
|
return basePaths.length > 0 ? (
|
||||||
<select value={currentBasePath} disabled={disabled} onChange={(event) => patchRule(index, { base_dir: event.target.value })}>
|
<select
|
||||||
{basePaths.map((basePath) => <option key={basePath.id} value={basePath.path}>{basePath.name || basePath.path}</option>)}
|
value={selectedBasePath?.id ?? ""}
|
||||||
|
disabled={disabled}
|
||||||
|
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 });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{basePaths.map((basePath) => <option key={basePath.id} value={basePath.id}>{basePath.name || basePath.path}</option>)}
|
||||||
</select>
|
</select>
|
||||||
) : (
|
) : (
|
||||||
<input value={currentBasePath} disabled={disabled} readOnly placeholder="optional/folder" />
|
<input value={currentBasePathValue} disabled={disabled} readOnly placeholder="optional/folder" />
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
value: (rule) => getText(rule, "base_dir", basePaths[0]?.path ?? "")
|
value: (rule) => getText(rule, "base_dir", basePaths[0]?.path ?? "")
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type { FileSpace } from "../../../api/files";
|
import type { FileSpace } from "../../../api/files";
|
||||||
import { asArray, asRecord, isRecord } from "./campaignView";
|
import { asArray, asRecord, isRecord } from "./campaignView";
|
||||||
import { getBool, getText } from "./draftEditor";
|
import { getBool, getText } from "./draftEditor";
|
||||||
import { buildTemplatePreviewContext, renderTemplatePreviewText } from "./templatePlaceholders";
|
|
||||||
|
|
||||||
export type AttachmentRule = Record<string, unknown>;
|
export type AttachmentRule = Record<string, unknown>;
|
||||||
|
|
||||||
@@ -38,17 +37,6 @@ export type AttachmentSummary = {
|
|||||||
rules: number;
|
rules: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EffectiveAttachmentPreview = {
|
|
||||||
scope: "global" | "recipient";
|
|
||||||
label: string;
|
|
||||||
basePathName: string;
|
|
||||||
basePath: string;
|
|
||||||
fileFilter: string;
|
|
||||||
required: boolean;
|
|
||||||
includeSubdirs: boolean;
|
|
||||||
matches: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type MockAttachmentPathOption = Partial<AttachmentBasePath> & {
|
export type MockAttachmentPathOption = Partial<AttachmentBasePath> & {
|
||||||
label: string;
|
label: string;
|
||||||
};
|
};
|
||||||
@@ -61,13 +49,7 @@ export const mockAttachmentPathOptions: MockAttachmentPathOption[] = [
|
|||||||
{ label: "Personal upload area", path: "user/uploads" }
|
{ label: "Personal upload area", path: "user/uploads" }
|
||||||
];
|
];
|
||||||
|
|
||||||
export const mockAttachmentFiles = [
|
|
||||||
"welcome.pdf",
|
|
||||||
"terms-and-conditions.pdf",
|
|
||||||
"invoice_{{local:invoice_number}}.pdf",
|
|
||||||
"{{local:recipient_id}}/certificate.pdf",
|
|
||||||
"attachments/{{local:email}}/*.pdf"
|
|
||||||
];
|
|
||||||
|
|
||||||
export function createAttachmentBasePath(name = "New attachment source", path = "."): AttachmentBasePath {
|
export function createAttachmentBasePath(name = "New attachment source", path = "."): AttachmentBasePath {
|
||||||
return {
|
return {
|
||||||
@@ -79,10 +61,11 @@ export function createAttachmentBasePath(name = "New attachment source", path =
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createAttachmentRule(baseDir = "", label = ""): AttachmentRule {
|
export function createAttachmentRule(baseDir = "", label = "", basePathId = ""): AttachmentRule {
|
||||||
return {
|
return {
|
||||||
id: `attachment-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
id: `attachment-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
||||||
label,
|
label,
|
||||||
|
base_path_id: basePathId || undefined,
|
||||||
base_dir: baseDir,
|
base_dir: baseDir,
|
||||||
file_filter: "",
|
file_filter: "",
|
||||||
required: true,
|
required: true,
|
||||||
@@ -136,6 +119,7 @@ export function normalizeAttachmentRules(value: unknown): AttachmentRule[] {
|
|||||||
return value.filter(isRecord).map((rule) => ({
|
return value.filter(isRecord).map((rule) => ({
|
||||||
id: getText(rule, "id", `attachment-${Math.random().toString(36).slice(2)}`),
|
id: getText(rule, "id", `attachment-${Math.random().toString(36).slice(2)}`),
|
||||||
label: getText(rule, "label"),
|
label: getText(rule, "label"),
|
||||||
|
base_path_id: getText(rule, "base_path_id"),
|
||||||
base_dir: getText(rule, "base_dir", ""),
|
base_dir: getText(rule, "base_dir", ""),
|
||||||
file_filter: getText(rule, "file_filter") || getText(rule, "file") || getText(rule, "filename") || getText(rule, "path"),
|
file_filter: getText(rule, "file_filter") || getText(rule, "file") || getText(rule, "filename") || getText(rule, "path"),
|
||||||
include_subdirs: getBool(rule, "include_subdirs"),
|
include_subdirs: getBool(rule, "include_subdirs"),
|
||||||
@@ -173,67 +157,3 @@ export function isDirectAttachmentRule(rule: AttachmentRule): boolean {
|
|||||||
if (!fileFilter) return false;
|
if (!fileFilter) return false;
|
||||||
return !/[{}*?\[\]]/.test(fileFilter);
|
return !/[{}*?\[\]]/.test(fileFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function buildEffectiveAttachmentPreviews(draft: Record<string, unknown> | null, entry: Record<string, unknown>): EffectiveAttachmentPreview[] {
|
|
||||||
const attachments = asRecord(draft?.attachments);
|
|
||||||
const basePaths = normalizeAttachmentBasePaths(attachments.base_paths, attachments);
|
|
||||||
const context = buildTemplatePreviewContext(draft, entry);
|
|
||||||
const includeGlobals = getBool(entry, "combine_attachments", true);
|
|
||||||
const globalRules = includeGlobals ? normalizeAttachmentRules(attachments.global) : [];
|
|
||||||
const entryRules = normalizeAttachmentRules(entry.attachments);
|
|
||||||
const individualPaths = basePaths.filter((basePath) => basePath.allow_individual);
|
|
||||||
const legacyIndividualAllowed = getBool(attachments, "allow_individual", individualPaths.length === 0);
|
|
||||||
|
|
||||||
const items: EffectiveAttachmentPreview[] = [];
|
|
||||||
for (const rule of globalRules) {
|
|
||||||
items.push(ruleToAttachmentPreview("global", rule, basePaths, context));
|
|
||||||
}
|
|
||||||
for (const rule of entryRules) {
|
|
||||||
const basePathValue = getText(rule, "base_dir", basePaths[0]?.path ?? ".");
|
|
||||||
const allowed = individualPaths.length > 0
|
|
||||||
? individualPaths.some((basePath) => basePath.path === basePathValue)
|
|
||||||
: legacyIndividualAllowed;
|
|
||||||
if (allowed) items.push(ruleToAttachmentPreview("recipient", rule, basePaths, context));
|
|
||||||
}
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ruleToAttachmentPreview(
|
|
||||||
scope: EffectiveAttachmentPreview["scope"],
|
|
||||||
rule: AttachmentRule,
|
|
||||||
basePaths: AttachmentBasePath[],
|
|
||||||
context: Record<string, string>
|
|
||||||
): EffectiveAttachmentPreview {
|
|
||||||
const basePathValue = getText(rule, "base_dir", basePaths[0]?.path ?? ".");
|
|
||||||
const basePath = basePaths.find((item) => item.path === basePathValue);
|
|
||||||
const fileFilterTemplate = getText(rule, "file_filter") || getText(rule, "file") || getText(rule, "filename") || getText(rule, "path");
|
|
||||||
const fileFilter = renderTemplatePreviewText(fileFilterTemplate, context, true);
|
|
||||||
return {
|
|
||||||
scope,
|
|
||||||
label: getText(rule, "label") || (scope === "global" ? "Global attachment" : "Recipient attachment"),
|
|
||||||
basePathName: basePath?.name || basePathValue || "Campaign files",
|
|
||||||
basePath: basePath?.path || basePathValue || ".",
|
|
||||||
fileFilter,
|
|
||||||
required: getBool(rule, "required", true),
|
|
||||||
includeSubdirs: getBool(rule, "include_subdirs"),
|
|
||||||
matches: previewFileMatches(fileFilter)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function previewFileMatches(fileFilter: string): string[] {
|
|
||||||
const value = fileFilter.trim();
|
|
||||||
if (!value) return [];
|
|
||||||
if (!/[{}*?\[\]]/.test(value)) {
|
|
||||||
return mockAttachmentFiles.filter((file) => file === value || file.endsWith(`/${value}`));
|
|
||||||
}
|
|
||||||
const pattern = globLikePatternToRegExp(value);
|
|
||||||
return mockAttachmentFiles.filter((file) => pattern.test(file));
|
|
||||||
}
|
|
||||||
|
|
||||||
function globLikePatternToRegExp(value: string): RegExp {
|
|
||||||
const escaped = value.replace(/[.+^${}()|\\]/g, "\\$&")
|
|
||||||
.replace(/\*/g, ".*")
|
|
||||||
.replace(/\?/g, ".");
|
|
||||||
return new RegExp(`^${escaped}$`);
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user