fix(webui): bind consequential campaign controls to help
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@govoplan/campaign-webui",
|
||||
"version": "0.1.26",
|
||||
"version": "0.1.27",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "webui/src/index.ts",
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "govoplan-campaign"
|
||||
version = "0.1.26"
|
||||
version = "0.1.27"
|
||||
description = "GovOPlaN campaigns module with backend and WebUI integration."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
@@ -463,8 +463,8 @@ def _campaigns_router(context: ModuleContext):
|
||||
manifest = ModuleManifest(
|
||||
id="campaigns",
|
||||
name="Campaigns",
|
||||
version="0.1.26",
|
||||
workflow_definitions=campaign_workflow_definitions(module_version="0.1.26"),
|
||||
version="0.1.27",
|
||||
workflow_definitions=campaign_workflow_definitions(module_version="0.1.27"),
|
||||
required_capabilities=(
|
||||
CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
|
||||
CAPABILITY_AUTH_PERMISSION_EVALUATOR,
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@govoplan/campaign-webui",
|
||||
"version": "0.1.26",
|
||||
"version": "0.1.27",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -615,6 +615,8 @@ function zipArchiveColumns({ disabled, archives, invalidNameIndexes, onEditName,
|
||||
value={archive.method}
|
||||
disabled={disabled}
|
||||
aria-label="Archive password encryption"
|
||||
data-help-context-id="campaign.archive-encryption"
|
||||
data-help-module-id="campaigns"
|
||||
onChange={(event) => {
|
||||
const method = event.target.value === "zip_standard" ? "zip_standard" : "aes";
|
||||
patchArchive(index, method === "zip_standard" ? {
|
||||
@@ -640,6 +642,8 @@ function zipArchiveColumns({ disabled, archives, invalidNameIndexes, onEditName,
|
||||
value={archive.password_delivery_channel}
|
||||
disabled={disabled || !archive.password_enabled}
|
||||
aria-label="Separate password-delivery channel"
|
||||
data-help-context-id="campaign.archive-encryption"
|
||||
data-help-module-id="campaigns"
|
||||
onChange={(event) => patchArchive(index, { password_delivery_channel: event.target.value as AttachmentZipArchive["password_delivery_channel"] })}>
|
||||
{(["separate_mail", "sms", "letter", "phone", "in_person"] as const).map((channel) =>
|
||||
<option key={channel} value={channel} disabled={!allowedDeliveryChannels.includes(channel)}>{passwordDeliveryChannelLabel(channel)}</option>
|
||||
@@ -653,6 +657,8 @@ function zipArchiveColumns({ disabled, archives, invalidNameIndexes, onEditName,
|
||||
<div className="campaign-legacy-zipcrypto-acknowledgement">
|
||||
<ToggleSwitch
|
||||
label="I acknowledge that ZipCrypto encryption is weak"
|
||||
helpContextId="campaign.archive-encryption"
|
||||
helpModuleId="campaigns"
|
||||
checked={archive.legacy_zipcrypto_acknowledged}
|
||||
disabled={disabled || !legacyAllowed || !canUseLegacy}
|
||||
onChange={(checked) => patchArchive(index, { legacy_zipcrypto_acknowledged: checked })} />
|
||||
@@ -663,6 +669,8 @@ function zipArchiveColumns({ disabled, archives, invalidNameIndexes, onEditName,
|
||||
maxLength={1000}
|
||||
placeholder="Operational reason (at least 10 characters)"
|
||||
aria-label="Reason for weak legacy encryption"
|
||||
data-help-context-id="campaign.archive-encryption"
|
||||
data-help-module-id="campaigns"
|
||||
onChange={(event) => patchArchive(index, { legacy_zipcrypto_reason: event.target.value })} />
|
||||
</div> : <span>Not required for AES</span>,
|
||||
value: (archive) => archive.legacy_zipcrypto_reason
|
||||
|
||||
@@ -433,6 +433,8 @@ export default function CampaignOverviewPage({ settings, auth, campaignId }: {se
|
||||
destructiveActions={<>
|
||||
{canDelete && <Button
|
||||
variant="danger"
|
||||
helpContextId="campaign.overview"
|
||||
helpModuleId="campaigns"
|
||||
onClick={() => void prepareLifecycleAction("delete_campaign")}
|
||||
disabled={loading || savingIdentity || lockBusy || lifecycleBusy || identityDirty}
|
||||
disabledReason={identityDirty ? "Save or discard overview changes before deleting." : undefined}>
|
||||
|
||||
@@ -537,7 +537,7 @@ export default function CampaignReportPage({ settings, campaignId }: {settings:
|
||||
<Button onClick={() => void retryFailedSynchronously(failedRowsOnPage)} disabled={!version || Boolean(busyAction) || failedRowsOnPage.length === 0}>
|
||||
{busyAction === "retry-sync-page" ? "Sending failed jobs..." : `Retry failed on this page now (${failedRowsOnPage.length})`}
|
||||
</Button>
|
||||
<Button onClick={() => void runExplicitAction("unattempted")} disabled={!version || Boolean(busyAction)}>i18n:govoplan-campaign.send_unattempted_jobs.db7acc9f</Button>
|
||||
<Button helpContextId="campaign.report" helpModuleId="campaigns" onClick={() => void runExplicitAction("unattempted")} disabled={!version || Boolean(busyAction)}>i18n:govoplan-campaign.send_unattempted_jobs.db7acc9f</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</ContentGrid>
|
||||
@@ -591,7 +591,7 @@ export default function CampaignReportPage({ settings, campaignId }: {settings:
|
||||
footer={
|
||||
<div className="button-row">
|
||||
<Button onClick={() => setEmailOpen(false)} disabled={busyAction === "email"}>i18n:govoplan-campaign.cancel.77dfd213</Button>
|
||||
<Button variant="primary" onClick={() => void sendReportEmail()} disabled={!emailRecipients.trim() || busyAction === "email"}>i18n:govoplan-campaign.send_report.a5b32af9</Button>
|
||||
<Button variant="primary" helpContextId="campaign.report" helpModuleId="campaigns" onClick={() => void sendReportEmail()} disabled={!emailRecipients.trim() || busyAction === "email"}>i18n:govoplan-campaign.send_report.a5b32af9</Button>
|
||||
</div>
|
||||
}>
|
||||
|
||||
|
||||
@@ -359,7 +359,7 @@ export default function CampaignWorkPage({
|
||||
) : null}
|
||||
{canComplete && (assignment.status === "open" || assignment.status === "in_progress") ? (
|
||||
<span className="campaign-work-destructive-action">
|
||||
<Button variant="danger" onClick={() => setRejecting(assignment)} disabled={Boolean(busyId)}>
|
||||
<Button variant="danger" helpContextId="campaign.work.action.reject" helpModuleId="campaigns" onClick={() => setRejecting(assignment)} disabled={Boolean(busyId)}>
|
||||
Reject work
|
||||
</Button>
|
||||
</span>
|
||||
|
||||
@@ -379,6 +379,8 @@ export default function GlobalSettingsPage({ settings, auth, campaignId, view =
|
||||
<FormGrid columns={2} collapseAt="wide" className="">
|
||||
<ToggleSwitch
|
||||
label="Send individualized invitations"
|
||||
helpContextId="campaign.global-settings"
|
||||
helpModuleId="campaigns"
|
||||
checked={getBool(calendarInvitation, "enabled")}
|
||||
disabled={locked || !calendarCatalog.available || !mailModuleInstalled}
|
||||
onChange={(checked) => patch(["delivery", "calendar_invitation", "enabled"], checked)}
|
||||
|
||||
@@ -383,7 +383,7 @@ export default function MailSettingsPage({ settings, campaignId, view = "setting
|
||||
title="i18n:govoplan-campaign.reusable_mail_profile.f9c9aab1"
|
||||
actions={<div className="button-row compact-actions">
|
||||
<Button onClick={() => void refreshMailProfiles()} disabled={profilesLoading}>{profilesLoading ? "i18n:govoplan-campaign.loading.33ce4174" : "i18n:govoplan-campaign.reload_profiles.0fe100d1"}</Button>
|
||||
<Button variant="primary" onClick={openCredentialDialog} disabled={locked || profilesLoading || !selectedProfile || !selectedSmtpServer && !selectedImapServer}>Add campaign credential</Button>
|
||||
<Button variant="primary" helpContextId="campaign.server-settings" helpModuleId="campaigns" onClick={openCredentialDialog} disabled={locked || profilesLoading || !selectedProfile || !selectedSmtpServer && !selectedImapServer}>Add campaign credential</Button>
|
||||
</div>}>
|
||||
<p className="muted small-note">The campaign stores stable references to the envelope, selected servers, and credentials.</p>
|
||||
<FormGrid columns={2} collapseAt="wide" className="">
|
||||
@@ -399,8 +399,8 @@ export default function MailSettingsPage({ settings, campaignId, view = "setting
|
||||
{smtpServers.map((item) => <option key={item.id} value={item.id}>{item.name} ({serverEndpointLabel(item.config)})</option>)}
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField label="SMTP credential">
|
||||
<select value={selectedSmtpCredential?.id ?? ""} disabled={locked || profilesLoading || !selectedSmtpServer} onChange={(event) => selectCredential("smtp", event.target.value)}>
|
||||
<FormField label="SMTP credential" helpContextId="campaign.server-settings" helpModuleId="campaigns">
|
||||
<select data-help-context-id="campaign.server-settings" data-help-module-id="campaigns" value={selectedSmtpCredential?.id ?? ""} disabled={locked || profilesLoading || !selectedSmtpServer} onChange={(event) => selectCredential("smtp", event.target.value)}>
|
||||
<option value="">No credential</option>
|
||||
{selectedSmtpServer?.credentials.filter((item) => item.is_active).map((item) =>
|
||||
<option key={item.id} value={item.id}>{credentialLabel(item)}</option>
|
||||
@@ -413,8 +413,8 @@ export default function MailSettingsPage({ settings, campaignId, view = "setting
|
||||
{imapServers.map((item) => <option key={item.id} value={item.id}>{item.name} ({serverEndpointLabel(item.config)})</option>)}
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField label="IMAP credential">
|
||||
<select value={selectedImapCredential?.id ?? ""} disabled={locked || profilesLoading || !selectedImapServer} onChange={(event) => selectCredential("imap", event.target.value)}>
|
||||
<FormField label="IMAP credential" helpContextId="campaign.server-settings" helpModuleId="campaigns">
|
||||
<select data-help-context-id="campaign.server-settings" data-help-module-id="campaigns" value={selectedImapCredential?.id ?? ""} disabled={locked || profilesLoading || !selectedImapServer} onChange={(event) => selectCredential("imap", event.target.value)}>
|
||||
<option value="">No credential</option>
|
||||
{selectedImapServer?.credentials.filter((item) => item.is_active).map((item) =>
|
||||
<option key={item.id} value={item.id}>{credentialLabel(item)}</option>
|
||||
@@ -476,8 +476,8 @@ export default function MailSettingsPage({ settings, campaignId, view = "setting
|
||||
<FormField label="Username">
|
||||
<input value={credentialDraft.username} disabled={credentialSaving} onChange={(event) => setCredentialDraft({ ...credentialDraft, username: event.target.value })} />
|
||||
</FormField>
|
||||
<FormField label="Password">
|
||||
<PasswordField value={credentialDraft.password} onValueChange={(password) => setCredentialDraft({ ...credentialDraft, password })} disabled={credentialSaving} generator autoComplete="new-password" />
|
||||
<FormField label="Password" helpContextId="campaign.server-settings" helpModuleId="campaigns">
|
||||
<PasswordField helpContextId="campaign.server-settings" helpModuleId="campaigns" value={credentialDraft.password} onValueChange={(password) => setCredentialDraft({ ...credentialDraft, password })} disabled={credentialSaving} generator autoComplete="new-password" />
|
||||
</FormField>
|
||||
</FormGrid>
|
||||
<FormGrid columns={2} collapseAt="standard" className="">
|
||||
|
||||
@@ -1638,7 +1638,7 @@ export default function ReviewSendPage({
|
||||
<p className="review-flow-inline-note is-stale">i18n:govoplan-campaign.mock_delivery_uses_the_mail_module_development_m.9d645a5f</p>
|
||||
}
|
||||
<div className="toggle-row mock-send-options">
|
||||
<ToggleSwitch label="i18n:govoplan-campaign.require_mock_before_real_send.a1ccc330" checked={mockVerificationRequired} disabled={!mockWorkflowAvailable || Boolean(busy)} onChange={setMockVerificationRequired} />
|
||||
<ToggleSwitch label="i18n:govoplan-campaign.require_mock_before_real_send.a1ccc330" helpContextId="campaign.review-send" helpModuleId="campaigns" checked={mockVerificationRequired} disabled={!mockWorkflowAvailable || Boolean(busy)} onChange={setMockVerificationRequired} />
|
||||
<ToggleSwitch label="i18n:govoplan-campaign.show_captured_mock_mailbox.019e1e79" checked={mockMailboxPreviewEnabled && mockWorkflowAvailable} disabled={!mockWorkflowAvailable || Boolean(busy)} onChange={setMockMailboxPreviewEnabled} />
|
||||
<ToggleSwitch label="i18n:govoplan-campaign.clear_mock_mailbox_first.7627dcad" checked={mockClearFirst} disabled={!mockWorkflowAvailable || Boolean(busy)} onChange={setMockClearFirst} />
|
||||
<ToggleSwitch label="i18n:govoplan-campaign.append_mock_sent_copy.49c2cf08" checked={mockAppendSent} disabled={!mockWorkflowAvailable || Boolean(busy)} onChange={setMockAppendSent} />
|
||||
|
||||
@@ -429,7 +429,7 @@ export default function CampaignAccessCard({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card title="i18n:govoplan-campaign.ownership_and_sharing.867283c0" actions={<div className="button-row compact-actions">{canProposeOwnership ? <Button onClick={openOwnerDialog}><ArrowRightLeft size={16} aria-hidden="true" /> Transfer ownership</Button> : null}{canRequestOwnership ? <Button onClick={openRequestDialog}><ArrowRightLeft size={16} aria-hidden="true" /> Request ownership</Button> : null}<Button onClick={() => setShareOpen(true)} disabled={available !== true}>i18n:govoplan-campaign.share.09ca55ca</Button><Button onClick={() => void openAccessExplanation()} disabled={!canExplainResourceAccess}><KeyRound size={16} aria-hidden="true" /> i18n:govoplan-campaign.explain_access.4d5fac37</Button></div>}>
|
||||
<Card title="i18n:govoplan-campaign.ownership_and_sharing.867283c0" actions={<div className="button-row compact-actions">{canProposeOwnership ? <Button helpContextId="campaign.overview" helpModuleId="campaigns" onClick={openOwnerDialog}><ArrowRightLeft size={16} aria-hidden="true" /> Transfer ownership</Button> : null}{canRequestOwnership ? <Button onClick={openRequestDialog}><ArrowRightLeft size={16} aria-hidden="true" /> Request ownership</Button> : null}<Button onClick={() => setShareOpen(true)} disabled={available !== true}>i18n:govoplan-campaign.share.09ca55ca</Button><Button onClick={() => void openAccessExplanation()} disabled={!canExplainResourceAccess}><KeyRound size={16} aria-hidden="true" /> i18n:govoplan-campaign.explain_access.4d5fac37</Button></div>}>
|
||||
<p><strong>i18n:govoplan-campaign.owner.719379ae</strong> {ownerLabel}</p>
|
||||
<p className="muted small-note">Ownership changes require acceptance by the new owner. They never transfer private encryption keys. A completed transfer clears owner-scoped mail profile selection and requires revalidation.</p>
|
||||
{ownershipTransfers.length > 0 ? (
|
||||
@@ -489,7 +489,7 @@ export default function CampaignAccessCard({
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Dialog open={ownerOpen} className="campaign-access-dialog" title="Transfer campaign ownership" onClose={() => !busy && closeOwnerDialog()} footer={<><Button onClick={closeOwnerDialog} disabled={busy}>i18n:govoplan-campaign.cancel.77dfd213</Button><Button variant="primary" onClick={() => void saveOwner()} disabled={busy || !ownerId || ownerType === currentOwnerType && ownerId === currentOwnerId}>Propose transfer</Button></>}>
|
||||
<Dialog open={ownerOpen} className="campaign-access-dialog" title="Transfer campaign ownership" helpContextId="campaign.overview" helpModuleId="campaigns" onClose={() => !busy && closeOwnerDialog()} footer={<><Button onClick={closeOwnerDialog} disabled={busy}>i18n:govoplan-campaign.cancel.77dfd213</Button><Button variant="primary" helpContextId="campaign.overview" helpModuleId="campaigns" onClick={() => void saveOwner()} disabled={busy || !ownerId || ownerType === currentOwnerType && ownerId === currentOwnerId}>Propose transfer</Button></>}>
|
||||
<FormField label="i18n:govoplan-campaign.owner_type.6b86eacc"><select value={ownerType} onChange={(event) => {const next = event.target.value as TargetType;setOwnerType(next);setOwnerId("");}}><option value="user">i18n:govoplan-campaign.user.9f8a2389</option><option value="group">i18n:govoplan-campaign.group.171a0606</option></select></FormField>
|
||||
<FormField label="i18n:govoplan-campaign.owner.89ff3122">
|
||||
<ReferenceSelect
|
||||
|
||||
Reference in New Issue
Block a user