@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.
Files changed (26) hide show
  1. package/dist/{CollectionEditorDialog-CSheud_E.js → CollectionEditorDialog-BykOIZH4.js} +3 -3
  2. package/dist/{CollectionEditorDialog-CSheud_E.js.map → CollectionEditorDialog-BykOIZH4.js.map} +1 -1
  3. package/dist/{PropertyEditView-CWZlC1gq.js → PropertyEditView-C9Der_Ej.js} +2 -2
  4. package/dist/{PropertyEditView-CWZlC1gq.js.map → PropertyEditView-C9Der_Ej.js.map} +1 -1
  5. package/dist/{RouterCollectionsStudioView-CwTpdzWF.js → RouterCollectionsStudioView-CfTHbBCA.js} +4 -4
  6. package/dist/{RouterCollectionsStudioView-CwTpdzWF.js.map → RouterCollectionsStudioView-CfTHbBCA.js.map} +1 -1
  7. package/dist/collection_editor_ui.js +4 -4
  8. package/dist/components/CollectionViewBinding/CollectionViewBinding.d.ts +2 -1
  9. package/dist/components/CollectionViewBinding/SortButton.d.ts +14 -4
  10. package/dist/{export-L53WektO.js → export-BDynF1En.js} +2 -2
  11. package/dist/{export-L53WektO.js.map → export-BDynF1En.js.map} +1 -1
  12. package/dist/{history-D5SKygpJ.js → history-DS2iXiGi.js} +2 -2
  13. package/dist/{history-D5SKygpJ.js.map → history-DS2iXiGi.js.map} +1 -1
  14. package/dist/{import-C_4edkoD.js → import-J5vb86xj.js} +2 -2
  15. package/dist/{import-C_4edkoD.js.map → import-J5vb86xj.js.map} +1 -1
  16. package/dist/index.js +6 -6
  17. package/dist/{util-C-D5yD2n.js → util-WQP-ljKW.js} +206 -90
  18. package/dist/util-WQP-ljKW.js.map +1 -0
  19. package/package.json +9 -9
  20. package/src/collection_editor/ui/EditorCollectionActionStart.tsx +3 -2
  21. package/src/components/CollectionViewBinding/CollectionListViewBinding.tsx +80 -39
  22. package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +19 -8
  23. package/src/components/CollectionViewBinding/FilterPresetsButton.tsx +6 -5
  24. package/src/components/CollectionViewBinding/SortButton.tsx +149 -37
  25. package/src/components/SelectableTable/SelectableTable.tsx +1 -1
  26. package/dist/util-C-D5yD2n.js.map +0 -1
