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.
202 lines
5.5 KiB
TypeScript
202 lines
5.5 KiB
TypeScript
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'
|
|
|
|
// Default query schema yang bisa digunakan semua list
|
|
export const defaultQuerySchema = z.object({
|
|
search: z
|
|
.union([z.literal(''), z.string().min(3)])
|
|
.optional()
|
|
.catch(''),
|
|
'page-number': z.coerce.number().int().min(1).default(1).catch(1),
|
|
'page-size': z.coerce.number().int().min(5).max(20).default(10).catch(10),
|
|
})
|
|
|
|
export const defaultQueryParams: Record<string, any> = {
|
|
search: '',
|
|
'page-number': 1,
|
|
'page-size': 10,
|
|
}
|
|
|
|
export type DefaultQueryParams = z.infer<typeof defaultQuerySchema>
|
|
|
|
interface UsePaginatedListOptions<T = any> {
|
|
// Schema untuk validasi query parameters
|
|
querySchema?: z.ZodSchema<any>
|
|
defaultQuery?: Record<string, any>
|
|
// Fungsi untuk fetch data
|
|
fetchFn: (params: any) => Promise<{
|
|
success: boolean
|
|
body: {
|
|
data: T[]
|
|
meta: {
|
|
record_totalCount: number
|
|
}
|
|
}
|
|
}>
|
|
// 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,
|
|
syncToUrl = true, // Default true agar behavior lama tetap jalan
|
|
} = options
|
|
|
|
// State management
|
|
const data = ref<T[]>([])
|
|
const isLoading = reactive<DataTableLoader>({
|
|
isTableLoading: false,
|
|
})
|
|
|
|
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)
|
|
return result.data || defaultQuery
|
|
})
|
|
|
|
// Pagination state - computed from URL params
|
|
const paginationMeta = reactive<PaginationMeta>({
|
|
recordCount: 0,
|
|
page: params.value['page-number'],
|
|
pageSize: params.value['page-size'],
|
|
totalPage: 0,
|
|
hasNext: false,
|
|
hasPrev: false,
|
|
})
|
|
|
|
// Search model with debounce
|
|
const searchInput = ref(params.value.search || '')
|
|
const debouncedSearch = refDebounced(searchInput, 500) // 500ms debounce
|
|
|
|
// Functions
|
|
async function fetchData() {
|
|
if (isLoading.isTableLoading) return
|
|
|
|
isLoading.isTableLoading = true
|
|
|
|
try {
|
|
// Use current params from URL state
|
|
const currentParams = params.value
|
|
|
|
const response = await fetchFn(currentParams)
|
|
|
|
if (response.success) {
|
|
const responseBody = response.body
|
|
data.value = responseBody.data || []
|
|
|
|
const pager = responseBody.meta
|
|
// Update pagination meta from response
|
|
paginationMeta.recordCount = pager.record_totalCount
|
|
paginationMeta.page = currentParams['page-number']
|
|
paginationMeta.pageSize = currentParams['page-size']
|
|
paginationMeta.totalPage = Math.ceil(pager.record_totalCount / paginationMeta.pageSize)
|
|
paginationMeta.hasNext = paginationMeta.page < paginationMeta.totalPage
|
|
paginationMeta.hasPrev = paginationMeta.page > 1
|
|
}
|
|
} catch (error) {
|
|
data.value = []
|
|
paginationMeta.recordCount = 0
|
|
paginationMeta.totalPage = 0
|
|
paginationMeta.hasNext = false
|
|
paginationMeta.hasPrev = false
|
|
} finally {
|
|
isLoading.isTableLoading = false
|
|
}
|
|
}
|
|
|
|
// Handle pagination page change
|
|
function handlePageChange(page: number) {
|
|
// Update URL params - this will trigger watcher
|
|
queryParams['page-number'] = page
|
|
}
|
|
|
|
// Handle search from header component
|
|
function handleSearch(searchValue: string) {
|
|
// Update URL params - this will trigger watcher and refetch data
|
|
queryParams.search = searchValue
|
|
queryParams['page-number'] = 1 // Reset to first page when searching
|
|
}
|
|
|
|
// Watchers
|
|
// Watch for URL param changes and trigger refetch
|
|
watch(
|
|
params,
|
|
(newParams) => {
|
|
console.log('watch ~ newParams', newParams)
|
|
// Sync search input with URL params (for back/forward navigation)
|
|
if (newParams.search !== searchInput.value) {
|
|
searchInput.value = newParams.search || ''
|
|
}
|
|
fetchData()
|
|
},
|
|
{ deep: true },
|
|
)
|
|
|
|
// Watch debounced search and update URL params (keeping for backward compatibility)
|
|
watch(debouncedSearch, (newValue) => {
|
|
// Only search if 3+ characters or empty (to clear search)
|
|
if (newValue.length === 0 || newValue.length >= 3) {
|
|
queryParams.search = newValue
|
|
queryParams['page-number'] = 1 // Reset to first page when searching
|
|
}
|
|
})
|
|
|
|
// Initialize data on mount
|
|
onMounted(() => {
|
|
fetchData()
|
|
})
|
|
|
|
return {
|
|
// State
|
|
data,
|
|
isLoading,
|
|
paginationMeta,
|
|
searchInput,
|
|
params,
|
|
queryParams,
|
|
|
|
// Functions
|
|
fetchData,
|
|
handlePageChange,
|
|
handleSearch,
|
|
}
|
|
}
|
|
|
|
export function transform(endpoint: string, params: any): string {
|
|
const urlParams = new URLSearchParams()
|
|
|
|
Object.entries(params).forEach(([key, value]) => {
|
|
if (value) {
|
|
urlParams.append(key, value.toString())
|
|
}
|
|
})
|
|
|
|
return `${endpoint}?${urlParams.toString()}`
|
|
}
|