805 lines
24 KiB
TypeScript
805 lines
24 KiB
TypeScript
import { computed, ref } from 'vue'
|
|
import { defineStore } from 'pinia'
|
|
import type { Connection } from '@vue-flow/core'
|
|
import {
|
|
ACTION_TYPE,
|
|
APPROVAL_TYPE,
|
|
END_TYPE,
|
|
PARALLEL_TYPE,
|
|
END_NODE_ID,
|
|
edgeLabel,
|
|
fromDocument,
|
|
layoutFromNodes,
|
|
toDocument,
|
|
type DesignerEdge,
|
|
type DesignerNode,
|
|
validateGraph,
|
|
} from '@/mapper/graph'
|
|
import { dropCrossLaneEdges, expandLegacyParallel, fitParallelFrames, laneOf, materializeParallel, reflowLanes, settleLaneNodes, spawnLaneStep } from '@/mapper/parallel'
|
|
import { applyLayeredLayout, assignEdgeHandles, normalizeHandleId } from '@/mapper/layout'
|
|
import { defaultActionData, defaultApprovalData, defaultEndData } from '@/types/definition'
|
|
import type { DesignerEdgeData, DesignerNodeData, ProcessDefinitionDocument } from '@/types/definition'
|
|
|
|
let seq = 0
|
|
function nextId(prefix: string) {
|
|
seq += 1
|
|
return `${prefix}-${Date.now().toString(36)}-${seq}`
|
|
}
|
|
|
|
interface GraphSnapshot {
|
|
definitionId: string
|
|
name: string
|
|
startStepId: string
|
|
nodes: DesignerNode[]
|
|
edges: DesignerEdge[]
|
|
}
|
|
|
|
interface ClipboardPayload {
|
|
nodes: DesignerNode[]
|
|
edges: DesignerEdge[]
|
|
}
|
|
|
|
function slimNode(node: DesignerNode): DesignerNode {
|
|
return {
|
|
id: node.id,
|
|
type: node.type,
|
|
position: { x: node.position.x, y: node.position.y },
|
|
parentNode: node.parentNode,
|
|
extent: node.extent,
|
|
style: node.style ? { ...node.style } : undefined,
|
|
zIndex: node.zIndex,
|
|
data: JSON.parse(JSON.stringify(node.data)) as DesignerNodeData,
|
|
}
|
|
}
|
|
|
|
function slimEdge(edge: DesignerEdge): DesignerEdge {
|
|
return {
|
|
id: edge.id,
|
|
source: edge.source,
|
|
target: edge.target,
|
|
sourceHandle: edge.sourceHandle,
|
|
targetHandle: edge.targetHandle,
|
|
label: edge.label,
|
|
data: { ...(edge.data ?? { priority: 0 }) },
|
|
}
|
|
}
|
|
|
|
export const useDesignerStore = defineStore('designer', () => {
|
|
const definitionId = ref('')
|
|
const name = ref('')
|
|
const startStepId = ref('')
|
|
const publishedVersion = ref<number | null>(null)
|
|
const loadedFromDraft = ref(false)
|
|
const nodes = ref<DesignerNode[]>([])
|
|
const edges = ref<DesignerEdge[]>([])
|
|
const selectedNodeId = ref<string | null>(null)
|
|
const selectedEdgeId = ref<string | null>(null)
|
|
const selectedNodeIds = ref<string[]>([])
|
|
const selectedEdgeIds = ref<string[]>([])
|
|
const baseline = ref('')
|
|
const undoStack = ref<GraphSnapshot[]>([])
|
|
const redoStack = ref<GraphSnapshot[]>([])
|
|
let historyPaused = false
|
|
let clipboard: ClipboardPayload | null = null
|
|
|
|
const selectedNode = computed<DesignerNode | undefined>(() =>
|
|
nodes.value.find((n) => n.id === selectedNodeId.value),
|
|
)
|
|
const selectedEdge = computed<DesignerEdge | undefined>(() =>
|
|
edges.value.find((e) => e.id === selectedEdgeId.value),
|
|
)
|
|
const graphErrors = computed(() =>
|
|
validateGraph(definitionId.value, name.value, startStepId.value, nodes.value, edges.value),
|
|
)
|
|
const canUndo = computed(() => undoStack.value.length > 0)
|
|
const canRedo = computed(() => redoStack.value.length > 0)
|
|
const canPaste = computed(() => Boolean(clipboard?.nodes.length))
|
|
|
|
function snapshot() {
|
|
return JSON.stringify({
|
|
id: definitionId.value,
|
|
name: name.value,
|
|
startStepId: startStepId.value,
|
|
doc: toDocument(definitionId.value, name.value, startStepId.value, nodes.value, edges.value),
|
|
layout: layoutFromNodes(nodes.value),
|
|
})
|
|
}
|
|
|
|
function captureBaseline() {
|
|
baseline.value = snapshot()
|
|
}
|
|
|
|
const isDirty = computed(() => snapshot() !== baseline.value)
|
|
|
|
function cloneGraph(): GraphSnapshot {
|
|
return {
|
|
definitionId: definitionId.value,
|
|
name: name.value,
|
|
startStepId: startStepId.value,
|
|
nodes: nodes.value.map(slimNode),
|
|
edges: edges.value.map(slimEdge),
|
|
}
|
|
}
|
|
|
|
function resetHistory() {
|
|
undoStack.value = []
|
|
redoStack.value = []
|
|
}
|
|
|
|
function pushHistory() {
|
|
if (historyPaused) {
|
|
return
|
|
}
|
|
undoStack.value = [...undoStack.value, cloneGraph()].slice(-50)
|
|
redoStack.value = []
|
|
}
|
|
|
|
function applySnapshot(snap: GraphSnapshot) {
|
|
historyPaused = true
|
|
definitionId.value = snap.definitionId
|
|
name.value = snap.name
|
|
startStepId.value = snap.startStepId
|
|
nodes.value = snap.nodes.map(slimNode)
|
|
edges.value = snap.edges.map(slimEdge)
|
|
selectedNodeId.value = null
|
|
selectedEdgeId.value = null
|
|
selectedNodeIds.value = []
|
|
selectedEdgeIds.value = []
|
|
historyPaused = false
|
|
}
|
|
|
|
function undo() {
|
|
const prev = undoStack.value.at(-1)
|
|
if (!prev) {
|
|
return false
|
|
}
|
|
const current = cloneGraph()
|
|
undoStack.value = undoStack.value.slice(0, -1)
|
|
redoStack.value = [...redoStack.value, current]
|
|
applySnapshot(prev)
|
|
return true
|
|
}
|
|
|
|
function redo() {
|
|
const next = redoStack.value.at(-1)
|
|
if (!next) {
|
|
return false
|
|
}
|
|
const current = cloneGraph()
|
|
redoStack.value = redoStack.value.slice(0, -1)
|
|
undoStack.value = [...undoStack.value, current]
|
|
applySnapshot(next)
|
|
return true
|
|
}
|
|
|
|
function syncStartFlags() {
|
|
const current = nodes.value.find((node) => node.id === startStepId.value)
|
|
if (current?.parentNode) {
|
|
startStepId.value = current.parentNode
|
|
}
|
|
nodes.value = nodes.value.map((node) => {
|
|
if (!node.data || node.data.kind === 'END') {
|
|
return node
|
|
}
|
|
const isStart = !node.parentNode && node.id === startStepId.value
|
|
return {
|
|
...node,
|
|
data: { ...node.data, isStart },
|
|
}
|
|
})
|
|
}
|
|
|
|
function clearSelection() {
|
|
selectedNodeId.value = null
|
|
selectedEdgeId.value = null
|
|
selectedNodeIds.value = []
|
|
selectedEdgeIds.value = []
|
|
}
|
|
|
|
function clear() {
|
|
definitionId.value = ''
|
|
name.value = ''
|
|
startStepId.value = ''
|
|
publishedVersion.value = null
|
|
loadedFromDraft.value = false
|
|
nodes.value = []
|
|
edges.value = []
|
|
clearSelection()
|
|
resetHistory()
|
|
captureBaseline()
|
|
}
|
|
|
|
function resetEmpty(id = `process-${Date.now().toString(36)}`) {
|
|
definitionId.value = id
|
|
name.value = 'Untitled process'
|
|
startStepId.value = ''
|
|
publishedVersion.value = null
|
|
loadedFromDraft.value = false
|
|
nodes.value = []
|
|
edges.value = []
|
|
clearSelection()
|
|
resetHistory()
|
|
captureBaseline()
|
|
}
|
|
|
|
function loadDocument(doc: ProcessDefinitionDocument) {
|
|
const mapped = fromDocument(doc)
|
|
definitionId.value = doc.id
|
|
name.value = doc.name
|
|
startStepId.value = mapped.startStepId
|
|
publishedVersion.value = doc.version ?? null
|
|
loadedFromDraft.value = false
|
|
const laid = applyLayeredLayout(mapped.nodes, mapped.edges, mapped.startStepId)
|
|
nodes.value = laid.nodes
|
|
edges.value = laid.edges
|
|
clearSelection()
|
|
resetHistory()
|
|
captureBaseline()
|
|
}
|
|
|
|
function loadGraph(params: {
|
|
id: string
|
|
name: string
|
|
startStepId: string
|
|
nodes: DesignerNode[]
|
|
edges: DesignerEdge[]
|
|
version?: number | null
|
|
fromDraft?: boolean
|
|
}) {
|
|
definitionId.value = params.id
|
|
name.value = params.name
|
|
startStepId.value = params.startStepId
|
|
publishedVersion.value = params.version ?? null
|
|
loadedFromDraft.value = Boolean(params.fromDraft)
|
|
const expanded = expandLegacyParallel(params.nodes, params.edges)
|
|
const parallels = expanded.nodes.filter((node) => node.data?.kind === 'PARALLEL').map((node) => node.id)
|
|
nodes.value = parallels.reduce((current, id) => reflowLanes(current, id), expanded.nodes)
|
|
edges.value = assignEdgeHandles(nodes.value, expanded.edges)
|
|
syncStartFlags()
|
|
clearSelection()
|
|
resetHistory()
|
|
captureBaseline()
|
|
}
|
|
|
|
function relayout() {
|
|
pushHistory()
|
|
const laid = applyLayeredLayout(nodes.value, edges.value, startStepId.value)
|
|
nodes.value = laid.nodes
|
|
edges.value = laid.edges
|
|
}
|
|
|
|
function syncEdgeHandles() {
|
|
edges.value = assignEdgeHandles(nodes.value, edges.value)
|
|
}
|
|
|
|
function settleLanes() {
|
|
const settled = settleLaneNodes(nodes.value)
|
|
nodes.value = settled
|
|
edges.value = assignEdgeHandles(settled, dropCrossLaneEdges(settled, edges.value))
|
|
}
|
|
|
|
function addNode(type: string, position: { x: number; y: number }): 'ok' | 'duplicate-end' {
|
|
let data: DesignerNodeData
|
|
let nodeType = type
|
|
if (type === APPROVAL_TYPE) {
|
|
data = defaultApprovalData()
|
|
} else if (type === ACTION_TYPE) {
|
|
data = defaultActionData()
|
|
} else if (type === PARALLEL_TYPE) {
|
|
pushHistory()
|
|
const id = nextId(nodeType)
|
|
const built = materializeParallel(id, 'Parallel', position, [
|
|
{ id: 'legal', steps: [{ id: nextId('legal'), name: 'Legal', candidates: [], policy: 'ANY' }], transitions: [] },
|
|
{ id: 'finance', steps: [{ id: nextId('finance'), name: 'Finance', candidates: [], policy: 'ANY' }], transitions: [] },
|
|
])
|
|
nodes.value = fitParallelFrames([...nodes.value, built.parent, ...built.children])
|
|
if (!startStepId.value) {
|
|
startStepId.value = id
|
|
syncStartFlags()
|
|
}
|
|
selectedNodeId.value = id
|
|
selectedNodeIds.value = [id]
|
|
selectedEdgeId.value = null
|
|
selectedEdgeIds.value = []
|
|
return 'ok'
|
|
} else {
|
|
nodeType = END_TYPE
|
|
if (nodes.value.some((n) => n.type === END_TYPE)) {
|
|
return 'duplicate-end'
|
|
}
|
|
data = defaultEndData()
|
|
}
|
|
pushHistory()
|
|
const id = nodeType === END_TYPE ? END_NODE_ID : nextId(nodeType)
|
|
const node: DesignerNode = { id, type: nodeType, position, data }
|
|
nodes.value = [...nodes.value, node]
|
|
if (!startStepId.value && nodeType !== END_TYPE) {
|
|
startStepId.value = id
|
|
syncStartFlags()
|
|
}
|
|
selectedNodeId.value = id
|
|
selectedNodeIds.value = [id]
|
|
selectedEdgeId.value = null
|
|
selectedEdgeIds.value = []
|
|
return 'ok'
|
|
}
|
|
|
|
function setStart(id: string) {
|
|
const node = nodes.value.find((n) => n.id === id)
|
|
if (!node || node.type === END_TYPE || node.parentNode) {
|
|
return
|
|
}
|
|
pushHistory()
|
|
startStepId.value = id
|
|
syncStartFlags()
|
|
}
|
|
|
|
function renameNode(oldId: string, newId: string): boolean {
|
|
const trimmed = newId.trim()
|
|
if (!trimmed || trimmed === oldId) {
|
|
return true
|
|
}
|
|
if (nodes.value.some((n) => n.id === trimmed)) {
|
|
return false
|
|
}
|
|
pushHistory()
|
|
nodes.value = nodes.value.map((n) => (n.id === oldId ? { ...n, id: trimmed } : n))
|
|
edges.value = edges.value.map((e) => ({
|
|
...e,
|
|
source: e.source === oldId ? trimmed : e.source,
|
|
target: e.target === oldId ? trimmed : e.target,
|
|
}))
|
|
if (startStepId.value === oldId) {
|
|
startStepId.value = trimmed
|
|
}
|
|
if (selectedNodeId.value === oldId) {
|
|
selectedNodeId.value = trimmed
|
|
}
|
|
selectedNodeIds.value = selectedNodeIds.value.map((id) => (id === oldId ? trimmed : id))
|
|
return true
|
|
}
|
|
|
|
function patchNodeData(id: string, patch: Record<string, unknown>) {
|
|
pushHistory()
|
|
nodes.value = nodes.value.map((n) => {
|
|
if (n.id !== id) {
|
|
return n
|
|
}
|
|
return { ...n, data: { ...n.data, ...patch } as DesignerNodeData }
|
|
})
|
|
}
|
|
|
|
function patchEdgeData(id: string, patch: Partial<DesignerEdgeData>) {
|
|
pushHistory()
|
|
edges.value = edges.value.map((e) => {
|
|
if (e.id !== id) {
|
|
return e
|
|
}
|
|
const data = { ...(e.data ?? { priority: 0 }), ...patch }
|
|
if ('when' in patch && patch.when === undefined) {
|
|
delete data.when
|
|
}
|
|
return { ...e, data, label: edgeLabel(data) }
|
|
})
|
|
}
|
|
|
|
function onConnect(connection: Connection) {
|
|
if (!connection.source || !connection.target) {
|
|
return
|
|
}
|
|
const sourceNode = nodes.value.find((n) => n.id === connection.source)
|
|
const targetNode = nodes.value.find((n) => n.id === connection.target)
|
|
if (sourceNode?.type === END_TYPE || targetNode?.type === PARALLEL_TYPE) {
|
|
return
|
|
}
|
|
const inner = Boolean(sourceNode?.parentNode || targetNode?.parentNode)
|
|
if (inner && (sourceNode?.parentNode !== targetNode?.parentNode || laneOf(sourceNode?.data) !== laneOf(targetNode?.data))) {
|
|
return
|
|
}
|
|
pushHistory()
|
|
const siblings = edges.value.filter((e) => e.source === connection.source)
|
|
const used = new Set(siblings.map((e) => e.data?.priority ?? 0))
|
|
let priority = 0
|
|
while (used.has(priority)) {
|
|
priority += 1
|
|
}
|
|
const data: DesignerEdgeData = { priority }
|
|
const edge: DesignerEdge = {
|
|
id: nextId('e'),
|
|
source: connection.source,
|
|
target: connection.target,
|
|
sourceHandle: normalizeHandleId(connection.sourceHandle) ?? 'source-bottom',
|
|
targetHandle: normalizeHandleId(connection.targetHandle) ?? 'target-top',
|
|
label: edgeLabel(data),
|
|
data,
|
|
}
|
|
edges.value = [...edges.value, edge]
|
|
edges.value = assignEdgeHandles(nodes.value, edges.value)
|
|
}
|
|
|
|
function selectNode(id: string | null) {
|
|
selectedNodeId.value = id
|
|
selectedNodeIds.value = id ? [id] : []
|
|
if (id) {
|
|
selectedEdgeId.value = null
|
|
selectedEdgeIds.value = []
|
|
}
|
|
}
|
|
|
|
function selectEdge(id: string | null) {
|
|
selectedEdgeId.value = id
|
|
selectedEdgeIds.value = id ? [id] : []
|
|
if (id) {
|
|
selectedNodeId.value = null
|
|
selectedNodeIds.value = []
|
|
}
|
|
}
|
|
|
|
function setSelection(nodeIds: string[], edgeIds: string[]) {
|
|
selectedNodeIds.value = nodeIds
|
|
selectedEdgeIds.value = edgeIds
|
|
selectedNodeId.value = nodeIds.length === 1 ? (nodeIds[0] ?? null) : nodeIds[0] ?? null
|
|
if (nodeIds.length > 1) {
|
|
selectedNodeId.value = nodeIds[0] ?? null
|
|
}
|
|
selectedEdgeId.value = nodeIds.length ? null : (edgeIds[0] ?? null)
|
|
}
|
|
|
|
function hasOutgoing(id: string) {
|
|
return edges.value.some((e) => e.source === id)
|
|
}
|
|
|
|
function hasOutgoingAmong(ids: string[]) {
|
|
return ids.some((id) => hasOutgoing(id))
|
|
}
|
|
|
|
function removeNode(id: string) {
|
|
const node = nodes.value.find((n) => n.id === id)
|
|
if (!node) {
|
|
return
|
|
}
|
|
const drop = new Set([id, ...nodes.value.filter((n) => n.parentNode === id).map((n) => n.id)])
|
|
pushHistory()
|
|
nodes.value = nodes.value.filter((n) => !drop.has(n.id))
|
|
edges.value = edges.value.filter((e) => !drop.has(e.source) && !drop.has(e.target))
|
|
selectedNodeIds.value = selectedNodeIds.value.filter((n) => !drop.has(n))
|
|
if (selectedNodeId.value && drop.has(selectedNodeId.value)) {
|
|
selectedNodeId.value = selectedNodeIds.value[0] ?? null
|
|
}
|
|
if (drop.has(startStepId.value)) {
|
|
const next = nodes.value.find((n) => n.type !== END_TYPE && !n.parentNode)
|
|
startStepId.value = next?.id ?? ''
|
|
syncStartFlags()
|
|
}
|
|
}
|
|
|
|
function removeEdge(id: string) {
|
|
pushHistory()
|
|
edges.value = edges.value.filter((e) => e.id !== id)
|
|
selectedEdgeIds.value = selectedEdgeIds.value.filter((e) => e !== id)
|
|
if (selectedEdgeId.value === id) {
|
|
selectedEdgeId.value = selectedEdgeIds.value[0] ?? null
|
|
}
|
|
}
|
|
|
|
function removeNodes(ids: string[]) {
|
|
const set = new Set(ids)
|
|
for (const node of nodes.value) {
|
|
if (node.parentNode && set.has(node.parentNode)) {
|
|
set.add(node.id)
|
|
}
|
|
}
|
|
if (!set.size) {
|
|
return
|
|
}
|
|
pushHistory()
|
|
nodes.value = nodes.value.filter((n) => !set.has(n.id))
|
|
edges.value = edges.value.filter((e) => !set.has(e.source) && !set.has(e.target))
|
|
selectedNodeIds.value = []
|
|
selectedNodeId.value = null
|
|
if (set.has(startStepId.value)) {
|
|
const next = nodes.value.find((n) => n.type !== END_TYPE && !n.parentNode)
|
|
startStepId.value = next?.id ?? ''
|
|
syncStartFlags()
|
|
}
|
|
}
|
|
|
|
function removeEdges(ids: string[]) {
|
|
const set = new Set(ids)
|
|
if (!set.size) {
|
|
return
|
|
}
|
|
pushHistory()
|
|
edges.value = edges.value.filter((e) => !set.has(e.id))
|
|
selectedEdgeIds.value = []
|
|
selectedEdgeId.value = null
|
|
}
|
|
|
|
function removeSelected() {
|
|
if (selectedNodeIds.value.length) {
|
|
removeNodes(selectedNodeIds.value)
|
|
return
|
|
}
|
|
if (selectedNodeId.value) {
|
|
removeNode(selectedNodeId.value)
|
|
return
|
|
}
|
|
if (selectedEdgeIds.value.length) {
|
|
removeEdges(selectedEdgeIds.value)
|
|
return
|
|
}
|
|
if (selectedEdgeId.value) {
|
|
removeEdge(selectedEdgeId.value)
|
|
}
|
|
}
|
|
|
|
function withDescendants(ids: string[]) {
|
|
const set = new Set(ids)
|
|
let grew = true
|
|
while (grew) {
|
|
grew = false
|
|
for (const node of nodes.value) {
|
|
if (node.parentNode && set.has(node.parentNode) && !set.has(node.id)) {
|
|
set.add(node.id)
|
|
grew = true
|
|
}
|
|
}
|
|
}
|
|
return [...set]
|
|
}
|
|
|
|
function copySelection() {
|
|
const ids = selectedNodeIds.value.length
|
|
? selectedNodeIds.value
|
|
: selectedNodeId.value
|
|
? [selectedNodeId.value]
|
|
: []
|
|
if (!ids.length) {
|
|
clipboard = null
|
|
return false
|
|
}
|
|
const idSet = new Set(withDescendants(ids))
|
|
clipboard = {
|
|
nodes: nodes.value.filter((n) => idSet.has(n.id)).map(slimNode),
|
|
edges: edges.value
|
|
.filter((e) => idSet.has(e.source) && idSet.has(e.target))
|
|
.map(slimEdge),
|
|
}
|
|
return true
|
|
}
|
|
|
|
function pasteClipboard() {
|
|
if (!clipboard?.nodes.length) {
|
|
return false
|
|
}
|
|
pushHistory()
|
|
const hasEnd = nodes.value.some((n) => n.type === END_TYPE)
|
|
const idMap = new Map<string, string>()
|
|
const pastedNodes: DesignerNode[] = []
|
|
const ordered = [...clipboard.nodes].sort((a, b) => Number(Boolean(a.parentNode)) - Number(Boolean(b.parentNode)))
|
|
for (const node of ordered) {
|
|
if (node.type === END_TYPE && hasEnd) {
|
|
continue
|
|
}
|
|
const id = node.type === END_TYPE ? END_NODE_ID : nextId(node.type)
|
|
idMap.set(node.id, id)
|
|
const parentMoved = Boolean(node.parentNode && idMap.has(node.parentNode))
|
|
pastedNodes.push({
|
|
...slimNode(node),
|
|
id,
|
|
parentNode: node.parentNode ? (idMap.get(node.parentNode) ?? node.parentNode) : undefined,
|
|
position: parentMoved || !node.parentNode
|
|
? node.parentNode
|
|
? { ...node.position }
|
|
: { x: node.position.x + 40, y: node.position.y + 40 }
|
|
: { x: node.position.x + 40, y: node.position.y },
|
|
})
|
|
}
|
|
const pastedEdges: DesignerEdge[] = []
|
|
for (const edge of clipboard.edges) {
|
|
const source = idMap.get(edge.source)
|
|
const target = idMap.get(edge.target)
|
|
if (!source || !target) {
|
|
continue
|
|
}
|
|
const data = { ...(edge.data ?? { priority: 0 }) }
|
|
pastedEdges.push({
|
|
...slimEdge(edge),
|
|
id: nextId('e'),
|
|
source,
|
|
target,
|
|
data,
|
|
label: edgeLabel(data),
|
|
})
|
|
}
|
|
nodes.value = fitParallelFrames([...nodes.value, ...pastedNodes])
|
|
edges.value = [...edges.value, ...pastedEdges]
|
|
selectedNodeIds.value = pastedNodes.map((n) => n.id)
|
|
selectedNodeId.value = pastedNodes[0]?.id ?? null
|
|
selectedEdgeId.value = null
|
|
selectedEdgeIds.value = []
|
|
return pastedNodes.length > 0
|
|
}
|
|
|
|
function alignSelected(mode: 'left' | 'center' | 'top'): boolean {
|
|
const ids = selectedNodeIds.value.length >= 2 ? selectedNodeIds.value : []
|
|
if (ids.length < 2) {
|
|
return false
|
|
}
|
|
const set = new Set(ids)
|
|
const selected = nodes.value.filter((n) => set.has(n.id))
|
|
pushHistory()
|
|
if (mode === 'left') {
|
|
const x = Math.min(...selected.map((n) => n.position.x))
|
|
for (const n of selected) {
|
|
n.position = { ...n.position, x }
|
|
}
|
|
} else if (mode === 'center') {
|
|
const xs = selected.map((n) => n.position.x)
|
|
const x = (Math.min(...xs) + Math.max(...xs)) / 2
|
|
for (const n of selected) {
|
|
n.position = { ...n.position, x }
|
|
}
|
|
} else {
|
|
const y = Math.min(...selected.map((n) => n.position.y))
|
|
for (const n of selected) {
|
|
n.position = { ...n.position, y }
|
|
}
|
|
}
|
|
nodes.value = settleLaneNodes(nodes.value)
|
|
return true
|
|
}
|
|
|
|
function laneName(lanes: string[]) {
|
|
let index = lanes.length + 1
|
|
let id = `branch-${index}`
|
|
while (lanes.includes(id)) {
|
|
index += 1
|
|
id = `branch-${index}`
|
|
}
|
|
return id
|
|
}
|
|
|
|
function addLane(parallelId: string) {
|
|
const parent = nodes.value.find((node) => node.id === parallelId)
|
|
if (!parent || parent.data.kind !== 'PARALLEL') {
|
|
return
|
|
}
|
|
pushHistory()
|
|
const lane = laneName(parent.data.lanes)
|
|
const step = spawnLaneStep(parallelId, lane, parent.data.lanes.length, 0, nextId(lane))
|
|
nodes.value = fitParallelFrames([
|
|
...nodes.value.map((node) =>
|
|
node.id === parallelId && node.data.kind === 'PARALLEL'
|
|
? { ...node, data: { ...node.data, lanes: [...node.data.lanes, lane] } }
|
|
: node,
|
|
),
|
|
step,
|
|
])
|
|
}
|
|
|
|
function removeLane(parallelId: string, lane: string) {
|
|
const parent = nodes.value.find((node) => node.id === parallelId)
|
|
if (!parent || parent.data.kind !== 'PARALLEL' || parent.data.lanes.length <= 2) {
|
|
return
|
|
}
|
|
pushHistory()
|
|
const drop = new Set(
|
|
nodes.value.filter((node) => node.parentNode === parallelId && laneOf(node.data) === lane).map((node) => node.id),
|
|
)
|
|
nodes.value = reflowLanes(
|
|
nodes.value
|
|
.filter((node) => !drop.has(node.id))
|
|
.map((node) =>
|
|
node.id === parallelId && node.data.kind === 'PARALLEL'
|
|
? { ...node, data: { ...node.data, lanes: node.data.lanes.filter((item: string) => item !== lane) } }
|
|
: node,
|
|
),
|
|
parallelId,
|
|
)
|
|
edges.value = edges.value.filter((edge) => !drop.has(edge.source) && !drop.has(edge.target))
|
|
}
|
|
|
|
function addLaneStep(parallelId: string, lane: string, kind: 'approval' | 'action' = 'approval') {
|
|
const parent = nodes.value.find((node) => node.id === parallelId)
|
|
if (!parent || parent.data.kind !== 'PARALLEL' || !parent.data.lanes.includes(lane)) {
|
|
return
|
|
}
|
|
pushHistory()
|
|
const laneIndex = parent.data.lanes.indexOf(lane)
|
|
const stepIndex = nodes.value.filter((node) => node.parentNode === parallelId && laneOf(node.data) === lane).length
|
|
nodes.value = reflowLanes(
|
|
[...nodes.value, spawnLaneStep(parallelId, lane, laneIndex, stepIndex, nextId(lane), kind)],
|
|
parallelId,
|
|
)
|
|
}
|
|
|
|
function renameLane(parallelId: string, from: string, to: string): boolean {
|
|
const trimmed = to.trim()
|
|
const parent = nodes.value.find((node) => node.id === parallelId)
|
|
if (!parent || parent.data.kind !== 'PARALLEL' || !trimmed || trimmed === from) {
|
|
return trimmed === from
|
|
}
|
|
if (parent.data.lanes.includes(trimmed)) {
|
|
return false
|
|
}
|
|
pushHistory()
|
|
nodes.value = nodes.value.map((node) => {
|
|
if (node.id === parallelId && node.data.kind === 'PARALLEL') {
|
|
return { ...node, data: { ...node.data, lanes: node.data.lanes.map((lane: string) => (lane === from ? trimmed : lane)) } }
|
|
}
|
|
if (node.parentNode === parallelId && laneOf(node.data) === from && (node.data.kind === 'APPROVAL' || node.data.kind === 'ACTION')) {
|
|
return { ...node, data: { ...node.data, laneId: trimmed } }
|
|
}
|
|
return node
|
|
})
|
|
return true
|
|
}
|
|
|
|
function document(): ProcessDefinitionDocument {
|
|
return toDocument(definitionId.value, name.value, startStepId.value, nodes.value, edges.value)
|
|
}
|
|
|
|
function localErrors() {
|
|
return graphErrors.value
|
|
}
|
|
|
|
return {
|
|
definitionId,
|
|
name,
|
|
startStepId,
|
|
publishedVersion,
|
|
loadedFromDraft,
|
|
nodes,
|
|
edges,
|
|
selectedNodeId,
|
|
selectedEdgeId,
|
|
selectedNodeIds,
|
|
selectedEdgeIds,
|
|
selectedNode,
|
|
selectedEdge,
|
|
graphErrors,
|
|
isDirty,
|
|
canUndo,
|
|
canRedo,
|
|
canPaste,
|
|
clear,
|
|
resetEmpty,
|
|
loadDocument,
|
|
loadGraph,
|
|
addNode,
|
|
setStart,
|
|
renameNode,
|
|
patchNodeData,
|
|
patchEdgeData,
|
|
onConnect,
|
|
selectNode,
|
|
selectEdge,
|
|
setSelection,
|
|
removeNode,
|
|
removeEdge,
|
|
removeNodes,
|
|
removeEdges,
|
|
removeSelected,
|
|
hasOutgoing,
|
|
hasOutgoingAmong,
|
|
copySelection,
|
|
pasteClipboard,
|
|
undo,
|
|
redo,
|
|
pushHistory,
|
|
alignSelected,
|
|
document,
|
|
localErrors,
|
|
captureBaseline,
|
|
relayout,
|
|
syncEdgeHandles,
|
|
settleLanes,
|
|
syncStartFlags,
|
|
addLane,
|
|
removeLane,
|
|
addLaneStep,
|
|
renameLane,
|
|
}
|
|
})
|