@asteby/metacore-runtime-react 23.9.1 → 23.9.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 +15 -0
- package/dist/dynamic-kanban.d.ts.map +1 -1
- package/dist/dynamic-kanban.js +6 -0
- package/dist/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +44 -26
- package/dist/use-i18n-resource-version.d.ts +2 -0
- package/dist/use-i18n-resource-version.d.ts.map +1 -0
- package/dist/use-i18n-resource-version.js +42 -0
- package/dist/use-infinite-scroll.d.ts.map +1 -1
- package/dist/use-infinite-scroll.js +12 -0
- package/package.json +3 -3
- package/src/dynamic-kanban.tsx +6 -0
- package/src/dynamic-table.tsx +37 -36
- package/src/use-i18n-resource-version.ts +42 -0
- package/src/use-infinite-scroll.ts +12 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 23.9.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- e67e403: fix(kanban): las etiquetas de lane dejan de mostrar la clave i18n cruda cuando el bundle del addon llega tarde. Un hook (useI18nResourceVersion) re-resuelve las etiquetas al mergearse el bundle async (addResourceBundle), sin depender del bindI18nStore del host.
|
|
8
|
+
|
|
9
|
+
## 23.9.2
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 38856fc: fix(data-table/kanban): filtros con un solo indicador y scroll infinito sin atascos
|
|
14
|
+
- 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).
|
|
15
|
+
- 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.
|
|
16
|
+
- 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.
|
|
17
|
+
|
|
3
18
|
## 23.9.1
|
|
4
19
|
|
|
5
20
|
### Patch Changes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-kanban.d.ts","sourceRoot":"","sources":["../src/dynamic-kanban.tsx"],"names":[],"mappings":"AA0BA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"dynamic-kanban.d.ts","sourceRoot":"","sources":["../src/dynamic-kanban.tsx"],"names":[],"mappings":"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"}
|
package/dist/dynamic-kanban.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { useI18nResourceVersion } from './use-i18n-resource-version';
|
|
4
5
|
import { DndContext, DragOverlay, PointerSensor, useSensor, useSensors, useDraggable, useDroppable, } from '@dnd-kit/core';
|
|
5
6
|
import { SortableContext, horizontalListSortingStrategy, useSortable, } from '@dnd-kit/sortable';
|
|
6
7
|
import { CSS } from '@dnd-kit/utilities';
|
|
@@ -1082,6 +1083,11 @@ function SortableSmartLane({ droppableDisabled, smartProps, }) {
|
|
|
1082
1083
|
}
|
|
1083
1084
|
function KanbanLane({ stage, count, totalCount, serverTotal, hasMore, loadingMore, onLoadMore, filterFields, laneFilter, onFunnelChange, onQueryChange, isDark, dimmed, dnd, model, columns, automationsAvailable, automationRules, onAutomationCreate, onAutomationUpdate, onAutomationRemove, extraFilters, configurable, onConfigure, children, }) {
|
|
1084
1085
|
const { t } = useTranslation();
|
|
1086
|
+
// Re-resolve the lane label when an addon i18n bundle lands after the board
|
|
1087
|
+
// painted (async addResourceBundle) — otherwise a manifest-key label like
|
|
1088
|
+
// "integration_github.stage.in_progress" stays raw until an unrelated
|
|
1089
|
+
// re-render. Independent of the host's react-i18next bindI18nStore config.
|
|
1090
|
+
useI18nResourceVersion();
|
|
1085
1091
|
// Infinite scroll: the sentinel lives at the bottom of the lane's own scroll
|
|
1086
1092
|
// container; a load in flight or an exhausted stage disables it.
|
|
1087
1093
|
const { rootRef, sentinelRef } = useInfiniteScrollSentinel({
|
|
@@ -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) => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-i18n-resource-version.d.ts","sourceRoot":"","sources":["../src/use-i18n-resource-version.ts"],"names":[],"mappings":"AAmBA,wBAAgB,sBAAsB,IAAI,MAAM,CAsB/C"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// A tiny hook that returns a version counter which bumps every time i18next's
|
|
2
|
+
// resource store changes (a bundle is `added`, a language `loaded`, or a key
|
|
3
|
+
// `removed`).
|
|
4
|
+
//
|
|
5
|
+
// Why this exists: addon i18n bundles are fetched and merged asynchronously
|
|
6
|
+
// (addResourceBundle) AFTER the board first paints. react-i18next only
|
|
7
|
+
// re-renders a `useTranslation()` consumer on a store mutation when the host
|
|
8
|
+
// configured `bindI18nStore` to include those events — a host-level setting the
|
|
9
|
+
// SDK cannot assume. Without it a lane whose label is a manifest i18n key
|
|
10
|
+
// (e.g. "integration_github.stage.in_progress") renders the RAW key until an
|
|
11
|
+
// unrelated re-render happens to re-run `t()`.
|
|
12
|
+
//
|
|
13
|
+
// Depending on this version inside a memo/render makes the component re-resolve
|
|
14
|
+
// its labels the moment the bundle lands, regardless of the host's
|
|
15
|
+
// react-i18next binding config. Self-contained and cheap: one listener per
|
|
16
|
+
// mounting component, torn down on unmount.
|
|
17
|
+
import { useEffect, useState } from 'react';
|
|
18
|
+
import { useTranslation } from 'react-i18next';
|
|
19
|
+
export function useI18nResourceVersion() {
|
|
20
|
+
const { i18n } = useTranslation();
|
|
21
|
+
const [version, setVersion] = useState(0);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!i18n)
|
|
24
|
+
return;
|
|
25
|
+
const bump = () => setVersion((v) => v + 1);
|
|
26
|
+
// `added`/`removed` fire on the resource STORE (addResourceBundle);
|
|
27
|
+
// `loaded` fires when a backend finishes a language; `languageChanged`
|
|
28
|
+
// covers a runtime locale flip. Listening to all keeps a label correct
|
|
29
|
+
// through every path a translation can arrive.
|
|
30
|
+
i18n.store?.on?.('added', bump);
|
|
31
|
+
i18n.store?.on?.('removed', bump);
|
|
32
|
+
i18n.on?.('loaded', bump);
|
|
33
|
+
i18n.on?.('languageChanged', bump);
|
|
34
|
+
return () => {
|
|
35
|
+
i18n.store?.off?.('added', bump);
|
|
36
|
+
i18n.store?.off?.('removed', bump);
|
|
37
|
+
i18n.off?.('loaded', bump);
|
|
38
|
+
i18n.off?.('languageChanged', bump);
|
|
39
|
+
};
|
|
40
|
+
}, [i18n]);
|
|
41
|
+
return version;
|
|
42
|
+
}
|
|
@@ -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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@asteby/metacore-runtime-react",
|
|
3
|
-
"version": "23.9.
|
|
3
|
+
"version": "23.9.3",
|
|
4
4
|
"description": "React runtime for metacore hosts — renders addon contributions dynamically",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"sonner": ">=1.7",
|
|
39
39
|
"zustand": ">=5",
|
|
40
40
|
"@asteby/metacore-sdk": "^3.2.0",
|
|
41
|
-
"@asteby/metacore-ui": "^2.9.
|
|
41
|
+
"@asteby/metacore-ui": "^2.9.3"
|
|
42
42
|
},
|
|
43
43
|
"peerDependenciesMeta": {
|
|
44
44
|
"@tanstack/react-router": {
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"vitest": "^4.0.0",
|
|
69
69
|
"zustand": "^5.0.0",
|
|
70
70
|
"@asteby/metacore-sdk": "3.2.0",
|
|
71
|
-
"@asteby/metacore-ui": "2.9.
|
|
71
|
+
"@asteby/metacore-ui": "2.9.3"
|
|
72
72
|
},
|
|
73
73
|
"scripts": {
|
|
74
74
|
"build": "tsc -p tsconfig.json",
|
package/src/dynamic-kanban.tsx
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
import * as React from 'react'
|
|
28
28
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
29
29
|
import { useTranslation } from 'react-i18next'
|
|
30
|
+
import { useI18nResourceVersion } from './use-i18n-resource-version'
|
|
30
31
|
import {
|
|
31
32
|
DndContext,
|
|
32
33
|
DragOverlay,
|
|
@@ -1919,6 +1920,11 @@ function KanbanLane({
|
|
|
1919
1920
|
children,
|
|
1920
1921
|
}: KanbanLaneProps) {
|
|
1921
1922
|
const { t } = useTranslation()
|
|
1923
|
+
// Re-resolve the lane label when an addon i18n bundle lands after the board
|
|
1924
|
+
// painted (async addResourceBundle) — otherwise a manifest-key label like
|
|
1925
|
+
// "integration_github.stage.in_progress" stays raw until an unrelated
|
|
1926
|
+
// re-render. Independent of the host's react-i18next bindI18nStore config.
|
|
1927
|
+
useI18nResourceVersion()
|
|
1922
1928
|
// Infinite scroll: the sentinel lives at the bottom of the lane's own scroll
|
|
1923
1929
|
// container; a load in flight or an exhausted stage disables it.
|
|
1924
1930
|
const { rootRef, sentinelRef } = useInfiniteScrollSentinel({
|
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
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// A tiny hook that returns a version counter which bumps every time i18next's
|
|
2
|
+
// resource store changes (a bundle is `added`, a language `loaded`, or a key
|
|
3
|
+
// `removed`).
|
|
4
|
+
//
|
|
5
|
+
// Why this exists: addon i18n bundles are fetched and merged asynchronously
|
|
6
|
+
// (addResourceBundle) AFTER the board first paints. react-i18next only
|
|
7
|
+
// re-renders a `useTranslation()` consumer on a store mutation when the host
|
|
8
|
+
// configured `bindI18nStore` to include those events — a host-level setting the
|
|
9
|
+
// SDK cannot assume. Without it a lane whose label is a manifest i18n key
|
|
10
|
+
// (e.g. "integration_github.stage.in_progress") renders the RAW key until an
|
|
11
|
+
// unrelated re-render happens to re-run `t()`.
|
|
12
|
+
//
|
|
13
|
+
// Depending on this version inside a memo/render makes the component re-resolve
|
|
14
|
+
// its labels the moment the bundle lands, regardless of the host's
|
|
15
|
+
// react-i18next binding config. Self-contained and cheap: one listener per
|
|
16
|
+
// mounting component, torn down on unmount.
|
|
17
|
+
import { useEffect, useState } from 'react'
|
|
18
|
+
import { useTranslation } from 'react-i18next'
|
|
19
|
+
|
|
20
|
+
export function useI18nResourceVersion(): number {
|
|
21
|
+
const { i18n } = useTranslation()
|
|
22
|
+
const [version, setVersion] = useState(0)
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (!i18n) return
|
|
25
|
+
const bump = () => setVersion((v) => v + 1)
|
|
26
|
+
// `added`/`removed` fire on the resource STORE (addResourceBundle);
|
|
27
|
+
// `loaded` fires when a backend finishes a language; `languageChanged`
|
|
28
|
+
// covers a runtime locale flip. Listening to all keeps a label correct
|
|
29
|
+
// through every path a translation can arrive.
|
|
30
|
+
i18n.store?.on?.('added', bump)
|
|
31
|
+
i18n.store?.on?.('removed', bump)
|
|
32
|
+
i18n.on?.('loaded', bump)
|
|
33
|
+
i18n.on?.('languageChanged', bump)
|
|
34
|
+
return () => {
|
|
35
|
+
i18n.store?.off?.('added', bump)
|
|
36
|
+
i18n.store?.off?.('removed', bump)
|
|
37
|
+
i18n.off?.('loaded', bump)
|
|
38
|
+
i18n.off?.('languageChanged', bump)
|
|
39
|
+
}
|
|
40
|
+
}, [i18n])
|
|
41
|
+
return version
|
|
42
|
+
}
|
|
@@ -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
|
}
|