import { useState } from "react"; import type { ApiSettings } from "../../types"; import Button from "../../components/Button"; import Card from "../../components/Card"; import FormField from "../../components/FormField"; import DismissibleAlert from "../../components/DismissibleAlert"; 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 } from "./utils/campaignView"; import { cloneJson, getBool, getNumber, getText, updateNested } from "./utils/draftEditor"; const behaviorOptions = ["block", "ask", "drop", "continue", "warn"]; type EditorState = Record; export default function GlobalSettingsPage({ settings, campaignId }: { settings: ApiSettings; campaignId: string }) { const { data, loading, error, reload, setError } = useCampaignWorkspaceData(settings, campaignId); const [editorState, setEditorState] = useState({}); const version = data.currentVersion; const locked = isAuditLockedVersion(version); const { draft, displayDraft, dirty, saveState, localError, patch, markDirty, saveDraft } = useCampaignDraftEditor({ settings, campaignId, version, locked, reload, setError, currentStep: "global-settings", unsavedTitle: "Unsaved global settings", unsavedMessage: "Global settings have unsaved changes. Save them before leaving, or discard them and continue.", extraPayload: () => ({ editor_state: editorState }), onLoaded: (loadedVersion) => setEditorState(cloneJson(loadedVersion.editor_state ?? {})), onSaved: (savedVersion) => setEditorState(cloneJson(savedVersion.editor_state ?? editorState)) }); const validationPolicy = asRecord(displayDraft.validation_policy); const attachments = asRecord(displayDraft.attachments); const delivery = asRecord(displayDraft.delivery); const rateLimit = asRecord(delivery.rate_limit); const retry = asRecord(delivery.retry); const statusTracking = asRecord(displayDraft.status_tracking); const optIns = asRecord(editorState.opt_ins); function patchEditor(path: string[], value: unknown) { if (locked) return; setEditorState((current) => updateNested(current, path, value)); markDirty(); } return (
Global settings
{error && {error}} {localError && {localError}} {locked && } <>
patch(["validation_policy", "missing_required_attachment"], value)} /> patch(["validation_policy", "missing_optional_attachment"], value)} /> patch(["validation_policy", "ambiguous_attachment_match"], value)} /> patch(["validation_policy", "unsent_attachment_files"], value)} /> patch(["validation_policy", "missing_email"], value)} options={["block", "drop"]} /> patch(["validation_policy", "template_error"], value)} options={["block", "drop"]} /> patch(["validation_policy", "ignore_empty_fields"], checked)} />
patch(["attachments", "send_without_attachments"], checked)} />

The actual global and per-recipient attachment rules live in Attachments. These settings define campaign-wide behavior used by validation and review. Individual-attachment permission is configured per attachment base path.

patch(["delivery", "rate_limit", "messages_per_minute"], Number(event.target.value || 1))} /> patch(["delivery", "rate_limit", "concurrency"], Number(event.target.value || 1))} /> patch(["delivery", "retry", "max_attempts"], Number(event.target.value || 1))} /> patch(["status_tracking", "enabled"], checked)} />
patchEditor(["opt_ins", "campaign_address_suggestions"], checked)} /> patchEditor(["opt_ins", "remember_used_addresses"], checked)} /> patchEditor(["opt_ins", "inline_guidance"], checked)} />

These opt-ins are stored in the draft editor metadata for now. A later backend patch can make address-book storage tenant/user aware.

); } function PolicySelect({ label, value, disabled, onChange, options = behaviorOptions }: { label: string; value: string; disabled?: boolean; onChange: (value: string) => void; options?: string[] }) { return ( ); }