@proveanything/smartlinks-utils-ui 0.12.2 → 0.12.5

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, 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';
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, CopyPlus, ClipboardPaste, Box, X, Search, Image, Table, ArrowLeft, ChevronLeft, AlertTriangle, Info, HelpCircle, CornerDownLeft, Circle, ArrowUpDown, ArrowUp, ArrowDown, MinusCircle, XCircle, AlertCircle, Undo2, Save, Loader2, Archive, ArrowRight, Globe2, Sparkles, 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';
@@ -240,6 +240,13 @@ var DEFAULT_I18N = {
240
240
  pasteWarnContinue: "Continue",
241
241
  duplicateAction: "Duplicate",
242
242
  duplicateToast: "Duplicated {name}. Review and Save.",
243
+ duplicateDialogTitle: 'Duplicate "{name}" to:',
244
+ duplicateTargetGlobal: "Global",
245
+ duplicateTargetSameRule: "Same rule as source",
246
+ duplicateTargetExistingRules: "Existing rules",
247
+ duplicateTargetNewRule: "New rule\u2026",
248
+ duplicateConfirm: "Duplicate",
249
+ duplicateCancel: "Cancel",
243
250
  copyAndNewRuleAction: "Copy and start new rule",
244
251
  itemsAllLabel: "All items",
245
252
  subtitleEmpty: "Not set",
@@ -1621,6 +1628,199 @@ var ClipboardToast = ({ message, variant = "copy", onDismiss }) => {
1621
1628
  }
1622
1629
  );
1623
1630
  };
