Files
simrsx-fe/app/composables/usePaginatedList.ts
Khafid Prayoga ccefb69f0c 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.
2025-11-28 12:45:45 +07:00

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()}`
}