Mock server workflow - first draft
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user