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:
Faton Ramadani
2024-01-31 16:54:12 +01:00
committed by GitHub
parent 56e458c300
commit 740801f4a7
10 changed files with 177 additions and 50 deletions
@@ -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.
@@ -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)
}
@@ -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 = {