@proveanything/smartlinks-utils-ui 0.7.3 → 0.7.4

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.
@@ -2140,9 +2140,18 @@ interface Props<T> {
2140
2140
  error: Error | null;
2141
2141
  onBack: () => void;
2142
2142
  onSelect: (itemId: string) => void;
2143
+ /**
2144
+ * Set of keys (recordIds + scope refs) currently dirty in the shell-level
2145
+ * draft store. Used to paint a per-row pip on items the user has edited
2146
+ * but isn't currently looking at — mirrors the affordance the scope rail
2147
+ * gets via `RecordList.dirtyKeys`.
2148
+ */
2149
+ dirtyKeys?: ReadonlySet<string>;
2150
+ /** Subset of `dirtyKeys` whose last save attempt failed. */
2151
+ errorKeys?: ReadonlySet<string>;
2143
2152
  i18n: Pick<RecordsAdminI18n, 'backToScopes' | 'siblingsHeading' | 'noItemsTitle' | 'noItemsBody'>;
2144
2153
  }
2145
- declare function SiblingRail<T>({ items, selectedItemId, isLoading, error, onBack, onSelect, i18n, }: Props<T>): react_jsx_runtime.JSX.Element;
2154
+ declare function SiblingRail<T>({ items, selectedItemId, isLoading, error, onBack, onSelect, dirtyKeys, errorKeys, i18n, }: Props<T>): react_jsx_runtime.JSX.Element;
2146
2155
 
