feat : smart tree component (#2865)

Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
This commit is contained in:
Nivedin
2023-01-31 17:15:03 +05:30
committed by GitHub
parent b95e2b365a
commit 2910164d5a
39 changed files with 4483 additions and 4142 deletions

View File

@@ -0,0 +1,65 @@
<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
:node-item="rootNode"
:adapter="adapter as SmartTreeAdapter<T>"
>
<template #default="{ node, toggleChildren, isOpen }">
<slot
name="content"
:node="node as TreeNode<T>"
:toggle-children="toggleChildren as () => void"
:is-open="isOpen as boolean"
></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-1 flex-col items-center justify-center 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 } from "vue"
import { useI18n } from "~/composables/i18n"
import { SmartTreeAdapter, TreeNode } from "~/helpers/treeAdapter"
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>
}>()
const t = useI18n()
/**
* Fetch the root nodes from the adapter by passing the node id as null
*/
const rootNodes = computed(() => props.adapter.getChildren(null).value)
</script>

View File

@@ -0,0 +1,103 @@
<template>
<slot
:node="nodeItem"
:toggle-children="toggleNodeChildren"
:is-open="isNodeOpen"
></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-1 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"
>
<TreeBranch
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 }">
<!-- Casting to help with type checking -->
<slot
:node="node as TreeNode<T>"
:toggle-children="toggleChildren as () => void"
:is-open="isOpen as boolean"
></slot>
</template>
<template #emptyNode="{ node }">
<slot name="emptyNode" :node="node"></slot>
</template>
</TreeBranch>
</div>
<div
v-if="childNodes.status === 'loading'"
class="flex flex-1 flex-col items-center justify-center 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, ref } from "vue"
import { useI18n } from "~/composables/i18n"
import { SmartTreeAdapter, TreeNode } from "~/helpers/treeAdapter"
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>
}>()
const CHILD_SLOT_NAME = "default"
const t = useI18n()
/**
* Marks whether the children on this branch were ever rendered
* See the usage inside '<template>' for more info
*/
const childrenRendered = ref(false)
const showChildren = ref(false)
const isNodeOpen = 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
}
</script>