@proveanything/smartlinks-utils-ui 0.10.9 → 0.11.0

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.
@@ -1,4 +1,4 @@
1
- import { AdminPageHeader } from '../../chunk-2MW54ZVG.js';
1
+ import { useIntroState, AdminPageHeader } from '../../chunk-BNC6Z6WB.js';
2
2
  import { assertComponentStylesLoaded } from '../../chunk-OLYC54YT.js';
3
3
  import '../../chunk-5UQQYXCX.js';
4
4
  import { FacetRuleEditor } from '../../chunk-JMCV6FOW.js';
@@ -6,12 +6,105 @@ import { useFacets } from '../../chunk-4LHF5JB7.js';
6
6
  import { cn } from '../../chunk-L7FQ52F5.js';
7
7
  import { parsedRefToTarget, parsedRefToScope, matchRecords, scopesEqual, getRecordById, listRecords, upsertRecord, updateRecord, createRecord, removeRecord } from '../../chunk-KA4MKRHL.js';
8
8
  export { bulkDelete, bulkUpsert, createRecord, getRecordById, listRecords, matchRecords, parsedRefToScope, parsedRefToTarget, removeRecord, restoreRecord, scopesEqual, upsertRecord } from '../../chunk-KA4MKRHL.js';
9
- import { createContext, useState, useEffect, useCallback, useMemo, useRef, isValidElement, useContext, useSyncExternalStore, useLayoutEffect, createElement } from 'react';
10
- import { ChevronDown, Database, Lightbulb, SearchX, Inbox, LayoutGrid, Eye, MoreHorizontal, Download, Upload, Trash2, Copy, Pencil, Plus, CircleDashed, ArrowDownLeft, CheckCircle2, List, SlidersHorizontal, Globe, Tag, Boxes, Layers, Package, Target, Rows3, ChevronRight, Eraser, ClipboardPaste, Box, X, Search, Image, Table, ArrowLeft, ChevronLeft, AlertTriangle, Info, HelpCircle, CornerDownLeft, Circle, MinusCircle, XCircle, CopyPlus, AlertCircle, Undo2, Save, Loader2, ArrowRight, Globe2, Check, Settings2 } from 'lucide-react';
9
+ import { createContext, useMemo, useState, useEffect, useCallback, useRef, isValidElement, useContext, useSyncExternalStore, useLayoutEffect, createElement } from 'react';
10
+ import { ChevronDown, Database, Lightbulb, SearchX, Inbox, LayoutGrid, Eye, MoreHorizontal, Download, Upload, Trash2, Copy, Pencil, Plus, CircleDashed, ArrowDownLeft, CheckCircle2, List, SlidersHorizontal, Globe, Tag, Boxes, Layers, Package, Target, Rows3, ChevronRight, Eraser, ClipboardPaste, Box, X, Search, Image, Table, ArrowLeft, ChevronLeft, AlertTriangle, Info, HelpCircle, CornerDownLeft, Circle, ArrowUpDown, ArrowUp, ArrowDown, MinusCircle, XCircle, CopyPlus, AlertCircle, Undo2, Save, Loader2, ArrowRight, Globe2, Check, Settings2 } from 'lucide-react';
11
11
  import { useQuery, useQueryClient, useInfiniteQuery } from '@tanstack/react-query';
12
12
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
13
13
  import { createPortal } from 'react-dom';
14
14
 
