@asteby/metacore-runtime-react 23.12.1 → 23.12.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/dist/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +46 -12
- package/package.json +1 -1
- package/src/dynamic-table.tsx +47 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 23.12.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 5e46e61: fix(dynamic-table): don't strip a deep-linked filter on first mount (URL race)
|
|
8
|
+
|
|
9
|
+
The URL-write effect ran on the very first commit — when `initializedFromUrl`
|
|
10
|
+
(a ref) is already true but the init effect's `setDynamicFilters` hasn't
|
|
11
|
+
re-rendered yet — so it wrote a URL WITHOUT `f_status`, stripping a
|
|
12
|
+
deep-linked/reloaded filter and flickering the address bar until it settled
|
|
13
|
+
without the filter. Gate the write on a STATE flag (`urlSynced`) set once the
|
|
14
|
+
URL has been adopted, so the first write only happens on the render where
|
|
15
|
+
`dynamicFilters`/`pagination` already mirror the URL (a true no-op).
|
|
16
|
+
|
|
17
|
+
## 23.12.2
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- 5333315: fix(dynamic-table): don't rewrite a clean filtered deep-link (no URL flicker)
|
|
22
|
+
|
|
23
|
+
Normalise the `eq:` operator when fingerprinting the query string and stop
|
|
24
|
+
stamping `per_page` for the plain server default, so opening/clicking a
|
|
25
|
+
sidebar deep-link (`f_status=eq:in_progress&view=list`) is a true no-op: the
|
|
26
|
+
table adopts the filter without rewriting the URL to its own spelling
|
|
27
|
+
(`f_status=in_progress&per_page=15`). This removes the visible URL flicker and
|
|
28
|
+
keeps the sidebar active-state exact-href match working.
|
|
29
|
+
|
|
3
30
|
## 23.12.1
|
|
4
31
|
|
|
5
32
|
### Patch Changes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-table.d.ts","sourceRoot":"","sources":["../src/dynamic-table.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAKH,KAAK,SAAS,EAajB,MAAM,uBAAuB,CAAA;AAgC9B,OAAO,KAAK,EAAsB,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAanF,MAAM,WAAW,iBAAiB;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC7C;;;;;OAKG;IACH,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC/B,cAAc,CAAC,EAAE,GAAG,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACpC,YAAY,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,CAAA;IAC/B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;IACrC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;CAC3B;AAED,wBAAgB,YAAY,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,aAAoB,EACpB,aAAkB,EAClB,QAAQ,EACR,UAAU,EACV,cAAc,EACd,cAAc,EACd,YAAiB,EACjB,iBAA4C,EAC5C,QAAQ,EACR,QAAQ,EACR,cAAsB,GACzB,EAAE,iBAAiB,+
|
|
1
|
+
{"version":3,"file":"dynamic-table.d.ts","sourceRoot":"","sources":["../src/dynamic-table.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAKH,KAAK,SAAS,EAajB,MAAM,uBAAuB,CAAA;AAgC9B,OAAO,KAAK,EAAsB,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAanF,MAAM,WAAW,iBAAiB;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC7C;;;;;OAKG;IACH,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC/B,cAAc,CAAC,EAAE,GAAG,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACpC,YAAY,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,CAAA;IAC/B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;IACrC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;CAC3B;AAED,wBAAgB,YAAY,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,aAAoB,EACpB,aAAkB,EAClB,QAAQ,EACR,UAAU,EACV,cAAc,EACd,cAAc,EACd,YAAiB,EACjB,iBAA4C,EAC5C,QAAQ,EACR,QAAQ,EACR,cAAsB,GACzB,EAAE,iBAAiB,+BAsnCnB"}
|
package/dist/dynamic-table.js
CHANGED
|
@@ -78,6 +78,24 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
|
|
|
78
78
|
const [filterOptionsMap, setFilterOptionsMap] = useState(new Map());
|
|
79
79
|
const initializedFromUrl = useRef(false);
|
|
80
80
|
const urlHadPerPage = useRef(false);
|
|
81
|
+
// The model's server-declared default page size. When the current page size
|
|
82
|
+
// is just this default (the user hasn't paged/resized and the URL didn't
|
|
83
|
+
// pin one), we deliberately DON'T stamp `per_page` into the URL — otherwise
|
|
84
|
+
// the table appends `per_page=15` to the sidebar's clean deep-link on mount,
|
|
85
|
+
// which reads as a URL flicker and fights the router's spelling. Starts at 10
|
|
86
|
+
// to match the initial pagination state until metadata arrives.
|
|
87
|
+
const defaultPerPage = useRef(10);
|
|
88
|
+
// Has the table finished adopting its state FROM the URL yet? The write
|
|
89
|
+
// effect must not run before this flips true. `initializedFromUrl` is a ref
|
|
90
|
+
// set synchronously at the top of the init effect, so on the very first
|
|
91
|
+
// commit the write effect (which runs right after, in the SAME commit) sees
|
|
92
|
+
// `initialized=true` but `dynamicFilters` still EMPTY — the init effect's
|
|
93
|
+
// setState hasn't re-rendered yet — and writes a URL WITHOUT `f_status`,
|
|
94
|
+
// stripping a deep-linked filter (the "recarga quita el filtro y parpadea"
|
|
95
|
+
// bug). A STATE flag defers the first write to the render AFTER the URL has
|
|
96
|
+
// been adopted, where `dynamicFilters`/`pagination` already mirror the URL,
|
|
97
|
+
// so the write is a no-op and the filter never gets stripped.
|
|
98
|
+
const [urlSynced, setUrlSynced] = useState(false);
|
|
81
99
|
useEffect(() => {
|
|
82
100
|
if (prevBranchId.current !== currentBranch?.id) {
|
|
83
101
|
prevBranchId.current = currentBranch?.id;
|
|
@@ -114,18 +132,26 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
|
|
|
114
132
|
const alias = operatorToUrlAlias[prefix];
|
|
115
133
|
return alias ? `${alias}:${rest}` : value;
|
|
116
134
|
};
|
|
117
|
-
// Order- and
|
|
118
|
-
// router (TanStack)
|
|
119
|
-
// — different key order
|
|
120
|
-
// (`f_status=eq%
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
135
|
+
// Order-, encoding- and operator-independent fingerprint of a query string.
|
|
136
|
+
// The host router (TanStack) and the sidebar deep-link the filter in THEIR
|
|
137
|
+
// canonical form — different key order, percent-encoded operator colons, and
|
|
138
|
+
// the explicit `eq:` operator (`f_status=eq%3Ain_progress`) — while the table
|
|
139
|
+
// internally unwraps `eq:` for selects and would write the bare value
|
|
140
|
+
// (`f_status=in_progress`). Comparing raw (or even just decoded) strings then
|
|
141
|
+
// NEVER matches, so the table rewrites the URL on every navigation and the
|
|
142
|
+
// two spellings visibly ping-pong ("parpadeo", "Throttling navigation…" +
|
|
143
|
+
// React #185). Normalising each `f_` value through `urlValueToInternal`
|
|
144
|
+
// collapses `eq:X` and `X` to the same token, so a sorted `key=value` list is
|
|
145
|
+
// a true semantic fingerprint and the write effect can detect real no-ops —
|
|
146
|
+
// leaving the router's own spelling untouched (so the sidebar active-state
|
|
147
|
+
// match on the exact href keeps working too).
|
|
125
148
|
const canonicalSearch = (searchStr) => {
|
|
126
149
|
const p = new URLSearchParams(searchStr);
|
|
127
150
|
const entries = [];
|
|
128
|
-
p.forEach((value, key) =>
|
|
151
|
+
p.forEach((value, key) => {
|
|
152
|
+
const norm = key.startsWith('f_') ? urlValueToInternal(value) : value;
|
|
153
|
+
entries.push(`${key}=${norm}`);
|
|
154
|
+
});
|
|
129
155
|
entries.sort();
|
|
130
156
|
return entries.join('&');
|
|
131
157
|
};
|
|
@@ -166,13 +192,16 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
|
|
|
166
192
|
});
|
|
167
193
|
if (Object.keys(filters).length > 0)
|
|
168
194
|
setDynamicFilters(filters);
|
|
195
|
+
// Adopted everything the URL carries; the write effect may now run on the
|
|
196
|
+
// NEXT render (where the setState above has landed) without clobbering it.
|
|
197
|
+
setUrlSynced(true);
|
|
169
198
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
170
199
|
// The exact query string this table last wrote to the URL — lets the
|
|
171
200
|
// resync effect below tell "our own replaceState" apart from an external
|
|
172
201
|
// rewrite by the host router.
|
|
173
202
|
const lastSelfSearch = useRef(null);
|
|
174
203
|
useEffect(() => {
|
|
175
|
-
if (!enableUrlSync || !
|
|
204
|
+
if (!enableUrlSync || !urlSynced)
|
|
176
205
|
return;
|
|
177
206
|
const params = new URLSearchParams();
|
|
178
207
|
// Preserve the route-owned view params. The table rebuilds the query
|
|
@@ -191,7 +220,10 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
|
|
|
191
220
|
}
|
|
192
221
|
if (pagination.pageIndex > 0)
|
|
193
222
|
params.set('page', String(pagination.pageIndex + 1));
|
|
194
|
-
|
|
223
|
+
// Only pin per_page when it deviates from the model's default (an explicit
|
|
224
|
+
// user page-size change) or the URL already carried one — not for the
|
|
225
|
+
// plain server default, which would needlessly mutate a clean deep-link.
|
|
226
|
+
if (pagination.pageSize !== defaultPerPage.current || urlHadPerPage.current)
|
|
195
227
|
params.set('per_page', String(pagination.pageSize));
|
|
196
228
|
if (sorting.length > 0) {
|
|
197
229
|
params.set('sortBy', sorting[0].id);
|
|
@@ -223,7 +255,7 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
|
|
|
223
255
|
const newUrl = search ? `${window.location.pathname}?${search}` : window.location.pathname;
|
|
224
256
|
lastSelfSearch.current = search ? `?${search}` : '';
|
|
225
257
|
window.history.replaceState(null, '', newUrl);
|
|
226
|
-
}, [enableUrlSync, pagination, sorting, globalFilter, dynamicFilters, defaultFilters]);
|
|
258
|
+
}, [enableUrlSync, urlSynced, pagination, sorting, globalFilter, dynamicFilters, defaultFilters]);
|
|
227
259
|
// The host router can rewrite the query string WITHOUT remounting the
|
|
228
260
|
// table — e.g. sidebar sibling entries deep-link different `f_` filters
|
|
229
261
|
// into the same model route. The mount-time init above never re-runs, so
|
|
@@ -296,6 +328,7 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
|
|
|
296
328
|
// localStorage.
|
|
297
329
|
if (cached) {
|
|
298
330
|
setMetadata(cached);
|
|
331
|
+
defaultPerPage.current = cached.defaultPerPage || 10;
|
|
299
332
|
if (!urlHadPerPage.current)
|
|
300
333
|
setPagination((prev) => ({ ...prev, pageSize: cached.defaultPerPage || 10 }));
|
|
301
334
|
setLoading(false);
|
|
@@ -311,6 +344,7 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
|
|
|
311
344
|
meta = fresh;
|
|
312
345
|
setMetadata(fresh);
|
|
313
346
|
cacheMetadata(model, fresh);
|
|
347
|
+
defaultPerPage.current = fresh.defaultPerPage || 10;
|
|
314
348
|
if (!urlHadPerPage.current)
|
|
315
349
|
setPagination((prev) => ({ ...prev, pageSize: fresh.defaultPerPage || 10 }));
|
|
316
350
|
}
|
package/package.json
CHANGED
package/src/dynamic-table.tsx
CHANGED
|
@@ -190,6 +190,24 @@ export function DynamicTable({
|
|
|
190
190
|
|
|
191
191
|
const initializedFromUrl = useRef(false)
|
|
192
192
|
const urlHadPerPage = useRef(false)
|
|
193
|
+
// The model's server-declared default page size. When the current page size
|
|
194
|
+
// is just this default (the user hasn't paged/resized and the URL didn't
|
|
195
|
+
// pin one), we deliberately DON'T stamp `per_page` into the URL — otherwise
|
|
196
|
+
// the table appends `per_page=15` to the sidebar's clean deep-link on mount,
|
|
197
|
+
// which reads as a URL flicker and fights the router's spelling. Starts at 10
|
|
198
|
+
// to match the initial pagination state until metadata arrives.
|
|
199
|
+
const defaultPerPage = useRef(10)
|
|
200
|
+
// Has the table finished adopting its state FROM the URL yet? The write
|
|
201
|
+
// effect must not run before this flips true. `initializedFromUrl` is a ref
|
|
202
|
+
// set synchronously at the top of the init effect, so on the very first
|
|
203
|
+
// commit the write effect (which runs right after, in the SAME commit) sees
|
|
204
|
+
// `initialized=true` but `dynamicFilters` still EMPTY — the init effect's
|
|
205
|
+
// setState hasn't re-rendered yet — and writes a URL WITHOUT `f_status`,
|
|
206
|
+
// stripping a deep-linked filter (the "recarga quita el filtro y parpadea"
|
|
207
|
+
// bug). A STATE flag defers the first write to the render AFTER the URL has
|
|
208
|
+
// been adopted, where `dynamicFilters`/`pagination` already mirror the URL,
|
|
209
|
+
// so the write is a no-op and the filter never gets stripped.
|
|
210
|
+
const [urlSynced, setUrlSynced] = useState(false)
|
|
193
211
|
|
|
194
212
|
useEffect(() => {
|
|
195
213
|
if (prevBranchId.current !== currentBranch?.id) {
|
|
@@ -229,18 +247,26 @@ export function DynamicTable({
|
|
|
229
247
|
return alias ? `${alias}:${rest}` : value
|
|
230
248
|
}
|
|
231
249
|
|
|
232
|
-
// Order- and
|
|
233
|
-
// router (TanStack)
|
|
234
|
-
// — different key order
|
|
235
|
-
// (`f_status=eq%
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
//
|
|
250
|
+
// Order-, encoding- and operator-independent fingerprint of a query string.
|
|
251
|
+
// The host router (TanStack) and the sidebar deep-link the filter in THEIR
|
|
252
|
+
// canonical form — different key order, percent-encoded operator colons, and
|
|
253
|
+
// the explicit `eq:` operator (`f_status=eq%3Ain_progress`) — while the table
|
|
254
|
+
// internally unwraps `eq:` for selects and would write the bare value
|
|
255
|
+
// (`f_status=in_progress`). Comparing raw (or even just decoded) strings then
|
|
256
|
+
// NEVER matches, so the table rewrites the URL on every navigation and the
|
|
257
|
+
// two spellings visibly ping-pong ("parpadeo", "Throttling navigation…" +
|
|
258
|
+
// React #185). Normalising each `f_` value through `urlValueToInternal`
|
|
259
|
+
// collapses `eq:X` and `X` to the same token, so a sorted `key=value` list is
|
|
260
|
+
// a true semantic fingerprint and the write effect can detect real no-ops —
|
|
261
|
+
// leaving the router's own spelling untouched (so the sidebar active-state
|
|
262
|
+
// match on the exact href keeps working too).
|
|
240
263
|
const canonicalSearch = (searchStr: string): string => {
|
|
241
264
|
const p = new URLSearchParams(searchStr)
|
|
242
265
|
const entries: string[] = []
|
|
243
|
-
p.forEach((value, key) =>
|
|
266
|
+
p.forEach((value, key) => {
|
|
267
|
+
const norm = key.startsWith('f_') ? urlValueToInternal(value) : value
|
|
268
|
+
entries.push(`${key}=${norm}`)
|
|
269
|
+
})
|
|
244
270
|
entries.sort()
|
|
245
271
|
return entries.join('&')
|
|
246
272
|
}
|
|
@@ -274,6 +300,9 @@ export function DynamicTable({
|
|
|
274
300
|
}
|
|
275
301
|
})
|
|
276
302
|
if (Object.keys(filters).length > 0) setDynamicFilters(filters)
|
|
303
|
+
// Adopted everything the URL carries; the write effect may now run on the
|
|
304
|
+
// NEXT render (where the setState above has landed) without clobbering it.
|
|
305
|
+
setUrlSynced(true)
|
|
277
306
|
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
|
278
307
|
|
|
279
308
|
// The exact query string this table last wrote to the URL — lets the
|
|
@@ -282,7 +311,7 @@ export function DynamicTable({
|
|
|
282
311
|
const lastSelfSearch = useRef<string | null>(null)
|
|
283
312
|
|
|
284
313
|
useEffect(() => {
|
|
285
|
-
if (!enableUrlSync || !
|
|
314
|
+
if (!enableUrlSync || !urlSynced) return
|
|
286
315
|
const params = new URLSearchParams()
|
|
287
316
|
// Preserve the route-owned view params. The table rebuilds the query
|
|
288
317
|
// string from scratch (it only knows its own page/sort/filter keys), but
|
|
@@ -298,7 +327,11 @@ export function DynamicTable({
|
|
|
298
327
|
if (v) params.set(key, v)
|
|
299
328
|
}
|
|
300
329
|
if (pagination.pageIndex > 0) params.set('page', String(pagination.pageIndex + 1))
|
|
301
|
-
|
|
330
|
+
// Only pin per_page when it deviates from the model's default (an explicit
|
|
331
|
+
// user page-size change) or the URL already carried one — not for the
|
|
332
|
+
// plain server default, which would needlessly mutate a clean deep-link.
|
|
333
|
+
if (pagination.pageSize !== defaultPerPage.current || urlHadPerPage.current)
|
|
334
|
+
params.set('per_page', String(pagination.pageSize))
|
|
302
335
|
if (sorting.length > 0) {
|
|
303
336
|
params.set('sortBy', sorting[0].id)
|
|
304
337
|
params.set('order', sorting[0].desc ? 'desc' : 'asc')
|
|
@@ -324,7 +357,7 @@ export function DynamicTable({
|
|
|
324
357
|
const newUrl = search ? `${window.location.pathname}?${search}` : window.location.pathname
|
|
325
358
|
lastSelfSearch.current = search ? `?${search}` : ''
|
|
326
359
|
window.history.replaceState(null, '', newUrl)
|
|
327
|
-
}, [enableUrlSync, pagination, sorting, globalFilter, dynamicFilters, defaultFilters])
|
|
360
|
+
}, [enableUrlSync, urlSynced, pagination, sorting, globalFilter, dynamicFilters, defaultFilters])
|
|
328
361
|
|
|
329
362
|
// The host router can rewrite the query string WITHOUT remounting the
|
|
330
363
|
// table — e.g. sidebar sibling entries deep-link different `f_` filters
|
|
@@ -389,6 +422,7 @@ export function DynamicTable({
|
|
|
389
422
|
// localStorage.
|
|
390
423
|
if (cached) {
|
|
391
424
|
setMetadata(cached)
|
|
425
|
+
defaultPerPage.current = cached.defaultPerPage || 10
|
|
392
426
|
if (!urlHadPerPage.current) setPagination((prev: PaginationState) => ({ ...prev, pageSize: cached.defaultPerPage || 10 }))
|
|
393
427
|
setLoading(false)
|
|
394
428
|
} else {
|
|
@@ -402,6 +436,7 @@ export function DynamicTable({
|
|
|
402
436
|
meta = fresh
|
|
403
437
|
setMetadata(fresh)
|
|
404
438
|
cacheMetadata(model, fresh)
|
|
439
|
+
defaultPerPage.current = fresh.defaultPerPage || 10
|
|
405
440
|
if (!urlHadPerPage.current) setPagination((prev: PaginationState) => ({ ...prev, pageSize: fresh.defaultPerPage || 10 }))
|
|
406
441
|
}
|
|
407
442
|
} catch (error) {
|