1631
+ var DEFAULT_I18N2 = {
1632
+ title: 'Duplicate "{name}" to:',
1633
+ global: "Global",
1634
+ sameRule: "Same rule as source",
1635
+ existingRules: "Existing rules",
1636
+ newRule: "New rule\u2026",
1637
+ confirm: "Duplicate",
1638
+ cancel: "Cancel"
1639
+ };
1640
+ function DuplicateTargetPicker({
1641
+ open,
1642
+ sourceLabel,
1643
+ sourceHasRule,
1644
+ rules,
1645
+ supportsRules,
1646
+ supportsGlobal = true,
1647
+ onCancel,
1648
+ onConfirm,
1649
+ i18n
1650
+ }) {
1651
+ const t = { ...DEFAULT_I18N2, ...i18n ?? {} };
1652
+ const [picked, setPicked] = useState(null);
1653
+ const containerRef = useRef(null);
1654
+ useEffect(() => {
1655
+ if (!open) {
1656
+ setPicked(null);
1657
+ return;
1658
+ }
1659
+ setPicked(sourceHasRule && supportsRules ? { kind: "sameRule" } : supportsGlobal ? { kind: "global" } : null);
1660
+ }, [open, sourceHasRule, supportsRules, supportsGlobal]);
1661
+ useEffect(() => {
1662
+ if (!open) return;
1663
+ const onKey = (e) => {
1664
+ if (e.key === "Escape") {
1665
+ e.preventDefault();
1666
+ onCancel();
1667
+ }
1668
+ };
1669
+ window.addEventListener("keydown", onKey);
1670
+ return () => window.removeEventListener("keydown", onKey);
1671
+ }, [open, onCancel]);
1672
+ const sortedRules = useMemo(() => rules.slice().sort((a, b) => {
1673
+ if (b.count !== a.count) return b.count - a.count;
1674
+ return a.summary.localeCompare(b.summary);
1675
+ }), [rules]);
1676
+ if (!open || typeof document === "undefined") return null;
1677
+ const stop = (e) => {
1678
+ e.stopPropagation();
1679
+ };
1680
+ const isSame = (a, b) => {
1681
+ if (!a || a.kind !== b.kind) return false;
1682
+ if (a.kind === "existingRule" && b.kind === "existingRule") {
1683
+ return a.ruleHash === b.ruleHash;
1684
+ }
1685
+ return true;
1686
+ };
1687
+ const RadioRow = ({
1688
+ target,
1689
+ label,
1690
+ sublabel,
1691
+ icon: Icon
1692
+ }) => {
1693
+ const checked = isSame(picked, target);
1694
+ return /* @__PURE__ */ jsxs(
1695
+ "button",
1696
+ {
1697
+ type: "button",
1698
+ onClick: () => setPicked(target),
1699
+ className: "w-full text-left px-3 py-2 rounded-md border transition-colors flex items-start gap-2.5",
1700
+ style: {
1701
+ borderColor: checked ? "hsl(var(--ra-accent))" : "hsl(var(--ra-border))",
1702
+ background: checked ? "hsl(var(--ra-accent) / 0.08)" : "transparent",
1703
+ color: "hsl(var(--ra-text))"
1704
+ },
1705
+ children: [
1706
+ /* @__PURE__ */ jsx(
1707
+ "span",
1708
+ {
1709
+ className: "mt-0.5 inline-flex items-center justify-center h-4 w-4 rounded-full shrink-0",
1710
+ style: {
1711
+ border: `2px solid ${checked ? "hsl(var(--ra-accent))" : "hsl(var(--ra-border))"}`
1712
+ },
1713
+ children: checked && /* @__PURE__ */ jsx(
1714
+ "span",
1715
+ {
1716
+ className: "h-2 w-2 rounded-full",
1717
+ style: { background: "hsl(var(--ra-accent))" }
1718
+ }
1719
+ )
1720
+ }
1721
+ ),
1722
+ Icon && /* @__PURE__ */ jsx(Icon, { className: "h-4 w-4 mt-0.5 shrink-0" }),
1723
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
1724
+ /* @__PURE__ */ jsx("span", { className: "block text-sm", children: label }),
1725
+ sublabel && /* @__PURE__ */ jsx("span", { className: "block text-xs truncate", style: { color: "hsl(var(--ra-muted-text))" }, children: sublabel })
1726
+ ] })
1727
+ ]
1728
+ }
1729
+ );
1730
+ };
1731
+ const showRuleSection = supportsRules && (sortedRules.length > 0 || sourceHasRule);
1732
+ return createPortal(
1733
+ /* @__PURE__ */ jsx(
1734
+ "div",
1735
+ {
1736
+ className: "fixed inset-0 z-[1000] flex items-center justify-center",
1737
+ style: { background: "hsl(0 0% 0% / 0.4)" },
1738
+ onClick: onCancel,
1739
+ onMouseDown: stop,
1740
+ onTouchStart: stop,
1741
+ children: /* @__PURE__ */ jsxs(
1742
+ "div",
1743
+ {
1744
+ ref: containerRef,
1745
+ role: "dialog",
1746
+ "aria-modal": "true",
1747
+ className: "w-[min(28rem,calc(100vw-2rem))] max-h-[min(36rem,calc(100vh-2rem))] flex flex-col rounded-lg shadow-xl",
1748
+ style: { background: "hsl(var(--ra-surface))", border: "1px solid hsl(var(--ra-border))" },
1749
+ onClick: stop,
1750
+ onMouseDown: stop,
1751
+ onTouchStart: stop,
1752
+ children: [
1753
+ /* @__PURE__ */ jsx("header", { className: "px-4 py-3 border-b", style: { borderColor: "hsl(var(--ra-border))" }, children: /* @__PURE__ */ jsx("h2", { className: "text-sm font-semibold", style: { color: "hsl(var(--ra-text))" }, children: t.title.replace("{name}", sourceLabel) }) }),
1754
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 min-h-0 overflow-y-auto px-4 py-3 space-y-3", children: [
1755
+ supportsGlobal && /* @__PURE__ */ jsx(RadioRow, { target: { kind: "global" }, label: t.global, icon: Globe2 }),
1756
+ showRuleSection && /* @__PURE__ */ jsxs(Fragment, { children: [
1757
+ sourceHasRule && /* @__PURE__ */ jsx(RadioRow, { target: { kind: "sameRule" }, label: t.sameRule, icon: Target }),
1758
+ sortedRules.length > 0 && /* @__PURE__ */ jsxs("div", { children: [
1759
+ /* @__PURE__ */ jsx(
1760
+ "div",
1761
+ {
1762
+ className: "text-[10px] uppercase tracking-wide mb-1.5 px-1",
1763
+ style: { color: "hsl(var(--ra-muted-text))" },
1764
+ children: t.existingRules
1765
+ }
1766
+ ),
1767
+ /* @__PURE__ */ jsx("div", { className: "space-y-1.5", children: sortedRules.map((r) => /* @__PURE__ */ jsx(
1768
+ RadioRow,
1769
+ {
1770
+ target: { kind: "existingRule", ruleHash: r.hash },
1771
+ label: r.summary,
1772
+ sublabel: `${r.count} record${r.count === 1 ? "" : "s"}`,
1773
+ icon: Target
1774
+ },
1775
+ r.hash
1776
+ )) })
1777
+ ] }),
1778
+ /* @__PURE__ */ jsx(RadioRow, { target: { kind: "newRule" }, label: t.newRule, icon: Sparkles })
1779
+ ] })
1780
+ ] }),
1781
+ /* @__PURE__ */ jsxs(
1782
+ "footer",
1783
+ {
1784
+ className: "px-4 py-3 border-t flex items-center justify-end gap-2",
1785
+ style: { borderColor: "hsl(var(--ra-border))" },
1786
+ children: [
1787
+ /* @__PURE__ */ jsx(
1788
+ "button",
1789
+ {
1790
+ type: "button",
1791
+ onClick: onCancel,
1792
+ className: "text-xs px-3 py-1.5 rounded-md border hover:bg-[hsl(var(--ra-muted))]",
1793
+ style: { borderColor: "hsl(var(--ra-border))", color: "hsl(var(--ra-text))" },
1794
+ children: t.cancel
1795
+ }
1796
+ ),
1797
+ /* @__PURE__ */ jsxs(
1798
+ "button",
1799
+ {
1800
+ type: "button",
1801
+ disabled: !picked,
1802
+ onClick: () => {
1803
+ if (picked) onConfirm(picked);
1804
+ },
1805
+ className: "text-xs px-3 py-1.5 rounded-md font-medium transition-opacity disabled:opacity-40 inline-flex items-center gap-1.5",
1806
+ style: { background: "hsl(var(--ra-accent))", color: "hsl(var(--ra-surface))" },
1807
+ children: [
1808
+ /* @__PURE__ */ jsx(Plus, { className: "h-3.5 w-3.5" }),
1809
+ t.confirm
1810
+ ]
1811
+ }
1812
+ )
1813
+ ]
1814
+ }
1815
+ )
1816
+ ]
1817
+ }
1818
+ )
1819
+ }
1820
+ ),
1821
+ document.body
1822
+ );
1823
+ }
1624
1824
  function useShellClipboard(args) {
1625
1825
  const {
1626
1826
  enabled,
@@ -1640,7 +1840,11 @@ function useShellClipboard(args) {
1640
1840
  onLeftSelectRef,
1641
1841
  onCreateItemDraftRef,
1642
1842
  onCreateRuleFromClipboardRef,
1643
- isRuleTab
1843
+ isRuleTab,
1844
+ ruleCatalogue,
1845
+ ruleCatalogueRef,
1846
+ supportsRules = true,
1847
+ onCreateItemDraftAtScopeRef
1644
1848
  } = args;
1645
1849
  const clipboard = useRecordClipboard({
1646
1850
  appId,
@@ -1765,9 +1969,103 @@ function useShellClipboard(args) {
1765
1969
  window.setTimeout(() => {
1766
1970
  onCreateRuleFromClipboardRef?.current?.();
1767
1971
  }, 0);
1972
+ } : void 0,
1973
+ // Collection-cardinality clone affordance — only shown when the host
1974
+ // wired item-draft creation. Pushes the editor value to the clipboard
1975
+ // (so the existing pendingPasteTarget effect lands it on the new
1976
+ // draft) and mints a fresh draft.
1977
+ // Collection-cardinality clone affordance — opens the Duplicate-To
1978
+ // picker so the admin chooses the destination scope (Global, an
1979
+ // existing rule, a new rule, or the source's own rule). We only show
1980
+ // this when the host wired the at-scope draft creator, since without
1981
+ // it we have nowhere to mint the duplicate.
1982
+ onDuplicate: isCollection && !!onCreateItemDraftAtScopeRef ? () => {
1983
+ if (!editingScope) return;
1984
+ openDuplicatePicker({
1985
+ value: editorCtx.value,
1986
+ scope: editingScope,
1987
+ label: editorHeaderLabel ?? editingScope.raw,
1988
+ facetRule: resolved.facetRule ?? null,
1989
+ recordId: resolved.recordId
1990
+ });
1768
1991
  } : void 0
1769
1992
  } : void 0;
1770
1993
  const [pendingPasteTarget, setPendingPasteTarget] = useState(null);
1994
+ const [pendingSeed, setPendingSeed] = useState(null);
1995
+ const [pickerOpen, setPickerOpen] = useState(false);
1996
+ const pickerSourceRef = useRef(null);
1997
+ const openDuplicatePicker = useCallback((source) => {
1998
+ pickerSourceRef.current = {
1999
+ value: source.value,
2000
+ scope: source.scope,
2001
+ label: source.label,
2002
+ facetRule: source.facetRule ?? null,
2003
+ recordId: source.recordId
2004
+ };
2005
+ setPickerOpen(true);
2006
+ }, []);
2007
+ const onDuplicatePickerCancel = useCallback(() => {
2008
+ setPickerOpen(false);
2009
+ pickerSourceRef.current = null;
2010
+ }, []);
2011
+ const onDuplicatePickerConfirm = useCallback((target) => {
2012
+ const source = pickerSourceRef.current;
2013
+ setPickerOpen(false);
2014
+ pickerSourceRef.current = null;
2015
+ if (!source) return;
2016
+ const transformed = onCopyOverride ? onCopyOverride({ value: source.value, scope: source.scope }) : cloneValue(source.value);
2017
+ if (target.kind === "newRule") {
2018
+ clipboard.set({
2019
+ value: transformed,
2020
+ sourceScope: source.scope,
2021
+ sourceRecordId: source.recordId,
2022
+ sourceLabel: source.label
2023
+ });
2024
+ onTelemetry?.({
2025
+ type: "clipboard.copy",
2026
+ recordType,
2027
+ sourceRef: anchorKey(source.scope)
2028
+ });
2029
+ window.setTimeout(() => onCreateRuleFromClipboardRef?.current?.(), 0);
2030
+ return;
2031
+ }
2032
+ const create = onCreateItemDraftAtScopeRef?.current;
2033
+ if (!create) return;
2034
+ let scopeArg;
2035
+ if (target.kind === "global") {
2036
+ scopeArg = { kind: "global" };
2037
+ } else if (target.kind === "sameRule") {
2038
+ if (!source.facetRule) return;
2039
+ scopeArg = { kind: "rule", rule: source.facetRule };
2040
+ } else {
2041
+ const list = ruleCatalogueRef?.current ?? ruleCatalogue ?? [];
2042
+ const hit = list.find((r) => r.hash === target.ruleHash);
2043
+ if (!hit) return;
2044
+ scopeArg = { kind: "rule", rule: hit.rule };
2045
+ }
2046
+ const newId = create(scopeArg);
2047
+ if (!newId) return;
2048
+ setPendingPasteTarget({ kind: "record", recordId: newId });
2049
+ setPendingSeed({ value: transformed, sourceLabel: source.label });
2050
+ onTelemetry?.({
2051
+ type: "clipboard.copy",
2052
+ recordType,
2053
+ sourceRef: anchorKey(source.scope)
2054
+ });
2055
+ setNotice({
2056
+ message: i18n.duplicateToast.replace("{name}", source.label),
2057
+ variant: "copy"
2058
+ });
2059
+ }, [
2060
+ onCopyOverride,
2061
+ clipboard,
2062
+ onTelemetry,
2063
+ recordType,
2064
+ onCreateRuleFromClipboardRef,
2065
+ onCreateItemDraftAtScopeRef,
2066
+ ruleCatalogue,
2067
+ i18n.duplicateToast
2068
+ ]);
1771
2069
  useEffect(() => {
1772
2070
  if (!pendingPasteTarget) return;
1773
2071
  if (!editingScope) return;
@@ -1775,14 +2073,27 @@ function useShellClipboard(args) {
1775
2073
  if (!matched) return;
1776
2074
  const t = window.setTimeout(() => {
1777
2075
  setPendingPasteTarget(null);
2076
+ if (pendingSeed) {
2077
+ const seed = pendingSeed;
2078
+ setPendingSeed(null);
2079
+ editorCtx.onChange(seed.value);
2080
+ onTelemetry?.({
2081
+ type: "clipboard.paste",
2082
+ recordType,
2083
+ sourceRef: anchorKey(editingScope),
2084
+ destinationRef: editingScope.raw,
2085
+ replaced: false
2086
+ });
2087
+ return;
2088
+ }
1778
2089
  void pasteCurrent();
1779
2090
  }, 0);
1780
2091
  return () => window.clearTimeout(t);
1781
- }, [pendingPasteTarget, editingScope, isCollection, selectedItemId, selectedRecordId, pasteCurrent]);
2092
+ }, [pendingPasteTarget, editingScope, isCollection, selectedItemId, selectedRecordId, pasteCurrent, pendingSeed, editorCtx, onTelemetry, recordType]);
1782
2093
  const rowClipboard = enabled ? (record) => {
1783
2094
  const summaryHasData = record.data != null;
1784
2095
  const sourceParsed = record.scope;
1785
- const canDuplicate = summaryHasData && isCollection;
2096
+ const canDuplicate = summaryHasData && isCollection && !!onCreateItemDraftAtScopeRef;
1786
2097
  const canCopyAndNewRule = summaryHasData && !isCollection && !!isRuleTab && !!onCreateRuleFromClipboardRef;
1787
2098
  return {
1788
2099
  onCopy: summaryHasData ? () => {
@@ -1813,24 +2124,12 @@ function useShellClipboard(args) {
1813
2124
  }, 0);
1814
2125
  } : void 0,
1815
2126
  onDuplicate: canDuplicate ? () => {
1816
- const value = onCopyOverride ? onCopyOverride({ value: record.data, scope: sourceParsed }) : cloneValue(record.data);
1817
- clipboard.set({
1818
- value,
1819
- sourceScope: sourceParsed,
1820
- sourceRecordId: record.id ?? void 0,
1821
- sourceLabel: record.label
1822
- });
1823
- onTelemetry?.({ type: "clipboard.copy", recordType, sourceRef: anchorKey(record.scope) });
1824
- onLeftSelectRef.current?.(record);
1825
- const create = onCreateItemDraftRef?.current;
1826
- if (!create) return;
1827
- window.setTimeout(() => {
1828
- const newId = create();
1829
- if (newId) setPendingPasteTarget({ kind: "record", recordId: newId });
1830
- }, 0);
1831
- setNotice({
1832
- message: i18n.duplicateToast.replace("{name}", record.label),
1833
- variant: "copy"
2127
+ openDuplicatePicker({
2128
+ value: record.data,
2129
+ scope: sourceParsed,
2130
+ label: record.label,
2131
+ facetRule: record.facetRule ?? null,
2132
+ recordId: record.id ?? void 0
1834
2133
  });
1835
2134
  } : void 0
1836
2135
  };
@@ -1843,10 +2142,36 @@ function useShellClipboard(args) {
1843
2142
  onDismiss: () => setNotice(null)
1844
2143
  }
1845
2144
  ) : null;
2145
+ const duplicatePicker = enabled ? /* @__PURE__ */ jsx(
2146
+ DuplicateTargetPicker,
2147
+ {
2148
+ open: pickerOpen,
2149
+ sourceLabel: pickerSourceRef.current?.label ?? "",
2150
+ sourceHasRule: !!pickerSourceRef.current?.facetRule,
2151
+ rules: (ruleCatalogueRef?.current ?? ruleCatalogue ?? []).map((r) => ({
2152
+ hash: r.hash,
2153
+ summary: r.summary,
2154
+ count: r.count
2155
+ })),
2156
+ supportsRules,
2157
+ onCancel: onDuplicatePickerCancel,
2158
+ onConfirm: onDuplicatePickerConfirm,
2159
+ i18n: {
2160
+ title: i18n.duplicateDialogTitle,
2161
+ global: i18n.duplicateTargetGlobal,
2162
+ sameRule: i18n.duplicateTargetSameRule,
2163
+ existingRules: i18n.duplicateTargetExistingRules,
2164
+ newRule: i18n.duplicateTargetNewRule,
2165
+ confirm: i18n.duplicateConfirm,
2166
+ cancel: i18n.duplicateCancel
2167
+ }
2168
+ }
2169
+ ) : null;
1846
2170
  return {
1847
2171
  editorClipboard,
1848
2172
  rowClipboard,
1849
2173
  confirmDialog: pasteConfirm.dialog,
2174
+ duplicatePicker,
1850
2175
  toast,
1851
2176
  store: clipboard
1852
2177
  };
@@ -3981,8 +4306,8 @@ var LifecycleStatusControl = ({
3981
4306
  if (next === value) return;
3982
4307
  setBusy(true);
3983
4308
  try {
3984
- await SL.app.records.update(collectionId, appId, recordId, { status: next }, true);
3985
- onChanged?.(next);
4309
+ const updated = await SL.app.records.update(collectionId, appId, recordId, { status: next }, true);
4310
+ onChanged?.(next, updated);
3986
4311
  } catch (err) {
3987
4312
  console.warn("[LifecycleStatusControl] update failed", err);
3988
4313
  } finally {
@@ -4176,9 +4501,9 @@ var LifecycleStatusMenu = ({
4176
4501
  }
4177
4502
  setBusy(next.value);
4178
4503
  try {
4179
- await SL.app.records.update(collectionId, appId, recordId, { status: next.value }, true);
4504
+ const updated = await SL.app.records.update(collectionId, appId, recordId, { status: next.value }, true);
4180
4505
  onTelemetry?.({ from: current, to: next.value });
4181
- onChanged?.(next.value);
4506
+ onChanged?.(next.value, updated);
4182
4507
  } catch (err) {
4183
4508
  console.warn("[LifecycleStatusMenu] update failed", err);
4184
4509
  } finally {
@@ -4684,7 +5009,8 @@ function useCollectionItems(args) {
4684
5009
  label: stableItemId,
4685
5010
  updatedAt: rec.updatedAt,
4686
5011
  itemId: stableItemId,
4687
- facetRule: recFacetRule
5012
+ facetRule: recFacetRule,
5013
+ lifecycleStatus: rec.status ?? void 0
4688
5014
  };
4689
5015
  return toSummary2(rec, base);
4690
5016
  }).filter((x) => x !== null);
@@ -5159,6 +5485,22 @@ function RecordEditor({
5159
5485
  ]
5160
5486
  }
5161
5487
  ),
5488
+ clipboard.onDuplicate && /* @__PURE__ */ jsxs(
5489
+ "button",
5490
+ {
5491
+ type: "button",
5492
+ onClick: clipboard.onDuplicate,
5493
+ disabled: !clipboard.canCopy || !!ctx.isSaving,
5494
+ title: clipboard.duplicateLabel ?? i18n.duplicateAction ?? "Duplicate",
5495
+ "aria-label": clipboard.duplicateLabel ?? i18n.duplicateAction ?? "Duplicate",
5496
+ className: "text-xs px-2.5 py-1.5 rounded-md border transition-opacity disabled:opacity-40 hover:bg-[hsl(var(--ra-muted))] inline-flex items-center gap-1.5",
5497
+ style: { borderColor: "hsl(var(--ra-border))", color: "hsl(var(--ra-text))" },
5498
+ children: [
5499
+ /* @__PURE__ */ jsx(CopyPlus, { className: "w-3 h-3" }),
5500
+ clipboard.duplicateLabel ?? i18n.duplicateAction ?? "Duplicate"
5501
+ ]
5502
+ }
5503
+ ),
5162
5504
  /* @__PURE__ */ jsxs(
5163
5505
  "button",
5164
5506
  {
@@ -6895,7 +7237,9 @@ function SiblingRail({
6895
7237
  total,
6896
7238
  hasNextPage,
6897
7239
  isFetchingNextPage,
6898
- onLoadMore
7240
+ onLoadMore,
7241
+ rowClipboard,
7242
+ rowActions
6899
7243
  }) {
6900
7244
  const ruleLabelLookup = useRuleLabelLookup();
6901
7245
  const newLabel = i18n.newItem.includes("{noun}") ? i18n.newItem.replace("{noun}", itemNoun ?? "item") : i18n.newItem;
@@ -6934,45 +7278,68 @@ function SiblingRail({
6934
7278
  const ruleClauses = item.facetRule ? summarizeFacetRule(item.facetRule, ruleLabelLookup) : [];
6935
7279
  const isTargeted = ruleClauses.length > 0;
6936
7280
  const ruleSummary = isTargeted ? ruleClauses.map((c) => c.label).join(" \xB7 ") : null;
6937
- return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
6938
- "button",
6939
- {
6940
- type: "button",
6941
- onClick: () => onSelect(id),
6942
- className: "ra-row",
6943
- "data-selected": selected,
6944
- children: [
6945
- /* @__PURE__ */ jsxs("div", { className: "ra-row-body", children: [
6946
- /* @__PURE__ */ jsx("div", { className: "ra-row-title", children: item.label }),
6947
- item.subtitle && /* @__PURE__ */ jsx("div", { className: "ra-row-sub", children: item.subtitle })
6948
- ] }),
6949
- isTargeted && /* @__PURE__ */ jsx(
6950
- "span",
6951
- {
6952
- className: "ra-row-rule-pip",
6953
- title: ruleSummary ? `Targeted: ${ruleSummary}` : "This item has targeting rules",
6954
- "aria-label": ruleSummary ? `Targeted: ${ruleSummary}` : "This item has targeting rules",
6955
- children: /* @__PURE__ */ jsx(Target, { className: "w-3 h-3", "aria-hidden": "true" })
6956
- }
6957
- ),
6958
- hasError ? /* @__PURE__ */ jsx(
6959
- "span",
6960
- {
6961
- className: "ra-error-pip",
6962
- title: "Save failed",
6963
- "aria-label": "Save failed"
6964
- }
6965
- ) : isDirty ? /* @__PURE__ */ jsx(
6966
- "span",
6967
- {
6968
- className: "ra-dirty-pip",
6969
- title: "Unsaved changes",
6970
- "aria-label": "Unsaved changes"
7281
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("div", { className: "ra-row-shell", "data-selected": selected, children: [
7282
+ /* @__PURE__ */ jsxs(
7283
+ "button",
7284
+ {
7285
+ type: "button",
7286
+ onClick: () => onSelect(id),
7287
+ className: "ra-row",
7288
+ "data-selected": selected,
7289
+ children: [
7290
+ /* @__PURE__ */ jsxs("div", { className: "ra-row-body", children: [
7291
+ /* @__PURE__ */ jsx("div", { className: "ra-row-title", children: item.label }),
7292
+ item.subtitle && /* @__PURE__ */ jsx("div", { className: "ra-row-sub", children: item.subtitle })
7293
+ ] }),
7294
+ isTargeted && /* @__PURE__ */ jsx(
7295
+ "span",
7296
+ {
7297
+ className: "ra-row-rule-pip",
7298
+ title: ruleSummary ? `Targeted: ${ruleSummary}` : "This item has targeting rules",
7299
+ "aria-label": ruleSummary ? `Targeted: ${ruleSummary}` : "This item has targeting rules",
7300
+ children: /* @__PURE__ */ jsx(Target, { className: "w-3 h-3", "aria-hidden": "true" })
7301
+ }
7302
+ ),
7303
+ hasError ? /* @__PURE__ */ jsx(
7304
+ "span",
7305
+ {
7306
+ className: "ra-error-pip",
7307
+ title: "Save failed",
7308
+ "aria-label": "Save failed"
7309
+ }
7310
+ ) : isDirty ? /* @__PURE__ */ jsx(
7311
+ "span",
7312
+ {
7313
+ className: "ra-dirty-pip",
7314
+ title: "Unsaved changes",
7315
+ "aria-label": "Unsaved changes"
7316
+ }
7317
+ ) : null
7318
+ ]
7319
+ }
7320
+ ),
7321
+ (() => {
7322
+ const cb = rowClipboard ? rowClipboard(item) : null;
7323
+ const extra = rowActions ? rowActions(item) ?? void 0 : void 0;
7324
+ if (!cb?.onCopy && !cb?.onDuplicate && !cb?.onCopyAndNewRule && !(extra && extra.length)) {
7325
+ return null;
7326
+ }
7327
+ return /* @__PURE__ */ jsx(
7328
+ RowContextMenu,
7329
+ {
7330
+ onCopy: cb?.onCopy,
7331
+ onDuplicate: cb?.onDuplicate,
7332
+ onCopyAndNewRule: cb?.onCopyAndNewRule,
7333
+ actions: extra,
7334
+ i18n: {
7335
+ copy: i18n.copy,
7336
+ duplicateAction: i18n.duplicateAction,
7337
+ copyAndNewRuleAction: i18n.copyAndNewRuleAction
6971
7338
  }
6972
- ) : null
6973
- ]
6974
- }
6975
- ) }, key);
7339
+ }
7340
+ );
7341
+ })()
7342
+ ] }) }, key);
6976
7343
  }) })
6977
7344
  ] }),
