feat: smart-tree component added to hoppscotch-ui (#3210)
This commit is contained in:
71
packages/hoppscotch-ui/src/components/smart/Tree.vue
Normal file
71
packages/hoppscotch-ui/src/components/smart/Tree.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<div class="flex flex-col flex-1">
|
||||
<div
|
||||
v-if="rootNodes.status === 'loaded' && rootNodes.data.length > 0"
|
||||
class="flex flex-col"
|
||||
>
|
||||
<div
|
||||
v-for="rootNode in rootNodes.data"
|
||||
:key="rootNode.id"
|
||||
class="flex flex-col flex-1"
|
||||
>
|
||||
<SmartTreeBranch
|
||||
:root-nodes-length="rootNodes.data.length"
|
||||
:node-item="rootNode"
|
||||
:adapter="adapter as SmartTreeAdapter<T>"
|
||||
>
|
||||
<template
|
||||
#default="{ node, toggleChildren, isOpen, highlightChildren }"
|
||||
>
|
||||
<slot
|
||||
name="content"
|
||||
:node="node as TreeNode<T>"
|
||||
:toggle-children="toggleChildren as () => void"
|
||||
:is-open="isOpen as boolean"
|
||||
:highlight-children="(id:string|null) => highlightChildren(id)"
|
||||
></slot>
|
||||
</template>
|
||||
|
||||
<template #emptyNode="{ node }">
|
||||
<slot name="emptyNode" :node="node"></slot>
|
||||
</template>
|
||||
</SmartTreeBranch>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="rootNodes.status === 'loading'"
|
||||
class="flex flex-col items-center justify-center flex-1 p-4"
|
||||
>
|
||||
<SmartSpinner class="my-4" />
|
||||
<span class="text-secondaryLight">{{ t?.("state.loading") }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="rootNodes.status === 'loaded' && rootNodes.data.length === 0"
|
||||
class="flex flex-col flex-1"
|
||||
>
|
||||
<slot name="emptyNode" :node="(null as TreeNode<T> | null)"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" generic="T extends any">
|
||||
import { computed, inject } from "vue"
|
||||
import SmartTreeBranch from "./TreeBranch.vue"
|
||||
import SmartSpinner from "./Spinner.vue"
|
||||
import { SmartTreeAdapter, TreeNode } from "~/helpers/treeAdapter"
|
||||
import { HOPP_UI_OPTIONS, HoppUIPluginOptions } from "./../../plugin"
|
||||
const { t } = inject<HoppUIPluginOptions>(HOPP_UI_OPTIONS) ?? {}
|
||||
|
||||
const props = defineProps<{
|
||||
/**
|
||||
* The adapter that will be used to fetch the tree data
|
||||
* @template T The type of the data that will be stored in the tree
|
||||
*/
|
||||
adapter: SmartTreeAdapter<T>
|
||||
}>()
|
||||
|
||||
/**
|
||||
* Fetch the root nodes from the adapter by passing the node id as null
|
||||
*/
|
||||
const rootNodes = computed(() => props.adapter.getChildren(null).value)
|
||||
</script>
|
||||
133
packages/hoppscotch-ui/src/components/smart/TreeBranch.vue
Normal file
133
packages/hoppscotch-ui/src/components/smart/TreeBranch.vue
Normal file
@@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<slot
|
||||
:node="nodeItem"
|
||||
:toggle-children="toggleNodeChildren"
|
||||
:is-open="isNodeOpen"
|
||||
:highlight-children="(id:string|null) => highlightNodeChildren(id)"
|
||||
></slot>
|
||||
|
||||
<!-- This is a performance optimization trick -->
|
||||
<!-- Once expanded, Vue will traverse through the children and expand the tree up
|
||||
but when we collapse, the tree and the components are disposed. This is wasteful
|
||||
and comes with performance issues if the children list is expensive to render.
|
||||
Hence, here we render children only when first expanded, and after that, even if collapsed,
|
||||
we just hide the children.
|
||||
-->
|
||||
<div v-if="childrenRendered" v-show="showChildren" class="flex">
|
||||
<div
|
||||
class="bg-dividerLight cursor-nsResize flex ml-5.5 transform transition w-0.5 hover:bg-dividerDark hover:scale-x-125"
|
||||
@click="toggleNodeChildren"
|
||||
></div>
|
||||
<div
|
||||
v-if="childNodes.status === 'loaded' && childNodes.data.length > 0"
|
||||
class="flex flex-col flex-1 truncate"
|
||||
:class="{
|
||||
'bg-divider': highlightNode,
|
||||
}"
|
||||
>
|
||||
<SmartTreeBranch
|
||||
v-for="childNode in childNodes.data"
|
||||
:key="childNode.id"
|
||||
:node-item="childNode"
|
||||
:adapter="adapter"
|
||||
>
|
||||
<!-- The child slot is given a dynamic name in order to not break Volar -->
|
||||
<template
|
||||
#[CHILD_SLOT_NAME]="{
|
||||
node,
|
||||
toggleChildren,
|
||||
isOpen,
|
||||
highlightChildren,
|
||||
}"
|
||||
>
|
||||
<!-- Casting to help with type checking -->
|
||||
<slot
|
||||
:node="node as TreeNode<T>"
|
||||
:toggle-children="toggleChildren as () => void"
|
||||
:is-open="isOpen as boolean"
|
||||
:highlight-children="(id:string|null) => highlightChildren(id) as void"
|
||||
></slot>
|
||||
</template>
|
||||
<template #emptyNode="{ node }">
|
||||
<slot name="emptyNode" :node="node"></slot>
|
||||
</template>
|
||||
</SmartTreeBranch>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="childNodes.status === 'loading'"
|
||||
class="flex flex-col items-center justify-center flex-1 p-4"
|
||||
>
|
||||
<SmartSpinner class="my-4" />
|
||||
<span class="text-secondaryLight">{{ t?.("state.loading") }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="childNodes.status === 'loaded' && childNodes.data.length === 0"
|
||||
class="flex flex-col flex-1"
|
||||
>
|
||||
<slot name="emptyNode" :node="nodeItem"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" generic="T extends any">
|
||||
import { computed, inject, ref } from "vue"
|
||||
import SmartTreeBranch from "./TreeBranch.vue"
|
||||
import SmartSpinner from "./Spinner.vue"
|
||||
import { SmartTreeAdapter, TreeNode } from "~/helpers/treeAdapter"
|
||||
import { HOPP_UI_OPTIONS, HoppUIPluginOptions } from "./../../plugin"
|
||||
const { t } = inject<HoppUIPluginOptions>(HOPP_UI_OPTIONS) ?? {}
|
||||
|
||||
const props = defineProps<{
|
||||
/**
|
||||
* The node item that will be used to render the tree branch
|
||||
* @template T The type of the data passed to the tree branch
|
||||
*/
|
||||
adapter: SmartTreeAdapter<T>
|
||||
/**
|
||||
* The node item that will be used to render the tree branch content
|
||||
*/
|
||||
nodeItem: TreeNode<T>
|
||||
/**
|
||||
* Total number of rootNode
|
||||
*/
|
||||
rootNodesLength?: number
|
||||
}>()
|
||||
|
||||
const CHILD_SLOT_NAME = "default"
|
||||
|
||||
const isOnlyRootChild = computed(() => props.rootNodesLength === 1)
|
||||
|
||||
/**
|
||||
* Marks whether the children on this branch were ever rendered
|
||||
* See the usage inside '<template>' for more info
|
||||
*/
|
||||
const childrenRendered = ref(isOnlyRootChild.value)
|
||||
|
||||
const showChildren = ref(isOnlyRootChild.value)
|
||||
const isNodeOpen = ref(isOnlyRootChild.value)
|
||||
|
||||
const highlightNode = ref(false)
|
||||
|
||||
/**
|
||||
* Fetch the child nodes from the adapter by passing the node id of the current node
|
||||
*/
|
||||
const childNodes = computed(
|
||||
() => props.adapter.getChildren(props.nodeItem.id).value
|
||||
)
|
||||
|
||||
const toggleNodeChildren = () => {
|
||||
if (!childrenRendered.value) childrenRendered.value = true
|
||||
|
||||
showChildren.value = !showChildren.value
|
||||
isNodeOpen.value = !isNodeOpen.value
|
||||
}
|
||||
|
||||
const highlightNodeChildren = (id: string | null) => {
|
||||
if (id) {
|
||||
highlightNode.value = true
|
||||
} else {
|
||||
highlightNode.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -21,3 +21,5 @@ export { default as HoppSmartWindow } from "./Window.vue"
|
||||
export { default as HoppSmartWindows } from "./Windows.vue"
|
||||
export { default as HoppSmartPicture } from "./Picture.vue"
|
||||
export { default as HoppSmartPlaceholder } from "./Placeholder.vue"
|
||||
export { default as HoppSmartTree } from "./Tree.vue"
|
||||
export { default as HoppSmartTreeBranch } from "./TreeBranch.vue"
|
||||
|
||||
34
packages/hoppscotch-ui/src/helpers/treeAdapter.ts
Normal file
34
packages/hoppscotch-ui/src/helpers/treeAdapter.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Ref } from "vue"
|
||||
|
||||
/**
|
||||
* Representation of a tree node in the SmartTreeAdapter.
|
||||
*/
|
||||
export type TreeNode<T> = {
|
||||
id: string
|
||||
data: T
|
||||
}
|
||||
|
||||
/**
|
||||
* Representation of children result from a tree node when there will be a loading state.
|
||||
*/
|
||||
export type ChildrenResult<T> =
|
||||
| {
|
||||
status: "loading"
|
||||
}
|
||||
| {
|
||||
status: "loaded"
|
||||
data: Array<TreeNode<T>>
|
||||
}
|
||||
|
||||
/**
|
||||
* A tree adapter that can be used with the SmartTree component.
|
||||
* @template T The type of data that is stored in the tree.
|
||||
*/
|
||||
export interface SmartTreeAdapter<T> {
|
||||
/**
|
||||
*
|
||||
* @param nodeID - id of the node to get children for
|
||||
* @returns - Ref that contains the children of the node. It is reactive and will be updated when the children are changed.
|
||||
*/
|
||||
getChildren: (nodeID: string | null) => Ref<ChildrenResult<T>>
|
||||
}
|
||||
Reference in New Issue
Block a user