Mock server workflow - first draft

This commit is contained in:
2026-06-11 11:27:14 +02:00
parent 8791de0959
commit 03c3f5f5c3
15 changed files with 1111 additions and 100 deletions

View File

@@ -1,4 +1,5 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { ArrowBigLeft, ArrowBigLeftDash, ArrowBigRight, ArrowBigRightDash } from "lucide-react";
import type { ApiSettings } from "../../types";
import Button from "../../components/Button";
import Card from "../../components/Card";
@@ -11,6 +12,7 @@ import { useCampaignWorkspaceData } from "./hooks/useCampaignWorkspaceData";
import { useCampaignDraftEditor } from "./hooks/useCampaignDraftEditor";
import { asArray, asRecord, isAuditLockedVersion } from "./utils/campaignView";
import { cloneJson, getBool, getText } from "./utils/draftEditor";
import { buildEffectiveAttachmentPreviews, type EffectiveAttachmentPreview } from "./utils/attachments";
import { humanizeFieldName } from "./utils/fieldDefinitions";
import { buildTemplatePreviewContext, buildUndefinedPlaceholders, extractTemplatePlaceholders, removePlaceholderFromText, renderTemplatePreviewText, uniquePlaceholders, valueToPreview, type TemplateNamespace, type TemplatePlaceholder, type UndefinedPlaceholder } from "./utils/templatePlaceholders";
@@ -61,6 +63,7 @@ export default function TemplateDataPage({ settings, campaignId }: { settings: A
const previewSubject = renderTemplatePreviewText(getText(template, "subject"), previewContext, ignoreEmptyFields);
const previewText = renderTemplatePreviewText(getText(template, "text"), previewContext, ignoreEmptyFields);
const previewHtml = renderTemplatePreviewText(getText(template, "html"), previewContext, ignoreEmptyFields);
const previewAttachments = useMemo(() => buildEffectiveAttachmentPreviews(displayDraft, previewEntry), [displayDraft, previewEntry]);
useEffect(() => {
@@ -239,9 +242,12 @@ export default function TemplateDataPage({ settings, campaignId }: { settings: A
text={previewText}
html={previewHtml}
hasRealRecipients={inlineEntries.length > 0}
attachments={previewAttachments}
onClose={() => setPreviewOpen(false)}
onFirst={() => setPreviewIndex(0)}
onPrevious={() => setPreviewIndex((value) => Math.max(0, value - 1))}
onNext={() => setPreviewIndex((value) => Math.min(previewEntries.length - 1, value + 1))}
onLast={() => setPreviewIndex(previewEntries.length - 1)}
/>
)}
@@ -319,9 +325,12 @@ function TemplatePreviewOverlay({
text,
html,
hasRealRecipients,
attachments,
onClose,
onFirst,
onPrevious,
onNext
onNext,
onLast
}: {
bodyMode: BodyMode;
entry: Record<string, unknown>;
@@ -331,9 +340,12 @@ function TemplatePreviewOverlay({
text: string;
html: string;
hasRealRecipients: boolean;
attachments: EffectiveAttachmentPreview[];
onClose: () => void;
onFirst: () => void;
onPrevious: () => void;
onNext: () => void;
onLast: () => void;
}) {
return (
<div className="overlay-backdrop" role="dialog" aria-modal="true" aria-labelledby="template-preview-title">
@@ -348,9 +360,12 @@ function TemplatePreviewOverlay({
<strong>{hasRealRecipients ? recipientLabel(entry, index) : "Global preview"}</strong>
<p className="muted small-note">{hasRealRecipients ? `${index + 1} of ${total}` : "No inline recipients are available yet."}</p>
</div>
<div className="button-row compact-actions">
<Button onClick={onPrevious} disabled={index <= 0}>Previous</Button>
<Button onClick={onNext} disabled={index >= total - 1}>Next</Button>
<div className="button-row compact-actions template-preview-nav" aria-label="Preview message navigation">
<button type="button" className="version-arrow" onClick={onFirst} disabled={index <= 0} title="First message" aria-label="First message"><ArrowBigLeftDash aria-hidden="true" /></button>
<button type="button" className="version-arrow" onClick={onPrevious} disabled={index <= 0} title="Previous message" aria-label="Previous message"><ArrowBigLeft aria-hidden="true" /></button>
<span className="template-preview-count">{index + 1} / {total}</span>
<button type="button" className="version-arrow" onClick={onNext} disabled={index >= total - 1} title="Next message" aria-label="Next message"><ArrowBigRight aria-hidden="true" /></button>
<button type="button" className="version-arrow" onClick={onLast} disabled={index >= total - 1} title="Last message" aria-label="Last message"><ArrowBigRightDash aria-hidden="true" /></button>
</div>
</div>
<div className="template-preview-box">
@@ -361,6 +376,7 @@ function TemplatePreviewOverlay({
<pre>{text || "No plain-text body to preview."}</pre>
)}
</div>
<EffectiveAttachmentPreviewTable attachments={attachments} />
</div>
<footer className="modal-footer"><Button variant="primary" onClick={onClose}>Close</Button></footer>
</div>
@@ -368,6 +384,45 @@ function TemplatePreviewOverlay({
);
}
function EffectiveAttachmentPreviewTable({ attachments }: { attachments: EffectiveAttachmentPreview[] }) {
return (
<div className="template-preview-attachments">
<h3>Effective attachments</h3>
{attachments.length === 0 ? (
<p className="muted small-note">No global or recipient attachments are effective for this message.</p>
) : (
<div className="app-table-wrap">
<table className="app-table compact-table">
<thead>
<tr>
<th>Scope</th>
<th>Label</th>
<th>Base path</th>
<th>File / pattern</th>
<th>Options</th>
<th>Preview match</th>
</tr>
</thead>
<tbody>
{attachments.map((attachment, index) => (
<tr key={`${attachment.scope}:${attachment.basePath}:${attachment.fileFilter}:${index}`}>
<td>{attachment.scope === "global" ? "Global" : "Recipient"}</td>
<td>{attachment.label}</td>
<td>{attachment.basePathName}<br /><span className="muted"><code>{attachment.basePath}</code></span></td>
<td><code>{attachment.fileFilter || "—"}</code></td>
<td>{attachment.required ? "required" : "optional"}{attachment.includeSubdirs ? ", subdirs" : ""}</td>
<td>{attachment.matches.length > 0 ? attachment.matches.join(", ") : "—"}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
);
}
function recipientLabel(entry: Record<string, unknown>, index: number): string {
const name = valueToPreview(entry.name).trim();
const email = valueToPreview(entry.email).trim();