2147
2156
  interface ClipboardEntry<T = unknown> {
2148
2157
  value: T;
@@ -713,6 +713,7 @@ function useRecordEditor(args) {
713
713
  const [savedFacetRule, setSavedFacetRule] = useState(
714
714
  initialDraft ? initialDraft.baselineFacetRule : initialFacetRule
715
715
  );
716
+ const [userInteracted, setUserInteracted] = useState(!!initialDraft);
716
717
  const [optimisticSource, setOptimisticSource] = useState(null);
717
718
  const [isSaving, setIsSaving] = useState(false);
718
719
  const [saveError, setSaveError] = useState(null);
@@ -732,8 +733,18 @@ function useRecordEditor(args) {
732
733
  setFacetRule(initialFacetRule);
733
734
  setSavedFacetRule(initialFacetRule);
734
735
  setOptimisticSource(null);
736
+ setUserInteracted(false);
735
737
  }, [scope.raw, resolved.source, resolved.sourceRef]);
736
- const isDirty = !isEqual(value, savedSnapshot) || !isEqual(facetRule, savedFacetRule);
738
+ const valueDiff = !isEqual(value, savedSnapshot) || !isEqual(facetRule, savedFacetRule);
739
+ const isDirty = userInteracted && valueDiff;
740
+ const handleChange = useCallback((next) => {
741
+ setUserInteracted(true);
742
+ setValue(next);
743
+ }, []);
744
+ const handleFacetRuleChange = useCallback((next) => {
745
+ setUserInteracted(true);
746
+ setFacetRule(next);
747
+ }, []);
737
748
  const save = useCallback(async () => {
738
749
  const anchors = parsedRefToScope(scope);
739
750
  const hasAnchors = !!(anchors.productId || anchors.variantId || anchors.batchId || anchors.proofId);
@@ -817,6 +828,7 @@ function useRecordEditor(args) {
817
828
  const reset = useCallback(() => {
818
829
  setValue(savedSnapshot);
819
830
  setFacetRule(savedFacetRule);
831
+ setUserInteracted(false);
820
832
  draftStore.clearDraft(draftKey);
821
833
  }, [savedSnapshot, savedFacetRule]);
822
834
  const remove = useCallback(async () => {
@@ -826,15 +838,42 @@ function useRecordEditor(args) {
826
838
  draftStore.clearDraft(draftKey);
827
839
  onDeleted?.();
828
840
  }, [resolved.source, resolved.recordId]);
841
+ const prevDraftKeyRef = useRef(draftKey);
842
+ const prevScopeRawRef = useRef(scope.raw);
843
+ useEffect(() => {
844
+ const prevKey = prevDraftKeyRef.current;
845
+ const prevScopeRaw = prevScopeRawRef.current;
846
+ if (prevKey && prevKey !== draftKey && prevScopeRaw === scope.raw) {
847
+ const stale = draftStore.get(prevKey);
848
+ if (stale) draftStore.clearDraft(prevKey);
849
+ }
850
+ prevDraftKeyRef.current = draftKey;
851
+ prevScopeRawRef.current = scope.raw;
852
+ }, [draftKey, scope.raw]);
829
853
  useEffect(() => {
830
854
  if (!isDirty) {
831
855
  return;
832
856
  }
833
857
  const anchors = parsedRefToScope(scope);
834
858
  const saveKind = resolved.recordId && resolved.source === "self" ? "update" : createMode ? "create" : "upsert";
859
+ const deriveLabel = () => {
860
+ const v = value;
861
+ if (v && typeof v === "object") {
862
+ for (const key of ["title", "name", "label", "heading", "question"]) {
863
+ const raw = v[key];
864
+ if (typeof raw === "string" && raw.trim()) return raw.trim();
865
+ }
866
+ }
867
+ if (scope.raw?.startsWith("item:")) return "Untitled item";
868
+ if (scope.kind === "rule") return "Rule";
869
+ if (scope.kind && scope.kind !== "collection") {
870
+ return scope.kind.charAt(0).toUpperCase() + scope.kind.slice(1);
871
+ }
872
+ return "Default";
873
+ };
835
874
  draftStore.upsertDraft({
836
875
  key: draftKey,
837
- label: scope.raw || "Default",
876
+ label: deriveLabel(),
838
877
  context: scope.kind,
839
878
  scopeRaw: scope.raw,
840
879
  recordId: resolved.recordId,
@@ -858,7 +897,7 @@ function useRecordEditor(args) {
858
897
  const cannotSaveReason = !ruleValid ? "Pick at least one value for every facet in the rule before saving." : void 0;
859
898
  return {
860
899
  value,
861
- onChange: setValue,
900
+ onChange: handleChange,
862
901
  source: effectiveSource,
863
902
  recordId: resolved.recordId,
864
903
  parentValue: resolved.parentValue,
@@ -873,7 +912,7 @@ function useRecordEditor(args) {
873
912
  isSaving,
874
913
  saveError,
875
914
  facetRule,
876
- onFacetRuleChange: setFacetRule
915
+ onFacetRuleChange: handleFacetRuleChange
877
916
  };
878
917
  }
879
918
  var RT_KEY = (recordType) => recordType ?? "_default";
@@ -3722,6 +3761,8 @@ function SiblingRail({
3722
3761
  error,
3723
3762
  onBack,
3724
3763
  onSelect,
3764
+ dirtyKeys,
3765
+ errorKeys,
3725
3766
  i18n
3726
3767
  }) {
3727
3768
  return /* @__PURE__ */ jsxs("div", { className: "ra-sibling-rail", children: [
@@ -3745,17 +3786,36 @@ function SiblingRail({
3745
3786
  !isLoading && !error && items.length > 0 && /* @__PURE__ */ jsx("ul", { className: "ra-sibling-list", children: items.map((item) => {
3746
3787
  const id = item.itemId ?? "";
3747
3788
  const selected = selectedItemId === id;
3748
- return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
3789
+ const isDirty = !!(id && dirtyKeys?.has(id) || item.ref && dirtyKeys?.has(item.ref));
3790
+ const hasError = !!(id && errorKeys?.has(id) || item.ref && errorKeys?.has(item.ref));
3791
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
3749
3792
  "button",
3750
3793
  {
3751
3794
  type: "button",
3752
3795
  onClick: () => onSelect(id),
3753
3796
  className: "ra-row",
3754
3797
  "data-selected": selected,
3755
- children: /* @__PURE__ */ jsxs("div", { className: "ra-row-body", children: [
3756
- /* @__PURE__ */ jsx("div", { className: "ra-row-title", children: item.label }),
3757
- item.subtitle && /* @__PURE__ */ jsx("div", { className: "ra-row-sub", children: item.subtitle })
3758
- ] })
3798
+ children: [
3799
+ /* @__PURE__ */ jsxs("div", { className: "ra-row-body", children: [
3800
+ /* @__PURE__ */ jsx("div", { className: "ra-row-title", children: item.label }),
3801
+ item.subtitle && /* @__PURE__ */ jsx("div", { className: "ra-row-sub", children: item.subtitle })
3802
+ ] }),
3803
+ hasError ? /* @__PURE__ */ jsx(
3804
+ "span",
3805
+ {
3806
+ className: "ra-error-pip",
3807
+ title: "Save failed",
3808
+ "aria-label": "Save failed"
3809
+ }
3810
+ ) : isDirty ? /* @__PURE__ */ jsx(
3811
+ "span",
3812
+ {
3813
+ className: "ra-dirty-pip",
3814
+ title: "Unsaved changes",
3815
+ "aria-label": "Unsaved changes"
3816
+ }
3817
+ ) : null
3818
+ ]
3759
3819
  }
3760
3820
  ) }, item.ref);
3761
3821
  }) })
@@ -4202,8 +4262,23 @@ var UnsavedTray = ({
4202
4262
  const wrapRef = useRef(null);
4203
4263
  const SI = SaveIcon ?? Save;
4204
4264
  const DI = DiscardIcon ?? Undo2;
4205
- const total = drafts.length;
4206
- const errors = drafts.filter((d) => d.status === "error").length;
4265
+ const uniqueDrafts = useMemo(() => {
4266
+ const byBucket = /* @__PURE__ */ new Map();
4267
+ for (const d of drafts) {
4268
+ const bucket = d.recordId || d.scopeRaw || d.key;
4269
+ const existing = byBucket.get(bucket);
4270
+ if (!existing) {
4271
+ byBucket.set(bucket, d);
4272
+ continue;
4273
+ }
4274
+ const existingSynthetic = existing.key.startsWith("draft:");
4275
+ const incomingSynthetic = d.key.startsWith("draft:");
4276
+ if (existingSynthetic && !incomingSynthetic) byBucket.set(bucket, d);
4277
+ }
4278
+ return Array.from(byBucket.values()).sort((a, b) => a.order - b.order);
4279
+ }, [drafts]);
4280
+ const total = uniqueDrafts.length;
4281
+ const errors = uniqueDrafts.filter((d) => d.status === "error").length;
4207
4282
  const isSingle = total === 1;
4208
4283
  useEffect(() => {
4209
4284
  if (!open) return;
@@ -4214,7 +4289,7 @@ var UnsavedTray = ({
4214
4289
  return () => window.removeEventListener("mousedown", onDoc);
4215
4290
  }, [open]);
4216
4291
  if (total === 0) return null;
4217
- const countLabel = isSingle ? drafts[0].label || "this record" : countTemplate.replace("{n}", String(total));
4292
+ const countLabel = isSingle ? uniqueDrafts[0].label || "this record" : countTemplate.replace("{n}", String(total));
4218
4293
  return /* @__PURE__ */ jsxs(
4219
4294
  "div",
4220
4295
  {
@@ -4284,7 +4359,7 @@ var UnsavedTray = ({
4284
4359
  }
4285
4360
  )
4286
4361
  ] }),
4287
- open && !isSingle && /* @__PURE__ */ jsx("div", { className: "ra-unsaved-popover", role: "menu", children: drafts.map((d) => /* @__PURE__ */ jsxs(
4362
+ open && !isSingle && /* @__PURE__ */ jsx("div", { className: "ra-unsaved-popover", role: "menu", children: uniqueDrafts.map((d) => /* @__PURE__ */ jsxs(
4288
4363
  "button",
4289
4364
  {
4290
4365
  type: "button",
@@ -5833,6 +5908,8 @@ function RecordsAdminShellInner(props) {
5833
5908
  error: collectionItems.error,
5834
5909
  onBack: onItemBack,
5835
5910
  onSelect: onItemOpen,
5911
+ dirtyKeys,
5912
+ errorKeys,
5836
5913
  i18n
5837
5914
  }
5838
5915
  ) : /* @__PURE__ */ jsxs(Fragment, { children: [