@rebasepro/admin 0.14.0 → 0.14.1-canary.g7e666eb
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/dist/{CollectionEditorDialog-CSheud_E.js → CollectionEditorDialog-BykOIZH4.js} +3 -3
- package/dist/{CollectionEditorDialog-CSheud_E.js.map → CollectionEditorDialog-BykOIZH4.js.map} +1 -1
- package/dist/{PropertyEditView-CWZlC1gq.js → PropertyEditView-C9Der_Ej.js} +2 -2
- package/dist/{PropertyEditView-CWZlC1gq.js.map → PropertyEditView-C9Der_Ej.js.map} +1 -1
- package/dist/{RouterCollectionsStudioView-CwTpdzWF.js → RouterCollectionsStudioView-CfTHbBCA.js} +4 -4
- package/dist/{RouterCollectionsStudioView-CwTpdzWF.js.map → RouterCollectionsStudioView-CfTHbBCA.js.map} +1 -1
- package/dist/collection_editor_ui.js +4 -4
- package/dist/components/CollectionViewBinding/CollectionViewBinding.d.ts +2 -1
- package/dist/components/CollectionViewBinding/SortButton.d.ts +14 -4
- package/dist/{export-L53WektO.js → export-BDynF1En.js} +2 -2
- package/dist/{export-L53WektO.js.map → export-BDynF1En.js.map} +1 -1
- package/dist/{history-D5SKygpJ.js → history-DS2iXiGi.js} +2 -2
- package/dist/{history-D5SKygpJ.js.map → history-DS2iXiGi.js.map} +1 -1
- package/dist/{import-C_4edkoD.js → import-J5vb86xj.js} +2 -2
- package/dist/{import-C_4edkoD.js.map → import-J5vb86xj.js.map} +1 -1
- package/dist/index.js +6 -6
- package/dist/{util-C-D5yD2n.js → util-WQP-ljKW.js} +206 -90
- package/dist/util-WQP-ljKW.js.map +1 -0
- package/package.json +9 -9
- package/src/collection_editor/ui/EditorCollectionActionStart.tsx +3 -2
- package/src/components/CollectionViewBinding/CollectionListViewBinding.tsx +80 -39
- package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +19 -8
- package/src/components/CollectionViewBinding/FilterPresetsButton.tsx +6 -5
- package/src/components/CollectionViewBinding/SortButton.tsx +149 -37
- package/src/components/SelectableTable/SelectableTable.tsx +1 -1
- package/dist/util-C-D5yD2n.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/admin",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.14.
|
|
4
|
+
"version": "0.14.1-canary.g7e666eb",
|
|
5
5
|
"description": "Rebase CMS — content management views, forms, and routing",
|
|
6
6
|
"funding": {
|
|
7
7
|
"url": "https://github.com/sponsors/rebaseco"
|
|
@@ -83,14 +83,14 @@
|
|
|
83
83
|
"react-dropzone": "^19.1.1",
|
|
84
84
|
"react-use-measure": "^2.1.7",
|
|
85
85
|
"zod": "^4.4.3",
|
|
86
|
-
"@rebasepro/
|
|
87
|
-
"@rebasepro/app": "0.14.
|
|
88
|
-
"@rebasepro/forms": "0.14.
|
|
89
|
-
"@rebasepro/inference": "0.14.
|
|
90
|
-
"@rebasepro/
|
|
91
|
-
"@rebasepro/utils": "0.14.
|
|
92
|
-
"@rebasepro/
|
|
93
|
-
"@rebasepro/
|
|
86
|
+
"@rebasepro/common": "0.14.1-canary.g7e666eb",
|
|
87
|
+
"@rebasepro/app": "0.14.1-canary.g7e666eb",
|
|
88
|
+
"@rebasepro/forms": "0.14.1-canary.g7e666eb",
|
|
89
|
+
"@rebasepro/inference": "0.14.1-canary.g7e666eb",
|
|
90
|
+
"@rebasepro/ui": "0.14.1-canary.g7e666eb",
|
|
91
|
+
"@rebasepro/utils": "0.14.1-canary.g7e666eb",
|
|
92
|
+
"@rebasepro/admin-types": "0.14.1-canary.g7e666eb",
|
|
93
|
+
"@rebasepro/types": "0.14.1-canary.g7e666eb"
|
|
94
94
|
},
|
|
95
95
|
"peerDependencies": {
|
|
96
96
|
"react": ">=19.2.7",
|
|
@@ -12,6 +12,7 @@ import { Button, SaveIcon, Tooltip, UndoIcon } from "@rebasepro/ui";
|
|
|
12
12
|
import { useCollectionEditorController } from "../useCollectionEditorController";
|
|
13
13
|
import { useCollectionsConfigController } from "../useCollectionsConfigController";
|
|
14
14
|
import { mergeDeep } from "@rebasepro/utils";
|
|
15
|
+
import { normalizeOrderBy } from "@rebasepro/common";
|
|
15
16
|
|
|
16
17
|
export function EditorCollectionActionStart({
|
|
17
18
|
path,
|
|
@@ -30,7 +31,7 @@ export function EditorCollectionActionStart({
|
|
|
30
31
|
|
|
31
32
|
let saveDefaultFilterButton = null;
|
|
32
33
|
if (!equal(getObjectOrNull(tableController.filterValues), getObjectOrNull(collection.defaultFilter)) ||
|
|
33
|
-
!equal(getObjectOrNull(tableController.sortBy), getObjectOrNull(collection.sort))) {
|
|
34
|
+
!equal(getObjectOrNull(tableController.sortBy), getObjectOrNull(normalizeOrderBy(collection.sort)))) {
|
|
34
35
|
saveDefaultFilterButton = <>
|
|
35
36
|
<Tooltip
|
|
36
37
|
asChild={true}
|
|
@@ -68,7 +69,7 @@ parentEntityIds,
|
|
|
68
69
|
if (collection?.defaultFilter)
|
|
69
70
|
tableController.setFilterValues?.(collection?.defaultFilter);
|
|
70
71
|
if (collection?.sort)
|
|
71
|
-
tableController.setSortBy?.(collection
|
|
72
|
+
tableController.setSortBy?.(normalizeOrderBy(collection.sort) as Parameters<NonNullable<typeof tableController.setSortBy>>[0]);
|
|
72
73
|
}}>
|
|
73
74
|
<UndoIcon/>
|
|
74
75
|
</Button>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
import type { Properties, Property } from "@rebasepro/types";
|
|
2
|
+
import type { OrderByTuple, Properties, Property } from "@rebasepro/types";
|
|
3
3
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { Entity } from "@rebasepro/types";
|
|
5
5
|
import { CollectionSize, EntityAction, EntityTableController, SelectionController, AdminCollection } from "@rebasepro/admin-types";
|
|
@@ -540,11 +540,11 @@ export function CollectionListViewBinding<M extends Record<string, unknown> = Re
|
|
|
540
540
|
...(columnMode ? [] : slotKeys.relationKeys)
|
|
541
541
|
]), [titleColumn, imagePropertyKey, subtitleKey, declaredColumns, columnMode, slotKeys.relationKeys]);
|
|
542
542
|
|
|
543
|
-
/** The
|
|
544
|
-
const
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
543
|
+
/** The properties the sort names, minus the ones a cell already shows. */
|
|
544
|
+
const sortedKeys = useMemo(
|
|
545
|
+
() => (sortBy ?? []).map(([key]) => key as string).filter(key => !shownKeys.has(key)),
|
|
546
|
+
[sortBy, shownKeys]
|
|
547
|
+
);
|
|
548
548
|
|
|
549
549
|
/** The properties the filters name, minus the ones a cell already shows. */
|
|
550
550
|
const filteredKeys = useMemo(
|
|
@@ -561,23 +561,28 @@ export function CollectionListViewBinding<M extends Record<string, unknown> = Re
|
|
|
561
561
|
* and clearing a sort says nothing about wanting to stop seeing the property
|
|
562
562
|
* it sorted on.
|
|
563
563
|
*
|
|
564
|
-
* What it does *not* do is accumulate.
|
|
565
|
-
* ordering by four properties in turn is changing one's mind four
|
|
566
|
-
* asking for four columns — and asking for four is how the row
|
|
567
|
-
* status and its date to a wall of numbers. A new sort therefore
|
|
568
|
-
* the
|
|
569
|
-
* the set before it. At most one sort
|
|
570
|
-
* outlive their request.
|
|
564
|
+
* What it does *not* do is accumulate. A sort names the properties it names
|
|
565
|
+
* *now*, so ordering by four properties in turn is changing one's mind four
|
|
566
|
+
* times, not asking for four columns — and asking for four is how the row
|
|
567
|
+
* lost its status and its date to a wall of numbers. A new sort therefore
|
|
568
|
+
* replaces the properties the last one left behind, and a new set of filters
|
|
569
|
+
* replaces the set before it. At most one sort's columns and one set of
|
|
570
|
+
* filter columns outlive their request.
|
|
571
571
|
*
|
|
572
572
|
* The list remounts per collection (it is keyed by path), so this is the
|
|
573
573
|
* memory of one visit to one collection, not a preference.
|
|
574
574
|
*/
|
|
575
|
-
const [
|
|
575
|
+
const [lastSortedKeys, setLastSortedKeys] = useState<string[]>([]);
|
|
576
576
|
const [lastFilteredKeys, setLastFilteredKeys] = useState<string[]>([]);
|
|
577
577
|
|
|
578
578
|
useEffect(() => {
|
|
579
|
-
if (
|
|
580
|
-
|
|
579
|
+
if (sortedKeys.length === 0) return;
|
|
580
|
+
// Same members, same array — see the filter effect below.
|
|
581
|
+
setLastSortedKeys(previous =>
|
|
582
|
+
previous.length === sortedKeys.length && previous.every((key, i) => key === sortedKeys[i])
|
|
583
|
+
? previous
|
|
584
|
+
: sortedKeys);
|
|
585
|
+
}, [sortedKeys]);
|
|
581
586
|
|
|
582
587
|
useEffect(() => {
|
|
583
588
|
if (filteredKeys.length === 0) return;
|
|
@@ -602,14 +607,14 @@ export function CollectionListViewBinding<M extends Record<string, unknown> = Re
|
|
|
602
607
|
// read in that order rather than from state alone, because the effects
|
|
603
608
|
// above have not run yet on the render that first sees a request, and
|
|
604
609
|
// waiting a frame would flash the row without its new column.
|
|
605
|
-
const active = new Set([...filteredKeys, ...
|
|
610
|
+
const active = new Set([...filteredKeys, ...sortedKeys]);
|
|
606
611
|
const keys: string[] = [];
|
|
607
612
|
const request = (key: string | undefined) => {
|
|
608
613
|
if (!key || keys.includes(key) || shownKeys.has(key)) return;
|
|
609
614
|
keys.push(key);
|
|
610
615
|
};
|
|
611
616
|
(filteredKeys.length > 0 ? filteredKeys : lastFilteredKeys).forEach(request);
|
|
612
|
-
|
|
617
|
+
(sortedKeys.length > 0 ? sortedKeys : lastSortedKeys).forEach(request);
|
|
613
618
|
|
|
614
619
|
return keys.flatMap((key, index) => {
|
|
615
620
|
const property = getResolvedPropertyInPath(resolvedCollection.properties, key) as Property | undefined;
|
|
@@ -624,7 +629,7 @@ export function CollectionListViewBinding<M extends Record<string, unknown> = Re
|
|
|
624
629
|
priority: (active.has(key) ? PRIORITY_ACTIVE_REQUEST : PRIORITY_STALE_REQUEST) - index
|
|
625
630
|
}];
|
|
626
631
|
});
|
|
627
|
-
}, [filteredKeys,
|
|
632
|
+
}, [filteredKeys, sortedKeys, lastFilteredKeys, lastSortedKeys, shownKeys, resolvedCollection, sortableKeys]);
|
|
628
633
|
|
|
629
634
|
// ── Compute list-view-visible actions per entity ──
|
|
630
635
|
const getListViewActions = useCallback((entity: Entity<M>): EntityAction[] => {
|
|
@@ -733,26 +738,51 @@ customEntityActions });
|
|
|
733
738
|
handleSelectionChange(entity, selected);
|
|
734
739
|
}, [handleSelectionChange]);
|
|
735
740
|
|
|
741
|
+
/** Where each sorted column sits in the order, so a header can show its rank. */
|
|
742
|
+
const sortIndex = useMemo(() => {
|
|
743
|
+
const index = new Map<string, { direction: "asc" | "desc"; position: number }>();
|
|
744
|
+
(sortBy ?? []).forEach(([key, direction], position) => {
|
|
745
|
+
if (!index.has(key as string)) index.set(key as string, { direction,
|
|
746
|
+
position });
|
|
747
|
+
});
|
|
748
|
+
return index;
|
|
749
|
+
}, [sortBy]);
|
|
750
|
+
|
|
736
751
|
/**
|
|
737
752
|
* Order by a column, on the same cycle a table header runs: unordered →
|
|
738
|
-
* ascending → descending → unordered
|
|
753
|
+
* ascending → descending → unordered, and shift-click to add the column
|
|
754
|
+
* under the sort already in place. Clicking a header here and clicking it
|
|
739
755
|
* in the table view have to mean the same thing, because they are the same
|
|
740
756
|
* collection under the same controller.
|
|
741
757
|
*/
|
|
742
|
-
const onColumnSort = useCallback((key: string) => {
|
|
758
|
+
const onColumnSort = useCallback((key: string, additive = false) => {
|
|
743
759
|
if (!setSortBy) return;
|
|
744
|
-
const active =
|
|
745
|
-
const next:
|
|
746
|
-
?
|
|
760
|
+
const active = sortIndex.get(key)?.direction;
|
|
761
|
+
const next: "asc" | "desc" | undefined = active === "asc"
|
|
762
|
+
? "desc"
|
|
747
763
|
: active === "desc"
|
|
748
764
|
? undefined
|
|
749
|
-
:
|
|
750
|
-
|
|
765
|
+
: "asc";
|
|
766
|
+
|
|
767
|
+
const existing = (sortBy ?? []) as OrderByTuple[];
|
|
768
|
+
let updated: OrderByTuple[] | undefined;
|
|
769
|
+
if (!additive) {
|
|
770
|
+
updated = next ? [[key, next]] : undefined;
|
|
771
|
+
} else if (next === undefined) {
|
|
772
|
+
updated = existing.filter(([existingKey]) => existingKey !== key);
|
|
773
|
+
} else if (active === undefined) {
|
|
774
|
+
updated = [...existing, [key, next]];
|
|
775
|
+
} else {
|
|
776
|
+
updated = existing.map((entry) => entry[0] === key ? [key, next] as OrderByTuple : entry);
|
|
777
|
+
}
|
|
778
|
+
if (updated?.length === 0) updated = undefined;
|
|
779
|
+
|
|
780
|
+
setSortBy(updated as Parameters<NonNullable<typeof setSortBy>>[0]);
|
|
751
781
|
// Re-ordering a partially loaded collection invalidates the pages
|
|
752
782
|
// already fetched — they are no longer the rows the query answers with —
|
|
753
783
|
// so pagination starts over, as it does for the table and the sort menu.
|
|
754
784
|
setItemCount?.(pageSize);
|
|
755
|
-
}, [setSortBy, sortBy, setItemCount, pageSize]);
|
|
785
|
+
}, [setSortBy, sortBy, sortIndex, setItemCount, pageSize]);
|
|
756
786
|
|
|
757
787
|
/**
|
|
758
788
|
* Whether a sort can be offered at all next to the filter already applied.
|
|
@@ -763,11 +793,11 @@ customEntityActions });
|
|
|
763
793
|
const sortIsAvailable = useCallback((key: string) => {
|
|
764
794
|
if (!setSortBy) return false;
|
|
765
795
|
if (!checkFilterCombination) return true;
|
|
766
|
-
const active =
|
|
796
|
+
const active = sortIndex.get(key)?.direction;
|
|
767
797
|
// Clearing the sort is always available: it asks nothing of the driver.
|
|
768
798
|
if (active === "desc") return true;
|
|
769
|
-
return checkFilterCombination(filterValues ?? {}, [key, active === "asc" ? "desc" : "asc"]);
|
|
770
|
-
}, [setSortBy, checkFilterCombination, filterValues,
|
|
799
|
+
return checkFilterCombination(filterValues ?? {}, [[key, active === "asc" ? "desc" : "asc"]]);
|
|
800
|
+
}, [setSortBy, checkFilterCombination, filterValues, sortIndex]);
|
|
771
801
|
|
|
772
802
|
const header = (titleColumn || visibleColumns.length > 0) && (
|
|
773
803
|
<ListHeader
|
|
@@ -776,7 +806,7 @@ customEntityActions });
|
|
|
776
806
|
selectionEnabled={selectionEnabled && !combineSelection}
|
|
777
807
|
showImage={showImage}
|
|
778
808
|
actionsWidth={actionsWidth}
|
|
779
|
-
|
|
809
|
+
sortIndex={sortIndex}
|
|
780
810
|
onColumnSort={onColumnSort}
|
|
781
811
|
sortIsAvailable={sortIsAvailable}
|
|
782
812
|
/>
|
|
@@ -1264,7 +1294,7 @@ function ListHeader({
|
|
|
1264
1294
|
selectionEnabled,
|
|
1265
1295
|
showImage,
|
|
1266
1296
|
actionsWidth,
|
|
1267
|
-
|
|
1297
|
+
sortIndex,
|
|
1268
1298
|
onColumnSort,
|
|
1269
1299
|
sortIsAvailable
|
|
1270
1300
|
}: {
|
|
@@ -1273,15 +1303,18 @@ function ListHeader({
|
|
|
1273
1303
|
selectionEnabled?: boolean;
|
|
1274
1304
|
showImage: boolean;
|
|
1275
1305
|
actionsWidth: number;
|
|
1276
|
-
|
|
1277
|
-
onColumnSort: (key: string) => void;
|
|
1306
|
+
sortIndex: Map<string, { direction: "asc" | "desc"; position: number }>;
|
|
1307
|
+
onColumnSort: (key: string, additive?: boolean) => void;
|
|
1278
1308
|
sortIsAvailable: (key: string) => boolean;
|
|
1279
1309
|
}) {
|
|
1280
1310
|
const headerCell = (column: ListColumn) => (
|
|
1281
1311
|
<ListHeaderLabel
|
|
1282
1312
|
column={column}
|
|
1283
|
-
direction={
|
|
1284
|
-
|
|
1313
|
+
direction={sortIndex.get(column.key)?.direction}
|
|
1314
|
+
position={sortIndex.size > 1 ? sortIndex.get(column.key)?.position : undefined}
|
|
1315
|
+
onSort={column.sortable && sortIsAvailable(column.key)
|
|
1316
|
+
? (additive: boolean) => onColumnSort(column.key, additive)
|
|
1317
|
+
: undefined}
|
|
1285
1318
|
/>
|
|
1286
1319
|
);
|
|
1287
1320
|
|
|
@@ -1329,17 +1362,25 @@ function ListHeader({
|
|
|
1329
1362
|
function ListHeaderLabel({
|
|
1330
1363
|
column,
|
|
1331
1364
|
direction,
|
|
1365
|
+
position,
|
|
1332
1366
|
onSort
|
|
1333
1367
|
}: {
|
|
1334
1368
|
column: ListColumn;
|
|
1335
1369
|
direction?: "asc" | "desc";
|
|
1336
|
-
|
|
1370
|
+
/** Rank in a multi-key sort, or `undefined` when one key says it all. */
|
|
1371
|
+
position?: number;
|
|
1372
|
+
onSort?: (additive: boolean) => void;
|
|
1337
1373
|
}) {
|
|
1338
1374
|
const content = (
|
|
1339
1375
|
<>
|
|
1340
1376
|
<span className="truncate">{column.label}</span>
|
|
1341
1377
|
{direction === "asc" && <ArrowUpIcon size={12} className="flex-shrink-0"/>}
|
|
1342
1378
|
{direction === "desc" && <ArrowDownIcon size={12} className="flex-shrink-0"/>}
|
|
1379
|
+
{direction && position !== undefined && (
|
|
1380
|
+
<span className="flex-shrink-0 text-[9px] font-bold leading-none tabular-nums">
|
|
1381
|
+
{position + 1}
|
|
1382
|
+
</span>
|
|
1383
|
+
)}
|
|
1343
1384
|
</>
|
|
1344
1385
|
);
|
|
1345
1386
|
|
|
@@ -1355,8 +1396,8 @@ function ListHeaderLabel({
|
|
|
1355
1396
|
return (
|
|
1356
1397
|
<button
|
|
1357
1398
|
type="button"
|
|
1358
|
-
title={`Sort by ${column.label}`}
|
|
1359
|
-
onClick={onSort}
|
|
1399
|
+
title={`Sort by ${column.label} — shift-click to add it under the current sort`}
|
|
1400
|
+
onClick={(event) => onSort(event.shiftKey)}
|
|
1360
1401
|
className={cls(base, tone, "cursor-pointer hover:text-surface-700 dark:hover:text-surface-200 transition-colors")}
|
|
1361
1402
|
>
|
|
1362
1403
|
{content}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { Property } from "@rebasepro/types";
|
|
1
|
+
import type { OrderByTuple, Property } from "@rebasepro/types";
|
|
2
|
+
import { serializeOrderBy } from "@rebasepro/common";
|
|
2
3
|
import type { AdditionalFieldDelegate, EntityAction, AdminCollection } from "@rebasepro/admin-types";
|
|
3
4
|
import {
|
|
4
5
|
Entity,
|
|
@@ -1322,7 +1323,7 @@ function DefaultCollectionEmptyState({
|
|
|
1322
1323
|
|
|
1323
1324
|
/**
|
|
1324
1325
|
* Inflight count request deduplication map.
|
|
1325
|
-
* Keyed by `path|filterKey|
|
|
1326
|
+
* Keyed by `path|filterKey|sort|search` so that concurrent
|
|
1326
1327
|
* callers (e.g. React StrictMode double-mount) share the same promise.
|
|
1327
1328
|
*/
|
|
1328
1329
|
const inflightCountRequests = new Map<string, Promise<number>>();
|
|
@@ -1338,7 +1339,7 @@ export function EntitiesCount({
|
|
|
1338
1339
|
path: string,
|
|
1339
1340
|
collection: AdminCollection,
|
|
1340
1341
|
filter?: FilterValues<any>,
|
|
1341
|
-
sortBy?: [
|
|
1342
|
+
sortBy?: OrderByTuple[],
|
|
1342
1343
|
/**
|
|
1343
1344
|
* Required, not optional. The term sits in the same scope as the element
|
|
1344
1345
|
* that mounts this and is passed to the toolbar and the empty state beside
|
|
@@ -1352,8 +1353,9 @@ export function EntitiesCount({
|
|
|
1352
1353
|
|
|
1353
1354
|
const dataClient = useData();
|
|
1354
1355
|
|
|
1355
|
-
|
|
1356
|
-
|
|
1356
|
+
// The whole sort as one string: it keys the dedup cache and drives the
|
|
1357
|
+
// effect, and a `sortBy` array is a new reference on every render.
|
|
1358
|
+
const sortKey = React.useMemo(() => sortBy ? serializeOrderBy(sortBy) ?? "" : "", [sortBy]);
|
|
1357
1359
|
|
|
1358
1360
|
// Use refs for values that should NOT trigger re-fetches
|
|
1359
1361
|
const dataClientRef = React.useRef(dataClient);
|
|
@@ -1375,14 +1377,16 @@ export function EntitiesCount({
|
|
|
1375
1377
|
|
|
1376
1378
|
// filterValues is already FilterValues — pass directly
|
|
1377
1379
|
const whereParams = filter && Object.keys(filter).length > 0 ? filter : undefined;
|
|
1378
|
-
const orderByParams
|
|
1380
|
+
const orderByParams = sortBy && sortBy.length > 0
|
|
1381
|
+
? sortBy.map(([field, direction]) => [String(field), direction] as OrderByTuple)
|
|
1382
|
+
: undefined;
|
|
1379
1383
|
|
|
1380
1384
|
// Deduplicate inflight count requests (e.g. React StrictMode double-mount)
|
|
1381
1385
|
// The search term is part of the query, so it is part of the key. The
|
|
1382
1386
|
// cache is module-level and outlives an unmount, so a key that omits it
|
|
1383
1387
|
// does not merely lose precision — it answers one search with a
|
|
1384
1388
|
// different search's total.
|
|
1385
|
-
const cacheKey = `${path}|${filterKey}|${
|
|
1389
|
+
const cacheKey = `${path}|${filterKey}|${sortKey}|${searchString ?? ""}`;
|
|
1386
1390
|
let countPromise = inflightCountRequests.get(cacheKey);
|
|
1387
1391
|
if (!countPromise) {
|
|
1388
1392
|
countPromise = accessor.count({
|
|
@@ -1403,7 +1407,14 @@ export function EntitiesCount({
|
|
|
1403
1407
|
});
|
|
1404
1408
|
|
|
1405
1409
|
return () => { cancelled = true; };
|
|
1406
|
-
|
|
1410
|
+
// `filterKey` and `sortKey` ARE `filter` and `sortBy` — the memoized
|
|
1411
|
+
// serializations a few lines up, and the only stable identity either
|
|
1412
|
+
// has. Both arrive as freshly built objects on every render, so
|
|
1413
|
+
// depending on them directly would re-run this effect each time and
|
|
1414
|
+
// fire a count request per render. The keys change exactly when the
|
|
1415
|
+
// values do, which is the condition this effect actually wants.
|
|
1416
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1417
|
+
}, [path, filterKey, sortKey, searchString]);
|
|
1407
1418
|
|
|
1408
1419
|
// Count is now displayed in the breadcrumb bar, this component only fetches and reports
|
|
1409
1420
|
return null;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useCallback, useMemo } from "react";
|
|
2
2
|
import { CheckIcon, ChevronsUpDownIcon, cls, FilterChip, Menu, MenuItem, Tooltip } from "@rebasepro/ui";
|
|
3
|
-
import type { FilterValues, FilterPreset } from "@rebasepro/types";
|
|
3
|
+
import type { FilterValues, FilterPreset, OrderByTuple } from "@rebasepro/types";
|
|
4
|
+
import { normalizeOrderBy } from "@rebasepro/common";
|
|
4
5
|
import type { EntityTableController, PropertyPath } from "@rebasepro/admin-types";
|
|
5
6
|
|
|
6
7
|
export interface FilterPresetsButtonProps<M extends Record<string, unknown>> {
|
|
@@ -216,19 +217,19 @@ export function FilterPresetsButton<M extends Record<string, unknown>>({
|
|
|
216
217
|
// otherwise find sort from remaining active presets
|
|
217
218
|
if (!wasActive && preset.sort) {
|
|
218
219
|
tableController.setSortBy?.(
|
|
219
|
-
preset.sort as
|
|
220
|
+
normalizeOrderBy(preset.sort) as OrderByTuple<Extract<keyof M, string> | (string & {})>[]
|
|
220
221
|
);
|
|
221
222
|
} else if (wasActive) {
|
|
222
|
-
let remainingSort: [
|
|
223
|
+
let remainingSort: OrderByTuple[] | undefined;
|
|
223
224
|
for (let i = 0; i < filterPresets.length; i++) {
|
|
224
225
|
if (i === index) continue;
|
|
225
226
|
const other = filterPresets[i] as FilterPreset<string>;
|
|
226
227
|
if (isPresetActive(other, currentFilters) && other.sort) {
|
|
227
|
-
remainingSort = other.sort;
|
|
228
|
+
remainingSort = normalizeOrderBy(other.sort);
|
|
228
229
|
}
|
|
229
230
|
}
|
|
230
231
|
tableController.setSortBy?.(
|
|
231
|
-
remainingSort as
|
|
232
|
+
remainingSort as OrderByTuple<Extract<keyof M, string> | (string & {})>[] | undefined
|
|
232
233
|
);
|
|
233
234
|
}
|
|
234
235
|
}
|