@asteby/metacore-runtime-react 23.9.1 → 23.9.2
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 +9 -0
- package/dist/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +44 -26
- package/dist/use-infinite-scroll.d.ts.map +1 -1
- package/dist/use-infinite-scroll.js +12 -0
- package/package.json +1 -1
- package/src/dynamic-table.tsx +37 -36
- package/src/use-infinite-scroll.ts +12 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 23.9.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 38856fc: fix(data-table/kanban): filtros con un solo indicador y scroll infinito sin atascos
|
|
8
|
+
- La tabla ya no renderiza la fila de chips de filtros activos: el indicador del header de columna es la única señal (los chips seguían duplicando la info).
|
|
9
|
+
- La tabla re-sincroniza sus filtros cuando el router del host reescribe la query string sin remontar (p. ej. entradas hermanas del sidebar que deep-linkean distintos `f_` sobre la misma ruta); antes quedaba mostrando el filtro anterior.
|
|
10
|
+
- El sentinel de scroll infinito encadena la siguiente página cuando una carga termina y el sentinel sigue visible (pantallas pequeñas / páginas cortas); antes se atascaba hasta que el usuario movía el scroll.
|
|
11
|
+
|
|
3
12
|
## 23.9.1
|
|
4
13
|
|
|
5
14
|
### 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,+BA8iCnB"}
|
package/dist/dynamic-table.js
CHANGED
|
@@ -25,7 +25,7 @@ import { useMetadataCache } from './metadata-cache';
|
|
|
25
25
|
import { useApi, useCurrentBranch } from './api-context';
|
|
26
26
|
import { defaultGetDynamicColumns, DATE_CELL_TYPES, aggregateOf, formatAggregateTotal } from './dynamic-columns';
|
|
27
27
|
import { useFacetLoaders, isLongTextColumn } from './use-facet-loaders';
|
|
28
|
-
import {
|
|
28
|
+
import { translateOptionLabels } from './filter-chips';
|
|
29
29
|
import { dedupeById, useInfiniteScrollSentinel } from './use-infinite-scroll';
|
|
30
30
|
import { OptionsContext } from './options-context';
|
|
31
31
|
import { getSearchableColumnKeys } from './column-visibility';
|
|
@@ -148,6 +148,10 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
|
|
|
148
148
|
if (Object.keys(filters).length > 0)
|
|
149
149
|
setDynamicFilters(filters);
|
|
150
150
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
151
|
+
// The exact query string this table last wrote to the URL — lets the
|
|
152
|
+
// resync effect below tell "our own replaceState" apart from an external
|
|
153
|
+
// rewrite by the host router.
|
|
154
|
+
const lastSelfSearch = useRef(null);
|
|
151
155
|
useEffect(() => {
|
|
152
156
|
if (!enableUrlSync || !initializedFromUrl.current)
|
|
153
157
|
return;
|
|
@@ -188,8 +192,46 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
|
|
|
188
192
|
});
|
|
189
193
|
const search = params.toString();
|
|
190
194
|
const newUrl = search ? `${window.location.pathname}?${search}` : window.location.pathname;
|
|
195
|
+
lastSelfSearch.current = search ? `?${search}` : '';
|
|
191
196
|
window.history.replaceState(null, '', newUrl);
|
|
192
197
|
}, [enableUrlSync, pagination, sorting, globalFilter, dynamicFilters, defaultFilters]);
|
|
198
|
+
// The host router can rewrite the query string WITHOUT remounting the
|
|
199
|
+
// table — e.g. sidebar sibling entries deep-link different `f_` filters
|
|
200
|
+
// into the same model route. The mount-time init above never re-runs, so
|
|
201
|
+
// the table kept showing the previous filter. On every render where the
|
|
202
|
+
// location's search differs from the last URL we ourselves wrote,
|
|
203
|
+
// re-parse the `f_` params and adopt them.
|
|
204
|
+
const locationSearch = typeof window !== 'undefined' ? window.location.search : '';
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
if (!enableUrlSync || !initializedFromUrl.current)
|
|
207
|
+
return;
|
|
208
|
+
if (locationSearch === lastSelfSearch.current)
|
|
209
|
+
return;
|
|
210
|
+
lastSelfSearch.current = locationSearch;
|
|
211
|
+
const params = new URLSearchParams(locationSearch);
|
|
212
|
+
const filters = {};
|
|
213
|
+
params.forEach((rawValue, key) => {
|
|
214
|
+
if (!key.startsWith('f_'))
|
|
215
|
+
return;
|
|
216
|
+
const filterKey = key.substring(2);
|
|
217
|
+
if (defaultFilters && filterKey in defaultFilters)
|
|
218
|
+
return;
|
|
219
|
+
const value = urlValueToInternal(rawValue);
|
|
220
|
+
if (value.startsWith('IN:'))
|
|
221
|
+
filters[filterKey] = value.substring(3).split(',');
|
|
222
|
+
else
|
|
223
|
+
filters[filterKey] = [value];
|
|
224
|
+
});
|
|
225
|
+
setDynamicFilters((prev) => {
|
|
226
|
+
if (JSON.stringify(prev) === JSON.stringify(filters))
|
|
227
|
+
return prev;
|
|
228
|
+
setPagination((p) => ({ ...p, pageIndex: 0 }));
|
|
229
|
+
return filters;
|
|
230
|
+
});
|
|
231
|
+
const search = params.get('search');
|
|
232
|
+
if (search !== null)
|
|
233
|
+
setGlobalFilter(search);
|
|
234
|
+
});
|
|
193
235
|
const prefetchOptions = useCallback(async (endpoints) => {
|
|
194
236
|
if (endpoints.length === 0)
|
|
195
237
|
return new Map();
|
|
@@ -714,30 +756,6 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
|
|
|
714
756
|
return;
|
|
715
757
|
prefetchFacets(facetFieldsSig.split('|'));
|
|
716
758
|
}, [facetFieldsSig, prefetchFacets]);
|
|
717
|
-
// Active-filter chips (shared row with the kanban). One chip per field with a
|
|
718
|
-
// selection; the label is the translated filter/column label.
|
|
719
|
-
const activeFilterChips = useMemo(() => {
|
|
720
|
-
if (!metadata)
|
|
721
|
-
return [];
|
|
722
|
-
const out = [];
|
|
723
|
-
for (const [key, config] of columnFilterConfigs) {
|
|
724
|
-
if ((config.selectedValues?.length ?? 0) === 0)
|
|
725
|
-
continue;
|
|
726
|
-
const f = metadata.filters?.find((x) => x.key === key);
|
|
727
|
-
const c = metadata.columns?.find((x) => x.key === key);
|
|
728
|
-
const rawLabel = f?.label || c?.label || key;
|
|
729
|
-
out.push({
|
|
730
|
-
key,
|
|
731
|
-
label: t(rawLabel, { defaultValue: rawLabel }),
|
|
732
|
-
config,
|
|
733
|
-
});
|
|
734
|
-
}
|
|
735
|
-
return out;
|
|
736
|
-
}, [metadata, columnFilterConfigs, t]);
|
|
737
|
-
const clearAllDynamicFilters = useCallback(() => {
|
|
738
|
-
setDynamicFilters({});
|
|
739
|
-
setPagination((prev) => ({ ...prev, pageIndex: 0 }));
|
|
740
|
-
}, []);
|
|
741
759
|
const columns = useMemo(() => {
|
|
742
760
|
if (!viewMetadata)
|
|
743
761
|
return [];
|
|
@@ -783,7 +801,7 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
|
|
|
783
801
|
if (!metadata) {
|
|
784
802
|
return _jsx("div", { className: "text-center text-muted-foreground py-8", children: "Error al cargar la configuraci\u00F3n de la tabla." });
|
|
785
803
|
}
|
|
786
|
-
return (_jsxs(OptionsContext.Provider, { value: { optionsMap }, children: [_jsxs("div", { className: 'flex flex-col h-full min-h-0 w-full', children: [
|
|
804
|
+
return (_jsxs(OptionsContext.Provider, { value: { optionsMap }, children: [_jsxs("div", { className: 'flex flex-col h-full min-h-0 w-full', children: [_jsx("div", { className: 'pb-4 shrink-0', children: _jsx(DataTableToolbar, { table: table, searchPlaceholder: metadata.searchPlaceholder || 'Buscar...', filters: filters, activeFilters: dynamicFilters, onDynamicFilterChange: handleDynamicFilterChange, dateFilter: { value: dateRange, onChange: setDateRange, placeholder: 'Filtrar por fecha' }, perPageOptions: metadata.perPageOptions, onRefresh: handleRefresh, isLoading: loadingData, selectedCount: Object.keys(rowSelection).length, onBulkDelete: () => setShowBulkDeleteConfirm(true), extraActions: _jsxs(_Fragment, { children: [viewMetadata?.canExport && (_jsxs(Button, { variant: "outline", size: "sm", className: "h-8", onClick: () => setExportOpen(true), children: [_jsx(Download, { className: "h-4 w-4 mr-1" }), " Exportar"] })), viewMetadata?.canImport && (_jsxs(Button, { variant: "outline", size: "sm", className: "h-8", onClick: () => setImportOpen(true), children: [_jsx(Upload, { className: "h-4 w-4 mr-1" }), " Importar"] }))] }) }) }), _jsxs("div", { ref: infDesktopRoot, className: 'hidden sm:block flex-1 min-h-0 overflow-auto border rounded-md bg-card', children: [_jsxs(Table, { noWrapper: true, className: cn('min-w-max w-full', aggregateColumns.length > 0 && Object.keys(footerTotals).length > 0 && 'h-full'), children: [_jsx(TableHeader, { className: 'sticky top-0 z-10', children: table.getHeaderGroups().map((headerGroup) => (_jsx(TableRow, { className: 'border-b-0 hover:bg-transparent', children: headerGroup.headers.map((header) => {
|
|
787
805
|
const isActionsColumn = header.id === 'actions';
|
|
788
806
|
return (_jsx(TableHead, { colSpan: header.colSpan, style: header.column.columnDef.size ? { width: header.column.columnDef.size } : undefined, className: cn('bg-card border-b h-10', isActionsColumn && 'sticky right-0 z-20 bg-card shadow-[-2px_0_5px_-2px_rgba(0,0,0,0.1)]'), children: header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext()) }, header.id));
|
|
789
807
|
}) }, headerGroup.id))) }), _jsx(TableBody, { children: loadingData && data.length === 0 ? (_jsx(TableSkeleton, {})) : table.getRowModel().rows?.length ? (_jsxs(_Fragment, { children: [table.getRowModel().rows.map((row) => (_jsx(TableRow, { "data-state": row.getIsSelected() && 'selected', className: cn(onRowClick && 'cursor-pointer'), onClick: onRowClick ? () => onRowClick(row.original) : undefined, children: row.getVisibleCells().map((cell) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-infinite-scroll.d.ts","sourceRoot":"","sources":["../src/use-infinite-scroll.ts"],"names":[],"mappings":"AAWA;;;GAGG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS;IAAE,EAAE,CAAC,EAAE,GAAG,CAAA;CAAE,EAC/C,QAAQ,EAAE,CAAC,EAAE,EACb,QAAQ,EAAE,CAAC,EAAE,GACZ,CAAC,EAAE,CAWL;AAED,MAAM,WAAW,wBAAwB;IACvC,wEAAwE;IACxE,UAAU,EAAE,MAAM,IAAI,CAAA;IACtB,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,kEAAkE;IAClE,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,kBAAkB,CACjC,CAAC,SAAS,WAAW,GAAG,cAAc,EACtC,CAAC,SAAS,WAAW,GAAG,cAAc;IAEtC,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC,GAAG,IAAI,CAAC,CAAA;IAClC,WAAW,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC,GAAG,IAAI,CAAC,CAAA;CACvC;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACvC,CAAC,SAAS,WAAW,GAAG,cAAc,EACtC,CAAC,SAAS,WAAW,GAAG,cAAc,EACtC,EACA,UAAU,EACV,QAAgB,EAChB,UAAgB,GACjB,EAAE,wBAAwB,GAAG,kBAAkB,CAAC,CAAC,EAAE,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"use-infinite-scroll.d.ts","sourceRoot":"","sources":["../src/use-infinite-scroll.ts"],"names":[],"mappings":"AAWA;;;GAGG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS;IAAE,EAAE,CAAC,EAAE,GAAG,CAAA;CAAE,EAC/C,QAAQ,EAAE,CAAC,EAAE,EACb,QAAQ,EAAE,CAAC,EAAE,GACZ,CAAC,EAAE,CAWL;AAED,MAAM,WAAW,wBAAwB;IACvC,wEAAwE;IACxE,UAAU,EAAE,MAAM,IAAI,CAAA;IACtB,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,kEAAkE;IAClE,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,kBAAkB,CACjC,CAAC,SAAS,WAAW,GAAG,cAAc,EACtC,CAAC,SAAS,WAAW,GAAG,cAAc;IAEtC,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC,GAAG,IAAI,CAAC,CAAA;IAClC,WAAW,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC,GAAG,IAAI,CAAC,CAAA;CACvC;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACvC,CAAC,SAAS,WAAW,GAAG,cAAc,EACtC,CAAC,SAAS,WAAW,GAAG,cAAc,EACtC,EACA,UAAU,EACV,QAAgB,EAChB,UAAgB,GACjB,EAAE,wBAAwB,GAAG,kBAAkB,CAAC,CAAC,EAAE,CAAC,CAAC,CA4CrD"}
|
|
@@ -37,6 +37,7 @@ export function useInfiniteScrollSentinel({ onLoadMore, disabled = false, rootMa
|
|
|
37
37
|
const sentinelRef = useRef(null);
|
|
38
38
|
const cb = useRef(onLoadMore);
|
|
39
39
|
const disabledRef = useRef(disabled);
|
|
40
|
+
const intersectingRef = useRef(false);
|
|
40
41
|
cb.current = onLoadMore;
|
|
41
42
|
disabledRef.current = disabled;
|
|
42
43
|
useEffect(() => {
|
|
@@ -47,6 +48,7 @@ export function useInfiniteScrollSentinel({ onLoadMore, disabled = false, rootMa
|
|
|
47
48
|
}
|
|
48
49
|
const observer = new IntersectionObserver((entries) => {
|
|
49
50
|
for (const entry of entries) {
|
|
51
|
+
intersectingRef.current = entry.isIntersecting;
|
|
50
52
|
if (entry.isIntersecting && !disabledRef.current) {
|
|
51
53
|
cb.current();
|
|
52
54
|
}
|
|
@@ -56,5 +58,15 @@ export function useInfiniteScrollSentinel({ onLoadMore, disabled = false, rootMa
|
|
|
56
58
|
return () => observer.disconnect();
|
|
57
59
|
// Re-create only when the margin changes; onLoadMore/disabled are read live.
|
|
58
60
|
}, [rootMargin]);
|
|
61
|
+
// IntersectionObserver only fires on intersection CHANGES. If a loaded page
|
|
62
|
+
// is too short to push the sentinel out of view (small screens, short
|
|
63
|
+
// lanes), the sentinel stays intersecting and no further event ever comes —
|
|
64
|
+
// the list stalls until the user jiggles the scroll. When a load finishes
|
|
65
|
+
// (disabled flips back to false) and the sentinel is still in view, chain
|
|
66
|
+
// the next page.
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (!disabled && intersectingRef.current)
|
|
69
|
+
cb.current();
|
|
70
|
+
}, [disabled]);
|
|
59
71
|
return { rootRef, sentinelRef };
|
|
60
72
|
}
|
package/package.json
CHANGED
package/src/dynamic-table.tsx
CHANGED
|
@@ -67,7 +67,7 @@ import { useApi, useCurrentBranch } from './api-context'
|
|
|
67
67
|
import type { ColumnFilterConfig, GetDynamicColumns } from './dynamic-columns-shim'
|
|
68
68
|
import { defaultGetDynamicColumns, DATE_CELL_TYPES, aggregateOf, formatAggregateTotal } from './dynamic-columns'
|
|
69
69
|
import { useFacetLoaders, isLongTextColumn } from './use-facet-loaders'
|
|
70
|
-
import {
|
|
70
|
+
import { translateOptionLabels } from './filter-chips'
|
|
71
71
|
import { dedupeById, useInfiniteScrollSentinel } from './use-infinite-scroll'
|
|
72
72
|
import { OptionsContext } from './options-context'
|
|
73
73
|
import type { TableMetadata, ApiResponse } from './types'
|
|
@@ -256,6 +256,11 @@ export function DynamicTable({
|
|
|
256
256
|
if (Object.keys(filters).length > 0) setDynamicFilters(filters)
|
|
257
257
|
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
|
258
258
|
|
|
259
|
+
// The exact query string this table last wrote to the URL — lets the
|
|
260
|
+
// resync effect below tell "our own replaceState" apart from an external
|
|
261
|
+
// rewrite by the host router.
|
|
262
|
+
const lastSelfSearch = useRef<string | null>(null)
|
|
263
|
+
|
|
259
264
|
useEffect(() => {
|
|
260
265
|
if (!enableUrlSync || !initializedFromUrl.current) return
|
|
261
266
|
const params = new URLSearchParams()
|
|
@@ -287,9 +292,40 @@ export function DynamicTable({
|
|
|
287
292
|
})
|
|
288
293
|
const search = params.toString()
|
|
289
294
|
const newUrl = search ? `${window.location.pathname}?${search}` : window.location.pathname
|
|
295
|
+
lastSelfSearch.current = search ? `?${search}` : ''
|
|
290
296
|
window.history.replaceState(null, '', newUrl)
|
|
291
297
|
}, [enableUrlSync, pagination, sorting, globalFilter, dynamicFilters, defaultFilters])
|
|
292
298
|
|
|
299
|
+
// The host router can rewrite the query string WITHOUT remounting the
|
|
300
|
+
// table — e.g. sidebar sibling entries deep-link different `f_` filters
|
|
301
|
+
// into the same model route. The mount-time init above never re-runs, so
|
|
302
|
+
// the table kept showing the previous filter. On every render where the
|
|
303
|
+
// location's search differs from the last URL we ourselves wrote,
|
|
304
|
+
// re-parse the `f_` params and adopt them.
|
|
305
|
+
const locationSearch = typeof window !== 'undefined' ? window.location.search : ''
|
|
306
|
+
useEffect(() => {
|
|
307
|
+
if (!enableUrlSync || !initializedFromUrl.current) return
|
|
308
|
+
if (locationSearch === lastSelfSearch.current) return
|
|
309
|
+
lastSelfSearch.current = locationSearch
|
|
310
|
+
const params = new URLSearchParams(locationSearch)
|
|
311
|
+
const filters: Record<string, string[]> = {}
|
|
312
|
+
params.forEach((rawValue, key) => {
|
|
313
|
+
if (!key.startsWith('f_')) return
|
|
314
|
+
const filterKey = key.substring(2)
|
|
315
|
+
if (defaultFilters && filterKey in defaultFilters) return
|
|
316
|
+
const value = urlValueToInternal(rawValue)
|
|
317
|
+
if (value.startsWith('IN:')) filters[filterKey] = value.substring(3).split(',')
|
|
318
|
+
else filters[filterKey] = [value]
|
|
319
|
+
})
|
|
320
|
+
setDynamicFilters((prev: Record<string, string[]>) => {
|
|
321
|
+
if (JSON.stringify(prev) === JSON.stringify(filters)) return prev
|
|
322
|
+
setPagination((p: PaginationState) => ({ ...p, pageIndex: 0 }))
|
|
323
|
+
return filters
|
|
324
|
+
})
|
|
325
|
+
const search = params.get('search')
|
|
326
|
+
if (search !== null) setGlobalFilter(search)
|
|
327
|
+
})
|
|
328
|
+
|
|
293
329
|
const prefetchOptions = useCallback(async (endpoints: string[]) => {
|
|
294
330
|
if (endpoints.length === 0) return new Map<string, any[]>()
|
|
295
331
|
const uniqueEndpoints = Array.from(new Set(endpoints))
|
|
@@ -790,30 +826,6 @@ export function DynamicTable({
|
|
|
790
826
|
prefetchFacets(facetFieldsSig.split('|'))
|
|
791
827
|
}, [facetFieldsSig, prefetchFacets])
|
|
792
828
|
|
|
793
|
-
// Active-filter chips (shared row with the kanban). One chip per field with a
|
|
794
|
-
// selection; the label is the translated filter/column label.
|
|
795
|
-
const activeFilterChips = useMemo<FilterChipField[]>(() => {
|
|
796
|
-
if (!metadata) return []
|
|
797
|
-
const out: FilterChipField[] = []
|
|
798
|
-
for (const [key, config] of columnFilterConfigs) {
|
|
799
|
-
if ((config.selectedValues?.length ?? 0) === 0) continue
|
|
800
|
-
const f = metadata.filters?.find((x) => x.key === key)
|
|
801
|
-
const c = metadata.columns?.find((x) => x.key === key)
|
|
802
|
-
const rawLabel = f?.label || c?.label || key
|
|
803
|
-
out.push({
|
|
804
|
-
key,
|
|
805
|
-
label: t(rawLabel, { defaultValue: rawLabel }),
|
|
806
|
-
config,
|
|
807
|
-
})
|
|
808
|
-
}
|
|
809
|
-
return out
|
|
810
|
-
}, [metadata, columnFilterConfigs, t])
|
|
811
|
-
|
|
812
|
-
const clearAllDynamicFilters = useCallback(() => {
|
|
813
|
-
setDynamicFilters({})
|
|
814
|
-
setPagination((prev: PaginationState) => ({ ...prev, pageIndex: 0 }))
|
|
815
|
-
}, [])
|
|
816
|
-
|
|
817
829
|
const columns = useMemo(() => {
|
|
818
830
|
if (!viewMetadata) return []
|
|
819
831
|
// Row-action column only renders per-row actions. Table-level placements
|
|
@@ -933,17 +945,6 @@ export function DynamicTable({
|
|
|
933
945
|
</>
|
|
934
946
|
}
|
|
935
947
|
/>
|
|
936
|
-
{/* Active-filter chips — shared with the kanban. Quick
|
|
937
|
-
visibility + one-click removal of the header filters. */}
|
|
938
|
-
{activeFilterChips.length > 0 && (
|
|
939
|
-
<div className='pt-2'>
|
|
940
|
-
<FilterChipsRow
|
|
941
|
-
fields={activeFilterChips}
|
|
942
|
-
onClearAll={clearAllDynamicFilters}
|
|
943
|
-
data-testid='table-filter-chips'
|
|
944
|
-
/>
|
|
945
|
-
</div>
|
|
946
|
-
)}
|
|
947
948
|
</div>
|
|
948
949
|
{/* Desktop: classic horizontal-scroll table. Hidden on phones —
|
|
949
950
|
a 7-column table forces a wide horizontal scroll there, so we
|
|
@@ -64,6 +64,7 @@ export function useInfiniteScrollSentinel<
|
|
|
64
64
|
const sentinelRef = useRef<S | null>(null)
|
|
65
65
|
const cb = useRef(onLoadMore)
|
|
66
66
|
const disabledRef = useRef(disabled)
|
|
67
|
+
const intersectingRef = useRef(false)
|
|
67
68
|
cb.current = onLoadMore
|
|
68
69
|
disabledRef.current = disabled
|
|
69
70
|
|
|
@@ -78,6 +79,7 @@ export function useInfiniteScrollSentinel<
|
|
|
78
79
|
const observer = new IntersectionObserver(
|
|
79
80
|
(entries) => {
|
|
80
81
|
for (const entry of entries) {
|
|
82
|
+
intersectingRef.current = entry.isIntersecting
|
|
81
83
|
if (entry.isIntersecting && !disabledRef.current) {
|
|
82
84
|
cb.current()
|
|
83
85
|
}
|
|
@@ -90,5 +92,15 @@ export function useInfiniteScrollSentinel<
|
|
|
90
92
|
// Re-create only when the margin changes; onLoadMore/disabled are read live.
|
|
91
93
|
}, [rootMargin])
|
|
92
94
|
|
|
95
|
+
// IntersectionObserver only fires on intersection CHANGES. If a loaded page
|
|
96
|
+
// is too short to push the sentinel out of view (small screens, short
|
|
97
|
+
// lanes), the sentinel stays intersecting and no further event ever comes —
|
|
98
|
+
// the list stalls until the user jiggles the scroll. When a load finishes
|
|
99
|
+
// (disabled flips back to false) and the sentinel is still in view, chain
|
|
100
|
+
// the next page.
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (!disabled && intersectingRef.current) cb.current()
|
|
103
|
+
}, [disabled])
|
|
104
|
+
|
|
93
105
|
return { rootRef, sentinelRef }
|
|
94
106
|
}
|