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 + +
+ +
@@ -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