PluginProbe
The Innovative Form Builder – IvyForms / 1.1
The Innovative Form Builder – IvyForms v1.1
1.5.1 1.5 1.4.1 1.4 trunk 0.1.2 0.2 0.2.1 0.3 0.3.1 0.4 0.5 0.6 0.6.1 0.6.1-backup 0.6.1.1 0.7 0.8 0.8.1 0.8.2 0.9 0.9.1 1.0 1.1 1.1.1 All 27 releases
ivyforms / frontend / src / composables / useTableUrlState.ts

useTableUrlState.ts in The Innovative Form Builder – IvyForms 1.1, at frontend/src/composables/useTableUrlState.ts

364 lines 8.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { reactive, watch, type Ref, onMounted } from 'vue'
2 import { useRoute, useRouter, type LocationQueryRaw } from 'vue-router'
3
4 export type TableFilterValue = string | number | boolean | null
5
6 export interface TableFilters {
7 [key: string]: TableFilterValue
8 }
9
10 export interface TablePagination {
11 page: number
12 perPage: number
13 }
14
15 export interface TableSort {
16 column: string | null
17 order: 'asc' | 'desc' | null
18 }
19
20 export interface TableUrlStateOptions<TFilters extends TableFilters = TableFilters> {
21 filters?: TFilters
22 pagination?: TablePagination
23 sort?: TableSort
24 searchQuery?: Ref<string>
25 dateRange?: Ref<[string, string] | null>
26 preserveOtherParams?: boolean
27 onRestored?: () => void
28 }
29
30 /**
31 * Composable for syncing table state (filters, pagination, sorting, search) with URL query parameters
32 * This ensures table state is preserved when navigating between pages
33 */
34 export function useTableUrlState<TFilters extends TableFilters = TableFilters>(
35 options: TableUrlStateOptions<TFilters> = {},
36 ) {
37 const route = useRoute()
38 const router = useRouter()
39
40 const filters = (options.filters ?? reactive({})) as TFilters
41 const pagination = options.pagination ?? reactive({ page: 1, perPage: 10 })
42 const sort = options.sort ?? reactive({ column: null, order: null })
43 const searchQuery = options.searchQuery
44 const dateRange = options.dateRange
45 const preserveOtherParams = options.preserveOtherParams ?? true
46 const onRestored = options.onRestored
47
48 // Store default values
49 const defaultPagination = { page: pagination.page, perPage: pagination.perPage }
50 const defaultSort = { column: sort.column, order: sort.order }
51 const defaultFilters = { ...filters }
52 const mutableFilters = filters as TableFilters
53
54 let isInitializing = true
55 let skipNextUpdate = false
56 let isRestoringFromUrl = false
57 let isUpdatingUrl = false
58 let suppressUpdates = false
59
60 const replaceQuery = (query: LocationQueryRaw) => {
61 isUpdatingUrl = true
62 router.replace({ query }).finally(() => {
63 isUpdatingUrl = false
64 })
65 }
66
67 /**
68 * Temporarily suppress URL updates for a block of code.
69 * @param fn - The function to execute with updates suppressed
70 */
71 const withSuppressedUpdates = async (fn: () => Promise<void> | void) => {
72 suppressUpdates = true
73 try {
74 await fn()
75 } finally {
76 suppressUpdates = false
77 }
78 }
79
80 /**
81 * Parse URL query parameters and restore table state
82 */
83 const restoreFromUrl = () => {
84 isRestoringFromUrl = true
85 const query = route.query
86
87 // Restore pagination
88 if (query.page) {
89 const page = parseInt(query.page as string, 10)
90 if (!isNaN(page) && page > 0) {
91 pagination.page = page
92 }
93 } else {
94 // Use default if not in URL
95 pagination.page = defaultPagination.page
96 }
97
98 if (query.perPage) {
99 const perPage = parseInt(query.perPage as string, 10)
100 if (!isNaN(perPage) && perPage > 0) {
101 pagination.perPage = perPage
102 }
103 } else {
104 // Use default if not in URL
105 pagination.perPage = defaultPagination.perPage
106 }
107
108 // Restore sorting
109 if (query.sortColumn) {
110 sort.column = query.sortColumn as string
111 } else {
112 // Use default if not in URL
113 sort.column = defaultSort.column
114 }
115
116 if (query.sortOrder && (query.sortOrder === 'asc' || query.sortOrder === 'desc')) {
117 sort.order = query.sortOrder as 'asc' | 'desc'
118 } else {
119 // Use default if not in URL
120 sort.order = defaultSort.order
121 }
122
123 // Restore search query
124 if (searchQuery) {
125 if (query.search) {
126 searchQuery.value = query.search as string
127 } else {
128 searchQuery.value = ''
129 }
130 }
131
132 // Restore date range
133 if (dateRange) {
134 if (query.dateFrom && query.dateTo) {
135 dateRange.value = [query.dateFrom as string, query.dateTo as string]
136 } else {
137 dateRange.value = null
138 }
139 }
140
141 // Restore filters
142 Object.keys(filters).forEach((key) => {
143 const queryValue = query[key]
144 if (queryValue !== undefined) {
145 if (queryValue === 'true') {
146 mutableFilters[key] = true
147 } else if (queryValue === 'false') {
148 mutableFilters[key] = false
149 } else if (queryValue === 'null') {
150 mutableFilters[key] = null
151 } else {
152 mutableFilters[key] = queryValue as TableFilterValue
153 }
154 } else {
155 // Use default if not in URL
156 mutableFilters[key] = defaultFilters[key] ?? null
157 }
158 })
159
160 isRestoringFromUrl = false
161 if (!isInitializing && onRestored) {
162 onRestored()
163 }
164 }
165
166 /**
167 * Update URL with current table state
168 */
169 const updateUrl = () => {
170 if (skipNextUpdate || isRestoringFromUrl || suppressUpdates) {
171 skipNextUpdate = false
172 return
173 }
174
175 const query: LocationQueryRaw = {}
176
177 // Preserve other query params if needed
178 if (preserveOtherParams) {
179 Object.keys(route.query).forEach((key) => {
180 // Don't preserve our managed params
181 if (
182 !['page', 'perPage', 'sortColumn', 'sortOrder', 'search', 'dateFrom', 'dateTo'].includes(
183 key,
184 ) &&
185 !Object.keys(filters).includes(key)
186 ) {
187 query[key] = route.query[key]
188 }
189 })
190 }
191
192 // Add pagination
193 if (pagination.page !== defaultPagination.page) {
194 query.page = pagination.page.toString()
195 }
196 if (pagination.perPage !== defaultPagination.perPage) {
197 query.perPage = pagination.perPage.toString()
198 }
199
200 // Add sorting
201 if (sort.column && sort.column !== defaultSort.column) {
202 query.sortColumn = sort.column
203 }
204 if (sort.order && sort.order !== defaultSort.order) {
205 query.sortOrder = sort.order
206 }
207
208 // Add search query
209 if (searchQuery?.value) {
210 query.search = searchQuery.value
211 }
212
213 // Add date range
214 if (dateRange?.value && Array.isArray(dateRange.value) && dateRange.value.length === 2) {
215 query.dateFrom = dateRange.value[0]
216 query.dateTo = dateRange.value[1]
217 }
218
219 // Add filters
220 Object.keys(filters).forEach((key) => {
221 const value = filters[key]
222 if (value !== null && value !== undefined && value !== defaultFilters[key]) {
223 query[key] = String(value)
224 }
225 })
226
227 // Update URL without triggering navigation
228 replaceQuery(query)
229 }
230
231 /**
232 * Reset all table state and URL parameters
233 */
234 const resetState = () => {
235 skipNextUpdate = true
236
237 // Reset pagination
238 pagination.page = 1
239 pagination.perPage = 10
240
241 // Reset sorting
242 sort.column = null
243 sort.order = null
244
245 // Reset search
246 if (searchQuery) {
247 searchQuery.value = ''
248 }
249
250 // Reset date range
251 if (dateRange) {
252 dateRange.value = null
253 }
254
255 // Reset filters
256 Object.keys(filters).forEach((key) => {
257 mutableFilters[key] = null
258 })
259
260 // Clear URL params (preserve other params if needed)
261 const query: LocationQueryRaw = {}
262 if (preserveOtherParams) {
263 Object.keys(route.query).forEach((key) => {
264 if (
265 !['page', 'perPage', 'sortColumn', 'sortOrder', 'search', 'dateFrom', 'dateTo'].includes(
266 key,
267 ) &&
268 !Object.keys(filters).includes(key)
269 ) {
270 query[key] = route.query[key]
271 }
272 })
273 }
274
275 replaceQuery(query)
276 }
277
278 // Watch for route query changes (browser back/forward, direct navigation)
279 watch(
280 () => route.query,
281 () => {
282 if (isUpdatingUrl) {
283 return
284 }
285 if (!isRestoringFromUrl && !isInitializing) {
286 restoreFromUrl()
287 }
288 },
289 { deep: true },
290 )
291
292 // Watch for changes and update URL (after router is ready)
293 onMounted(() => {
294 router.isReady().then(() => {
295 restoreFromUrl()
296 isInitializing = false
297 if (onRestored) {
298 onRestored()
299 }
300
301 // Watch pagination
302 watch(
303 () => [pagination.page, pagination.perPage],
304 () => {
305 if (!isInitializing && !isRestoringFromUrl) {
306 updateUrl()
307 }
308 },
309 { deep: true },
310 )
311
312 // Watch sorting
313 watch(
314 () => [sort.column, sort.order],
315 () => {
316 if (!isInitializing && !isRestoringFromUrl) {
317 updateUrl()
318 }
319 },
320 { deep: true },
321 )
322
323 // Watch search query
324 if (searchQuery) {
325 watch(searchQuery, () => {
326 if (!isInitializing && !isRestoringFromUrl) {
327 updateUrl()
328 }
329 })
330 }
331
332 // Watch date range
333 if (dateRange) {
334 watch(dateRange, () => {
335 if (!isInitializing && !isRestoringFromUrl) {
336 updateUrl()
337 }
338 })
339 }
340
341 // Watch filters
342 watch(
343 filters,
344 () => {
345 if (!isInitializing && !isRestoringFromUrl) {
346 updateUrl()
347 }
348 },
349 { deep: true },
350 )
351 })
352 })
353
354 return {
355 filters,
356 pagination,
357 sort,
358 resetState,
359 updateUrl,
360 restoreFromUrl,
361 withSuppressedUpdates,
362 }
363 }
364