chore: sync GovOPlaN module split state

This commit is contained in:
2026-07-10 12:51:18 +02:00
parent 57f6066bf7
commit 52e6ed49c5
66 changed files with 8868 additions and 4072 deletions

View File

@@ -14,17 +14,17 @@ import { useCampaignDraftEditor } from "../hooks/useCampaignDraftEditor";
import { AttachmentsStep, BasicsStep, FieldsStep, RecipientsStep, ReviewStep, SenderStep, SendStep, TemplateStep } from "./steps/CreateWizardSteps";
const steps: WizardStep[] = [
{ id: "basics", label: "Basics", description: "Name and scenario" },
{ id: "sender", label: "Sender", description: "Mail account and headers" },
{ id: "fields", label: "Fields", description: "Define campaign data" },
{ id: "recipients", label: "Recipients", description: "Import and map source data" },
{ id: "template", label: "Template", description: "Subject and body" },
{ id: "attachments", label: "Attachments", description: "Rules and ZIP options" },
{ id: "review", label: "Review", description: "Validate before build" },
{ id: "send", label: "Send", description: "Test and queue" }
];
{ id: "basics", label: "i18n:govoplan-campaign.basics.5fcebeef", description: "i18n:govoplan-campaign.name_and_scenario.f2bc5241" },
{ id: "sender", label: "i18n:govoplan-campaign.sender.17b874d2", description: "i18n:govoplan-campaign.mail_account_and_headers.9e269243" },
{ id: "fields", label: "i18n:govoplan-campaign.fields.e8b68527", description: "i18n:govoplan-campaign.define_campaign_data.7045c0a4" },
{ id: "recipients", label: "i18n:govoplan-campaign.recipients.78cbf8eb", description: "i18n:govoplan-campaign.import_and_map_source_data.b875d471" },
{ id: "template", label: "i18n:govoplan-campaign.template.3ec1ae06", description: "i18n:govoplan-campaign.subject_and_body.915da39b" },
{ id: "attachments", label: "i18n:govoplan-campaign.attachments.6771ade6", description: "i18n:govoplan-campaign.rules_and_zip_options.e3656774" },
{ id: "review", label: "i18n:govoplan-campaign.review.e29a79fe", description: "i18n:govoplan-campaign.validate_before_build.27723ad7" },
{ id: "send", label: "i18n:govoplan-campaign.send.9bc2575c", description: "i18n:govoplan-campaign.test_and_queue.c3c940e4" }];
export default function CreateWizard({ settings, campaignId }: { settings: ApiSettings; campaignId: string }) {
export default function CreateWizard({ settings, campaignId }: {settings: ApiSettings;campaignId: string;}) {
const [activeStep, setActiveStep] = useState("basics");
const [localError, setLocalError] = useState("");
const [validationMessage, setValidationMessage] = useState("");
@@ -41,8 +41,8 @@ export default function CreateWizard({ settings, campaignId }: { settings: ApiSe
setError: setLocalError,
currentFlow: "create",
currentStep: () => activeStep,
unsavedTitle: "Unsaved wizard changes",
unsavedMessage: "This campaign wizard has unsaved changes. Save them before leaving, or discard them and continue.",
unsavedTitle: "i18n:govoplan-campaign.unsaved_wizard_changes.ba907144",
unsavedMessage: "i18n:govoplan-campaign.this_campaign_wizard_has_unsaved_changes_save_th.7e4be033",
onLoaded: (loadedVersion) => {
if (loadedVersion.current_step && steps.some((step) => step.id === loadedVersion.current_step)) {
setActiveStep(loadedVersion.current_step);
@@ -68,7 +68,7 @@ export default function CreateWizard({ settings, campaignId }: { settings: ApiSe
async function validateCurrentStep() {
if (!version || !draft) return;
setValidationMessage("Validating…");
setValidationMessage("i18n:govoplan-campaign.validating.c07434c9");
try {
const result = await validatePartial(settings, campaignId, version.id, { campaign_json: draft, section: activeStep });
setValidationMessage(`${result.error_count} errors, ${result.warning_count} warnings, ${result.info_count} info messages.`);
@@ -84,9 +84,9 @@ export default function CreateWizard({ settings, campaignId }: { settings: ApiSe
<div className="wizard-body standalone-wizard-body">
<div className="wizard-heading">
<div>
<PageTitle>Create campaign</PageTitle>
<PageTitle>i18n:govoplan-campaign.create_campaign.59812bbc</PageTitle>
</div>
<div className="save-state">Locked</div>
<div className="save-state">i18n:govoplan-campaign.locked.a798882f</div>
</div>
<Card>
<LockedVersionNotice
@@ -95,16 +95,16 @@ export default function CreateWizard({ settings, campaignId }: { settings: ApiSe
version={data.currentVersion}
currentVersionId={data.campaign?.current_version_id}
reload={reload}
message="This wizard is read-only for the selected version."
/>
message="i18n:govoplan-campaign.this_wizard_is_read_only_for_the_selected_versio.b0865947" />
<div className="button-row">
<Link to="../.."><Button>Back to overview</Button></Link>
<Link to="../.."><Button>i18n:govoplan-campaign.back_to_overview.ec986cba</Button></Link>
</div>
</Card>
</div>
</div>
</div>
);
</div>);
}
return (
@@ -114,7 +114,7 @@ export default function CreateWizard({ settings, campaignId }: { settings: ApiSe
<div className="wizard-body">
<div className="wizard-heading">
<div>
<PageTitle loading={loading}>Create campaign</PageTitle>
<PageTitle loading={loading}>i18n:govoplan-campaign.create_campaign.59812bbc</PageTitle>
</div>
<div className="save-state">{saveState}</div>
</div>
@@ -131,13 +131,13 @@ export default function CreateWizard({ settings, campaignId }: { settings: ApiSe
{draft && activeStep === "send" && <SendStep draft={draft} patch={patch} />}
</Card>
<div className="wizard-footer">
<Button onClick={previousStep}>Back</Button>
<Button onClick={() => saveDraft("manual")} disabled={!dirty}>{dirty ? "Save now" : "Saved"}</Button>
<Button onClick={validateCurrentStep}>Validate step</Button>
<Button variant="primary" onClick={nextStep}>Continue</Button>
<Button onClick={previousStep}>i18n:govoplan-campaign.back.b52b36b7</Button>
<Button onClick={() => saveDraft("manual")} disabled={!dirty}>{dirty ? "i18n:govoplan-campaign.save_now.3989b7c0" : "i18n:govoplan-campaign.saved.c0ae8f6e"}</Button>
<Button onClick={validateCurrentStep}>i18n:govoplan-campaign.validate_step.6a8b527c</Button>
<Button variant="primary" onClick={nextStep}>i18n:govoplan-campaign.continue.2e026239</Button>
</div>
</div>
</div>
</div>
);
}
</div>);
}

View File

@@ -3,20 +3,20 @@ import { MetricCard } from "@govoplan/core-webui";
import { Button } from "@govoplan/core-webui";
export default function ReviewWizard() {
return (
<div className="content-pad">
<div className="page-heading">
<h1>Review Wizard</h1>
<div className="content-pad workspace-data-page">
<div className="page-heading workspace-heading">
<h1>i18n:govoplan-campaign.review_wizard.3d8bf0aa</h1>
</div>
<div className="metric-grid">
<MetricCard label="Needs review" value="—" tone="warning" />
<MetricCard label="Missing attachments" value="—" tone="warning" />
<MetricCard label="Ambiguous matches" value="—" tone="info" />
<MetricCard label="Blocked" value="—" tone="danger" />
<MetricCard label="i18n:govoplan-campaign.needs_review.33a506cf" value="—" tone="warning" />
<MetricCard label="i18n:govoplan-campaign.missing_attachments.729ad125" value="—" tone="warning" />
<MetricCard label="i18n:govoplan-campaign.ambiguous_matches.dc658a9c" value="—" tone="info" />
<MetricCard label="i18n:govoplan-campaign.blocked.99613c74" value="—" tone="danger" />
</div>
<Card title="Resolution workflow">
<p className="muted">This wizard will guide users through issues one class at a time.</p>
<Button variant="primary">Start review</Button>
<Card title="i18n:govoplan-campaign.resolution_workflow.708d6c0b">
<p className="muted">i18n:govoplan-campaign.this_wizard_will_guide_users_through_issues_one_.5cb212c3</p>
<Button variant="primary">i18n:govoplan-campaign.start_review.d0bc5cdf</Button>
</Card>
</div>
);
</div>);
}

View File

@@ -2,20 +2,20 @@ import { Card } from "@govoplan/core-webui";
import { Button } from "@govoplan/core-webui";
export default function SendWizard() {
return (
<div className="content-pad">
<div className="page-heading">
<h1>Send Wizard</h1>
<div className="content-pad workspace-data-page">
<div className="page-heading workspace-heading">
<h1>i18n:govoplan-campaign.send_wizard.3c137422</h1>
</div>
<div className="dashboard-grid">
<Card title="Test send">
<p className="muted">Send one generated message to a test address.</p>
<Button>Open test-send dialog</Button>
<Card title="i18n:govoplan-campaign.test_send.03dfff38">
<p className="muted">i18n:govoplan-campaign.send_one_generated_message_to_a_test_address.bc0a4e47</p>
<Button>i18n:govoplan-campaign.open_test_send_dialog.661db713</Button>
</Card>
<Card title="Queue estimate">
<p className="muted">Estimated duration will be based on ready jobs and rate limits.</p>
<Button variant="primary">Queue dry run</Button>
<Card title="i18n:govoplan-campaign.queue_estimate.5480288a">
<p className="muted">i18n:govoplan-campaign.estimated_duration_will_be_based_on_ready_jobs_a.15b63283</p>
<Button variant="primary">i18n:govoplan-campaign.queue_dry_run.800e1606</Button>
</Card>
</div>
</div>
);
</div>);
}

View File

@@ -21,24 +21,24 @@ export function BasicsStep({ draft, patch }: WizardStepProps) {
const campaign = asRecord(draft.campaign);
return (
<div className="form-grid">
<FormField label="Campaign name" help="A human-readable name shown in lists and reports.">
<FormField label="i18n:govoplan-campaign.campaign_name.aa5d0e72" help="i18n:govoplan-campaign.a_human_readable_name_shown_in_lists_and_reports.afc23e7e">
<input value={getText(campaign, "name")} onChange={(event) => patch(["campaign", "name"], event.target.value)} />
</FormField>
<FormField label="Campaign ID" help="Stable technical identifier.">
<FormField label="i18n:govoplan-campaign.campaign_id.4c4ed79e" help="i18n:govoplan-campaign.stable_technical_identifier.28c406b2">
<input value={getText(campaign, "id")} onChange={(event) => patch(["campaign", "id"], event.target.value)} />
</FormField>
<FormField label="Mode">
<FormField label="i18n:govoplan-campaign.mode.a7b93d21">
<select value={getText(campaign, "mode", "draft")} onChange={(event) => patch(["campaign", "mode"], event.target.value)}>
<option value="draft">Draft</option>
<option value="test">Test</option>
<option value="send">Send</option>
<option value="draft">i18n:govoplan-campaign.draft.23d33e22</option>
<option value="test">i18n:govoplan-campaign.test.640ab2ba</option>
<option value="send">i18n:govoplan-campaign.send.9bc2575c</option>
</select>
</FormField>
<FormField label="Description">
<FormField label="i18n:govoplan-campaign.description.55f8ebc8">
<textarea rows={5} value={getText(campaign, "description")} onChange={(event) => patch(["campaign", "description"], event.target.value)} />
</FormField>
</div>
);
</div>);
}
export function SenderStep({ draft, patch }: WizardStepProps) {
@@ -55,86 +55,86 @@ export function SenderStep({ draft, patch }: WizardStepProps) {
const imapAppend = asRecord(delivery.imap_append_sent);
return (
<div className="form-grid">
<FormField label="Default From address">
<FormField label="i18n:govoplan-campaign.default_from_address.b9ee6d77">
<EmailAddressInput
value={from}
suggestions={suggestions}
allowMultiple
addLabel="Add From"
emptyText="No global From address configured."
onChange={(addresses: MailboxAddress[]) => patch(["recipients", "from"], addresses)}
/>
addLabel="i18n:govoplan-campaign.add_from.a095bb50"
emptyText="i18n:govoplan-campaign.no_global_from_address_configured.28141d2b"
onChange={(addresses: MailboxAddress[]) => patch(["recipients", "from"], addresses)} />
</FormField>
<FormField label="Global recipients">
<FormField label="i18n:govoplan-campaign.global_recipients.d9aaa427">
<EmailAddressInput
value={globalTo}
suggestions={suggestions}
allowMultiple
addLabel="Add recipient"
emptyText="No global recipients configured."
onChange={(addresses: MailboxAddress[]) => patch(["recipients", "to"], addresses)}
/>
addLabel="i18n:govoplan-campaign.add_recipient.a989d1f1"
emptyText="i18n:govoplan-campaign.no_global_recipients_configured.d4e20e92"
onChange={(addresses: MailboxAddress[]) => patch(["recipients", "to"], addresses)} />
</FormField>
<FormField label="CC">
<FormField label="i18n:govoplan-campaign.cc.c5a976de">
<EmailAddressInput
value={globalCc}
suggestions={suggestions}
allowMultiple
addLabel="Add CC"
emptyText="No global CC recipients configured."
onChange={(addresses: MailboxAddress[]) => patch(["recipients", "cc"], addresses)}
/>
addLabel="i18n:govoplan-campaign.add_cc.bcb39ea3"
emptyText="i18n:govoplan-campaign.no_global_cc_recipients_configured.8afb23c1"
onChange={(addresses: MailboxAddress[]) => patch(["recipients", "cc"], addresses)} />
</FormField>
<FormField label="BCC">
<FormField label="i18n:govoplan-campaign.bcc.4c0145a3">
<EmailAddressInput
value={globalBcc}
suggestions={suggestions}
allowMultiple
addLabel="Add BCC"
emptyText="No global BCC recipients configured."
onChange={(addresses: MailboxAddress[]) => patch(["recipients", "bcc"], addresses)}
/>
addLabel="i18n:govoplan-campaign.add_bcc.ae9feacc"
emptyText="i18n:govoplan-campaign.no_global_bcc_recipients_configured.86ef64ab"
onChange={(addresses: MailboxAddress[]) => patch(["recipients", "bcc"], addresses)} />
</FormField>
<FormField label="Reply-To">
<FormField label="i18n:govoplan-campaign.reply_to.c1733667">
<EmailAddressInput
value={globalReplyTo.slice(0, 1)}
suggestions={suggestions}
allowMultiple={false}
showAddButton={false}
addLabel={globalReplyTo.length ? "Replace" : "Add Reply-To"}
emptyText="No Reply-To address configured."
onChange={(addresses: MailboxAddress[]) => patch(["recipients", "reply_to"], addresses.slice(0, 1))}
/>
addLabel={globalReplyTo.length ? "i18n:govoplan-campaign.replace.a7cf7b25" : "i18n:govoplan-campaign.add_reply_to.84b195f4"}
emptyText="i18n:govoplan-campaign.no_reply_to_address_configured.0665c1d9"
onChange={(addresses: MailboxAddress[]) => patch(["recipients", "reply_to"], addresses.slice(0, 1))} />
</FormField>
<FormField label="SMTP host"><input value={getText(smtp, "host")} onChange={(event) => patch(["server", "smtp", "host"], event.target.value)} /></FormField>
<FormField label="SMTP port"><input type="number" value={getNumber(smtp, "port", 587)} onChange={(event) => patch(["server", "smtp", "port"], Number(event.target.value || 0))} /></FormField>
<ToggleSwitch label="Append successful messages to Sent via IMAP" checked={getBool(imapAppend, "enabled")} onChange={(checked) => patch(["delivery", "imap_append_sent", "enabled"], checked)} />
</div>
);
<FormField label="i18n:govoplan-campaign.smtp_host.2d4a434b"><input value={getText(smtp, "host")} onChange={(event) => patch(["server", "smtp", "host"], event.target.value)} /></FormField>
<FormField label="i18n:govoplan-campaign.smtp_port.65b5a108"><input type="number" value={getNumber(smtp, "port", 587)} onChange={(event) => patch(["server", "smtp", "port"], Number(event.target.value || 0))} /></FormField>
<ToggleSwitch label="i18n:govoplan-campaign.append_successful_messages_to_sent_via_imap.dbd1b1d8" checked={getBool(imapAppend, "enabled")} onChange={(checked) => patch(["delivery", "imap_append_sent", "enabled"], checked)} />
</div>);
}
export function FieldsStep({ draft, patchRoot }: { draft: Record<string, unknown>; patchRoot: (key: string, value: unknown) => void }) {
export function FieldsStep({ draft, patchRoot }: {draft: Record<string, unknown>;patchRoot: (key: string, value: unknown) => void;}) {
return (
<div>
<div className="step-intro">
<h2>Campaign fields</h2>
<p>Define reusable fields for templates, attachment rules, ZIP passwords and recipient data.</p>
<h2>i18n:govoplan-campaign.campaign_fields.969e7d80</h2>
<p>i18n:govoplan-campaign.define_reusable_fields_for_templates_attachment_.6424f892</p>
</div>
<JsonEditor value={draft.fields ?? []} onValid={(value) => patchRoot("fields", value)} />
</div>
);
</div>);
}
export function RecipientsStep({ draft, patchRoot }: { draft: Record<string, unknown>; patchRoot: (key: string, value: unknown) => void }) {
export function RecipientsStep({ draft, patchRoot }: {draft: Record<string, unknown>;patchRoot: (key: string, value: unknown) => void;}) {
return (
<div>
<div className="step-intro">
<h2>Recipients</h2>
<p>Store inline recipients or source/mapping configuration. A table editor will replace this JSON editor in the recipient section pass.</p>
<h2>i18n:govoplan-campaign.recipients.78cbf8eb</h2>
<p>i18n:govoplan-campaign.store_inline_recipients_or_source_mapping_config.77bbe98f</p>
</div>
<JsonEditor value={draft.entries ?? { inline: [] }} onValid={(value) => patchRoot("entries", value)} />
</div>
);
</div>);
}
export function TemplateStep({ draft, patch }: WizardStepProps) {
@@ -142,16 +142,16 @@ export function TemplateStep({ draft, patch }: WizardStepProps) {
return (
<div>
<div className="step-intro">
<h2>Template</h2>
<p>Compose the subject and body. Merge fields can later be inserted from the field picker.</p>
<h2>i18n:govoplan-campaign.template.3ec1ae06</h2>
<p>i18n:govoplan-campaign.compose_the_subject_and_body_merge_fields_can_la.4fb6e97b</p>
</div>
<div className="form-grid">
<FormField label="Subject"><input value={getText(template, "subject")} onChange={(event) => patch(["template", "subject"], event.target.value)} /></FormField>
<FormField label="Plain text body"><textarea rows={12} value={getText(template, "text")} onChange={(event) => patch(["template", "text"], event.target.value)} /></FormField>
<FormField label="HTML body"><textarea rows={8} value={getText(template, "html")} onChange={(event) => patch(["template", "html"], event.target.value)} /></FormField>
<FormField label="i18n:govoplan-campaign.subject.8d183dbd"><input value={getText(template, "subject")} onChange={(event) => patch(["template", "subject"], event.target.value)} /></FormField>
<FormField label="i18n:govoplan-campaign.plain_text_body.030a0da0"><textarea rows={12} value={getText(template, "text")} onChange={(event) => patch(["template", "text"], event.target.value)} /></FormField>
<FormField label="i18n:govoplan-campaign.html_body.77b5ba37"><textarea rows={8} value={getText(template, "html")} onChange={(event) => patch(["template", "html"], event.target.value)} /></FormField>
</div>
</div>
);
</div>);
}
export function AttachmentsStep({ draft, patch }: WizardStepProps) {
@@ -159,36 +159,36 @@ export function AttachmentsStep({ draft, patch }: WizardStepProps) {
return (
<div>
<div className="step-intro">
<h2>Attachments</h2>
<p>Configure campaign-wide attachment behavior and global matching rules.</p>
<h2>i18n:govoplan-campaign.attachments.6771ade6</h2>
<p>i18n:govoplan-campaign.configure_campaign_wide_attachment_behavior_and_.441a6cd2</p>
</div>
<div className="form-grid compact responsive-form-grid">
<FormField label="Campaign attachment base path"><input value={getText(attachments, "base_path", ".")} onChange={(event) => patch(["attachments", "base_path"], event.target.value)} /></FormField>
<FormField label="Missing behavior"><select value={getText(attachments, "missing_behavior", "ask")} onChange={(event) => patch(["attachments", "missing_behavior"], event.target.value)}>{behaviorOptions.map((option) => <option key={option}>{option}</option>)}</select></FormField>
<FormField label="Ambiguous behavior"><select value={getText(attachments, "ambiguous_behavior", "ask")} onChange={(event) => patch(["attachments", "ambiguous_behavior"], event.target.value)}>{behaviorOptions.map((option) => <option key={option}>{option}</option>)}</select></FormField>
<ToggleSwitch label="Allow individual attachments" checked={getBool(attachments, "allow_individual")} onChange={(checked) => patch(["attachments", "allow_individual"], checked)} />
<FormField label="i18n:govoplan-campaign.campaign_attachment_base_path.84827619"><input value={getText(attachments, "base_path", ".")} onChange={(event) => patch(["attachments", "base_path"], event.target.value)} /></FormField>
<FormField label="i18n:govoplan-campaign.missing_behavior.de3d1ac7"><select value={getText(attachments, "missing_behavior", "ask")} onChange={(event) => patch(["attachments", "missing_behavior"], event.target.value)}>{behaviorOptions.map((option) => <option key={option}>{option}</option>)}</select></FormField>
<FormField label="i18n:govoplan-campaign.ambiguous_behavior.e86e724b"><select value={getText(attachments, "ambiguous_behavior", "ask")} onChange={(event) => patch(["attachments", "ambiguous_behavior"], event.target.value)}>{behaviorOptions.map((option) => <option key={option}>{option}</option>)}</select></FormField>
<ToggleSwitch label="i18n:govoplan-campaign.allow_individual_attachments.a6ff0e87" checked={getBool(attachments, "allow_individual")} onChange={(checked) => patch(["attachments", "allow_individual"], checked)} />
</div>
<JsonEditor value={attachments.global ?? []} onValid={(value) => patch(["attachments", "global"], value)} />
</div>
);
</div>);
}
export function ReviewStep({ version, onValidate }: { version: unknown; onValidate: () => void }) {
export function ReviewStep({ version, onValidate }: {version: unknown;onValidate: () => void;}) {
const record = asRecord(version);
return (
<div>
<div className="step-intro">
<h2>Review setup</h2>
<p>Validate the campaign definition before building message drafts.</p>
<h2>i18n:govoplan-campaign.review_setup.d0059acb</h2>
<p>i18n:govoplan-campaign.validate_the_campaign_definition_before_building.d36ee1dc</p>
</div>
<div className="metric-grid inside">
<MetricCard label="Errors" value={summaryValue(asRecord(record.validation_summary), ["error_count", "errors", "blocked"])} tone="danger" />
<MetricCard label="Warnings" value={summaryValue(asRecord(record.validation_summary), ["warning_count", "warnings"])} tone="warning" />
<MetricCard label="Built" value={summaryValue(asRecord(record.build_summary), ["built_count", "built", "messages_built"])} tone="info" />
<MetricCard label="i18n:govoplan-campaign.errors.805e86a8" value={summaryValue(asRecord(record.validation_summary), ["error_count", "errors", "blocked"])} tone="danger" />
<MetricCard label="i18n:govoplan-campaign.warnings.1430f976" value={summaryValue(asRecord(record.validation_summary), ["warning_count", "warnings"])} tone="warning" />
<MetricCard label="i18n:govoplan-campaign.built.a6ad3f82" value={summaryValue(asRecord(record.build_summary), ["built_count", "built", "messages_built"])} tone="info" />
</div>
<Button variant="primary" onClick={onValidate}>Validate campaign</Button>
</div>
);
<Button variant="primary" onClick={onValidate}>i18n:govoplan-campaign.validate_campaign.6934c1c2</Button>
</div>);
}
export function SendStep({ draft, patch }: WizardStepProps) {
@@ -197,19 +197,19 @@ export function SendStep({ draft, patch }: WizardStepProps) {
return (
<div>
<div className="step-intro">
<h2>Send preparation</h2>
<p>Configure rate limits and prepare the final send workflow.</p>
<h2>i18n:govoplan-campaign.send_preparation.6e078f43</h2>
<p>i18n:govoplan-campaign.configure_rate_limits_and_prepare_the_final_send.af3d4e48</p>
</div>
<div className="form-grid compact responsive-form-grid">
<FormField label="Messages per minute"><input type="number" min={1} value={getNumber(rateLimit, "messages_per_minute", 5)} onChange={(event) => patch(["delivery", "rate_limit", "messages_per_minute"], Number(event.target.value || 1))} /></FormField>
<FormField label="Concurrency"><input type="number" min={1} value={getNumber(rateLimit, "concurrency", 1)} onChange={(event) => patch(["delivery", "rate_limit", "concurrency"], Number(event.target.value || 1))} /></FormField>
<FormField label="i18n:govoplan-campaign.messages_per_minute.bea49348"><input type="number" min={1} value={getNumber(rateLimit, "messages_per_minute", 5)} onChange={(event) => patch(["delivery", "rate_limit", "messages_per_minute"], Number(event.target.value || 1))} /></FormField>
<FormField label="i18n:govoplan-campaign.concurrency.2ec390bf"><input type="number" min={1} value={getNumber(rateLimit, "concurrency", 1)} onChange={(event) => patch(["delivery", "rate_limit", "concurrency"], Number(event.target.value || 1))} /></FormField>
</div>
<p className="muted">Test send and queue actions remain in the Send Wizard for now.</p>
</div>
);
<p className="muted">i18n:govoplan-campaign.test_send_and_queue_actions_remain_in_the_send_w.60dee716</p>
</div>);
}
function JsonEditor({ value, onValid }: { value: unknown; onValid: (value: unknown) => void }) {
function JsonEditor({ value, onValid }: {value: unknown;onValid: (value: unknown) => void;}) {
const [text, setText] = useState(stringifyJson(value));
const [error, setError] = useState("");
@@ -228,8 +228,8 @@ function JsonEditor({ value, onValid }: { value: unknown; onValid: (value: unkno
return (
<div className="json-edit-block">
<textarea rows={12} value={text} onChange={(event) => change(event.target.value)} />
{error ? <p className="form-help danger-text">Invalid JSON: {error}</p> : <p className="form-help">Valid JSON is saved with the wizard draft.</p>}
{Array.isArray(value) && value.length > 0 && <p className="form-help">Preview: {stringifyPreview(asArray(value)[0], 140)}</p>}
</div>
);
}
{error ? <p className="form-help danger-text">i18n:govoplan-campaign.invalid_json.03f61ae0 {error}</p> : <p className="form-help">i18n:govoplan-campaign.valid_json_is_saved_with_the_wizard_draft.16f5d341</p>}
{Array.isArray(value) && value.length > 0 && <p className="form-help">i18n:govoplan-campaign.preview.4bf30626 {stringifyPreview(asArray(value)[0], 140)}</p>}
</div>);
}