adjustment on picker dialog procedure

refactor(procedure-picker): improve component structure and add readonly support

- Reorganize imports and add type imports section
- Replace custom Button with ButtonAction component
- Add readonly state handling for fields and buttons
- Improve type safety with ProcedureSrc type casting

feat(usePaginatedList): add syncToUrl option for nested components

Add syncToUrl option to disable URL synchronization when used in modals or nested components to prevent overriding parent page URL. Default remains true for backward compatibility.

Also includes minor formatting improvements in procedure-list.vue template.
This commit is contained in:
Khafid Prayoga
2025-11-28 10:36:30 +07:00
parent 1fbd20d9ae
commit ccefb69f0c
4 changed files with 77 additions and 55 deletions
@@ -45,6 +45,7 @@ const {
handleSearch,
fetchData: getItemList,
} = usePaginatedList({
syncToUrl: false,
fetchFn: async (params: any) => {
const result = await getList({
search: params.search,
@@ -102,19 +103,23 @@ onMounted(async () => {
</script>
<template>
<Dialog v-model:open="isModalOpen" title="" size="xl">
<Header
v-model="searchInput"
:prep="headerPrep"
:ref-search-nav="headerPrep.refSearchNav"
@search="handleSearch"
class=""
/>
<AppProcedureSrcList
:table-config="config"
:data="data"
:pagination-meta="paginationMeta"
@page-change="handlePageChange"
/>
<Dialog
v-model:open="isModalOpen"
title=""
size="xl"
>
<Header
v-model="searchInput"
:prep="headerPrep"
:ref-search-nav="headerPrep.refSearchNav"
@search="handleSearch"
class=""
/>
<AppProcedureSrcList
:table-config="config"
:data="data"
:pagination-meta="paginationMeta"
@page-change="handlePageChange"
/>
</Dialog>
</template>
@@ -1,23 +1,24 @@
<script setup lang="ts">
import ProcedureListDialog from './procedure-list.vue'
import type { FormErrors } from '~/types/error'
import { toTypedSchema } from '@vee-validate/zod'
import { Form } from '~/components/pub/ui/form'
import { FieldArray } from 'vee-validate'
import InputBase from '~/components/pub/my-ui/form/input-base.vue'
import * as DE from '~/components/pub/my-ui/doc-entry'
import TextAreaInput from '~/components/pub/my-ui/form/text-area-input.vue'
import { cn } from '~/lib/utils'
// types
import { type ProcedureSrc } from '~/models/procedure-src'
// componenets
import { FieldArray } from 'vee-validate'
import { ButtonAction } from '~/components/pub/my-ui/form'
import TableHeader from '~/components/pub/ui/table/TableHeader.vue'
import { is } from 'date-fns/locale'
import ProcedureListDialog from './procedure-list.vue'
interface Props {
fieldName: string
title: string
subTitle?: string
isReadonly?: boolean
}
const props = defineProps<Props>()
const { isReadonly = false } = props
const isProcedurePickerDialogOpen = ref<boolean>(false)
provide(`isProcedurePickerDialogOpen`, isProcedurePickerDialogOpen)
</script>
@@ -26,19 +27,15 @@ provide(`isProcedurePickerDialogOpen`, isProcedurePickerDialogOpen)
<div class="">
<div class="mb-2 flex items-center justify-between">
<p class="mb-2 font-medium">{{ title }}</p>
<Button
type="button"
<ButtonAction
preset="add"
title="Tambah Item"
icon="i-lucide-search"
:disabled="isReadonly"
:label="subTitle || 'Pilih Diagnosis'"
:full-width-mobile="true"
@click="isProcedurePickerDialogOpen = true"
size="xs"
variant="outline"
class="border-orange-400 bg-transparent text-orange-400"
>
<Icon
name="i-lucide-search"
class="h-4 w-4 align-middle transition-colors"
/>
{{ subTitle || 'Pilih Diagnosis' }}
</Button>
/>
</div>
<FieldArray
@@ -61,20 +58,20 @@ provide(`isProcedurePickerDialogOpen`, isProcedurePickerDialogOpen)
v-for="(field, idx) in fields"
:key="idx"
>
<TableCell class="">{{ field.value?.name }}</TableCell>
<TableCell class="">{{ field.value?.code }}</TableCell>
<TableCell :class="cn(isReadonly && 'text-muted-foreground')">
{{ (field.value as ProcedureSrc)?.name }}
</TableCell>
<TableCell :class="cn(isReadonly && 'text-muted-foreground')">
{{ (field.value as ProcedureSrc)?.code }}
</TableCell>
<TableCell class="">
<Button
type="button"
variant="destructive"
size="sm"
<ButtonAction
:disabled="isReadonly"
preset="delete"
:title="`Hapus prosedur '${(field.value as ProcedureSrc)?.name}'`"
icon-only
@click="remove(idx)"
>
<Icon
name="i-lucide-trash-2"
class="h-4 w-4"
/>
</Button>
/>
</TableCell>
</TableRow>
</TableBody>
+28 -8
View File
@@ -2,7 +2,6 @@ import type { DataTableLoader } from '~/components/pub/my-ui/data-table/type'
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
import { refDebounced, useUrlSearchParams } from '@vueuse/core'
import * as z from 'zod'
import { is } from "date-fns/locale"
// Default query schema yang bisa digunakan semua list
export const defaultQuerySchema = z.object({
@@ -38,10 +37,23 @@ interface UsePaginatedListOptions<T = any> {
}>
// Nama endpoint untuk logging error
entityName: string
/**
* Apakah state harus disinkronkan ke URL Browser?
* Set `false` jika digunakan di dalam Modal, Drawer, atau Nested Component
* agar tidak menimpa URL halaman induk.
* @default true
*/
syncToUrl?: boolean
}
export function usePaginatedList<T = any>(options: UsePaginatedListOptions<T>) {
const { querySchema = defaultQuerySchema, defaultQuery = defaultQueryParams, fetchFn, entityName } = options
const {
querySchema = defaultQuerySchema,
defaultQuery = defaultQueryParams,
fetchFn,
entityName,
syncToUrl = true, // Default true agar behavior lama tetap jalan
} = options
// State management
const data = ref<T[]>([])
@@ -49,11 +61,19 @@ export function usePaginatedList<T = any>(options: UsePaginatedListOptions<T>) {
isTableLoading: false,
})
// URL state management
const queryParams = useUrlSearchParams('history', {
initialValue: defaultQuery,
removeFalsyValues: true,
})
let queryParams: any
if (syncToUrl) {
// Mode Halaman Utama: Sync ke URL
queryParams = useUrlSearchParams('history', {
initialValue: defaultQuery,
removeFalsyValues: true,
write: false,
})
} else {
// Mode Nested/Modal: Local Reactive State
queryParams = reactive({ ...defaultQuery })
}
const params = computed(() => {
const result = querySchema.safeParse(queryParams)
@@ -168,7 +188,7 @@ export function usePaginatedList<T = any>(options: UsePaginatedListOptions<T>) {
}
}
export function transform(endpoint: string ,params: any): string {
export function transform(endpoint: string, params: any): string {
const urlParams = new URLSearchParams()
Object.entries(params).forEach(([key, value]) => {