From 717b9b711299aa89233e35c376eddcea4792aacd Mon Sep 17 00:00:00 2001
From: Remi <73385395+itsrubberduck@users.noreply.github.com>
Date: Wed, 24 Sep 2025 00:00:47 +0200
Subject: [PATCH] feat(editor): manage flow properties in inspector
---
app/pages/editor/index.vue | 360 ++++++++++++++++++++++++++++++++++++-
1 file changed, 354 insertions(+), 6 deletions(-)
diff --git a/app/pages/editor/index.vue b/app/pages/editor/index.vue
index 651f823..0888412 100644
--- a/app/pages/editor/index.vue
+++ b/app/pages/editor/index.vue
@@ -365,7 +365,29 @@
v-if="inspectorOpen && flowDetail"
class="w-[380px] shrink-0 overflow-y-auto border-l border-white/10 bg-[#0b1224]/85 backdrop-blur fixed right-0 bottom-0 top-12"
>
-
+
+
+ Nodes
+ Flow
+
+
+ Flow speichern
+
+
+
+
-
- Wähle einen Node auf der Canvas aus.
-
+
+
+ Wähle einen Node auf der Canvas aus.
+
+
+
+
+
+
+
+
{{ flowDetail.flow.slug }}
+
+ {{ flowForm.name || 'Unbenannter Flow' }}
+
+
+ Start: {{ flowForm.startState || '–' }}
+ Nodes: {{ flowDetail.nodes.length }}
+
+
+
+
+ {{ flowForm.entryMode === 'linear' ? 'Einzeln' : 'Parallel' }}
+
+
+ Hauptflow
+
+
+
+
+
+
+
+
+
+
+
+
{{ currentFlowModeDescription }}
+
+
+
+
+
+
+
+
+
+
+
+ Erweiterte Einstellungen
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -961,6 +1151,7 @@ import { useApi } from '~/composables/useApi'
import { useAuthStore } from '~/stores/auth'
import DecisionNodeCanvas from '~/components/editor/DecisionNodeCanvas.vue'
import type {
+ DecisionFlowEntryMode,
DecisionFlowModel,
DecisionFlowSummary,
DecisionNodeCondition,
@@ -1003,6 +1194,8 @@ interface FlowFormState {
name: string
description: string
schemaVersion: string
+ entryMode: DecisionFlowEntryMode
+ isMain: boolean
startState: string
endStates: string[]
roles: string[]
@@ -1075,6 +1268,7 @@ const autosaveIndicator = ref(false)
const canvasComponent = ref | null>(null)
const inspectorOpen = ref(false)
+const inspectorMode = ref<'node' | 'flow'>('node')
const selectedFlowSlug = ref(null)
const flowDetail = ref<{ flow: DecisionFlowModel; nodes: DecisionNodeModel[] } | null>(null)
@@ -1087,6 +1281,8 @@ const flowForm = reactive({
name: '',
description: '',
schemaVersion: '1.0',
+ entryMode: 'parallel',
+ isMain: false,
startState: '',
endStates: [],
roles: ['pilot', 'atc', 'system'],
@@ -1249,6 +1445,61 @@ const nodeSelectorItems = computed(() => {
.map(({ matchesSearch, matchesRole, matchesPhase, matchesAuto, ...rest }) => rest)
})
+const flowModeOptions: Array<{ value: DecisionFlowEntryMode; title: string; subtitle: string }> = [
+ { value: 'parallel', title: 'Parallel', subtitle: 'Mehrere Branches können gleichzeitig laufen' },
+ { value: 'linear', title: 'Einzeln', subtitle: 'Es läuft immer nur ein Branch' },
+]
+
+const flowNodeOptions = computed(() => {
+ if (!flowDetail.value) return []
+ const items = flowDetail.value.nodes
+ .slice()
+ .sort((a, b) => a.stateId.localeCompare(b.stateId))
+ .map((node) => ({
+ value: node.stateId,
+ title: node.title ? `${node.stateId} — ${node.title}` : node.stateId,
+ }))
+
+ const known = new Set(items.map((item) => item.value))
+ const extras = [flowForm.startState, ...flowForm.endStates]
+ .map((state) => (typeof state === 'string' ? state.trim() : ''))
+ .filter((state): state is string => Boolean(state && !known.has(state)))
+
+ extras.forEach((state) => {
+ items.push({ value: state, title: `${state} (nicht im Flow)` })
+ known.add(state)
+ })
+
+ return items
+})
+
+const flowRoleOptions = computed(() => {
+ const roles = new Set(roleOptions)
+ flowForm.roles.forEach((role) => {
+ const trimmed = typeof role === 'string' ? role.trim() : ''
+ if (trimmed) roles.add(trimmed)
+ })
+ return Array.from(roles)
+})
+
+const flowPhaseOptions = computed(() => {
+ const phases = new Set()
+ flowForm.phases.forEach((phase) => {
+ const trimmed = typeof phase === 'string' ? phase.trim() : ''
+ if (trimmed) phases.add(trimmed)
+ })
+ flowDetail.value?.nodes.forEach((node) => {
+ const trimmed = typeof node.phase === 'string' ? node.phase.trim() : ''
+ if (trimmed) phases.add(trimmed)
+ })
+ return Array.from(phases)
+})
+
+const currentFlowModeDescription = computed(() => {
+ const option = flowModeOptions.find((entry) => entry.value === flowForm.entryMode)
+ return option?.subtitle ?? ''
+})
+
const canvasNodes = computed(() => {
if (!flowDetail.value) return []
const flow = flowDetail.value.flow
@@ -1353,6 +1604,7 @@ watch(
inspectorOpen,
(open) => {
if (!open || !flowDetail.value) return
+ if (inspectorMode.value !== 'node') return
if (selectedNodeId.value) return
const preferred =
flowDetail.value.flow.startState &&
@@ -1397,7 +1649,7 @@ watch(
() => flowForm.endStates,
(states) => {
if (!flowDetail.value) return
- flowDetail.value.flow.endStates = [...states]
+ flowDetail.value.flow.endStates = Array.from(new Set(states))
if (!flowInitializing) {
scheduleFlowSave()
}
@@ -1405,6 +1657,70 @@ watch(
{ deep: true }
)
+watch(
+ () => flowForm.name,
+ (name) => {
+ if (!flowDetail.value || flowInitializing) return
+ flowDetail.value.flow.name = name
+ }
+)
+
+watch(
+ () => flowForm.description,
+ (description) => {
+ if (!flowDetail.value || flowInitializing) return
+ flowDetail.value.flow.description = description || ''
+ }
+)
+
+watch(
+ () => flowForm.schemaVersion,
+ (version) => {
+ if (!flowDetail.value || flowInitializing) return
+ flowDetail.value.flow.schemaVersion = version
+ }
+)
+
+watch(
+ () => flowForm.entryMode,
+ (mode) => {
+ if (!flowDetail.value || flowInitializing) return
+ flowDetail.value.flow.entryMode = mode
+ }
+)
+
+watch(
+ () => flowForm.isMain,
+ (isMain) => {
+ if (!flowDetail.value || flowInitializing) return
+ flowDetail.value.flow.isMain = isMain
+ }
+)
+
+watch(
+ () => flowForm.roles,
+ (roles) => {
+ if (!flowDetail.value || flowInitializing) return
+ const sanitized = roles
+ .map((role) => (typeof role === 'string' ? role.trim() : ''))
+ .filter((role): role is string => Boolean(role))
+ flowDetail.value.flow.roles = Array.from(new Set(sanitized))
+ },
+ { deep: true }
+)
+
+watch(
+ () => flowForm.phases,
+ (phases) => {
+ if (!flowDetail.value || flowInitializing) return
+ const sanitized = phases
+ .map((phase) => (typeof phase === 'string' ? phase.trim() : ''))
+ .filter((phase): phase is string => Boolean(phase))
+ flowDetail.value.flow.phases = Array.from(new Set(sanitized))
+ },
+ { deep: true }
+)
+
watch(
flowForm,
() => {
@@ -1641,6 +1957,8 @@ function populateFlowForm(flow: DecisionFlowModel) {
flowForm.name = flow.name
flowForm.description = flow.description ?? ''
flowForm.schemaVersion = flow.schemaVersion ?? '1.0'
+ flowForm.entryMode = flow.entryMode || 'parallel'
+ flowForm.isMain = Boolean(flow.isMain)
flowForm.startState = flow.startState
flowForm.endStates = Array.isArray(flow.endStates) ? [...new Set(flow.endStates)] : []
flowForm.roles = flow.roles?.length ? [...new Set(flow.roles)] : ['pilot', 'atc', 'system']
@@ -1762,6 +2080,7 @@ function selectNode(stateId: string | null, options: { focus?: boolean } = {}) {
selectedNodeId.value = null
return
}
+ inspectorMode.value = 'node'
inspectorOpen.value = true
if (selectedNodeId.value !== stateId) {
selectedNodeId.value = stateId
@@ -1806,10 +2125,14 @@ async function persistFlow(options: { silent?: boolean } = {}) {
flowSaveLoading.value = true
let payload: Record
try {
+ const entryMode = flowForm.entryMode === 'linear' ? 'linear' : 'parallel'
+ const isMain = Boolean(flowForm.isMain)
payload = {
name: flowForm.name.trim(),
description: flowForm.description,
schemaVersion: flowForm.schemaVersion.trim(),
+ entryMode,
+ isMain,
startState: flowForm.startState.trim(),
endStates: flowForm.endStates,
roles: flowForm.roles,
@@ -1826,6 +2149,8 @@ async function persistFlow(options: { silent?: boolean } = {}) {
}
flowForm.name = payload.name
flowForm.schemaVersion = payload.schemaVersion
+ flowForm.entryMode = payload.entryMode
+ flowForm.isMain = payload.isMain
flowForm.startState = payload.startState
} catch (error: any) {
flowSaveLoading.value = false
@@ -1848,8 +2173,25 @@ async function persistFlow(options: { silent?: boolean } = {}) {
flowDetail.value.nodes = updated.nodes
}
}
+ flowInitializing = true
+ flowForm.name = updated.flow.name
+ flowForm.description = updated.flow.description ?? ''
+ flowForm.schemaVersion = updated.flow.schemaVersion ?? '1.0'
+ flowForm.entryMode = updated.flow.entryMode || 'parallel'
+ flowForm.isMain = Boolean(updated.flow.isMain)
+ flowForm.startState = updated.flow.startState
+ flowForm.endStates = Array.isArray(updated.flow.endStates)
+ ? [...new Set(updated.flow.endStates)]
+ : []
+ flowForm.roles = updated.flow.roles?.length
+ ? [...new Set(updated.flow.roles)]
+ : ['pilot', 'atc', 'system']
+ flowForm.phases = updated.flow.phases?.length ? [...new Set(updated.flow.phases)] : []
flowSnapshot.value = cloneNode(flowForm)
lastFlowAutosaveError = ''
+ nextTick(() => {
+ flowInitializing = false
+ })
if (silent) {
const summaryIndex = flows.value.findIndex((flow) => flow.slug === updated.flow.slug)
@@ -1861,6 +2203,8 @@ async function persistFlow(options: { silent?: boolean } = {}) {
description: updated.flow.description,
startState: updated.flow.startState,
nodeCount: updated.nodes?.length ?? previous.nodeCount,
+ entryMode: updated.flow.entryMode,
+ isMain: updated.flow.isMain,
})
}
flashAutosaveIndicator()
@@ -1880,6 +2224,10 @@ async function persistFlow(options: { silent?: boolean } = {}) {
}
}
+function persistFlowNow() {
+ void persistFlow({ silent: false })
+}
+
async function persistNode(options: { silent?: boolean } = {}) {
if (!flowDetail.value || !nodeForm.value) return
const silent = options.silent ?? false