199 lines
5.6 KiB
TypeScript
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);
|
|
}
|
|
}
|