diff --git a/src/api/campaigns.ts b/src/api/campaigns.ts index 1dff887..985ec7e 100644 --- a/src/api/campaigns.ts +++ b/src/api/campaigns.ts @@ -9,6 +9,14 @@ export type CampaignListResponse = results?: CampaignListItem[]; }; + +export type CampaignUpdatePayload = { + external_id?: string | null; + name?: string | null; + status?: string | null; + description?: string | null; +}; + export type CampaignCreateMinimalPayload = { external_id?: string; name?: string; @@ -138,6 +146,17 @@ export async function getCampaign(settings: ApiSettings, campaignId: string): Pr return apiFetch(settings, `/api/v1/campaigns/${campaignId}`); } +export async function updateCampaignMetadata( + settings: ApiSettings, + campaignId: string, + payload: CampaignUpdatePayload +): Promise { + return apiFetch(settings, `/api/v1/campaigns/${campaignId}`, { + method: "PUT", + body: JSON.stringify(payload) + }); +} + export async function createNewCampaign( settings: ApiSettings, overrides: CampaignCreateMinimalPayload = {} @@ -177,6 +196,16 @@ export async function getCampaignVersion( return apiFetch(settings, `/api/v1/campaigns/${campaignId}/versions/${versionId}`); } +export async function unlockCampaignVersionValidation( + settings: ApiSettings, + campaignId: string, + versionId: string +): Promise { + return apiFetch(settings, `/api/v1/campaigns/${campaignId}/versions/${versionId}/unlock-validation`, { + method: "POST" + }); +} + export async function updateCampaignVersion( settings: ApiSettings, campaignId: string, @@ -189,6 +218,18 @@ export async function updateCampaignVersion( }); } +export async function forkCampaignVersion( + settings: ApiSettings, + campaignId: string, + versionId: string, + payload: CampaignVersionUpdatePayload = {} +): Promise { + return apiFetch(settings, `/api/v1/campaigns/${campaignId}/versions/${versionId}/fork`, { + method: "POST", + body: JSON.stringify(payload) + }); +} + export async function autosaveCampaignVersion( settings: ApiSettings, campaignId: string, diff --git a/src/components/ConfirmDialog.tsx b/src/components/ConfirmDialog.tsx new file mode 100644 index 0000000..d943348 --- /dev/null +++ b/src/components/ConfirmDialog.tsx @@ -0,0 +1,44 @@ +import Button from "./Button"; + +export type ConfirmDialogTone = "default" | "danger"; + +export type ConfirmDialogProps = { + open: boolean; + title: string; + message: string; + confirmLabel?: string; + cancelLabel?: string; + tone?: ConfirmDialogTone; + busy?: boolean; + onConfirm: () => void; + onCancel: () => void; +}; + +export default function ConfirmDialog({ + open, + title, + message, + confirmLabel = "Confirm", + cancelLabel = "Cancel", + tone = "default", + busy = false, + onConfirm, + onCancel +}: ConfirmDialogProps) { + if (!open) return null; + + return ( +
{ + if (event.target === event.currentTarget && !busy) onCancel(); + }}> +
+

{title}

+

{message}

+
+ + +
+
+
+ ); +} diff --git a/src/features/campaigns/AttachmentsDataPage.tsx b/src/features/campaigns/AttachmentsDataPage.tsx index efd80c4..c47fa1c 100644 --- a/src/features/campaigns/AttachmentsDataPage.tsx +++ b/src/features/campaigns/AttachmentsDataPage.tsx @@ -5,10 +5,12 @@ import Button from "../../components/Button"; import Card from "../../components/Card"; import PageTitle from "../../components/PageTitle"; import LoadingFrame from "../../components/LoadingFrame"; +import LockedVersionNotice from "./components/LockedVersionNotice"; +import VersionLine from "./components/VersionLine"; import ToggleSwitch from "../../components/ToggleSwitch"; import { useCampaignWorkspaceData } from "./hooks/useCampaignWorkspaceData"; import { useCampaignDraftEditor } from "./hooks/useCampaignDraftEditor"; -import { asRecord, isAuditLockedVersion, versionLockReason } from "./utils/campaignView"; +import { asRecord, isAuditLockedVersion } from "./utils/campaignView"; import { updateNested } from "./utils/draftEditor"; import { AttachmentRulesTable } from "./components/AttachmentRulesOverlay"; import { countIndividualAttachmentRules, createAttachmentBasePath, createAttachmentRule, ensureAttachmentBasePaths, mockAttachmentPathOptions, normalizeAttachmentBasePaths, normalizeAttachmentRules, summarizeAttachmentRules, type AttachmentBasePath } from "./utils/attachments"; @@ -73,7 +75,7 @@ export default function AttachmentsDataPage({ settings, campaignId }: { settings
Attachments -

Version {version ? `#${version.version_number}` : "—"} · {saveState}

+
@@ -84,7 +86,7 @@ export default function AttachmentsDataPage({ settings, campaignId }: { settings {error &&
{error}
} {localError &&
{localError}
} - {locked &&
This version is read-only. {versionLockReason(version)}
} + {locked && } <> diff --git a/src/features/campaigns/CampaignAuditPage.tsx b/src/features/campaigns/CampaignAuditPage.tsx index a01ec62..2b11471 100644 --- a/src/features/campaigns/CampaignAuditPage.tsx +++ b/src/features/campaigns/CampaignAuditPage.tsx @@ -2,9 +2,9 @@ import type { ApiSettings } from "../../types"; import Button from "../../components/Button"; import Card from "../../components/Card"; import PageTitle from "../../components/PageTitle"; +import VersionLine from "./components/VersionLine"; import LoadingFrame from "../../components/LoadingFrame"; import { useCampaignWorkspaceData } from "./hooks/useCampaignWorkspaceData"; -import { formatDateTime } from "./utils/campaignView"; export default function CampaignAuditPage({ settings, campaignId }: { settings: ApiSettings; campaignId: string }) { const { data, loading, error, reload } = useCampaignWorkspaceData(settings, campaignId); @@ -15,7 +15,7 @@ export default function CampaignAuditPage({ settings, campaignId }: { settings:
Audit log -

Version {version ? `#${version.version_number}` : "—"} · Loaded {formatDateTime(version?.updated_at)}

+
diff --git a/src/features/campaigns/CampaignDataPage.tsx b/src/features/campaigns/CampaignDataPage.tsx deleted file mode 100644 index 831763c..0000000 --- a/src/features/campaigns/CampaignDataPage.tsx +++ /dev/null @@ -1,168 +0,0 @@ -import { useMemo } from "react"; -import { Link } from "react-router-dom"; -import type { ApiSettings } from "../../types"; -import Button from "../../components/Button"; -import Card from "../../components/Card"; -import FormField from "../../components/FormField"; -import PageTitle from "../../components/PageTitle"; -import LoadingFrame from "../../components/LoadingFrame"; -import ToggleSwitch from "../../components/ToggleSwitch"; -import EmailAddressInput from "../../components/email/EmailAddressInput"; -import { addressesFromValue, collectCampaignAddressSuggestions, type MailboxAddress } from "../../utils/emailAddresses"; -import { useCampaignWorkspaceData } from "./hooks/useCampaignWorkspaceData"; -import { useCampaignDraftEditor } from "./hooks/useCampaignDraftEditor"; -import { asRecord, isAuditLockedVersion, versionLockReason } from "./utils/campaignView"; -import { getBool, getText } from "./utils/draftEditor"; - -const campaignModeOptions = ["draft", "test", "send"]; - -export default function CampaignDataPage({ settings, campaignId }: { settings: ApiSettings; campaignId: string }) { - const { data, loading, error, reload, setError } = useCampaignWorkspaceData(settings, campaignId); - - const version = data.currentVersion; - const locked = isAuditLockedVersion(version); - const { draft, displayDraft, dirty, saveState, localError, patch, saveDraft } = useCampaignDraftEditor({ - settings, - campaignId, - version, - locked, - reload, - setError, - currentStep: "campaign-settings", - unsavedTitle: "Unsaved general campaign data", - unsavedMessage: "General campaign data has unsaved changes. Save it before leaving, or discard it and continue." - }); - const campaign = asRecord(displayDraft.campaign); - const recipients = asRecord(displayDraft.recipients); - const from = asRecord(recipients.from); - const defaultFrom = addressesFromValue(from); - const globalReplyTo = addressesFromValue(recipients.reply_to); - const addressSuggestions = useMemo(() => collectCampaignAddressSuggestions(displayDraft), [displayDraft]); - - - - - - return ( -
-
-
- General -

Version {version ? `#${version.version_number}` : "—"} · {saveState}

-
-
- - -
-
- - {error &&
{error}
} - {localError &&
{localError}
} - {locked &&
This version is read-only. {versionLockReason(version)} Copy the campaign before editing general campaign data.
} - - - <> -
- -
- - patch(["campaign", "id"], event.target.value)} /> - - - - - - patch(["campaign", "name"], event.target.value)} /> - - -