@ai-matrx/associations 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -429,6 +429,7 @@ var ENTITY_TYPE_TOKENS = [
429
429
  "commerce_asset_reshoot_request",
430
430
  "commerce_asset_review",
431
431
  "commerce_asset_unknown",
432
+ "commerce_certified_printer",
432
433
  "commerce_cloud_sync_connection",
433
434
  "commerce_ebay_business_policy",
434
435
  "commerce_ebay_category",
@@ -1108,6 +1109,12 @@ var useEntityRelationships = useAssociations;
1108
1109
 
1109
1110
  // src/react/hooks/useContainerLinks.ts
1110
1111
  var import_react3 = require("react");
1112
+ function metadataRecord(metadata) {
1113
+ if (metadata === null || typeof metadata !== "object" || Array.isArray(metadata)) {
1114
+ return metadata == null ? {} : null;
1115
+ }
1116
+ return Object.fromEntries(Object.entries(metadata));
1117
+ }
1111
1118
  function useContainerLinks(args) {
1112
1119
  const { containerType, containerId, orgId } = args;
1113
1120
  const store = useAssociationsStore();
@@ -1229,6 +1236,31 @@ function useContainerLinks(args) {
1229
1236
  }
1230
1237
  return result;
1231
1238
  };
1239
+ const setPinned = async (token, resourceId, pinned, role) => {
1240
+ if (!containerId) return { ok: false, error: "Missing container id" };
1241
+ const exactRole = role ?? null;
1242
+ const link = incoming.find(
1243
+ (candidate) => candidate.token === token && candidate.resourceId === resourceId && candidate.role === exactRole
1244
+ );
1245
+ if (!link) return { ok: false, error: "Association edge not found" };
1246
+ const currentMetadata = metadataRecord(link.metadata);
1247
+ if (!currentMetadata) {
1248
+ return {
1249
+ ok: false,
1250
+ error: "Association metadata is not an object and cannot be updated safely"
1251
+ };
1252
+ }
1253
+ return store.add({
1254
+ sourceType: token,
1255
+ sourceId: resourceId,
1256
+ targetType: containerType,
1257
+ targetId: containerId,
1258
+ ...orgId != null ? { orgId } : {},
1259
+ ...link.label != null ? { label: link.label } : {},
1260
+ ...role !== void 0 ? { role } : {},
1261
+ metadata: { ...currentMetadata, pinned }
1262
+ });
1263
+ };
1232
1264
  const reload = async () => {
1233
1265
  await Promise.all([
1234
1266
  reloadGeneric(),
@@ -1246,7 +1278,8 @@ function useContainerLinks(args) {
1246
1278
  attachedIdsFor,
1247
1279
  linksFor,
1248
1280
  attach,
1249
- detach
1281
+ detach,
1282
+ setPinned
1250
1283
  };
1251
1284
  }
1252
1285
 
@@ -2627,6 +2660,9 @@ function UniversalAssociationPicker(props) {
2627
2660
  var import_react15 = require("react");
2628
2661
  var import_design_system5 = require("@ai-matrx/design-system");
2629
2662
  var import_jsx_runtime9 = require("react/jsx-runtime");
2663
+ function metadataIsPinned(metadata) {
2664
+ return metadata !== null && typeof metadata === "object" && !Array.isArray(metadata) && Reflect.get(metadata, "pinned") === true;
2665
+ }
2630
2666
  function AttachedItemsSheet(props) {
2631
2667
  const store = useAssociationsStore();
2632
2668
  const info = store.registry.getEntityInfo(props.token);
@@ -2662,7 +2698,8 @@ function AttachedItemsSheet(props) {
2662
2698
  enabled: props.open,
2663
2699
  links: props.links,
2664
2700
  ...props.onAdd ? { onAdd: props.onAdd } : {},
2665
- onDetach: props.onDetach
2701
+ onDetach: props.onDetach,
2702
+ ...props.onSetPinned ? { onSetPinned: props.onSetPinned } : {}
2666
2703
  }
2667
2704
  )
2668
2705
  }
@@ -2673,7 +2710,8 @@ function AttachedItemsBody({
2673
2710
  enabled,
2674
2711
  links,
2675
2712
  onAdd,
2676
- onDetach
2713
+ onDetach,
2714
+ onSetPinned
2677
2715
  }) {
2678
2716
  const store = useAssociationsStore();
2679
2717
  const notifier = useNotifier();
@@ -2715,11 +2753,12 @@ function AttachedItemsBody({
2715
2753
  cancelled = true;
2716
2754
  };
2717
2755
  }, [token, idKey, enabled, store]);
2718
- const detach = async (resourceId) => {
2756
+ const detach = async (link) => {
2757
+ const resourceId = link.resourceId;
2719
2758
  if (busyId) return;
2720
2759
  setBusyId(resourceId);
2721
2760
  try {
2722
- const res = await onDetach(resourceId);
2761
+ const res = await onDetach(resourceId, link.role);
2723
2762
  if (!res.ok) {
2724
2763
  notifier.error(`Couldn't detach${res.error ? `: ${res.error}` : ""}`);
2725
2764
  }
@@ -2727,6 +2766,20 @@ function AttachedItemsBody({
2727
2766
  setBusyId(null);
2728
2767
  }
2729
2768
  };
2769
+ const setPinned = async (link, pinned) => {
2770
+ if (!onSetPinned || busyId) return;
2771
+ setBusyId(link.resourceId);
2772
+ try {
2773
+ const res = await onSetPinned(link.resourceId, pinned, link.role);
2774
+ if (!res.ok) {
2775
+ notifier.error(
2776
+ `Couldn't ${pinned ? "pin" : "unpin"}${res.error ? `: ${res.error}` : ""}`
2777
+ );
2778
+ }
2779
+ } finally {
2780
+ setBusyId(null);
2781
+ }
2782
+ };
2730
2783
  return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
2731
2784
  titleError ? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "mb-2 rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-[12px] text-destructive", children: [
2732
2785
  "Showing names recorded at attach time \u2014 they may be out of date.",
@@ -2741,6 +2794,7 @@ function AttachedItemsBody({
2741
2794
  const missing = !titles.has(link.resourceId);
2742
2795
  const name = resolved ?? link.label ?? (missing ? null : "Untitled");
2743
2796
  const busy = busyId === link.resourceId;
2797
+ const pinned = metadataIsPinned(link.metadata);
2744
2798
  return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
2745
2799
  "li",
2746
2800
  {
@@ -2760,12 +2814,35 @@ function AttachedItemsBody({
2760
2814
  className: "text-foreground"
2761
2815
  }
2762
2816
  ) }),
2817
+ onSetPinned ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2818
+ "button",
2819
+ {
2820
+ type: "button",
2821
+ disabled: busy,
2822
+ onClick: () => void setPinned(link, !pinned),
2823
+ title: pinned ? "Unpin (stops staying in agent context)" : "Pin \u2014 always in agent context",
2824
+ "aria-label": pinned ? "Unpin" : "Pin",
2825
+ className: (0, import_design_system5.cn)(
2826
+ "flex h-6 w-6 shrink-0 items-center justify-center rounded-md transition-colors hover:bg-accent hover:text-foreground disabled:opacity-50",
2827
+ pinned ? "text-primary" : "text-muted-foreground"
2828
+ ),
2829
+ children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2830
+ PinIcon,
2831
+ {
2832
+ className: (0, import_design_system5.cn)(
2833
+ "h-3.5 w-3.5",
2834
+ pinned && "fill-primary/20"
2835
+ )
2836
+ }
2837
+ )
2838
+ }
2839
+ ) : null,
2763
2840
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2764
2841
  "button",
2765
2842
  {
2766
2843
  type: "button",
2767
2844
  disabled: busy,
2768
- onClick: () => void detach(link.resourceId),
2845
+ onClick: () => void detach(link),
2769
2846
  title: "Detach",
2770
2847
  className: (0, import_design_system5.cn)(
2771
2848
  "flex h-6 w-6 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors",
@@ -2861,7 +2938,15 @@ function AssociationCard({
2861
2938
  const role = getContentRoleMeta(info.contentRole);
2862
2939
  const Icon = info.Icon ?? DefaultEntityIcon;
2863
2940
  const [listOpen, setListOpen] = (0, import_react16.useState)(false);
2864
- const { status, countFor, attachedIdsFor, linksFor, attach, detach } = useContainerLinks({
2941
+ const {
2942
+ status,
2943
+ countFor,
2944
+ attachedIdsFor,
2945
+ linksFor,
2946
+ attach,
2947
+ detach,
2948
+ setPinned
2949
+ } = useContainerLinks({
2865
2950
  containerType: container?.type ?? "organization",
2866
2951
  containerId: container?.id ?? null,
2867
2952
  orgId: container?.orgId ?? null
@@ -2958,7 +3043,8 @@ function AssociationCard({
2958
3043
  setOpen(true);
2959
3044
  }
2960
3045
  } : {},
2961
- onDetach: (resourceId) => detach(token, resourceId)
3046
+ onDetach: (resourceId, role2) => detach(token, resourceId, role2 ?? void 0),
3047
+ onSetPinned: (resourceId, pinned, role2) => setPinned(token, resourceId, pinned, role2 ?? void 0)
2962
3048
  }
2963
3049
  ),
2964
3050
  canAttach && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
@@ -3017,6 +3103,9 @@ function AssociationCardGrid({
3017
3103
  var import_react17 = require("react");
3018
3104
  var import_design_system8 = require("@ai-matrx/design-system");
3019
3105
  var import_jsx_runtime12 = require("react/jsx-runtime");
3106
+ function metadataIsPinned2(metadata) {
3107
+ return metadata !== null && typeof metadata === "object" && !Array.isArray(metadata) && Reflect.get(metadata, "pinned") === true;
3108
+ }
3020
3109
  function useContainerLinksAdapter(container, tokens) {
3021
3110
  const store = useAssociationsStore();
3022
3111
  const links = useContainerLinks({
@@ -3031,6 +3120,7 @@ function useContainerLinksAdapter(container, tokens) {
3031
3120
  resourceId: l.resourceId,
3032
3121
  label: l.label,
3033
3122
  role: l.role,
3123
+ pinned: metadataIsPinned2(l.metadata),
3034
3124
  removable: true
3035
3125
  }))
3036
3126
  ) : [];
@@ -3040,11 +3130,13 @@ function useContainerLinksAdapter(container, tokens) {
3040
3130
  reload: links.reload,
3041
3131
  rows,
3042
3132
  attach: async (token, id, title) => links.attach(token, id, title),
3043
- detach: async (token, id, role) => links.detach(token, id, role)
3133
+ detach: async (token, id, role) => links.detach(token, id, role),
3134
+ setPinned: async (token, id, pinned, role) => links.setPinned(token, id, pinned, role)
3044
3135
  };
3045
3136
  }
3046
3137
  function AssociationList(props) {
3047
3138
  const store = useAssociationsStore();
3139
+ const notifier = useNotifier();
3048
3140
  const ctxEntity = usePrimaryEntity();
3049
3141
  const container = props.container ?? ctxEntity ?? null;
3050
3142
  const defaultAdapter = useContainerLinksAdapter(
@@ -3057,6 +3149,7 @@ function AssociationList(props) {
3057
3149
  const [pickerToken, setPickerToken] = (0, import_react17.useState)(null);
3058
3150
  const [showUniversal, setShowUniversal] = (0, import_react17.useState)(false);
3059
3151
  const [removingKeys, setRemovingKeys] = (0, import_react17.useState)(/* @__PURE__ */ new Set());
3152
+ const [pinningKeys, setPinningKeys] = (0, import_react17.useState)(/* @__PURE__ */ new Set());
3060
3153
  const rows = tokenFilter ? adapter.rows.filter((r) => tokenFilter.includes(r.token)) : adapter.rows;
3061
3154
  const visibleRows = rows.filter((r) => !removingKeys.has(r.key));
3062
3155
  const { titleFor } = useEntityTitles(
@@ -3080,6 +3173,31 @@ function AssociationList(props) {
3080
3173
  });
3081
3174
  return res;
3082
3175
  };
3176
+ const handlePin = async (row, pinned) => {
3177
+ if (!adapter.setPinned) return;
3178
+ const info = store.registry.tryGetEntityInfo(row.token);
3179
+ if (!info) return;
3180
+ setPinningKeys((prev) => new Set(prev).add(row.key));
3181
+ try {
3182
+ const res = await adapter.setPinned(
3183
+ info.token,
3184
+ row.resourceId,
3185
+ pinned,
3186
+ row.role ?? void 0
3187
+ );
3188
+ if (!res.ok) {
3189
+ notifier.error(
3190
+ `Couldn't ${pinned ? "pin" : "unpin"} resource${res.error ? `: ${res.error}` : ""}`
3191
+ );
3192
+ }
3193
+ } finally {
3194
+ setPinningKeys((prev) => {
3195
+ const next = new Set(prev);
3196
+ next.delete(row.key);
3197
+ return next;
3198
+ });
3199
+ }
3200
+ };
3083
3201
  const openRow = (row) => {
3084
3202
  if (props.openEntity) {
3085
3203
  props.openEntity(row.token, row.resourceId);
@@ -3270,18 +3388,15 @@ function AssociationList(props) {
3270
3388
  "button",
3271
3389
  {
3272
3390
  type: "button",
3273
- onClick: () => void adapter.setPinned?.(
3274
- info.token,
3275
- row.resourceId,
3276
- !(row.pinned ?? false)
3277
- ),
3391
+ disabled: pinningKeys.has(row.key),
3392
+ onClick: () => void handlePin(row, !(row.pinned ?? false)),
3278
3393
  title: row.pinned ? "Unpin (stops staying in agent context)" : "Pin \u2014 always in agent context",
3279
3394
  "aria-label": row.pinned ? "Unpin" : "Pin",
3280
3395
  className: (0, import_design_system8.cn)(
3281
3396
  "shrink-0 rounded p-0.5 transition-colors hover:!text-foreground",
3282
3397
  row.pinned ? "text-primary" : "text-muted-foreground/0 group-hover:text-muted-foreground"
3283
3398
  ),
3284
- children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
3399
+ children: pinningKeys.has(row.key) ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(SpinnerIcon, { className: "h-3 w-3 animate-spin" }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
3285
3400
  PinIcon,
3286
3401
  {
3287
3402
  className: (0, import_design_system8.cn)(