15
+ // src/components/RecordsAdmin/data/recordCache.ts
16
+ var RECORD_LIST_QK = ["records-admin", "list"];
17
+ var COLLECTION_ITEMS_QK = ["records-admin", "collection-items"];
18
+ var matchesCtx = (queryKey, prefix, ctx) => {
19
+ if (queryKey.length < prefix.length + 3) return false;
20
+ for (let i = 0; i < prefix.length; i += 1) {
21
+ if (queryKey[i] !== prefix[i]) return false;
22
+ }
23
+ if (queryKey[prefix.length] !== ctx.collectionId) return false;
24
+ if (queryKey[prefix.length + 1] !== ctx.appId) return false;
25
+ if (queryKey[prefix.length + 2] !== (ctx.recordType ?? void 0) && queryKey[prefix.length + 2] !== ctx.recordType) {
26
+ const slot = queryKey[prefix.length + 2];
27
+ if (slot !== void 0 && slot !== ctx.recordType) return false;
28
+ if (slot === void 0 && ctx.recordType) return false;
29
+ }
30
+ return true;
31
+ };
32
+ var replaceOrAppend = (cache, record) => {
33
+ let replaced = false;
34
+ const nextPages = cache.pages.map((page) => {
35
+ const idx = page.data.findIndex((r) => r.id === record.id);
36
+ if (idx === -1) return page;
37
+ replaced = true;
38
+ const nextData = [...page.data];
39
+ nextData[idx] = record;
40
+ return { ...page, data: nextData };
41
+ });
42
+ if (replaced) return { ...cache, pages: nextPages };
43
+ if (nextPages.length === 0) {
44
+ return {
45
+ pageParams: [0],
46
+ pages: [{ data: [record], total: 1, hasMore: false, nextOffset: 1 }]
47
+ };
48
+ }
49
+ const last = nextPages[nextPages.length - 1];
50
+ const updatedLast = {
51
+ ...last,
52
+ data: [...last.data, record],
53
+ total: (last.total ?? 0) + 1,
54
+ nextOffset: (last.nextOffset ?? last.data.length) + 1
55
+ };
56
+ const bumped = nextPages.map((p, i) => i === nextPages.length - 1 ? updatedLast : { ...p, total: (p.total ?? 0) + 1 });
57
+ return { ...cache, pages: bumped };
58
+ };
59
+ var removeFromPages = (cache, recordId) => {
60
+ let found = false;
61
+ const nextPages = cache.pages.map((page) => {
62
+ const idx = page.data.findIndex((r) => r.id === recordId);
63
+ if (idx === -1) return page;
64
+ found = true;
65
+ return {
66
+ ...page,
67
+ data: page.data.filter((r) => r.id !== recordId),
68
+ total: Math.max(0, (page.total ?? 0) - 1)
69
+ };
70
+ });
71
+ if (!found) return null;
72
+ const bumped = nextPages.map((p) => p.data.some((r) => r.id === recordId) ? p : { ...p, total: Math.max(0, p.total ?? 0) });
73
+ return { ...cache, pages: bumped };
74
+ };
75
+ function patchRecordIntoCaches(queryClient, ctx, record) {
76
+ if (!record || !record.id) return;
77
+ const all = [
78
+ ...queryClient.getQueriesData({ queryKey: RECORD_LIST_QK }),
79
+ ...queryClient.getQueriesData({ queryKey: COLLECTION_ITEMS_QK })
80
+ ];
81
+ for (const [key, cache] of all) {
82
+ if (!cache || !Array.isArray(cache.pages)) continue;
83
+ const prefix = key[0] === RECORD_LIST_QK[0] && key[1] === RECORD_LIST_QK[1] ? RECORD_LIST_QK : COLLECTION_ITEMS_QK;
84
+ if (!matchesCtx(key, prefix, ctx)) continue;
85
+ queryClient.setQueryData(key, (prev) => {
86
+ if (!prev || !Array.isArray(prev.pages)) return prev;
87
+ return replaceOrAppend(prev, record);
88
+ });
89
+ }
90
+ }
91
+ function removeRecordFromCaches(queryClient, ctx, recordId) {
92
+ if (!recordId) return;
93
+ const all = [
94
+ ...queryClient.getQueriesData({ queryKey: RECORD_LIST_QK }),
95
+ ...queryClient.getQueriesData({ queryKey: COLLECTION_ITEMS_QK })
96
+ ];
97
+ for (const [key, cache] of all) {
98
+ if (!cache || !Array.isArray(cache.pages)) continue;
99
+ const prefix = key[0] === RECORD_LIST_QK[0] && key[1] === RECORD_LIST_QK[1] ? RECORD_LIST_QK : COLLECTION_ITEMS_QK;
100
+ if (!matchesCtx(key, prefix, ctx)) continue;
101
+ queryClient.setQueryData(key, (prev) => {
102
+ if (!prev || !Array.isArray(prev.pages)) return prev;
103
+ const next = removeFromPages(prev, recordId);
104
+ return next ?? prev;
105
+ });
106
+ }
107
+ }
15
108
  var DEFAULT_ICONS = {
16
109
  scope: {
17
110
  collection: Globe,
@@ -412,58 +505,12 @@ function useResolvedRecord(args) {
412
505
  error: query.error ?? null
413
506
  };
414
507
  }
415
- var RT_KEY = (recordType) => recordType ?? "_default";
416
- var lsKey = (appId, recordType) => `ra:intro:${appId}:${RT_KEY(recordType)}`;
417
- var useIntroDismissed = (SL, collectionId, appId, recordType) => {
418
- const [dismissed, setDismissed] = useState(() => {
419
- try {
420
- return localStorage.getItem(lsKey(appId, recordType)) === "1";
421
- } catch {
422
- return false;
423
- }
424
- });
425
- useEffect(() => {
426
- let cancelled = false;
427
- (async () => {
428
- try {
429
- const cfg = await SL?.appConfiguration?.getConfig?.({ collectionId, appId, admin: true });
430
- if (cancelled) return;
431
- const flag = cfg?._meta?.introDismissed?.[RT_KEY(recordType)];
432
- if (flag) setDismissed(true);
433
- } catch {
434
- }
435
- })();
436
- return () => {
437
- cancelled = true;
438
- };
439
- }, [SL, collectionId, appId, recordType]);
440
- const dismiss = useCallback(async () => {
441
- setDismissed(true);
442
- try {
443
- localStorage.setItem(lsKey(appId, recordType), "1");
444
- } catch {
445
- }
446
- try {
447
- const cfg = await SL?.appConfiguration?.getConfig?.({ collectionId, appId, admin: true }).catch(() => ({}));
448
- const next = {
449
- ...cfg ?? {},
450
- _meta: {
451
- ...cfg?._meta ?? {},
452
- introDismissed: { ...cfg?._meta?.introDismissed ?? {}, [RT_KEY(recordType)]: true }
453
- }
454
- };
455
- await SL?.appConfiguration?.setConfig?.({ collectionId, appId, admin: true, config: next });
456
- } catch {
457
- }
458
- }, [SL, collectionId, appId, recordType]);
459
- const undismiss = useCallback(() => {
460
- setDismissed(false);
461
- try {
462
- localStorage.removeItem(lsKey(appId, recordType));
463
- } catch {
464
- }
465
- }, [appId, recordType]);
466
- return { dismissed, dismiss, undismiss };
508
+
509
+ // src/components/RecordsAdmin/hooks/useIntroDismissed.ts
510
+ var useIntroDismissed = (SL, _collectionId, appId, recordType) => {
511
+ const persistKey = recordType ? `${appId}:${recordType}` : appId;
512
+ const { dismissed, onDismiss, onReopen } = useIntroState({ SL, persistKey });
513
+ return { dismissed, dismiss: onDismiss, undismiss: onReopen };
467
514
  };
468
515
  var useScopeProbe = ({ SL, collectionId, admin = true, enabled = true }) => {
469
516
  const query = useQuery({
@@ -1055,7 +1102,8 @@ function useShellBrowser(opts) {
1055
1102
  probeIsLoading,
1056
1103
  selectedProductId,
1057
1104
  drillTab,
1058
- classify: classify3
1105
+ classify: classify3,
1106
+ pageSize
1059
1107
  } = opts;
1060
1108
  const [search, setSearch] = useState("");
1061
1109
  const [filter, setFilter] = useState("all");
@@ -1081,7 +1129,8 @@ function useShellBrowser(opts) {
1081
1129
  filter,
1082
1130
  classify: classify3,
1083
1131
  contextScope,
1084
- enabled: recordListEnabled
1132
+ enabled: recordListEnabled,
1133
+ pageSize
1085
1134
  });
1086
1135
  const facetBrowse = useFacetBrowse({
1087
1136
  SL,
@@ -2352,6 +2401,7 @@ var createEditorStore = () => {
2352
2401
  let nextOrder = 0;
2353
2402
  let hooksBundle = null;
2354
2403
  let notifier = null;
2404
+ let recordChangeNotifier = null;
2355
2405
  const buildRecordSummary = (entry, value) => ({
2356
2406
  id: entry.recordId ?? null,
2357
2407
  ref: entry.spec.scope.raw,
@@ -2557,8 +2607,9 @@ var createEditorStore = () => {
2557
2607
  });
2558
2608
  try {
2559
2609
  let nextRecordId = entry.recordId;
2610
+ let savedRecord = null;
2560
2611
  if (entry.recordId && entry.source === "self") {
2561
- await updateRecord(ctx, entry.recordId, {
2612
+ savedRecord = await updateRecord(ctx, entry.recordId, {
2562
2613
  data: persistedValue,
2563
2614
  facetRule: persistedFacetRule
2564
2615
  });
@@ -2577,6 +2628,7 @@ var createEditorStore = () => {
2577
2628
  facetRule: persistedFacetRule
2578
2629
  });
2579
2630
  nextRecordId = created?.id ?? nextRecordId;
2631
+ savedRecord = created;
2580
2632
  } else {
2581
2633
  const upserted = await upsertRecord(ctx, {
2582
2634
  ref: spec.ref,
@@ -2585,6 +2637,7 @@ var createEditorStore = () => {
2585
2637
  facetRule: persistedFacetRule
2586
2638
  });
2587
2639
  nextRecordId = upserted.record?.id ?? nextRecordId;
2640
+ savedRecord = upserted.record;
2588
2641
  }
2589
2642
  update(editorId, (e) => {
2590
2643
  if (persistedValue !== entry.value) {
@@ -2597,6 +2650,12 @@ var createEditorStore = () => {
2597
2650
  e.status = "saved";
2598
2651
  e.error = void 0;
2599
2652
  });
2653
+ if (savedRecord && savedRecord.id) {
2654
+ try {
2655
+ recordChangeNotifier?.({ kind: "saved", record: savedRecord, isCreate, ctx });
2656
+ } catch {
2657
+ }
2658
+ }
2600
2659
  if (hooksBundle?.afterSave) {
2601
2660
  const refreshed = map.get(editorId);
2602
2661
  const hookCtx = {
@@ -2654,6 +2713,10 @@ var createEditorStore = () => {
2654
2713
  await removeRecord(entry.saveSpec.ctx, entry.recordId);
2655
2714
  map.delete(editorId);
2656
2715
  emit();
2716
+ try {
2717
+ recordChangeNotifier?.({ kind: "deleted", recordId: entry.recordId, ctx: entry.saveSpec.ctx });
2718
+ } catch {
2719
+ }
2657
2720
  if (hooksBundle?.afterDelete && deletedSnapshot) {
2658
2721
  try {
2659
2722
  await hooksBundle.afterDelete(deletedSnapshot);
@@ -2688,6 +2751,9 @@ var createEditorStore = () => {
2688
2751
  },
2689
2752
  setNotifier(notify2) {
2690
2753
  notifier = notify2 ?? null;
2754
+ },
2755
+ setRecordChangeNotifier(notify2) {
2756
+ recordChangeNotifier = notify2 ?? null;
2691
2757
  }
2692
2758
  };
2693
2759
  };
@@ -2711,12 +2777,14 @@ var EditorSessionProvider = ({
2711
2777
  maxOpenEditors = 8,
2712
2778
  defaultValueFactory,
2713
2779
  hooks,
2714
- onHookNotice
2780
+ onHookNotice,
2781
+ onRecordChange
2715
2782
  }) => {
2716
2783
  const storeRef = useRef(null);
2717
2784
  if (!storeRef.current) storeRef.current = createEditorStore();
2718
2785
  storeRef.current.setHooks(hooks ?? null);
2719
2786
  storeRef.current.setNotifier(onHookNotice ?? null);
2787
+ storeRef.current.setRecordChangeNotifier(onRecordChange ?? null);
2720
2788
  const currentRef = useRef(void 0);
2721
2789
  const listenersRef = useRef(/* @__PURE__ */ new Set());
2722
2790
  const subscribeCurrent = useCallback((cb) => {
@@ -3749,6 +3817,56 @@ var ProductList = RecordList;
3749
3817
  var FacetList = RecordList;
3750
3818
  var VariantList = RecordList;
3751
3819
  var BatchList = RecordList;
3820
+ function LoadMoreFooter({
3821
+ shown,
3822
+ total,
3823
+ hasNextPage,
3824
+ isFetchingNextPage,
3825
+ onLoadMore,
3826
+ label
3827
+ }) {
3828
+ if (!hasNextPage && (total === void 0 || total <= shown)) return null;
3829
+ const knownTotal = typeof total === "number" && total >= shown;
3830
+ const summary = knownTotal ? `Showing ${shown} of ${total}` : `${shown} shown`;
3831
+ return /* @__PURE__ */ jsxs(
3832
+ "div",
3833
+ {
3834
+ className: "ra-loadmore",
3835
+ style: {
3836
+ display: "flex",
3837
+ alignItems: "center",
3838
+ gap: "8px",
3839
+ padding: "8px 12px",
3840
+ borderTop: "1px solid hsl(var(--ra-border))",
3841
+ background: "hsl(var(--ra-surface))"
3842
+ },
3843
+ children: [
3844
+ /* @__PURE__ */ jsx(
3845
+ "span",
3846
+ {
3847
+ style: {
3848
+ fontSize: "11px",
3849
+ color: "hsl(var(--ra-muted-text))",
3850
+ flex: 1
3851
+ },
3852
+ children: summary
3853
+ }
3854
+ ),
3855
+ hasNextPage && /* @__PURE__ */ jsx(
3856
+ "button",
3857
+ {
3858
+ type: "button",
3859
+ onClick: onLoadMore,
3860
+ disabled: isFetchingNextPage,
3861
+ className: "ra-btn",
3862
+ style: { fontSize: "11px", padding: "4px 10px" },
3863
+ children: isFetchingNextPage ? "Loading\u2026" : label ?? "Load more"
3864
+ }
3865
+ )
3866
+ ]
3867
+ }
3868
+ );
3869
+ }
3752
3870
  var COLLAPSED_FACET_CAP = 6;
3753
3871
  var COLLAPSED_VALUE_CAP = 12;
3754
3872
  var VALUE_SEARCH_THRESHOLD = 12;
@@ -5659,18 +5777,52 @@ function DefaultItemTable({
5659
5777
  selectedId,
5660
5778
  onOpen,
5661
5779
  onDelete,
5780
+ sort,
5781
+ onToggleSort,
5662
5782
  rowActions,
5663
5783
  rowClipboard,
5664
5784
  i18n
5665
5785
  }) {
5666
5786
  const cols = columns ?? [];
5667
5787
  const useFallback = cols.length === 0;
5788
+ const renderSortIcon = (key, sortable) => {
5789
+ if (!sortable) return null;
5790
+ if (sort?.key !== key || !sort?.dir) {
5791
+ return /* @__PURE__ */ jsx(ArrowUpDown, { className: "w-3 h-3 opacity-40", "aria-hidden": "true" });
5792
+ }
5793
+ return sort.dir === "asc" ? /* @__PURE__ */ jsx(ArrowUp, { className: "w-3 h-3", "aria-hidden": "true" }) : /* @__PURE__ */ jsx(ArrowDown, { className: "w-3 h-3", "aria-hidden": "true" });
5794
+ };
5668
5795
  return /* @__PURE__ */ jsx("div", { className: "ra-item-table-wrap", children: /* @__PURE__ */ jsxs("table", { className: "ra-item-table", children: [
5669
5796
  /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
5670
5797
  useFallback ? /* @__PURE__ */ jsxs(Fragment, { children: [
5671
- /* @__PURE__ */ jsx("th", { children: i18n.itemColumnLabel }),
5672
- /* @__PURE__ */ jsx("th", { style: { width: "12rem" }, children: i18n.itemColumnUpdated })
5673
- ] }) : cols.map((c) => /* @__PURE__ */ jsx("th", { style: { width: c.width, textAlign: c.align ?? "left" }, children: c.header }, c.key)),
5798
+ /* @__PURE__ */ jsx("th", { children: /* @__PURE__ */ jsx(
5799
+ SortableHeader,
5800
+ {
5801
+ label: i18n.itemColumnLabel,
5802
+ sortable: !!onToggleSort,
5803
+ icon: renderSortIcon("__label", !!onToggleSort),
5804
+ onClick: () => onToggleSort?.("__label")
5805
+ }
5806
+ ) }),
5807
+ /* @__PURE__ */ jsx("th", { style: { width: "12rem" }, children: /* @__PURE__ */ jsx(
5808
+ SortableHeader,
5809
+ {
5810
+ label: i18n.itemColumnUpdated,
5811
+ sortable: !!onToggleSort,
5812
+ icon: renderSortIcon("__updated", !!onToggleSort),
5813
+ onClick: () => onToggleSort?.("__updated")
5814
+ }
5815
+ ) })
5816
+ ] }) : cols.map((c) => /* @__PURE__ */ jsx("th", { style: { width: c.width, textAlign: c.align ?? "left" }, children: /* @__PURE__ */ jsx(
5817
+ SortableHeader,
5818
+ {
5819
+ label: c.header,
5820
+ sortable: !!c.sortBy && !!onToggleSort,
5821
+ icon: renderSortIcon(c.key, !!c.sortBy && !!onToggleSort),
5822
+ onClick: () => onToggleSort?.(c.key),
5823
+ align: c.align ?? "left"
5824
+ }
5825
+ ) }, c.key)),
5674
5826
  /* @__PURE__ */ jsx(
5675
5827
  "th",
5676
5828
  {
@@ -5758,6 +5910,39 @@ function DefaultItemTable({
5758
5910
  }) })
5759
5911
  ] }) });
5760
5912
  }
5913
+ function SortableHeader({
5914
+ label,
5915
+ sortable,
5916
+ icon,
5917
+ onClick,
5918
+ align = "left"
5919
+ }) {
5920
+ if (!sortable) return /* @__PURE__ */ jsx(Fragment, { children: label });
5921
+ return /* @__PURE__ */ jsxs(
5922
+ "button",
5923
+ {
5924
+ type: "button",
5925
+ onClick,
5926
+ className: "ra-item-th-sort",
5927
+ style: {
5928
+ display: "inline-flex",
5929
+ alignItems: "center",
5930
+ gap: "4px",
5931
+ background: "transparent",
5932
+ border: 0,
5933
+ padding: 0,
5934
+ font: "inherit",
5935
+ color: "inherit",
5936
+ cursor: "pointer",
5937
+ textAlign: align
5938
+ },
5939
+ children: [
5940
+ /* @__PURE__ */ jsx("span", { children: label }),
5941
+ icon
5942
+ ]
5943
+ }
5944
+ );
5945
+ }
5761
5946
  var initials = (label) => label.split(/\s+/).slice(0, 2).map((s) => s[0]?.toUpperCase() ?? "").join("") || "?";
5762
5947
  function DefaultItemCards({
5763
5948
  items,
@@ -5872,9 +6057,75 @@ function ItemListView({
5872
6057
  cardSize = "md",
5873
6058
  rowActions,
5874
6059
  rowClipboard,
6060
+ searchableFields,
6061
+ searchable = true,
6062
+ total,
6063
+ hasNextPage,
6064
+ isFetchingNextPage,
6065
+ onLoadMore,
5875
6066
  i18n
5876
6067
  }) {
5877
6068
  const newLabel = i18n.newItem.includes("{noun}") ? i18n.newItem.replace("{noun}", itemNoun) : i18n.newItem;
6069
+ const [search, setSearch] = useState("");
6070
+ const [sort, setSort] = useState({ key: null, dir: null });
6071
+ const onToggleSort = (key) => {
6072
+ setSort((cur) => {
6073
+ if (cur.key !== key) return { key, dir: "asc" };
6074
+ const next = cur.dir === "asc" ? "desc" : cur.dir === "desc" ? null : "asc";
6075
+ return { key: next ? key : null, dir: next };
6076
+ });
6077
+ };
6078
+ const wantsFullSet = search.trim().length > 0 || sort.dir !== null;
6079
+ const loadingNextRef = useRef(false);
6080
+ useEffect(() => {
6081
+ if (!wantsFullSet || !hasNextPage || !onLoadMore) return;
6082
+ if (isFetchingNextPage || loadingNextRef.current) return;
6083
+ loadingNextRef.current = true;
6084
+ onLoadMore();
6085
+ }, [wantsFullSet, hasNextPage, onLoadMore, isFetchingNextPage]);
6086
+ useEffect(() => {
6087
+ if (!isFetchingNextPage) loadingNextRef.current = false;
6088
+ }, [isFetchingNextPage]);
6089
+ const lookupColumn = useMemo(() => {
6090
+ const m = /* @__PURE__ */ new Map();
6091
+ (itemColumns ?? []).forEach((c) => m.set(c.key, c));
6092
+ return m;
6093
+ }, [itemColumns]);
6094
+ const visibleItems = useMemo(() => {
6095
+ let out = items;
6096
+ const q = search.trim().toLowerCase();
6097
+ if (q) {
6098
+ out = out.filter((it) => {
6099
+ const fields = [it.label, it.subtitle];
6100
+ if (searchableFields) fields.push(...searchableFields(it));
6101
+ return fields.some((f) => typeof f === "string" && f.toLowerCase().includes(q));
6102
+ });
6103
+ }
6104
+ if (sort.key && sort.dir) {
6105
+ const dir = sort.dir === "asc" ? 1 : -1;
6106
+ const getter = (it) => {
6107
+ if (sort.key === "__label") return it.label;
6108
+ if (sort.key === "__updated") return it.updatedAt;
6109
+ const col = lookupColumn.get(sort.key);
6110
+ return col?.sortBy ? col.sortBy(it) : void 0;
6111
+ };
6112
+ const norm = (v) => {
6113
+ if (v == null) return Number.POSITIVE_INFINITY;
6114
+ if (v instanceof Date) return v.getTime();
6115
+ if (typeof v === "boolean") return v ? 1 : 0;
6116
+ if (typeof v === "number") return v;
6117
+ return String(v).toLowerCase();
6118
+ };
6119
+ out = [...out].sort((a, b) => {
6120
+ const av = norm(getter(a));
6121
+ const bv = norm(getter(b));
6122
+ if (av < bv) return -1 * dir;
6123
+ if (av > bv) return 1 * dir;
6124
+ return 0;
6125
+ });
6126
+ }
6127
+ return out;
6128
+ }, [items, search, sort, lookupColumn, searchableFields]);
5878
6129
  const [pendingDeleteId, setPendingDeleteId] = useState(null);
5879
6130
  const pendingRecord = useMemo(
5880
6131
  () => pendingDeleteId ? items.find((it) => (it.itemId ?? "") === pendingDeleteId) ?? null : null,
@@ -5889,9 +6140,62 @@ function ItemListView({
5889
6140
  const toolbar = /* @__PURE__ */ jsxs("div", { className: "ra-item-toolbar", children: [
5890
6141
  /* @__PURE__ */ jsxs("div", { className: "ra-item-toolbar-title", children: [
5891
6142
  /* @__PURE__ */ jsx("h2", { className: "ra-display", style: { fontSize: "0.95rem", margin: 0 }, children: i18n.itemListTitle }),
5892
- /* @__PURE__ */ jsx("span", { className: "ra-item-toolbar-count", children: items.length })
6143
+ /* @__PURE__ */ jsx("span", { className: "ra-item-toolbar-count", children: visibleItems.length === items.length ? items.length : `${visibleItems.length} / ${items.length}` })
5893
6144
  ] }),
5894
6145
  /* @__PURE__ */ jsxs("div", { className: "ra-item-toolbar-actions", children: [
6146
+ searchable && items.length > 0 && /* @__PURE__ */ jsxs(
6147
+ "div",
6148
+ {
6149
+ className: "ra-item-search",
6150
+ style: {
6151
+ display: "inline-flex",
6152
+ alignItems: "center",
6153
+ gap: "6px",
6154
+ padding: "4px 8px",
6155
+ border: "1px solid hsl(var(--ra-border))",
6156
+ borderRadius: "6px",
6157
+ background: "hsl(var(--ra-surface))"
6158
+ },
6159
+ children: [
6160
+ /* @__PURE__ */ jsx(Search, { className: "w-3.5 h-3.5", "aria-hidden": "true", style: { opacity: 0.6 } }),
6161
+ /* @__PURE__ */ jsx(
6162
+ "input",
6163
+ {
6164
+ type: "text",
6165
+ value: search,
6166
+ onChange: (e) => setSearch(e.target.value),
6167
+ placeholder: `Search ${itemNoun}s`,
6168
+ "aria-label": `Search ${itemNoun}s`,
6169
+ style: {
6170
+ background: "transparent",
6171
+ border: 0,
6172
+ outline: "none",
6173
+ font: "inherit",
6174
+ fontSize: "12px",
6175
+ color: "inherit",
6176
+ width: "160px"
6177
+ }
6178
+ }
6179
+ ),
6180
+ search && /* @__PURE__ */ jsx(
6181
+ "button",
6182
+ {
6183
+ type: "button",
6184
+ onClick: () => setSearch(""),
6185
+ "aria-label": "Clear search",
6186
+ style: {
6187
+ background: "transparent",
6188
+ border: 0,
6189
+ padding: 0,
6190
+ cursor: "pointer",
6191
+ color: "hsl(var(--ra-muted-text))"
6192
+ },
6193
+ children: /* @__PURE__ */ jsx(X, { className: "w-3.5 h-3.5", "aria-hidden": "true" })
6194
+ }
6195
+ )
6196
+ ]
6197
+ }
6198
+ ),
5895
6199
  !renderItemList && /* @__PURE__ */ jsx(
5896
6200
  ItemViewSwitcher,
5897
6201
  {
@@ -5942,17 +6246,27 @@ function ItemListView({
5942
6246
  )
5943
6247
  }
5944
6248
  );
6249
+ } else if (visibleItems.length === 0) {
6250
+ body = /* @__PURE__ */ jsx(
6251
+ EmptyState,
6252
+ {
6253
+ title: "No matches",
6254
+ body: `No ${itemNoun}s match "${search}".`
6255
+ }
6256
+ );
5945
6257
  } else if (renderItemList) {
5946
- body = renderItemList(items, guardedCtx);
6258
+ body = renderItemList(visibleItems, guardedCtx);
5947
6259
  } else if (view === "table") {
5948
6260
  body = /* @__PURE__ */ jsx(
5949
6261
  DefaultItemTable,
5950
6262
  {
5951
- items,
6263
+ items: visibleItems,
5952
6264
  columns: itemColumns,
5953
6265
  selectedId: guardedCtx.selectedId,
5954
6266
  onOpen: guardedCtx.onOpen,
5955
6267
  onDelete: guardedCtx.onDelete,
6268
+ sort,
6269
+ onToggleSort,
5956
6270
  rowActions,
5957
6271
  rowClipboard,
5958
6272
  i18n
@@ -5962,7 +6276,7 @@ function ItemListView({
5962
6276
  body = /* @__PURE__ */ jsx(
5963
6277
  DefaultItemCards,
5964
6278
  {
5965
- items,
6279
+ items: visibleItems,
5966
6280
  variant: view,
5967
6281
  selectedId: guardedCtx.selectedId,
5968
6282
  ctx: guardedCtx,
@@ -5998,6 +6312,29 @@ function ItemListView({
5998
6312
  }
5999
6313
  ) : null,
6000
6314
  /* @__PURE__ */ jsx("div", { className: "ra-item-list-body", children: body }),
6315
+ wantsFullSet && hasNextPage && /* @__PURE__ */ jsx(
6316
+ "div",
6317
+ {
6318
+ style: {
6319
+ padding: "6px 12px",
6320
+ fontSize: "11px",
6321
+ color: "hsl(var(--ra-muted-text))",
6322
+ borderTop: "1px solid hsl(var(--ra-border))",
6323
+ background: "hsl(var(--ra-muted) / 0.4)"
6324
+ },
6325
+ children: isFetchingNextPage ? `Loading more ${itemNoun}s to ${search ? "search" : "sort"}\u2026` : `${search ? "Searching" : "Sorting"} the ${items.length} ${itemNoun}s loaded so far. More available \u2014 keep loading for the full set.`
6326
+ }
6327
+ ),
6328
+ onLoadMore && /* @__PURE__ */ jsx(
6329
+ LoadMoreFooter,
6330
+ {
6331
+ shown: items.length,
6332
+ total,
6333
+ hasNextPage: !!hasNextPage,
6334
+ isFetchingNextPage: !!isFetchingNextPage,
6335
+ onLoadMore
6336
+ }
6337
+ ),
6001
6338
  /* @__PURE__ */ jsx(
6002
6339
  ConfirmDialog,
6003
6340
  {
@@ -6086,7 +6423,11 @@ function SiblingRail({
6086
6423
  itemNoun,
6087
6424
  dirtyKeys,
6088
6425
  errorKeys,
6089
- i18n
6426
+ i18n,
6427
+ total,
6428
+ hasNextPage,
6429
+ isFetchingNextPage,
6430
+ onLoadMore
6090
6431
  }) {
6091
6432
  const ruleLabelLookup = useRuleLabelLookup();
6092
6433
  const newLabel = i18n.newItem.includes("{noun}") ? i18n.newItem.replace("{noun}", itemNoun ?? "item") : i18n.newItem;
@@ -6166,6 +6507,16 @@ function SiblingRail({
6166
6507
  ) }, key);
6167
6508
  }) })
6168
6509
  ] }),
6510
+ onLoadMore && /* @__PURE__ */ jsx(
6511
+ LoadMoreFooter,
6512
+ {
6513
+ shown: items.length,
6514
+ total,
6515
+ hasNextPage: !!hasNextPage,
6516
+ isFetchingNextPage: !!isFetchingNextPage,
6517
+ onLoadMore
6518
+ }
6519
+ ),
6169
6520
  onCreate && /* @__PURE__ */ jsx("div", { className: "ra-sibling-footer", children: /* @__PURE__ */ jsxs(
6170
6521
  "button",
6171
6522
  {
@@ -7334,12 +7685,17 @@ function RecordsAdminShell(props) {
7334
7685
  }),
7335
7686
  [props.SL, props.collectionId, props.appId, props.recordType]
7336
7687
  );
7688
+ const recordChangeRef = useRef(null);
7689
+ const onRecordChange = useCallback((notice) => {
7690
+ recordChangeRef.current?.(notice);
7691
+ }, []);
7337
7692
  return /* @__PURE__ */ jsx(
7338
7693
  EditorSessionProvider,
7339
7694
  {
7340
7695
  ctx,
7341
7696
  defaultValueFactory: props.defaultData,
7342
7697
  hooks: props.hooks,
7698
+ onRecordChange,
7343
7699
  onHookNotice: (notice) => {
7344
7700
  console.warn(`[RecordsAdmin] ${notice.kind} hook failed`, notice.error);
7345
7701
  try {
@@ -7352,7 +7708,7 @@ function RecordsAdminShell(props) {
7352
7708
  } catch {
7353
7709
  }
7354
7710
  },
7355
- children: /* @__PURE__ */ jsx(RecordsAdminShellInner, { ...props })
7711
+ children: /* @__PURE__ */ jsx(RecordsAdminShellInner, { ...props, recordChangeRef })
7356
7712
  }
7357
7713
  );
7358
7714
  }
@@ -7385,7 +7741,8 @@ function RecordsAdminShellInner(props) {
7385
7741
  recordActions,
7386
7742
  icons: iconsOverride,
7387
7743
  // Deep linking
7388
- deepLink
7744
+ deepLink,
7745
+ recordChangeRef
7389
7746
  } = props;
7390
7747
  const {
7391
7748
  show: showHeader,
@@ -7412,7 +7769,8 @@ function RecordsAdminShellInner(props) {
7412
7769
  groupBy,
7413
7770
  defaultGroupKey,
7414
7771
  onGroupExpanded,
7415
- density = "comfortable"
7772
+ density = "comfortable",
7773
+ pageSize: railPageSize
7416
7774
  } = rail ?? {};
7417
7775
  const {
7418
7776
  tabs: editorTabs = "off",
@@ -7435,7 +7793,10 @@ function RecordsAdminShellInner(props) {
7435
7793
  renderEmpty: renderItemEmpty,
7436
7794
  cardSize: itemCardSize = "md",
7437
7795
  railMode: collectionRailMode = "siblings",
7438
- toSummary: itemToSummary
7796
+ toSummary: itemToSummary,
7797
+ pageSize: itemsPageSize,
7798
+ searchableFields: itemsSearchableFields,
7799
+ searchable: itemsSearchable
7439
7800
  } = items ?? {};
7440
7801
  const {
7441
7802
  strategy: dirtyStrategy = "keep",
@@ -7495,6 +7856,21 @@ function RecordsAdminShellInner(props) {
7495
7856
  [SL, collectionId, appId, recordType]
7496
7857
  );
7497
7858
  const queryClient = useQueryClient();
7859
+ useEffect(() => {
7860
+ recordChangeRef.current = (notice) => {
7861
+ if (notice.kind === "saved") {
7862
+ patchRecordIntoCaches(queryClient, notice.ctx, notice.record);
7863
+ } else {
7864
+ removeRecordFromCaches(queryClient, notice.ctx, notice.recordId);
7865
+ }
7866
+ queryClient.invalidateQueries({
7867
+ queryKey: scopeCountsQueryKey(notice.ctx.collectionId, notice.ctx.appId, notice.ctx.recordType)
7868
+ });
7869
+ };
7870
+ return () => {
7871
+ recordChangeRef.current = null;
7872
+ };
7873
+ }, [queryClient, recordChangeRef]);
7498
7874
  const probe = useScopeProbe({ SL, collectionId });
7499
7875
  const scopeCounts = useScopeCounts({ ctx });
7500
7876
  const topLevelScopes = useMemo(() => {
@@ -7577,7 +7953,8 @@ function RecordsAdminShellInner(props) {
7577
7953
  probeIsLoading: probe.isLoading,
7578
7954
  selectedProductId,
7579
7955
  drillTab,
7580
- classify: classify3
7956
+ classify: classify3,
7957
+ pageSize: railPageSize
7581
7958
  });
7582
7959
  const {
7583
7960
  search,
@@ -7658,7 +8035,8 @@ function RecordsAdminShellInner(props) {
7658
8035
  // host-supplied lifecycle grouping has the full picture.
7659
8036
  includeAll: isCollection && activeScope === "all",
7660
8037
  enabled: isCollection,
7661
- toSummary: itemToSummary
8038
+ toSummary: itemToSummary,
8039
+ pageSize: itemsPageSize
7662
8040
  });
7663
8041
  useEffect(() => {
7664
8042
  if (skipNextItemResetRef.current) {
@@ -7667,6 +8045,36 @@ function RecordsAdminShellInner(props) {
7667
8045
  }
7668
8046
  setSelectedItemId(null);
7669
8047
  }, [editingScope?.raw]);
8048
+ const isLifecycleRailEarly = (activeScope === "all" || activeScope === "collection") && isCollection && !!groupBy;
8049
+ const lifecycleBucketLabel = useMemo(() => {
8050
+ if (!isLifecycleRailEarly || !selectedLifecycleKey || !groupBy) return null;
8051
+ for (const it of collectionItems.items) {
8052
+ const g = groupBy(it);
8053
+ if (g && g.key === selectedLifecycleKey) return g.label ?? null;
8054
+ }
8055
+ return null;
8056
+ }, [isLifecycleRailEarly, selectedLifecycleKey, groupBy, collectionItems.items]);
8057
+ const scopedCollectionItemsList = useMemo(() => {
8058
+ if (!isLifecycleRailEarly || !selectedLifecycleKey || !groupBy) return collectionItems.items;
8059
+ return collectionItems.items.filter((it) => {
8060
+ const g = groupBy(it);
8061
+ return g?.key === selectedLifecycleKey;
8062
+ });
8063
+ }, [isLifecycleRailEarly, selectedLifecycleKey, groupBy, collectionItems.items]);
8064
+ useEffect(() => {
8065
+ if (!isLifecycleRailEarly || !groupBy) return;
8066
+ if (selectedLifecycleKey || !selectedItemId) return;
8067
+ const row = collectionItems.items.find(
8068
+ (it) => it.itemId === selectedItemId || it.id === selectedItemId
8069
+ );
8070
+ if (!row) return;
8071
+ const g = groupBy(row);
8072
+ if (g?.key) setSelectedLifecycleKey(g.key);
8073
+ }, [isLifecycleRailEarly, groupBy, selectedItemId, selectedLifecycleKey, collectionItems.items, setSelectedLifecycleKey]);
8074
+ const scopedCollectionItems = useMemo(() => ({
8075
+ ...collectionItems,
8076
+ items: scopedCollectionItemsList
8077
+ }), [collectionItems, scopedCollectionItemsList]);
7670
8078
  const { lastAppliedDLRef } = useShellDeepLink({
7671
8079
  deepLinkState,
7672
8080
  editingScope,
@@ -7769,7 +8177,6 @@ function RecordsAdminShellInner(props) {
7769
8177
  if (isCreate && isCollection && savedRecordId && isDraftId3(selectedItemId)) {
7770
8178
  setSelectedItemId(savedRecordId);
7771
8179
  }
7772
- await refetchAll();
7773
8180
  if (!isCollection && isCreate && activeScope === "collection") {
7774
8181
  setSelectedRecordId(savedRecordId ?? null);
7775
8182
  }
@@ -7788,7 +8195,6 @@ function RecordsAdminShellInner(props) {
7788
8195
  setSelectedRecordId(null);
7789
8196
  setDraftKind(null);
7790
8197
  }
7791
- await refetchAll();
7792
8198
  setIsReconcilingRecordSelection(false);
7793
8199
  }
7794
8200
  });
@@ -7954,7 +8360,7 @@ function RecordsAdminShellInner(props) {
7954
8360
  setSelectedItemId,
7955
8361
  editingScope,
7956
8362
  baseScopeRef,
7957
- collectionItems,
8363
+ collectionItems: scopedCollectionItems,
7958
8364
  queryClient,
7959
8365
  ctx,
7960
8366
  collectionId,
@@ -8670,7 +9076,7 @@ function RecordsAdminShellInner(props) {
8670
9076
  !railHidden && /* @__PURE__ */ jsx("aside", { className: "border-r overflow-hidden flex flex-col", style: { borderColor: "hsl(var(--ra-border))", background: "hsl(var(--ra-surface))" }, children: isCollection && selectedItemId && collectionRailMode === "siblings" && ruleWizardStep === null ? /* @__PURE__ */ jsx(
8671
9077
  SiblingRail,
8672
9078
  {
8673
- items: collectionItems.items,
9079
+ items: scopedCollectionItemsList,
8674
9080
  selectedItemId,
8675
9081
  isLoading: collectionItems.isLoading,
8676
9082
  error: collectionItems.error,
@@ -8680,8 +9086,13 @@ function RecordsAdminShellInner(props) {
8680
9086
  itemNoun: itemNounLabel,
8681
9087
  dirtyKeys,
8682
9088
  errorKeys,
8683
- contextKind: activeScope === "rule" ? "Rule" : activeScope === "product" ? "Product" : activeScope === "collection" ? "Global" : activeScope === "all" ? "All records" : activeScope === "variant" ? "Variant" : activeScope === "batch" ? "Batch" : activeScope === "facet" ? "Facet" : void 0,
8684
- contextSummary: activeScope === "rule" ? activeRuleSummary : activeScope === "product" ? editorHeaderLabel ?? null : null,
9089
+ hasNextPage: !!collectionItems.hasNextPage,
9090
+ isFetchingNextPage: !!collectionItems.isFetchingNextPage,
9091
+ onLoadMore: () => {
9092
+ void collectionItems.fetchNextPage();
9093
+ },
9094
+ contextKind: isLifecycleRailEarly && lifecycleBucketLabel ? lifecycleBucketLabel : activeScope === "rule" ? "Rule" : activeScope === "product" ? "Product" : activeScope === "collection" ? "Global" : activeScope === "all" ? "All records" : activeScope === "variant" ? "Variant" : activeScope === "batch" ? "Batch" : activeScope === "facet" ? "Facet" : void 0,
9095
+ contextSummary: isLifecycleRailEarly && lifecycleBucketLabel ? `${scopedCollectionItemsList.length} ${itemNounLabel}${scopedCollectionItemsList.length === 1 ? "" : "s"}` : activeScope === "rule" ? activeRuleSummary : activeScope === "product" ? editorHeaderLabel ?? null : null,
8685
9096
  i18n
8686
9097
  }
8687
9098
  ) : /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -8859,19 +9270,29 @@ function RecordsAdminShellInner(props) {
8859
9270
  i18n
8860
9271
  }
8861
9272
  ),
8862
- isProductTab && !productPinned && productBrowse.hasNextPage && /* @__PURE__ */ jsx("div", { className: "p-2", children: /* @__PURE__ */ jsx(
8863
- "button",
9273
+ isProductTab && !productPinned && /* @__PURE__ */ jsx(
9274
+ LoadMoreFooter,
8864
9275
  {
8865
- type: "button",
8866
- onClick: () => {
9276
+ shown: leftItems.length,
9277
+ hasNextPage: !!productBrowse.hasNextPage,
9278
+ isFetchingNextPage: !!productBrowse.isFetchingNextPage,
9279
+ onLoadMore: () => {
8867
9280
  void productBrowse.fetchNextPage();
8868
- },
8869
- disabled: productBrowse.isFetchingNextPage,
8870
- className: "w-full text-xs py-2 rounded-md border transition-opacity disabled:opacity-50 hover:bg-[hsl(var(--ra-muted))]",
8871
- style: { borderColor: "hsl(var(--ra-border))", color: "hsl(var(--ra-text))" },
8872
- children: productBrowse.isFetchingNextPage ? "Loading\u2026" : `Load more (${leftItems.length} shown)`
9281
+ }
8873
9282
  }
8874
- ) })
9283
+ ),
9284
+ isRecordsTab && (!isCollection || !(isAllTab || isGlobalTab)) && /* @__PURE__ */ jsx(
9285
+ LoadMoreFooter,
9286
+ {
9287
+ shown: recordList.items.length,
9288
+ total: recordList.total,
9289
+ hasNextPage: !!recordList.hasNextPage,
9290
+ isFetchingNextPage: !!recordList.isFetchingNextPage,
9291
+ onLoadMore: () => {
9292
+ void recordList.fetchNextPage();
9293
+ }
9294
+ }
9295
+ )
8875
9296
  ] })
8876
9297
  ] }) })
8877
9298
  ] }) }),
@@ -8956,6 +9377,14 @@ function RecordsAdminShellInner(props) {
8956
9377
  ruleSummary: activeRuleSummary,
8957
9378
  rowActions: wrappedRecordActions,
8958
9379
  rowClipboard,
9380
+ searchableFields: itemsSearchableFields,
9381
+ searchable: itemsSearchable,
9382
+ total: collectionItems.total,
9383
+ hasNextPage: !!collectionItems.hasNextPage,
9384
+ isFetchingNextPage: !!collectionItems.isFetchingNextPage,
9385
+ onLoadMore: () => {
9386
+ void collectionItems.fetchNextPage();
9387
+ },
8959
9388
  i18n
8960
9389
  }
8961
9390
  ),