@proveanything/smartlinks-utils-ui 0.11.11 → 0.12.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,32 @@ 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
+ () => {
8690
+ if (cardinality !== "singleton") return [];
8691
+ if (recordList.isLoading) return [];
8692
+ if (recordList.isFetchingNextPage || recordList.hasNextPage) return [];
8693
+ return groupSingletonConflicts(recordList.items, activeStatuses);
8694
+ },
8695
+ [
8696
+ cardinality,
8697
+ recordList.items,
8698
+ recordList.isLoading,
8699
+ recordList.isFetchingNextPage,
8700
+ recordList.hasNextPage,
8701
+ activeStatuses
8702
+ ]
8703
+ );
8704
+ const hasSingletonConflicts = singletonConflicts.length > 0;
8705
+ const totalDuplicateCount = useMemo(
8706
+ () => singletonConflicts.reduce((sum, c) => sum + c.duplicates.length, 0),
8707
+ [singletonConflicts]
8708
+ );
8709
+ const activeRecordIdsBySlot = useMemo(() => {
8710
+ const map = /* @__PURE__ */ new Map();
8711
+ for (const c of singletonConflicts) map.set(c.key, c.active.id);
8712
+ return map;
8713
+ }, [singletonConflicts]);
8086
8714
  useEffect(() => {
8087
8715
  if (activeScope !== "product") return;
8088
8716
  if (selectedProductId) return;
@@ -8102,8 +8730,16 @@ function RecordsAdminShellInner(props) {
8102
8730
  return;
8103
8731
  }
8104
8732
  const first = recordList.items[0];
8105
- if (first?.id) setSelectedRecordId(first.id);
8106
- }, [activeScope, selectedRecordId, recordList.items, cardinality, ruleWizardStep, draftKind, isReconcilingRecordSelection]);
8733
+ if (!first?.id) return;
8734
+ if (cardinality === "singleton") {
8735
+ const conflict = findConflictForRecord(first.id, singletonConflicts);
8736
+ if (conflict?.active.id) {
8737
+ setSelectedRecordId(conflict.active.id);
8738
+ return;
8739
+ }
8740
+ }
8741
+ setSelectedRecordId(first.id);
8742
+ }, [activeScope, selectedRecordId, recordList.items, cardinality, ruleWizardStep, draftKind, isReconcilingRecordSelection, singletonConflicts]);
8107
8743
  const editingScopes = useEditingScope({
8108
8744
  activeScope,
8109
8745
  cardinality,
@@ -8135,6 +8771,26 @@ function RecordsAdminShellInner(props) {
8135
8771
  toSummary: itemToSummary,
8136
8772
  pageSize: itemsPageSize
8137
8773
  });
8774
+ const autoLifecycleGroupBy = useMemo(() => {
8775
+ if (!isCollection) return void 0;
8776
+ if (!lifecycleAutoGroup) return void 0;
8777
+ if (groupBy) return void 0;
8778
+ if (!hasMixedLifecycle(collectionItems.items, resolvedActiveStatuses)) {
8779
+ return void 0;
8780
+ }
8781
+ return (record) => {
8782
+ const def = resolveLifecycleStatus(record, lifecycleConfig);
8783
+ return { key: def.value, label: def.label, tone: def.tone };
8784
+ };
8785
+ }, [
8786
+ isCollection,
8787
+ lifecycleAutoGroup,
8788
+ groupBy,
8789
+ collectionItems.items,
8790
+ resolvedActiveStatuses,
8791
+ lifecycleConfig
8792
+ ]);
8793
+ const lcGroupBy = groupBy ?? autoLifecycleGroupBy;
8138
8794
  useEffect(() => {
8139
8795
  if (skipNextItemResetRef.current) {
8140
8796
  skipNextItemResetRef.current = false;
@@ -8142,32 +8798,32 @@ function RecordsAdminShellInner(props) {
8142
8798
  }
8143
8799
  setSelectedItemId(null);
8144
8800
  }, [editingScope?.raw]);
8145
- const isLifecycleRailEarly = (activeScope === "all" || activeScope === "collection") && isCollection && !!groupBy;
8801
+ const isLifecycleRailEarly = (activeScope === "all" || activeScope === "collection") && isCollection && !!lcGroupBy;
8146
8802
  const lifecycleBucketLabel = useMemo(() => {
8147
- if (!isLifecycleRailEarly || !selectedLifecycleKey || !groupBy) return null;
8803
+ if (!isLifecycleRailEarly || !selectedLifecycleKey || !lcGroupBy) return null;
8148
8804
  for (const it of collectionItems.items) {
8149
- const g = groupBy(it);
8805
+ const g = lcGroupBy(it);
8150
8806
  if (g && g.key === selectedLifecycleKey) return g.label ?? null;
8151
8807
  }
8152
8808
  return null;
8153
- }, [isLifecycleRailEarly, selectedLifecycleKey, groupBy, collectionItems.items]);
8809
+ }, [isLifecycleRailEarly, selectedLifecycleKey, lcGroupBy, collectionItems.items]);
8154
8810
  const scopedCollectionItemsList = useMemo(() => {
8155
- if (!isLifecycleRailEarly || !selectedLifecycleKey || !groupBy) return collectionItems.items;
8811
+ if (!isLifecycleRailEarly || !selectedLifecycleKey || !lcGroupBy) return collectionItems.items;
8156
8812
  return collectionItems.items.filter((it) => {
8157
- const g = groupBy(it);
8813
+ const g = lcGroupBy(it);
8158
8814
  return g?.key === selectedLifecycleKey;
8159
8815
  });
8160
- }, [isLifecycleRailEarly, selectedLifecycleKey, groupBy, collectionItems.items]);
8816
+ }, [isLifecycleRailEarly, selectedLifecycleKey, lcGroupBy, collectionItems.items]);
8161
8817
  useEffect(() => {
8162
- if (!isLifecycleRailEarly || !groupBy) return;
8818
+ if (!isLifecycleRailEarly || !lcGroupBy) return;
8163
8819
  if (selectedLifecycleKey || !selectedItemId) return;
8164
8820
  const row = collectionItems.items.find(
8165
8821
  (it) => it.itemId === selectedItemId || it.id === selectedItemId
8166
8822
  );
8167
8823
  if (!row) return;
8168
- const g = groupBy(row);
8824
+ const g = lcGroupBy(row);
8169
8825
  if (g?.key) setSelectedLifecycleKey(g.key);
8170
- }, [isLifecycleRailEarly, groupBy, selectedItemId, selectedLifecycleKey, collectionItems.items, setSelectedLifecycleKey]);
8826
+ }, [isLifecycleRailEarly, lcGroupBy, selectedItemId, selectedLifecycleKey, collectionItems.items, setSelectedLifecycleKey]);
8171
8827
  const scopedCollectionItems = useMemo(() => ({
8172
8828
  ...collectionItems,
8173
8829
  items: scopedCollectionItemsList
@@ -8302,6 +8958,15 @@ function RecordsAdminShellInner(props) {
8302
8958
  return JSON.parse(JSON.stringify(globalSeedSource));
8303
8959
  }
8304
8960
  }
8961
+ if (draftKind === "paste") {
8962
+ const entry = wizardClipboard.entry;
8963
+ if (!entry) return defaultData?.() ?? {};
8964
+ try {
8965
+ return structuredClone(entry.value);
8966
+ } catch {
8967
+ return JSON.parse(JSON.stringify(entry.value));
8968
+ }
8969
+ }
8305
8970
  return defaultData?.() ?? {};
8306
8971
  }, [
8307
8972
  isCollection,
@@ -8312,7 +8977,8 @@ function RecordsAdminShellInner(props) {
8312
8977
  directGlobalSeedData,
8313
8978
  resolvedGlobalSeed.data,
8314
8979
  onCopyOverride,
8315
- defaultData
8980
+ defaultData,
8981
+ wizardClipboard.entry
8316
8982
  ]);
8317
8983
  const refetchAll = useCallback(async () => {
8318
8984
  await Promise.all([
@@ -8344,6 +9010,7 @@ function RecordsAdminShellInner(props) {
8344
9010
  },
8345
9011
  defaultData,
8346
9012
  deriveDraftLabel,
9013
+ defaultStatus: lifecycleDefaultStatus,
8347
9014
  onSaved: async (isCreate, savedRecordId) => {
8348
9015
  onTelemetry?.({ type: "record.save", recordType, ref: editingTargetScope?.raw ?? "", isCreate });
8349
9016
  const savedFromRuleWizard = ruleWizardStep !== null;
@@ -8521,8 +9188,7 @@ function RecordsAdminShellInner(props) {
8521
9188
  const rowClipboard = shellClipboard.rowClipboard;
8522
9189
  const wrappedRecordActions = recordActions ? (record) => {
8523
9190
  const list = recordActions(record, record.scope);
8524
- if (!list || list.length === 0) return list ?? void 0;
8525
- return list.map((a) => ({
9191
+ const baseList = (list ?? []).map((a) => ({
8526
9192
  ...a,
8527
9193
  onAction: () => {
8528
9194
  onTelemetry?.({
@@ -8534,7 +9200,37 @@ function RecordsAdminShellInner(props) {
8534
9200
  return a.onAction();
8535
9201
  }
8536
9202
  }));
8537
- } : void 0;
9203
+ const lifecycleAction = buildLifecycleAction(record);
9204
+ const out = lifecycleAction ? [...baseList, lifecycleAction] : baseList;
9205
+ return out.length > 0 ? out : void 0;
9206
+ } : (record) => {
9207
+ const a = buildLifecycleAction(record);
9208
+ return a ? [a] : void 0;
9209
+ };
9210
+ function buildLifecycleAction(record) {
9211
+ if (!record.id) return null;
9212
+ const allow = activeStatuses ?? ["active"];
9213
+ const s = record.lifecycleStatus;
9214
+ const active = s == null || s === "" || allow.includes(s);
9215
+ const next = active ? "archived" : "active";
9216
+ const label2 = active ? i18n.actionArchive : i18n.actionRestore;
9217
+ return {
9218
+ key: active ? "lifecycle.archive" : "lifecycle.restore",
9219
+ label: label2,
9220
+ onAction: async () => {
9221
+ try {
9222
+ await SL.app.records.update(collectionId, appId, record.id, { status: next }, true);
9223
+ recordList.refetch();
9224
+ if (cardinality === "singleton") {
9225
+ ruleScopedList.refetch();
9226
+ globalScopedList.refetch();
9227
+ }
9228
+ } catch (err) {
9229
+ console.warn("[RecordsAdminShell] lifecycle update failed", err);
9230
+ }
9231
+ }
9232
+ };
9233
+ }
8538
9234
  const baseScopeRef = editingScope?.raw ?? "";
8539
9235
  const itemNounLabel = itemNoun || "item";
8540
9236
  const {
@@ -8598,6 +9294,70 @@ function RecordsAdminShellInner(props) {
8598
9294
  i18n
8599
9295
  }
8600
9296
  ) : null;
9297
+ const conflictForCurrent = selectedRecordId && selectedRecordId !== DRAFT_ID3 ? findConflictForRecord(selectedRecordId, singletonConflicts) : null;
9298
+ const editorHeaderNotice = conflictForCurrent ? /* @__PURE__ */ jsxs(
9299
+ "div",
9300
+ {
9301
+ role: "alert",
9302
+ className: "mb-3 px-3 py-2 rounded-md text-xs flex items-start gap-2",
9303
+ style: {
9304
+ background: "hsl(var(--ra-danger, 0 70% 45%) / 0.08)",
9305
+ border: "1px solid hsl(var(--ra-danger, 0 70% 45%) / 0.35)",
9306
+ color: "hsl(var(--ra-text))"
9307
+ },
9308
+ children: [
9309
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\u26A0" }),
9310
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
9311
+ /* @__PURE__ */ jsx("div", { className: "font-medium", children: `This record is 1 of ${conflictForCurrent.records.length} sharing the same slot.` }),
9312
+ /* @__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.` })
9313
+ ] })
9314
+ ]
9315
+ }
9316
+ ) : null;
9317
+ 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;
9318
+ const editorLifecycleControl = selectedSummary?.id ? /* @__PURE__ */ jsx(
9319
+ LifecycleStatusControl,
9320
+ {
9321
+ SL,
9322
+ collectionId,
9323
+ appId,
9324
+ recordId: selectedSummary.id,
9325
+ current: selectedSummary.lifecycleStatus,
9326
+ i18n,
9327
+ onChanged: () => {
9328
+ void recordList.refetch();
9329
+ }
9330
+ }
9331
+ ) : null;
9332
+ const editorLifecycleFooter = selectedSummary?.id && (lifecycleSurface === "footer" || lifecycleSurface === "both") ? /* @__PURE__ */ jsx(
9333
+ LifecycleStatusMenu,
9334
+ {
9335
+ SL,
9336
+ collectionId,
9337
+ appId,
9338
+ recordId: selectedSummary.id,
9339
+ scope: selectedSummary.scope,
9340
+ current: selectedSummary.lifecycleStatus,
9341
+ statuses: lifecycleStatuses,
9342
+ i18n,
9343
+ beforeChange: lifecycleBeforeChange,
9344
+ onTelemetry: (e) => onTelemetry?.({
9345
+ type: "lifecycle.change",
9346
+ recordType,
9347
+ ref: selectedSummary.id,
9348
+ from: e.from,
9349
+ to: e.to
9350
+ }),
9351
+ onChanged: () => {
9352
+ void refetchAll();
9353
+ if (cardinality === "singleton") {
9354
+ ruleScopedList.refetch();
9355
+ globalScopedList.refetch();
9356
+ }
9357
+ }
9358
+ }
9359
+ ) : null;
9360
+ const headerLifecycleControl = lifecycleSurface === "header" || lifecycleSurface === "both" ? editorLifecycleControl : null;
8601
9361
  const baseEditor = (extraFooter, inlinePreviewBody) => /* @__PURE__ */ jsx(
8602
9362
  RecordEditor,
8603
9363
  {
@@ -8635,11 +9395,14 @@ function RecordsAdminShellInner(props) {
8635
9395
  onCustomise: () => setTargetingExpandNonce((n) => n + 1)
8636
9396
  }
8637
9397
  ) : void 0,
9398
+ lifecycleControl: headerLifecycleControl,
9399
+ lifecycleControlFooter: editorLifecycleFooter,
8638
9400
  onBeforeDelete: onBeforeDelete && editingTargetScope ? () => onBeforeDelete(editingTargetScope) : void 0,
8639
9401
  headerLabel: editorHeaderLabel,
8640
9402
  headerSubtitle: editorHeaderSubtitle,
8641
9403
  headerMeta: editorHeaderMeta,
8642
9404
  headerLeading: itemNav,
9405
+ headerNotice: editorHeaderNotice,
8643
9406
  clipboard: editorClipboard,
8644
9407
  actionLabels,
8645
9408
  actionIcons,
@@ -8905,8 +9668,12 @@ function RecordsAdminShellInner(props) {
8905
9668
  setRuleWizardStep(2);
8906
9669
  } else {
8907
9670
  setRuleWizardStep(2);
9671
+ if (ruleWizardSeedMode) {
9672
+ setRuleWizardDraftKey(mintRuleWizardDraftKey());
9673
+ setSelectedRecordId(DRAFT_ID3);
9674
+ }
8908
9675
  }
8909
- }, [cardinality]);
9676
+ }, [cardinality, ruleWizardSeedMode, mintRuleWizardDraftKey]);
8910
9677
  const startRuleWizardDraft = useCallback((seed) => {
8911
9678
  setRuleWizardSeedMode(seed);
8912
9679
  setRuleWizardDraftKey(mintRuleWizardDraftKey());
@@ -8940,7 +9707,7 @@ function RecordsAdminShellInner(props) {
8940
9707
  void runWithGuard(() => {
8941
9708
  if (activeScope !== "product") setActiveScope("product");
8942
9709
  setSelectedRecordId(DRAFT_ID3);
8943
- setDraftKind(seed === "global" ? "global" : "item");
9710
+ setDraftKind(seed === "global" ? "global" : seed === "paste" ? "paste" : "item");
8944
9711
  });
8945
9712
  }, [runWithGuard, activeScope]);
8946
9713
  const filteredRuleItems = useMemo(
@@ -9035,13 +9802,13 @@ function RecordsAdminShellInner(props) {
9035
9802
  }),
9036
9803
  [i18n.itemsAllLabel, collectionItems.items.length, itemNoun]
9037
9804
  );
9038
- const isLifecycleRail = (isAllTab || isGlobalTab) && isCollection && !!groupBy;
9805
+ const isLifecycleRail = (isAllTab || isGlobalTab) && isCollection && !!lcGroupBy;
9039
9806
  const lifecycleBuckets = useMemo(() => {
9040
- if (!isLifecycleRail || !groupBy) return [];
9807
+ if (!isLifecycleRail || !lcGroupBy) return [];
9041
9808
  const map = /* @__PURE__ */ new Map();
9042
9809
  const order = [];
9043
9810
  for (const item of collectionItems.items) {
9044
- const g = groupBy(item) ?? { key: "__other", label: "Other" };
9811
+ const g = lcGroupBy(item) ?? { key: "__other", label: "Other" };
9045
9812
  let bucket = map.get(g.key);
9046
9813
  if (!bucket) {
9047
9814
  bucket = { key: g.key, label: g.label, icon: g.icon, tone: g.tone, items: [] };
@@ -9050,8 +9817,9 @@ function RecordsAdminShellInner(props) {
9050
9817
  }
9051
9818
  bucket.items.push(item);
9052
9819
  }
9053
- return order.sort().map((k) => map.get(k));
9054
- }, [isLifecycleRail, groupBy, collectionItems.items]);
9820
+ const sortFn = !groupBy ? compareLifecycleBuckets : (a, b) => a.localeCompare(b);
9821
+ return order.sort(sortFn).map((k) => map.get(k));
9822
+ }, [isLifecycleRail, lcGroupBy, groupBy, collectionItems.items]);
9055
9823
  const LIFECYCLE_PREFIX = "lifecycle:";
9056
9824
  const lifecycleRows = useMemo(() => {
9057
9825
  if (!isLifecycleRail) return [];
@@ -9101,15 +9869,47 @@ function RecordsAdminShellInner(props) {
9101
9869
  lifecycleSeededRef.current = true;
9102
9870
  }, [isLifecycleRail, defaultGroupKey, lifecycleBuckets, selectedLifecycleKey, setSelectedLifecycleKey]);
9103
9871
  const filteredCollectionItems = useMemo(() => {
9104
- if (!isLifecycleRail || !selectedLifecycleKey || !groupBy) return collectionItems.items;
9872
+ if (!isLifecycleRail || !selectedLifecycleKey || !lcGroupBy) return collectionItems.items;
9105
9873
  return collectionItems.items.filter((it) => {
9106
- const g = groupBy(it) ?? { key: "__other" };
9874
+ const g = lcGroupBy(it) ?? { key: "__other" };
9107
9875
  return g.key === selectedLifecycleKey;
9108
9876
  });
9109
- }, [isLifecycleRail, selectedLifecycleKey, groupBy, collectionItems.items]);
9877
+ }, [isLifecycleRail, selectedLifecycleKey, lcGroupBy, collectionItems.items]);
9110
9878
  const leftItems = isProductTab ? productListItems : isRuleTab ? applyFacetBrowseFilter(
9111
9879
  isCollection ? collectionRuleRailItems : filteredRuleItems
9112
9880
  ) : isLifecycleRail ? lifecycleRows : (isGlobalTab || isAllTab) && isCollection ? [collectionGlobalAllRow] : isRecordsTab ? applyFacetBrowseFilter(recordList.items) : [];
9881
+ const railShowsHistoryDisclosure = isRecordsTab && !isLifecycleRail && !((isGlobalTab || isAllTab) && isCollection);
9882
+ const filteredLeftItems = useMemo(() => {
9883
+ if (!railShowsHistoryDisclosure) return leftItems;
9884
+ return leftItems.filter((r) => {
9885
+ const s = r.lifecycleStatus;
9886
+ if (s == null || s === "") return true;
9887
+ const allow = activeStatuses ?? ["active"];
9888
+ return allow.includes(s);
9889
+ });
9890
+ }, [leftItems, railShowsHistoryDisclosure, activeStatuses]);
9891
+ const railHistoryBySlot = useMemo(() => {
9892
+ if (!railShowsHistoryDisclosure) return void 0;
9893
+ return recordList.historyBySlot;
9894
+ }, [railShowsHistoryDisclosure, recordList.historyBySlot]);
9895
+ const decoratedLeftItems = useMemo(() => {
9896
+ if (!hasSingletonConflicts) return filteredLeftItems;
9897
+ return filteredLeftItems.map((row) => {
9898
+ if (!row.id) return row;
9899
+ const key = slotKey(row);
9900
+ const activeId = activeRecordIdsBySlot.get(key);
9901
+ if (activeId === void 0) return row;
9902
+ const isActive2 = row.id === activeId;
9903
+ const conflictBadge = [{
9904
+ label: isActive2 ? "Active" : "Duplicate",
9905
+ tone: isActive2 ? "success" : "warning"
9906
+ }];
9907
+ return {
9908
+ ...row,
9909
+ badges: [...conflictBadge, ...row.badges ?? []]
9910
+ };
9911
+ });
9912
+ }, [filteredLeftItems, hasSingletonConflicts, activeRecordIdsBySlot]);
9113
9913
  const leftLoading = isProductTab ? !productPinned && productBrowse.isLoading : isRecordsTab ? recordList.isLoading || probe.isLoading : false;
9114
9914
  const leftError = isProductTab ? productBrowse.error : isRecordsTab ? recordList.error : null;
9115
9915
  const leftSelectedId = isProductTab ? void 0 : isLifecycleRail ? `${LIFECYCLE_PREFIX}${selectedLifecycleKey ?? "__all"}` : selectedRecordId && selectedRecordId !== DRAFT_ID3 ? selectedRecordId : void 0;
@@ -9456,69 +10256,138 @@ function RecordsAdminShellInner(props) {
9456
10256
  )
9457
10257
  ] })
9458
10258
  ] }),
9459
- /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto", children: isGlobalTab && !isCollection ? null : /* @__PURE__ */ jsxs(Fragment, { children: [
9460
- leftLoading && /* @__PURE__ */ jsx(LoadingState, {}),
9461
- !leftLoading && leftError && /* @__PURE__ */ jsx(ErrorState, { error: leftError }),
9462
- !leftLoading && !leftError && leftItems.length === 0 && (renderEmptyState ? renderEmptyState({ scope: activeScope }) : /* @__PURE__ */ jsx(
9463
- EmptyState,
10259
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-y-auto", children: [
10260
+ hasSingletonConflicts && /* @__PURE__ */ jsx(
10261
+ SingletonConflictBanner,
9464
10262
  {
9465
- icon: search ? icons.empty.search : icons.empty.default,
9466
- title: search ? i18n.noResults : i18n.railEmptyTitle,
9467
- body: search ? void 0 : isRuleTab ? i18n.rulesEmptyBody : i18n.railEmptyBody
10263
+ conflictCount: singletonConflicts.length,
10264
+ duplicateCount: totalDuplicateCount,
10265
+ onResolve: () => {
10266
+ const first = singletonConflicts[0]?.duplicates[0] ?? singletonConflicts[0]?.active;
10267
+ if (first?.id) {
10268
+ void runWithGuard(() => {
10269
+ setSelectedRecordId(first.id);
10270
+ });
10271
+ }
10272
+ },
10273
+ onArchiveDuplicates: enableArchiveDuplicates ? async () => {
10274
+ const ids = singletonConflicts.flatMap((c) => c.duplicates.map((d) => d.id)).filter((id) => !!id);
10275
+ for (const id of ids) {
10276
+ try {
10277
+ await SL.app.records.update(collectionId, appId, id, { status: archivedStatusValue }, true);
10278
+ onTelemetry?.({
10279
+ type: "recordAction.invoke",
10280
+ recordType,
10281
+ key: "conflict.archiveDuplicates",
10282
+ ref: id
10283
+ });
10284
+ } catch (err) {
10285
+ console.warn("[RecordsAdminShell] archive-duplicate failed", id, err);
10286
+ }
10287
+ }
10288
+ if (cardinality === "singleton") {
10289
+ ruleScopedList.refetch();
10290
+ globalScopedList.refetch();
10291
+ }
10292
+ await refetchAll();
10293
+ } : void 0,
10294
+ onDeleteDuplicates: enableDeleteDuplicates ? async () => {
10295
+ const ids = singletonConflicts.flatMap((c) => c.duplicates.map((d) => d.id)).filter((id) => !!id);
10296
+ for (const id of ids) {
10297
+ try {
10298
+ await SL.app.records.remove(collectionId, appId, id, true);
10299
+ onTelemetry?.({
10300
+ type: "recordAction.invoke",
10301
+ recordType,
10302
+ key: "conflict.deleteDuplicates",
10303
+ ref: id
10304
+ });
10305
+ } catch (err) {
10306
+ console.warn("[RecordsAdminShell] delete-duplicate failed", id, err);
10307
+ }
10308
+ }
10309
+ if (cardinality === "singleton") {
10310
+ ruleScopedList.refetch();
10311
+ globalScopedList.refetch();
10312
+ }
10313
+ await refetchAll();
10314
+ } : void 0,
10315
+ i18n: {
10316
+ title: i18n.conflictBannerTitle,
10317
+ bodyOne: i18n.conflictBannerBodyOne,
10318
+ bodyMany: i18n.conflictBannerBodyMany,
10319
+ archiveLabel: i18n.conflictArchiveDuplicates,
10320
+ deleteLabel: i18n.conflictDeleteDuplicates,
10321
+ deleteConfirm: i18n.conflictDeleteConfirm,
10322
+ resolveLabel: i18n.conflictResolveLabel
10323
+ }
9468
10324
  }
9469
- )),
9470
- !leftLoading && !leftError && leftItems.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
9471
- /* @__PURE__ */ jsx(
9472
- RecordList,
10325
+ ),
10326
+ isGlobalTab && !isCollection && !hasSingletonConflicts ? null : /* @__PURE__ */ jsxs(Fragment, { children: [
10327
+ leftLoading && /* @__PURE__ */ jsx(LoadingState, {}),
10328
+ !leftLoading && leftError && /* @__PURE__ */ jsx(ErrorState, { error: leftError }),
10329
+ !leftLoading && !leftError && decoratedLeftItems.length === 0 && (renderEmptyState ? renderEmptyState({ scope: activeScope }) : /* @__PURE__ */ jsx(
10330
+ EmptyState,
9473
10331
  {
9474
- items: leftItems,
9475
- selectedId: leftSelectedId,
9476
- selectedAnchorKey: leftSelectedAnchorKey,
9477
- onSelect: onLeftSelect,
9478
- dirtyId,
9479
- dirtyAnchorKey,
9480
- dirtyKeys,
9481
- errorKeys,
9482
- presentation: effectivePresentation,
9483
- renderListRow,
9484
- groupBy: (
9485
- // The synthetic "All items" row in collection mode is a
9486
- // navigational anchor, not a real record — applying the
9487
- // host's groupBy bucketed it under "Other" (its
9488
- // data is null). Skip grouping for that single-row rail.
9489
- (isGlobalTab || isAllTab) && isCollection || isLifecycleRail ? void 0 : effectiveGroupBy
9490
- ),
9491
- renderGroupActions: renderRuleGroupActions,
9492
- rowClipboard,
9493
- rowActions: wrappedRecordActions,
9494
- i18n
10332
+ icon: search ? icons.empty.search : icons.empty.default,
10333
+ title: search ? i18n.noResults : i18n.railEmptyTitle,
10334
+ body: search ? void 0 : isRuleTab ? i18n.rulesEmptyBody : i18n.railEmptyBody
9495
10335
  }
9496
- ),
9497
- isProductTab && !productPinned && /* @__PURE__ */ jsx(
9498
- LoadMoreFooter,
9499
- {
9500
- shown: leftItems.length,
9501
- hasNextPage: !!productBrowse.hasNextPage,
9502
- isFetchingNextPage: !!productBrowse.isFetchingNextPage,
9503
- onLoadMore: () => {
9504
- void productBrowse.fetchNextPage();
10336
+ )),
10337
+ !leftLoading && !leftError && decoratedLeftItems.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
10338
+ /* @__PURE__ */ jsx(
10339
+ RecordList,
10340
+ {
10341
+ items: decoratedLeftItems,
10342
+ selectedId: leftSelectedId,
10343
+ selectedAnchorKey: leftSelectedAnchorKey,
10344
+ onSelect: onLeftSelect,
10345
+ dirtyId,
10346
+ dirtyAnchorKey,
10347
+ dirtyKeys,
10348
+ errorKeys,
10349
+ presentation: effectivePresentation,
10350
+ renderListRow,
10351
+ groupBy: (
10352
+ // The synthetic "All items" row in collection mode is a
10353
+ // navigational anchor, not a real record — applying the
10354
+ // host's groupBy bucketed it under "Other" (its
10355
+ // data is null). Skip grouping for that single-row rail.
10356
+ (isGlobalTab || isAllTab) && isCollection || isLifecycleRail ? void 0 : effectiveGroupBy
10357
+ ),
10358
+ renderGroupActions: renderRuleGroupActions,
10359
+ rowClipboard,
10360
+ rowActions: wrappedRecordActions,
10361
+ i18n,
10362
+ historyBySlot: railHistoryBySlot
9505
10363
  }
9506
- }
9507
- ),
9508
- isRecordsTab && (!isCollection || !(isAllTab || isGlobalTab)) && /* @__PURE__ */ jsx(
9509
- LoadMoreFooter,
9510
- {
9511
- shown: recordList.items.length,
9512
- total: recordList.total,
9513
- hasNextPage: !!recordList.hasNextPage,
9514
- isFetchingNextPage: !!recordList.isFetchingNextPage,
9515
- onLoadMore: () => {
9516
- void recordList.fetchNextPage();
10364
+ ),
10365
+ isProductTab && !productPinned && /* @__PURE__ */ jsx(
10366
+ LoadMoreFooter,
10367
+ {
10368
+ shown: leftItems.length,
10369
+ hasNextPage: !!productBrowse.hasNextPage,
10370
+ isFetchingNextPage: !!productBrowse.isFetchingNextPage,
10371
+ onLoadMore: () => {
10372
+ void productBrowse.fetchNextPage();
10373
+ }
9517
10374
  }
9518
- }
9519
- )
10375
+ ),
10376
+ isRecordsTab && (!isCollection || !(isAllTab || isGlobalTab)) && /* @__PURE__ */ jsx(
10377
+ LoadMoreFooter,
10378
+ {
10379
+ shown: recordList.items.length,
10380
+ total: recordList.total,
10381
+ hasNextPage: !!recordList.hasNextPage,
10382
+ isFetchingNextPage: !!recordList.isFetchingNextPage,
10383
+ onLoadMore: () => {
10384
+ void recordList.fetchNextPage();
10385
+ }
10386
+ }
10387
+ )
10388
+ ] })
9520
10389
  ] })
9521
- ] }) })
10390
+ ] })
9522
10391
  ] }) }),
