Fix reload behaviour; don't override global with empty local value

This commit is contained in:
2026-06-10 04:17:54 +02:00
parent 7491c0a1b4
commit d666dd90ee
16 changed files with 48 additions and 39 deletions

1
.gitignore vendored
View File

@@ -420,3 +420,4 @@ bin-release/
# should NOT be excluded as they contain compiler settings and other important # should NOT be excluded as they contain compiler settings and other important
# information for Eclipse / Flash Builder. # information for Eclipse / Flash Builder.
.fuse_*

Binary file not shown.

View File

@@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import type { ApiSettings } from "../../types"; import type { ApiSettings } from "../../types";
import Button from "../../components/Button"; import Button from "../../components/Button";
@@ -22,7 +22,6 @@ export default function AttachmentsDataPage({ settings, campaignId }: { settings
const [dirty, setDirty] = useState(false); const [dirty, setDirty] = useState(false);
const [saveState, setSaveState] = useState("Loaded"); const [saveState, setSaveState] = useState("Loaded");
const [localError, setLocalError] = useState(""); const [localError, setLocalError] = useState("");
const loadedVersionId = useRef<string | null>(null);
const version = data.currentVersion; const version = data.currentVersion;
const locked = isAuditLockedVersion(version); const locked = isAuditLockedVersion(version);
@@ -34,8 +33,6 @@ export default function AttachmentsDataPage({ settings, campaignId }: { settings
useEffect(() => { useEffect(() => {
if (!version) return; if (!version) return;
if (loadedVersionId.current === version.id) return;
loadedVersionId.current = version.id;
setDraft(ensureCampaignDraft(version)); setDraft(ensureCampaignDraft(version));
setDirty(false); setDirty(false);
setSaveState(version.autosaved_at ? `Loaded saved draft ${formatDateTime(version.autosaved_at)}` : "Loaded"); setSaveState(version.autosaved_at ? `Loaded saved draft ${formatDateTime(version.autosaved_at)}` : "Loaded");

View File

@@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import type { ApiSettings } from "../../types"; import type { ApiSettings } from "../../types";
import Button from "../../components/Button"; import Button from "../../components/Button";
@@ -22,7 +22,6 @@ export default function CampaignDataPage({ settings, campaignId }: { settings: A
const [dirty, setDirty] = useState(false); const [dirty, setDirty] = useState(false);
const [saveState, setSaveState] = useState("Loaded"); const [saveState, setSaveState] = useState("Loaded");
const [localError, setLocalError] = useState(""); const [localError, setLocalError] = useState("");
const loadedVersionId = useRef<string | null>(null);
const version = data.currentVersion; const version = data.currentVersion;
const locked = isAuditLockedVersion(version); const locked = isAuditLockedVersion(version);
@@ -35,8 +34,6 @@ export default function CampaignDataPage({ settings, campaignId }: { settings: A
useEffect(() => { useEffect(() => {
if (!version) return; if (!version) return;
if (loadedVersionId.current === version.id) return;
loadedVersionId.current = version.id;
setDraft(ensureCampaignDraft(version)); setDraft(ensureCampaignDraft(version));
setDirty(false); setDirty(false);
setSaveState(version.autosaved_at ? `Loaded saved draft ${formatDateTime(version.autosaved_at)}` : "Loaded"); setSaveState(version.autosaved_at ? `Loaded saved draft ${formatDateTime(version.autosaved_at)}` : "Loaded");

View File

@@ -22,7 +22,7 @@ import { addressesFromValue } from "../../utils/emailAddresses";
export default function CampaignOverviewPage({ settings, campaignId }: { settings: ApiSettings; campaignId: string }) { export default function CampaignOverviewPage({ settings, campaignId }: { settings: ApiSettings; campaignId: string }) {
const navigate = useNavigate(); 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 [copying, setCopying] = useState(false);
const [locking, setLocking] = useState(false); const [locking, setLocking] = useState(false);
const [message, setMessage] = useState(""); const [message, setMessage] = useState("");

View File

@@ -6,7 +6,7 @@ import { useCampaignWorkspaceData } from "./hooks/useCampaignWorkspaceData";
import { formatDateTime } from "./utils/campaignView"; import { formatDateTime } from "./utils/campaignView";
export default function CampaignReportPage({ settings, campaignId }: { settings: ApiSettings; campaignId: string }) { 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; const cards = data.summary?.cards;
return ( return (

View File

@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useState } from "react";
import type { ApiSettings } from "../../types"; import type { ApiSettings } from "../../types";
import Button from "../../components/Button"; import Button from "../../components/Button";
import Card from "../../components/Card"; import Card from "../../components/Card";
@@ -22,7 +22,6 @@ export default function GlobalSettingsPage({ settings, campaignId }: { settings:
const [dirty, setDirty] = useState(false); const [dirty, setDirty] = useState(false);
const [saveState, setSaveState] = useState("Loaded"); const [saveState, setSaveState] = useState("Loaded");
const [localError, setLocalError] = useState(""); const [localError, setLocalError] = useState("");
const loadedVersionId = useRef<string | null>(null);
const version = data.currentVersion; const version = data.currentVersion;
const locked = isAuditLockedVersion(version); const locked = isAuditLockedVersion(version);
@@ -36,8 +35,6 @@ export default function GlobalSettingsPage({ settings, campaignId }: { settings:
useEffect(() => { useEffect(() => {
if (!version) return; if (!version) return;
if (loadedVersionId.current === version.id) return;
loadedVersionId.current = version.id;
setDraft(ensureCampaignDraft(version)); setDraft(ensureCampaignDraft(version));
setEditorState(cloneJson(version.editor_state ?? {})); setEditorState(cloneJson(version.editor_state ?? {}));
setDirty(false); setDirty(false);

View File

@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useState } from "react";
import type { ApiSettings } from "../../types"; import type { ApiSettings } from "../../types";
import Button from "../../components/Button"; import Button from "../../components/Button";
import Card from "../../components/Card"; import Card from "../../components/Card";
@@ -24,7 +24,6 @@ export default function MailSettingsPage({ settings, campaignId }: { settings: A
const [imapTestResult, setImapTestResult] = useState<MailConnectionTestResponse | null>(null); const [imapTestResult, setImapTestResult] = useState<MailConnectionTestResponse | null>(null);
const [folderResult, setFolderResult] = useState<MailImapFolderListResponse | null>(null); const [folderResult, setFolderResult] = useState<MailImapFolderListResponse | null>(null);
const [mailActionState, setMailActionState] = useState<"smtp" | "imap" | "folders" | null>(null); const [mailActionState, setMailActionState] = useState<"smtp" | "imap" | "folders" | null>(null);
const loadedVersionId = useRef<string | null>(null);
const version = data.currentVersion; const version = data.currentVersion;
const locked = isAuditLockedVersion(version); const locked = isAuditLockedVersion(version);
@@ -38,8 +37,6 @@ export default function MailSettingsPage({ settings, campaignId }: { settings: A
useEffect(() => { useEffect(() => {
if (!version) return; if (!version) return;
if (loadedVersionId.current === version.id) return;
loadedVersionId.current = version.id;
setDraft(ensureCampaignDraft(version)); setDraft(ensureCampaignDraft(version));
setDirty(false); setDirty(false);
setSaveState(version.autosaved_at ? `Loaded saved draft ${formatDateTime(version.autosaved_at)}` : "Loaded"); setSaveState(version.autosaved_at ? `Loaded saved draft ${formatDateTime(version.autosaved_at)}` : "Loaded");

View File

@@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import type { ApiSettings } from "../../types"; import type { ApiSettings } from "../../types";
import Button from "../../components/Button"; import Button from "../../components/Button";
@@ -31,7 +31,6 @@ export default function RecipientDataPage({ settings, campaignId }: { settings:
const [dirty, setDirty] = useState(false); const [dirty, setDirty] = useState(false);
const [saveState, setSaveState] = useState("Loaded"); const [saveState, setSaveState] = useState("Loaded");
const [localError, setLocalError] = useState(""); const [localError, setLocalError] = useState("");
const loadedVersionId = useRef<string | null>(null);
const version = data.currentVersion; const version = data.currentVersion;
const locked = isAuditLockedVersion(version); const locked = isAuditLockedVersion(version);
@@ -49,8 +48,6 @@ export default function RecipientDataPage({ settings, campaignId }: { settings:
useEffect(() => { useEffect(() => {
if (!version) return; if (!version) return;
if (loadedVersionId.current === version.id) return;
loadedVersionId.current = version.id;
setDraft(ensureCampaignDraft(version)); setDraft(ensureCampaignDraft(version));
setDirty(false); setDirty(false);
setSaveState(version.autosaved_at ? `Loaded saved draft ${formatDateTime(version.autosaved_at)}` : "Loaded"); setSaveState(version.autosaved_at ? `Loaded saved draft ${formatDateTime(version.autosaved_at)}` : "Loaded");

View File

@@ -8,7 +8,7 @@ import { useCampaignWorkspaceData } from "./hooks/useCampaignWorkspaceData";
import { asArray, asRecord, formatDateTime, stringifyPreview, summaryValue } from "./utils/campaignView"; import { asArray, asRecord, formatDateTime, stringifyPreview, summaryValue } from "./utils/campaignView";
export default function ReviewDataPage({ settings, campaignId }: { settings: ApiSettings; campaignId: string }) { 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 version = data.currentVersion;
const issues = collectIssues(data.summary?.issues); const issues = collectIssues(data.summary?.issues);

View File

@@ -8,7 +8,7 @@ import { useCampaignWorkspaceData } from "./hooks/useCampaignWorkspaceData";
import { asRecord, getDeliverySection, getNestedString } from "./utils/campaignView"; import { asRecord, getDeliverySection, getNestedString } from "./utils/campaignView";
export default function SendDataPage({ settings, campaignId }: { settings: ApiSettings; campaignId: string }) { 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 cards = data.summary?.cards;
const delivery = getDeliverySection(data.currentVersion); const delivery = getDeliverySection(data.currentVersion);
const rateLimit = asRecord(delivery.rate_limit); const rateLimit = asRecord(delivery.rate_limit);

View File

@@ -474,7 +474,7 @@ function buildPreviewContext(draft: Record<string, unknown> | null, entry: Recor
addContextValue(context, key, "local", value); addContextValue(context, key, "local", value);
} }
for (const [key, value] of Object.entries(entryFields)) { for (const [key, value] of Object.entries(entryFields)) {
if (canOverrideField(overridePolicy, key)) { if (canOverrideField(overridePolicy, key) && hasPreviewOverrideValue(value)) {
addContextValue(context, key, "local", value); addContextValue(context, key, "local", value);
} }
} }
@@ -505,6 +505,12 @@ function addContextValue(context: Record<string, string>, key: string, namespace
context[`${namespace}::${key}`] = text; 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<string, string>, ignoreEmptyFields: boolean): string { function renderPreviewText(text: string, context: Record<string, string>, ignoreEmptyFields: boolean): string {
if (!text) return ""; if (!text) return "";
return text return text

View File

@@ -18,7 +18,22 @@ const initialData: CampaignWorkspaceData = {
summary: null 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<CampaignWorkspaceData>(initialData); const [data, setData] = useState<CampaignWorkspaceData>(initialData);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(""); const [error, setError] = useState("");
@@ -28,20 +43,25 @@ export function useCampaignWorkspaceData(settings: ApiSettings, campaignId: stri
setLoading(true); setLoading(true);
setError(""); setError("");
try { try {
const [campaign, versions] = await Promise.all([ const needsCampaign = includeCurrentVersion || includeVersions;
getCampaign(settings, campaignId), const summaryPromise = includeSummary ? getCampaignSummary(settings, campaignId) : Promise.resolve(null);
listCampaignVersions(settings, campaignId) const campaign = needsCampaign ? await getCampaign(settings, campaignId) : null;
]); let versions: CampaignVersionListItem[] = [];
const selectedVersionId = campaign.current_version_id ?? versions[0]?.id; 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([ const [versionResult, summaryResult] = await Promise.allSettled([
selectedVersionId ? getCampaignVersion(settings, campaignId, selectedVersionId) : Promise.resolve(null), includeCurrentVersion && selectedVersionId ? getCampaignVersion(settings, campaignId, selectedVersionId) : Promise.resolve(null),
getCampaignSummary(settings, campaignId) summaryPromise
]); ]);
setData({ setData({
campaign, campaign,
versions, versions: includeVersions ? versions : [],
currentVersion: versionResult.status === "fulfilled" ? (versionResult.value as CampaignVersionDetail | null) : null, currentVersion: versionResult.status === "fulfilled" ? (versionResult.value as CampaignVersionDetail | null) : null,
summary: summaryResult.status === "fulfilled" ? (summaryResult.value as CampaignSummary | null) : null summary: summaryResult.status === "fulfilled" ? (summaryResult.value as CampaignSummary | null) : null
}); });
@@ -51,7 +71,7 @@ export function useCampaignWorkspaceData(settings: ApiSettings, campaignId: stri
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [settings, campaignId]); }, [settings, campaignId, includeCurrentVersion, includeSummary, includeVersions]);
useEffect(() => { useEffect(() => {
reload(); reload();

View File

@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useState } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import type { ApiSettings, WizardStep } from "../../../types"; import type { ApiSettings, WizardStep } from "../../../types";
import Stepper from "../../../components/Stepper"; import Stepper from "../../../components/Stepper";
@@ -36,7 +36,6 @@ export default function CreateWizard({ settings, campaignId }: { settings: ApiSe
const [saveState, setSaveState] = useState("Loading…"); const [saveState, setSaveState] = useState("Loading…");
const [localError, setLocalError] = useState(""); const [localError, setLocalError] = useState("");
const [validationMessage, setValidationMessage] = useState(""); const [validationMessage, setValidationMessage] = useState("");
const loadedVersionId = useRef<string | null>(null);
const index = steps.findIndex((s) => s.id === activeStep); const index = steps.findIndex((s) => s.id === activeStep);
const { data, loading, reload } = useCampaignWorkspaceData(settings, campaignId); const { data, loading, reload } = useCampaignWorkspaceData(settings, campaignId);
const version = data.currentVersion; const version = data.currentVersion;
@@ -44,8 +43,6 @@ export default function CreateWizard({ settings, campaignId }: { settings: ApiSe
useEffect(() => { useEffect(() => {
if (!version) return; if (!version) return;
if (loadedVersionId.current === version.id) return;
loadedVersionId.current = version.id;
setDraft(ensureCampaignDraft(version)); setDraft(ensureCampaignDraft(version));
setDirty(false); setDirty(false);
setSaveState(version.autosaved_at ? `Loaded saved draft ${formatDateTime(version.autosaved_at)}` : "Loaded"); setSaveState(version.autosaved_at ? `Loaded saved draft ${formatDateTime(version.autosaved_at)}` : "Loaded");