@asteby/metacore-runtime-react 23.11.0 → 23.11.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 +12 -0
- package/dist/dynamic-form.d.ts.map +1 -1
- package/dist/dynamic-form.js +12 -6
- package/dist/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +12 -3
- package/package.json +1 -1
- package/src/dynamic-form.tsx +16 -6
- package/src/dynamic-table.tsx +12 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 23.11.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 990bb63: DynamicForm oculta los campos readonly (estado de conexión, valores escritos por sync): el servidor los posee, el form no los captura ni los envía en el submit.
|
|
8
|
+
|
|
9
|
+
## 23.11.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 6e3cf4f: DynamicTable: el sentinel de infinite scroll se detiene cuando el backend devuelve una página corta/vacía, aunque meta.total reporte más filas de las que la lista entrega (drift count vs list). Elimina el skeleton accent parpadeando bajo las filas.
|
|
14
|
+
|
|
3
15
|
## 23.11.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-form.d.ts","sourceRoot":"","sources":["../src/dynamic-form.tsx"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAC7C,OAAO,EACH,cAAc,EACd,aAAa,EAKhB,MAAM,uBAAuB,CAAA;AAO9B,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,CAAA;AACxC,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAE5C,MAAM,WAAW,gBAAgB;IAC7B,MAAM,EAAE,cAAc,EAAE,CAAA;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACnC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/D,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACrB;AAED,wBAAgB,WAAW,CAAC,EACxB,MAAM,EACN,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,WAAuB,EACvB,WAAwB,EACxB,QAAgB,GACnB,EAAE,gBAAgB,+
|
|
1
|
+
{"version":3,"file":"dynamic-form.d.ts","sourceRoot":"","sources":["../src/dynamic-form.tsx"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAC7C,OAAO,EACH,cAAc,EACd,aAAa,EAKhB,MAAM,uBAAuB,CAAA;AAO9B,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,CAAA;AACxC,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAE5C,MAAM,WAAW,gBAAgB;IAC7B,MAAM,EAAE,cAAc,EAAE,CAAA;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACnC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/D,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACrB;AAED,wBAAgB,WAAW,CAAC,EACxB,MAAM,EACN,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,WAAuB,EACvB,WAAwB,EACxB,QAAgB,GACnB,EAAE,gBAAgB,+BA8FlB"}
|
package/dist/dynamic-form.js
CHANGED
|
@@ -19,22 +19,28 @@ export function DynamicForm({ fields, initialValues, onSubmit, onCancel, submitL
|
|
|
19
19
|
const [values, setValues] = useState({});
|
|
20
20
|
const [errors, setErrors] = useState({});
|
|
21
21
|
const [submitting, setSubmitting] = useState(false);
|
|
22
|
-
|
|
22
|
+
// Readonly fields are system-owned (connection state, sync-written values):
|
|
23
|
+
// the server writes them, so the form never captures them — hidden outright
|
|
24
|
+
// instead of rendered disabled, matching the metadata contract
|
|
25
|
+
// (ColumnDef.Readonly). They are also excluded from defaults/submit so a
|
|
26
|
+
// create never posts e.g. `status: ""` over the column default.
|
|
27
|
+
const editableFields = useMemo(() => fields.filter((f) => !f.readonly), [fields]);
|
|
28
|
+
const schema = useMemo(() => buildZodSchema(editableFields), [editableFields]);
|
|
23
29
|
// Line-items fields carrying a balance rule gate submit: an unbalanced entry
|
|
24
30
|
// (Σdebit ≠ Σcredit, or all-zero when require_nonzero) can't be saved. This
|
|
25
31
|
// is fully declarative — `evaluateBalance` returns undefined for fields with
|
|
26
32
|
// no rule, so non-balanced forms are unaffected.
|
|
27
33
|
const balanceBlocked = useMemo(() => {
|
|
28
|
-
for (const f of
|
|
34
|
+
for (const f of editableFields) {
|
|
29
35
|
const state = evaluateBalance(f, values[f.key]);
|
|
30
36
|
if (state && !state.balanced)
|
|
31
37
|
return true;
|
|
32
38
|
}
|
|
33
39
|
return false;
|
|
34
|
-
}, [
|
|
40
|
+
}, [editableFields, values]);
|
|
35
41
|
useEffect(() => {
|
|
36
42
|
const defaults = {};
|
|
37
|
-
for (const f of
|
|
43
|
+
for (const f of editableFields) {
|
|
38
44
|
if (isLineItemsField(f)) {
|
|
39
45
|
defaults[f.key] = initialValues?.[f.key] ?? f.defaultValue ?? [];
|
|
40
46
|
continue;
|
|
@@ -43,7 +49,7 @@ export function DynamicForm({ fields, initialValues, onSubmit, onCancel, submitL
|
|
|
43
49
|
}
|
|
44
50
|
setValues(defaults);
|
|
45
51
|
setErrors({});
|
|
46
|
-
}, [
|
|
52
|
+
}, [editableFields, initialValues]);
|
|
47
53
|
const update = (k, v) => setValues((prev) => ({ ...prev, [k]: v }));
|
|
48
54
|
const handleSubmit = async (e) => {
|
|
49
55
|
e.preventDefault();
|
|
@@ -73,7 +79,7 @@ export function DynamicForm({ fields, initialValues, onSubmit, onCancel, submitL
|
|
|
73
79
|
// line-items grids (and textareas) span the full width so the row table /
|
|
74
80
|
// memo gets room. Mirrors the pro look of the federated journal modal but
|
|
75
81
|
// stays fully declarative — driven only by field shape.
|
|
76
|
-
return (_jsxs("form", { onSubmit: handleSubmit, className: "grid gap-4", children: [_jsx("div", { className: "grid gap-4 sm:grid-cols-2", children:
|
|
82
|
+
return (_jsxs("form", { onSubmit: handleSubmit, className: "grid gap-4", children: [_jsx("div", { className: "grid gap-4 sm:grid-cols-2", children: editableFields.map((field) => (_jsx(FieldRow, { field: field, value: values[field.key], onChange: (v) => update(field.key, v), values: values, error: errors[field.key], initialValues: initialValues }, field.key))) }), _jsxs("div", { className: "flex justify-end gap-2 pt-2", children: [onCancel && (_jsx(Button, { type: "button", variant: "outline", onClick: onCancel, disabled: submitting || disabled, children: cancelLabel })), _jsx(Button, { type: "submit", disabled: submitting || disabled || balanceBlocked, children: submitLabel })] })] }));
|
|
77
83
|
}
|
|
78
84
|
// One form field row: label + renderer + inline error. Encapsulated as its own
|
|
79
85
|
// component so a STATIC enum select whose options are all gated out by a sibling
|
|
@@ -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,+BAujCnB"}
|
package/dist/dynamic-table.js
CHANGED
|
@@ -50,6 +50,10 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
|
|
|
50
50
|
// Infinite-scroll: a top-up page is in flight (distinct from the initial
|
|
51
51
|
// page load so only a small bottom spinner shows, not the whole-table one).
|
|
52
52
|
const [loadingMore, setLoadingMore] = useState(false);
|
|
53
|
+
// True once the backend returned a short/empty page: no more rows exist
|
|
54
|
+
// even if meta.total says otherwise (count/list drift would otherwise
|
|
55
|
+
// keep the sentinel re-firing forever). Reset by any page-1 fetch.
|
|
56
|
+
const [infExhausted, setInfExhausted] = useState(false);
|
|
53
57
|
const infPageRef = useRef(1);
|
|
54
58
|
const [optionsMap, setOptionsMap] = useState(new Map());
|
|
55
59
|
const [exportOpen, setExportOpen] = useState(false);
|
|
@@ -467,6 +471,11 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
|
|
|
467
471
|
setData((prev) => (append ? dedupeById(prev, rows) : rows));
|
|
468
472
|
if (res.data.meta)
|
|
469
473
|
setRowCount(res.data.meta.total);
|
|
474
|
+
// A short page means the backend has no more rows, even if
|
|
475
|
+
// meta.total disagrees with the visible count (count query
|
|
476
|
+
// vs list query drift, dedupe). Without this the sentinel
|
|
477
|
+
// re-fires forever on empty pages.
|
|
478
|
+
setInfExhausted(rows.length < infPageSize);
|
|
470
479
|
}
|
|
471
480
|
}
|
|
472
481
|
catch (error) {
|
|
@@ -483,19 +492,19 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
|
|
|
483
492
|
// the filters/search AND the sort (both live in buildFilterParams).
|
|
484
493
|
const filterSignature = useMemo(() => JSON.stringify(buildFilterParams()), [buildFilterParams]);
|
|
485
494
|
const loadNextPage = useCallback(() => {
|
|
486
|
-
if (loadingMore || loadingData)
|
|
495
|
+
if (loadingMore || loadingData || infExhausted)
|
|
487
496
|
return;
|
|
488
497
|
if (data.length >= rowCount)
|
|
489
498
|
return;
|
|
490
499
|
infPageRef.current += 1;
|
|
491
500
|
void fetchPage(infPageRef.current, true);
|
|
492
|
-
}, [loadingMore, loadingData, data.length, rowCount, fetchPage]);
|
|
501
|
+
}, [loadingMore, loadingData, infExhausted, data.length, rowCount, fetchPage]);
|
|
493
502
|
// Infinite-scroll sentinels. There are two scroll containers (desktop
|
|
494
503
|
// table + mobile card list) but only one is laid out at a time — the CSS
|
|
495
504
|
// `hidden`/`sm:hidden` container has no box, so its observer never fires.
|
|
496
505
|
// Each sentinel drives the SAME `loadNextPage`; its internal guards + the
|
|
497
506
|
// `disabled` flag keep concurrent/exhausted fetches from doubling up.
|
|
498
|
-
const infScrollDisabled = !infiniteScroll || loadingMore || loadingData || data.length >= rowCount;
|
|
507
|
+
const infScrollDisabled = !infiniteScroll || loadingMore || loadingData || infExhausted || data.length >= rowCount;
|
|
499
508
|
const { rootRef: infDesktopRoot, sentinelRef: infDesktopSentinel } = useInfiniteScrollSentinel({ onLoadMore: loadNextPage, disabled: infScrollDisabled });
|
|
500
509
|
const { rootRef: infMobileRoot, sentinelRef: infMobileSentinel } = useInfiniteScrollSentinel({ onLoadMore: loadNextPage, disabled: infScrollDisabled });
|
|
501
510
|
const initialFetchDone = useRef(false);
|
package/package.json
CHANGED
package/src/dynamic-form.tsx
CHANGED
|
@@ -58,23 +58,33 @@ export function DynamicForm({
|
|
|
58
58
|
const [errors, setErrors] = useState<Record<string, string>>({})
|
|
59
59
|
const [submitting, setSubmitting] = useState(false)
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
// Readonly fields are system-owned (connection state, sync-written values):
|
|
62
|
+
// the server writes them, so the form never captures them — hidden outright
|
|
63
|
+
// instead of rendered disabled, matching the metadata contract
|
|
64
|
+
// (ColumnDef.Readonly). They are also excluded from defaults/submit so a
|
|
65
|
+
// create never posts e.g. `status: ""` over the column default.
|
|
66
|
+
const editableFields = useMemo(
|
|
67
|
+
() => fields.filter((f) => !(f as { readonly?: boolean }).readonly),
|
|
68
|
+
[fields],
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
const schema = useMemo(() => buildZodSchema(editableFields), [editableFields])
|
|
62
72
|
|
|
63
73
|
// Line-items fields carrying a balance rule gate submit: an unbalanced entry
|
|
64
74
|
// (Σdebit ≠ Σcredit, or all-zero when require_nonzero) can't be saved. This
|
|
65
75
|
// is fully declarative — `evaluateBalance` returns undefined for fields with
|
|
66
76
|
// no rule, so non-balanced forms are unaffected.
|
|
67
77
|
const balanceBlocked = useMemo(() => {
|
|
68
|
-
for (const f of
|
|
78
|
+
for (const f of editableFields) {
|
|
69
79
|
const state = evaluateBalance(f, values[f.key])
|
|
70
80
|
if (state && !state.balanced) return true
|
|
71
81
|
}
|
|
72
82
|
return false
|
|
73
|
-
}, [
|
|
83
|
+
}, [editableFields, values])
|
|
74
84
|
|
|
75
85
|
useEffect(() => {
|
|
76
86
|
const defaults: Record<string, any> = {}
|
|
77
|
-
for (const f of
|
|
87
|
+
for (const f of editableFields) {
|
|
78
88
|
if (isLineItemsField(f)) {
|
|
79
89
|
defaults[f.key] = initialValues?.[f.key] ?? f.defaultValue ?? []
|
|
80
90
|
continue
|
|
@@ -83,7 +93,7 @@ export function DynamicForm({
|
|
|
83
93
|
}
|
|
84
94
|
setValues(defaults)
|
|
85
95
|
setErrors({})
|
|
86
|
-
}, [
|
|
96
|
+
}, [editableFields, initialValues])
|
|
87
97
|
|
|
88
98
|
const update = (k: string, v: any) =>
|
|
89
99
|
setValues((prev: Record<string, any>) => ({ ...prev, [k]: v }))
|
|
@@ -113,7 +123,7 @@ export function DynamicForm({
|
|
|
113
123
|
return (
|
|
114
124
|
<form onSubmit={handleSubmit} className="grid gap-4">
|
|
115
125
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
116
|
-
{
|
|
126
|
+
{editableFields.map((field) => (
|
|
117
127
|
<FieldRow
|
|
118
128
|
key={field.key}
|
|
119
129
|
field={field}
|
package/src/dynamic-table.tsx
CHANGED
|
@@ -157,6 +157,10 @@ export function DynamicTable({
|
|
|
157
157
|
// Infinite-scroll: a top-up page is in flight (distinct from the initial
|
|
158
158
|
// page load so only a small bottom spinner shows, not the whole-table one).
|
|
159
159
|
const [loadingMore, setLoadingMore] = useState(false)
|
|
160
|
+
// True once the backend returned a short/empty page: no more rows exist
|
|
161
|
+
// even if meta.total says otherwise (count/list drift would otherwise
|
|
162
|
+
// keep the sentinel re-firing forever). Reset by any page-1 fetch.
|
|
163
|
+
const [infExhausted, setInfExhausted] = useState(false)
|
|
160
164
|
const infPageRef = useRef(1)
|
|
161
165
|
const [optionsMap, setOptionsMap] = useState<Map<string, any[]>>(new Map())
|
|
162
166
|
|
|
@@ -546,6 +550,11 @@ export function DynamicTable({
|
|
|
546
550
|
const rows = res.data.data || []
|
|
547
551
|
setData((prev) => (append ? dedupeById(prev, rows) : rows))
|
|
548
552
|
if (res.data.meta) setRowCount(res.data.meta.total)
|
|
553
|
+
// A short page means the backend has no more rows, even if
|
|
554
|
+
// meta.total disagrees with the visible count (count query
|
|
555
|
+
// vs list query drift, dedupe). Without this the sentinel
|
|
556
|
+
// re-fires forever on empty pages.
|
|
557
|
+
setInfExhausted(rows.length < infPageSize)
|
|
549
558
|
}
|
|
550
559
|
} catch (error) {
|
|
551
560
|
console.error('Error al cargar los datos', error)
|
|
@@ -565,11 +574,11 @@ export function DynamicTable({
|
|
|
565
574
|
)
|
|
566
575
|
|
|
567
576
|
const loadNextPage = useCallback(() => {
|
|
568
|
-
if (loadingMore || loadingData) return
|
|
577
|
+
if (loadingMore || loadingData || infExhausted) return
|
|
569
578
|
if (data.length >= rowCount) return
|
|
570
579
|
infPageRef.current += 1
|
|
571
580
|
void fetchPage(infPageRef.current, true)
|
|
572
|
-
}, [loadingMore, loadingData, data.length, rowCount, fetchPage])
|
|
581
|
+
}, [loadingMore, loadingData, infExhausted, data.length, rowCount, fetchPage])
|
|
573
582
|
|
|
574
583
|
// Infinite-scroll sentinels. There are two scroll containers (desktop
|
|
575
584
|
// table + mobile card list) but only one is laid out at a time — the CSS
|
|
@@ -577,7 +586,7 @@ export function DynamicTable({
|
|
|
577
586
|
// Each sentinel drives the SAME `loadNextPage`; its internal guards + the
|
|
578
587
|
// `disabled` flag keep concurrent/exhausted fetches from doubling up.
|
|
579
588
|
const infScrollDisabled =
|
|
580
|
-
!infiniteScroll || loadingMore || loadingData || data.length >= rowCount
|
|
589
|
+
!infiniteScroll || loadingMore || loadingData || infExhausted || data.length >= rowCount
|
|
581
590
|
const { rootRef: infDesktopRoot, sentinelRef: infDesktopSentinel } =
|
|
582
591
|
useInfiniteScrollSentinel({ onLoadMore: loadNextPage, disabled: infScrollDisabled })
|
|
583
592
|
const { rootRef: infMobileRoot, sentinelRef: infMobileSentinel } =
|