9523
10392
  /* @__PURE__ */ jsxs("main", { className: "overflow-hidden", children: [
9524
10393
  ruleWizardStep !== null && /* @__PURE__ */ jsxs(
@@ -9666,6 +10535,8 @@ function RecordsAdminShellInner(props) {
9666
10535
  ),
9667
10536
  ruleWizardStep === null && isProductTab && selectedProductId && !isCollection && editingTargetScope && resolved.source !== "self" && selectedRecordId !== DRAFT_ID3 ? (() => {
9668
10537
  const productName = productLookupItems.find((p) => p.id === selectedProductId)?.name ?? selectedProductId;
10538
+ const pasteEntry = wizardClipboard.entry;
10539
+ const pasteSourceLabel = pasteEntry?.sourceLabel ?? pasteEntry?.sourceScope.raw;
9669
10540
  return /* @__PURE__ */ jsx(
9670
10541
  CreateRecordChooser,
9671
10542
  {
@@ -9674,7 +10545,9 @@ function RecordsAdminShellInner(props) {
9674
10545
  primaryLabel: "Start blank",
9675
10546
  onPrimary: () => onCreateProductRecord("blank"),
9676
10547
  secondaryLabel: singletonGlobalSeedAvailable ? "Copy from global" : void 0,
9677
- onSecondary: singletonGlobalSeedAvailable ? () => onCreateProductRecord("global") : void 0
10548
+ onSecondary: singletonGlobalSeedAvailable ? () => onCreateProductRecord("global") : void 0,
10549
+ tertiaryLabel: pasteEntry ? pasteSourceLabel ? `Paste from ${pasteSourceLabel}` : "Paste from clipboard" : void 0,
10550
+ onTertiary: pasteEntry ? () => onCreateProductRecord("paste") : void 0
9678
10551
  }
9679
10552
  );
9680
10553
  })() : null,
@@ -10247,11 +11120,40 @@ function useRecordEditor(args) {
10247
11120
  if (!resolved.recordId) return;
10248
11121
  await removeRecord(ctx, resolved.recordId);
10249
11122
  draftStore.clearDraft(draftKey);
11123
+ removeRecordFromCaches(queryClient, ctx, resolved.recordId);
11124
+ const cacheKey = resolvedRecordQueryKey({
11125
+ collectionId: ctx.collectionId,
11126
+ appId: ctx.appId,
11127
+ recordType: ctx.recordType,
11128
+ productId: scope.productId,
11129
+ variantId: scope.variantId,
11130
+ batchId: scope.batchId,
11131
+ facetId: scope.facetId,
11132
+ facetValue: scope.facetValue,
11133
+ proofId: scope.proofId,
11134
+ recordId: resolved.recordId,
11135
+ withParent: true
11136
+ });
11137
+ queryClient.removeQueries({ queryKey: cacheKey });
11138
+ const anchorCacheKey = resolvedRecordQueryKey({
11139
+ collectionId: ctx.collectionId,
11140
+ appId: ctx.appId,
11141
+ recordType: ctx.recordType,
11142
+ productId: scope.productId,
11143
+ variantId: scope.variantId,
11144
+ batchId: scope.batchId,
11145
+ facetId: scope.facetId,
11146
+ facetValue: scope.facetValue,
11147
+ proofId: scope.proofId,
11148
+ recordId: void 0,
11149
+ withParent: true
11150
+ });
11151
+ queryClient.removeQueries({ queryKey: anchorCacheKey });
10250
11152
  queryClient.invalidateQueries({
10251
11153
  queryKey: scopeCountsQueryKey(ctx.collectionId, ctx.appId, ctx.recordType)
10252
11154
  });
10253
11155
  onDeleted?.();
10254
- }, [resolved.source, resolved.recordId, draftKey]);
11156
+ }, [resolved.source, resolved.recordId, draftKey, scope.raw]);
10255
11157
  const prevDraftKeyRef = useRef(draftKey);
10256
11158
  const prevScopeRawRef = useRef(scope.raw);
10257
11159
  useEffect(() => {
@@ -10728,6 +11630,6 @@ function useMergedRecord(args) {
10728
11630
  // src/components/RecordsAdmin/index.ts
10729
11631
  assertComponentStylesLoaded("records-admin");
10730
11632
 
10731
- 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 };
11633
+ 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 };
10732
11634
  //# sourceMappingURL=index.js.map
10733
11635
  //# sourceMappingURL=index.js.map