Files
simrsx-fe/app/components/pub/base/select-tree/tree-node.vue
Khafid Prayoga ba6485a3e7 feat(division): wip tree select component
feat(division): update division list components and add mock api

- Replace patient API endpoint with division mock endpoint
- Simplify table columns and headers for division list
- Add mock API endpoint for division list with tree/flat format

feat(select-tree): add collapsible tree select component with lazy loading

Implement a tree select component with collapsible sections and lazy loading of child items. Includes:
- Collapsible component wrappers for Vue
- Command component wrappers for combobox functionality
- Tree select item component with loading states
- Example implementation in dev page

todo:
- scroll on overflow
- long text truncate possibly with tooltip
- more than > 5 depth of child
- mutate the children lazy
- integration backend for search based text and return  keys

feat(select-tree): add command-item component for tree selection

adjust hover bg-accent (remove state on-highlighted at styling) to avoid conflict on global component

refactor(select-tree): extract TreeItem interface to shared type file

Move TreeItem interface to a dedicated type file for better code organization and reusability. Update components to import the interface and add styling improvements to the tree-select component.

adjust text size for tree to sm

refactor(select-tree): rename tree-select-item to leaf and improve component

- Rename component to better reflect its purpose as a leaf node
- Improve UI with better spacing and hover states
- Simplify toggle logic using v-model
- Add checkmark icon for selected items

checkpoint

wip
2025-09-11 09:50:18 +07:00

122 lines
3.6 KiB
Vue

<script setup lang="ts">
import type { TreeItem } from './type'
import { Check, ChevronRight, Loader2 } from 'lucide-vue-next'
import TreeView from './tree-view.vue'
const props = defineProps<{
item: TreeItem
selectedValue?: string
onFetchChildren: (parentId: string) => Promise<void>
}>()
const emit = defineEmits(['select'])
// Computed untuk memastikan reactivity pada children
const hasChildren = computed(() => props.item.children && props.item.children.length > 0)
// State terpisah untuk chevron animation dan loading
const isOpen = ref(false)
const isLoading = ref(false)
const isChevronRotated = ref(false)
function handleSelect(value: string) {
emit('select', value)
}
function handleLabelClick() {
handleSelect(props.item.value)
}
// Watch untuk handle fetch data ketika collapsible dibuka
watch(isOpen, async (newValue) => {
console.log(`[TreeNode] ${props.item.label} - isOpen changed to:`, newValue)
// Update chevron rotation berdasarkan open state
isChevronRotated.value = newValue
// Jika membuka dan belum ada children, fetch data
if (newValue && props.item.hasChildren && !props.item.children && !isLoading.value) {
console.log(`[TreeNode] Fetching children for: ${props.item.label}`)
isLoading.value = true
try {
await props.onFetchChildren(props.item.value)
console.log(`[TreeNode] Fetch completed for: ${props.item.label}`, props.item.children)
// Force reactivity update dengan nextTick
await nextTick()
} catch (error) {
console.error('Gagal memuat data anak:', error)
// Tutup kembali jika gagal fetch
isOpen.value = false
} finally {
isLoading.value = false
}
}
})
</script>
<template>
<div class="tree-node min-w-max">
<Collapsible v-model:open="isOpen" class="w-full">
<!-- Node Header -->
<div class="flex items-center justify-start w-full p-2 rounded-md hover:bg-accent gap-2">
<!-- Chevron Toggle Button -->
<CollapsibleTrigger as-child>
<Button
variant="ghost"
class="h-4 w-4 p-0 flex items-center justify-center"
>
<!-- Loading State -->
<Loader2 v-if="isLoading" class="w-4 h-4 animate-spin text-muted-foreground" />
<!-- Chevron dengan animasi terpisah -->
<ChevronRight
v-else
class="w-4 h-4 transition-transform duration-200 ease-in-out text-muted-foreground"
:class="{
'rotate-90': isChevronRotated,
}"
/>
</Button>
</CollapsibleTrigger>
<!-- Node Label -->
<span
class="text-sm font-normal cursor-pointer hover:text-primary flex-1 flex items-center justify-between"
@click="handleLabelClick"
>
{{ item.label }}
<!-- Check Icon untuk selected state -->
<Check
v-if="selectedValue === item.value"
class="w-4 h-4 text-primary ml-2 flex-shrink-0"
/>
</span>
</div>
<!-- Children Container -->
<CollapsibleContent class="pl-8">
<div v-if="!hasChildren" class="text-sm text-muted-foreground p-2">
{{ isLoading ? 'Memuat...' : 'Tidak ada data' }}
</div>
<TreeView
v-else
:data="item.children!"
:selected-value="selectedValue"
:on-fetch-children="onFetchChildren"
@select="handleSelect"
/>
</CollapsibleContent>
</Collapsible>
</div>
</template>
<style scoped>
.tree-node {
@apply w-full;
}
/* Animasi tambahan untuk smooth transition */
.tree-node .collapsible-content {
transition: all 0.2s ease-in-out;
}
</style>