fix: polish PARALLEL swimlanes and color-code edge conditions
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -241,7 +241,7 @@ function deleteSelectedEdge() {
|
|||||||
@update:value="(v: string) => patchSelected({ name: v })"
|
@update:value="(v: string) => patchSelected({ name: v })"
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="start step">
|
<n-form-item v-if="!selectedNode.parentNode" label="start step">
|
||||||
<n-switch :disabled="readOnly" :value="isStart" @update:value="onStartChange" />
|
<n-switch :disabled="readOnly" :value="isStart" @update:value="onStartChange" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { BaseEdge, EdgeLabelRenderer, getBezierPath, type EdgeProps } from '@vue-flow/core'
|
import { BaseEdge, EdgeLabelRenderer, getBezierPath, useVueFlow, type EdgeProps } from '@vue-flow/core'
|
||||||
import type { DesignerEdgeData } from '@/types/definition'
|
import type { DesignerEdgeData } from '@/types/definition'
|
||||||
import { useDesignerStore } from '@/stores/designer'
|
import { useDesignerStore } from '@/stores/designer'
|
||||||
|
|
||||||
const props = defineProps<EdgeProps<DesignerEdgeData>>()
|
const props = defineProps<EdgeProps<DesignerEdgeData>>()
|
||||||
const designer = useDesignerStore()
|
const designer = useDesignerStore()
|
||||||
|
const { addSelectedEdges, findEdge } = useVueFlow()
|
||||||
|
|
||||||
const route = computed(() => {
|
const route = computed(() => {
|
||||||
const [path, labelX, labelY] = getBezierPath({
|
const [path, labelX, labelY] = getBezierPath({
|
||||||
@@ -19,23 +20,58 @@ const route = computed(() => {
|
|||||||
return { path, labelX, labelY }
|
return { path, labelX, labelY }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const kind = computed(() => (props.label === 'host' || props.label === 'predicate' ? props.label : 'default'))
|
||||||
|
const picked = computed(() => props.selected || designer.selectedEdgeId === props.id)
|
||||||
const hasError = computed(() => designer.graphErrors.some((e) => e.edgeId === props.id))
|
const hasError = computed(() => designer.graphErrors.some((e) => e.edgeId === props.id))
|
||||||
|
|
||||||
const edgeStyle = computed(() => ({
|
const edgeStyle = computed(() => {
|
||||||
stroke: hasError.value ? '#ef4444' : '#94a3b8',
|
if (hasError.value) {
|
||||||
strokeWidth: hasError.value ? 2 : 1.75,
|
return { stroke: '#ef4444', strokeWidth: 2.25 }
|
||||||
}))
|
}
|
||||||
|
const stroke =
|
||||||
|
kind.value === 'host' ? (picked.value ? '#b45309' : '#d97706')
|
||||||
|
: kind.value === 'predicate' ? (picked.value ? '#0369a1' : '#0284c7')
|
||||||
|
: picked.value ? '#64748b'
|
||||||
|
: '#94a3b8'
|
||||||
|
return { stroke, strokeWidth: picked.value ? 2.25 : 1.75 }
|
||||||
|
})
|
||||||
|
|
||||||
|
const labelClass = computed(() => {
|
||||||
|
if (hasError.value) {
|
||||||
|
return 'border-red-200 bg-red-50 text-red-600'
|
||||||
|
}
|
||||||
|
if (kind.value === 'host') {
|
||||||
|
return 'border-amber-300 bg-amber-50 text-amber-800'
|
||||||
|
}
|
||||||
|
if (kind.value === 'predicate') {
|
||||||
|
return 'border-sky-300 bg-sky-50 text-sky-800'
|
||||||
|
}
|
||||||
|
return picked.value ? 'border-slate-400 bg-white text-slate-800' : 'border-slate-200 bg-white/95 text-slate-600'
|
||||||
|
})
|
||||||
|
|
||||||
|
function selectEdge(event: MouseEvent) {
|
||||||
|
event.stopPropagation()
|
||||||
|
const edge = findEdge(props.id)
|
||||||
|
if (edge) {
|
||||||
|
addSelectedEdges([edge])
|
||||||
|
}
|
||||||
|
designer.selectEdge(props.id)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<BaseEdge :id="id" :path="route.path" :marker-end="markerEnd" :style="edgeStyle" />
|
<BaseEdge :id="id" :path="route.path" :marker-end="markerEnd" :style="edgeStyle" :interaction-width="20" />
|
||||||
<EdgeLabelRenderer>
|
<EdgeLabelRenderer>
|
||||||
<div
|
<div
|
||||||
class="nodrag nopan pointer-events-all absolute rounded-md border border-slate-200 bg-white/95 px-1.5 py-0.5 text-[10px] font-medium text-slate-600 shadow-sm"
|
class="nodrag nopan pointer-events-all absolute z-10 cursor-pointer rounded-md border px-1.5 py-0.5 text-[10px] font-medium shadow-sm"
|
||||||
:class="hasError ? 'border-red-200 text-red-600' : ''"
|
:class="labelClass"
|
||||||
|
:title="label"
|
||||||
:style="{
|
:style="{
|
||||||
transform: `translate(-50%, -50%) translate(${route.labelX}px, ${route.labelY}px)`,
|
transform: `translate(-50%, -50%) translate(${route.labelX}px, ${route.labelY}px)`,
|
||||||
|
pointerEvents: 'all',
|
||||||
}"
|
}"
|
||||||
|
@pointerdown.stop
|
||||||
|
@click.stop="selectEdge"
|
||||||
>
|
>
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ const props = defineProps<{
|
|||||||
const designer = useDesignerStore()
|
const designer = useDesignerStore()
|
||||||
const { action } = useCatalogLabels()
|
const { action } = useCatalogLabels()
|
||||||
const hasError = computed(() => designer.graphErrors.some((e) => e.nodeId === props.id))
|
const hasError = computed(() => designer.graphErrors.some((e) => e.nodeId === props.id))
|
||||||
|
const showStart = computed(
|
||||||
|
() => Boolean(props.data.isStart) && !designer.nodes.some((node) => node.id === props.id && node.parentNode),
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -22,7 +25,7 @@ const hasError = computed(() => designer.graphErrors.some((e) => e.nodeId === pr
|
|||||||
:class="
|
:class="
|
||||||
hasError
|
hasError
|
||||||
? 'border-red-300 ring-2 ring-red-100'
|
? 'border-red-300 ring-2 ring-red-100'
|
||||||
: data.isStart
|
: data.isStart && showStart
|
||||||
? 'border-amber-300 ring-2 ring-amber-100'
|
? 'border-amber-300 ring-2 ring-amber-100'
|
||||||
: 'border-slate-200 hover:shadow-[0_2px_8px_rgba(15,23,42,0.1)]'
|
: 'border-slate-200 hover:shadow-[0_2px_8px_rgba(15,23,42,0.1)]'
|
||||||
"
|
"
|
||||||
@@ -36,7 +39,7 @@ const hasError = computed(() => designer.graphErrors.some((e) => e.nodeId === pr
|
|||||||
ACTION
|
ACTION
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="data.isStart"
|
v-if="showStart"
|
||||||
class="rounded-md bg-amber-500 px-1.5 py-0.5 text-[10px] font-medium text-white"
|
class="rounded-md bg-amber-500 px-1.5 py-0.5 text-[10px] font-medium text-white"
|
||||||
>
|
>
|
||||||
START
|
START
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ const props = defineProps<{
|
|||||||
const designer = useDesignerStore()
|
const designer = useDesignerStore()
|
||||||
const { assignees } = useCatalogLabels()
|
const { assignees } = useCatalogLabels()
|
||||||
const hasError = computed(() => designer.graphErrors.some((e) => e.nodeId === props.id))
|
const hasError = computed(() => designer.graphErrors.some((e) => e.nodeId === props.id))
|
||||||
|
const showStart = computed(
|
||||||
|
() => Boolean(props.data.isStart) && !designer.nodes.some((node) => node.id === props.id && node.parentNode),
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -22,7 +25,7 @@ const hasError = computed(() => designer.graphErrors.some((e) => e.nodeId === pr
|
|||||||
:class="
|
:class="
|
||||||
hasError
|
hasError
|
||||||
? 'border-red-300 ring-2 ring-red-100'
|
? 'border-red-300 ring-2 ring-red-100'
|
||||||
: data.isStart
|
: data.isStart && showStart
|
||||||
? 'border-sky-300 ring-2 ring-sky-100'
|
? 'border-sky-300 ring-2 ring-sky-100'
|
||||||
: 'border-slate-200 hover:shadow-[0_2px_8px_rgba(15,23,42,0.1)]'
|
: 'border-slate-200 hover:shadow-[0_2px_8px_rgba(15,23,42,0.1)]'
|
||||||
"
|
"
|
||||||
@@ -36,7 +39,7 @@ const hasError = computed(() => designer.graphErrors.some((e) => e.nodeId === pr
|
|||||||
APPROVAL
|
APPROVAL
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="data.isStart"
|
v-if="showStart"
|
||||||
class="rounded-md bg-sky-500 px-1.5 py-0.5 text-[10px] font-medium text-white"
|
class="rounded-md bg-sky-500 px-1.5 py-0.5 text-[10px] font-medium text-white"
|
||||||
>
|
>
|
||||||
START
|
START
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ const hasError = computed(() => designer.graphErrors.some((e) => e.nodeId === pr
|
|||||||
<span class="min-w-0 flex-1 truncate text-[13px] font-semibold text-slate-800">{{ data.name }}</span>
|
<span class="min-w-0 flex-1 truncate text-[13px] font-semibold text-slate-800">{{ data.name }}</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="nodrag nopan inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[11px] font-medium text-violet-700 hover:bg-violet-100 disabled:opacity-40"
|
class="nodrag nopan cursor-pointer! inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[11px] font-medium text-violet-700 hover:bg-violet-100 disabled:opacity-40"
|
||||||
:disabled="readOnly"
|
:disabled="readOnly"
|
||||||
@click.stop="designer.addLane(id)"
|
@click.stop="designer.addLane(id)"
|
||||||
>
|
>
|
||||||
@@ -64,19 +64,28 @@ const hasError = computed(() => designer.graphErrors.some((e) => e.nodeId === pr
|
|||||||
<div
|
<div
|
||||||
v-for="(lane, index) in data.lanes"
|
v-for="(lane, index) in data.lanes"
|
||||||
:key="lane"
|
:key="lane"
|
||||||
class="pointer-events-none absolute right-2 left-0 flex items-start border-b border-violet-100/80"
|
class="pointer-events-none absolute flex items-start rounded-lg border border-violet-100/80"
|
||||||
:style="{ top: `${LANE_HEADER + index * LANE_H}px`, height: `${LANE_H}px` }"
|
:style="{ top: `${LANE_HEADER + index * LANE_H + 8}px`, height: `${LANE_H - 12}px`, left: '12px', right: '16px' }"
|
||||||
>
|
>
|
||||||
<div class="pointer-events-auto flex h-full flex-col justify-center gap-1 px-2" :style="{ width: `${LANE_LABEL_W}px` }">
|
<div class="pointer-events-auto flex h-full flex-col justify-center gap-1 pl-2" :style="{ width: `${LANE_LABEL_W - 12}px` }">
|
||||||
<div class="truncate text-[11px] font-semibold text-violet-800">{{ lane }}</div>
|
<div class="truncate text-[11px] font-semibold text-violet-800">{{ lane }}</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="nodrag nopan inline-flex items-center gap-0.5 text-[10px] font-medium text-slate-500 hover:text-violet-700 disabled:opacity-40"
|
class="nodrag nopan cursor-pointer! inline-flex items-center gap-0.5 text-[10px] font-medium text-sky-700 hover:text-sky-800 disabled:opacity-40"
|
||||||
:disabled="readOnly"
|
:disabled="readOnly"
|
||||||
@click.stop="designer.addLaneStep(id, lane)"
|
@click.stop="designer.addLaneStep(id, lane, 'approval')"
|
||||||
>
|
>
|
||||||
<Plus :size="11" />
|
<Plus :size="11" />
|
||||||
Step
|
Approval
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="nodrag nopan cursor-pointer! inline-flex items-center gap-0.5 text-[10px] font-medium text-amber-700 hover:text-amber-800 disabled:opacity-40"
|
||||||
|
:disabled="readOnly"
|
||||||
|
@click.stop="designer.addLaneStep(id, lane, 'action')"
|
||||||
|
>
|
||||||
|
<Plus :size="11" />
|
||||||
|
Action
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+2
-3
@@ -7,7 +7,7 @@ import type {
|
|||||||
StepDocument,
|
StepDocument,
|
||||||
} from '@/types/definition'
|
} from '@/types/definition'
|
||||||
import { defaultActionData, defaultApprovalData, defaultEndData } from '@/types/definition'
|
import { defaultActionData, defaultApprovalData, defaultEndData } from '@/types/definition'
|
||||||
import { formatWhen } from '@/mapper/when'
|
import { formatWhenKind } from '@/mapper/when'
|
||||||
import { approvalStepDocument, branchesOf, laneOf, materializeParallel } from '@/mapper/parallel'
|
import { approvalStepDocument, branchesOf, laneOf, materializeParallel } from '@/mapper/parallel'
|
||||||
|
|
||||||
export const APPROVAL_TYPE = 'approval'
|
export const APPROVAL_TYPE = 'approval'
|
||||||
@@ -40,8 +40,7 @@ export interface DesignerEdge {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function edgeLabel(data: DesignerEdgeData | undefined): string {
|
export function edgeLabel(data: DesignerEdgeData | undefined): string {
|
||||||
const when = formatWhen(data?.when)
|
return formatWhenKind(data?.when) || 'default'
|
||||||
return when || 'default'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toDocument(
|
export function toDocument(
|
||||||
|
|||||||
+68
-37
@@ -1,11 +1,28 @@
|
|||||||
import { END_TYPE, type DesignerEdge, type DesignerNode } from '@/mapper/graph'
|
import { END_TYPE, edgeLabel, type DesignerEdge, type DesignerNode } from '@/mapper/graph'
|
||||||
import { fitParallelFrames } from '@/mapper/parallel'
|
import { fitParallelFrames, reflowLanes } from '@/mapper/parallel'
|
||||||
|
|
||||||
const RANK_Y = 220
|
const RANK_GAP_Y = 96
|
||||||
const NODE_GAP_X = 320
|
const COL_GAP_X = 88
|
||||||
const ORIGIN_X = 80
|
const ORIGIN_X = 80
|
||||||
const ORIGIN_Y = 40
|
const ORIGIN_Y = 40
|
||||||
|
|
||||||
|
function boxOf(node: DesignerNode): { width: number; height: number } {
|
||||||
|
const width = Number.parseFloat(String(node.style?.width ?? ''))
|
||||||
|
const height = Number.parseFloat(String(node.style?.height ?? ''))
|
||||||
|
if (width > 0 && height > 0) {
|
||||||
|
return { width, height }
|
||||||
|
}
|
||||||
|
if (node.type === END_TYPE) {
|
||||||
|
return { width: 148, height: 52 }
|
||||||
|
}
|
||||||
|
return { width: 248, height: 140 }
|
||||||
|
}
|
||||||
|
|
||||||
|
function tidyLanes(nodes: DesignerNode[]): DesignerNode[] {
|
||||||
|
const ids = nodes.filter((node) => node.data?.kind === 'PARALLEL').map((node) => node.id)
|
||||||
|
return fitParallelFrames(ids.reduce((current, id) => reflowLanes(current, id), nodes))
|
||||||
|
}
|
||||||
|
|
||||||
function buildOutgoing(edges: DesignerEdge[]) {
|
function buildOutgoing(edges: DesignerEdge[]) {
|
||||||
const outgoing = new Map<string, DesignerEdge[]>()
|
const outgoing = new Map<string, DesignerEdge[]>()
|
||||||
for (const edge of edges) {
|
for (const edge of edges) {
|
||||||
@@ -195,11 +212,12 @@ export function assignEdgeHandles(nodes: DesignerNode[], edges: DesignerEdge[]):
|
|||||||
if (!source || !target) {
|
if (!source || !target) {
|
||||||
return {
|
return {
|
||||||
...edge,
|
...edge,
|
||||||
|
label: edgeLabel(edge.data),
|
||||||
sourceHandle: normalizeHandleId(edge.sourceHandle) ?? 'source-bottom',
|
sourceHandle: normalizeHandleId(edge.sourceHandle) ?? 'source-bottom',
|
||||||
targetHandle: normalizeHandleId(edge.targetHandle) ?? 'target-top',
|
targetHandle: normalizeHandleId(edge.targetHandle) ?? 'target-top',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { ...edge, ...pickHandlesForEdge(source, target) }
|
return { ...edge, label: edgeLabel(edge.data), ...pickHandlesForEdge(source, target) }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,28 +231,27 @@ export function applyLayeredLayout(
|
|||||||
edges: DesignerEdge[],
|
edges: DesignerEdge[],
|
||||||
startStepId: string,
|
startStepId: string,
|
||||||
): { nodes: DesignerNode[]; edges: DesignerEdge[] } {
|
): { nodes: DesignerNode[]; edges: DesignerEdge[] } {
|
||||||
const parked = nodes.filter((node) => node.parentNode)
|
const tidy = tidyLanes(nodes)
|
||||||
if (parked.length > 0) {
|
const parked = tidy.filter((node) => node.parentNode)
|
||||||
const top = nodes.filter((node) => !node.parentNode)
|
const top = tidy.filter((node) => !node.parentNode)
|
||||||
const topIds = new Set(top.map((node) => node.id))
|
if (top.length === 0) {
|
||||||
|
return { nodes: tidy, edges: assignEdgeHandles(tidy, edges) }
|
||||||
|
}
|
||||||
|
|
||||||
|
const byId = new Map(top.map((n) => [n.id, n]))
|
||||||
|
const nodeIds = top.map((n) => n.id)
|
||||||
|
const topIds = new Set(nodeIds)
|
||||||
const topEdges = edges.filter((edge) => topIds.has(edge.source) && topIds.has(edge.target))
|
const topEdges = edges.filter((edge) => topIds.has(edge.source) && topIds.has(edge.target))
|
||||||
const laid = applyLayeredLayout(top, topEdges, startStepId)
|
const outgoing = buildOutgoing(topEdges)
|
||||||
const merged = fitParallelFrames([...laid.nodes, ...parked])
|
const startNode = tidy.find((node) => node.id === startStepId)
|
||||||
return { nodes: merged, edges: assignEdgeHandles(merged, edges) }
|
const start = startNode?.parentNode
|
||||||
}
|
? startNode.parentNode
|
||||||
if (nodes.length === 0) {
|
: byId.has(startStepId)
|
||||||
return { nodes, edges }
|
|
||||||
}
|
|
||||||
|
|
||||||
const byId = new Map(nodes.map((n) => [n.id, n]))
|
|
||||||
const nodeIds = nodes.map((n) => n.id)
|
|
||||||
const outgoing = buildOutgoing(edges)
|
|
||||||
const start = byId.has(startStepId)
|
|
||||||
? startStepId
|
? startStepId
|
||||||
: (nodes.find((n) => n.type !== END_TYPE)?.id ?? nodes[0]?.id)
|
: (top.find((n) => n.type !== END_TYPE)?.id ?? top[0]?.id)
|
||||||
|
|
||||||
const backEdgeIds = findBackEdgeIds(nodeIds, edges, start)
|
const backEdgeIds = findBackEdgeIds(nodeIds, topEdges, start)
|
||||||
const rank = computeRanks(nodeIds, edges, backEdgeIds)
|
const rank = computeRanks(nodeIds, topEdges, backEdgeIds)
|
||||||
if (start && (rank.get(start) ?? 0) > 0) {
|
if (start && (rank.get(start) ?? 0) > 0) {
|
||||||
const shift = rank.get(start) ?? 0
|
const shift = rank.get(start) ?? 0
|
||||||
for (const id of nodeIds) {
|
for (const id of nodeIds) {
|
||||||
@@ -243,23 +260,37 @@ export function applyLayeredLayout(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const col = assignColumns(nodeIds, start, rank, outgoing, backEdgeIds)
|
const col = assignColumns(nodeIds, start, rank, outgoing, backEdgeIds)
|
||||||
|
const ranks = [...new Set(nodeIds.map((id) => rank.get(id) ?? 0))].sort((a, b) => a - b)
|
||||||
|
const cols = [...new Set(nodeIds.map((id) => col.get(id) ?? 0))].sort((a, b) => a - b)
|
||||||
|
const rankY = new Map<number, number>()
|
||||||
|
let y = ORIGIN_Y
|
||||||
|
for (const r of ranks) {
|
||||||
|
rankY.set(r, y)
|
||||||
|
const height = Math.max(
|
||||||
|
...nodeIds.filter((id) => (rank.get(id) ?? 0) === r).map((id) => boxOf(byId.get(id)!).height),
|
||||||
|
)
|
||||||
|
y += height + RANK_GAP_Y
|
||||||
|
}
|
||||||
|
const colX = new Map<number, number>()
|
||||||
|
let x = ORIGIN_X
|
||||||
|
for (const c of cols) {
|
||||||
|
colX.set(c, x)
|
||||||
|
const width = Math.max(
|
||||||
|
...nodeIds.filter((id) => (col.get(id) ?? 0) === c).map((id) => boxOf(byId.get(id)!).width),
|
||||||
|
)
|
||||||
|
x += width + COL_GAP_X
|
||||||
|
}
|
||||||
|
|
||||||
const laidOut = nodes.map((node) => {
|
const laidOut = top.map((node) => ({
|
||||||
const r = rank.get(node.id) ?? 0
|
|
||||||
const c = col.get(node.id) ?? 0
|
|
||||||
return {
|
|
||||||
...node,
|
...node,
|
||||||
position: {
|
position: {
|
||||||
x: ORIGIN_X + c * NODE_GAP_X,
|
x: colX.get(col.get(node.id) ?? 0) ?? ORIGIN_X,
|
||||||
y: ORIGIN_Y + r * RANK_Y,
|
y: rankY.get(rank.get(node.id) ?? 0) ?? ORIGIN_Y,
|
||||||
},
|
},
|
||||||
}
|
}))
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
const merged = [...laidOut, ...parked]
|
||||||
nodes: laidOut,
|
return { nodes: merged, edges: assignEdgeHandles(merged, edges) }
|
||||||
edges: assignEdgeHandles(laidOut, edges),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { RANK_Y, NODE_GAP_X }
|
export { RANK_GAP_Y as RANK_Y, COL_GAP_X as NODE_GAP_X }
|
||||||
|
|||||||
+55
-14
@@ -8,7 +8,7 @@ import type {
|
|||||||
TransitionDocument,
|
TransitionDocument,
|
||||||
} from '@/types/definition'
|
} from '@/types/definition'
|
||||||
import { defaultActionData, defaultApprovalData } from '@/types/definition'
|
import { defaultActionData, defaultApprovalData } from '@/types/definition'
|
||||||
import { formatWhen } from '@/mapper/when'
|
import { formatWhenKind } from '@/mapper/when'
|
||||||
import type { RoutingWhen } from '@/mapper/when'
|
import type { RoutingWhen } from '@/mapper/when'
|
||||||
|
|
||||||
export interface CanvasNode {
|
export interface CanvasNode {
|
||||||
@@ -31,10 +31,14 @@ export interface CanvasEdge {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const LANE_HEADER = 44
|
export const LANE_HEADER = 44
|
||||||
export const LANE_H = 128
|
export const LANE_H = 176
|
||||||
export const LANE_LABEL_W = 76
|
export const LANE_LABEL_W = 88
|
||||||
|
export const LANE_INSET_X = 28
|
||||||
|
export const LANE_INSET_Y = 28
|
||||||
|
export const LANE_PAD_RIGHT = 40
|
||||||
|
export const LANE_PAD_BOTTOM = 20
|
||||||
export const LANE_STEP_W = 220
|
export const LANE_STEP_W = 220
|
||||||
export const LANE_STEP_GAP = 28
|
export const LANE_STEP_GAP = 72
|
||||||
|
|
||||||
export function laneOf(data: DesignerNodeData | undefined): string | undefined {
|
export function laneOf(data: DesignerNodeData | undefined): string | undefined {
|
||||||
if (!data || (data.kind !== 'APPROVAL' && data.kind !== 'ACTION')) {
|
if (!data || (data.kind !== 'APPROVAL' && data.kind !== 'ACTION')) {
|
||||||
@@ -45,15 +49,15 @@ export function laneOf(data: DesignerNodeData | undefined): string | undefined {
|
|||||||
|
|
||||||
export function parallelSize(laneCount: number, maxSteps: number): { width: number; height: number } {
|
export function parallelSize(laneCount: number, maxSteps: number): { width: number; height: number } {
|
||||||
return {
|
return {
|
||||||
width: LANE_LABEL_W + 16 + Math.max(1, maxSteps) * (LANE_STEP_W + LANE_STEP_GAP) + 12,
|
width: LANE_LABEL_W + LANE_INSET_X + Math.max(1, maxSteps) * (LANE_STEP_W + LANE_STEP_GAP) - LANE_STEP_GAP + LANE_PAD_RIGHT,
|
||||||
height: LANE_HEADER + Math.max(1, laneCount) * LANE_H + 10,
|
height: LANE_HEADER + Math.max(1, laneCount) * LANE_H + LANE_PAD_BOTTOM,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function laneStepPosition(laneIndex: number, stepIndex: number): { x: number; y: number } {
|
export function laneStepPosition(laneIndex: number, stepIndex: number): { x: number; y: number } {
|
||||||
return {
|
return {
|
||||||
x: LANE_LABEL_W + 8 + stepIndex * (LANE_STEP_W + LANE_STEP_GAP),
|
x: LANE_LABEL_W + LANE_INSET_X + stepIndex * (LANE_STEP_W + LANE_STEP_GAP),
|
||||||
y: LANE_HEADER + laneIndex * LANE_H + 16,
|
y: LANE_HEADER + laneIndex * LANE_H + LANE_INSET_Y,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,10 +70,11 @@ export function fitParallelFrames(nodes: CanvasNode[]): CanvasNode[] {
|
|||||||
const children = nodes.filter((child) => child.parentNode === node.id)
|
const children = nodes.filter((child) => child.parentNode === node.id)
|
||||||
const maxSteps = Math.max(1, ...lanes.map((lane) => children.filter((child) => laneOf(child.data) === lane).length))
|
const maxSteps = Math.max(1, ...lanes.map((lane) => children.filter((child) => laneOf(child.data) === lane).length))
|
||||||
const size = parallelSize(lanes.length, maxSteps)
|
const size = parallelSize(lanes.length, maxSteps)
|
||||||
|
const right = children.reduce((max, child) => Math.max(max, child.position.x + LANE_STEP_W), 0)
|
||||||
return {
|
return {
|
||||||
...node,
|
...node,
|
||||||
zIndex: 0,
|
zIndex: 0,
|
||||||
style: { width: `${size.width}px`, height: `${size.height}px` },
|
style: { width: `${Math.max(size.width, right + LANE_PAD_RIGHT)}px`, height: `${size.height}px` },
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -101,12 +106,34 @@ export function settleLaneNodes(nodes: CanvasNode[]): CanvasNode[] {
|
|||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
data: { ...data, laneId: lane },
|
data: { ...data, laneId: lane },
|
||||||
position: {
|
position: {
|
||||||
x: Math.max(LANE_LABEL_W, node.position.x),
|
x: Math.max(LANE_LABEL_W + LANE_INSET_X, node.position.x),
|
||||||
y: LANE_HEADER + laneIndex * LANE_H + 16,
|
y: LANE_HEADER + laneIndex * LANE_H + LANE_INSET_Y,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return fitParallelFrames(next)
|
const spaced = new Map<string, { x: number; y: number }>()
|
||||||
|
for (const parent of next) {
|
||||||
|
if (parent.data?.kind !== 'PARALLEL') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for (const lane of parent.data.lanes) {
|
||||||
|
const steps = next
|
||||||
|
.filter((node) => node.parentNode === parent.id && laneOf(node.data) === lane)
|
||||||
|
.sort((a, b) => a.position.x - b.position.x || a.id.localeCompare(b.id))
|
||||||
|
let minX = LANE_LABEL_W + LANE_INSET_X
|
||||||
|
for (const step of steps) {
|
||||||
|
const x = Math.max(minX, step.position.x)
|
||||||
|
spaced.set(step.id, { x, y: step.position.y })
|
||||||
|
minX = x + LANE_STEP_W + LANE_STEP_GAP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fitParallelFrames(
|
||||||
|
next.map((node) => {
|
||||||
|
const position = spaced.get(node.id)
|
||||||
|
return position ? { ...node, position } : node
|
||||||
|
}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function approvalStepDocument(data: ApprovalNodeData, id: string): StepDocument {
|
export function approvalStepDocument(data: ApprovalNodeData, id: string): StepDocument {
|
||||||
@@ -171,7 +198,21 @@ export function spawnLaneStep(
|
|||||||
laneIndex: number,
|
laneIndex: number,
|
||||||
stepIndex: number,
|
stepIndex: number,
|
||||||
id: string,
|
id: string,
|
||||||
|
kind: 'approval' | 'action' = 'approval',
|
||||||
): CanvasNode {
|
): CanvasNode {
|
||||||
|
const position = laneStepPosition(laneIndex, stepIndex)
|
||||||
|
if (kind === 'action') {
|
||||||
|
const data: ActionNodeData = { ...defaultActionData(), name: 'Action', laneId }
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
type: 'action',
|
||||||
|
parentNode: parallelId,
|
||||||
|
extent: 'parent',
|
||||||
|
zIndex: 1,
|
||||||
|
position,
|
||||||
|
data,
|
||||||
|
}
|
||||||
|
}
|
||||||
const data: ApprovalNodeData = { ...defaultApprovalData(), name: 'Approval', laneId }
|
const data: ApprovalNodeData = { ...defaultApprovalData(), name: 'Approval', laneId }
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
@@ -179,7 +220,7 @@ export function spawnLaneStep(
|
|||||||
parentNode: parallelId,
|
parentNode: parallelId,
|
||||||
extent: 'parent',
|
extent: 'parent',
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
position: laneStepPosition(laneIndex, stepIndex),
|
position,
|
||||||
data,
|
data,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -218,7 +259,7 @@ export function materializeParallel(
|
|||||||
id: `e-${id}-${branch.id}-${index}`,
|
id: `e-${id}-${branch.id}-${index}`,
|
||||||
source: transition.from,
|
source: transition.from,
|
||||||
target: transition.to,
|
target: transition.to,
|
||||||
label: formatWhen(transition.when) || 'default',
|
label: formatWhenKind(transition.when) || 'default',
|
||||||
data,
|
data,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -19,6 +19,16 @@ export function isRefWhen(when: RoutingWhen | undefined): when is { ref: string;
|
|||||||
return Boolean(when && typeof when === 'object' && 'ref' in when)
|
return Boolean(when && typeof when === 'object' && 'ref' in when)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatWhenKind(when: RoutingWhen | undefined): string {
|
||||||
|
if (!when) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
if (isRefWhen(when)) {
|
||||||
|
return 'host'
|
||||||
|
}
|
||||||
|
return 'predicate'
|
||||||
|
}
|
||||||
|
|
||||||
export function formatWhen(when: RoutingWhen | undefined): string {
|
export function formatWhen(when: RoutingWhen | undefined): string {
|
||||||
if (!when) {
|
if (!when) {
|
||||||
return ''
|
return ''
|
||||||
|
|||||||
+11
-3
@@ -173,13 +173,18 @@ export const useDesignerStore = defineStore('designer', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function syncStartFlags() {
|
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) => {
|
nodes.value = nodes.value.map((node) => {
|
||||||
if (!node.data || node.data.kind === 'END') {
|
if (!node.data || node.data.kind === 'END') {
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
const isStart = !node.parentNode && node.id === startStepId.value
|
||||||
return {
|
return {
|
||||||
...node,
|
...node,
|
||||||
data: { ...node.data, isStart: node.id === startStepId.value },
|
data: { ...node.data, isStart },
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -694,7 +699,7 @@ export const useDesignerStore = defineStore('designer', () => {
|
|||||||
edges.value = edges.value.filter((edge) => !drop.has(edge.source) && !drop.has(edge.target))
|
edges.value = edges.value.filter((edge) => !drop.has(edge.source) && !drop.has(edge.target))
|
||||||
}
|
}
|
||||||
|
|
||||||
function addLaneStep(parallelId: string, lane: string) {
|
function addLaneStep(parallelId: string, lane: string, kind: 'approval' | 'action' = 'approval') {
|
||||||
const parent = nodes.value.find((node) => node.id === parallelId)
|
const parent = nodes.value.find((node) => node.id === parallelId)
|
||||||
if (!parent || parent.data.kind !== 'PARALLEL' || !parent.data.lanes.includes(lane)) {
|
if (!parent || parent.data.kind !== 'PARALLEL' || !parent.data.lanes.includes(lane)) {
|
||||||
return
|
return
|
||||||
@@ -702,7 +707,10 @@ export const useDesignerStore = defineStore('designer', () => {
|
|||||||
pushHistory()
|
pushHistory()
|
||||||
const laneIndex = parent.data.lanes.indexOf(lane)
|
const laneIndex = parent.data.lanes.indexOf(lane)
|
||||||
const stepIndex = nodes.value.filter((node) => node.parentNode === parallelId && laneOf(node.data) === lane).length
|
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))])
|
nodes.value = fitParallelFrames([
|
||||||
|
...nodes.value,
|
||||||
|
spawnLaneStep(parallelId, lane, laneIndex, stepIndex, nextId(lane), kind),
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
function renameLane(parallelId: string, from: string, to: string): boolean {
|
function renameLane(parallelId: string, from: string, to: string): boolean {
|
||||||
|
|||||||
@@ -25,6 +25,14 @@ body,
|
|||||||
transform: scale(1.25);
|
transform: scale(1.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vue-flow__node-parallel button {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__edge-labels {
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
.pointer-events-all {
|
.pointer-events-all {
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user