@proveanything/smartlinks-utils-ui 0.11.10 → 0.12.1

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.
@@ -7,7 +7,7 @@ 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
9
  import { createContext, useMemo, useState, useEffect, useCallback, useRef, isValidElement, useLayoutEffect, useContext, useSyncExternalStore, 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, FilePlus2, 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';
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, Check, Rows3, ChevronRight, Eraser, FilePlus2, ClipboardPaste, Box, X, Search, Image, Table, ArrowLeft, ChevronLeft, AlertTriangle, Info, HelpCircle, CornerDownLeft, Circle, ArrowUpDown, ArrowUp, ArrowDown, MinusCircle, XCircle, CopyPlus, AlertCircle, Undo2, Save, Loader2, Archive, ArrowRight, Globe2, 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';
@@ -250,7 +250,26 @@ var DEFAULT_I18N = {
250
250
  hookBeforeSaveFailed: "Couldn't save: {message}",
251
251
  hookAfterSaveFailed: "Saved, but a follow-up step failed: {message}",
252
252
  hookBeforeDeleteFailed: "Couldn't delete: {message}",
253
- hookAfterDeleteFailed: "Deleted, but a follow-up step failed: {message}"
253
+ hookAfterDeleteFailed: "Deleted, but a follow-up step failed: {message}",
254
+ historyDisclosureShow: "Show {n} archived",
255
+ historyDisclosureHide: "Hide {n} archived",
256
+ lifecycleStatusLabel: "Status",
257
+ lifecycleStatusActive: "Active",
258
+ lifecycleStatusArchived: "Archived",
259
+ lifecycleStatusDraft: "Draft",
260
+ lifecycleStatusHint: "Archived records aren't returned to public consumers.",
261
+ actionArchive: "Archive",
262
+ actionRestore: "Restore",
263
+ lifecycleMenuLabel: "Status",
264
+ lifecycleChangeTo: "Change status to {label}",
265
+ lifecycleCurrentBadge: "Current",
266
+ conflictBannerTitle: "Duplicate records detected",
267
+ conflictBannerBodyOne: "{n} extra active record shares this slot. Only the oldest is used.",
268
+ conflictBannerBodyMany: "{slots} slots have duplicates ({dups} extra records). Only the oldest in each is used.",
269
+ conflictArchiveDuplicates: "Archive duplicates",
270
+ conflictDeleteDuplicates: "Delete duplicates",
271
+ conflictDeleteConfirm: "Permanently delete {n} duplicate record(s)? This cannot be undone.",
272
+ conflictResolveLabel: "Resolve"
254
273
  };
255
274
 
256
275
  // src/components/RecordsAdmin/types/presentation.ts
@@ -603,6 +622,72 @@ var useScopeCounts = (args) => {
603
622
  return result;
604
623
  };
605
624
  var scopeCountsQueryKey = (collectionId, appId, recordType) => [...QK_BASE, collectionId, appId, recordType ?? null];
625
+
626
+ // src/components/RecordsAdmin/data/singletonConflicts.ts
627
+ var NO_RULE = "__no_rule__";
628
+ var DEFAULT_ACTIVE_STATUSES = ["active"];
629
+ var isActive = (record, activeStatuses = DEFAULT_ACTIVE_STATUSES) => {
630
+ const s = record.lifecycleStatus;
631
+ if (s == null || s === "") return true;
632
+ return activeStatuses.includes(s);
633
+ };
634
+ var slotKey = (record) => {
635
+ const anchor = anchorKey(record.scope);
636
+ const rule = ruleHash(record.facetRule) ?? NO_RULE;
637
+ return `${anchor}::${rule}`;
638
+ };
639
+ var pickActiveRecord = (records) => {
640
+ if (records.length === 0) return null;
641
+ if (records.length === 1) return records[0];
642
+ const sorted = [...records].sort((a, b) => {
643
+ const aT = a.updatedAt;
644
+ const bT = b.updatedAt;
645
+ if (aT && bT) {
646
+ if (aT < bT) return -1;
647
+ if (aT > bT) return 1;
648
+ } else if (aT && !bT) {
649
+ return -1;
650
+ } else if (!aT && bT) {
651
+ return 1;
652
+ }
653
+ const aId = a.id ?? "";
654
+ const bId = b.id ?? "";
655
+ if (aId < bId) return -1;
656
+ if (aId > bId) return 1;
657
+ return 0;
658
+ });
659
+ return sorted[0];
660
+ };
661
+ var groupSingletonConflicts = (items, activeStatuses = DEFAULT_ACTIVE_STATUSES) => {
662
+ const buckets = /* @__PURE__ */ new Map();
663
+ for (const item of items) {
664
+ if (!isActive(item, activeStatuses)) continue;
665
+ const k = slotKey(item);
666
+ const existing = buckets.get(k);
667
+ if (existing) existing.push(item);
668
+ else buckets.set(k, [item]);
669
+ }
670
+ const conflicts = [];
671
+ for (const [key, records] of buckets) {
672
+ if (records.length < 2) continue;
673
+ const active = pickActiveRecord(records);
674
+ conflicts.push({
675
+ key,
676
+ records,
677
+ active,
678
+ duplicates: records.filter((r) => r !== active)
679
+ });
680
+ }
681
+ return conflicts;
682
+ };
683
+ var findConflictForRecord = (recordId, conflicts) => {
684
+ for (const c of conflicts) {
685
+ if (c.records.some((r) => r.id === recordId)) return c;
686
+ }
687
+ return null;
688
+ };
689
+
690
+ // src/components/RecordsAdmin/hooks/useRecordList.ts
606
691
  var defaultClassify = (r) => {
607
692
  if (!r.data) return "empty";
608
693
  const keys = Object.keys(r.data);
@@ -662,7 +747,8 @@ var toSummary = (rec) => {
662
747
  status: "configured",
663
748
  label: fallbackLabel,
664
749
  updatedAt: rec.updatedAt,
665
- facetRule
750
+ facetRule,
751
+ lifecycleStatus: rec.status ?? void 0
666
752
  };
667
753
  };
668
754
  var QK_BASE2 = ["records-admin", "list"];
@@ -676,7 +762,8 @@ var useRecordList = (args) => {
676
762
  enabled = true,
677
763
  scaffolder,
678
764
  contextScope,
679
- pageSize = 100
765
+ pageSize = 100,
766
+ activeStatuses = DEFAULT_ACTIVE_STATUSES
680
767
  } = args;
681
768
  const queryClient = useQueryClient();
682
769
  const queryKey = useMemo(
@@ -739,6 +826,24 @@ var useRecordList = (args) => {
739
826
  partial: items.filter((r) => r.status === "partial").length,
740
827
  empty: items.filter((r) => r.status === "empty").length
741
828
  }), [items]);
829
+ const activeItems = useMemo(
830
+ () => filtered.filter((r) => isActive(r, activeStatuses)),
831
+ [filtered, activeStatuses]
832
+ );
833
+ const historyItems = useMemo(
834
+ () => filtered.filter((r) => !isActive(r, activeStatuses)),
835
+ [filtered, activeStatuses]
836
+ );
837
+ const historyBySlot = useMemo(() => {
838
+ const map = /* @__PURE__ */ new Map();
839
+ for (const r of historyItems) {
840
+ const k = slotKey(r);
841
+ const list = map.get(k);
842
+ if (list) list.push(r);
843
+ else map.set(k, [r]);
844
+ }
845
+ return map;
846
+ }, [historyItems]);
742
847
  const refetch = useCallback(() => queryClient.refetchQueries({
743
848
  queryKey: [...QK_BASE2, ctx.collectionId, ctx.appId, ctx.recordType]
744
849
  }), [queryClient, ctx.collectionId, ctx.appId, ctx.recordType]);
