Сделал элементы согласно нотации, убрал хэш при билде

This commit is contained in:
NikitaKalitov
2026-07-28 14:03:16 +03:00
parent 99d48f393a
commit 33939dffaf
7 changed files with 767 additions and 208 deletions
+215 -5
View File
@@ -6,7 +6,24 @@ import {
type Node,
type NodeProps,
} from "@xyflow/react";
import type { DiagramNodeData, NodeKind } from "./types";
import { IconUser } from "@consta/icons/IconUser";
import { IconSettings } from "@consta/icons/IconSettings";
import { IconCode } from "@consta/icons/IconCode";
import { IconHand } from "@consta/icons/IconHand";
import { IconTable } from "@consta/icons/IconTable";
import { IconSendMessage } from "@consta/icons/IconSendMessage";
import { IconMail } from "@consta/icons/IconMail";
import { IconWatchStroked } from "@consta/icons/IconWatchStroked";
import { IconTable2 } from "@consta/icons/IconTable2";
import { IconAlert } from "@consta/icons/IconAlert";
import { IconLightningBolt } from "@consta/icons/IconLightningBolt";
import { IconArrowUp } from "@consta/icons/IconArrowUp";
import { IconArrowPrevious } from "@consta/icons/IconArrowPrevious";
import { IconRecord } from "@consta/icons/IconRecord";
import { IconPolygonMultiple } from "@consta/icons/IconPolygonMultiple";
import { IconGisMultiple } from "@consta/icons/IconGisMultiple";
import type { IconComponent } from "@consta/icons/Icon";
import type { DiagramNodeData, EventSubtype, NodeKind, TaskSubtype } from "./types";
type ShapeNode = Node<DiagramNodeData, NodeKind>;
@@ -27,6 +44,10 @@ export const DEFAULT_NODE_BACKGROUND: Record<NodeKind, string> = {
exclusiveGateway: "var(--gradient-purple)",
parallelGateway: "var(--gradient-cyan)",
inclusiveGateway: "var(--gradient-olive)",
complexGateway: "var(--gradient-violet)",
eventBasedGateway: "var(--gradient-teal-dark)",
hint: "var(--gradient-frost)",
dataObject: "var(--gradient-ice)",
};
// The color picker only understands flat hex, so it needs a solid fallback
@@ -39,6 +60,92 @@ export const DEFAULT_NODE_COLOR: Record<NodeKind, string> = {
exclusiveGateway: "#A12C7E",
parallelGateway: "#1BD3F7",
inclusiveGateway: "#BBBD39",
complexGateway: "#8A5CE7",
eventBasedGateway: "#3C81A0",
hint: "#D7E5ED",
dataObject: "#CEEAFF",
};
// BPMN task-type icon shown as a small badge in the shape's top-left corner —
// 'none' (the plain abstract Task) intentionally has no entry, so it renders bare.
export const TASK_SUBTYPE_ICON: Partial<Record<TaskSubtype, IconComponent>> = {
user: IconUser,
service: IconSettings,
script: IconCode,
manual: IconHand,
businessRule: IconTable,
send: IconSendMessage,
receive: IconMail,
};
export const TASK_SUBTYPE_LABEL: Record<TaskSubtype, string> = {
none: "Действие",
user: "Действие пользователя",
service: "Сервисное действие",
script: "Действие-скрипт",
manual: "Ручное действие",
businessRule: "Действие по бизнес-правилам",
send: "Действие отправки",
receive: "Действие получения",
};
// BPMN event-trigger icon shown centered inside the event circle — 'none'
// (the plain, untyped event) intentionally has no entry.
export const EVENT_SUBTYPE_ICON: Partial<Record<EventSubtype, IconComponent>> = {
message: IconMail,
timer: IconWatchStroked,
conditional: IconTable2,
signal: IconAlert,
error: IconLightningBolt,
escalation: IconArrowUp,
compensation: IconArrowPrevious,
terminate: IconRecord,
multiple: IconPolygonMultiple,
parallelMultiple: IconGisMultiple,
};
export const EVENT_SUBTYPE_LABEL: Record<EventSubtype, string> = {
none: "Без триггера",
message: "Сообщение",
timer: "Таймер",
conditional: "Условие",
signal: "Сигнал",
error: "Ошибка",
escalation: "Эскалация",
compensation: "Компенсация",
terminate: "Завершение процесса",
multiple: "Множественное",
parallelMultiple: "Параллельное множественное",
};
// Which trigger subtypes are valid on each event kind, per BPMN 2.0 — used to
// filter the subtype picker in NodeInspector so it never offers an invalid combination.
export const EVENT_SUBTYPES_BY_KIND: Record<
"startEvent" | "intermediateEvent" | "endEvent",
EventSubtype[]
> = {
startEvent: ["none", "message", "timer", "conditional", "signal", "multiple", "parallelMultiple"],
intermediateEvent: [
"none",
"message",
"timer",
"conditional",
"signal",
"escalation",
"compensation",
"multiple",
"parallelMultiple",
],
endEvent: [
"none",
"message",
"error",
"escalation",
"compensation",
"signal",
"terminate",
"multiple",
],
};
export const DEFAULT_TASK_BORDER_RADIUS = 6;
@@ -109,6 +216,9 @@ function ShapeResizer({
}
function TaskNode({ id, data, selected }: NodeProps<ShapeNode>) {
const subtype = (data.subtype as TaskSubtype | undefined) ?? "none";
const SubtypeIcon = TASK_SUBTYPE_ICON[subtype];
const textColor = data.textColor ?? DEFAULT_NODE_TEXT_COLOR;
return (
<div
className="diagram-node diagram-node--task"
@@ -125,10 +235,19 @@ function TaskNode({ id, data, selected }: NodeProps<ShapeNode>) {
/>
<ShapeHandles />
<SubProcessBadge id={id} />
{SubtypeIcon && (
<span
className="diagram-node__subtype-badge"
style={{ color: textColor }}
title={TASK_SUBTYPE_LABEL[subtype]}
>
<SubtypeIcon size="xs" />
</span>
)}
<span
className="diagram-node__label"
style={{
color: data.textColor ?? DEFAULT_NODE_TEXT_COLOR,
color: textColor,
fontSize: data.fontSize ?? DEFAULT_NODE_FONT_SIZE,
}}
>
@@ -141,6 +260,8 @@ function TaskNode({ id, data, selected }: NodeProps<ShapeNode>) {
function EventNode({ id, data, type, selected }: NodeProps<ShapeNode>) {
const kind =
(type as "startEvent" | "intermediateEvent" | "endEvent") ?? "startEvent";
const subtype = (data.subtype as EventSubtype | undefined) ?? "none";
const SubtypeIcon = EVENT_SUBTYPE_ICON[subtype];
return (
<div
className="diagram-node diagram-node--circle"
@@ -149,6 +270,15 @@ function EventNode({ id, data, type, selected }: NodeProps<ShapeNode>) {
<ShapeResizer selected={selected} minWidth={24} minHeight={24} />
<ShapeHandles />
<SubProcessBadge id={id} />
{SubtypeIcon && (
<span
className="diagram-node__event-icon"
style={{ color: data.textColor ?? DEFAULT_NODE_TEXT_COLOR }}
title={EVENT_SUBTYPE_LABEL[subtype]}
>
<SubtypeIcon size="s" />
</span>
)}
<span
className="diagram-node__caption"
style={{
@@ -163,18 +293,24 @@ function EventNode({ id, data, type, selected }: NodeProps<ShapeNode>) {
}
const GATEWAY_ICON: Record<
"exclusiveGateway" | "parallelGateway" | "inclusiveGateway",
"exclusiveGateway" | "parallelGateway" | "inclusiveGateway" | "complexGateway" | "eventBasedGateway",
string
> = {
exclusiveGateway: "✕",
parallelGateway: "+",
inclusiveGateway: "○",
complexGateway: "✳",
eventBasedGateway: "⬠",
};
function GatewayNode({ id, data, type, selected }: NodeProps<ShapeNode>) {
const kind =
(type as "exclusiveGateway" | "parallelGateway" | "inclusiveGateway") ??
"exclusiveGateway";
(type as
| "exclusiveGateway"
| "parallelGateway"
| "inclusiveGateway"
| "complexGateway"
| "eventBasedGateway") ?? "exclusiveGateway";
return (
<div className="diagram-node--gateway-outer">
<ShapeResizer selected={selected} minWidth={32} minHeight={32} />
@@ -211,6 +347,76 @@ function GatewayNode({ id, data, type, selected }: NodeProps<ShapeNode>) {
);
}
// Not a BPMN element — a free-floating annotation the user places next to
// other shapes. No handles: it's never meant to be wired into the flow.
function HintNode({ data, selected }: NodeProps<ShapeNode>) {
return (
<div
className="diagram-node diagram-node--hint"
style={{
background: data.color ?? DEFAULT_NODE_BACKGROUND.hint,
borderRadius: data.borderRadius ?? DEFAULT_TASK_BORDER_RADIUS,
}}
>
<ShapeResizer
selected={selected}
minWidth={60}
minHeight={32}
keepAspectRatio={false}
/>
<span
className="diagram-node__label"
style={{
// The default hint fill (--gradient-frost) is pale, unlike the
// vivid task backgrounds, so it needs the dark caption color, not
// the light one — same reasoning as DataObjectNode below.
color: data.textColor ?? DEFAULT_CAPTION_TEXT_COLOR,
fontSize: data.fontSize ?? DEFAULT_NODE_FONT_SIZE,
}}
>
{data.label}
</span>
</div>
);
}
// BPMN Data Object — represents a document/input/output attached to a task
// via a (typically dashed) Data Association edge. The folded top-right corner
// is clipped on an inner div, not the outer one, so ShapeHandles — siblings of
// it — still land at the plain bounding-box edges instead of being cut off
// (same reasoning as the gateway diamond above).
function DataObjectNode({ data, selected }: NodeProps<ShapeNode>) {
return (
<div className="diagram-node--data-object-outer">
<ShapeResizer
selected={selected}
minWidth={50}
minHeight={60}
keepAspectRatio={false}
/>
<div
className="diagram-node--data-object-bg"
style={{ background: data.color ?? DEFAULT_NODE_BACKGROUND.dataObject }}
/>
<ShapeHandles />
<span
className="diagram-node__label"
style={{
// The folded-corner bg's `clip-path` gives it its own stacking
// context (z-index: 0, set in CSS), so the label needs to be
// positioned with a higher z-index to paint above it.
position: "relative",
zIndex: 1,
color: data.textColor ?? DEFAULT_CAPTION_TEXT_COLOR,
fontSize: data.fontSize ?? DEFAULT_NODE_FONT_SIZE,
}}
>
{data.label}
</span>
</div>
);
}
export const nodeTypes = {
task: TaskNode,
startEvent: EventNode,
@@ -219,4 +425,8 @@ export const nodeTypes = {
exclusiveGateway: GatewayNode,
parallelGateway: GatewayNode,
inclusiveGateway: GatewayNode,
complexGateway: GatewayNode,
eventBasedGateway: GatewayNode,
hint: HintNode,
dataObject: DataObjectNode,
};