mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-26 00:01:37 +00:00
fix(frontend): Fix adding nodes to decision tree (#3107)
* fix(frontend): wip * fix(frontend): fix decision tree + add missing outputs * fix(frontend): remove console.logs * feat(frontend): add fallback to go previous when we have no history (manual selection) * feat(frontend): add a context to keep track of when a decision tree is being debugged * fix(frontend): fix nameoverride * fix(frontend): getFirstNode to make sure we correclty select the head
This commit is contained in:
@@ -10,6 +10,9 @@
|
||||
import type { DecisionTreeNode } from '../../editor/component'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import { ArrowLeft, ArrowRight } from 'lucide-svelte'
|
||||
import { initOutput } from '../../editor/appUtils'
|
||||
import Badge from '$lib/components/common/badge/Badge.svelte'
|
||||
import { getFirstNode, isDebugging } from '../../editor/settingsPanel/decisionTree/utils'
|
||||
|
||||
export let id: string
|
||||
export let componentContainerHeight: number
|
||||
@@ -17,12 +20,24 @@
|
||||
export let render: boolean
|
||||
export let nodes: DecisionTreeNode[]
|
||||
|
||||
const { app, focusedGrid, selectedComponent, connectingInput, componentControl } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
const {
|
||||
app,
|
||||
focusedGrid,
|
||||
selectedComponent,
|
||||
connectingInput,
|
||||
componentControl,
|
||||
worldStore,
|
||||
debuggingComponents
|
||||
} = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
let css = initCss($app.css?.conditionalwrapper, customCss)
|
||||
let selectedConditionIndex = 0
|
||||
let currentNodeId = nodes[0].id
|
||||
let currentNodeId = getFirstNode(nodes)?.id ?? ''
|
||||
|
||||
let outputs = initOutput($worldStore, id, {
|
||||
currentNodeId,
|
||||
currentNodeIndex: selectedConditionIndex
|
||||
})
|
||||
|
||||
$: resolvedConditions = nodes.reduce((acc, node) => {
|
||||
acc[node.id] = acc[node.id] || []
|
||||
@@ -35,7 +50,11 @@
|
||||
}, resolvedNext || {})
|
||||
|
||||
$: if (!nodes.map((n) => n.id).includes(currentNodeId)) {
|
||||
currentNodeId = nodes[0].id
|
||||
const firstNode = getFirstNode(nodes)?.id
|
||||
|
||||
if (firstNode) {
|
||||
currentNodeId = firstNode
|
||||
}
|
||||
}
|
||||
|
||||
$: lastNodeId = nodes?.find((node) => node.next.length === 0)?.id
|
||||
@@ -81,15 +100,35 @@
|
||||
parentComponentId: id,
|
||||
subGridIndex: selectedConditionIndex
|
||||
}
|
||||
} else {
|
||||
// if no history, go to first node
|
||||
|
||||
const node = getFirstNode(nodes)
|
||||
|
||||
if (node) {
|
||||
currentNodeId = node.id
|
||||
|
||||
selectedConditionIndex = nodes.findIndex((next) => next.id == currentNodeId)
|
||||
|
||||
$focusedGrid = {
|
||||
parentComponentId: id,
|
||||
subGridIndex: selectedConditionIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onFocus(newIndex: number) {
|
||||
selectedConditionIndex = newIndex
|
||||
currentNodeId = nodes[selectedConditionIndex].id
|
||||
$focusedGrid = {
|
||||
parentComponentId: id,
|
||||
subGridIndex: selectedConditionIndex
|
||||
|
||||
const nodeId = nodes[newIndex]?.id
|
||||
|
||||
if (nodeId) {
|
||||
currentNodeId = nodeId
|
||||
$focusedGrid = {
|
||||
parentComponentId: id,
|
||||
subGridIndex: selectedConditionIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,6 +140,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: if (currentNodeId) {
|
||||
outputs.currentNodeId.set(currentNodeId)
|
||||
outputs.currentNodeIndex.set(nodes.findIndex((next) => next.id == currentNodeId))
|
||||
}
|
||||
|
||||
$: if ($selectedComponent?.[0] === id && !$focusedGrid) {
|
||||
$focusedGrid = {
|
||||
parentComponentId: id,
|
||||
@@ -169,15 +213,30 @@
|
||||
</div>
|
||||
|
||||
<div class="h-8 flex flex-row gap-2 justify-end items-center px-2 bg-surface-primary z-50">
|
||||
{#if nodes[0].id !== currentNodeId}
|
||||
<Button on:click={prev} size="xs2" color="light" startIcon={{ icon: ArrowLeft }}>Prev</Button>
|
||||
{#if isDebugging($debuggingComponents, id)}
|
||||
<Badge color="red" size="xs2">
|
||||
{`Debugging. Actions are disabled.`}
|
||||
</Badge>
|
||||
{/if}
|
||||
{#if getFirstNode(nodes)?.id !== currentNodeId}
|
||||
<Button
|
||||
on:click={prev}
|
||||
size="xs2"
|
||||
color="light"
|
||||
startIcon={{ icon: ArrowLeft }}
|
||||
disabled={isDebugging($debuggingComponents, id)}
|
||||
>
|
||||
Prev
|
||||
</Button>
|
||||
{/if}
|
||||
<Button
|
||||
on:click={next}
|
||||
size="xs2"
|
||||
color="dark"
|
||||
endIcon={{ icon: ArrowRight }}
|
||||
disabled={isNextDisabled || currentNodeId === lastNodeId}
|
||||
disabled={isNextDisabled ||
|
||||
currentNodeId === lastNodeId ||
|
||||
isDebugging($debuggingComponents, id)}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
|
||||
@@ -153,7 +153,8 @@
|
||||
allIdsInPath: writable([]),
|
||||
darkMode,
|
||||
cssEditorOpen,
|
||||
previewTheme
|
||||
previewTheme,
|
||||
debuggingComponents: writable({})
|
||||
})
|
||||
|
||||
let scale = writable(100)
|
||||
|
||||
@@ -102,7 +102,8 @@
|
||||
allIdsInPath,
|
||||
darkMode,
|
||||
cssEditorOpen: writable(false),
|
||||
previewTheme: writable(undefined)
|
||||
previewTheme: writable(undefined),
|
||||
debuggingComponents: writable({})
|
||||
})
|
||||
|
||||
let previousSelectedIds: string[] | undefined = undefined
|
||||
|
||||
@@ -5,22 +5,43 @@
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import type { AppViewerContext } from '../types'
|
||||
import type { DecisionTreeNode } from './component'
|
||||
import { isDebugging } from './settingsPanel/decisionTree/utils'
|
||||
import { X } from 'lucide-svelte'
|
||||
|
||||
export let nodes: DecisionTreeNode[] = []
|
||||
export let id: string
|
||||
|
||||
const { componentControl } = getContext<AppViewerContext>('AppViewerContext')
|
||||
const { componentControl, debuggingComponents, worldStore } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let isManuallySelected: boolean = false
|
||||
let selected: number | null = null
|
||||
let currentNodeId: string = ''
|
||||
|
||||
$worldStore.outputsById[id]?.currentNodeId?.subscribe(
|
||||
{
|
||||
id: id,
|
||||
next: (value) => {
|
||||
currentNodeId = value
|
||||
}
|
||||
},
|
||||
currentNodeId
|
||||
)
|
||||
|
||||
$: if (nodes[$debuggingComponents[id] ?? 0]?.id === undefined) {
|
||||
currentNodeId = ''
|
||||
$componentControl?.[id]?.setTab?.(0)
|
||||
|
||||
$debuggingComponents = Object.fromEntries(
|
||||
Object.entries($debuggingComponents).filter(([key]) => key !== id)
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
title={'Debug tabs'}
|
||||
class={classNames(
|
||||
'text-2xs py-0.5 font-bold w-fit border cursor-pointer rounded-sm',
|
||||
isManuallySelected
|
||||
isDebugging($debuggingComponents, id)
|
||||
? 'bg-red-100 text-red-600 border-red-500 hover:bg-red-200 hover:text-red-800'
|
||||
: 'bg-indigo-100 text-indigo-600 border-indigo-500 hover:bg-indigo-200 hover:text-indigo-800'
|
||||
)}
|
||||
@@ -30,12 +51,23 @@
|
||||
<ButtonDropdown hasPadding={false}>
|
||||
<svelte:fragment slot="buttonReplacement">
|
||||
<div class="px-1">
|
||||
{#if isManuallySelected}
|
||||
<div>
|
||||
{`Debugging node ${nodes[selected ?? 0].id}`}
|
||||
{#if isDebugging($debuggingComponents, id)}
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
{`Debugging node ${nodes[$debuggingComponents[id] ?? 0]?.id}`}
|
||||
<button
|
||||
on:click={() => {
|
||||
$componentControl?.[id]?.setTab?.(0)
|
||||
|
||||
$debuggingComponents = Object.fromEntries(
|
||||
Object.entries($debuggingComponents).filter(([key]) => key !== id)
|
||||
)
|
||||
}}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
{`Debug nodes`}
|
||||
{`Debug nodes (current node: ${currentNodeId})`}
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
@@ -44,8 +76,8 @@
|
||||
<MenuItem
|
||||
on:click={() => {
|
||||
$componentControl?.[id]?.setTab?.(index)
|
||||
selected = index
|
||||
isManuallySelected = true
|
||||
|
||||
$debuggingComponents[id] = index
|
||||
}}
|
||||
>
|
||||
<div
|
||||
@@ -59,8 +91,11 @@
|
||||
{/each}
|
||||
<MenuItem
|
||||
on:click={() => {
|
||||
$componentControl?.[id]?.setTab?.(-1)
|
||||
isManuallySelected = false
|
||||
$componentControl?.[id]?.setTab?.(0)
|
||||
|
||||
$debuggingComponents = Object.fromEntries(
|
||||
Object.entries($debuggingComponents).filter(([key]) => key !== id)
|
||||
)
|
||||
}}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -19,9 +19,9 @@
|
||||
const { connectingInput } = getContext<AppViewerContext>('AppViewerContext')
|
||||
const name = getComponentNameById(gridItem.id)
|
||||
|
||||
const nameOverrides =
|
||||
$: nameOverrides =
|
||||
gridItem.data.type === 'decisiontreecomponent'
|
||||
? gridItem.data.nodes.map((n) => n.label)
|
||||
? gridItem.data.nodes.map((n, i) => `${n.label} (Tab index ${i})`)
|
||||
: undefined
|
||||
|
||||
function getComponentNameById(componentId: string) {
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
{node.label === '' ? `Tab: ${node.id}` : node.label}
|
||||
</div>
|
||||
<Badge color="indigo" small>
|
||||
{index}
|
||||
Tab index: {index}
|
||||
<Tooltip>
|
||||
You can manually select a node using the <b>setTab</b> function with this index in a frontend
|
||||
runnable.
|
||||
|
||||
+20
-7
@@ -15,6 +15,7 @@
|
||||
addNewBranch,
|
||||
addNode,
|
||||
findCollapseNode,
|
||||
getFirstNode,
|
||||
getParents,
|
||||
insertNode,
|
||||
removeBranch,
|
||||
@@ -52,6 +53,12 @@
|
||||
}
|
||||
|
||||
function buildStartNode() {
|
||||
const firstNode = getFirstNode(nodes)
|
||||
|
||||
if (!firstNode) {
|
||||
return
|
||||
}
|
||||
|
||||
const startNodeConfig = {
|
||||
id: 'start',
|
||||
data: {
|
||||
@@ -62,7 +69,7 @@
|
||||
id: 'start',
|
||||
label: 'Start',
|
||||
next: {
|
||||
id: nodes[0].id,
|
||||
id: firstNode.id,
|
||||
condition: {
|
||||
type: 'evalv2',
|
||||
expr: 'true',
|
||||
@@ -80,15 +87,16 @@
|
||||
|
||||
const startNode = createNode(startNodeConfig)
|
||||
displayedNodes.push(startNode)
|
||||
|
||||
edges.push(
|
||||
createEdge({
|
||||
id: `start-${nodes[0].id}`,
|
||||
id: `start-${firstNode?.id}`,
|
||||
source: 'start',
|
||||
target: nodes[0].id
|
||||
target: firstNode?.id
|
||||
})
|
||||
)
|
||||
}
|
||||
const { app, runnableComponents, componentControl } =
|
||||
const { app, runnableComponents, componentControl, debuggingComponents } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
function buildEndNode() {
|
||||
@@ -183,16 +191,18 @@
|
||||
$selectedNodeId = detail
|
||||
const index = nodes.findIndex((node) => node.id === detail)
|
||||
$componentControl?.[component.id]?.setTab?.(index)
|
||||
$debuggingComponents[component.id] = index
|
||||
|
||||
break
|
||||
case 'nodeInsert': {
|
||||
addSubGrid()
|
||||
|
||||
if (branchInsert) {
|
||||
if (parentIds.length === 1 && graphNode) {
|
||||
console.log('A', parentIds)
|
||||
// console.log('A', parentIds)
|
||||
nodes = insertNode(nodes, parentIds[0], graphNode)
|
||||
} else {
|
||||
console.log('B', parentIds)
|
||||
// console.log('B', parentIds)
|
||||
// find parent with multiple next
|
||||
const parentWithMultipleNext = nodes.find((node) => {
|
||||
return node.next.length > 1 && parentIds.includes(node.id)
|
||||
@@ -214,9 +224,11 @@
|
||||
|
||||
case 'delete': {
|
||||
const graphhNodeIndex = nodes.findIndex((node) => node.id == graphNode?.id)
|
||||
|
||||
if (graphhNodeIndex > -1) {
|
||||
deleteSubgrid(graphhNodeIndex)
|
||||
}
|
||||
|
||||
nodes = removeNode(nodes, graphNode)
|
||||
break
|
||||
}
|
||||
@@ -228,6 +240,7 @@
|
||||
case 'removeBranch': {
|
||||
nodes = removeBranch(nodes, graphNode, parentIds[0], (nodeId) => {
|
||||
const index = nodes.findIndex((node) => node.id === nodeId)
|
||||
|
||||
deleteSubgrid(index)
|
||||
})
|
||||
break
|
||||
@@ -449,7 +462,7 @@
|
||||
})
|
||||
|
||||
$: if (nodes.length > 0 && !$selectedNodeId) {
|
||||
$selectedNodeId = nodes[0].id
|
||||
$selectedNodeId = getFirstNode(nodes)?.id
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -14,19 +14,18 @@ function createBooleanRC(): RichConfiguration {
|
||||
export function addNode(nodes: DecisionTreeNode[], sourceNode: DecisionTreeNode | undefined) {
|
||||
const nextId = getNextId(nodes.map((node) => node.id))
|
||||
|
||||
const newNode: DecisionTreeNode = {
|
||||
id: nextId,
|
||||
label: nextId,
|
||||
next: sourceNode ? sourceNode.next : [{ id: nodes[0].id }],
|
||||
allowed: createBooleanRC()
|
||||
}
|
||||
|
||||
if (sourceNode) {
|
||||
let index = nodes.findIndex((n) => n.id === sourceNode.id)
|
||||
nodes = [...nodes.slice(0, index), newNode, ...nodes.slice(index)]
|
||||
const newNode: DecisionTreeNode = {
|
||||
id: nextId,
|
||||
label: nextId,
|
||||
next: sourceNode.next,
|
||||
allowed: createBooleanRC()
|
||||
}
|
||||
|
||||
nodes.push(newNode)
|
||||
|
||||
nodes = nodes.map((node) => {
|
||||
if (node.id === sourceNode.id) {
|
||||
if (node.id === sourceNode?.id) {
|
||||
node.next = [
|
||||
{
|
||||
id: newNode.id,
|
||||
@@ -36,10 +35,23 @@ export function addNode(nodes: DecisionTreeNode[], sourceNode: DecisionTreeNode
|
||||
}
|
||||
return node
|
||||
})
|
||||
|
||||
return nodes
|
||||
} else {
|
||||
nodes = [newNode, ...nodes]
|
||||
const firstNode = getFirstNode(nodes)
|
||||
|
||||
if (firstNode) {
|
||||
const newNode: DecisionTreeNode = {
|
||||
id: nextId,
|
||||
label: nextId,
|
||||
next: [{ id: firstNode.id, condition: createBooleanRC() }],
|
||||
allowed: createBooleanRC()
|
||||
}
|
||||
|
||||
nodes.push(newNode)
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
export function insertNode(
|
||||
@@ -49,7 +61,6 @@ export function insertNode(
|
||||
) {
|
||||
const nextId = getNextId(nodes.map((node) => node.id))
|
||||
|
||||
console.log('insertNode', parentId, sourceNode)
|
||||
const newNode: DecisionTreeNode = {
|
||||
id: nextId,
|
||||
label: nextId,
|
||||
@@ -273,7 +284,11 @@ export function getParents(nodes: DecisionTreeNode[], nodeId: string): string[]
|
||||
return parentIds
|
||||
}
|
||||
|
||||
function getFirstNode(nodes: DecisionTreeNode[]): DecisionTreeNode | undefined {
|
||||
export function getFirstNode(nodes: DecisionTreeNode[]): DecisionTreeNode | undefined {
|
||||
// No other nodes has this node as next
|
||||
return nodes.find((node) => !nodes.some((n) => n.next.some((next) => next.id === node.id)))
|
||||
}
|
||||
|
||||
export function isDebugging(debuggingComponents: Record<string, number>, id: string): boolean {
|
||||
return Object.keys(debuggingComponents).includes(id)
|
||||
}
|
||||
|
||||
+3
-1
@@ -10,7 +10,9 @@
|
||||
|
||||
const tabComponents = allItems($app.grid, $app.subgrids).filter(
|
||||
(component) =>
|
||||
component.data.type === 'tabscomponent' || component.data.type === 'conditionalwrapper'
|
||||
component.data.type === 'tabscomponent' ||
|
||||
component.data.type === 'conditionalwrapper' ||
|
||||
component.data.type === 'decisiontreecomponent'
|
||||
)
|
||||
</script>
|
||||
|
||||
|
||||
@@ -260,6 +260,7 @@ export type AppViewerContext = {
|
||||
darkMode: Writable<boolean>
|
||||
cssEditorOpen: Writable<boolean>
|
||||
previewTheme: Writable<string | undefined>
|
||||
debuggingComponents: Writable<Record<string, number>>
|
||||
}
|
||||
|
||||
export type AppEditorContext = {
|
||||
|
||||
Reference in New Issue
Block a user