@@ -2,7 +2,7 @@ import * as React$1 from "react";
2
2
  import React, { Suspense, createContext, createElement, forwardRef, lazy, memo, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
3
3
  import { deepEqual } from "fast-equals";
4
4
  import { Alert, AlertCircleIcon, AlertTriangleIcon, AlignLeftIcon, ArrowDownIcon, ArrowLeftIcon, ArrowRightToLineIcon, ArrowUpDownIcon, ArrowUpIcon, Badge, BooleanSwitchWithLabel, Button, CHIP_COLORS, CHIP_SEED_KEYS, CalendarIcon, Card, CardView, CenteredView, CheckCircleIcon, CheckIcon, Checkbox, ChevronDownIcon, ChevronRightIcon, ChevronUpIcon, ChevronsUpDownIcon, Chip, CircleIcon, CircularProgress, CircularProgressCenter, CodeIcon, Collapse, ColumnsIcon, CopyIcon, DateTimeField, Dialog, DialogActions, DialogContent, DialogTitle, ErrorBoundary, ExpandablePanel, ExternalLinkIcon, EyeIcon, EyeOffIcon, FileTextIcon, FileUpload, FilterChip, FilterIcon, FilterXIcon, FlagIcon, FolderUpIcon, FunctionSquareIcon, GlobeIcon, HandleIcon, HashIcon, HistoryIcon, IconButton, ImageOffIcon, KanbanIcon, KanbanView, KeyRoundIcon, Label, LayoutGridIcon, Link2Icon, LinkIcon, ListIcon, ListOrderedIcon, ListView, LoadingButton, LucideIconByName, MailIcon, Markdown, Maximize2Icon, Menu, MenuItem, MinusIcon, MoreVerticalIcon, MultiSelect, MultiSelectItem, PanelLeftCloseIcon, PanelLeftOpenIcon, PencilIcon, PlusIcon, Popover, PopoverPrimitive, Portal, QuoteIcon, RadioGroup, RadioGroupItem, RefreshCwIcon, RepeatIcon, ResizablePanels, Rows3Icon, SearchBar, SearchIcon, Select, SelectItem, Separator, SettingsIcon, Sheet, Skeleton, Tab, Table, TableBody, TableCell, TableHeader, TableIcon, TableRow, Tabs, TextField, TextIcon, TextareaAutosize, ToggleButtonGroup, Tooltip, Trash2Icon, Typography, Unlink2Icon, UploadIcon, UserIcon, VirtualTable, VirtualTableDateField, VirtualTableInput, VirtualTableInput as VirtualTableInput$1, VirtualTableNumberInput, VirtualTableSwitch, VoteIcon, XCircleIcon, XIcon, cls, coolIconKeys, createVirtualTableSelectionStore, debounce, defaultBorderMixin, fieldBackgroundDisabledMixin, fieldBackgroundHoverMixin, fieldBackgroundInvisibleMixin, fieldBackgroundMixin, focusedDisabled, getColorSchemeForSeed, iconSize, paperMixin, useDebouncedCallback, useOutsideAlerter, usePortalContainer } from "@rebasepro/ui";
5
- import { CollectionRegistry, DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE, canReadCollection, enumToObjectEntries, getArrayResolvedProperties, getDefaultValueFor, getDefaultValueFortype, getDefaultValuesFor, getEntityChildViews, getLabelOrConfigFrom, getReferenceFrom, getRelationFrom, getRelationTargetPath, getSubcollections, getTableName, isPropertyBuilder, normalizeToEntityRelation, resolveArrayProperties, resolveEnumValues, resolveProperty, resolveRelationProperty, resolveStorageFilenameString, resolveStoragePathString, resolveStorageSource } from "@rebasepro/common";
5
+ import { CollectionRegistry, DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE, canReadCollection, enumToObjectEntries, getArrayResolvedProperties, getDefaultValueFor, getDefaultValueFortype, getDefaultValuesFor, getEntityChildViews, getLabelOrConfigFrom, getReferenceFrom, getRelationFrom, getRelationTargetPath, getSubcollections, getTableName, isPropertyBuilder, normalizeOrderBy, normalizeToEntityRelation, resolveArrayProperties, resolveEnumValues, resolveProperty, resolveRelationProperty, resolveStorageFilenameString, resolveStoragePathString, resolveStorageSource, serializeOrderBy } from "@rebasepro/common";
6
6
  import { CONTAINER_FULL_WIDTH, CollectionScopeProvider, DEFAULT_PAGE_SIZE, EntityDisplayCache, ErrorTooltip, ErrorView, IconForView, PluginProviderStack, SIDE_PANEL_DEFAULT_WIDTH, StorageSourceContext, UnsavedChangesDialog, UserDisplay, addInitialSlash, apiBaseOf, buildEnumLabel, createFormexStub, deleteEntityWithCallbacks, entityDisplayKey, flattenKeys, getAdminEntityChildViews, getCollectionBySlugWithin, getCollectionPathsCombinations, getColorScheme, getColumnKeysForProperty, getDisplayPropertyKey, getDisplayResolver, getEntityFromCache, getEntityFromMemoryCache, getEntityImagePreviewPropertyKey, getEntityPreviewKeys, getFormFieldKeys, getIcon, getLastSegment as getLastSegment$1, getLeadingRelationTitleKey, getLocalChangesBackup, getNavigationEntriesFromPath, getParentReferencesFromPath, getPropertyInPath, getRedundantChildViewColumnIds, getRelationIncludeParams, getRowHeight, getTitlePropertyCandidates, getTitlePropertyKey, getTitlePropertyKeyForValues, hasDeclaredDisplay, iconsSearch, isDisabled, isFilterableRelation, isHidden, isReadOnly, looksLikeIdentifierValue, removeEntityFromCache, removeEntityFromMemoryCache, removeInitialAndTrailingSlashes, removeInitialAndTrailingSlashes as removeInitialAndTrailingSlashes$1, removeInitialSlash, removeTrailingSlash, removeTrailingSlash as removeTrailingSlash$1, resolveCollectionPathIds, resolveCollectionPathIds as resolveCollectionPathIds$1, resolveComponentRef, resolveDefaultSelectedView, resolveFilterOperators, resolveFormLayout, saveEntityToCache, saveEntityToMemoryCache, saveEntityWithCallbacks, useAnalyticsController, useApiBase, useApiConfig, useAuthController, useCollectionScope, useColumnIds, useComponentOverride, useCustomizationController, useData, useDataTableController, useFetch, useLargeLayout, useModeController, useNavigationBlocker, usePermissions, useRebaseClient, useRebaseContext, useRelationSelector, useResolvedComponent, useScrollRestoration, useSlot, useSnackbarController, useStorageSource, useStorageSources, useStorageUploadController, useTranslation, useUnsavedChangesDialog, useUserConfigurationPersistence } from "@rebasepro/app";
7
7
  import { Field, Formex, getIn, setIn, useCreateFormex, useFormex } from "@rebasepro/forms";
8
8
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -9318,8 +9318,8 @@ function EditorCollectionAction({ path, parentCollectionSlugs, parentEntityIds,
9318
9318
  }
9319
9319
  //#endregion
9320
9320
  //#region src/components/CollectionViewBinding/CollectionViewActions.tsx
9321
- var ImportCollectionAction = lazy(() => import("./import-C_4edkoD.js").then((n) => n.t).then((m) => ({ default: m.ImportCollectionAction })));
9322
- var ExportCollectionAction = lazy(() => import("./export-L53WektO.js").then((n) => n.t).then((m) => ({ default: m.ExportCollectionAction })));
9321
+ var ImportCollectionAction = lazy(() => import("./import-J5vb86xj.js").then((n) => n.t).then((m) => ({ default: m.ImportCollectionAction })));
9322
+ var ExportCollectionAction = lazy(() => import("./export-BDynF1En.js").then((n) => n.t).then((m) => ({ default: m.ExportCollectionAction })));
9323
9323
  function CollectionViewActions({ collection, relativePath, parentCollectionSlugs, parentEntityIds, onNewClick, onAddExistingClick, onMultipleDeleteClick, selectionEnabled, path, selectionController, tableController, collectionEntitiesCount, compact, children, openNewDocument }) {
9324
9324
  const context = useAdminContext();
9325
9325
  const { canCreate, canDelete } = usePermissions();
@@ -10397,11 +10397,8 @@ function CollectionListViewBinding({ collection, tableController, onEntityClick,
10397
10397
  columnMode,
10398
10398
  slotKeys.relationKeys
10399
10399
  ]);
10400
- /** The property the sort names, when no cell already shows it. */
10401
- const sortedKey = useMemo(() => {
10402
- const key = sortBy?.[0];
10403
- return key && !shownKeys.has(key) ? key : void 0;
10404
- }, [sortBy, shownKeys]);
10400
+ /** The properties the sort names, minus the ones a cell already shows. */
10401
+ const sortedKeys = useMemo(() => (sortBy ?? []).map(([key]) => key).filter((key) => !shownKeys.has(key)), [sortBy, shownKeys]);
10405
10402
  /** The properties the filters name, minus the ones a cell already shows. */
10406
10403
  const filteredKeys = useMemo(() => Object.keys(filterValues ?? {}).filter((key) => !shownKeys.has(key)), [filterValues, shownKeys]);
10407
10404
  /**
@@ -10413,22 +10410,23 @@ function CollectionListViewBinding({ collection, tableController, onEntityClick,
10413
10410
  * and clearing a sort says nothing about wanting to stop seeing the property
10414
10411
  * it sorted on.
10415
10412
  *
10416
- * What it does *not* do is accumulate. There is only ever one sort, so
10417
- * ordering by four properties in turn is changing one's mind four times, not
10418
- * asking for four columns — and asking for four is how the row lost its
10419
- * status and its date to a wall of numbers. A new sort therefore replaces
10420
- * the property the last one left behind, and a new set of filters replaces
10421
- * the set before it. At most one sort column and one set of filter columns
10422
- * outlive their request.
10413
+ * What it does *not* do is accumulate. A sort names the properties it names
10414
+ * *now*, so ordering by four properties in turn is changing one's mind four
10415
+ * times, not asking for four columns — and asking for four is how the row
10416
+ * lost its status and its date to a wall of numbers. A new sort therefore
10417
+ * replaces the properties the last one left behind, and a new set of filters
10418
+ * replaces the set before it. At most one sort's columns and one set of
10419
+ * filter columns outlive their request.
10423
10420
  *
10424
10421
  * The list remounts per collection (it is keyed by path), so this is the
10425
10422
  * memory of one visit to one collection, not a preference.
10426
10423
  */
10427
- const [lastSortedKey, setLastSortedKey] = useState(void 0);
10424
+ const [lastSortedKeys, setLastSortedKeys] = useState([]);
10428
10425
  const [lastFilteredKeys, setLastFilteredKeys] = useState([]);
10429
10426
  useEffect(() => {
10430
- if (sortedKey) setLastSortedKey(sortedKey);
10431
- }, [sortedKey]);
10427
+ if (sortedKeys.length === 0) return;
10428
+ setLastSortedKeys((previous) => previous.length === sortedKeys.length && previous.every((key, i) => key === sortedKeys[i]) ? previous : sortedKeys);
10429
+ }, [sortedKeys]);
10432
10430
  useEffect(() => {
10433
10431
  if (filteredKeys.length === 0) return;
10434
10432
  setLastFilteredKeys((previous) => previous.length === filteredKeys.length && previous.every((key, i) => key === filteredKeys[i]) ? previous : filteredKeys);
@@ -10442,14 +10440,14 @@ function CollectionListViewBinding({ collection, tableController, onEntityClick,
10442
10440
  * their own order, then the sort's.
10443
10441
  */
10444
10442
  const requestedColumns = useMemo(() => {
10445
- const active = /* @__PURE__ */ new Set([...filteredKeys, ...sortedKey ? [sortedKey] : []]);
10443
+ const active = /* @__PURE__ */ new Set([...filteredKeys, ...sortedKeys]);
10446
10444
  const keys = [];
10447
10445
  const request = (key) => {
10448
10446
  if (!key || keys.includes(key) || shownKeys.has(key)) return;
10449
10447
  keys.push(key);
10450
10448
  };
10451
10449
  (filteredKeys.length > 0 ? filteredKeys : lastFilteredKeys).forEach(request);
10452
- request(sortedKey ?? lastSortedKey);
10450
+ (sortedKeys.length > 0 ? sortedKeys : lastSortedKeys).forEach(request);
10453
10451
  return keys.flatMap((key, index) => {
10454
10452
  const property = getResolvedPropertyInPath(resolvedCollection.properties, key);
10455
10453
  if (!property) return [];
@@ -10465,9 +10463,9 @@ function CollectionListViewBinding({ collection, tableController, onEntityClick,
10465
10463
  });
10466
10464
  }, [
10467
10465
  filteredKeys,
10468
- sortedKey,
10466
+ sortedKeys,
10469
10467
  lastFilteredKeys,
10470
- lastSortedKey,
10468
+ lastSortedKeys,
10471
10469
  shownKeys,
10472
10470
  resolvedCollection,
10473
10471
  sortableKeys
@@ -10559,20 +10557,41 @@ function CollectionListViewBinding({ collection, tableController, onEntityClick,
10559
10557
  const handleRowSelectionChange = useCallback((entity, selected) => {
10560
10558
  handleSelectionChange(entity, selected);
10561
10559
  }, [handleSelectionChange]);
10560
+ /** Where each sorted column sits in the order, so a header can show its rank. */
10561
+ const sortIndex = useMemo(() => {
10562
+ const index = /* @__PURE__ */ new Map();
10563
+ (sortBy ?? []).forEach(([key, direction], position) => {
10564
+ if (!index.has(key)) index.set(key, {
10565
+ direction,
10566
+ position
10567
+ });
10568
+ });
10569
+ return index;
10570
+ }, [sortBy]);
10562
10571
  /**
10563
10572
  * Order by a column, on the same cycle a table header runs: unordered →
10564
- * ascending → descending → unordered. Clicking a header here and clicking it
10573
+ * ascending → descending → unordered, and shift-click to add the column
10574
+ * under the sort already in place. Clicking a header here and clicking it
10565
10575
  * in the table view have to mean the same thing, because they are the same
10566
10576
  * collection under the same controller.
10567
10577
  */
10568
- const onColumnSort = useCallback((key) => {
10578
+ const onColumnSort = useCallback((key, additive = false) => {
10569
10579
  if (!setSortBy) return;
10570
- const active = sortBy?.[0] === key ? sortBy[1] : void 0;
10571
- setSortBy(active === "asc" ? [key, "desc"] : active === "desc" ? void 0 : [key, "asc"]);
10580
+ const active = sortIndex.get(key)?.direction;
10581
+ const next = active === "asc" ? "desc" : active === "desc" ? void 0 : "asc";
10582
+ const existing = sortBy ?? [];
10583
+ let updated;
10584
+ if (!additive) updated = next ? [[key, next]] : void 0;
10585
+ else if (next === void 0) updated = existing.filter(([existingKey]) => existingKey !== key);
10586
+ else if (active === void 0) updated = [...existing, [key, next]];
10587
+ else updated = existing.map((entry) => entry[0] === key ? [key, next] : entry);
10588
+ if (updated?.length === 0) updated = void 0;
10589
+ setSortBy(updated);
10572
10590
  setItemCount?.(pageSize);
10573
10591
  }, [
10574
10592
  setSortBy,
10575
10593
  sortBy,
10594
+ sortIndex,
10576
10595
  setItemCount,
10577
10596
  pageSize
10578
10597
  ]);
@@ -10585,14 +10604,14 @@ function CollectionListViewBinding({ collection, tableController, onEntityClick,
10585
10604
  const sortIsAvailable = useCallback((key) => {
10586
10605
  if (!setSortBy) return false;
10587
10606
  if (!checkFilterCombination) return true;
10588
- const active = sortBy?.[0] === key ? sortBy[1] : void 0;
10607
+ const active = sortIndex.get(key)?.direction;
10589
10608
  if (active === "desc") return true;
10590
- return checkFilterCombination(filterValues ?? {}, [key, active === "asc" ? "desc" : "asc"]);
10609
+ return checkFilterCombination(filterValues ?? {}, [[key, active === "asc" ? "desc" : "asc"]]);
10591
10610
  }, [
10592
10611
  setSortBy,
10593
10612
  checkFilterCombination,
10594
10613
  filterValues,
10595
- sortBy
10614
+ sortIndex
10596
10615
  ]);
10597
10616
  return /* @__PURE__ */ jsx("div", {
10598
10617
  ref: containerRef,
@@ -10619,7 +10638,7 @@ function CollectionListViewBinding({ collection, tableController, onEntityClick,
10619
10638
  selectionEnabled: selectionEnabled && !combineSelection,
10620
10639
  showImage,
10621
10640
  actionsWidth,
10622
- sortBy,
10641
+ sortIndex,
10623
10642
  onColumnSort,
10624
10643
  sortIsAvailable
10625
10644
  }),
@@ -10926,11 +10945,12 @@ function EmptyCell() {
10926
10945
  * against the same widths the rows do, and skips the checkbox and the image —
10927
10946
  * neither is a column, and neither has anything to be called.
10928
10947
  */
10929
- function ListHeader({ titleColumn, columns, selectionEnabled, showImage, actionsWidth, sortBy, onColumnSort, sortIsAvailable }) {
10948
+ function ListHeader({ titleColumn, columns, selectionEnabled, showImage, actionsWidth, sortIndex, onColumnSort, sortIsAvailable }) {
10930
10949
  const headerCell = (column) => /* @__PURE__ */ jsx(ListHeaderLabel, {
10931
10950
  column,
10932
- direction: sortBy?.[0] === column.key ? sortBy[1] : void 0,
10933
- onSort: column.sortable && sortIsAvailable(column.key) ? () => onColumnSort(column.key) : void 0
10951
+ direction: sortIndex.get(column.key)?.direction,
10952
+ position: sortIndex.size > 1 ? sortIndex.get(column.key)?.position : void 0,
10953
+ onSort: column.sortable && sortIsAvailable(column.key) ? (additive) => onColumnSort(column.key, additive) : void 0
10934
10954
  });
10935
10955
  return /* @__PURE__ */ jsxs("div", {
10936
10956
  className: cls("flex items-center gap-4 px-5 py-1.5 select-none border-b bg-surface-50 dark:bg-surface-900", defaultBorderMixin),
@@ -10962,7 +10982,7 @@ function ListHeader({ titleColumn, columns, selectionEnabled, showImage, actions
10962
10982
  * in a header row are three invitations, and only one of them is ever the
10963
10983
  * answer to "how is this sorted".
10964
10984
  */
10965
- function ListHeaderLabel({ column, direction, onSort }) {
10985
+ function ListHeaderLabel({ column, direction, position, onSort }) {
10966
10986
  const content = /* @__PURE__ */ jsxs(Fragment, { children: [
10967
10987
  /* @__PURE__ */ jsx("span", {
10968
10988
  className: "truncate",
@@ -10975,6 +10995,10 @@ function ListHeaderLabel({ column, direction, onSort }) {
10975
10995
  direction === "desc" && /* @__PURE__ */ jsx(ArrowDownIcon, {
10976
10996
  size: 12,
10977
10997
  className: "flex-shrink-0"
10998
+ }),
10999
+ direction && position !== void 0 && /* @__PURE__ */ jsx("span", {
11000
+ className: "flex-shrink-0 text-[9px] font-bold leading-none tabular-nums",
11001
+ children: position + 1
10978
11002
  })
10979
11003
  ] });
10980
11004
  const base = "inline-flex items-center gap-1 max-w-full text-[11px] leading-none uppercase tracking-wider font-medium";
@@ -10985,8 +11009,8 @@ function ListHeaderLabel({ column, direction, onSort }) {
10985
11009
  });
10986
11010
  return /* @__PURE__ */ jsx("button", {
10987
11011
  type: "button",
10988
- title: `Sort by ${column.label}`,
10989
- onClick: onSort,
11012
+ title: `Sort by ${column.label} — shift-click to add it under the current sort`,
11013
+ onClick: (event) => onSort(event.shiftKey),
10990
11014
  className: cls(base, tone, "cursor-pointer hover:text-surface-700 dark:hover:text-surface-200 transition-colors"),
10991
11015
  children: content
10992
11016
  });
@@ -11644,7 +11668,7 @@ function JsonPreviewBinding({ values }) {
11644
11668
  }
11645
11669
  //#endregion
11646
11670
  //#region src/components/EntityInspector.tsx
11647
- var EntityHistoryView = lazy(() => import("./history-D5SKygpJ.js").then((m) => ({ default: m.EntityHistoryView })));
11671
+ var EntityHistoryView = lazy(() => import("./history-DS2iXiGi.js").then((m) => ({ default: m.EntityHistoryView })));
11648
11672
  /**
11649
11673
  * Raw values and revision history, as an inspector rather than as tabs.
11650
11674
  *
@@ -15101,13 +15125,13 @@ function FilterPresetsButton({ filterPresets, tableController, compact }) {
15101
15125
  tableController.setSortBy?.(void 0);
15102
15126
  } else {
15103
15127
  tableController.setFilterValues?.(currentFilters);
15104
- if (!wasActive && preset.sort) tableController.setSortBy?.(preset.sort);
15128
+ if (!wasActive && preset.sort) tableController.setSortBy?.(normalizeOrderBy(preset.sort));
15105
15129
  else if (wasActive) {
15106
15130
  let remainingSort;
15107
15131
  for (let i = 0; i < filterPresets.length; i++) {
15108
15132
  if (i === index) continue;
15109
15133
  const other = filterPresets[i];
15110
- if (isPresetActive(other, currentFilters) && other.sort) remainingSort = other.sort;
15134
+ if (isPresetActive(other, currentFilters) && other.sort) remainingSort = normalizeOrderBy(other.sort);
15111
15135
  }
15112
15136
  tableController.setSortBy?.(remainingSort);
15113
15137
  }
@@ -15153,59 +15177,98 @@ function FilterPresetsButton({ filterPresets, tableController, compact }) {
15153
15177
  //#endregion
15154
15178
  //#region src/components/CollectionViewBinding/SortButton.tsx
15155
15179
  /**
15156
- * Toolbar control that orders the collection by one property.
15180
+ * Toolbar control that orders the collection.
15157
15181
  *
15158
15182
  * The table orders by clicking a column header, which leaves every other view
15159
15183
  * mode — list, cards, and either of them inside a split view — with no way to
15160
15184
  * order at all, even though the controller has always carried `sortBy` and the
15161
15185
  * query has always honoured it. This binds a control to it.
15162
15186
  *
15163
- * Clicking the active property flips its direction, so ascending descending
15164
- * is one click rather than a trip through a direction selector; clicking a
15165
- * different one starts it ascending. The popover stays open through both.
15187
+ * It edits the *whole* order rather than a single key. A sort can have several
15188
+ * keys, applied in the order shown: the second decides between rows the first
15189
+ * calls equal, the third between rows the first two do. So the active keys sit
15190
+ * at the top, numbered and movable, and the properties below them are the ones
15191
+ * that can still be added under the last. Shift-clicking a column header does
15192
+ * the same thing from the table; this is the view for the modes that have no
15193
+ * headers, and the only place the order between keys can be changed.
15194
+ *
15195
+ * Clicking an active key flips its direction, so ascending → descending is one
15196
+ * click rather than a trip through a direction selector; clicking an inactive
15197
+ * one appends it ascending. The popover stays open through all of it — building
15198
+ * a two-key order out of a control that closes on every click is four round
15199
+ * trips through a button.
15166
15200
  */
15167
15201
  function SortButton({ tableController, properties, compact }) {
15168
15202
  const { t } = useTranslation();
15169
15203
  const [open, setOpen] = useState(false);
15170
15204
  const options = useMemo(() => getSortablePropertyOptions(properties), [properties]);
15171
15205
  const { sortBy, setSortBy, setItemCount, pageSize, filterValues, checkFilterCombination } = tableController;
15172
- const activeKey = sortBy?.[0];
15173
- const activeDirection = sortBy?.[1];
15206
+ const keys = useMemo(() => sortBy ?? [], [sortBy]);
15207
+ const activeIndex = useMemo(() => {
15208
+ const index = /* @__PURE__ */ new Map();
15209
+ keys.forEach(([key, direction], position) => {
15210
+ if (!index.has(key)) index.set(key, {
15211
+ direction,
15212
+ position
15213
+ });
15214
+ });
15215
+ return index;
15216
+ }, [keys]);
15174
15217
  const applySort = useCallback((next) => {
15175
- setSortBy?.(next);
15218
+ setSortBy?.(next && next.length > 0 ? next : void 0);
15176
15219
  if (pageSize !== void 0) setItemCount?.(pageSize);
15177
15220
  }, [
15178
15221
  setSortBy,
15179
15222
  setItemCount,
15180
15223
  pageSize
15181
15224
  ]);
15225
+ /** Flip an active key's direction, or append an inactive one ascending. */
15182
15226
  const onOptionClick = useCallback((key) => {
15183
- if (key === activeKey) applySort([key, activeDirection === "asc" ? "desc" : "asc"]);
15184
- else applySort([key, "asc"]);
15227
+ const active = activeIndex.get(key);
15228
+ if (!active) {
15229
+ applySort([...keys, [key, "asc"]]);
15230
+ return;
15231
+ }
15232
+ applySort(keys.map((entry) => entry[0] === key ? [key, active.direction === "asc" ? "desc" : "asc"] : entry));
15185
15233
  }, [
15186
- activeKey,
15187
- activeDirection,
15188
- applySort
15234
+ activeIndex,
15235
+ applySort,
15236
+ keys
15189
15237
  ]);
15238
+ const removeKey = useCallback((key) => {
15239
+ applySort(keys.filter(([existing]) => existing !== key));
15240
+ }, [applySort, keys]);
15241
+ /** Swap a key with its neighbour, which is what changes which one decides. */
15242
+ const moveKey = useCallback((position, delta) => {
15243
+ const target = position + delta;
15244
+ if (target < 0 || target >= keys.length) return;
15245
+ const next = [...keys];
15246
+ [next[position], next[target]] = [next[target], next[position]];
15247
+ applySort(next);
15248
+ }, [applySort, keys]);
15190
15249
  if (!setSortBy || options.length === 0) return null;
15191
- const activeOption = options.find((option) => option.key === activeKey);
15192
- const label = activeOption ? `${t("sort_by")}: ${activeOption.title}` : t("sort");
15250
+ const primary = keys[0];
15251
+ const primaryOption = primary ? options.find((option) => option.key === primary[0]) : void 0;
15252
+ const label = primaryOption ? keys.length > 1 ? `${t("sort_by")}: ${primaryOption.title} +${keys.length - 1}` : `${t("sort_by")}: ${primaryOption.title}` : t("sort");
15193
15253
  const arrowSize = compact ? iconSize.smallest : iconSize.small;
15254
+ const primaryDirection = primary?.[1];
15255
+ const trigger = /* @__PURE__ */ jsx(IconButton, {
15256
+ size: "small",
15257
+ "aria-label": label,
15258
+ title: label,
15259
+ className: cls(primaryOption && "text-primary"),
15260
+ children: primaryDirection === "desc" ? /* @__PURE__ */ jsx(ArrowDownIcon, { size: arrowSize }) : primaryDirection === "asc" ? /* @__PURE__ */ jsx(ArrowUpIcon, { size: arrowSize }) : /* @__PURE__ */ jsx(ArrowUpDownIcon, { size: arrowSize })
15261
+ });
15262
+ const inactiveOptions = options.filter((option) => !activeIndex.has(option.key));
15194
15263
  return /* @__PURE__ */ jsx(Popover, {
15195
15264
  open,
15196
15265
  onOpenChange: setOpen,
15197
15266
  modal: false,
15198
15267
  side: "bottom",
15199
15268
  align: "start",
15200
- trigger: /* @__PURE__ */ jsx(IconButton, {
15201
- size: "small",
15202
- "aria-label": label,
15203
- title: label,
15204
- className: cls(activeOption && "text-primary"),
15205
- children: activeDirection === "desc" ? /* @__PURE__ */ jsx(ArrowDownIcon, { size: arrowSize }) : activeDirection === "asc" ? /* @__PURE__ */ jsx(ArrowUpIcon, { size: arrowSize }) : /* @__PURE__ */ jsx(ArrowUpDownIcon, { size: arrowSize })
15206
- }),
15269
+ trigger,
15207
15270
  children: /* @__PURE__ */ jsxs("div", {
15208
- className: "py-2 min-w-[240px] flex flex-col",
15271
+ className: "py-2 min-w-[280px] max-w-[360px] flex flex-col",
15209
15272
  children: [
15210
15273
  /* @__PURE__ */ jsx(Typography, {
15211
15274
  variant: "caption",
@@ -15213,42 +15276,97 @@ function SortButton({ tableController, properties, compact }) {
15213
15276
  className: "font-medium uppercase tracking-wider px-3 pb-1",
15214
15277
  children: t("sort_by")
15215
15278
  }),
15216
- /* @__PURE__ */ jsx("div", {
15217
- className: "max-h-[320px] overflow-y-auto flex flex-col",
15218
- children: options.map(({ key, title, property }) => {
15219
- const active = key === activeKey;
15220
- const disabled = checkFilterCombination ? !checkFilterCombination(filterValues ?? {}, [key, active && activeDirection === "asc" ? "desc" : "asc"]) : false;
15221
- return /* @__PURE__ */ jsxs("button", {
15222
- type: "button",
15223
- role: "menuitemradio",
15224
- "aria-checked": active,
15225
- disabled,
15226
- onClick: () => onOptionClick(key),
15227
- className: cls("flex items-center gap-2 px-3 py-1.5 text-sm text-left w-full", disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer hover:bg-surface-accent-100 dark:hover:bg-surface-accent-900", active && "text-primary"),
15279
+ keys.length > 0 && /* @__PURE__ */ jsx("div", {
15280
+ className: "flex flex-col",
15281
+ children: keys.map(([key, direction], position) => {
15282
+ const option = options.find((o) => o.key === key);
15283
+ return /* @__PURE__ */ jsxs("div", {
15284
+ className: "flex items-center gap-1 pl-3 pr-2 py-1 text-sm text-primary",
15228
15285
  children: [
15229
15286
  /* @__PURE__ */ jsx("span", {
15230
- className: "flex-shrink-0 flex items-center",
15231
- children: getIconForProperty(property, "smallest")
15287
+ className: "flex-shrink-0 w-4 text-xs tabular-nums text-text-secondary dark:text-text-secondary-dark",
15288
+ children: position + 1
15232
15289
  }),
15233
- /* @__PURE__ */ jsx("span", {
15234
- className: "flex-grow truncate",
15235
- children: title
15290
+ /* @__PURE__ */ jsxs("button", {
15291
+ type: "button",
15292
+ onClick: () => onOptionClick(key),
15293
+ title: direction === "asc" ? t("sort_descending") : t("sort_ascending"),
15294
+ className: "flex items-center gap-2 flex-grow min-w-0 text-left cursor-pointer",
15295
+ children: [
15296
+ option && /* @__PURE__ */ jsx("span", {
15297
+ className: "flex-shrink-0 flex items-center",
15298
+ children: getIconForProperty(option.property, "smallest")
15299
+ }),
15300
+ /* @__PURE__ */ jsx("span", {
15301
+ className: "flex-grow truncate",
15302
+ children: option?.title ?? key
15303
+ }),
15304
+ /* @__PURE__ */ jsx("span", {
15305
+ className: "flex-shrink-0 flex items-center",
15306
+ children: direction === "desc" ? /* @__PURE__ */ jsx(ArrowDownIcon, { size: iconSize.smallest }) : /* @__PURE__ */ jsx(ArrowUpIcon, { size: iconSize.smallest })
15307
+ })
15308
+ ]
15236
15309
  }),
15237
- active && /* @__PURE__ */ jsx("span", {
15238
- className: "flex-shrink-0 flex items-center",
15239
- children: activeDirection === "desc" ? /* @__PURE__ */ jsx(ArrowDownIcon, { size: iconSize.smallest }) : /* @__PURE__ */ jsx(ArrowUpIcon, { size: iconSize.smallest })
15310
+ keys.length > 1 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(IconButton, {
15311
+ size: "smallest",
15312
+ "aria-label": t("sort_move_up"),
15313
+ title: t("sort_move_up"),
15314
+ disabled: position === 0,
15315
+ onClick: () => moveKey(position, -1),
15316
+ children: /* @__PURE__ */ jsx(ChevronUpIcon, { size: iconSize.smallest })
15317
+ }), /* @__PURE__ */ jsx(IconButton, {
15318
+ size: "smallest",
15319
+ "aria-label": t("sort_move_down"),
15320
+ title: t("sort_move_down"),
15321
+ disabled: position === keys.length - 1,
15322
+ onClick: () => moveKey(position, 1),
15323
+ children: /* @__PURE__ */ jsx(ChevronDownIcon, { size: iconSize.smallest })
15324
+ })] }),
15325
+ /* @__PURE__ */ jsx(IconButton, {
15326
+ size: "smallest",
15327
+ "aria-label": t("sort_remove_key"),
15328
+ title: t("sort_remove_key"),
15329
+ onClick: () => removeKey(key),
15330
+ children: /* @__PURE__ */ jsx(XIcon, { size: iconSize.smallest })
15240
15331
  })
15241
15332
  ]
15242
15333
  }, key);
15243
15334
  })
15244
15335
  }),
15336
+ inactiveOptions.length > 0 && /* @__PURE__ */ jsx(Typography, {
15337
+ variant: "caption",
15338
+ color: "secondary",
15339
+ className: "font-medium uppercase tracking-wider px-3 pt-2 pb-1",
15340
+ children: keys.length > 0 ? t("sort_then_by") : ""
15341
+ }),
15342
+ /* @__PURE__ */ jsx("div", {
15343
+ className: "max-h-[280px] overflow-y-auto flex flex-col",
15344
+ children: inactiveOptions.map(({ key, title, property }) => {
15345
+ const disabled = checkFilterCombination ? !checkFilterCombination(filterValues ?? {}, [...keys, [key, "asc"]]) : false;
15346
+ return /* @__PURE__ */ jsxs("button", {
15347
+ type: "button",
15348
+ role: "menuitemradio",
15349
+ "aria-checked": false,
15350
+ disabled,
15351
+ onClick: () => onOptionClick(key),
15352
+ className: cls("flex items-center gap-2 px-3 py-1.5 text-sm text-left w-full", disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer hover:bg-surface-accent-100 dark:hover:bg-surface-accent-900"),
15353
+ children: [/* @__PURE__ */ jsx("span", {
15354
+ className: "flex-shrink-0 flex items-center",
15355
+ children: getIconForProperty(property, "smallest")
15356
+ }), /* @__PURE__ */ jsx("span", {
15357
+ className: "flex-grow truncate",
15358
+ children: title
15359
+ })]
15360
+ }, key);
15361
+ })
15362
+ }),
15245
15363
  /* @__PURE__ */ jsx("div", {
15246
15364
  className: "px-3 pt-2",
15247
15365
  children: /* @__PURE__ */ jsx(Button, {
15248
15366
  variant: "text",
15249
15367
  size: "small",
15250
15368
  className: "w-full",
15251
- disabled: !activeOption,
15369
+ disabled: keys.length === 0,
15252
15370
  onClick: () => applySort(void 0),
15253
15371
  children: t("clear_sort")
15254
15372
  })
@@ -16251,14 +16369,13 @@ function DefaultCollectionEmptyState({ canCreate, onNewClick, isSearching, searc
16251
16369
  }
16252
16370
  /**
16253
16371
  * Inflight count request deduplication map.
16254
- * Keyed by `path|filterKey|sortByProperty|sortDir` so that concurrent
16372
+ * Keyed by `path|filterKey|sort|search` so that concurrent
16255
16373
  * callers (e.g. React StrictMode double-mount) share the same promise.
16256
16374
  */
16257
16375
  var inflightCountRequests = /* @__PURE__ */ new Map();
16258
16376
  function EntitiesCount({ path, collection, filter, sortBy, searchString, onCountChange }) {
16259
16377
  const dataClient = useData();
16260
- const sortByProperty = sortBy ? sortBy[0] : void 0;
16261
- const currentSort = sortBy ? sortBy[1] : void 0;
16378
+ const sortKey = React.useMemo(() => sortBy ? serializeOrderBy(sortBy) ?? "" : "", [sortBy]);
16262
16379
  const dataClientRef = React.useRef(dataClient);
16263
16380
  dataClientRef.current = dataClient;
16264
16381
  const onCountChangeRef = React.useRef(onCountChange);
@@ -16272,8 +16389,8 @@ function EntitiesCount({ path, collection, filter, sortBy, searchString, onCount
16272
16389
  }
16273
16390
  let cancelled = false;
16274
16391
  const whereParams = filter && Object.keys(filter).length > 0 ? filter : void 0;
16275
- const orderByParams = sortByProperty && currentSort ? [String(sortByProperty), currentSort] : void 0;
16276
- const cacheKey = `${path}|${filterKey}|${sortByProperty ?? ""}|${currentSort ?? ""}|${searchString ?? ""}`;
16392
+ const orderByParams = sortBy && sortBy.length > 0 ? sortBy.map(([field, direction]) => [String(field), direction]) : void 0;
16393
+ const cacheKey = `${path}|${filterKey}|${sortKey}|${searchString ?? ""}`;
16277
16394
  let countPromise = inflightCountRequests.get(cacheKey);
16278
16395
  if (!countPromise) {
16279
16396
  countPromise = accessor.count({
@@ -16296,8 +16413,7 @@ function EntitiesCount({ path, collection, filter, sortBy, searchString, onCount
16296
16413
  }, [
16297
16414
  path,
16298
16415
  filterKey,
16299
- sortByProperty,
16300
- currentSort,
16416
+ sortKey,
16301
16417
  searchString
16302
16418
  ]);
16303
16419
  return null;
@@ -24441,4 +24557,4 @@ function getFullIdPath(propertyKey, propertyNamespace) {
24441
24557
  //#endregion
24442
24558
  export { getEntityViewWidth as $, SkeletonPropertyComponent as $n, resolveCollectionPathIds$1 as $t, SelectFieldBinding as A, ArrayOfStringsPreview as An, saveImportedEntities as At, useSelectionDialog as B, useSidePanel as Bn, FieldCaption as Bt, getDefaultFieldId as C, UserPreview as Cn, isReferenceProperty as Cr, convertDataToEntity as Ct, TextFieldBinding as D, KeyValuePreview as Dn, useImportConfig as Dt, VectorFieldBinding as E, DatePreview as En, getInferenceType as Et, MapFieldBinding as F, ArrayOfReferencesPreview as Fn, parseCsvToObjects as Ft, useTopLevelNavigation as G, getEntityPreviewKeys as Gn, mergeEntityActions as Gt, SideDialogs as H, useCollectionRegistryController as Hn, BreadcrumbsProvider as Ht, KeyValueFieldBinding as I, InlineEntityListPreview as In, unflattenObject as It, useBuildCollectionRegistryController as J, ArrayPropertyPreview as Jn, getCollectionPathsCombinations as Jt, useResolvedViews as K, getEntityTitlePropertyKey as Kn, addInitialSlash as Kt, DateTimeFieldBinding as L, ReferencePreview as Ln, ArrayContainer as Lt, ReferenceFieldBinding as M, ArrayEnumPreview as Mn, convertFileToJson as Mt, MultiSelectFieldBinding as N, ArrayOfStorageComponentsPreview as Nn, detectCsvDelimiter as Nt, SwitchFieldBinding as O, MapPropertyPreview as On, ImportSaveInProgress as Ot, MarkdownEditorFieldBinding as P, RelationPreview as Pn, parseCsvRows as Pt, buildSidePanelsFromUrl as Q, StorageThumbnailInternal as Qn, removeTrailingSlash$1 as Qt, BlockFieldBinding as R, EntityPreviewBinding as Rn, PropertyConfigBadge as Rt, getDefaultFieldConfig as S, PropertyPreview as Sn, getResolvedPropertyInPath as Sr, DataNewPropertiesMapping as St, getFieldId as T, BooleanPreview as Tn, processValueMapping as Tt, useBuildUrlController as U, getUserLabel as Un, resolveEntityAction as Ut, SelectionTableBinding as V, CollectionRegistryContext as Vn, useBreadcrumbsController as Vt, useBuildNavigationStateController as W, useResolvedUser as Wn, resolveEntityView as Wt, resolveNavigationFrom as X, EnumValuesChip as Xn, removeInitialAndTrailingSlashes$1 as Xt, useHistory as Y, StringPropertyPreview as Yn, getLastSegment$1 as Yt, useResolvedNavigationFrom as Z, StorageThumbnail as Zn, removeInitialSlash as Zt, useCollectionsConfigController as _, ReadOnlyFieldBinding as _n, getDefaultPropertiesOrder as _r, CollectionViewActions as _t, namespaceToPropertiesPath as a, SelectableTable as an, ImagePreview as ar, removeEmptyContainers as at, PropertyFieldBinding as b, FieldHelperText as bn, getPropertiesWithPropertiesOrder as br, useCollectionEditorDialogsState as bt, buildCollectionGenerationCallback as c, UrlContext as cn, FormSections as cr, copyEntityAction as ct, fromSerializableCollectionConfigs as d, useNavigationStateController as dn, FieldBlock as dr, resetPasswordAction as dt, resolveOpenEntityMode as en, renderSkeletonCaptionText as er, useBuildSidePanel as et, fromSerializableProperties as f, useSideDialogsController as fn, LABEL_ICON_SIZE as fr, CreationResultDialog as ft, toSerializableProperty as g, useClearRestoreValue as gn, getBracketNotation as gr, EntityCardBinding as gt, toSerializableProperties as h, ArrayCustomShapedFieldBinding as hn, PropertyIdCopyTooltip as hr, CollectionCardViewBinding as ht, namespaceToPropertiesOrderPath as i, CollectionTableBinding as in, UrlComponentPreview as ir, getInitialEntityValues as it, RepeatFieldBinding as j, ArrayPropertyEnumPreview as jn, ImportFileUpload as jt, StorageUploadFieldBinding as k, ArrayOneOfPreview as kn, IMPORT_BATCH_SIZE as kt, validateCollectionJson as l, useUrlController as ln, FormRail as lr, deleteEntityAction as lt, toSerializableCollectionConfig as m, SelectableTableContext as mn, spanClass as mr, EntityViewBinding as mt, getFullIdPath as n, useSelectionController as nn, renderSkeletonImageThumbnail as nr, extractTouchedValues as nt, CollectionGenerationApiError as o, CollectionRowActions as on, sanitizeUrl as or, zodToFormErrors as ot, fromSerializableProperty as p, SideDialogsControllerContext as pn, isSelfLabellingProperty as pr, DetailViewBinding as pt, useResolvedCollections as q, getEntityTitlePropertyKeyForEntity as qn, getCollectionBySlugWithin as qt, idToPropertiesPath as r, VirtualTableInput$1 as rn, renderSkeletonText as rr, getChanges as rt, DEFAULT_COLLECTION_GENERATION_ENDPOINT as s, useAdminContext as sn, EmptyValue as sr, CollectionViewBinding as st, getFullId as t, resolveViewMode as tn, renderSkeletonIcon as tr, EditViewBinding as tt, fromSerializableCollectionConfig as u, NavigationStateContext as un, RecordMeta as ur, editEntityAction as ut, EntityFormBinding as v, LabelWithIconAndTooltip as vn, getIconForProperty as vr, useCollectionEditorController as vt, getFieldConfig as w, NumberPropertyPreview as wn, isRelationProperty as wr, flattenEntry as wt, DEFAULT_FIELD_CONFIGS as x, ArrayOfMapsPreview as xn, getPropertyInPath$1 as xr, ImportNewPropertyFieldPreview as xt, EntityForm as y, LabelWithIcon as yn, getIconForWidget as yr, ConfigControllerProvider as yt, ArrayOfReferencesFieldBinding as z, SidePanelControllerContext as zn, SearchIconsView as zt };
24443
24559
 
24444
- //# sourceMappingURL=util-C-D5yD2n.js.map
24560
+ //# sourceMappingURL=util-WQP-ljKW.js.map