mirror of
https://github.com/enso-org/enso.git
synced 2024-11-22 22:10:15 +03:00
Graph editor breadcrumbs integration (#8519)
Implements #7788 [Peek 2023-12-12 10-46.webm](https://github.com/enso-org/enso/assets/1428930/7d1f45f6-8323-4adc-9a8c-f6f4bcaeb143)
This commit is contained in:
parent
21d164ec3e
commit
23e0bafc75
@ -35,6 +35,7 @@ app/ide-desktop/lib/dashboard/playwright-report/
|
|||||||
app/ide-desktop/lib/dashboard/playwright/.cache/
|
app/ide-desktop/lib/dashboard/playwright/.cache/
|
||||||
app/gui/view/documentation/assets/stylesheet.css
|
app/gui/view/documentation/assets/stylesheet.css
|
||||||
app/gui2/rust-ffi/pkg
|
app/gui2/rust-ffi/pkg
|
||||||
|
app/gui2/src/assets/font-*.css
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
build.json
|
build.json
|
||||||
app/gui2/playwright-report/
|
app/gui2/playwright-report/
|
||||||
|
@ -16,6 +16,7 @@ import PlusButton from '@/components/PlusButton.vue'
|
|||||||
import TopBar from '@/components/TopBar.vue'
|
import TopBar from '@/components/TopBar.vue'
|
||||||
import { useDoubleClick } from '@/composables/doubleClick'
|
import { useDoubleClick } from '@/composables/doubleClick'
|
||||||
import { keyboardBusy, keyboardBusyExceptIn, useEvent } from '@/composables/events'
|
import { keyboardBusy, keyboardBusyExceptIn, useEvent } from '@/composables/events'
|
||||||
|
import { useStackNavigator } from '@/composables/stackNavigator'
|
||||||
import { provideGraphNavigator } from '@/providers/graphNavigator'
|
import { provideGraphNavigator } from '@/providers/graphNavigator'
|
||||||
import { provideGraphSelection } from '@/providers/graphSelection'
|
import { provideGraphSelection } from '@/providers/graphSelection'
|
||||||
import { provideInteractionHandler, type Interaction } from '@/providers/interactionHandler'
|
import { provideInteractionHandler, type Interaction } from '@/providers/interactionHandler'
|
||||||
@ -27,7 +28,6 @@ import { groupColorVar, useSuggestionDbStore } from '@/stores/suggestionDatabase
|
|||||||
import { colorFromString } from '@/util/colors'
|
import { colorFromString } from '@/util/colors'
|
||||||
import { Rect } from '@/util/data/rect'
|
import { Rect } from '@/util/data/rect'
|
||||||
import { Vec2 } from '@/util/data/vec2'
|
import { Vec2 } from '@/util/data/vec2'
|
||||||
import { qnLastSegment, tryQualifiedName } from '@/util/qualifiedName'
|
|
||||||
import * as set from 'lib0/set'
|
import * as set from 'lib0/set'
|
||||||
import type { ExprId, NodeMetadata } from 'shared/yjsModel'
|
import type { ExprId, NodeMetadata } from 'shared/yjsModel'
|
||||||
import { computed, onMounted, ref, watch } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
@ -206,12 +206,12 @@ const graphBindingsHandler = graphBindings.handler({
|
|||||||
if (keyboardBusy()) return false
|
if (keyboardBusy()) return false
|
||||||
const selectedNode = set.first(nodeSelection.selected)
|
const selectedNode = set.first(nodeSelection.selected)
|
||||||
if (selectedNode) {
|
if (selectedNode) {
|
||||||
enterNode(selectedNode)
|
stackNavigator.enterNode(selectedNode)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
exitNode() {
|
exitNode() {
|
||||||
if (keyboardBusy()) return false
|
if (keyboardBusy()) return false
|
||||||
exitNode()
|
stackNavigator.exitNode()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -220,7 +220,7 @@ const handleClick = useDoubleClick(
|
|||||||
graphBindingsHandler(e)
|
graphBindingsHandler(e)
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
exitNode()
|
stackNavigator.exitNode()
|
||||||
},
|
},
|
||||||
).handleClick
|
).handleClick
|
||||||
const codeEditorArea = ref<HTMLElement>()
|
const codeEditorArea = ref<HTMLElement>()
|
||||||
@ -232,31 +232,6 @@ const codeEditorHandler = codeEditorBindings.handler({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
function enterNode(id: ExprId) {
|
|
||||||
const expressionInfo = graphStore.db.getExpressionInfo(id)
|
|
||||||
if (expressionInfo == undefined || expressionInfo.methodCall == undefined) {
|
|
||||||
console.debug('Cannot enter node that has no method call.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const definedOnType = tryQualifiedName(expressionInfo.methodCall.methodPointer.definedOnType)
|
|
||||||
if (!projectStore.modulePath?.ok) {
|
|
||||||
console.warn('Cannot enter node while no module is open.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const openModuleName = qnLastSegment(projectStore.modulePath.value)
|
|
||||||
if (definedOnType.ok && qnLastSegment(definedOnType.value) != openModuleName) {
|
|
||||||
console.debug('Cannot enter node that is not defined on current module.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
projectStore.executionContext.push(id)
|
|
||||||
graphStore.updateState()
|
|
||||||
}
|
|
||||||
|
|
||||||
function exitNode() {
|
|
||||||
projectStore.executionContext.pop()
|
|
||||||
graphStore.updateState()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Track play button presses. */
|
/** Track play button presses. */
|
||||||
function onPlayButtonPress() {
|
function onPlayButtonPress() {
|
||||||
projectStore.lsRpcConnection.then(async () => {
|
projectStore.lsRpcConnection.then(async () => {
|
||||||
@ -410,17 +385,6 @@ async function handleFileDrop(event: DragEvent) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const breadcrumbs = computed(() =>
|
|
||||||
projectStore.executionContext.desiredStack.map((frame) => {
|
|
||||||
switch (frame.type) {
|
|
||||||
case 'ExplicitCall':
|
|
||||||
return frame.methodPointer.name
|
|
||||||
case 'LocalCall':
|
|
||||||
return frame.expressionId
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
// === Clipboard ===
|
// === Clipboard ===
|
||||||
|
|
||||||
const ENSO_MIME_TYPE = 'web application/enso'
|
const ENSO_MIME_TYPE = 'web application/enso'
|
||||||
@ -507,6 +471,8 @@ function handleNodeOutputPortDoubleClick(id: ExprId) {
|
|||||||
interaction.setCurrent(creatingNodeFromPortDoubleClick)
|
interaction.setCurrent(creatingNodeFromPortDoubleClick)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const stackNavigator = useStackNavigator()
|
||||||
|
|
||||||
function handleEdgeDrop(source: ExprId, position: Vec2) {
|
function handleEdgeDrop(source: ExprId, position: Vec2) {
|
||||||
componentBrowserUsage.value = { type: 'newNode', sourcePort: source }
|
componentBrowserUsage.value = { type: 'newNode', sourcePort: source }
|
||||||
componentBrowserNodePosition.value = position
|
componentBrowserNodePosition.value = position
|
||||||
@ -532,7 +498,7 @@ function handleEdgeDrop(source: ExprId, position: Vec2) {
|
|||||||
<div :style="{ transform: graphNavigator.transform }" class="htmlLayer">
|
<div :style="{ transform: graphNavigator.transform }" class="htmlLayer">
|
||||||
<GraphNodes
|
<GraphNodes
|
||||||
@nodeOutputPortDoubleClick="handleNodeOutputPortDoubleClick"
|
@nodeOutputPortDoubleClick="handleNodeOutputPortDoubleClick"
|
||||||
@nodeDoubleClick="enterNode"
|
@nodeDoubleClick="(id) => stackNavigator.enterNode(id)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ComponentBrowser
|
<ComponentBrowser
|
||||||
@ -549,10 +515,12 @@ function handleEdgeDrop(source: ExprId, position: Vec2) {
|
|||||||
v-model:mode="projectStore.executionMode"
|
v-model:mode="projectStore.executionMode"
|
||||||
:title="projectStore.name"
|
:title="projectStore.name"
|
||||||
:modes="EXECUTION_MODES"
|
:modes="EXECUTION_MODES"
|
||||||
:breadcrumbs="breadcrumbs"
|
:breadcrumbs="stackNavigator.breadcrumbLabels.value"
|
||||||
@breadcrumbClick="console.log(`breadcrumb #${$event + 1} clicked.`)"
|
:allowNavigationLeft="stackNavigator.allowNavigationLeft.value"
|
||||||
@back="exitNode"
|
:allowNavigationRight="stackNavigator.allowNavigationRight.value"
|
||||||
@forward="console.log('breadcrumbs \'forward\' button clicked.')"
|
@breadcrumbClick="stackNavigator.handleBreadcrumbClick"
|
||||||
|
@back="stackNavigator.exitNode"
|
||||||
|
@forward="stackNavigator.enterNextNodeFromHistory"
|
||||||
@execute="onPlayButtonPress()"
|
@execute="onPlayButtonPress()"
|
||||||
/>
|
/>
|
||||||
<PlusButton @pointerdown="interaction.setCurrent(creatingNodeFromButton)" />
|
<PlusButton @pointerdown="interaction.setCurrent(creatingNodeFromButton)" />
|
||||||
|
@ -49,7 +49,6 @@ const emit = defineEmits<{
|
|||||||
draggingCommited: []
|
draggingCommited: []
|
||||||
delete: []
|
delete: []
|
||||||
replaceSelection: []
|
replaceSelection: []
|
||||||
nodeDoubleClick: []
|
|
||||||
outputPortClick: [portId: ExprId]
|
outputPortClick: [portId: ExprId]
|
||||||
outputPortDoubleClick: [portId: ExprId]
|
outputPortDoubleClick: [portId: ExprId]
|
||||||
doubleClick: []
|
doubleClick: []
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import NavBreadcrumbs from '@/components/NavBreadcrumbs.vue'
|
import NavBreadcrumbs, { type BreadcrumbItem } from '@/components/NavBreadcrumbs.vue'
|
||||||
import SvgIcon from '@/components/SvgIcon.vue'
|
import SvgIcon from '@/components/SvgIcon.vue'
|
||||||
|
|
||||||
const props = defineProps<{ breadcrumbs: string[] }>()
|
const props = defineProps<{
|
||||||
|
breadcrumbs: BreadcrumbItem[]
|
||||||
|
allowNavigationLeft: boolean
|
||||||
|
allowNavigationRight: boolean
|
||||||
|
}>()
|
||||||
const emit = defineEmits<{ back: []; forward: []; breadcrumbClick: [index: number] }>()
|
const emit = defineEmits<{ back: []; forward: []; breadcrumbClick: [index: number] }>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -13,20 +17,19 @@ const emit = defineEmits<{ back: []; forward: []; breadcrumbClick: [index: numbe
|
|||||||
<SvgIcon
|
<SvgIcon
|
||||||
name="arrow_left"
|
name="arrow_left"
|
||||||
draggable="false"
|
draggable="false"
|
||||||
class="icon button inactive"
|
class="icon button"
|
||||||
|
:class="{ inactive: !props.allowNavigationLeft }"
|
||||||
@pointerdown="emit('back')"
|
@pointerdown="emit('back')"
|
||||||
/>
|
/>
|
||||||
<SvgIcon
|
<SvgIcon
|
||||||
name="arrow_right"
|
name="arrow_right"
|
||||||
draggable="false"
|
draggable="false"
|
||||||
class="icon button"
|
class="icon button"
|
||||||
|
:class="{ inactive: !props.allowNavigationRight }"
|
||||||
@pointerdown="emit('forward')"
|
@pointerdown="emit('forward')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<NavBreadcrumbs
|
<NavBreadcrumbs :breadcrumbs="props.breadcrumbs" @selected="emit('breadcrumbClick', $event)" />
|
||||||
:breadcrumbs="props.breadcrumbs"
|
|
||||||
@pointerdown="emit('breadcrumbClick', $event)"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const props = defineProps<{ text: string }>()
|
const props = defineProps<{ text: string; active: boolean }>()
|
||||||
const emit = defineEmits<{ click: [] }>()
|
const emit = defineEmits<{ click: [] }>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="NavBreadcrumb"><span @click="emit('click')" v-text="props.text"></span></div>
|
<div :class="['NavBreadcrumb', { inactive: !props.active }]">
|
||||||
|
<span @click="emit('click')" v-text="props.text"></span>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -25,4 +27,8 @@ span {
|
|||||||
backdrop-filter: var(--backdrop-blur);
|
backdrop-filter: var(--backdrop-blur);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.inactive {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -3,15 +3,28 @@ import SvgIcon from '@/components/SvgIcon.vue'
|
|||||||
|
|
||||||
import NavBreadcrumb from '@/components/NavBreadcrumb.vue'
|
import NavBreadcrumb from '@/components/NavBreadcrumb.vue'
|
||||||
|
|
||||||
const props = defineProps<{ breadcrumbs: string[] }>()
|
export interface BreadcrumbItem {
|
||||||
const emit = defineEmits<{ click: [index: number] }>()
|
label: string
|
||||||
|
active: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{ breadcrumbs: BreadcrumbItem[] }>()
|
||||||
|
const emit = defineEmits<{ selected: [index: number] }>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="NavBreadcrumbs">
|
<div class="NavBreadcrumbs">
|
||||||
<template v-for="(breadcrumb, index) in props.breadcrumbs" :key="index">
|
<template v-for="(breadcrumb, index) in props.breadcrumbs" :key="index">
|
||||||
<SvgIcon v-if="index > 0" name="arrow_right_head_only" class="arrow" />
|
<SvgIcon
|
||||||
<NavBreadcrumb :text="breadcrumb" @click="emit('click', index)" />
|
v-if="index > 0"
|
||||||
|
name="arrow_right_head_only"
|
||||||
|
:class="['arrow', { inactive: !breadcrumb.active }]"
|
||||||
|
/>
|
||||||
|
<NavBreadcrumb
|
||||||
|
:text="breadcrumb.label"
|
||||||
|
:active="breadcrumb.active"
|
||||||
|
@pointerdown="emit('selected', index)"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -26,4 +39,8 @@ const emit = defineEmits<{ click: [index: number] }>()
|
|||||||
.arrow {
|
.arrow {
|
||||||
color: #666666;
|
color: #666666;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.inactive {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,10 +1,18 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import NavBar from '@/components/NavBar.vue'
|
import NavBar from '@/components/NavBar.vue'
|
||||||
|
import type { BreadcrumbItem } from '@/components/NavBreadcrumbs.vue'
|
||||||
import ProjectTitle from '@/components/ProjectTitle.vue'
|
import ProjectTitle from '@/components/ProjectTitle.vue'
|
||||||
import { injectGuiConfig } from '@/providers/guiConfig'
|
import { injectGuiConfig } from '@/providers/guiConfig'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
const props = defineProps<{ title: string; breadcrumbs: string[]; modes: string[]; mode: string }>()
|
const props = defineProps<{
|
||||||
|
title: string
|
||||||
|
breadcrumbs: BreadcrumbItem[]
|
||||||
|
modes: string[]
|
||||||
|
mode: string
|
||||||
|
allowNavigationLeft: boolean
|
||||||
|
allowNavigationRight: boolean
|
||||||
|
}>()
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
execute: []
|
execute: []
|
||||||
back: []
|
back: []
|
||||||
@ -36,6 +44,8 @@ const barStyle = computed(() => {
|
|||||||
/>
|
/>
|
||||||
<NavBar
|
<NavBar
|
||||||
:breadcrumbs="props.breadcrumbs"
|
:breadcrumbs="props.breadcrumbs"
|
||||||
|
:allowNavigationLeft="props.allowNavigationLeft"
|
||||||
|
:allowNavigationRight="props.allowNavigationRight"
|
||||||
@back="emit('back')"
|
@back="emit('back')"
|
||||||
@forward="emit('forward')"
|
@forward="emit('forward')"
|
||||||
@breadcrumbClick="emit('breadcrumbClick', $event)"
|
@breadcrumbClick="emit('breadcrumbClick', $event)"
|
||||||
|
119
app/gui2/src/composables/stackNavigator.ts
Normal file
119
app/gui2/src/composables/stackNavigator.ts
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import type { BreadcrumbItem } from '@/components/NavBreadcrumbs.vue'
|
||||||
|
import { useGraphStore } from '@/stores/graph'
|
||||||
|
import { useProjectStore } from '@/stores/project'
|
||||||
|
import { qnLastSegment, tryQualifiedName } from '@/util/qualifiedName'
|
||||||
|
import type { StackItem } from 'shared/languageServerTypes.ts'
|
||||||
|
import type { ExprId } from 'shared/yjsModel.ts'
|
||||||
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
|
export function useStackNavigator() {
|
||||||
|
const projectStore = useProjectStore()
|
||||||
|
const graphStore = useGraphStore()
|
||||||
|
|
||||||
|
const breadcrumbs = ref<StackItem[]>([])
|
||||||
|
|
||||||
|
const breadcrumbLabels = computed(() => {
|
||||||
|
const activeStackLength = projectStore.executionContext.desiredStack.length
|
||||||
|
return breadcrumbs.value.map((item, index) => {
|
||||||
|
const label = stackItemToLabel(item)
|
||||||
|
const isActive = index < activeStackLength
|
||||||
|
return { label, active: isActive } satisfies BreadcrumbItem
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const allowNavigationLeft = computed(() => {
|
||||||
|
return projectStore.executionContext.desiredStack.length > 1
|
||||||
|
})
|
||||||
|
|
||||||
|
const allowNavigationRight = computed(() => {
|
||||||
|
return projectStore.executionContext.desiredStack.length < breadcrumbs.value.length
|
||||||
|
})
|
||||||
|
|
||||||
|
function stackItemToLabel(item: StackItem): string {
|
||||||
|
switch (item.type) {
|
||||||
|
case 'ExplicitCall': {
|
||||||
|
return item.methodPointer.name
|
||||||
|
}
|
||||||
|
case 'LocalCall': {
|
||||||
|
const exprId = item.expressionId
|
||||||
|
const info = graphStore.db.getExpressionInfo(exprId)
|
||||||
|
return info?.methodCall?.methodPointer.name ?? 'unknown'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBreadcrumbClick(index: number) {
|
||||||
|
const activeStack = projectStore.executionContext.desiredStack
|
||||||
|
if (index < activeStack.length) {
|
||||||
|
const diff = activeStack.length - index - 1
|
||||||
|
for (let i = 0; i < diff; i++) {
|
||||||
|
projectStore.executionContext.pop()
|
||||||
|
}
|
||||||
|
} else if (index >= activeStack.length) {
|
||||||
|
const diff = index - activeStack.length + 1
|
||||||
|
for (let i = 0; i < diff; i++) {
|
||||||
|
const stackItem = breadcrumbs.value[index - i]
|
||||||
|
if (stackItem?.type === 'LocalCall') {
|
||||||
|
const exprId = stackItem.expressionId
|
||||||
|
projectStore.executionContext.push(exprId)
|
||||||
|
} else {
|
||||||
|
console.warn('Cannot enter non-local call.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
graphStore.updateState()
|
||||||
|
}
|
||||||
|
|
||||||
|
function enterNode(id: ExprId) {
|
||||||
|
const expressionInfo = graphStore.db.getExpressionInfo(id)
|
||||||
|
if (expressionInfo == null || expressionInfo.methodCall == null) {
|
||||||
|
console.debug('Cannot enter node that has no method call.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const definedOnType = tryQualifiedName(expressionInfo.methodCall.methodPointer.definedOnType)
|
||||||
|
if (!projectStore.modulePath?.ok) {
|
||||||
|
console.warn('Cannot enter node while no module is open.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const openModuleName = qnLastSegment(projectStore.modulePath.value)
|
||||||
|
if (definedOnType.ok && qnLastSegment(definedOnType.value) !== openModuleName) {
|
||||||
|
console.debug('Cannot enter node that is not defined on current module.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
projectStore.executionContext.push(id)
|
||||||
|
graphStore.updateState()
|
||||||
|
breadcrumbs.value = projectStore.executionContext.desiredStack.slice()
|
||||||
|
}
|
||||||
|
|
||||||
|
function exitNode() {
|
||||||
|
projectStore.executionContext.pop()
|
||||||
|
graphStore.updateState()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enter the next node from the history stack. This is the node that is the first greyed out item in the breadcrumbs.
|
||||||
|
function enterNextNodeFromHistory() {
|
||||||
|
const nextNodeIndex = projectStore.executionContext.desiredStack.length
|
||||||
|
const nextNode = breadcrumbs.value[nextNodeIndex]
|
||||||
|
if (nextNode?.type !== 'LocalCall') {
|
||||||
|
console.warn('Cannot enter non-local call.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
projectStore.executionContext.push(nextNode.expressionId)
|
||||||
|
graphStore.updateState()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
breadcrumbs.value = projectStore.executionContext.desiredStack.slice()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
breadcrumbs,
|
||||||
|
breadcrumbLabels,
|
||||||
|
allowNavigationLeft,
|
||||||
|
allowNavigationRight,
|
||||||
|
handleBreadcrumbClick,
|
||||||
|
enterNode,
|
||||||
|
exitNode,
|
||||||
|
enterNextNodeFromHistory,
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user