diff --git a/.gitignore b/.gitignore index cf68d44..5588837 100644 --- a/.gitignore +++ b/.gitignore @@ -420,3 +420,4 @@ bin-release/ # should NOT be excluded as they contain compiler settings and other important # information for Eclipse / Flash Builder. +.fuse_* \ No newline at end of file diff --git a/multi-seal-mail-webui.tar.gz b/multi-seal-mail-webui.tar.gz deleted file mode 100644 index df7878b..0000000 Binary files a/multi-seal-mail-webui.tar.gz and /dev/null differ diff --git a/multisealmail-webui-reload-preview-patch.zip b/multisealmail-webui-reload-preview-patch.zip deleted file mode 100644 index af93aab..0000000 Binary files a/multisealmail-webui-reload-preview-patch.zip and /dev/null differ diff --git a/multisealmail-webui-reload-streamline-patch.zip b/multisealmail-webui-reload-streamline-patch.zip deleted file mode 100644 index 4a9507e..0000000 Binary files a/multisealmail-webui-reload-streamline-patch.zip and /dev/null differ diff --git a/src/features/campaigns/AttachmentsDataPage.tsx b/src/features/campaigns/AttachmentsDataPage.tsx index f68beee..bfb9656 100644 --- a/src/features/campaigns/AttachmentsDataPage.tsx +++ b/src/features/campaigns/AttachmentsDataPage.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { Link } from "react-router-dom"; import type { ApiSettings } from "../../types"; import Button from "../../components/Button"; @@ -22,7 +22,6 @@ export default function AttachmentsDataPage({ settings, campaignId }: { settings const [dirty, setDirty] = useState(false); const [saveState, setSaveState] = useState("Loaded"); const [localError, setLocalError] = useState(""); - const loadedVersionId = useRef(null); const version = data.currentVersion; const locked = isAuditLockedVersion(version); @@ -34,8 +33,6 @@ export default function AttachmentsDataPage({ settings, campaignId }: { settings useEffect(() => { if (!version) return; - if (loadedVersionId.current === version.id) return; - loadedVersionId.current = version.id; setDraft(ensureCampaignDraft(version)); setDirty(false); setSaveState(version.autosaved_at ? `Loaded saved draft ${formatDateTime(version.autosaved_at)}` : "Loaded"); diff --git a/src/features/campaigns/CampaignDataPage.tsx b/src/features/campaigns/CampaignDataPage.tsx index c19ef8b..12c8a99 100644 --- a/src/features/campaigns/CampaignDataPage.tsx +++ b/src/features/campaigns/CampaignDataPage.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { Link } from "react-router-dom"; import type { ApiSettings } from "../../types"; import Button from "../../components/Button"; @@ -22,7 +22,6 @@ export default function CampaignDataPage({ settings, campaignId }: { settings: A const [dirty, setDirty] = useState(false); const [saveState, setSaveState] = useState("Loaded"); const [localError, setLocalError] = useState(""); - const loadedVersionId = useRef(null); const version = data.currentVersion; const locked = isAuditLockedVersion(version); @@ -35,8 +34,6 @@ export default function CampaignDataPage({ settings, campaignId }: { settings: A useEffect(() => { if (!version) return; - if (loadedVersionId.current === version.id) return; - loadedVersionId.current = version.id; setDraft(ensureCampaignDraft(version)); setDirty(false); setSaveState(version.autosaved_at ? `Loaded saved draft ${formatDateTime(version.autosaved_at)}` : "Loaded"); diff --git a/src/features/campaigns/CampaignOverviewPage.tsx b/src/features/campaigns/CampaignOverviewPage.tsx index 0cea11e..7afd638 100644 --- a/src/features/campaigns/CampaignOverviewPage.tsx +++ b/src/features/campaigns/CampaignOverviewPage.tsx @@ -22,7 +22,7 @@ import { addressesFromValue } from "../../utils/emailAddresses"; export default function CampaignOverviewPage({ settings, campaignId }: { settings: ApiSettings; campaignId: string }) { const navigate = useNavigate(); - const { data, loading, error, reload, setError } = useCampaignWorkspaceData(settings, campaignId); + const { data, loading, error, reload, setError } = useCampaignWorkspaceData(settings, campaignId, { includeSummary: true }); const [copying, setCopying] = useState(false); const [locking, setLocking] = useState(false); const [message, setMessage] = useState(""); diff --git a/src/features/campaigns/CampaignReportPage.tsx b/src/features/campaigns/CampaignReportPage.tsx index 22c91b5..5eee7e1 100644 --- a/src/features/campaigns/CampaignReportPage.tsx +++ b/src/features/campaigns/CampaignReportPage.tsx @@ -6,7 +6,7 @@ import { useCampaignWorkspaceData } from "./hooks/useCampaignWorkspaceData"; import { formatDateTime } from "./utils/campaignView"; export default function CampaignReportPage({ settings, campaignId }: { settings: ApiSettings; campaignId: string }) { - const { data, loading, error, reload } = useCampaignWorkspaceData(settings, campaignId); + const { data, loading, error, reload } = useCampaignWorkspaceData(settings, campaignId, { includeCurrentVersion: false, includeSummary: true }); const cards = data.summary?.cards; return ( diff --git a/src/features/campaigns/GlobalSettingsPage.tsx b/src/features/campaigns/GlobalSettingsPage.tsx index e2e3997..98819e9 100644 --- a/src/features/campaigns/GlobalSettingsPage.tsx +++ b/src/features/campaigns/GlobalSettingsPage.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from "react"; +import { useEffect, useState } from "react"; import type { ApiSettings } from "../../types"; import Button from "../../components/Button"; import Card from "../../components/Card"; @@ -22,7 +22,6 @@ export default function GlobalSettingsPage({ settings, campaignId }: { settings: const [dirty, setDirty] = useState(false); const [saveState, setSaveState] = useState("Loaded"); const [localError, setLocalError] = useState(""); - const loadedVersionId = useRef(null); const version = data.currentVersion; const locked = isAuditLockedVersion(version); @@ -36,8 +35,6 @@ export default function GlobalSettingsPage({ settings, campaignId }: { settings: useEffect(() => { if (!version) return; - if (loadedVersionId.current === version.id) return; - loadedVersionId.current = version.id; setDraft(ensureCampaignDraft(version)); setEditorState(cloneJson(version.editor_state ?? {})); setDirty(false); diff --git a/src/features/campaigns/MailSettingsPage.tsx b/src/features/campaigns/MailSettingsPage.tsx index eb06af8..0a18b69 100644 --- a/src/features/campaigns/MailSettingsPage.tsx +++ b/src/features/campaigns/MailSettingsPage.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from "react"; +import { useEffect, useState } from "react"; import type { ApiSettings } from "../../types"; import Button from "../../components/Button"; import Card from "../../components/Card"; @@ -24,7 +24,6 @@ export default function MailSettingsPage({ settings, campaignId }: { settings: A const [imapTestResult, setImapTestResult] = useState(null); const [folderResult, setFolderResult] = useState(null); const [mailActionState, setMailActionState] = useState<"smtp" | "imap" | "folders" | null>(null); - const loadedVersionId = useRef(null); const version = data.currentVersion; const locked = isAuditLockedVersion(version); @@ -38,8 +37,6 @@ export default function MailSettingsPage({ settings, campaignId }: { settings: A useEffect(() => { if (!version) return; - if (loadedVersionId.current === version.id) return; - loadedVersionId.current = version.id; setDraft(ensureCampaignDraft(version)); setDirty(false); setSaveState(version.autosaved_at ? `Loaded saved draft ${formatDateTime(version.autosaved_at)}` : "Loaded"); diff --git a/src/features/campaigns/RecipientDataPage.tsx b/src/features/campaigns/RecipientDataPage.tsx index 95a493a..471ffd8 100644 --- a/src/features/campaigns/RecipientDataPage.tsx +++ b/src/features/campaigns/RecipientDataPage.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { Link } from "react-router-dom"; import type { ApiSettings } from "../../types"; import Button from "../../components/Button"; @@ -31,7 +31,6 @@ export default function RecipientDataPage({ settings, campaignId }: { settings: const [dirty, setDirty] = useState(false); const [saveState, setSaveState] = useState("Loaded"); const [localError, setLocalError] = useState(""); - const loadedVersionId = useRef(null); const version = data.currentVersion; const locked = isAuditLockedVersion(version); @@ -49,8 +48,6 @@ export default function RecipientDataPage({ settings, campaignId }: { settings: useEffect(() => { if (!version) return; - if (loadedVersionId.current === version.id) return; - loadedVersionId.current = version.id; setDraft(ensureCampaignDraft(version)); setDirty(false); setSaveState(version.autosaved_at ? `Loaded saved draft ${formatDateTime(version.autosaved_at)}` : "Loaded"); diff --git a/src/features/campaigns/ReviewDataPage.tsx b/src/features/campaigns/ReviewDataPage.tsx index cb5315a..e7e3d1b 100644 --- a/src/features/campaigns/ReviewDataPage.tsx +++ b/src/features/campaigns/ReviewDataPage.tsx @@ -8,7 +8,7 @@ import { useCampaignWorkspaceData } from "./hooks/useCampaignWorkspaceData"; import { asArray, asRecord, formatDateTime, stringifyPreview, summaryValue } from "./utils/campaignView"; export default function ReviewDataPage({ settings, campaignId }: { settings: ApiSettings; campaignId: string }) { - const { data, loading, error, reload } = useCampaignWorkspaceData(settings, campaignId); + const { data, loading, error, reload } = useCampaignWorkspaceData(settings, campaignId, { includeSummary: true }); const version = data.currentVersion; const issues = collectIssues(data.summary?.issues); diff --git a/src/features/campaigns/SendDataPage.tsx b/src/features/campaigns/SendDataPage.tsx index b297ad1..e40c580 100644 --- a/src/features/campaigns/SendDataPage.tsx +++ b/src/features/campaigns/SendDataPage.tsx @@ -8,7 +8,7 @@ import { useCampaignWorkspaceData } from "./hooks/useCampaignWorkspaceData"; import { asRecord, getDeliverySection, getNestedString } from "./utils/campaignView"; export default function SendDataPage({ settings, campaignId }: { settings: ApiSettings; campaignId: string }) { - const { data, loading, error, reload } = useCampaignWorkspaceData(settings, campaignId); + const { data, loading, error, reload } = useCampaignWorkspaceData(settings, campaignId, { includeSummary: true }); const cards = data.summary?.cards; const delivery = getDeliverySection(data.currentVersion); const rateLimit = asRecord(delivery.rate_limit); diff --git a/src/features/campaigns/TemplateDataPage.tsx b/src/features/campaigns/TemplateDataPage.tsx index 84c143a..a79a862 100644 --- a/src/features/campaigns/TemplateDataPage.tsx +++ b/src/features/campaigns/TemplateDataPage.tsx @@ -474,7 +474,7 @@ function buildPreviewContext(draft: Record | null, entry: Recor addContextValue(context, key, "local", value); } for (const [key, value] of Object.entries(entryFields)) { - if (canOverrideField(overridePolicy, key)) { + if (canOverrideField(overridePolicy, key) && hasPreviewOverrideValue(value)) { addContextValue(context, key, "local", value); } } @@ -505,6 +505,12 @@ function addContextValue(context: Record, key: string, namespace context[`${namespace}::${key}`] = text; } +function hasPreviewOverrideValue(value: unknown): boolean { + if (value === undefined || value === null) return false; + if (typeof value === "string") return value.trim() !== ""; + return true; +} + function renderPreviewText(text: string, context: Record, ignoreEmptyFields: boolean): string { if (!text) return ""; return text diff --git a/src/features/campaigns/hooks/useCampaignWorkspaceData.ts b/src/features/campaigns/hooks/useCampaignWorkspaceData.ts index c8f06d5..8243620 100644 --- a/src/features/campaigns/hooks/useCampaignWorkspaceData.ts +++ b/src/features/campaigns/hooks/useCampaignWorkspaceData.ts @@ -18,7 +18,22 @@ const initialData: CampaignWorkspaceData = { summary: null }; -export function useCampaignWorkspaceData(settings: ApiSettings, campaignId: string) { +type UseCampaignWorkspaceDataOptions = { + includeCurrentVersion?: boolean; + includeSummary?: boolean; + includeVersions?: boolean; +}; + +export function useCampaignWorkspaceData( + settings: ApiSettings, + campaignId: string, + options: UseCampaignWorkspaceDataOptions = {} +) { + const { + includeCurrentVersion = true, + includeSummary = false, + includeVersions = false + } = options; const [data, setData] = useState(initialData); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); @@ -28,20 +43,25 @@ export function useCampaignWorkspaceData(settings: ApiSettings, campaignId: stri setLoading(true); setError(""); try { - const [campaign, versions] = await Promise.all([ - getCampaign(settings, campaignId), - listCampaignVersions(settings, campaignId) - ]); - const selectedVersionId = campaign.current_version_id ?? versions[0]?.id; + const needsCampaign = includeCurrentVersion || includeVersions; + const summaryPromise = includeSummary ? getCampaignSummary(settings, campaignId) : Promise.resolve(null); + const campaign = needsCampaign ? await getCampaign(settings, campaignId) : null; + let versions: CampaignVersionListItem[] = []; + let selectedVersionId = campaign?.current_version_id; + + if (includeVersions || (includeCurrentVersion && !selectedVersionId)) { + versions = await listCampaignVersions(settings, campaignId); + selectedVersionId = selectedVersionId ?? versions[0]?.id; + } const [versionResult, summaryResult] = await Promise.allSettled([ - selectedVersionId ? getCampaignVersion(settings, campaignId, selectedVersionId) : Promise.resolve(null), - getCampaignSummary(settings, campaignId) + includeCurrentVersion && selectedVersionId ? getCampaignVersion(settings, campaignId, selectedVersionId) : Promise.resolve(null), + summaryPromise ]); setData({ campaign, - versions, + versions: includeVersions ? versions : [], currentVersion: versionResult.status === "fulfilled" ? (versionResult.value as CampaignVersionDetail | null) : null, summary: summaryResult.status === "fulfilled" ? (summaryResult.value as CampaignSummary | null) : null }); @@ -51,7 +71,7 @@ export function useCampaignWorkspaceData(settings: ApiSettings, campaignId: stri } finally { setLoading(false); } - }, [settings, campaignId]); + }, [settings, campaignId, includeCurrentVersion, includeSummary, includeVersions]); useEffect(() => { reload(); diff --git a/src/features/campaigns/wizard/CreateWizard.tsx b/src/features/campaigns/wizard/CreateWizard.tsx index 59871cb..bd2f28a 100644 --- a/src/features/campaigns/wizard/CreateWizard.tsx +++ b/src/features/campaigns/wizard/CreateWizard.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from "react"; +import { useEffect, useState } from "react"; import { Link } from "react-router-dom"; import type { ApiSettings, WizardStep } from "../../../types"; import Stepper from "../../../components/Stepper"; @@ -36,7 +36,6 @@ export default function CreateWizard({ settings, campaignId }: { settings: ApiSe const [saveState, setSaveState] = useState("Loading…"); const [localError, setLocalError] = useState(""); const [validationMessage, setValidationMessage] = useState(""); - const loadedVersionId = useRef(null); const index = steps.findIndex((s) => s.id === activeStep); const { data, loading, reload } = useCampaignWorkspaceData(settings, campaignId); const version = data.currentVersion; @@ -44,8 +43,6 @@ export default function CreateWizard({ settings, campaignId }: { settings: ApiSe useEffect(() => { if (!version) return; - if (loadedVersionId.current === version.id) return; - loadedVersionId.current = version.id; setDraft(ensureCampaignDraft(version)); setDirty(false); setSaveState(version.autosaved_at ? `Loaded saved draft ${formatDateTime(version.autosaved_at)}` : "Loaded");