feat: bootstrap Ordo process designer with PARALLEL swimlanes
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,795 @@
|
||||
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() {
|
||||
nodes.value = nodes.value.map((node) => {
|
||||
if (!node.data || node.data.kind === 'END') {
|
||||
return node
|
||||
}
|
||||
return {
|
||||
...node,
|
||||
data: { ...node.data, isStart: node.id === startStepId.value },
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
nodes.value = fitParallelFrames(expanded.nodes)
|
||||
edges.value = assignEdgeHandles(expanded.nodes, 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) {
|
||||
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 = fitParallelFrames([...nodes.value, spawnLaneStep(parallelId, lane, laneIndex, stepIndex, nextId(lane))])
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user