Adopt shared WebUI structural primitives
This commit is contained in:
@@ -38,7 +38,7 @@ import {
|
|||||||
type CampaignSummary } from
|
type CampaignSummary } from
|
||||||
"../../api/campaigns";
|
"../../api/campaigns";
|
||||||
import { getMockMailboxMessage, type MockMailboxMessage } from "../../api/mail";
|
import { getMockMailboxMessage, type MockMailboxMessage } from "../../api/mail";
|
||||||
import { FormGrid, Button, Dialog, FormField, SegmentedControl, hasScope, useDeltaWatermarks, useGuardedNavigate, usePlatformUiCapability, type AuthInfo, type MailDevMailboxUiCapability } from "@govoplan/core-webui";
|
import { FormGrid, Button, Dialog, FormField, MetricCard, MetricGrid, SegmentedControl, hasScope, useDeltaWatermarks, useGuardedNavigate, usePlatformUiCapability, type AuthInfo, type MailDevMailboxUiCapability } from "@govoplan/core-webui";
|
||||||
import { DataGrid, type DataGridQueryState } from "@govoplan/core-webui";
|
import { DataGrid, type DataGridQueryState } from "@govoplan/core-webui";
|
||||||
import { DismissibleAlert } from "@govoplan/core-webui";
|
import { DismissibleAlert } from "@govoplan/core-webui";
|
||||||
import { ConfirmDialog } from "@govoplan/core-webui";
|
import { ConfirmDialog } from "@govoplan/core-webui";
|
||||||
@@ -80,7 +80,6 @@ import {
|
|||||||
} from "./review/ReviewWorkflowGuidance";
|
} from "./review/ReviewWorkflowGuidance";
|
||||||
import { calculateBuildReviewProgress } from "./review/reviewProgress";
|
import { calculateBuildReviewProgress } from "./review/reviewProgress";
|
||||||
import {
|
import {
|
||||||
WorkflowFact,
|
|
||||||
WorkflowNavigation,
|
WorkflowNavigation,
|
||||||
WorkflowStage,
|
WorkflowStage,
|
||||||
stageConnectorState,
|
stageConnectorState,
|
||||||
@@ -1398,12 +1397,12 @@ export default function ReviewSendPage({
|
|||||||
|
|
||||||
<div className="review-flow-timeline" aria-label="i18n:govoplan-campaign.campaign_review_and_sending_workflow.e784ea7b">
|
<div className="review-flow-timeline" aria-label="i18n:govoplan-campaign.campaign_review_and_sending_workflow.e784ea7b">
|
||||||
<WorkflowStage stage={stages[0]} nextState={stages[1].state} nextConnectorState={stageConnectorState(stages[1])}>
|
<WorkflowStage stage={stages[0]} nextState={stages[1].state} nextConnectorState={stageConnectorState(stages[1])}>
|
||||||
<div className="review-flow-fact-grid">
|
<MetricGrid columns={4} density="compact" spacing="block" minimum="compact">
|
||||||
<WorkflowFact label="i18n:govoplan-campaign.status.bae7d5be" value={validationPresent ? validationOk ? "i18n:govoplan-campaign.passed.271d60f4" : "i18n:govoplan-campaign.needs_attention.a126722e" : "i18n:govoplan-campaign.not_run.9e019cd5"} />
|
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.status.bae7d5be" value={validationPresent ? validationOk ? "i18n:govoplan-campaign.passed.271d60f4" : "i18n:govoplan-campaign.needs_attention.a126722e" : "i18n:govoplan-campaign.not_run.9e019cd5"} />
|
||||||
<WorkflowFact label="i18n:govoplan-campaign.blocking.d785c0d4" value={validationPresent ? validationErrors : "—"} />
|
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.blocking.d785c0d4" value={validationPresent ? validationErrors : "—"} />
|
||||||
<WorkflowFact label="i18n:govoplan-campaign.warnings.1430f976" value={validationPresent ? validationWarnings : "—"} />
|
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.warnings.1430f976" value={validationPresent ? validationWarnings : "—"} />
|
||||||
<WorkflowFact label="i18n:govoplan-campaign.jobs_needing_attention.95613b02" value={cards?.needs_attention ?? "—"} />
|
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.jobs_needing_attention.95613b02" value={cards?.needs_attention ?? "—"} />
|
||||||
</div>
|
</MetricGrid>
|
||||||
<ValidationWorkflowGuidance
|
<ValidationWorkflowGuidance
|
||||||
errors={validationErrors}
|
errors={validationErrors}
|
||||||
warnings={validationWarnings}
|
warnings={validationWarnings}
|
||||||
@@ -1456,15 +1455,15 @@ export default function ReviewSendPage({
|
|||||||
</WorkflowStage>
|
</WorkflowStage>
|
||||||
|
|
||||||
<WorkflowStage stage={stages[1]} nextState={stages[2].state} nextConnectorState={stageConnectorState(stages[2])}>
|
<WorkflowStage stage={stages[1]} nextState={stages[2].state} nextConnectorState={stageConnectorState(stages[2])}>
|
||||||
<div className="review-flow-fact-grid">
|
<MetricGrid columns={4} density="compact" spacing="block" minimum="compact">
|
||||||
<WorkflowFact label="i18n:govoplan-campaign.built.a6ad3f82" value={hasBuild ? builtCount : "—"} />
|
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.built.a6ad3f82" value={hasBuild ? builtCount : "—"} />
|
||||||
<WorkflowFact label="i18n:govoplan-campaign.blocked.99613c74" value={hasBuild ? buildBlocked : "—"} />
|
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.blocked.99613c74" value={hasBuild ? buildBlocked : "—"} />
|
||||||
<WorkflowFact label="i18n:govoplan-campaign.need_review.201a4493" value={hasBuild ? buildNeedsReview : "—"} />
|
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.need_review.201a4493" value={hasBuild ? buildNeedsReview : "—"} />
|
||||||
<WorkflowFact label="i18n:govoplan-campaign.reviewed.31ef8593" value={hasBuild ? buildReviewProgress.reviewed : "—"} />
|
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.reviewed.31ef8593" value={hasBuild ? buildReviewProgress.reviewed : "—"} />
|
||||||
<WorkflowFact label="i18n:govoplan-campaign.remaining.cc632b5e" value={hasBuild ? buildReviewProgress.remaining : "—"} />
|
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.remaining.cc632b5e" value={hasBuild ? buildReviewProgress.remaining : "—"} />
|
||||||
<WorkflowFact label="i18n:govoplan-campaign.review_candidates.438b8b57" value={reviewJobs.total || "—"} />
|
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.review_candidates.438b8b57" value={reviewJobs.total || "—"} />
|
||||||
<WorkflowFact label="i18n:govoplan-campaign.attachment_issues.69748336" value={missingAttachments + ambiguousAttachments} />
|
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.attachment_issues.69748336" value={missingAttachments + ambiguousAttachments} />
|
||||||
</div>
|
</MetricGrid>
|
||||||
{hasBuild && <BuiltMessageReviewProgress progress={buildReviewProgress} />}
|
{hasBuild && <BuiltMessageReviewProgress progress={buildReviewProgress} />}
|
||||||
{hasBuild && <BuiltMessageWorkflowGuidance progress={buildReviewProgress} buildWarnings={buildWarnings} />}
|
{hasBuild && <BuiltMessageWorkflowGuidance progress={buildReviewProgress} buildWarnings={buildWarnings} />}
|
||||||
<p className="muted">i18n:govoplan-campaign.building_freezes_the_current_recipients_rendered.273a8170</p>
|
<p className="muted">i18n:govoplan-campaign.building_freezes_the_current_recipients_rendered.273a8170</p>
|
||||||
@@ -1559,12 +1558,12 @@ export default function ReviewSendPage({
|
|||||||
</WorkflowStage>
|
</WorkflowStage>
|
||||||
|
|
||||||
<WorkflowStage stage={stages[2]} nextState={stages[3].state} nextConnectorState={stageConnectorState(stages[3])}>
|
<WorkflowStage stage={stages[2]} nextState={stages[3].state} nextConnectorState={stageConnectorState(stages[3])}>
|
||||||
<div className="review-flow-fact-grid">
|
<MetricGrid columns={4} density="compact" spacing="block" minimum="compact">
|
||||||
<WorkflowFact label="i18n:govoplan-campaign.captured_smtp.78f2e2ca" value={mockResult ? mockSent : "—"} />
|
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.captured_smtp.78f2e2ca" value={mockResult ? mockSent : "—"} />
|
||||||
<WorkflowFact label="i18n:govoplan-campaign.mock_failures.9475ce2a" value={mockResult ? mockFailed : "—"} />
|
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.mock_failures.9475ce2a" value={mockResult ? mockFailed : "—"} />
|
||||||
<WorkflowFact label="i18n:govoplan-campaign.skipped.5a000ad7" value={mockResult ? mockSkipped : "—"} />
|
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.skipped.5a000ad7" value={mockResult ? mockSkipped : "—"} />
|
||||||
<WorkflowFact label="i18n:govoplan-campaign.captured_messages.a833d293" value={!mockWorkflowAvailable ? "i18n:govoplan-campaign.unavailable.2c9c1f79" : mockResult ? mockMailboxMessages.length : "—"} />
|
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.captured_messages.a833d293" value={!mockWorkflowAvailable ? "i18n:govoplan-campaign.unavailable.2c9c1f79" : mockResult ? mockMailboxMessages.length : "—"} />
|
||||||
</div>
|
</MetricGrid>
|
||||||
<div className="button-row compact-actions review-flow-stage-actions">
|
<div className="button-row compact-actions review-flow-stage-actions">
|
||||||
<Button variant="primary" onClick={() => void runMockSend()} disabled={!version || Boolean(busy) || !mockWorkflowAvailable || readOnlyVersion || !inspectionSatisfied || deliveryQueued || deliveryStarted}>
|
<Button variant="primary" onClick={() => void runMockSend()} disabled={!version || Boolean(busy) || !mockWorkflowAvailable || readOnlyVersion || !inspectionSatisfied || deliveryQueued || deliveryStarted}>
|
||||||
{busy === "mock" ? "i18n:govoplan-campaign.running_mock_delivery.06c6a3bf" : mockResult ? "i18n:govoplan-campaign.run_mock_delivery_again.77fa416e" : "i18n:govoplan-campaign.run_mock_delivery.b74ff33d"}
|
{busy === "mock" ? "i18n:govoplan-campaign.running_mock_delivery.06c6a3bf" : mockResult ? "i18n:govoplan-campaign.run_mock_delivery_again.77fa416e" : "i18n:govoplan-campaign.run_mock_delivery.b74ff33d"}
|
||||||
@@ -1616,19 +1615,19 @@ export default function ReviewSendPage({
|
|||||||
</WorkflowStage>
|
</WorkflowStage>
|
||||||
|
|
||||||
<WorkflowStage stage={stages[3]} nextState={stages[4].state} nextConnectorState={stageConnectorState(stages[4])}>
|
<WorkflowStage stage={stages[3]} nextState={stages[4].state} nextConnectorState={stageConnectorState(stages[4])}>
|
||||||
<div className="review-flow-execution-summary">
|
<MetricGrid columns={4} density="compact" spacing="block" minimum="compact">
|
||||||
<div><span>i18n:govoplan-campaign.recipients.78cbf8eb</span><strong>{jobsTotal || "—"}</strong></div>
|
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.recipients.78cbf8eb" value={jobsTotal || "—"} />
|
||||||
<div><span>i18n:govoplan-campaign.messages_to_send.f7763bf9</span><strong>{builtCount || jobsTotal || "—"}</strong></div>
|
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.messages_to_send.f7763bf9" value={builtCount || jobsTotal || "—"} />
|
||||||
<div><span>i18n:govoplan-campaign.matched_attachments.ead1eeb1</span><strong>{matchedAttachments || "—"}</strong></div>
|
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.matched_attachments.ead1eeb1" value={matchedAttachments || "—"} />
|
||||||
<div><span>i18n:govoplan-campaign.missing_ambiguous.fffdd3f5</span><strong>{missingAttachments} / {ambiguousAttachments}</strong></div>
|
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.missing_ambiguous.fffdd3f5" value={`${missingAttachments} / ${ambiguousAttachments}`} />
|
||||||
<div><span>i18n:govoplan-campaign.rate_limit.d08e55f5</span><strong>{messagesPerMinute > 0 ? i18nMessage("i18n:govoplan-campaign.value_min.c9d89eae", { value0: messagesPerMinute }) : "i18n:govoplan-campaign.not_set.93039e60"}</strong></div>
|
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.rate_limit.d08e55f5" value={messagesPerMinute > 0 ? i18nMessage("i18n:govoplan-campaign.value_min.c9d89eae", { value0: messagesPerMinute }) : "i18n:govoplan-campaign.not_set.93039e60"} />
|
||||||
<div><span>i18n:govoplan-campaign.minimum_duration.91a71a6d</span><strong>{estimatedMinutes ? i18nMessage("i18n:govoplan-campaign.about_value_min.7c2e77fc", { value0: estimatedMinutes }) : "—"}</strong></div>
|
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.minimum_duration.91a71a6d" value={estimatedMinutes ? i18nMessage("i18n:govoplan-campaign.about_value_min.7c2e77fc", { value0: estimatedMinutes }) : "—"} />
|
||||||
<div><span>i18n:govoplan-campaign.imap_append.8c0d9e96</span><strong>{Boolean(imapAppend.enabled) ? "i18n:govoplan-campaign.enabled.df174a3f" : "i18n:govoplan-campaign.disabled.f4f4473d"}</strong></div>
|
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.imap_append.8c0d9e96" value={Boolean(imapAppend.enabled) ? "i18n:govoplan-campaign.enabled.df174a3f" : "i18n:govoplan-campaign.disabled.f4f4473d"} />
|
||||||
<div><span>i18n:govoplan-campaign.synchronous_limit.f88c0bcd</span><strong>{deliveryOptionsLoading ? "…" : synchronousSendLimit}</strong></div>
|
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.synchronous_limit.f88c0bcd" value={deliveryOptionsLoading ? "…" : synchronousSendLimit} />
|
||||||
<div><span>i18n:govoplan-campaign.limit_source.bd933adb</span><strong>{deliveryPolicySourceLabel(String(synchronousSendPolicy.source ?? ""))}</strong></div>
|
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.limit_source.bd933adb" value={deliveryPolicySourceLabel(String(synchronousSendPolicy.source ?? ""))} />
|
||||||
<div><span>i18n:govoplan-campaign.worker_queue.c911e32c</span><strong>{workerQueueAvailable ? "i18n:govoplan-campaign.available.7c62a142" : "i18n:govoplan-campaign.not_configured.811931bb"}</strong></div>
|
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.worker_queue.c911e32c" value={workerQueueAvailable ? "i18n:govoplan-campaign.available.7c62a142" : "i18n:govoplan-campaign.not_configured.811931bb"} />
|
||||||
<div><span>i18n:govoplan-campaign.version.2da600bf</span><strong>{version ? `v${version.version_number}` : "—"}</strong></div>
|
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.version.2da600bf" value={version ? `v${version.version_number}` : "—"} />
|
||||||
</div>
|
</MetricGrid>
|
||||||
<DeliverabilityPreflight items={deliverabilityPreflightItems} />
|
<DeliverabilityPreflight items={deliverabilityPreflightItems} />
|
||||||
<div className="review-send-controls">
|
<div className="review-send-controls">
|
||||||
<span>Approval gate</span>
|
<span>Approval gate</span>
|
||||||
@@ -1642,12 +1641,12 @@ export default function ReviewSendPage({
|
|||||||
{approvalGateConfigured &&
|
{approvalGateConfigured &&
|
||||||
<Button disabled={Boolean(busy)} onClick={() => navigate("/approvals")}>Open approval</Button>}
|
<Button disabled={Boolean(busy)} onClick={() => navigate("/approvals")}>Open approval</Button>}
|
||||||
</div>
|
</div>
|
||||||
<div className="review-flow-fact-grid">
|
<MetricGrid columns={4} density="compact" spacing="block" minimum="compact">
|
||||||
<WorkflowFact label="i18n:govoplan-campaign.queued.6a599877" value={queuedSendCount} />
|
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.queued.6a599877" value={queuedSendCount} />
|
||||||
<WorkflowFact label="i18n:govoplan-campaign.claimed_sending.6951622a" value={activeSendCount} />
|
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.claimed_sending.6951622a" value={activeSendCount} />
|
||||||
<WorkflowFact label="i18n:govoplan-campaign.not_queued.b7f41e1e" value={notQueuedCount} />
|
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.not_queued.b7f41e1e" value={notQueuedCount} />
|
||||||
<WorkflowFact label="i18n:govoplan-campaign.send_attempts.2cdb19e8" value={sendAttemptCount} />
|
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.send_attempts.2cdb19e8" value={sendAttemptCount} />
|
||||||
</div>
|
</MetricGrid>
|
||||||
{queueStatusNote &&
|
{queueStatusNote &&
|
||||||
<p className={`review-flow-inline-note ${queueStatusTone}`.trim()}>{queueStatusNote}</p>
|
<p className={`review-flow-inline-note ${queueStatusTone}`.trim()}>{queueStatusNote}</p>
|
||||||
}
|
}
|
||||||
@@ -1718,18 +1717,18 @@ export default function ReviewSendPage({
|
|||||||
</WorkflowStage>
|
</WorkflowStage>
|
||||||
|
|
||||||
<WorkflowStage stage={stages[4]}>
|
<WorkflowStage stage={stages[4]}>
|
||||||
<div className="review-flow-fact-grid">
|
<MetricGrid columns={4} density="compact" spacing="block" minimum="compact">
|
||||||
<WorkflowFact label="i18n:govoplan-campaign.smtp_accepted.e3aa7603" value={sentCount} />
|
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.smtp_accepted.e3aa7603" value={sentCount} />
|
||||||
<WorkflowFact label="i18n:govoplan-campaign.smtp_failed.0ce5516d" value={failedCount} />
|
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.smtp_failed.0ce5516d" value={failedCount} />
|
||||||
<WorkflowFact label="i18n:govoplan-campaign.queued_active.a2784a4a" value={queuedOrActiveCount} />
|
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.queued_active.a2784a4a" value={queuedOrActiveCount} />
|
||||||
<WorkflowFact label="i18n:govoplan-campaign.paused.c7dfb6f1" value={pausedQueueCount} />
|
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.paused.c7dfb6f1" value={pausedQueueCount} />
|
||||||
<WorkflowFact label="i18n:govoplan-campaign.delivery_mode.109ed9d1" value={deliveryModeLabel(persistedDeliveryMode)} />
|
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.delivery_mode.109ed9d1" value={deliveryModeLabel(persistedDeliveryMode)} />
|
||||||
<WorkflowFact label="i18n:govoplan-campaign.outcome_unknown.6e929fca" value={outcomeUnknownCount} />
|
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.outcome_unknown.6e929fca" value={outcomeUnknownCount} />
|
||||||
<WorkflowFact label="i18n:govoplan-campaign.imap_appended.56017ea3" value={imapAppended} />
|
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.imap_appended.56017ea3" value={imapAppended} />
|
||||||
<WorkflowFact label="i18n:govoplan-campaign.imap_pending.ed50375e" value={imapPendingForDisplay} />
|
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.imap_pending.ed50375e" value={imapPendingForDisplay} />
|
||||||
<WorkflowFact label="i18n:govoplan-campaign.imap_failed.50dbca55" value={imapFailedForDisplay} />
|
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.imap_failed.50dbca55" value={imapFailedForDisplay} />
|
||||||
<WorkflowFact label="i18n:govoplan-campaign.imap_skipped.5a97b542" value={imapSkipped} />
|
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.imap_skipped.5a97b542" value={imapSkipped} />
|
||||||
</div>
|
</MetricGrid>
|
||||||
<div className="review-flow-result-line">
|
<div className="review-flow-result-line">
|
||||||
<StatusBadge status={deliveryDisplayStatus} />
|
<StatusBadge status={deliveryDisplayStatus} />
|
||||||
<span>{deliveryStarted ? "i18n:govoplan-campaign.delivery_activity_is_available_in_the_report_and.cb163d1d" : "i18n:govoplan-campaign.no_real_delivery_has_started_for_this_campaign_v.3b9235ff"}</span>
|
<span>{deliveryStarted ? "i18n:govoplan-campaign.delivery_activity_is_available_in_the_report_and.cb163d1d" : "i18n:govoplan-campaign.no_real_delivery_has_started_for_this_campaign_v.3b9235ff"}</span>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useMemo, useRef, useState, type ClipboardEvent } from "react";
|
import { useEffect, useMemo, useRef, useState, type ClipboardEvent } from "react";
|
||||||
import { ArrowDown, ArrowUp, Copy, Pencil, Plus, Trash2 } from "lucide-react";
|
import { ArrowDown, ArrowUp, Copy, Pencil, Plus, Trash2 } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
|
ActionToolbar,
|
||||||
Button,
|
Button,
|
||||||
Dialog,
|
Dialog,
|
||||||
DismissibleAlert,
|
DismissibleAlert,
|
||||||
@@ -309,7 +310,7 @@ function RecipientAddressCategoryEditor({ column, addresses, merge, locked, onAd
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="recipient-address-category" tabIndex={locked ? undefined : 0} onPaste={handlePaste}>
|
<section className="recipient-address-category" tabIndex={locked ? undefined : 0} onPaste={handlePaste}>
|
||||||
<div className="recipient-address-category-header">
|
<ActionToolbar surface="section-header" className="recipient-address-category-header">
|
||||||
<h3>{column.label}</h3>
|
<h3>{column.label}</h3>
|
||||||
<div className="recipient-address-category-controls">
|
<div className="recipient-address-category-controls">
|
||||||
{onMergeChange ?
|
{onMergeChange ?
|
||||||
@@ -322,7 +323,7 @@ function RecipientAddressCategoryEditor({ column, addresses, merge, locked, onAd
|
|||||||
onChange={onMergeChange} /> :
|
onChange={onMergeChange} /> :
|
||||||
null}
|
null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ActionToolbar>
|
||||||
|
|
||||||
<div className="recipient-address-lines">
|
<div className="recipient-address-lines">
|
||||||
{column.allowMultiple && draftAddresses.length === 0 &&
|
{column.allowMultiple && draftAddresses.length === 0 &&
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Link2, X } from "lucide-react";
|
import { Link2, X } from "lucide-react";
|
||||||
import { Button, Dialog, DismissibleAlert, i18nMessage } from "@govoplan/core-webui";
|
import { Button, Dialog, DismissibleAlert, MetricCard, MetricGrid, i18nMessage } from "@govoplan/core-webui";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
CampaignAttachmentPreviewFile,
|
CampaignAttachmentPreviewFile,
|
||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
attachmentPreviewLinkableFiles,
|
attachmentPreviewLinkableFiles,
|
||||||
attachmentPreviewMatchedFiles
|
attachmentPreviewMatchedFiles
|
||||||
} from "../utils/attachmentPreview";
|
} from "../utils/attachmentPreview";
|
||||||
import { WorkflowFact } from "./WorkflowNavigation";
|
|
||||||
|
|
||||||
export default function AttachmentLinkingPreview({
|
export default function AttachmentLinkingPreview({
|
||||||
preview,
|
preview,
|
||||||
@@ -70,8 +69,8 @@ export default function AttachmentLinkingPreview({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="review-flow-fact-grid">
|
<MetricGrid columns={4} density="compact" spacing="block" minimum="compact">
|
||||||
<WorkflowFact
|
<MetricCard density="compact" surface="subtle"
|
||||||
label="i18n:govoplan-campaign.matched.1bf3ec5b"
|
label="i18n:govoplan-campaign.matched.1bf3ec5b"
|
||||||
value={
|
value={
|
||||||
!loading && matchedFiles.length > 0 ? (
|
!loading && matchedFiles.length > 0 ? (
|
||||||
@@ -95,19 +94,19 @@ export default function AttachmentLinkingPreview({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<WorkflowFact
|
<MetricCard density="compact" surface="subtle"
|
||||||
label="i18n:govoplan-campaign.linked.a089f600"
|
label="i18n:govoplan-campaign.linked.a089f600"
|
||||||
value={loading ? "..." : linkedCount || "—"}
|
value={loading ? "..." : linkedCount || "—"}
|
||||||
/>
|
/>
|
||||||
<WorkflowFact
|
<MetricCard density="compact" surface="subtle"
|
||||||
label="i18n:govoplan-campaign.need_link.fa4ab530"
|
label="i18n:govoplan-campaign.need_link.fa4ab530"
|
||||||
value={loading ? "..." : unlinkedCount || "—"}
|
value={loading ? "..." : unlinkedCount || "—"}
|
||||||
/>
|
/>
|
||||||
<WorkflowFact
|
<MetricCard density="compact" surface="subtle"
|
||||||
label="i18n:govoplan-campaign.shared_source.7d4a1bf2"
|
label="i18n:govoplan-campaign.shared_source.7d4a1bf2"
|
||||||
value={loading ? "..." : preview?.shared_file_count ?? "—"}
|
value={loading ? "..." : preview?.shared_file_count ?? "—"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</MetricGrid>
|
||||||
{error && (
|
{error && (
|
||||||
<DismissibleAlert tone="danger" compact dismissible={false}>
|
<DismissibleAlert tone="danger" compact dismissible={false}>
|
||||||
{error}
|
{error}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, type CSSProperties, type ReactNode } from "react";
|
import { type CSSProperties, type ReactNode } from "react";
|
||||||
import { ChevronDown, LockKeyhole, type LucideIcon } from "lucide-react";
|
import { LockKeyhole, type LucideIcon } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
|
Card,
|
||||||
InlineHelp,
|
InlineHelp,
|
||||||
StageRail,
|
StageRail,
|
||||||
i18nMessage,
|
i18nMessage,
|
||||||
@@ -107,7 +108,6 @@ export function WorkflowStage({
|
|||||||
}) {
|
}) {
|
||||||
const Icon = stage.icon;
|
const Icon = stage.icon;
|
||||||
const locked = stage.state === "locked";
|
const locked = stage.state === "locked";
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
|
||||||
const title = i18nMessage(stage.title);
|
const title = i18nMessage(stage.title);
|
||||||
const description = i18nMessage(stage.description);
|
const description = i18nMessage(stage.description);
|
||||||
const stateText = i18nMessage(stage.stateLabel);
|
const stateText = i18nMessage(stage.stateLabel);
|
||||||
@@ -127,11 +127,16 @@ export function WorkflowStage({
|
|||||||
</div>
|
</div>
|
||||||
{nextState && <div className="review-flow-stage-line" />}
|
{nextState && <div className="review-flow-stage-line" />}
|
||||||
</div>
|
</div>
|
||||||
<article
|
<Card
|
||||||
className={`card card-collapsible review-flow-stage-card${locked ? " is-locked" : ""}${collapsed ? " is-collapsed" : ""}`}
|
as="article"
|
||||||
|
collapsible
|
||||||
|
persistCollapse={false}
|
||||||
|
className={`review-flow-stage-card${locked ? " is-locked" : ""}`}
|
||||||
|
headerClassName="review-flow-stage-header"
|
||||||
|
bodyClassName="review-flow-stage-content"
|
||||||
|
actionsClassName="review-flow-stage-header-actions"
|
||||||
aria-disabled={locked || undefined}
|
aria-disabled={locked || undefined}
|
||||||
>
|
title={
|
||||||
<header className="card-header review-flow-stage-header">
|
|
||||||
<h2>
|
<h2>
|
||||||
<span>{title}</span>
|
<span>{title}</span>
|
||||||
{locked && (
|
{locked && (
|
||||||
@@ -153,54 +158,22 @@ export function WorkflowStage({
|
|||||||
)}
|
)}
|
||||||
<InlineHelp>{description}</InlineHelp>
|
<InlineHelp>{description}</InlineHelp>
|
||||||
</h2>
|
</h2>
|
||||||
<div className="review-flow-stage-header-actions">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="card-collapse-toggle"
|
|
||||||
aria-expanded={!collapsed}
|
|
||||||
aria-label={
|
|
||||||
collapsed
|
|
||||||
? i18nMessage("i18n:govoplan-campaign.expand_value.be085ae4", { value0: title })
|
|
||||||
: i18nMessage("i18n:govoplan-campaign.collapse_value.29095640", { value0: title })
|
|
||||||
}
|
}
|
||||||
title={
|
afterBody={locked ? (
|
||||||
collapsed
|
|
||||||
? "i18n:govoplan-campaign.show_content.0528d8d2"
|
|
||||||
: "i18n:govoplan-campaign.show_header_only.24afefca"
|
|
||||||
}
|
|
||||||
onClick={() => setCollapsed((value) => !value)}
|
|
||||||
>
|
|
||||||
<ChevronDown size={18} strokeWidth={2.4} aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
{!collapsed && (
|
|
||||||
<>
|
|
||||||
<div className="card-body review-flow-stage-content">{children}</div>
|
|
||||||
{locked && (
|
|
||||||
<div className="review-flow-lock-message">
|
<div className="review-flow-lock-message">
|
||||||
<span className="review-flow-lock-icon">
|
<span className="review-flow-lock-icon">
|
||||||
<LockKeyhole size={20} aria-hidden="true" />
|
<LockKeyhole size={20} aria-hidden="true" />
|
||||||
</span>
|
</span>
|
||||||
<span>{lockReason}</span>
|
<span>{lockReason}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : undefined}
|
||||||
</>
|
>
|
||||||
)}
|
{children}
|
||||||
</article>
|
</Card>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WorkflowFact({ label, value }: { label: string; value: ReactNode }) {
|
|
||||||
return (
|
|
||||||
<div className="review-flow-fact">
|
|
||||||
<span>{label}</span>
|
|
||||||
<strong>{value}</strong>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function stateLabel(state: FlowState): string {
|
export function stateLabel(state: FlowState): string {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case "complete":
|
case "complete":
|
||||||
|
|||||||
@@ -133,12 +133,6 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
letter-spacing: .01em;
|
letter-spacing: .01em;
|
||||||
}
|
}
|
||||||
.subsection-heading.split {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
.toggle-grid {
|
.toggle-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(180px, 1fr));
|
grid-template-columns: repeat(2, minmax(180px, 1fr));
|
||||||
@@ -1042,19 +1036,6 @@
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recipient-address-category-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recipient-address-category-header h3 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 15px;
|
|
||||||
color: var(--text-strong);
|
|
||||||
}
|
|
||||||
|
|
||||||
.recipient-address-category-controls {
|
.recipient-address-category-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1821,43 +1802,6 @@
|
|||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
.review-flow-fact-grid,
|
|
||||||
.review-flow-execution-summary {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(4, minmax(130px, 1fr));
|
|
||||||
gap: 10px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.review-flow-fact,
|
|
||||||
.review-flow-execution-summary > div {
|
|
||||||
display: grid;
|
|
||||||
gap: 5px;
|
|
||||||
min-width: 0;
|
|
||||||
padding: 12px 14px;
|
|
||||||
border: var(--border-line);
|
|
||||||
border-radius: 6px;
|
|
||||||
background: var(--panel-soft);
|
|
||||||
}
|
|
||||||
|
|
||||||
.review-flow-fact span,
|
|
||||||
.review-flow-execution-summary span {
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 800;
|
|
||||||
letter-spacing: .04em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.review-flow-fact strong,
|
|
||||||
.review-flow-execution-summary strong {
|
|
||||||
min-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
color: var(--text-strong);
|
|
||||||
font-size: 16px;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.review-flow-inline-note {
|
.review-flow-inline-note {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -2009,13 +1953,6 @@
|
|||||||
50% { box-shadow: 0 0 0 7px color-mix(in srgb, var(--review-stage-color) 18%, transparent), 0 4px 12px var(--campaign-stage-shadow-color); }
|
50% { box-shadow: 0 0 0 7px color-mix(in srgb, var(--review-stage-color) 18%, transparent), 0 4px 12px var(--campaign-stage-shadow-color); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 980px) {
|
|
||||||
.review-flow-fact-grid,
|
|
||||||
.review-flow-execution-summary {
|
|
||||||
grid-template-columns: repeat(2, minmax(130px, 1fr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 680px) {
|
@media (max-width: 680px) {
|
||||||
.review-flow-stage {
|
.review-flow-stage {
|
||||||
grid-template-columns: 36px minmax(0, 1fr);
|
grid-template-columns: 36px minmax(0, 1fr);
|
||||||
@@ -2036,11 +1973,6 @@
|
|||||||
.review-flow-lock-message {
|
.review-flow-lock-message {
|
||||||
inset: 100px 0 0;
|
inset: 100px 0 0;
|
||||||
}
|
}
|
||||||
.review-flow-fact-grid,
|
|
||||||
.review-flow-execution-summary {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.deliverability-preflight-header {
|
.deliverability-preflight-header {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
Reference in New Issue
Block a user