Merge branch 'dev' into feat/page-cleaning
This commit is contained in:
@@ -0,0 +1,132 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { ContentHeader } from './index'
|
||||||
|
import { refDebounced } from '@vueuse/core'
|
||||||
|
|
||||||
|
const props = defineProps<ContentHeader>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:searchModelValue': [value: string]
|
||||||
|
'search': [value: string]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// Quick search
|
||||||
|
const search = ref(props.quickSearchNav?.modelValue || props.refSearchNav?.modelValue || '')
|
||||||
|
// const qsModelValue = ref(props.quickSearchNav?.modelValue || '')
|
||||||
|
// const rsModelValue = ref(props.refSearchNav?.modelValue || '')
|
||||||
|
const debouncedSearch = refDebounced(search, props.quickSearchNav?.debounceDuration || 500)
|
||||||
|
|
||||||
|
// Computed search model for v-model
|
||||||
|
const searchModel = computed({
|
||||||
|
get: () => search.value,
|
||||||
|
set: (value: string) => {
|
||||||
|
search.value = value
|
||||||
|
emit('update:searchModelValue', value)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Watch for external changes to modelValue
|
||||||
|
watch(() => props.quickSearchNav?.modelValue, (newValue) => {
|
||||||
|
if (newValue !== props.quickSearchNav?.modelValue) {
|
||||||
|
search.value = newValue || ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Watch debounced search and emit search event
|
||||||
|
watch(debouncedSearch, (newValue) => {
|
||||||
|
const minLength = props.quickSearchNav?.minLength || 3
|
||||||
|
// Only search if meets minimum length or empty (to clear search)
|
||||||
|
if (newValue.length === 0 || newValue.length >= minLength) {
|
||||||
|
emit('search', newValue)
|
||||||
|
props.refSearchNav?.onInput(newValue)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Handle clear search
|
||||||
|
function clearSearch() {
|
||||||
|
searchModel.value = ''
|
||||||
|
props.quickSearchNav?.onClear()
|
||||||
|
props.refSearchNav?.onClear()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex items-center justify-between pb-4 2xl:pb-5 ">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="ml-3 text-lg font-semibold text-gray-900">
|
||||||
|
<Icon v-if="icon" :name="icon" class="mr-2 size-4 md:size-6 align-middle" />
|
||||||
|
{{ title }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center [&>*]:ms-2">
|
||||||
|
<!-- Slot -->
|
||||||
|
<slot />
|
||||||
|
|
||||||
|
<!-- Search Section -->
|
||||||
|
<div v-if="quickSearchNav || refSearchNav" class="relative">
|
||||||
|
<Input
|
||||||
|
v-model="searchModel"
|
||||||
|
name="search"
|
||||||
|
type="text"
|
||||||
|
:class="quickSearchNav?.inputClass || refSearchNav?.inputClass"
|
||||||
|
:placeholder="quickSearchNav?.placeholder || refSearchNav?.placeholder || 'Cari (min. 3 karakter)...'"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="search.length > 0"
|
||||||
|
class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||||
|
type="button"
|
||||||
|
@click="clearSearch"
|
||||||
|
>
|
||||||
|
<Icon name="i-lucide-x" class="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="quickSearchNav && quickSearchNav.showValidationFeedback !== false && searchModel.length > 0 && searchModel.length < (quickSearchNav.minLength || 3)"
|
||||||
|
class="absolute -bottom-6 left-0 text-xs text-amber-600 whitespace-nowrap"
|
||||||
|
>
|
||||||
|
Minimal {{ quickSearchNav.minLength || 3 }} karakter untuk mencari
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Button -->
|
||||||
|
<div v-if="addNav" class="flex items-center">
|
||||||
|
<Button
|
||||||
|
class="rounded-md border border-gray-300 px-4 py-2 text-white sm:text-sm"
|
||||||
|
:class="addNav.classVal"
|
||||||
|
:variant="(addNav.variant as any) || 'default'"
|
||||||
|
@click="addNav?.onClick"
|
||||||
|
>
|
||||||
|
<Icon :name="addNav.icon || 'i-lucide-plus'" class="mr-2 h-4 w-4 align-middle" />
|
||||||
|
{{ addNav.label }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter Button -->
|
||||||
|
<div v-if="filterNav" class="flex items-center">
|
||||||
|
<Button
|
||||||
|
class="rounded-md border border-gray-300 px-4 py-2 sm:text-sm"
|
||||||
|
:class="filterNav.classVal"
|
||||||
|
:variant="(filterNav.variant as any) || 'default'"
|
||||||
|
@click="filterNav?.onClick"
|
||||||
|
>
|
||||||
|
<Icon :name="filterNav.icon || 'i-lucide-filter'" class="mr-2 h-4 w-4 align-middle" />
|
||||||
|
{{ filterNav.label }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Print Button -->
|
||||||
|
<div v-if="printNav" class="flex items-center">
|
||||||
|
<Button
|
||||||
|
class="rounded-md border border-gray-300 px-4 py-2 sm:text-sm"
|
||||||
|
:class="printNav.classVal"
|
||||||
|
:variant="(printNav.variant as any) || 'default'"
|
||||||
|
@click="printNav?.onClick"
|
||||||
|
>
|
||||||
|
<Icon :name="printNav.icon || 'i-lucide-printer'" class="mr-2 h-4 w-4 align-middle" />
|
||||||
|
{{ printNav.label }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
export type ComponentWithProps = { component: Component, props: Record<string, any> }
|
||||||
|
|
||||||
|
export interface ButtonNav {
|
||||||
|
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'
|
||||||
|
classVal?: string
|
||||||
|
classValExt?: string
|
||||||
|
icon?: string
|
||||||
|
label: string
|
||||||
|
onClick?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
// can type directly
|
||||||
|
export interface QuickSearchNav {
|
||||||
|
modelValue?: string
|
||||||
|
placeholder?: string
|
||||||
|
inputClass?: string
|
||||||
|
inputPlaceHolder?: string
|
||||||
|
minLength?: number
|
||||||
|
btnClass?: string
|
||||||
|
btnIcon?: string
|
||||||
|
btnLabel?: string
|
||||||
|
showValidationFeedback?: boolean
|
||||||
|
debounceDuration?: number
|
||||||
|
searchParams: object
|
||||||
|
onSubmit?: (searchParams: object) => void
|
||||||
|
onClear: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
// callback on event
|
||||||
|
export interface RefSearchNav {
|
||||||
|
modelValue?: string
|
||||||
|
placeholder?: string
|
||||||
|
inputClass?: string
|
||||||
|
inputPlaceHolder?: string
|
||||||
|
btnClass?: string
|
||||||
|
btnIcon?: string
|
||||||
|
onInput: (val: string) => void
|
||||||
|
onClick: () => void
|
||||||
|
onClear: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RefExportNav {
|
||||||
|
onExportPdf?: () => void
|
||||||
|
onExportCsv?: () => void
|
||||||
|
onExportExcel?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContentHeader {
|
||||||
|
title?: string
|
||||||
|
icon?: string
|
||||||
|
components?: ComponentWithProps[]
|
||||||
|
quickSearchNav?: QuickSearchNav
|
||||||
|
refSearchNav?: RefSearchNav // either ref or quick
|
||||||
|
filterNav?: ButtonNav
|
||||||
|
addNav?: ButtonNav
|
||||||
|
printNav?: ButtonNav
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user