Files
govoplan-campaign/webui/src/features/campaigns/review/WorkflowNavigation.tsx
T

199 lines
5.6 KiB
TypeScript

import { type CSSProperties, type ReactNode } from "react";
import { LockKeyhole, type LucideIcon } from "lucide-react";
import {
Card,
InlineHelp,
StageRail,
i18nMessage,
type StageRailTone
} from "@govoplan/core-webui";
import { humanize } from "../utils/campaignView";
export type FlowState =
| "complete"
| "warning"
| "danger"
| "active"
| "locked"
| "running"
| "partial"
| "stale"
| "pending";
export type FlowStageDefinition = {
id: string;
title: string;
shortTitle: string;
description: string;
icon: LucideIcon;
state: FlowState;
connectorState?: FlowState;
stateLabel: string;
lockReason?: string;
};
const stateColors: Record<FlowState, string> = {
complete: "var(--green)",
warning: "var(--amber)",
danger: "var(--red)",
active: "var(--blue)",
locked: "var(--line-dark)",
running: "var(--blue)",
partial: "var(--review-flow-partial)",
stale: "var(--review-flow-partial)",
pending: "var(--muted)"
};
const stageRailTones: Record<FlowState, StageRailTone> = {
complete: "success",
warning: "warning",
danger: "danger",
active: "active",
locked: "neutral",
running: "active",
partial: "partial",
stale: "partial",
pending: "neutral"
};
export function WorkflowNavigation({
stages,
onSelect
}: {
stages: FlowStageDefinition[];
onSelect: (id: string) => void;
}) {
return (
<StageRail
className="review-flow-navigation"
ariaLabel="i18n:govoplan-campaign.review_and_send_workflow_steps.77fc8af2"
onSelect={onSelect}
items={stages.map((stage) => {
const Icon = stage.icon;
const title = i18nMessage(stage.title);
const stateText = i18nMessage(stage.stateLabel);
return {
id: stage.id,
label: i18nMessage(stage.shortTitle),
statusLabel: ["active", "locked"].includes(stage.state)
? undefined
: stateText,
title: `${title}: ${stateText}`,
icon: <Icon size={17} strokeWidth={1.8} aria-hidden="true" />,
tone: stageRailTones[stage.state],
connectorTone: stageRailTones[stageConnectorState(stage)],
locked: stage.state === "locked",
lockedLabel: "i18n:govoplan-campaign.locked.a798882f"
};
})}
/>
);
}
export function stageConnectorState(stage: FlowStageDefinition): FlowState {
return stage.connectorState ?? stage.state;
}
export function WorkflowStage({
stage,
nextState,
nextConnectorState,
children
}: {
stage: FlowStageDefinition;
nextState?: FlowState;
nextConnectorState?: FlowState;
children: ReactNode;
}) {
const Icon = stage.icon;
const locked = stage.state === "locked";
const title = i18nMessage(stage.title);
const description = i18nMessage(stage.description);
const stateText = i18nMessage(stage.stateLabel);
const lockReason = stage.lockReason ? i18nMessage(stage.lockReason) : "";
const connectorState = stageConnectorState(stage);
const style = {
"--review-stage-color": stateColors[stage.state],
"--review-stage-line-color": stateColors[connectorState],
"--review-next-stage-line-color": stateColors[nextConnectorState ?? connectorState]
} as CSSProperties;
return (
<section id={stage.id} className="review-flow-stage" data-state={stage.state} style={style} tabIndex={-1}>
<div className="review-flow-stage-marker" aria-hidden="true">
<div className="review-flow-stage-node">
<Icon size={20} strokeWidth={1.8} />
</div>
{nextState && <div className="review-flow-stage-line" />}
</div>
<Card
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}
title={
<h2>
<span>{title}</span>
{locked && (
<LockKeyhole
className="review-flow-title-lock"
size={15}
aria-label="i18n:govoplan-campaign.locked.a798882f"
/>
)}
{!locked && (
<span
className="review-flow-state-badge"
data-state={stage.state}
aria-label={stateText}
title={stateText}
>
{stateText}
</span>
)}
<InlineHelp>{description}</InlineHelp>
</h2>
}
afterBody={locked ? (
<div className="review-flow-lock-message">
<span className="review-flow-lock-icon">
<LockKeyhole size={20} aria-hidden="true" />
</span>
<span>{lockReason}</span>
</div>
) : undefined}
>
{children}
</Card>
</section>
);
}
export function stateLabel(state: FlowState): string {
switch (state) {
case "complete":
return "i18n:govoplan-campaign.passed.271d60f4";
case "warning":
return "i18n:govoplan-campaign.warnings.1430f976";
case "danger":
return "i18n:govoplan-campaign.blocked.99613c74";
case "active":
return "i18n:govoplan-campaign.available.7c62a142";
case "locked":
return "i18n:govoplan-campaign.locked.a798882f";
case "running":
return "i18n:govoplan-campaign.running.73989d9c";
case "partial":
return "i18n:govoplan-campaign.partial.65de2e2a";
case "stale":
return "i18n:govoplan-campaign.stale.189cc40c";
default:
return humanize(state);
}
}