Сделал элементы согласно нотации, убрал хэш при билде
This commit is contained in:
+215
-5
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user