6978
7345
  onLoadMore && /* @__PURE__ */ jsx(
@@ -8327,14 +8694,14 @@ var coerceDraftItemId2 = (generateItemId) => {
8327
8694
  const candidate = generateItemId ? generateItemId() : mintDraftItemId2();
8328
8695
  return isDraftId3(candidate) ? candidate : `draft:${candidate}`;
8329
8696
  };
8330
- var productItemToSummary = (p) => {
8697
+ var productItemToSummary = (p, configured) => {
8331
8698
  const ref = buildRef({ productId: p.id });
8332
8699
  return {
8333
8700
  id: null,
8334
8701
  ref,
8335
8702
  scope: parseRef(ref),
8336
8703
  data: null,
8337
- status: "empty",
8704
+ status: configured?.has(p.id) ? "configured" : "empty",
8338
8705
  label: p.name,
8339
8706
  subtitle: p.sku ?? void 0
8340
8707
  };
@@ -9123,6 +9490,8 @@ function RecordsAdminShellInner(props) {
9123
9490
  ]);
9124
9491
  const onLeftSelectRef = useRef(null);
9125
9492
  const onCreateItemDraftRef = useRef(null);
9493
+ const onCreateItemDraftAtScopeRef = useRef(null);
9494
+ const ruleCatalogueRef = useRef([]);
9126
9495
  const onCreateRuleFromClipboardRef = useRef(null);
9127
9496
  const previewReopenAnchorRef = useRef(null);
9128
9497
  const { runWithGuard } = useDirtyNavigation({
@@ -9175,14 +9544,17 @@ function RecordsAdminShellInner(props) {
9175
9544
  isCollection,
9176
9545
  selectedItemId,
9177
9546
  selectedRecordId,
9178
- resolved: { source: resolved.source, recordId: resolved.recordId },
9547
+ resolved: { source: resolved.source, recordId: resolved.recordId, facetRule: resolved.facetRule ?? null },
9179
9548
  onTelemetry,
9180
9549
  onCopyOverride,
9181
9550
  onPasteOverride,
9182
9551
  onLeftSelectRef,
9183
9552
  onCreateItemDraftRef,
9184
9553
  onCreateRuleFromClipboardRef,
9185
- isRuleTab: activeScope === "rule" || activeScope === "collection"
9554
+ isRuleTab: activeScope === "rule" || activeScope === "collection",
9555
+ ruleCatalogueRef,
9556
+ supportsRules: effectiveTopLevelScopes.includes("rule"),
9557
+ onCreateItemDraftAtScopeRef
9186
9558
  });
9187
9559
  const editorClipboard = shellClipboard.editorClipboard;
9188
9560
  const rowClipboard = shellClipboard.rowClipboard;
@@ -9219,11 +9591,12 @@ function RecordsAdminShellInner(props) {
9219
9591
  label: label2,
9220
9592
  onAction: async () => {
9221
9593
  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();
9594
+ const updated = await SL.app.records.update(collectionId, appId, record.id, { status: next }, true);
9595
+ if (updated) {
9596
+ patchRecordIntoCaches(queryClient, ctx, updated);
9597
+ queryClient.invalidateQueries({
9598
+ queryKey: scopeCountsQueryKey(ctx.collectionId, ctx.appId, ctx.recordType)
9599
+ });
9227
9600
  }
9228
9601
  } catch (err) {
9229
9602
  console.warn("[RecordsAdminShell] lifecycle update failed", err);
@@ -9263,6 +9636,14 @@ function RecordsAdminShellInner(props) {
9263
9636
  const itemViewCtx = baseItemViewCtx;
9264
9637
  const renderEditorWithPreview = () => {
9265
9638
  if (!editingTargetScope) return null;
9639
+ const targetingSavedRecord = isCollection && selectedItemId && !isDraftId3(selectedItemId) || !!selectedRecordId && !isDraftId3(selectedRecordId);
9640
+ if (resolved.isLoading && targetingSavedRecord && resolved.source !== "self") {
9641
+ return /* @__PURE__ */ jsxs("div", { className: "p-4 space-y-3", "aria-busy": "true", "aria-live": "polite", children: [
9642
+ /* @__PURE__ */ jsx("div", { className: "h-6 rounded-md animate-pulse", style: { background: "hsl(var(--ra-muted))", width: "40%" } }),
9643
+ /* @__PURE__ */ jsx("div", { className: "h-24 rounded-md animate-pulse", style: { background: "hsl(var(--ra-muted))" } }),
9644
+ /* @__PURE__ */ jsx("div", { className: "h-24 rounded-md animate-pulse", style: { background: "hsl(var(--ra-muted))" } })
9645
+ ] });
9646
+ }
9266
9647
  const previewAnchorRef = previewReopenAnchorRef;
9267
9648
  const previewBody = renderPreview && effectivePreviewScope ? renderPreview({ resolved: editorCtx.value, previewScope: effectivePreviewScope }) : null;
9268
9649
  const scopePicker = previewScopePicker && effectivePreviewScope ? /* @__PURE__ */ jsx(
@@ -9314,7 +9695,7 @@ function RecordsAdminShellInner(props) {
9314
9695
  ]
9315
9696
  }
9316
9697
  ) : 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;
9698
+ 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) ?? collectionItems.items.find((r) => r.id === selectedRecordId) : isCollection && selectedItemId && !isDraftId3(selectedItemId) ? collectionItems.items.find((r) => r.id === selectedItemId) : void 0;
9318
9699
  const editorLifecycleControl = selectedSummary?.id ? /* @__PURE__ */ jsx(
9319
9700
  LifecycleStatusControl,
9320
9701
  {
@@ -9324,8 +9705,13 @@ function RecordsAdminShellInner(props) {
9324
9705
  recordId: selectedSummary.id,
9325
9706
  current: selectedSummary.lifecycleStatus,
9326
9707
  i18n,
9327
- onChanged: () => {
9328
- void recordList.refetch();
9708
+ onChanged: (_next, updated) => {
9709
+ if (updated) {
9710
+ patchRecordIntoCaches(queryClient, ctx, updated);
9711
+ queryClient.invalidateQueries({
9712
+ queryKey: scopeCountsQueryKey(ctx.collectionId, ctx.appId, ctx.recordType)
9713
+ });
9714
+ }
9329
9715
  }
9330
9716
  }
9331
9717
  ) : null;
@@ -9348,11 +9734,12 @@ function RecordsAdminShellInner(props) {
9348
9734
  from: e.from,
9349
9735
  to: e.to
9350
9736
  }),
9351
- onChanged: () => {
9352
- void refetchAll();
9353
- if (cardinality === "singleton") {
9354
- ruleScopedList.refetch();
9355
- globalScopedList.refetch();
9737
+ onChanged: (_next, updated) => {
9738
+ if (updated) {
9739
+ patchRecordIntoCaches(queryClient, ctx, updated);
9740
+ queryClient.invalidateQueries({
9741
+ queryKey: scopeCountsQueryKey(ctx.collectionId, ctx.appId, ctx.recordType)
9742
+ });
9356
9743
  }
9357
9744
  }
9358
9745
  }
@@ -9524,7 +9911,7 @@ function RecordsAdminShellInner(props) {
9524
9911
  }];
9525
9912
  }
9526
9913
  const configured = scopeCounts.productIds;
9527
- const all = productBrowse.items.map(productItemToSummary);
9914
+ const all = productBrowse.items.map((p) => productItemToSummary(p, configured));
9528
9915
  const isConfigured = (s) => {
9529
9916
  const pid = s.scope.productId;
9530
9917
  return !!pid && configured.has(pid);
@@ -9949,6 +10336,26 @@ function RecordsAdminShellInner(props) {
9949
10336
  onLeftSelectRef.current = onLeftSelect;
9950
10337
  onCreateItemDraftRef.current = onItemCreate;
9951
10338
  onCreateRuleFromClipboardRef.current = () => onCreateRule("paste");
10339
+ ruleCatalogueRef.current = ruleCatalogue;
10340
+ onCreateItemDraftAtScopeRef.current = (target) => {
10341
+ if (!isCollection) return null;
10342
+ if (target.kind === "global") {
10343
+ if (activeScope !== "collection") setActiveScope("collection");
10344
+ setSelectedRecordId(null);
10345
+ setDraftKind(null);
10346
+ const id2 = onItemCreate();
10347
+ return id2 ?? null;
10348
+ }
10349
+ if (activeScope !== "rule") setActiveScope("rule");
10350
+ setRuleWizardRule(target.rule);
10351
+ setRuleWizardSeedMode(null);
10352
+ setRuleWizardDraftKey(null);
10353
+ setRuleWizardStep(2);
10354
+ setDraftKind("rule");
10355
+ setSelectedRecordId(null);
10356
+ const id = onItemCreate();
10357
+ return id ?? null;
10358
+ };
9952
10359
  return /* @__PURE__ */ jsx(RuleLabelLookupProvider, { value: ruleLabelLookup, children: /* @__PURE__ */ jsxs(
9953
10360
  "div",
9954
10361
  {
@@ -9957,6 +10364,7 @@ function RecordsAdminShellInner(props) {
9957
10364
  children: [
9958
10365
  dirtyConfirm.dialog,
9959
10366
  shellClipboard.confirmDialog,
10367
+ shellClipboard.duplicatePicker,
9960
10368
  shellClipboard.toast,
9961
10369
  (() => {
9962
10370
  const showFloatHelp = !!intro && dismissed && resolvedReopenAffordance === "footer" && !headerWillRender;
@@ -10123,6 +10531,8 @@ function RecordsAdminShellInner(props) {
10123
10531
  onLoadMore: () => {
10124
10532
  void collectionItems.fetchNextPage();
10125
10533
  },
10534
+ rowClipboard,
10535
+ rowActions: wrappedRecordActions,
10126
10536
  contextKind: isLifecycleRailEarly && lifecycleBucketLabel ? lifecycleBucketLabel : activeScope === "rule" ? "Rule" : activeScope === "product" ? "Product" : activeScope === "collection" ? "Global" : activeScope === "all" ? "All records" : activeScope === "variant" ? "Variant" : activeScope === "batch" ? "Batch" : activeScope === "facet" ? "Facet" : void 0,
10127
10537
  contextSummary: isLifecycleRailEarly && lifecycleBucketLabel ? `${scopedCollectionItemsList.length} ${itemNounLabel}${scopedCollectionItemsList.length === 1 ? "" : "s"}` : activeScope === "rule" ? activeRuleSummary : activeScope === "product" ? editorHeaderLabel ?? null : null,
10128
10538
  i18n
@@ -10274,7 +10684,8 @@ function RecordsAdminShellInner(props) {
10274
10684
  const ids = singletonConflicts.flatMap((c) => c.duplicates.map((d) => d.id)).filter((id) => !!id);
10275
10685
  for (const id of ids) {
10276
10686
  try {
10277
- await SL.app.records.update(collectionId, appId, id, { status: archivedStatusValue }, true);
10687
+ const updated = await SL.app.records.update(collectionId, appId, id, { status: archivedStatusValue }, true);
10688
+ if (updated) patchRecordIntoCaches(queryClient, ctx, updated);
10278
10689
  onTelemetry?.({
10279
10690
  type: "recordAction.invoke",
10280
10691
  recordType,
@@ -10285,17 +10696,16 @@ function RecordsAdminShellInner(props) {
10285
10696
  console.warn("[RecordsAdminShell] archive-duplicate failed", id, err);
10286
10697
  }
10287
10698
  }
10288
- if (cardinality === "singleton") {
10289
- ruleScopedList.refetch();
10290
- globalScopedList.refetch();
10291
- }
10292
- await refetchAll();
10699
+ queryClient.invalidateQueries({
10700
+ queryKey: scopeCountsQueryKey(ctx.collectionId, ctx.appId, ctx.recordType)
10701
+ });
10293
10702
  } : void 0,
10294
10703
  onDeleteDuplicates: enableDeleteDuplicates ? async () => {
10295
10704
  const ids = singletonConflicts.flatMap((c) => c.duplicates.map((d) => d.id)).filter((id) => !!id);
10296
10705
  for (const id of ids) {
10297
10706
  try {
10298
10707
  await SL.app.records.remove(collectionId, appId, id, true);
10708
+ removeRecordFromCaches(queryClient, ctx, id);
10299
10709
  onTelemetry?.({
10300
10710
  type: "recordAction.invoke",
10301
10711
  recordType,
@@ -10306,11 +10716,9 @@ function RecordsAdminShellInner(props) {
10306
10716
  console.warn("[RecordsAdminShell] delete-duplicate failed", id, err);
10307
10717
  }
10308
10718
  }
10309
- if (cardinality === "singleton") {
10310
- ruleScopedList.refetch();
10311
- globalScopedList.refetch();
10312
- }
10313
- await refetchAll();
10719
+ queryClient.invalidateQueries({
10720
+ queryKey: scopeCountsQueryKey(ctx.collectionId, ctx.appId, ctx.recordType)
10721
+ });
10314
10722
  } : void 0,
10315
10723
  i18n: {
10316
10724
  title: i18n.conflictBannerTitle,