@ai-matrx/associations 0.4.0 → 0.5.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.
@@ -35,12 +35,16 @@ __export(react_exports, {
35
35
  CaptureToolbarAction: () => CaptureToolbarAction,
36
36
  CategorySelect: () => CategorySelect,
37
37
  CategoryTagPicker: () => CategoryTagPicker,
38
+ CommentComposer: () => CommentComposer,
39
+ CommentThread: () => CommentThread,
38
40
  DefaultEntityIcon: () => DefaultEntityIcon,
39
41
  DoorRef: () => DoorRef,
40
42
  PrimaryEntityProvider: () => PrimaryEntityProvider,
41
43
  UniversalAssociationPicker: () => UniversalAssociationPicker,
42
44
  UnresolvedRef: () => UnresolvedRef,
43
45
  attachedKey: () => attachedKey,
46
+ buildCommentTree: () => buildCommentTree,
47
+ formatRelativeTime: () => formatRelativeTime,
44
48
  getContentRoleMeta: () => getContentRoleMeta,
45
49
  useAssociationCandidates: () => useAssociationCandidates,
46
50
  useAssociationEntitySelectAdapter: () => useAssociationEntitySelectAdapter,
@@ -48,6 +52,7 @@ __export(react_exports, {
48
52
  useAssociationsStore: () => useAssociationsStore,
49
53
  useAssociationsUiPorts: () => useAssociationsUiPorts,
50
54
  useCategories: () => useCategories,
55
+ useComments: () => useComments,
51
56
  useContainerLinks: () => useContainerLinks,
52
57
  useContainerLinksAdapter: () => useContainerLinksAdapter,
53
58
  useEntityRelationships: () => useEntityRelationships,
@@ -71,6 +76,7 @@ function AssociationsProvider({
71
76
  capture,
72
77
  pickerOverrides,
73
78
  entityDoors,
79
+ authorDisplay,
74
80
  children
75
81
  }) {
76
82
  const notifierWarned = (0, import_react.useRef)(false);
@@ -84,7 +90,8 @@ function AssociationsProvider({
84
90
  ...windowShell !== void 0 ? { windowShell } : {},
85
91
  ...capture !== void 0 ? { capture } : {},
86
92
  ...pickerOverrides !== void 0 ? { pickerOverrides } : {},
87
- ...entityDoors !== void 0 ? { entityDoors } : {}
93
+ ...entityDoors !== void 0 ? { entityDoors } : {},
94
+ ...authorDisplay !== void 0 ? { authorDisplay } : {}
88
95
  },
89
96
  children
90
97
  }
@@ -824,6 +831,12 @@ var IDLE_CATEGORIES = Object.freeze({
824
831
  fetchedAt: null,
825
832
  error: null
826
833
  });
834
+ var IDLE_COMMENTS = Object.freeze({
835
+ status: "idle",
836
+ comments: Object.freeze([]),
837
+ fetchedAt: null,
838
+ error: null
839
+ });
827
840
 
828
841
  // src/react/hooks/useAssociations.ts
829
842
  var noopSubscribe = () => () => {
@@ -1302,8 +1315,74 @@ function currentUserIdOrNull(store) {
1302
1315
  }
1303
1316
  }
1304
1317
 
1305
- // src/react/hooks/useAssociationEntitySelect.ts
1318
+ // src/react/hooks/useComments.ts
1306
1319
  var import_react8 = require("react");
1320
+ var noopSubscribe3 = () => () => {
1321
+ };
1322
+ function useComments(args) {
1323
+ const { token, id, autoLoad = true } = args;
1324
+ const store = useAssociationsStore();
1325
+ const key = token && id ? associationsKey(token, id) : null;
1326
+ const entry = (0, import_react8.useSyncExternalStore)(
1327
+ key ? (cb) => store.subscribeComments(key, cb) : noopSubscribe3,
1328
+ () => store.getComments(token, id ?? ""),
1329
+ () => store.getComments(token, id ?? "")
1330
+ );
1331
+ const loadedKey = (0, import_react8.useRef)(null);
1332
+ (0, import_react8.useEffect)(() => {
1333
+ if (!autoLoad || !key || !id) return;
1334
+ if (loadedKey.current === key) return;
1335
+ loadedKey.current = key;
1336
+ void store.loadComments(token, id);
1337
+ }, [autoLoad, store, key, token, id]);
1338
+ let currentUserId = null;
1339
+ try {
1340
+ currentUserId = store.identity.requireUserId();
1341
+ } catch {
1342
+ currentUserId = null;
1343
+ }
1344
+ return {
1345
+ comments: entry.comments,
1346
+ status: entry.status,
1347
+ error: entry.error,
1348
+ fetchedAt: entry.fetchedAt,
1349
+ add: async (body, opts) => {
1350
+ if (!id) return { ok: false, error: "Missing entity id" };
1351
+ return store.addComment({
1352
+ entityType: token,
1353
+ entityId: id,
1354
+ body,
1355
+ ...opts?.parentId !== void 0 ? { parentId: opts.parentId } : {},
1356
+ ...opts?.orgId !== void 0 ? { orgId: opts.orgId } : {}
1357
+ });
1358
+ },
1359
+ edit: async (commentId, body) => {
1360
+ if (!id) return { ok: false, error: "Missing entity id" };
1361
+ return store.editComment({
1362
+ entityType: token,
1363
+ entityId: id,
1364
+ id: commentId,
1365
+ body
1366
+ });
1367
+ },
1368
+ remove: async (commentId) => {
1369
+ if (!id) return { ok: false, error: "Missing entity id" };
1370
+ return store.deleteComment({
1371
+ entityType: token,
1372
+ entityId: id,
1373
+ id: commentId
1374
+ });
1375
+ },
1376
+ reload: async () => {
1377
+ if (!id) return;
1378
+ await store.loadComments(token, id, { force: true });
1379
+ },
1380
+ currentUserId
1381
+ };
1382
+ }
1383
+
1384
+ // src/react/hooks/useAssociationEntitySelect.ts
1385
+ var import_react9 = require("react");
1307
1386
  function useAssociationEntitySelectAdapter(args) {
1308
1387
  const { token, container, activeId, onActiveChange, createColumns } = args;
1309
1388
  const store = useAssociationsStore();
@@ -1317,10 +1396,10 @@ function useAssociationEntitySelectAdapter(args) {
1317
1396
  const { titleFor } = useEntityTitles(
1318
1397
  rows.map((r) => ({ token, id: r.resourceId, label: r.label }))
1319
1398
  );
1320
- const [titleOverrides, setTitleOverrides] = (0, import_react8.useState)(
1399
+ const [titleOverrides, setTitleOverrides] = (0, import_react9.useState)(
1321
1400
  {}
1322
1401
  );
1323
- const [localActiveId, setLocalActiveId] = (0, import_react8.useState)(null);
1402
+ const [localActiveId, setLocalActiveId] = (0, import_react9.useState)(null);
1324
1403
  const items = rows.map((r) => ({
1325
1404
  id: r.resourceId,
1326
1405
  title: titleOverrides[r.resourceId] ?? titleFor({ token, id: r.resourceId, label: r.label })
@@ -1377,9 +1456,9 @@ function useAssociationEntitySelectAdapter(args) {
1377
1456
  }
1378
1457
 
1379
1458
  // src/react/components/PrimaryEntityContext.tsx
1380
- var import_react9 = require("react");
1459
+ var import_react10 = require("react");
1381
1460
  var import_jsx_runtime2 = require("react/jsx-runtime");
1382
- var PrimaryEntityCtx = (0, import_react9.createContext)(null);
1461
+ var PrimaryEntityCtx = (0, import_react10.createContext)(null);
1383
1462
  function PrimaryEntityProvider({
1384
1463
  value,
1385
1464
  children
@@ -1387,11 +1466,11 @@ function PrimaryEntityProvider({
1387
1466
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PrimaryEntityCtx.Provider, { value, children });
1388
1467
  }
1389
1468
  function usePrimaryEntity() {
1390
- return (0, import_react9.useContext)(PrimaryEntityCtx);
1469
+ return (0, import_react10.useContext)(PrimaryEntityCtx);
1391
1470
  }
1392
1471
 
1393
1472
  // src/react/components/AssociationWindow.tsx
1394
- var import_react10 = require("react");
1473
+ var import_react11 = require("react");
1395
1474
  var import_design_system = require("@ai-matrx/design-system");
1396
1475
 
1397
1476
  // src/react/icons.tsx
@@ -1522,7 +1601,7 @@ function AssociationWindow({
1522
1601
  subtitle,
1523
1602
  children
1524
1603
  }) {
1525
- const instanceId = (0, import_react10.useId)();
1604
+ const instanceId = (0, import_react11.useId)();
1526
1605
  const { windowShell } = useAssociationsUiPorts();
1527
1606
  if (!open) return null;
1528
1607
  const body = /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex h-full min-h-0 flex-col gap-2 p-3", children: [
@@ -1574,7 +1653,7 @@ function AssociationWindow({
1574
1653
  }
1575
1654
 
1576
1655
  // src/react/components/AssociationPicker.tsx
1577
- var import_react11 = require("react");
1656
+ var import_react12 = require("react");
1578
1657
  var import_design_system2 = require("@ai-matrx/design-system");
1579
1658
  var import_jsx_runtime5 = require("react/jsx-runtime");
1580
1659
  function AssociationPicker(props) {
@@ -1643,8 +1722,8 @@ function EntityCandidateList({
1643
1722
  }) {
1644
1723
  const store = useAssociationsStore();
1645
1724
  const notifier = useNotifier();
1646
- const [search, setSearch] = (0, import_react11.useState)("");
1647
- const [busyId, setBusyId] = (0, import_react11.useState)(null);
1725
+ const [search, setSearch] = (0, import_react12.useState)("");
1726
+ const [busyId, setBusyId] = (0, import_react12.useState)(null);
1648
1727
  const info = store.registry.getEntityInfo(token);
1649
1728
  const Icon = info.Icon ?? DefaultEntityIcon;
1650
1729
  const { candidates, loading, error, reload } = useAssociationCandidates({
@@ -1750,9 +1829,9 @@ function CreateAndAttachFooter({
1750
1829
  const store = useAssociationsStore();
1751
1830
  const notifier = useNotifier();
1752
1831
  const info = store.registry.getEntityInfo(token);
1753
- const [editing, setEditing] = (0, import_react11.useState)(false);
1754
- const [name, setName] = (0, import_react11.useState)("");
1755
- const [busy, setBusy] = (0, import_react11.useState)(false);
1832
+ const [editing, setEditing] = (0, import_react12.useState)(false);
1833
+ const [name, setName] = (0, import_react12.useState)("");
1834
+ const [busy, setBusy] = (0, import_react12.useState)(false);
1756
1835
  const submit = async () => {
1757
1836
  const title = name.trim();
1758
1837
  if (!title || busy) return;
@@ -1860,7 +1939,7 @@ function ListMessage({ children }) {
1860
1939
  }
1861
1940
 
1862
1941
  // src/react/components/UniversalAssociationPicker.tsx
1863
- var import_react12 = require("react");
1942
+ var import_react13 = require("react");
1864
1943
  var import_design_system3 = require("@ai-matrx/design-system");
1865
1944
  var import_jsx_runtime6 = require("react/jsx-runtime");
1866
1945
  function attachedKey(token, id) {
@@ -1872,9 +1951,9 @@ function UniversalAssociationPicker(props) {
1872
1951
  const { entityDoors, pickerOverrides } = useAssociationsUiPorts();
1873
1952
  const notifier = useNotifier();
1874
1953
  const tokens = props.tokens ?? store.registry.curatedTokens();
1875
- const [query, setQuery] = (0, import_react12.useState)("");
1876
- const [browseToken, setBrowseToken] = (0, import_react12.useState)(null);
1877
- const [busyKey, setBusyKey] = (0, import_react12.useState)(null);
1954
+ const [query, setQuery] = (0, import_react13.useState)("");
1955
+ const [browseToken, setBrowseToken] = (0, import_react13.useState)(null);
1956
+ const [busyKey, setBusyKey] = (0, import_react13.useState)(null);
1878
1957
  const { results, loading, isRecents } = useUniversalEntitySearch({
1879
1958
  query,
1880
1959
  tokens,
@@ -2045,7 +2124,7 @@ function UniversalAssociationPicker(props) {
2045
2124
  }
2046
2125
 
2047
2126
  // src/react/components/AttachedItemsSheet.tsx
2048
- var import_react13 = require("react");
2127
+ var import_react14 = require("react");
2049
2128
  var import_design_system5 = require("@ai-matrx/design-system");
2050
2129
 
2051
2130
  // src/react/components/entityDoors.tsx
@@ -2161,11 +2240,11 @@ function AttachedItemsBody({
2161
2240
  const notifier = useNotifier();
2162
2241
  const info = store.registry.getEntityInfo(token);
2163
2242
  const Icon = info.Icon ?? DefaultEntityIcon;
2164
- const [titles, setTitles] = (0, import_react13.useState)(null);
2165
- const [titleError, setTitleError] = (0, import_react13.useState)(null);
2166
- const [busyId, setBusyId] = (0, import_react13.useState)(null);
2243
+ const [titles, setTitles] = (0, import_react14.useState)(null);
2244
+ const [titleError, setTitleError] = (0, import_react14.useState)(null);
2245
+ const [busyId, setBusyId] = (0, import_react14.useState)(null);
2167
2246
  const idKey = links.map((l) => l.resourceId).sort().join(",");
2168
- (0, import_react13.useEffect)(() => {
2247
+ (0, import_react14.useEffect)(() => {
2169
2248
  if (!enabled) return;
2170
2249
  const ids = idKey ? idKey.split(",") : [];
2171
2250
  if (ids.length === 0) {
@@ -2278,7 +2357,7 @@ function AttachedItemsBody({
2278
2357
  }
2279
2358
 
2280
2359
  // src/react/components/AssociationCard.tsx
2281
- var import_react14 = require("react");
2360
+ var import_react15 = require("react");
2282
2361
  var import_design_system6 = require("@ai-matrx/design-system");
2283
2362
 
2284
2363
  // src/react/contentRoles.ts
@@ -2338,11 +2417,11 @@ function AssociationCard({
2338
2417
  const store = useAssociationsStore();
2339
2418
  const fromCtx = usePrimaryEntity();
2340
2419
  const container = containerProp ?? fromCtx;
2341
- const [open, setOpen] = (0, import_react14.useState)(false);
2420
+ const [open, setOpen] = (0, import_react15.useState)(false);
2342
2421
  const info = store.registry.getEntityInfo(token);
2343
2422
  const role = getContentRoleMeta(info.contentRole);
2344
2423
  const Icon = info.Icon ?? DefaultEntityIcon;
2345
- const [listOpen, setListOpen] = (0, import_react14.useState)(false);
2424
+ const [listOpen, setListOpen] = (0, import_react15.useState)(false);
2346
2425
  const { status, countFor, attachedIdsFor, linksFor, attach, detach } = useContainerLinks({
2347
2426
  containerType: container?.type ?? "organization",
2348
2427
  containerId: container?.id ?? null,
@@ -2496,7 +2575,7 @@ function AssociationCardGrid({
2496
2575
  }
2497
2576
 
2498
2577
  // src/react/components/AssociationList.tsx
2499
- var import_react15 = require("react");
2578
+ var import_react16 = require("react");
2500
2579
  var import_design_system8 = require("@ai-matrx/design-system");
2501
2580
  var import_jsx_runtime11 = require("react/jsx-runtime");
2502
2581
  function useContainerLinksAdapter(container, tokens) {
@@ -2536,9 +2615,9 @@ function AssociationList(props) {
2536
2615
  const adapter = props.adapter ?? defaultAdapter;
2537
2616
  const variant = props.variant ?? "full";
2538
2617
  const tokenFilter = props.tokens ?? null;
2539
- const [pickerToken, setPickerToken] = (0, import_react15.useState)(null);
2540
- const [showUniversal, setShowUniversal] = (0, import_react15.useState)(false);
2541
- const [removingKeys, setRemovingKeys] = (0, import_react15.useState)(/* @__PURE__ */ new Set());
2618
+ const [pickerToken, setPickerToken] = (0, import_react16.useState)(null);
2619
+ const [showUniversal, setShowUniversal] = (0, import_react16.useState)(false);
2620
+ const [removingKeys, setRemovingKeys] = (0, import_react16.useState)(/* @__PURE__ */ new Set());
2542
2621
  const rows = tokenFilter ? adapter.rows.filter((r) => tokenFilter.includes(r.token)) : adapter.rows;
2543
2622
  const visibleRows = rows.filter((r) => !removingKeys.has(r.key));
2544
2623
  const { titleFor } = useEntityTitles(
@@ -2850,7 +2929,7 @@ function titleize(token) {
2850
2929
  }
2851
2930
 
2852
2931
  // src/react/components/AssociationCaptureToolbar.tsx
2853
- var import_react16 = require("react");
2932
+ var import_react17 = require("react");
2854
2933
  var import_design_system9 = require("@ai-matrx/design-system");
2855
2934
  var import_jsx_runtime12 = require("react/jsx-runtime");
2856
2935
  function AssociationCaptureToolbar({
@@ -2870,13 +2949,13 @@ function AssociationCaptureToolbar({
2870
2949
  const store = useAssociationsStore();
2871
2950
  const { capture } = useAssociationsUiPorts();
2872
2951
  const notifier = useNotifier();
2873
- const fileInputRef = (0, import_react16.useRef)(null);
2874
- const [isUploading, setIsUploading] = (0, import_react16.useState)(false);
2875
- const [isDragOver, setIsDragOver] = (0, import_react16.useState)(false);
2876
- const [isPicking, setIsPicking] = (0, import_react16.useState)(false);
2877
- const [namingDoc, setNamingDoc] = (0, import_react16.useState)(false);
2878
- const [docName, setDocName] = (0, import_react16.useState)("");
2879
- const [creatingDoc, setCreatingDoc] = (0, import_react16.useState)(false);
2952
+ const fileInputRef = (0, import_react17.useRef)(null);
2953
+ const [isUploading, setIsUploading] = (0, import_react17.useState)(false);
2954
+ const [isDragOver, setIsDragOver] = (0, import_react17.useState)(false);
2955
+ const [isPicking, setIsPicking] = (0, import_react17.useState)(false);
2956
+ const [namingDoc, setNamingDoc] = (0, import_react17.useState)(false);
2957
+ const [docName, setDocName] = (0, import_react17.useState)("");
2958
+ const [creatingDoc, setCreatingDoc] = (0, import_react17.useState)(false);
2880
2959
  const actions = {
2881
2960
  upload: (showActions?.upload ?? true) && Boolean(capture?.requestUpload),
2882
2961
  addFile: (showActions?.addFile ?? true) && Boolean(capture?.openFilePicker),
@@ -3184,7 +3263,7 @@ function CaptureToolbarAction({
3184
3263
  }
3185
3264
 
3186
3265
  // src/react/components/AssociationEntitySelect.tsx
3187
- var import_react17 = require("react");
3266
+ var import_react18 = require("react");
3188
3267
  var import_design_system10 = require("@ai-matrx/design-system");
3189
3268
  var import_jsx_runtime13 = require("react/jsx-runtime");
3190
3269
  function AssociationEntitySelect({
@@ -3208,11 +3287,11 @@ function AssociationEntitySelect({
3208
3287
  const { items, activeId } = adapter;
3209
3288
  const active = items.find((i) => i.id === activeId) ?? null;
3210
3289
  const activeIndex = active ? items.indexOf(active) : -1;
3211
- const [open, setOpen] = (0, import_react17.useState)(false);
3212
- const [query, setQuery] = (0, import_react17.useState)("");
3213
- const [creating, setCreating] = (0, import_react17.useState)(false);
3214
- const [draftName, setDraftName] = (0, import_react17.useState)("");
3215
- const [busy, setBusy] = (0, import_react17.useState)(false);
3290
+ const [open, setOpen] = (0, import_react18.useState)(false);
3291
+ const [query, setQuery] = (0, import_react18.useState)("");
3292
+ const [creating, setCreating] = (0, import_react18.useState)(false);
3293
+ const [draftName, setDraftName] = (0, import_react18.useState)("");
3294
+ const [busy, setBusy] = (0, import_react18.useState)(false);
3216
3295
  const entityLabel = info.label.toLowerCase();
3217
3296
  const close = () => {
3218
3297
  setOpen(false);
@@ -3435,7 +3514,7 @@ function AssociationEntitySelect({
3435
3514
  }
3436
3515
 
3437
3516
  // src/react/components/CategorySelect.tsx
3438
- var import_react18 = require("react");
3517
+ var import_react19 = require("react");
3439
3518
  var import_design_system11 = require("@ai-matrx/design-system");
3440
3519
 
3441
3520
  // src/core/categoryHierarchy.ts
@@ -3530,7 +3609,7 @@ function CategorySelect({
3530
3609
  reload
3531
3610
  } = useCategories({ dimension });
3532
3611
  const hierarchy = buildCategoryHierarchy(categories);
3533
- const [createParentId, setCreateParentId] = (0, import_react18.useState)(ROOT_PARENT);
3612
+ const [createParentId, setCreateParentId] = (0, import_react19.useState)(ROOT_PARENT);
3534
3613
  const options = [
3535
3614
  ...allowNone ? [{ value: NONE, label: "None", keywords: "none clear empty" }] : [],
3536
3615
  ...hierarchy.hasHierarchy ? hierarchy.items.map(({ category, depth, parent }) => ({
@@ -3647,7 +3726,7 @@ function CategorySelect({
3647
3726
  }
3648
3727
 
3649
3728
  // src/react/components/CategoryTagPicker.tsx
3650
- var import_react19 = require("react");
3729
+ var import_react20 = require("react");
3651
3730
  var import_design_system12 = require("@ai-matrx/design-system");
3652
3731
  var import_jsx_runtime15 = require("react/jsx-runtime");
3653
3732
  var ROOT_PARENT2 = "__root__";
@@ -3665,11 +3744,11 @@ function CategoryTagPicker({
3665
3744
  }) {
3666
3745
  const store = useAssociationsStore();
3667
3746
  const notifier = useNotifier();
3668
- const [open, setOpen] = (0, import_react19.useState)(false);
3669
- const [creating, setCreating] = (0, import_react19.useState)(false);
3670
- const [writing, setWriting] = (0, import_react19.useState)(false);
3671
- const [search, setSearch] = (0, import_react19.useState)("");
3672
- const [createParentId, setCreateParentId] = (0, import_react19.useState)(ROOT_PARENT2);
3747
+ const [open, setOpen] = (0, import_react20.useState)(false);
3748
+ const [creating, setCreating] = (0, import_react20.useState)(false);
3749
+ const [writing, setWriting] = (0, import_react20.useState)(false);
3750
+ const [search, setSearch] = (0, import_react20.useState)("");
3751
+ const [createParentId, setCreateParentId] = (0, import_react20.useState)(ROOT_PARENT2);
3673
3752
  const {
3674
3753
  categories,
3675
3754
  create: createCategory,
@@ -3902,4 +3981,409 @@ function CategoryTagPicker({
3902
3981
  ] })
3903
3982
  ] });
3904
3983
  }
3984
+
3985
+ // src/react/components/CommentThread.tsx
3986
+ var import_react21 = require("react");
3987
+ var import_design_system13 = require("@ai-matrx/design-system");
3988
+
3989
+ // src/react/relativeTime.ts
3990
+ var UNITS = [
3991
+ { unit: "year", ms: 365 * 24 * 60 * 60 * 1e3 },
3992
+ { unit: "month", ms: 30 * 24 * 60 * 60 * 1e3 },
3993
+ { unit: "week", ms: 7 * 24 * 60 * 60 * 1e3 },
3994
+ { unit: "day", ms: 24 * 60 * 60 * 1e3 },
3995
+ { unit: "hour", ms: 60 * 60 * 1e3 },
3996
+ { unit: "minute", ms: 60 * 1e3 }
3997
+ ];
3998
+ function formatRelativeTime(iso, now = Date.now()) {
3999
+ const t = Date.parse(iso);
4000
+ if (Number.isNaN(t)) return iso;
4001
+ const delta = t - now;
4002
+ const magnitude = Math.abs(delta);
4003
+ if (magnitude < 60 * 1e3) return "just now";
4004
+ const rtf = new Intl.RelativeTimeFormat(void 0, { numeric: "auto" });
4005
+ for (const { unit, ms } of UNITS) {
4006
+ if (magnitude >= ms) {
4007
+ return rtf.format(Math.trunc(delta / ms), unit);
4008
+ }
4009
+ }
4010
+ return "just now";
4011
+ }
4012
+
4013
+ // src/react/components/CommentThread.tsx
4014
+ var import_jsx_runtime16 = require("react/jsx-runtime");
4015
+ var AUTHOR_DOOR_TOKEN = "user";
4016
+ function resolveAuthor(comment, authorDisplay) {
4017
+ const base2 = {
4018
+ userId: comment.createdBy,
4019
+ email: comment.author.email,
4020
+ displayName: comment.author.displayName,
4021
+ avatarUrl: comment.author.avatarUrl
4022
+ };
4023
+ const enriched = authorDisplay ? authorDisplay(base2) : null;
4024
+ const name = enriched?.displayName ?? base2.displayName ?? base2.email ?? "Unknown user";
4025
+ return {
4026
+ name,
4027
+ avatarUrl: enriched?.avatarUrl !== void 0 && enriched?.avatarUrl !== null ? enriched.avatarUrl : base2.avatarUrl,
4028
+ userId: comment.createdBy
4029
+ };
4030
+ }
4031
+ function AuthorAvatar({ author }) {
4032
+ if (author.avatarUrl) {
4033
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4034
+ "img",
4035
+ {
4036
+ src: author.avatarUrl,
4037
+ alt: "",
4038
+ className: "h-6 w-6 shrink-0 rounded-full object-cover"
4039
+ }
4040
+ );
4041
+ }
4042
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4043
+ "span",
4044
+ {
4045
+ "aria-hidden": true,
4046
+ className: "flex h-6 w-6 shrink-0 select-none items-center justify-center rounded-full bg-muted text-[11px] font-medium uppercase text-muted-foreground",
4047
+ children: author.name.trim().charAt(0) || "?"
4048
+ }
4049
+ );
4050
+ }
4051
+ function AuthorName({
4052
+ author,
4053
+ entityDoors
4054
+ }) {
4055
+ const DoorRef2 = entityDoors?.EntityRef;
4056
+ if (DoorRef2 && author.userId) {
4057
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4058
+ DoorRef2,
4059
+ {
4060
+ token: AUTHOR_DOOR_TOKEN,
4061
+ id: author.userId,
4062
+ name: author.name,
4063
+ showIcon: false,
4064
+ className: "text-sm font-medium text-foreground"
4065
+ }
4066
+ );
4067
+ }
4068
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-sm font-medium text-foreground", children: author.name });
4069
+ }
4070
+ function CommentComposer({
4071
+ onSubmit,
4072
+ placeholder = "Write a comment\u2026",
4073
+ submitLabel = "Comment",
4074
+ initialValue = "",
4075
+ trailing,
4076
+ autoFocus = false
4077
+ }) {
4078
+ const notifier = useNotifier();
4079
+ const [body, setBody] = (0, import_react21.useState)(initialValue);
4080
+ const [submitting, setSubmitting] = (0, import_react21.useState)(false);
4081
+ const [error, setError] = (0, import_react21.useState)(null);
4082
+ async function submit(e) {
4083
+ e?.preventDefault();
4084
+ const trimmed = body.trim();
4085
+ if (!trimmed || submitting) return;
4086
+ setSubmitting(true);
4087
+ setError(null);
4088
+ const res = await onSubmit(trimmed);
4089
+ setSubmitting(false);
4090
+ if (!res.ok) {
4091
+ const msg = res.error ?? "Could not save the comment";
4092
+ setError(msg);
4093
+ notifier.error(msg);
4094
+ return;
4095
+ }
4096
+ setBody("");
4097
+ }
4098
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("form", { onSubmit: submit, className: "flex flex-col gap-1.5", children: [
4099
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4100
+ "textarea",
4101
+ {
4102
+ value: body,
4103
+ onChange: (e) => setBody(e.target.value),
4104
+ onKeyDown: (e) => {
4105
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) void submit();
4106
+ },
4107
+ placeholder,
4108
+ rows: 2,
4109
+ autoFocus,
4110
+ "aria-label": placeholder,
4111
+ className: "w-full resize-y rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
4112
+ }
4113
+ ),
4114
+ error && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { role: "alert", className: "text-xs text-destructive", children: error }),
4115
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex items-center gap-2", children: [
4116
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_design_system13.Button, { type: "submit", size: "sm", disabled: !body.trim() || submitting, children: [
4117
+ submitting && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(SpinnerIcon, { className: "mr-1 h-3 w-3 animate-spin" }),
4118
+ submitLabel
4119
+ ] }),
4120
+ trailing
4121
+ ] })
4122
+ ] });
4123
+ }
4124
+ function CommentItem({
4125
+ comment,
4126
+ childrenOf,
4127
+ depth,
4128
+ thread,
4129
+ orgId
4130
+ }) {
4131
+ const { authorDisplay, entityDoors } = useAssociationsUiPorts();
4132
+ const notifier = useNotifier();
4133
+ const [replying, setReplying] = (0, import_react21.useState)(false);
4134
+ const [editing, setEditing] = (0, import_react21.useState)(false);
4135
+ const [confirmingDelete, setConfirmingDelete] = (0, import_react21.useState)(false);
4136
+ const [deleting, setDeleting] = (0, import_react21.useState)(false);
4137
+ const author = resolveAuthor(comment, authorDisplay);
4138
+ const isOwn = thread.currentUserId !== null && comment.createdBy === thread.currentUserId;
4139
+ const edited = comment.updatedAt !== comment.createdAt;
4140
+ const replies = childrenOf.get(comment.id) ?? [];
4141
+ async function confirmDelete() {
4142
+ setDeleting(true);
4143
+ const res = await thread.remove(comment.id);
4144
+ setDeleting(false);
4145
+ setConfirmingDelete(false);
4146
+ if (!res.ok) {
4147
+ notifier.error(res.error ?? "Could not delete the comment");
4148
+ }
4149
+ }
4150
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
4151
+ "div",
4152
+ {
4153
+ "data-comment-id": comment.id,
4154
+ className: (0, import_design_system13.cn)(
4155
+ "flex flex-col gap-1",
4156
+ depth > 0 && "border-l border-border pl-3"
4157
+ ),
4158
+ children: [
4159
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex items-center gap-2", children: [
4160
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(AuthorAvatar, { author }),
4161
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(AuthorName, { author, entityDoors }),
4162
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4163
+ "span",
4164
+ {
4165
+ className: "text-xs text-muted-foreground",
4166
+ title: comment.createdAt,
4167
+ children: formatRelativeTime(comment.createdAt)
4168
+ }
4169
+ ),
4170
+ edited && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4171
+ "span",
4172
+ {
4173
+ className: "text-xs text-muted-foreground",
4174
+ title: comment.updatedAt,
4175
+ children: "(edited)"
4176
+ }
4177
+ )
4178
+ ] }),
4179
+ editing ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "pl-8", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4180
+ CommentComposer,
4181
+ {
4182
+ initialValue: comment.body,
4183
+ submitLabel: "Save",
4184
+ placeholder: "Edit your comment\u2026",
4185
+ autoFocus: true,
4186
+ onSubmit: async (body) => {
4187
+ const res = await thread.edit(comment.id, body);
4188
+ if (res.ok) setEditing(false);
4189
+ return res;
4190
+ },
4191
+ trailing: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4192
+ import_design_system13.Button,
4193
+ {
4194
+ type: "button",
4195
+ size: "sm",
4196
+ variant: "ghost",
4197
+ onClick: () => setEditing(false),
4198
+ children: "Cancel"
4199
+ }
4200
+ )
4201
+ }
4202
+ ) }) : /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { className: "whitespace-pre-wrap pl-8 text-sm text-foreground", children: comment.body }),
4203
+ !editing && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "flex items-center gap-1 pl-7", children: confirmingDelete ? /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
4204
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-xs text-destructive", children: "Deletes this comment for everyone." }),
4205
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
4206
+ import_design_system13.Button,
4207
+ {
4208
+ type: "button",
4209
+ size: "sm",
4210
+ variant: "destructive",
4211
+ disabled: deleting,
4212
+ onClick: () => void confirmDelete(),
4213
+ children: [
4214
+ deleting && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(SpinnerIcon, { className: "mr-1 h-3 w-3 animate-spin" }),
4215
+ "Delete"
4216
+ ]
4217
+ }
4218
+ ),
4219
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4220
+ import_design_system13.Button,
4221
+ {
4222
+ type: "button",
4223
+ size: "sm",
4224
+ variant: "ghost",
4225
+ disabled: deleting,
4226
+ onClick: () => setConfirmingDelete(false),
4227
+ children: "Cancel"
4228
+ }
4229
+ )
4230
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
4231
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4232
+ "button",
4233
+ {
4234
+ type: "button",
4235
+ onClick: () => setReplying((v) => !v),
4236
+ className: "rounded px-1.5 py-0.5 text-xs text-muted-foreground transition-colors hover:bg-accent hover:text-foreground",
4237
+ children: "Reply"
4238
+ }
4239
+ ),
4240
+ isOwn && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
4241
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4242
+ "button",
4243
+ {
4244
+ type: "button",
4245
+ onClick: () => setEditing(true),
4246
+ className: "rounded px-1.5 py-0.5 text-xs text-muted-foreground transition-colors hover:bg-accent hover:text-foreground",
4247
+ children: "Edit"
4248
+ }
4249
+ ),
4250
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4251
+ "button",
4252
+ {
4253
+ type: "button",
4254
+ onClick: () => setConfirmingDelete(true),
4255
+ className: "rounded px-1.5 py-0.5 text-xs text-muted-foreground transition-colors hover:bg-accent hover:text-destructive",
4256
+ children: "Delete"
4257
+ }
4258
+ )
4259
+ ] })
4260
+ ] }) }),
4261
+ replying && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex items-start gap-2 pl-8 pt-1", children: [
4262
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(CornerDownRightIcon, { className: "mt-2 h-3.5 w-3.5 shrink-0 text-muted-foreground" }),
4263
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4264
+ CommentComposer,
4265
+ {
4266
+ submitLabel: "Reply",
4267
+ placeholder: `Reply to ${author.name}\u2026`,
4268
+ autoFocus: true,
4269
+ onSubmit: async (body) => {
4270
+ const res = await thread.add(body, {
4271
+ parentId: comment.id,
4272
+ orgId: orgId ?? null
4273
+ });
4274
+ if (res.ok) setReplying(false);
4275
+ return res;
4276
+ },
4277
+ trailing: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4278
+ import_design_system13.Button,
4279
+ {
4280
+ type: "button",
4281
+ size: "sm",
4282
+ variant: "ghost",
4283
+ onClick: () => setReplying(false),
4284
+ children: "Cancel"
4285
+ }
4286
+ )
4287
+ }
4288
+ ) })
4289
+ ] }),
4290
+ replies.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "flex flex-col gap-3 pl-8 pt-1", children: replies.map((reply) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4291
+ CommentItem,
4292
+ {
4293
+ comment: reply,
4294
+ childrenOf,
4295
+ depth: depth + 1,
4296
+ thread,
4297
+ orgId
4298
+ },
4299
+ reply.id
4300
+ )) })
4301
+ ]
4302
+ }
4303
+ );
4304
+ }
4305
+ function buildCommentTree(comments) {
4306
+ const byId = new Set(comments.map((c) => c.id));
4307
+ const roots = [];
4308
+ const childrenOf = /* @__PURE__ */ new Map();
4309
+ for (const c of comments) {
4310
+ if (c.parentId && byId.has(c.parentId)) {
4311
+ const bucket = childrenOf.get(c.parentId);
4312
+ if (bucket) bucket.push(c);
4313
+ else childrenOf.set(c.parentId, [c]);
4314
+ } else {
4315
+ roots.push(c);
4316
+ }
4317
+ }
4318
+ return { roots, childrenOf };
4319
+ }
4320
+ function CommentThread({
4321
+ token,
4322
+ id,
4323
+ orgId,
4324
+ showHeader = true,
4325
+ className
4326
+ }) {
4327
+ const thread = useComments({ token, id });
4328
+ const { roots, childrenOf } = buildCommentTree(thread.comments);
4329
+ const loading = (thread.status === "loading" || thread.status === "idle") && thread.comments.length === 0;
4330
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("section", { className: (0, import_design_system13.cn)("flex flex-col gap-3", className), children: [
4331
+ showHeader && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("header", { className: "flex items-center gap-2", children: [
4332
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("h3", { className: "text-sm font-semibold text-foreground", children: "Comments" }),
4333
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-xs text-muted-foreground tabular-nums", children: thread.comments.length }),
4334
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4335
+ "button",
4336
+ {
4337
+ type: "button",
4338
+ onClick: () => void thread.reload(),
4339
+ "aria-label": "Refresh comments",
4340
+ title: "Refresh comments",
4341
+ className: "ml-auto flex h-6 w-6 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-foreground",
4342
+ children: thread.status === "loading" ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(SpinnerIcon, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(RefreshIcon, { className: "h-3.5 w-3.5" })
4343
+ }
4344
+ )
4345
+ ] }),
4346
+ thread.error && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
4347
+ "div",
4348
+ {
4349
+ role: "alert",
4350
+ className: "flex items-center gap-2 rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-xs text-destructive",
4351
+ children: [
4352
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "min-w-0 flex-1", children: thread.error }),
4353
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4354
+ import_design_system13.Button,
4355
+ {
4356
+ type: "button",
4357
+ size: "sm",
4358
+ variant: "outline",
4359
+ disabled: thread.status === "loading",
4360
+ onClick: () => void thread.reload(),
4361
+ children: "Retry"
4362
+ }
4363
+ )
4364
+ ]
4365
+ }
4366
+ ),
4367
+ loading ? /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex flex-col gap-3", "aria-hidden": true, "data-testid": "comments-loading", children: [
4368
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_design_system13.Skeleton, { className: "h-12 w-full" }),
4369
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_design_system13.Skeleton, { className: "h-12 w-4/5" })
4370
+ ] }) : roots.length === 0 && !thread.error ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { className: "text-sm text-muted-foreground", children: "No comments yet." }) : /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "flex flex-col gap-4", children: roots.map((comment) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4371
+ CommentItem,
4372
+ {
4373
+ comment,
4374
+ childrenOf,
4375
+ depth: 0,
4376
+ thread,
4377
+ orgId
4378
+ },
4379
+ comment.id
4380
+ )) }),
4381
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4382
+ CommentComposer,
4383
+ {
4384
+ onSubmit: (body) => thread.add(body, { orgId: orgId ?? null })
4385
+ }
4386
+ )
4387
+ ] });
4388
+ }
3905
4389
  //# sourceMappingURL=index.cjs.map