[Major Refactor] Use TreeExplorer on nodeLibrarySidebarTab (#699)

* Basic move

* Add back node bookmark

* Move node preview

* Fix drag node to canvas

* Restore click node to add to canvas

* Split bookmark tree and library tree

* Migrate rename and delete context menu

* Fix expanded keys

* Split components

* Support extra menu items

* Context menu only for folder

* Migrate add folder

* Handle drop

* Store color customization

* remove extra padding

* Do not show context menu if no item

* Hide divider if no bookmark

* Sort bookmarks alphabetically default

* nit

* proper edit

* Update test selectors

* Auto expand on item drop

* nit

* Fix tests

* Search also searches bookmarks tree

* Add serach playwright test
This commit is contained in:
Chenlei Hu
2024-09-01 14:03:15 -04:00
committed by GitHub
parent 5383f97eba
commit d04dbcd2c1
9 changed files with 516 additions and 477 deletions

View File

@@ -4,6 +4,7 @@
:class="props.class"
v-model:expandedKeys="expandedKeys"
:value="renderedRoots"
selectionMode="single"
:pt="{
nodeLabel: 'tree-explorer-node-label',
nodeContent: ({ props }) => ({
@@ -40,25 +41,31 @@ import type {
TreeExplorerNode
} from '@/types/treeExplorerTypes'
import type { MenuItem } from 'primevue/menuitem'
import { useTreeExpansion } from '@/hooks/treeHooks'
import { useI18n } from 'vue-i18n'
const expandedKeys = defineModel<Record<string, boolean>>('expandedKeys')
provide('expandedKeys', expandedKeys)
const props = defineProps<{
roots: TreeExplorerNode[]
class?: string
extraMenuItems?: MenuItem[]
extraMenuItems?:
| MenuItem[]
| ((targetNode: RenderedTreeExplorerNode) => MenuItem[])
}>()
const emit = defineEmits<{
(e: 'nodeClick', node: RenderedTreeExplorerNode): void
(e: 'nodeClick', node: RenderedTreeExplorerNode, event: MouseEvent): void
(e: 'nodeDelete', node: RenderedTreeExplorerNode): void
(e: 'contextMenu', node: RenderedTreeExplorerNode, event: MouseEvent): void
}>()
const { expandedKeys, toggleNodeOnEvent } = useTreeExpansion()
const renderedRoots = computed<RenderedTreeExplorerNode[]>(() => {
return props.roots.map(fillNodeInfo)
})
const getTreeNodeIcon = (node: TreeExplorerNode) => {
if (node.getIcon) {
return node.getIcon(node)
const icon = node.getIcon(node)
if (icon) {
return icon
}
} else if (node.icon) {
return node.icon
}
@@ -82,46 +89,57 @@ const fillNodeInfo = (node: TreeExplorerNode): RenderedTreeExplorerNode => {
}
}
const onNodeContentClick = (e: MouseEvent, node: RenderedTreeExplorerNode) => {
if (!node.key) return
if (node.type === 'folder') {
toggleNodeOnEvent(e, node)
}
emit('nodeClick', node)
emit('nodeClick', node, e)
}
const menu = ref(null)
const menuTargetNode = ref<RenderedTreeExplorerNode | null>(null)
provide('menuTargetNode', menuTargetNode)
const renameEditingNode = ref<RenderedTreeExplorerNode | null>(null)
provide('renameEditingNode', renameEditingNode)
const { t } = useI18n()
const renameCommand = (node: RenderedTreeExplorerNode) => {
renameEditingNode.value = node
}
const deleteCommand = (node: RenderedTreeExplorerNode) => {
node.handleDelete?.(node)
emit('nodeDelete', node)
}
const menuItems = computed<MenuItem[]>(() => [
{
label: 'Rename',
label: t('rename'),
icon: 'pi pi-file-edit',
command: () => {
renameEditingNode.value = menuTargetNode.value
},
command: () => renameCommand(menuTargetNode.value),
visible: menuTargetNode.value?.handleRename !== undefined
},
{
label: 'Delete',
label: t('delete'),
icon: 'pi pi-trash',
command: () => {
menuTargetNode.value?.handleDelete?.(menuTargetNode.value)
emit('nodeDelete', menuTargetNode.value)
},
command: () => deleteCommand(menuTargetNode.value),
visible: menuTargetNode.value?.handleDelete !== undefined
},
...(props.extraMenuItems || [])
...(props.extraMenuItems
? typeof props.extraMenuItems === 'function'
? props.extraMenuItems(menuTargetNode.value)
: props.extraMenuItems
: [])
])
const handleContextMenu = (node: RenderedTreeExplorerNode, e: MouseEvent) => {
menuTargetNode.value = node
emit('contextMenu', node, e)
menu.value?.show(e)
if (menuItems.value.filter((item) => item.visible).length > 0) {
menu.value?.show(e)
}
}
defineExpose({
renameCommand,
deleteCommand
})
</script>
<style scoped>
.tree-explorer-node-label {
:deep(.tree-explorer-node-label) {
width: 100%;
display: flex;
align-items: center;
margin-left: var(--p-tree-node-gap);

View File

@@ -16,7 +16,7 @@
<EditableText
:modelValue="node.label"
:isEditing="isEditing"
@edit="(newName: string) => props.node.handleRename(node, newName)"
@edit="handleRename"
/>
<slot name="after-label" :node="props.node"></slot>
</span>
@@ -26,9 +26,9 @@
severity="secondary"
class="leaf-count-badge"
/>
<slot name="actions" :node="node">
<!-- Default slot content for actions -->
</slot>
</div>
<div class="node-actions">
<slot name="actions" :node="props.node"></slot>
</div>
</div>
</template>
@@ -62,13 +62,15 @@ const emit = defineEmits<{
}>()
const labelEditable = computed<boolean>(() => !!props.node.handleRename)
const renameEditingNode = inject(
'renameEditingNode'
) as Ref<TreeExplorerNode | null>
const renameEditingNode =
inject<Ref<TreeExplorerNode | null>>('renameEditingNode')
const isEditing = computed(
() => labelEditable.value && renameEditingNode.value?.key === props.node.key
)
const handleRename = (newName: string) => {
props.node.handleRename(props.node, newName)
renameEditingNode.value = null
}
const container = ref<HTMLElement | null>(null)
const canDrop = ref(false)
const treeNodeElement = ref<HTMLElement | null>(null)