@@ -746,6 +851,9 @@ var useRecordList = (args) => {
746
851
  return {
747
852
  allItems: items,
748
853
  items: filtered,
854
+ activeItems,
855
+ historyItems,
856
+ historyBySlot,
749
857
  total,
750
858
  counts,
751
859
  isLoading: query.isLoading,
@@ -1021,7 +1129,8 @@ function useShellBrowser(opts) {
1021
1129
  selectedProductId,
1022
1130
  drillTab,
1023
1131
  classify: classify3,
1024
- pageSize
1132
+ pageSize,
1133
+ activeStatuses
1025
1134
  } = opts;
1026
1135
  const [search, setSearch] = useState("");
1027
1136
  const [filter, setFilter] = useState("all");
@@ -1048,7 +1157,8 @@ function useShellBrowser(opts) {
1048
1157
  classify: classify3,
1049
1158
  contextScope,
1050
1159
  enabled: recordListEnabled,
1051
- pageSize
1160
+ pageSize,
1161
+ activeStatuses
1052
1162
  });
1053
1163
  const facetBrowse = useFacetBrowse({
1054
1164
  SL,
@@ -2569,7 +2679,11 @@ var createEditorStore = () => {
2569
2679
  ref: spec.ref,
2570
2680
  scope: anchors,
2571
2681
  data: persistedValue,
2572
- facetRule: persistedFacetRule
2682
+ facetRule: persistedFacetRule,
2683
+ // Seed the configured lifecycle default (e.g. 'draft') so
2684
+ // brand-new records land in the right bucket without the host
2685
+ // wiring it into every editor's beforeSave.
2686
+ status: spec.defaultStatus
2573
2687
  });
2574
2688
  nextRecordId = created?.id ?? nextRecordId;
2575
2689
  savedRecord = created;
@@ -2578,7 +2692,10 @@ var createEditorStore = () => {
2578
2692
  ref: spec.ref,
2579
2693
  scope: anchors,
2580
2694
  data: persistedValue,
2581
- facetRule: persistedFacetRule
2695
+ facetRule: persistedFacetRule,
2696
+ // Same seeding rule applies on the upsert path used by
2697
+ // singleton-cardinality first-time saves.
2698
+ status: !entry.recordId ? spec.defaultStatus : void 0
2582
2699
  });
2583
2700
  nextRecordId = upserted.record?.id ?? nextRecordId;
2584
2701
  savedRecord = upserted.record;
@@ -3044,7 +3161,8 @@ function useShellEditorTarget(args) {
3044
3161
  defaultData,
3045
3162
  deriveDraftLabel,
3046
3163
  onSaved,
3047
- onDeleted
3164
+ onDeleted,
3165
+ defaultStatus
3048
3166
  } = args;
3049
3167
  const editorTargetSpec = useMemo(() => {
3050
3168
  if (!editingTargetScope) return null;
@@ -3076,7 +3194,8 @@ function useShellEditorTarget(args) {
3076
3194
  label,
3077
3195
  draftKey,
3078
3196
  isDraftScope,
3079
- initialData: createMode ? ruleWizardInitialData : explicitSingletonInitialData ?? void 0
3197
+ initialData: createMode ? ruleWizardInitialData : explicitSingletonInitialData ?? void 0,
3198
+ defaultStatus
3080
3199
  };
3081
3200
  }, [
3082
3201
  editingTargetScope?.raw,
@@ -3091,7 +3210,8 @@ function useShellEditorTarget(args) {
3091
3210
  ruleWizardDraftKey,
3092
3211
  ruleWizardInitialData,
3093
3212
  explicitSingletonInitialData,
3094
- ruleWizardRule
3213
+ ruleWizardRule,
3214
+ defaultStatus
3095
3215
  ]);
3096
3216
  const editorCtx = useEditorBridge({
3097
3217
  target: editorTargetSpec,
@@ -3207,7 +3327,7 @@ var ScopeTabs = ({
3207
3327
  const iconMap = icons ?? DEFAULT_ICONS.scope;
3208
3328
  return /* @__PURE__ */ jsx("div", { role: "tablist", className: "ra-tabs", "aria-label": "Record scope", children: scopes.map((s) => {
3209
3329
  const Icon = iconMap[s] ?? DEFAULT_ICONS.scope[s];
3210
- const isActive = active === s;
3330
+ const isActive2 = active === s;
3211
3331
  const count = counts?.[s];
3212
3332
  const tooltip = tooltips?.[s];
3213
3333
  return /* @__PURE__ */ jsxs(
@@ -3215,7 +3335,7 @@ var ScopeTabs = ({
3215
3335
  {
3216
3336
  type: "button",
3217
3337
  role: "tab",
3218
- "aria-selected": isActive,
3338
+ "aria-selected": isActive2,
3219
3339
  onClick: () => onChange(s),
3220
3340
  disabled: loading,
3221
3341
  className: "ra-tab",
@@ -3650,7 +3770,8 @@ var RecordList = ({
3650
3770
  renderGroupActions,
3651
3771
  rowClipboard,
3652
3772
  rowActions,
3653
- i18n
3773
+ i18n,
3774
+ historyBySlot
3654
3775
  }) => {
3655
3776
  const containerRef = useRef(null);
3656
3777
  const onKeyDown = useCallback((e) => {
@@ -3712,12 +3833,53 @@ var RecordList = ({
3712
3833
  }
3713
3834
  return orderedKeys.map((k) => buckets.get(k));
3714
3835
  }, [items, groupBy]);
3836
+ const [expandedSlots, setExpandedSlots] = useState(() => /* @__PURE__ */ new Set());
3837
+ const toggleSlot = useCallback((k) => {
3838
+ setExpandedSlots((prev) => {
3839
+ const next = new Set(prev);
3840
+ if (next.has(k)) next.delete(k);
3841
+ else next.add(k);
3842
+ return next;
3843
+ });
3844
+ }, []);
3715
3845
  const renderItems = (rows) => {
3716
3846
  const compact = presentation === "compact";
3717
3847
  return /* @__PURE__ */ jsx("ul", { children: rows.map((item, idx) => {
3718
3848
  const ctx = buildCtx(item);
3719
3849
  const key = item.id ?? (anchorKey(item.scope) || `pos:${idx}`);
3720
- return /* @__PURE__ */ jsx("li", { children: renderListRow ? renderListRow(item, ctx) : /* @__PURE__ */ jsx(DefaultRecordRow, { record: item, ctx, compact }) }, key);
3850
+ const sKey = historyBySlot ? slotKey(item) : null;
3851
+ const history = sKey ? historyBySlot.get(sKey) : void 0;
3852
+ const expanded = sKey ? expandedSlots.has(sKey) : false;
3853
+ const showLabel = i18n?.historyDisclosureShow ?? "Show {n} archived";
3854
+ const hideLabel = i18n?.historyDisclosureHide ?? "Hide {n} archived";
3855
+ const label = (expanded ? hideLabel : showLabel).replace("{n}", String(history?.length ?? 0));
3856
+ return /* @__PURE__ */ jsxs("li", { children: [
3857
+ renderListRow ? renderListRow(item, ctx) : /* @__PURE__ */ jsx(DefaultRecordRow, { record: item, ctx, compact }),
3858
+ history && history.length > 0 ? /* @__PURE__ */ jsxs("div", { className: "ra-history-block", children: [
3859
+ expanded ? /* @__PURE__ */ jsx("ul", { className: "ra-history-rows", "aria-label": "Archived records", children: history.map((h, hIdx) => {
3860
+ const hCtx = buildCtx(h);
3861
+ const hKey = h.id ?? `hist:${idx}:${hIdx}`;
3862
+ const badged = {
3863
+ ...h,
3864
+ badges: [
3865
+ ...h.badges ?? [],
3866
+ { label: h.lifecycleStatus ?? "archived", tone: "warning" }
3867
+ ]
3868
+ };
3869
+ return /* @__PURE__ */ jsx("li", { className: "ra-history-row", "data-history": "true", children: renderListRow ? renderListRow(badged, hCtx) : /* @__PURE__ */ jsx(DefaultRecordRow, { record: badged, ctx: hCtx, compact }) }, hKey);
3870
+ }) }) : null,
3871
+ /* @__PURE__ */ jsx(
3872
+ "button",
3873
+ {
3874
+ type: "button",
3875
+ className: "ra-history-disclosure",
3876
+ onClick: () => sKey && toggleSlot(sKey),
3877
+ "aria-expanded": expanded,
3878
+ children: label
3879
+ }
3880
+ )
3881
+ ] }) : null
3882
+ ] }, key);
3721
3883
  }) });
3722
3884
  };
3723
3885
  if (groups) {
@@ -3797,6 +3959,306 @@ var ProductList = RecordList;
3797
3959
  var FacetList = RecordList;
3798
3960
  var VariantList = RecordList;
3799
3961
  var BatchList = RecordList;
3962
+ var LifecycleStatusControl = ({
3963
+ SL,
3964
+ collectionId,
3965
+ appId,
3966
+ recordId,
3967
+ current,
3968
+ options,
3969
+ i18n,
3970
+ onChanged
3971
+ }) => {
3972
+ const [busy, setBusy] = useState(false);
3973
+ const value = current ?? "active";
3974
+ const opts = options ?? [
3975
+ { value: "active", label: i18n.lifecycleStatusActive },
3976
+ { value: "archived", label: i18n.lifecycleStatusArchived },
3977
+ { value: "draft", label: i18n.lifecycleStatusDraft }
3978
+ ];
3979
+ const onChange = useCallback(async (e) => {
3980
+ const next = e.target.value;
3981
+ if (next === value) return;
3982
+ setBusy(true);
3983
+ try {
3984
+ await SL.app.records.update(collectionId, appId, recordId, { status: next }, true);
3985
+ onChanged?.(next);
3986
+ } catch (err) {
3987
+ console.warn("[LifecycleStatusControl] update failed", err);
3988
+ } finally {
3989
+ setBusy(false);
3990
+ }
3991
+ }, [SL, collectionId, appId, recordId, value, onChanged]);
3992
+ return /* @__PURE__ */ jsxs(
3993
+ "label",
3994
+ {
3995
+ className: "inline-flex items-center gap-1.5 text-[11px] mt-0.5",
3996
+ title: i18n.lifecycleStatusHint,
3997
+ style: { color: "hsl(var(--ra-muted-text))" },
3998
+ children: [
3999
+ /* @__PURE__ */ jsx("span", { className: "uppercase tracking-wide", children: i18n.lifecycleStatusLabel }),
4000
+ /* @__PURE__ */ jsx(
4001
+ "select",
4002
+ {
4003
+ value,
4004
+ onChange,
4005
+ disabled: busy,
4006
+ className: "text-xs px-1.5 py-0.5 rounded border bg-transparent",
4007
+ style: {
4008
+ borderColor: "hsl(var(--ra-border))",
4009
+ color: "hsl(var(--ra-text))",
4010
+ background: "hsl(var(--ra-surface))"
4011
+ },
4012
+ children: opts.map((o) => /* @__PURE__ */ jsx("option", { value: o.value, children: o.label }, o.value))
4013
+ }
4014
+ )
4015
+ ]
4016
+ }
4017
+ );
4018
+ };
4019
+
4020
+ // src/components/RecordsAdmin/data/lifecycleStatuses.ts
4021
+ var DEFAULT_LIFECYCLE_STATUSES = [
4022
+ { value: "draft", label: "Draft", tone: "warning" },
4023
+ { value: "active", label: "Active", tone: "success", isActive: true },
4024
+ { value: "archived", label: "Archived", tone: "muted" }
4025
+ ];
4026
+ var getLifecycleStatuses = (config) => {
4027
+ if (config?.statuses && config.statuses.length > 0) return config.statuses;
4028
+ return DEFAULT_LIFECYCLE_STATUSES;
4029
+ };
4030
+ var getActiveStatusValues = (config, legacyActiveStatuses) => {
4031
+ const fromDefs = getLifecycleStatuses(config).filter((s) => s.isActive).map((s) => s.value);
4032
+ if (legacyActiveStatuses && legacyActiveStatuses.length > 0) {
4033
+ const set = /* @__PURE__ */ new Set([...fromDefs, ...legacyActiveStatuses]);
4034
+ return Array.from(set);
4035
+ }
4036
+ return fromDefs.length > 0 ? fromDefs : ["active"];
4037
+ };
4038
+ var UNKNOWN_DEF = (value) => ({
4039
+ value,
4040
+ label: value,
4041
+ tone: "default"
4042
+ });
4043
+ var resolveLifecycleStatus = (record, config) => {
4044
+ const defs = getLifecycleStatuses(config);
4045
+ const raw = record.lifecycleStatus;
4046
+ if (raw == null || raw === "") {
4047
+ return defs.find((d) => d.value === "active") ?? defs.find((d) => d.isActive) ?? defs[0];
4048
+ }
4049
+ return defs.find((d) => d.value === raw) ?? UNKNOWN_DEF(raw);
4050
+ };
4051
+ var hasMixedLifecycle = (records, activeValues) => {
4052
+ for (const r of records) {
4053
+ const s = r.lifecycleStatus;
4054
+ if (s == null || s === "") continue;
4055
+ if (!activeValues.includes(s)) return true;
4056
+ }
4057
+ return false;
4058
+ };
4059
+ var LIFECYCLE_BUCKET_ORDER = ["draft", "active", "archived"];
4060
+ var compareLifecycleBuckets = (a, b) => {
4061
+ const ai = LIFECYCLE_BUCKET_ORDER.indexOf(a);
4062
+ const bi = LIFECYCLE_BUCKET_ORDER.indexOf(b);
4063
+ if (ai === -1 && bi === -1) return a.localeCompare(b);
4064
+ if (ai === -1) return 1;
4065
+ if (bi === -1) return -1;
4066
+ return ai - bi;
4067
+ };
4068
+ var toneColor = (tone) => {
4069
+ switch (tone) {
4070
+ case "success":
4071
+ return "hsl(var(--ra-success, 142 70% 40%))";
4072
+ case "warning":
4073
+ return "hsl(var(--ra-warning, 38 92% 50%))";
4074
+ case "danger":
4075
+ return "hsl(var(--ra-danger, 0 70% 45%))";
4076
+ case "muted":
4077
+ return "hsl(var(--ra-muted-text))";
4078
+ default:
4079
+ return "hsl(var(--ra-text))";
4080
+ }
4081
+ };
4082
+ var ToneDot = ({ tone, className }) => /* @__PURE__ */ jsx(
4083
+ "span",
4084
+ {
4085
+ "aria-hidden": "true",
4086
+ className: className ?? "inline-block w-2 h-2 rounded-full shrink-0",
4087
+ style: { background: toneColor(tone) }
4088
+ }
4089
+ );
4090
+ var LifecycleStatusMenu = ({
4091
+ SL,
4092
+ collectionId,
4093
+ appId,
4094
+ recordId,
4095
+ scope,
4096
+ current,
4097
+ statuses,
4098
+ i18n,
4099
+ beforeChange,
4100
+ onChanged,
4101
+ onTelemetry,
4102
+ size = "sm"
4103
+ }) => {
4104
+ const [open, setOpen] = useState(false);
4105
+ const [busy, setBusy] = useState(null);
4106
+ const wrapperRef = useRef(null);
4107
+ const triggerRef = useRef(null);
4108
+ const menuRef = useRef(null);
4109
+ const [pos, setPos] = useState(null);
4110
+ const currentDef = resolveLifecycleStatus({ lifecycleStatus: current }, { statuses });
4111
+ useEffect(() => {
4112
+ if (!open) return;
4113
+ const onDoc = (e) => {
4114
+ const t = e.target;
4115
+ if (wrapperRef.current?.contains(t)) return;
4116
+ if (menuRef.current?.contains(t)) return;
4117
+ setOpen(false);
4118
+ };
4119
+ const onKey = (e) => {
4120
+ if (e.key === "Escape") setOpen(false);
4121
+ };
4122
+ document.addEventListener("mousedown", onDoc);
4123
+ document.addEventListener("keydown", onKey);
4124
+ return () => {
4125
+ document.removeEventListener("mousedown", onDoc);
4126
+ document.removeEventListener("keydown", onKey);
4127
+ };
4128
+ }, [open]);
4129
+ useLayoutEffect(() => {
4130
+ if (!open) {
4131
+ setPos(null);
4132
+ return;
4133
+ }
4134
+ const update = () => {
4135
+ const el = triggerRef.current;
4136
+ if (!el) return;
4137
+ const r = el.getBoundingClientRect();
4138
+ const menuHeight = menuRef.current?.offsetHeight ?? 36 * statuses.length + 16;
4139
+ const menuWidth = Math.max(r.width, 180);
4140
+ const margin = 8;
4141
+ const fitsAbove = r.top - menuHeight - margin >= 0;
4142
+ const top = fitsAbove ? r.top - menuHeight - 4 : r.bottom + 4;
4143
+ const left = Math.max(margin, Math.min(window.innerWidth - menuWidth - margin, r.left));
4144
+ setPos({ top, left, width: menuWidth });
4145
+ };
4146
+ update();
4147
+ window.addEventListener("resize", update);
4148
+ window.addEventListener("scroll", update, true);
4149
+ return () => {
4150
+ window.removeEventListener("resize", update);
4151
+ window.removeEventListener("scroll", update, true);
4152
+ };
4153
+ }, [open, statuses.length]);
4154
+ const choose = useCallback(async (next) => {
4155
+ if (busy) return;
4156
+ if (next.value === (current ?? currentDef.value)) {
4157
+ setOpen(false);
4158
+ return;
4159
+ }
4160
+ if (beforeChange) {
4161
+ try {
4162
+ const ok = await beforeChange({
4163
+ recordId,
4164
+ scope,
4165
+ from: current,
4166
+ to: next.value
4167
+ });
4168
+ if (ok === false) {
4169
+ setOpen(false);
4170
+ return;
4171
+ }
4172
+ } catch {
4173
+ setOpen(false);
4174
+ return;
4175
+ }
4176
+ }
4177
+ setBusy(next.value);
4178
+ try {
4179
+ await SL.app.records.update(collectionId, appId, recordId, { status: next.value }, true);
4180
+ onTelemetry?.({ from: current, to: next.value });
4181
+ onChanged?.(next.value);
4182
+ } catch (err) {
4183
+ console.warn("[LifecycleStatusMenu] update failed", err);
4184
+ } finally {
4185
+ setBusy(null);
4186
+ setOpen(false);
4187
+ }
4188
+ }, [busy, current, currentDef.value, beforeChange, recordId, scope, SL, collectionId, appId, onChanged, onTelemetry]);
4189
+ const padding = size === "xs" ? "px-2 py-1" : "px-3 py-1.5";
4190
+ const fontSize = size === "xs" ? "0.7rem" : "0.75rem";
4191
+ return /* @__PURE__ */ jsxs("div", { ref: wrapperRef, className: "relative inline-flex", children: [
4192
+ /* @__PURE__ */ jsxs(
4193
+ "button",
4194
+ {
4195
+ ref: triggerRef,
4196
+ type: "button",
4197
+ className: `${padding} rounded-md border transition-colors hover:bg-[hsl(var(--ra-muted))] inline-flex items-center gap-1.5`,
4198
+ "aria-haspopup": "menu",
4199
+ "aria-expanded": open,
4200
+ "aria-label": i18n.lifecycleMenuLabel ?? i18n.lifecycleStatusLabel,
4201
+ title: i18n.lifecycleStatusHint,
4202
+ disabled: !!busy,
4203
+ style: {
4204
+ borderColor: "hsl(var(--ra-border))",
4205
+ color: "hsl(var(--ra-text))",
4206
+ background: "hsl(var(--ra-surface))",
4207
+ fontSize
4208
+ },
4209
+ onClick: (e) => {
4210
+ e.stopPropagation();
4211
+ setOpen((v) => !v);
4212
+ },
4213
+ children: [
4214
+ /* @__PURE__ */ jsx(ToneDot, { tone: currentDef.tone }),
4215
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: currentDef.label }),
4216
+ /* @__PURE__ */ jsx(ChevronDown, { className: "w-3 h-3 opacity-60", "aria-hidden": "true" })
4217
+ ]
4218
+ }
4219
+ ),
4220
+ open && typeof document !== "undefined" && createPortal(
4221
+ /* @__PURE__ */ jsx(
4222
+ "div",
4223
+ {
4224
+ ref: menuRef,
4225
+ role: "menu",
4226
+ className: "ra-row-menu ra-row-menu-portal",
4227
+ style: pos ? { top: pos.top, left: pos.left, minWidth: pos.width } : { visibility: "hidden" },
4228
+ onClick: (e) => e.stopPropagation(),
4229
+ onMouseDown: (e) => e.stopPropagation(),
4230
+ children: statuses.map((s) => {
4231
+ const isCurrent = s.value === (current ?? currentDef.value);
4232
+ const Icon = s.icon;
4233
+ return /* @__PURE__ */ jsxs(
4234
+ "button",
4235
+ {
4236
+ type: "button",
4237
+ role: "menuitemradio",
4238
+ "aria-checked": isCurrent,
4239
+ disabled: busy !== null,
4240
+ className: "ra-row-menu-item",
4241
+ onClick: (e) => {
4242
+ e.stopPropagation();
4243
+ void choose(s);
4244
+ },
4245
+ children: [
4246
+ /* @__PURE__ */ jsx(ToneDot, { tone: s.tone }),
4247
+ Icon && /* @__PURE__ */ jsx(Icon, { className: "w-3.5 h-3.5 opacity-70" }),
4248
+ /* @__PURE__ */ jsx("span", { className: "flex-1 text-left", children: s.label }),
4249
+ isCurrent && /* @__PURE__ */ jsx(Check, { className: "w-3.5 h-3.5 opacity-80", "aria-hidden": "true" }),
4250
+ busy === s.value && /* @__PURE__ */ jsx("span", { className: "opacity-60", children: "\u2026" })
4251
+ ]
4252
+ },
4253
+ s.value
4254
+ );
4255
+ })
4256
+ }
4257
+ ),
4258
+ document.body
4259
+ )
4260
+ ] });
4261
+ };
3800
4262
  function LoadMoreFooter({
3801
4263
  shown,
3802
4264
  total,
@@ -4514,6 +4976,8 @@ function RecordEditor({
4514
4976
  preview,
4515
4977
  targeting,
4516
4978
  targetingControl,
4979
+ lifecycleControl,
4980
+ lifecycleControlFooter,
4517
4981
  bulkActions,
4518
4982
  footerExtra,
4519
4983
  onBeforeDelete,
@@ -4521,6 +4985,7 @@ function RecordEditor({
4521
4985
  headerSubtitle,
4522
4986
  headerMeta,
4523
4987
  headerLeading,
4988
+ headerNotice,
4524
4989
  clipboard,
4525
4990
  actionLabels,
4526
4991
  actionIcons
@@ -4538,7 +5003,7 @@ function RecordEditor({
4538
5003
  return Boolean(s?.facetId || s?.productId || s?.variantId || s?.batchId);
4539
5004
  })();
4540
5005
  const hasLeftContent = Boolean(headerLabel) || hasBreadcrumb || Boolean(headerLeading);
4541
- const hasRightContent = showInherited || showEmpty || Boolean(headerMeta) || Boolean(bulkActions) || Boolean(targetingControl);
5006
+ const hasRightContent = showInherited || showEmpty || Boolean(headerMeta) || Boolean(bulkActions) || Boolean(targetingControl) || Boolean(lifecycleControl);
4542
5007
  const showHeader = hasLeftContent || hasRightContent;
4543
5008
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full", children: [
4544
5009
  showHeader && /* @__PURE__ */ jsxs(
@@ -4617,12 +5082,14 @@ function RecordEditor({
4617
5082
  }
4618
5083
  ),
4619
5084
  bulkActions && /* @__PURE__ */ jsx(BulkActionsMenu, { ...bulkActions, i18n }),
5085
+ lifecycleControl,
4620
5086
  targetingControl
4621
5087
  ] })
4622
5088
  ]
4623
5089
  }
4624
5090
  ),
4625
5091
  /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-y-auto px-5 py-4", children: [
5092
+ headerNotice,
4626
5093
  targeting,
4627
5094
  children,
4628
5095
  preview
@@ -4658,6 +5125,7 @@ function RecordEditor({
4658
5125
  icon: DeleteIcon
4659
5126
  }
4660
5127
  ),
5128
+ lifecycleControlFooter,
4661
5129
  clipboard && /* @__PURE__ */ jsxs(Fragment, { children: [
4662
5130
  /* @__PURE__ */ jsxs(
4663
5131
  "button",
@@ -5186,21 +5654,21 @@ var ProductDrillDown = ({
5186
5654
  children: tabs.map((t) => {
5187
5655
  const meta = TAB_META[t];
5188
5656
  const Icon = meta.icon;
5189
- const isActive = active === t;
5657
+ const isActive2 = active === t;
5190
5658
  const label = t === "product" ? productLabel : meta.label;
5191
5659
  return /* @__PURE__ */ jsxs(
5192
5660
  "button",
5193
5661
  {
5194
5662
  type: "button",
5195
5663
  role: "tab",
5196
- "aria-selected": isActive,
5664
+ "aria-selected": isActive2,
5197
5665
  onClick: () => onChange(t),
5198
5666
  className: cn(
5199
5667
  "flex items-center gap-1.5 px-3 py-2 text-xs border-b-2 -mb-px transition-colors",
5200
- isActive ? "font-medium" : "opacity-60 hover:opacity-100"
5668
+ isActive2 ? "font-medium" : "opacity-60 hover:opacity-100"
5201
5669
  ),
5202
5670
  style: {
5203
- borderColor: isActive ? "hsl(var(--ra-accent))" : "transparent",
5671
+ borderColor: isActive2 ? "hsl(var(--ra-accent))" : "transparent",
5204
5672
  color: "hsl(var(--ra-text))"
5205
5673
  },
5206
5674
  children: [
@@ -5225,7 +5693,7 @@ var ProductDrillDown = ({
5225
5693
  childLoading && /* @__PURE__ */ jsx("div", { className: "p-3 space-y-2", children: [0, 1, 2].map((i) => /* @__PURE__ */ jsx("div", { className: "h-9 rounded animate-pulse", style: { background: "hsl(var(--ra-muted))" } }, i)) }),
5226
5694
  !childLoading && childList.length === 0 && /* @__PURE__ */ jsx("div", { className: "p-4 text-xs", style: { color: "hsl(var(--ra-muted-text))" }, children: childEmptyLabel }),
5227
5695
  !childLoading && childList.length > 0 && /* @__PURE__ */ jsx("ul", { className: "divide-y", style: { borderColor: "hsl(var(--ra-border))" }, children: childList.map((c) => {
5228
- const isActive = c.id === selectedChildId;
5696
+ const isActive2 = c.id === selectedChildId;
5229
5697
  return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
5230
5698
  "button",
5231
5699
  {
@@ -5233,7 +5701,7 @@ var ProductDrillDown = ({
5233
5701
  onClick: () => onSelectChild(c.id),
5234
5702
  className: cn(
5235
5703
  "w-full text-left px-3 py-2 transition-colors hover:bg-[hsl(var(--ra-muted))]",
5236
- isActive && "ra-row-active"
5704
+ isActive2 && "ra-row-active"
5237
5705
  ),
5238
5706
  children: [
5239
5707
  /* @__PURE__ */ jsx("div", { className: "text-sm truncate", style: { color: "hsl(var(--ra-text))" }, children: c.name }),
@@ -5344,21 +5812,21 @@ var TabbedPreview = ({
5344
5812
  style: { borderColor: "hsl(var(--ra-border))" },
5345
5813
  children: [
5346
5814
  ["editor", "preview"].map((t) => {
5347
- const isActive = tab === t;
5815
+ const isActive2 = tab === t;
5348
5816
  const lbl = t === "editor" ? i18n?.editor ?? "Editor" : i18n?.preview ?? "Preview";
5349
5817
  return /* @__PURE__ */ jsx(
5350
5818
  "button",
5351
5819
  {
5352
5820
  type: "button",
5353
5821
  role: "tab",
5354
- "aria-selected": isActive,
5822
+ "aria-selected": isActive2,
5355
5823
  onClick: () => setTab(t),
5356
5824
  className: cn(
5357
5825
  "px-3 py-2 text-xs border-b-2 -mb-px transition-colors",
5358
- isActive ? "font-medium" : "opacity-60 hover:opacity-100"
5826
+ isActive2 ? "font-medium" : "opacity-60 hover:opacity-100"
5359
5827
  ),
5360
5828
  style: {
5361
- borderColor: isActive ? "hsl(var(--ra-accent))" : "transparent",
5829
+ borderColor: isActive2 ? "hsl(var(--ra-accent))" : "transparent",
5362
5830
  color: "hsl(var(--ra-text))"
5363
5831
  },
5364
5832
  children: lbl
@@ -7096,6 +7564,117 @@ var RuleGroupEditDialog = ({
7096
7564
  document.body
7097
7565
  );
7098
7566
  };
7567
+ var fmt = (s, vars) => s.replace(/\{(\w+)\}/g, (_, k) => String(vars[k] ?? ""));
7568
+ function SingletonConflictBanner({
7569
+ conflictCount,
7570
+ duplicateCount,
7571
+ onResolve,
7572
+ onArchiveDuplicates,
7573
+ onDeleteDuplicates,
7574
+ i18n
7575
+ }) {
7576
+ const [busy, setBusy] = useState(null);
7577
+ const message = conflictCount === 1 ? fmt(i18n.bodyOne, { n: duplicateCount }) : fmt(i18n.bodyMany, { slots: conflictCount, dups: duplicateCount });
7578
+ const showActions = !!(onArchiveDuplicates || onDeleteDuplicates);
7579
+ const runArchive = async () => {
7580
+ if (!onArchiveDuplicates) return;
7581
+ setBusy("archive");
7582
+ try {
7583
+ await onArchiveDuplicates();
7584
+ } finally {
7585
+ setBusy(null);
7586
+ }
7587
+ };
7588
+ const runDelete = async () => {
7589
+ if (!onDeleteDuplicates) return;
7590
+ if (typeof window !== "undefined" && !window.confirm(fmt(i18n.deleteConfirm, { n: duplicateCount }))) {
7591
+ return;
7592
+ }
7593
+ setBusy("delete");
7594
+ try {
7595
+ await onDeleteDuplicates();
7596
+ } finally {
7597
+ setBusy(null);
7598
+ }
7599
+ };
7600
+ return /* @__PURE__ */ jsxs(
7601
+ "div",
7602
+ {
7603
+ role: "alert",
7604
+ className: "px-3 py-2 border-b text-xs flex items-start gap-2 flex-wrap",
7605
+ style: {
7606
+ background: "hsl(var(--ra-danger, 0 70% 45%) / 0.08)",
7607
+ borderColor: "hsl(var(--ra-danger, 0 70% 45%) / 0.35)",
7608
+ color: "hsl(var(--ra-text))"
7609
+ },
7610
+ children: [
7611
+ /* @__PURE__ */ jsx(
7612
+ AlertTriangle,
7613
+ {
7614
+ "aria-hidden": "true",
7615
+ className: "w-3.5 h-3.5 shrink-0 mt-0.5",
7616
+ style: { color: "hsl(var(--ra-danger, 0 70% 45%))" }
7617
+ }
7618
+ ),
7619
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-[10rem]", children: [
7620
+ /* @__PURE__ */ jsx("div", { className: "font-medium leading-tight", children: i18n.title }),
7621
+ /* @__PURE__ */ jsx("div", { className: "leading-tight", style: { color: "hsl(var(--ra-muted-text))" }, children: message })
7622
+ ] }),
7623
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 shrink-0", children: [
7624
+ onArchiveDuplicates && /* @__PURE__ */ jsxs(
7625
+ "button",
7626
+ {
7627
+ type: "button",
7628
+ onClick: runArchive,
7629
+ disabled: busy !== null,
7630
+ className: "ra-btn",
7631
+ "data-variant": "ghost",
7632
+ style: { fontSize: "0.7rem", padding: "0.2rem 0.5rem", display: "inline-flex", alignItems: "center", gap: "0.25rem" },
7633
+ children: [
7634
+ /* @__PURE__ */ jsx(Archive, { className: "w-3 h-3", "aria-hidden": "true" }),
7635
+ busy === "archive" ? "\u2026" : i18n.archiveLabel
7636
+ ]
7637
+ }
7638
+ ),
7639
+ onDeleteDuplicates && /* @__PURE__ */ jsxs(
7640
+ "button",
7641
+ {
7642
+ type: "button",
7643
+ onClick: runDelete,
7644
+ disabled: busy !== null,
7645
+ className: "ra-btn",
7646
+ "data-variant": "ghost",
7647
+ style: {
7648
+ fontSize: "0.7rem",
7649
+ padding: "0.2rem 0.5rem",
7650
+ display: "inline-flex",
7651
+ alignItems: "center",
7652
+ gap: "0.25rem",
7653
+ color: "hsl(var(--ra-danger, 0 70% 45%))",
7654
+ borderColor: "hsl(var(--ra-danger, 0 70% 45%) / 0.4)"
7655
+ },
7656
+ children: [
7657
+ /* @__PURE__ */ jsx(Trash2, { className: "w-3 h-3", "aria-hidden": "true" }),
7658
+ busy === "delete" ? "\u2026" : i18n.deleteLabel
7659
+ ]
7660
+ }
7661
+ ),
7662
+ !showActions && onResolve && /* @__PURE__ */ jsx(
7663
+ "button",
7664
+ {
7665
+ type: "button",
7666
+ onClick: onResolve,
7667
+ className: "ra-btn",
7668
+ "data-variant": "ghost",
7669
+ style: { fontSize: "0.7rem", padding: "0.2rem 0.5rem" },
7670
+ children: i18n.resolveLabel
7671
+ }
7672
+ )
7673
+ ] })
7674
+ ]
7675
+ }
7676
+ );
7677
+ }
7099
7678
  var statusDot = (status) => {
7100
7679
  switch (status) {
7101
7680
  case "saving":
@@ -7540,20 +8119,20 @@ var EditorMountPool = ({
7540
8119
  }
7541
8120
  const visibleIds = keepMountedHidden ? ids : currentEditorId ? [currentEditorId] : [];
7542
8121
  return /* @__PURE__ */ jsx("div", { className, style: { display: "contents" }, children: visibleIds.map((id) => {
7543
- const isActive = id === currentEditorId;
7544
- return /* @__PURE__ */ jsx(EditorPoolSlot, { editorId: id, isActive, children: renderSlot(id) }, id);
8122
+ const isActive2 = id === currentEditorId;
8123
+ return /* @__PURE__ */ jsx(EditorPoolSlot, { editorId: id, isActive: isActive2, children: renderSlot(id) }, id);
7545
8124
  }) });
7546
8125
  };
7547
- var EditorPoolSlot = ({ editorId, isActive, children }) => {
7548
- const inertProps = isActive ? {} : { inert: "" };
8126
+ var EditorPoolSlot = ({ editorId, isActive: isActive2, children }) => {
8127
+ const inertProps = isActive2 ? {} : { inert: "" };
7549
8128
  return /* @__PURE__ */ jsx(
7550
8129
  "div",
7551
8130
  {
7552
8131
  "data-editor-slot": editorId,
7553
- "data-active": isActive ? "true" : "false",
7554
- "aria-hidden": isActive ? void 0 : true,
8132
+ "data-active": isActive2 ? "true" : "false",
8133
+ "aria-hidden": isActive2 ? void 0 : true,
7555
8134
  style: {
7556
- display: isActive ? "contents" : "none"
8135
+ display: isActive2 ? "contents" : "none"
7557
8136
  },
7558
8137
  ...inertProps,
7559
8138
  children
@@ -7827,8 +8406,28 @@ function RecordsAdminShellInner(props) {
7827
8406
  icons: iconsOverride,
7828
8407
  // Deep linking
7829
8408
  deepLink,
7830
- recordChangeRef
8409
+ recordChangeRef,
8410
+ activeStatuses,
8411
+ conflicts: conflictsConfig,
8412
+ lifecycle: lifecycleConfig
7831
8413
  } = props;
8414
+ const lifecycleStatuses = useMemo(
8415
+ () => getLifecycleStatuses(lifecycleConfig),
8416
+ [lifecycleConfig]
8417
+ );
8418
+ const lifecycleSurface = lifecycleConfig?.surface ?? "footer";
8419
+ const lifecycleAutoGroup = lifecycleConfig?.autoGroup ?? true;
8420
+ const lifecycleDefaultStatus = lifecycleConfig?.defaultStatus;
8421
+ const lifecycleBeforeChange = lifecycleConfig?.beforeChange;
8422
+ const resolvedActiveStatuses = useMemo(
8423
+ () => getActiveStatusValues(lifecycleConfig, activeStatuses),
8424
+ [lifecycleConfig, activeStatuses]
8425
+ );
8426
+ const {
8427
+ archiveDuplicates: enableArchiveDuplicates = true,
8428
+ deleteDuplicates: enableDeleteDuplicates = true,
8429
+ archivedStatus: archivedStatusValue = "archived"
8430
+ } = conflictsConfig ?? {};
7832
8431
  const {
7833
8432
  show: showHeader,
7834
8433
  title,
@@ -8047,7 +8646,8 @@ function RecordsAdminShellInner(props) {
8047
8646
  selectedProductId,
8048
8647
  drillTab,
8049
8648
  classify: classify3,
8050
- pageSize: railPageSize
8649
+ pageSize: railPageSize,
8650
+ activeStatuses
8051
8651
  });
8052
8652
  const {
8053
8653
  search,
@@ -8066,12 +8666,14 @@ function RecordsAdminShellInner(props) {
8066
8666
  const ruleScopedList = useRecordList({
8067
8667
  ctx,
8068
8668
  scopeKind: "rule",
8069
- enabled: true
8669
+ enabled: true,
8670
+ activeStatuses
8070
8671
  });
8071
8672
  const globalScopedList = useRecordList({
8072
8673
  ctx,
8073
8674
  scopeKind: "collection",
8074
- enabled: cardinality === "singleton"
8675
+ enabled: cardinality === "singleton",
8676
+ activeStatuses
8075
8677
  });
8076
8678
  const pinnedProduct = useSingleProduct({
8077
8679
  SL,
@@ -8083,6 +8685,20 @@ function RecordsAdminShellInner(props) {
8083
8685
  if (pinnedProduct.item) return [pinnedProduct.item];
8084
8686
  return productBrowse.items;
8085
8687
  }, [pinnedProduct.item, productBrowse.items]);
8688
+ const singletonConflicts = useMemo(
8689
+ () => cardinality === "singleton" ? groupSingletonConflicts(recordList.items, activeStatuses) : [],
8690
+ [cardinality, recordList.items, activeStatuses]
8691
+ );
8692
+ const hasSingletonConflicts = singletonConflicts.length > 0;
8693
+ const totalDuplicateCount = useMemo(
8694
+ () => singletonConflicts.reduce((sum, c) => sum + c.duplicates.length, 0),
8695
+ [singletonConflicts]
8696
+ );
8697
+ const activeRecordIdsBySlot = useMemo(() => {
8698
+ const map = /* @__PURE__ */ new Map();
8699
+ for (const c of singletonConflicts) map.set(c.key, c.active.id);
8700
+ return map;
8701
+ }, [singletonConflicts]);
8086
8702
  useEffect(() => {
8087
8703
  if (activeScope !== "product") return;
8088
8704
  if (selectedProductId) return;
@@ -8102,8 +8718,16 @@ function RecordsAdminShellInner(props) {
8102
8718
  return;
8103
8719
  }
8104
8720
  const first = recordList.items[0];
8105
- if (first?.id) setSelectedRecordId(first.id);
8106
- }, [activeScope, selectedRecordId, recordList.items, cardinality, ruleWizardStep, draftKind, isReconcilingRecordSelection]);
8721
+ if (!first?.id) return;
8722
+ if (cardinality === "singleton") {
8723
+ const conflict = findConflictForRecord(first.id, singletonConflicts);
8724
+ if (conflict?.active.id) {
8725
+ setSelectedRecordId(conflict.active.id);
8726
+ return;
8727
+ }
8728
+ }
8729
+ setSelectedRecordId(first.id);
8730
+ }, [activeScope, selectedRecordId, recordList.items, cardinality, ruleWizardStep, draftKind, isReconcilingRecordSelection, singletonConflicts]);
8107
8731
  const editingScopes = useEditingScope({
8108
8732
  activeScope,
8109
8733
  cardinality,
@@ -8135,6 +8759,26 @@ function RecordsAdminShellInner(props) {
8135
8759
  toSummary: itemToSummary,
8136
8760
  pageSize: itemsPageSize
8137
8761
  });
8762
+ const autoLifecycleGroupBy = useMemo(() => {
8763
+ if (!isCollection) return void 0;
8764
+ if (!lifecycleAutoGroup) return void 0;
8765
+ if (groupBy) return void 0;
8766
+ if (!hasMixedLifecycle(collectionItems.items, resolvedActiveStatuses)) {
8767
+ return void 0;
8768
+ }
8769
+ return (record) => {
8770
+ const def = resolveLifecycleStatus(record, lifecycleConfig);
8771
+ return { key: def.value, label: def.label, tone: def.tone };
8772
+ };
8773
+ }, [
8774
+ isCollection,
8775
+ lifecycleAutoGroup,
8776
+ groupBy,
8777
+ collectionItems.items,
8778
+ resolvedActiveStatuses,
8779
+ lifecycleConfig
8780
+ ]);
8781
+ const lcGroupBy = groupBy ?? autoLifecycleGroupBy;
8138
8782
  useEffect(() => {
8139
8783
  if (skipNextItemResetRef.current) {
8140
8784
  skipNextItemResetRef.current = false;
@@ -8142,32 +8786,32 @@ function RecordsAdminShellInner(props) {
8142
8786
  }
8143
8787
  setSelectedItemId(null);
8144
8788
  }, [editingScope?.raw]);
8145
- const isLifecycleRailEarly = (activeScope === "all" || activeScope === "collection") && isCollection && !!groupBy;
8789
+ const isLifecycleRailEarly = (activeScope === "all" || activeScope === "collection") && isCollection && !!lcGroupBy;
8146
8790
  const lifecycleBucketLabel = useMemo(() => {
8147
- if (!isLifecycleRailEarly || !selectedLifecycleKey || !groupBy) return null;
8791
+ if (!isLifecycleRailEarly || !selectedLifecycleKey || !lcGroupBy) return null;
8148
8792
  for (const it of collectionItems.items) {
8149
- const g = groupBy(it);
8793
+ const g = lcGroupBy(it);
8150
8794
  if (g && g.key === selectedLifecycleKey) return g.label ?? null;
8151
8795
  }
8152
8796
  return null;
8153
- }, [isLifecycleRailEarly, selectedLifecycleKey, groupBy, collectionItems.items]);
8797
+ }, [isLifecycleRailEarly, selectedLifecycleKey, lcGroupBy, collectionItems.items]);
8154
8798
  const scopedCollectionItemsList = useMemo(() => {
8155
- if (!isLifecycleRailEarly || !selectedLifecycleKey || !groupBy) return collectionItems.items;
8799
+ if (!isLifecycleRailEarly || !selectedLifecycleKey || !lcGroupBy) return collectionItems.items;
8156
8800
  return collectionItems.items.filter((it) => {
8157
- const g = groupBy(it);
8801
+ const g = lcGroupBy(it);
8158
8802
  return g?.key === selectedLifecycleKey;
8159
8803
  });
8160
- }, [isLifecycleRailEarly, selectedLifecycleKey, groupBy, collectionItems.items]);
8804
+ }, [isLifecycleRailEarly, selectedLifecycleKey, lcGroupBy, collectionItems.items]);
8161
8805
  useEffect(() => {
8162
- if (!isLifecycleRailEarly || !groupBy) return;
8806
+ if (!isLifecycleRailEarly || !lcGroupBy) return;
8163
8807
  if (selectedLifecycleKey || !selectedItemId) return;
8164
8808
  const row = collectionItems.items.find(
8165
8809
  (it) => it.itemId === selectedItemId || it.id === selectedItemId
8166
8810
  );
8167
8811
  if (!row) return;
8168
- const g = groupBy(row);
8812
+ const g = lcGroupBy(row);
8169
8813
  if (g?.key) setSelectedLifecycleKey(g.key);
8170
- }, [isLifecycleRailEarly, groupBy, selectedItemId, selectedLifecycleKey, collectionItems.items, setSelectedLifecycleKey]);
8814
+ }, [isLifecycleRailEarly, lcGroupBy, selectedItemId, selectedLifecycleKey, collectionItems.items, setSelectedLifecycleKey]);
8171
8815
  const scopedCollectionItems = useMemo(() => ({
8172
8816
  ...collectionItems,
8173
8817
  items: scopedCollectionItemsList
@@ -8302,6 +8946,15 @@ function RecordsAdminShellInner(props) {
8302
8946
  return JSON.parse(JSON.stringify(globalSeedSource));
8303
8947
  }
8304
8948
  }
8949
+ if (draftKind === "paste") {
8950
+ const entry = wizardClipboard.entry;
8951
+ if (!entry) return defaultData?.() ?? {};
8952
+ try {
8953
+ return structuredClone(entry.value);
8954
+ } catch {
8955
+ return JSON.parse(JSON.stringify(entry.value));
8956
+ }
8957
+ }
8305
8958
  return defaultData?.() ?? {};
8306
8959
  }, [
8307
8960
  isCollection,
@@ -8312,7 +8965,8 @@ function RecordsAdminShellInner(props) {
8312
8965
  directGlobalSeedData,
8313
8966
  resolvedGlobalSeed.data,
8314
8967
  onCopyOverride,
8315
- defaultData
8968
+ defaultData,
8969
+ wizardClipboard.entry
8316
8970
  ]);
8317
8971
  const refetchAll = useCallback(async () => {
8318
8972
  await Promise.all([
@@ -8344,6 +8998,7 @@ function RecordsAdminShellInner(props) {
8344
8998
  },
8345
8999
  defaultData,
8346
9000
  deriveDraftLabel,
9001
+ defaultStatus: lifecycleDefaultStatus,
8347
9002
  onSaved: async (isCreate, savedRecordId) => {
8348
9003
  onTelemetry?.({ type: "record.save", recordType, ref: editingTargetScope?.raw ?? "", isCreate });
8349
9004
  const savedFromRuleWizard = ruleWizardStep !== null;
@@ -8521,8 +9176,7 @@ function RecordsAdminShellInner(props) {
8521
9176
  const rowClipboard = shellClipboard.rowClipboard;
8522
9177
  const wrappedRecordActions = recordActions ? (record) => {
8523
9178
  const list = recordActions(record, record.scope);
8524
- if (!list || list.length === 0) return list ?? void 0;
8525
- return list.map((a) => ({
9179
+ const baseList = (list ?? []).map((a) => ({
8526
9180
  ...a,
8527
9181
  onAction: () => {
8528
9182
  onTelemetry?.({
@@ -8534,7 +9188,37 @@ function RecordsAdminShellInner(props) {
8534
9188
  return a.onAction();
8535
9189
  }
8536
9190
  }));
8537
- } : void 0;
9191
+ const lifecycleAction = buildLifecycleAction(record);
9192
+ const out = lifecycleAction ? [...baseList, lifecycleAction] : baseList;
9193
+ return out.length > 0 ? out : void 0;
9194
+ } : (record) => {
9195
+ const a = buildLifecycleAction(record);
9196
+ return a ? [a] : void 0;
9197
+ };
9198
+ function buildLifecycleAction(record) {
9199
+ if (!record.id) return null;
9200
+ const allow = activeStatuses ?? ["active"];
9201
+ const s = record.lifecycleStatus;
9202
+ const active = s == null || s === "" || allow.includes(s);
9203
+ const next = active ? "archived" : "active";
9204
+ const label2 = active ? i18n.actionArchive : i18n.actionRestore;
9205
+ return {
9206
+ key: active ? "lifecycle.archive" : "lifecycle.restore",
9207
+ label: label2,
9208
+ onAction: async () => {
9209
+ try {
9210
+ await SL.app.records.update(collectionId, appId, record.id, { status: next }, true);
9211
+ recordList.refetch();
9212
+ if (cardinality === "singleton") {
9213
+ ruleScopedList.refetch();
9214
+ globalScopedList.refetch();
9215
+ }
9216
+ } catch (err) {
9217
+ console.warn("[RecordsAdminShell] lifecycle update failed", err);
9218
+ }
9219
+ }
9220
+ };
9221
+ }
8538
9222
  const baseScopeRef = editingScope?.raw ?? "";
8539
9223
  const itemNounLabel = itemNoun || "item";
8540
9224
  const {
@@ -8598,6 +9282,70 @@ function RecordsAdminShellInner(props) {
8598
9282
  i18n
8599
9283
  }
8600
9284
  ) : null;
9285
+ const conflictForCurrent = selectedRecordId && selectedRecordId !== DRAFT_ID3 ? findConflictForRecord(selectedRecordId, singletonConflicts) : null;
9286
+ const editorHeaderNotice = conflictForCurrent ? /* @__PURE__ */ jsxs(
9287
+ "div",
9288
+ {
9289
+ role: "alert",
9290
+ className: "mb-3 px-3 py-2 rounded-md text-xs flex items-start gap-2",
9291
+ style: {
9292
+ background: "hsl(var(--ra-danger, 0 70% 45%) / 0.08)",
9293
+ border: "1px solid hsl(var(--ra-danger, 0 70% 45%) / 0.35)",
9294
+ color: "hsl(var(--ra-text))"
9295
+ },
9296
+ children: [
9297
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\u26A0" }),
9298
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
9299
+ /* @__PURE__ */ jsx("div", { className: "font-medium", children: `This record is 1 of ${conflictForCurrent.records.length} sharing the same slot.` }),
9300
+ /* @__PURE__ */ jsx("div", { style: { color: "hsl(var(--ra-muted-text))" }, children: conflictForCurrent.active.id === selectedRecordId ? "It is the active record \u2014 duplicates below will be ignored at runtime. Delete the duplicates to clean this up." : `Active record: "${conflictForCurrent.active.label}". This duplicate is ignored at runtime \u2014 delete it (or the active one) to resolve.` })
9301
+ ] })
9302
+ ]
9303
+ }
9304
+ ) : null;
9305
+ const selectedSummary = selectedRecordId && selectedRecordId !== DRAFT_ID3 ? recordList.items.find((r) => r.id === selectedRecordId) ?? globalScopedList.items.find((r) => r.id === selectedRecordId) ?? ruleScopedList.items.find((r) => r.id === selectedRecordId) : void 0;
9306
+ const editorLifecycleControl = selectedSummary?.id ? /* @__PURE__ */ jsx(
9307
+ LifecycleStatusControl,
9308
+ {
9309
+ SL,
9310
+ collectionId,
9311
+ appId,
9312
+ recordId: selectedSummary.id,
9313
+ current: selectedSummary.lifecycleStatus,
9314
+ i18n,
9315
+ onChanged: () => {
9316
+ void recordList.refetch();
9317
+ }
9318
+ }
9319
+ ) : null;
9320
+ const editorLifecycleFooter = selectedSummary?.id && (lifecycleSurface === "footer" || lifecycleSurface === "both") ? /* @__PURE__ */ jsx(
9321
+ LifecycleStatusMenu,
9322
+ {
9323
+ SL,
9324
+ collectionId,
9325
+ appId,
9326
+ recordId: selectedSummary.id,
9327
+ scope: selectedSummary.scope,
9328
+ current: selectedSummary.lifecycleStatus,
9329
+ statuses: lifecycleStatuses,
9330
+ i18n,
9331
+ beforeChange: lifecycleBeforeChange,
9332
+ onTelemetry: (e) => onTelemetry?.({
9333
+ type: "lifecycle.change",
9334
+ recordType,
9335
+ ref: selectedSummary.id,
9336
+ from: e.from,
9337
+ to: e.to
9338
+ }),
9339
+ onChanged: () => {
9340
+ void recordList.refetch();
9341
+ if (cardinality === "singleton") {
9342
+ ruleScopedList.refetch();
9343
+ globalScopedList.refetch();
9344
+ }
9345
+ }
9346
+ }
9347
+ ) : null;
9348
+ const headerLifecycleControl = lifecycleSurface === "header" || lifecycleSurface === "both" ? editorLifecycleControl : null;
8601
9349
  const baseEditor = (extraFooter, inlinePreviewBody) => /* @__PURE__ */ jsx(
8602
9350
  RecordEditor,
8603
9351
  {
@@ -8635,11 +9383,14 @@ function RecordsAdminShellInner(props) {
8635
9383
  onCustomise: () => setTargetingExpandNonce((n) => n + 1)
8636
9384
  }
8637
9385
  ) : void 0,
9386
+ lifecycleControl: headerLifecycleControl,
9387
+ lifecycleControlFooter: editorLifecycleFooter,
8638
9388
  onBeforeDelete: onBeforeDelete && editingTargetScope ? () => onBeforeDelete(editingTargetScope) : void 0,
8639
9389
  headerLabel: editorHeaderLabel,
8640
9390
  headerSubtitle: editorHeaderSubtitle,
8641
9391
  headerMeta: editorHeaderMeta,
8642
9392
  headerLeading: itemNav,
9393
+ headerNotice: editorHeaderNotice,
8643
9394
  clipboard: editorClipboard,
8644
9395
  actionLabels,
8645
9396
  actionIcons,
@@ -8940,7 +9691,7 @@ function RecordsAdminShellInner(props) {
8940
9691
  void runWithGuard(() => {
8941
9692
  if (activeScope !== "product") setActiveScope("product");
8942
9693
  setSelectedRecordId(DRAFT_ID3);
8943
- setDraftKind(seed === "global" ? "global" : "item");
9694
+ setDraftKind(seed === "global" ? "global" : seed === "paste" ? "paste" : "item");
8944
9695
  });
8945
9696
  }, [runWithGuard, activeScope]);
8946
9697
  const filteredRuleItems = useMemo(
@@ -9035,13 +9786,13 @@ function RecordsAdminShellInner(props) {
9035
9786
  }),
9036
9787
  [i18n.itemsAllLabel, collectionItems.items.length, itemNoun]
9037
9788
  );
9038
- const isLifecycleRail = (isAllTab || isGlobalTab) && isCollection && !!groupBy;
9789
+ const isLifecycleRail = (isAllTab || isGlobalTab) && isCollection && !!lcGroupBy;
9039
9790
  const lifecycleBuckets = useMemo(() => {
9040
- if (!isLifecycleRail || !groupBy) return [];
9791
+ if (!isLifecycleRail || !lcGroupBy) return [];
9041
9792
  const map = /* @__PURE__ */ new Map();
9042
9793
  const order = [];
9043
9794
  for (const item of collectionItems.items) {
9044
- const g = groupBy(item) ?? { key: "__other", label: "Other" };
9795
+ const g = lcGroupBy(item) ?? { key: "__other", label: "Other" };
9045
9796
  let bucket = map.get(g.key);
9046
9797
  if (!bucket) {
9047
9798
  bucket = { key: g.key, label: g.label, icon: g.icon, tone: g.tone, items: [] };
@@ -9050,8 +9801,9 @@ function RecordsAdminShellInner(props) {
9050
9801
  }
9051
9802
  bucket.items.push(item);
9052
9803
  }
9053
- return order.sort().map((k) => map.get(k));
9054
- }, [isLifecycleRail, groupBy, collectionItems.items]);
9804
+ const sortFn = !groupBy ? compareLifecycleBuckets : (a, b) => a.localeCompare(b);
9805
+ return order.sort(sortFn).map((k) => map.get(k));
9806
+ }, [isLifecycleRail, lcGroupBy, groupBy, collectionItems.items]);
9055
9807
  const LIFECYCLE_PREFIX = "lifecycle:";
9056
9808
  const lifecycleRows = useMemo(() => {
9057
9809
  if (!isLifecycleRail) return [];
@@ -9101,15 +9853,47 @@ function RecordsAdminShellInner(props) {
9101
9853
  lifecycleSeededRef.current = true;
9102
9854
  }, [isLifecycleRail, defaultGroupKey, lifecycleBuckets, selectedLifecycleKey, setSelectedLifecycleKey]);
9103
9855
  const filteredCollectionItems = useMemo(() => {
9104
- if (!isLifecycleRail || !selectedLifecycleKey || !groupBy) return collectionItems.items;
9856
+ if (!isLifecycleRail || !selectedLifecycleKey || !lcGroupBy) return collectionItems.items;
9105
9857
  return collectionItems.items.filter((it) => {
9106
- const g = groupBy(it) ?? { key: "__other" };
9858
+ const g = lcGroupBy(it) ?? { key: "__other" };
9107
9859
  return g.key === selectedLifecycleKey;
9108
9860
  });
9109
- }, [isLifecycleRail, selectedLifecycleKey, groupBy, collectionItems.items]);
9861
+ }, [isLifecycleRail, selectedLifecycleKey, lcGroupBy, collectionItems.items]);
9110
9862
  const leftItems = isProductTab ? productListItems : isRuleTab ? applyFacetBrowseFilter(
9111
9863
  isCollection ? collectionRuleRailItems : filteredRuleItems
9112
9864
  ) : isLifecycleRail ? lifecycleRows : (isGlobalTab || isAllTab) && isCollection ? [collectionGlobalAllRow] : isRecordsTab ? applyFacetBrowseFilter(recordList.items) : [];
9865
+ const railShowsHistoryDisclosure = isRecordsTab && !isLifecycleRail && !((isGlobalTab || isAllTab) && isCollection);
9866
+ const filteredLeftItems = useMemo(() => {
9867
+ if (!railShowsHistoryDisclosure) return leftItems;
9868
+ return leftItems.filter((r) => {
9869
+ const s = r.lifecycleStatus;
9870
+ if (s == null || s === "") return true;
9871
+ const allow = activeStatuses ?? ["active"];
9872
+ return allow.includes(s);
9873
+ });
9874
+ }, [leftItems, railShowsHistoryDisclosure, activeStatuses]);
9875
+ const railHistoryBySlot = useMemo(() => {
9876
+ if (!railShowsHistoryDisclosure) return void 0;
9877
+ return recordList.historyBySlot;
9878
+ }, [railShowsHistoryDisclosure, recordList.historyBySlot]);
9879
+ const decoratedLeftItems = useMemo(() => {
9880
+ if (!hasSingletonConflicts) return filteredLeftItems;
9881
+ return filteredLeftItems.map((row) => {
9882
+ if (!row.id) return row;
9883
+ const key = slotKey(row);
9884
+ const activeId = activeRecordIdsBySlot.get(key);
9885
+ if (activeId === void 0) return row;
9886
+ const isActive2 = row.id === activeId;
9887
+ const conflictBadge = [{
9888
+ label: isActive2 ? "Active" : "Duplicate",
9889
+ tone: isActive2 ? "success" : "warning"
9890
+ }];
9891
+ return {
9892
+ ...row,
9893
+ badges: [...conflictBadge, ...row.badges ?? []]
9894
+ };
9895
+ });
9896
+ }, [filteredLeftItems, hasSingletonConflicts, activeRecordIdsBySlot]);
9113
9897
  const leftLoading = isProductTab ? !productPinned && productBrowse.isLoading : isRecordsTab ? recordList.isLoading || probe.isLoading : false;
9114
9898
  const leftError = isProductTab ? productBrowse.error : isRecordsTab ? recordList.error : null;
9115
9899
  const leftSelectedId = isProductTab ? void 0 : isLifecycleRail ? `${LIFECYCLE_PREFIX}${selectedLifecycleKey ?? "__all"}` : selectedRecordId && selectedRecordId !== DRAFT_ID3 ? selectedRecordId : void 0;
@@ -9126,6 +9910,10 @@ function RecordsAdminShellInner(props) {
9126
9910
  return;
9127
9911
  }
9128
9912
  if (isProductTab) {
9913
+ if (item.scope.productId !== selectedProductId) {
9914
+ setSelectedRecordId(null);
9915
+ setDraftKind(null);
9916
+ }
9129
9917
  setSelectedProductId(item.scope.productId);
9130
9918
  setSelectedVariantId(void 0);
9131
9919
  setSelectedBatchId(void 0);
@@ -9452,69 +10240,138 @@ function RecordsAdminShellInner(props) {
9452
10240
  )
9453
10241
  ] })
9454
10242
  ] }),
9455
- /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto", children: isGlobalTab && !isCollection ? null : /* @__PURE__ */ jsxs(Fragment, { children: [
9456
- leftLoading && /* @__PURE__ */ jsx(LoadingState, {}),
9457
- !leftLoading && leftError && /* @__PURE__ */ jsx(ErrorState, { error: leftError }),
9458
- !leftLoading && !leftError && leftItems.length === 0 && (renderEmptyState ? renderEmptyState({ scope: activeScope }) : /* @__PURE__ */ jsx(
9459
- EmptyState,
10243
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-y-auto", children: [
10244
+ hasSingletonConflicts && /* @__PURE__ */ jsx(
10245
+ SingletonConflictBanner,
9460
10246
  {
9461
- icon: search ? icons.empty.search : icons.empty.default,
9462
- title: search ? i18n.noResults : i18n.railEmptyTitle,
9463
- body: search ? void 0 : isRuleTab ? i18n.rulesEmptyBody : i18n.railEmptyBody
10247
+ conflictCount: singletonConflicts.length,
10248
+ duplicateCount: totalDuplicateCount,
10249
+ onResolve: () => {
10250
+ const first = singletonConflicts[0]?.duplicates[0] ?? singletonConflicts[0]?.active;
10251
+ if (first?.id) {
10252
+ void runWithGuard(() => {
10253
+ setSelectedRecordId(first.id);
10254
+ });
10255
+ }
10256
+ },
10257
+ onArchiveDuplicates: enableArchiveDuplicates ? async () => {
10258
+ const ids = singletonConflicts.flatMap((c) => c.duplicates.map((d) => d.id)).filter((id) => !!id);
10259
+ for (const id of ids) {
10260
+ try {
10261
+ await SL.app.records.update(collectionId, appId, id, { status: archivedStatusValue }, true);
10262
+ onTelemetry?.({
10263
+ type: "recordAction.invoke",
10264
+ recordType,
10265
+ key: "conflict.archiveDuplicates",
10266
+ ref: id
10267
+ });
10268
+ } catch (err) {
10269
+ console.warn("[RecordsAdminShell] archive-duplicate failed", id, err);
10270
+ }
10271
+ }
10272
+ recordList.refetch();
10273
+ if (cardinality === "singleton") {
10274
+ ruleScopedList.refetch();
10275
+ globalScopedList.refetch();
10276
+ }
10277
+ } : void 0,
10278
+ onDeleteDuplicates: enableDeleteDuplicates ? async () => {
10279
+ const ids = singletonConflicts.flatMap((c) => c.duplicates.map((d) => d.id)).filter((id) => !!id);
10280
+ for (const id of ids) {
10281
+ try {
10282
+ await SL.app.records.remove(collectionId, appId, id, true);
10283
+ onTelemetry?.({
10284
+ type: "recordAction.invoke",
10285
+ recordType,
10286
+ key: "conflict.deleteDuplicates",
10287
+ ref: id
10288
+ });
10289
+ } catch (err) {
10290
+ console.warn("[RecordsAdminShell] delete-duplicate failed", id, err);
10291
+ }
10292
+ }
10293
+ recordList.refetch();
10294
+ if (cardinality === "singleton") {
10295
+ ruleScopedList.refetch();
10296
+ globalScopedList.refetch();
10297
+ }
10298
+ } : void 0,
10299
+ i18n: {
10300
+ title: i18n.conflictBannerTitle,
10301
+ bodyOne: i18n.conflictBannerBodyOne,
10302
+ bodyMany: i18n.conflictBannerBodyMany,
10303
+ archiveLabel: i18n.conflictArchiveDuplicates,
10304
+ deleteLabel: i18n.conflictDeleteDuplicates,
10305
+ deleteConfirm: i18n.conflictDeleteConfirm,
10306
+ resolveLabel: i18n.conflictResolveLabel
10307
+ }
9464
10308
  }
9465
- )),
9466
- !leftLoading && !leftError && leftItems.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
9467
- /* @__PURE__ */ jsx(
9468
- RecordList,
10309
+ ),
10310
+ isGlobalTab && !isCollection && !hasSingletonConflicts ? null : /* @__PURE__ */ jsxs(Fragment, { children: [
10311
+ leftLoading && /* @__PURE__ */ jsx(LoadingState, {}),
10312
+ !leftLoading && leftError && /* @__PURE__ */ jsx(ErrorState, { error: leftError }),
10313
+ !leftLoading && !leftError && decoratedLeftItems.length === 0 && (renderEmptyState ? renderEmptyState({ scope: activeScope }) : /* @__PURE__ */ jsx(
10314
+ EmptyState,
9469
10315
  {
9470
- items: leftItems,
9471
- selectedId: leftSelectedId,
9472
- selectedAnchorKey: leftSelectedAnchorKey,
9473
- onSelect: onLeftSelect,
9474
- dirtyId,
9475
- dirtyAnchorKey,
9476
- dirtyKeys,
9477
- errorKeys,
9478
- presentation: effectivePresentation,
9479
- renderListRow,
9480
- groupBy: (
9481
- // The synthetic "All items" row in collection mode is a
9482
- // navigational anchor, not a real record — applying the
9483
- // host's groupBy bucketed it under "Other" (its
9484
- // data is null). Skip grouping for that single-row rail.
9485
- (isGlobalTab || isAllTab) && isCollection || isLifecycleRail ? void 0 : effectiveGroupBy
9486
- ),
9487
- renderGroupActions: renderRuleGroupActions,
9488
- rowClipboard,
9489
- rowActions: wrappedRecordActions,
9490
- i18n
10316
+ icon: search ? icons.empty.search : icons.empty.default,
10317
+ title: search ? i18n.noResults : i18n.railEmptyTitle,
10318
+ body: search ? void 0 : isRuleTab ? i18n.rulesEmptyBody : i18n.railEmptyBody
9491
10319
  }
9492
- ),
9493
- isProductTab && !productPinned && /* @__PURE__ */ jsx(
9494
- LoadMoreFooter,
9495
- {
9496
- shown: leftItems.length,
9497
- hasNextPage: !!productBrowse.hasNextPage,
9498
- isFetchingNextPage: !!productBrowse.isFetchingNextPage,
9499
- onLoadMore: () => {
9500
- void productBrowse.fetchNextPage();
10320
+ )),
10321
+ !leftLoading && !leftError && decoratedLeftItems.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
10322
+ /* @__PURE__ */ jsx(
10323
+ RecordList,
10324
+ {
10325
+ items: decoratedLeftItems,
10326
+ selectedId: leftSelectedId,
10327
+ selectedAnchorKey: leftSelectedAnchorKey,
10328
+ onSelect: onLeftSelect,
10329
+ dirtyId,
10330
+ dirtyAnchorKey,
10331
+ dirtyKeys,
10332
+ errorKeys,
10333
+ presentation: effectivePresentation,
10334
+ renderListRow,
10335
+ groupBy: (
10336
+ // The synthetic "All items" row in collection mode is a
10337
+ // navigational anchor, not a real record — applying the
10338
+ // host's groupBy bucketed it under "Other" (its
10339
+ // data is null). Skip grouping for that single-row rail.
10340
+ (isGlobalTab || isAllTab) && isCollection || isLifecycleRail ? void 0 : effectiveGroupBy
10341
+ ),
10342
+ renderGroupActions: renderRuleGroupActions,
10343
+ rowClipboard,
10344
+ rowActions: wrappedRecordActions,
10345
+ i18n,
10346
+ historyBySlot: railHistoryBySlot
9501
10347
  }
9502
- }
9503
- ),
9504
- isRecordsTab && (!isCollection || !(isAllTab || isGlobalTab)) && /* @__PURE__ */ jsx(
9505
- LoadMoreFooter,
9506
- {
9507
- shown: recordList.items.length,
9508
- total: recordList.total,
9509
- hasNextPage: !!recordList.hasNextPage,
9510
- isFetchingNextPage: !!recordList.isFetchingNextPage,
9511
- onLoadMore: () => {
9512
- void recordList.fetchNextPage();
10348
+ ),
10349
+ isProductTab && !productPinned && /* @__PURE__ */ jsx(
10350
+ LoadMoreFooter,
10351
+ {
10352
+ shown: leftItems.length,
10353
+ hasNextPage: !!productBrowse.hasNextPage,
10354
+ isFetchingNextPage: !!productBrowse.isFetchingNextPage,
10355
+ onLoadMore: () => {
10356
+ void productBrowse.fetchNextPage();
10357
+ }
9513
10358
  }
9514
- }
9515
- )
10359
+ ),
10360
+ isRecordsTab && (!isCollection || !(isAllTab || isGlobalTab)) && /* @__PURE__ */ jsx(
10361
+ LoadMoreFooter,
10362
+ {
10363
+ shown: recordList.items.length,
10364
+ total: recordList.total,
10365
+ hasNextPage: !!recordList.hasNextPage,
10366
+ isFetchingNextPage: !!recordList.isFetchingNextPage,
10367
+ onLoadMore: () => {
10368
+ void recordList.fetchNextPage();
10369
+ }
10370
+ }
10371
+ )
10372
+ ] })
9516
10373
  ] })
9517
- ] }) })
10374
+ ] })
9518
10375
  ] }) }),
9519
10376
  /* @__PURE__ */ jsxs("main", { className: "overflow-hidden", children: [
9520
10377
  ruleWizardStep !== null && /* @__PURE__ */ jsxs(
@@ -9660,17 +10517,24 @@ function RecordsAdminShellInner(props) {
9660
10517
  )
9661
10518
  }
9662
10519
  ),
9663
- ruleWizardStep === null && isProductTab && selectedProductId && !isCollection && editingTargetScope && resolved.source !== "self" && selectedRecordId !== DRAFT_ID3 ? /* @__PURE__ */ jsx(
9664
- CreateRecordChooser,
9665
- {
9666
- title: "No record set for this product",
9667
- body: `Choose whether to create a fresh ${itemNoun} for this product or start from the global default.`,
9668
- primaryLabel: "Start blank",
9669
- onPrimary: () => onCreateProductRecord("blank"),
9670
- secondaryLabel: singletonGlobalSeedAvailable ? "Copy from global" : void 0,
9671
- onSecondary: singletonGlobalSeedAvailable ? () => onCreateProductRecord("global") : void 0
9672
- }
9673
- ) : null,
10520
+ ruleWizardStep === null && isProductTab && selectedProductId && !isCollection && editingTargetScope && resolved.source !== "self" && selectedRecordId !== DRAFT_ID3 ? (() => {
10521
+ const productName = productLookupItems.find((p) => p.id === selectedProductId)?.name ?? selectedProductId;
10522
+ const pasteEntry = wizardClipboard.entry;
10523
+ const pasteSourceLabel = pasteEntry?.sourceLabel ?? pasteEntry?.sourceScope.raw;
10524
+ return /* @__PURE__ */ jsx(
10525
+ CreateRecordChooser,
10526
+ {
10527
+ title: `No ${itemNoun} set for product: ${productName}`,
10528
+ body: `Choose whether to create a fresh ${itemNoun} for this product or start from the global default.`,
10529
+ primaryLabel: "Start blank",
10530
+ onPrimary: () => onCreateProductRecord("blank"),
10531
+ secondaryLabel: singletonGlobalSeedAvailable ? "Copy from global" : void 0,
10532
+ onSecondary: singletonGlobalSeedAvailable ? () => onCreateProductRecord("global") : void 0,
10533
+ tertiaryLabel: pasteEntry ? pasteSourceLabel ? `Paste from ${pasteSourceLabel}` : "Paste from clipboard" : void 0,
10534
+ onTertiary: pasteEntry ? () => onCreateProductRecord("paste") : void 0
10535
+ }
10536
+ );
10537
+ })() : null,
9674
10538
  ruleWizardStep === null && !isProductTab && editingTargetScope && (!isCollection || selectedItemId) && renderEditorWithPreview()
9675
10539
  ] })
9676
10540
  ]
@@ -10240,11 +11104,40 @@ function useRecordEditor(args) {
10240
11104
  if (!resolved.recordId) return;
10241
11105
  await removeRecord(ctx, resolved.recordId);
10242
11106
  draftStore.clearDraft(draftKey);
11107
+ removeRecordFromCaches(queryClient, ctx, resolved.recordId);
11108
+ const cacheKey = resolvedRecordQueryKey({
11109
+ collectionId: ctx.collectionId,
11110
+ appId: ctx.appId,
11111
+ recordType: ctx.recordType,
11112
+ productId: scope.productId,
11113
+ variantId: scope.variantId,
11114
+ batchId: scope.batchId,
11115
+ facetId: scope.facetId,
11116
+ facetValue: scope.facetValue,
11117
+ proofId: scope.proofId,
11118
+ recordId: resolved.recordId,
11119
+ withParent: true
11120
+ });
11121
+ queryClient.removeQueries({ queryKey: cacheKey });
11122
+ const anchorCacheKey = resolvedRecordQueryKey({
11123
+ collectionId: ctx.collectionId,
11124
+ appId: ctx.appId,
11125
+ recordType: ctx.recordType,
11126
+ productId: scope.productId,
11127
+ variantId: scope.variantId,
11128
+ batchId: scope.batchId,
11129
+ facetId: scope.facetId,
11130
+ facetValue: scope.facetValue,
11131
+ proofId: scope.proofId,
11132
+ recordId: void 0,
11133
+ withParent: true
11134
+ });
11135
+ queryClient.removeQueries({ queryKey: anchorCacheKey });
10243
11136
  queryClient.invalidateQueries({
10244
11137
  queryKey: scopeCountsQueryKey(ctx.collectionId, ctx.appId, ctx.recordType)
10245
11138
  });
10246
11139
  onDeleted?.();
10247
- }, [resolved.source, resolved.recordId, draftKey]);
11140
+ }, [resolved.source, resolved.recordId, draftKey, scope.raw]);
10248
11141
  const prevDraftKeyRef = useRef(draftKey);
10249
11142
  const prevScopeRawRef = useRef(scope.raw);
10250
11143
  useEffect(() => {
@@ -10721,6 +11614,6 @@ function useMergedRecord(args) {
10721
11614
  // src/components/RecordsAdmin/index.ts
10722
11615
  assertComponentStylesLoaded("records-admin");
10723
11616
 
10724
- export { ALL_ITEM_VIEWS, ALL_PRESENTATIONS, BatchList, BulkActionsMenu, DEFAULT_DEEP_LINK_PARAM_NAMES, DEFAULT_I18N, DEFAULT_ICONS, DefaultItemCards, DefaultItemTable, DefaultRecordCard, DefaultRecordRow, DeleteButton, DirtyDraftProvider, DrawerPreview, EditorItemNav, EmptyState, ErrorState, FacetList, InheritanceMarker, InheritanceProvider, InlinePreview, IntroCard, ItemListView, ItemViewSwitcher, LoadingState, PresentationSwitcher, PreviewReopenPill, PreviewScopePicker, PreviewToggleButton, ProductDrillDown, ProductList, RecordBrowser, RecordEditor, RecordList, RecordsAdminShell, ResolvedPreview, ScopeBreadcrumb, ScopeTabs, SiblingRail, SidePreview, StatusDot, StatusFilterPills, StatusIcon, TabbedPreview, UtilityRow, VariantList, buildDraftKey, buildRef, checkPasteCompatibility, cloneValue, createDefaultDeepLinkAdapter, createPostMessageDeepLinkAdapter, createRouterDeepLinkAdapter, downloadBlob, exportCsv, importCsv, isInSmartLinksIframe, mergeIcons, normaliseRule, parseRef, pickHeaderIcon, resolutionChain, resolveRecord, ruleHash, rulesEqual, scopeCountsQueryKey, statusToneLabel, summariseRule, useCollectedRecords, useCollectionItems, useDeepLinkState, useDirtyDraft, useDirtyDraftActions, useDirtyDraftStore, useDirtyDrafts, useDirtyNavigation, useFacetBrowse, useIntroDismissed, useItemViewPref, useMergedRecord, usePresentationPref, useProductBrowse, useProductChildren, useRecordClipboard, useRecordEditor, useRecordList, useResolveAllRecords, useResolvedRecord, useRulePreview, useScopeCounts, useScopeProbe, useUnsavedGuard };
11617
+ export { ALL_ITEM_VIEWS, ALL_PRESENTATIONS, BatchList, BulkActionsMenu, DEFAULT_DEEP_LINK_PARAM_NAMES, DEFAULT_I18N, DEFAULT_ICONS, DEFAULT_LIFECYCLE_STATUSES, DefaultItemCards, DefaultItemTable, DefaultRecordCard, DefaultRecordRow, DeleteButton, DirtyDraftProvider, DrawerPreview, EditorItemNav, EmptyState, ErrorState, FacetList, InheritanceMarker, InheritanceProvider, InlinePreview, IntroCard, ItemListView, ItemViewSwitcher, LifecycleStatusMenu, LoadingState, PresentationSwitcher, PreviewReopenPill, PreviewScopePicker, PreviewToggleButton, ProductDrillDown, ProductList, RecordBrowser, RecordEditor, RecordList, RecordsAdminShell, ResolvedPreview, ScopeBreadcrumb, ScopeTabs, SiblingRail, SidePreview, StatusDot, StatusFilterPills, StatusIcon, TabbedPreview, UtilityRow, VariantList, buildDraftKey, buildRef, checkPasteCompatibility, cloneValue, compareLifecycleBuckets, createDefaultDeepLinkAdapter, createPostMessageDeepLinkAdapter, createRouterDeepLinkAdapter, downloadBlob, exportCsv, getActiveStatusValues, getLifecycleStatuses, hasMixedLifecycle, importCsv, isInSmartLinksIframe, mergeIcons, normaliseRule, parseRef, pickHeaderIcon, resolutionChain, resolveLifecycleStatus, resolveRecord, ruleHash, rulesEqual, scopeCountsQueryKey, statusToneLabel, summariseRule, useCollectedRecords, useCollectionItems, useDeepLinkState, useDirtyDraft, useDirtyDraftActions, useDirtyDraftStore, useDirtyDrafts, useDirtyNavigation, useFacetBrowse, useIntroDismissed, useItemViewPref, useMergedRecord, usePresentationPref, useProductBrowse, useProductChildren, useRecordClipboard, useRecordEditor, useRecordList, useResolveAllRecords, useResolvedRecord, useRulePreview, useScopeCounts, useScopeProbe, useUnsavedGuard };
10725
11618
  //# sourceMappingURL=index.js.map
10726
11619
  //# sourceMappingURL=index.js.map