@burdenoff/planmagnet-fe-sdk 2026.705.2 → 2026.706.1

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.
package/dist/index.d.ts CHANGED
@@ -881,22 +881,25 @@ interface TaskCommentsCoreProps {
881
881
  entityId: string;
882
882
  /** The current actor — enables "You" + delete on own comments. */
883
883
  currentActorId?: string;
884
- /** Resolve an author id → display name (reuse the member directory). */
884
+ /** Resolve an author/mention id → display name (reuse the member directory). */
885
885
  resolveAssignee?: ResolveAssignee;
886
+ /** Member directory for @-mention autocomplete. Omit to disable mentions. */
887
+ members?: AssigneeOption[];
886
888
  /** Hide the composer + delete affordances (pure read-only). */
887
889
  readonly?: boolean;
888
890
  /** Extra classes on the root container. */
889
891
  className?: string;
890
892
  }
891
893
  /**
892
- * TaskCommentsCore — a comment thread on a work item, backed by the SHARED
893
- * wspace-conversations-svc `EntityComment` (the platform's canonical comment
894
- * owner) rather than a per-product comment table. BYO-Apollo, semantic-token
894
+ * TaskCommentsCore — a threaded comment thread on a work item, backed by the
895
+ * SHARED wspace-conversations-svc `EntityComment` (the platform's canonical
896
+ * comment owner) rather than a per-product comment table. Supports 1-level
897
+ * replies (grouped by thread root) and @-mentions. BYO-Apollo, semantic-token
895
898
  * only, farm-free. RBAC is enforced server-side (create/read are editor-level,
896
899
  * a workspace Member gets editor on `entity-comment`); the composer fails open
897
- * and shows a graceful error if the server denies it.
900
+ * and surfaces a graceful error if the server denies it.
898
901
  */
899
- declare function TaskCommentsCore({ entityType, entityId, currentActorId, resolveAssignee, readonly, className, }: TaskCommentsCoreProps): react.JSX.Element;
902
+ declare function TaskCommentsCore({ entityType, entityId, currentActorId, resolveAssignee, members, readonly, className, }: TaskCommentsCoreProps): react.JSX.Element;
900
903
 
901
904
  /**
902
905
  * TaskLabels — every user-facing string the SDK renders, so a host (e.g.
@@ -969,6 +972,9 @@ interface TaskLabels {
969
972
  editedTag: string;
970
973
  failedToComment: string;
971
974
  failedToDeleteComment: string;
975
+ reply: string;
976
+ replyPlaceholder: string;
977
+ repliesCount: string;
972
978
  create: string;
973
979
  creating: string;
974
980
  cancel: string;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createContext, useMemo, useEffect, useContext, useState, useCallback } from 'react';
1
+ import { createContext, useMemo, useEffect, useContext, useState, useCallback, useRef } from 'react';
2
2
  import { ApolloProvider, useQuery, useApolloClient, useMutation } from '@apollo/client/react';
3
3
  import { createHttpLink, ApolloClient, from, InMemoryCache } from '@apollo/client/core';
4
4
  import { setContext } from '@apollo/client/link/context';
@@ -524,6 +524,12 @@ var ENTITY_COMMENT_FIELDS = gql`
524
524
  edited
525
525
  editedAt
526
526
  status
527
+ parentCommentId
528
+ threadRootId
529
+ threadLevel
530
+ mentions {
531
+ mentionedUserId
532
+ }
527
533
  }
528
534
  `;
529
535
  var GET_ENTITY_COMMENTS = gql`
@@ -798,6 +804,9 @@ var DEFAULT_TASK_LABELS = {
798
804
  editedTag: "(edited)",
799
805
  failedToComment: "Failed to post comment",
800
806
  failedToDeleteComment: "Failed to delete comment",
807
+ reply: "Reply",
808
+ replyPlaceholder: "Write a reply\u2026",
809
+ repliesCount: "{n} replies",
801
810
  create: "Create task",
802
811
  creating: "Creating\u2026",
803
812
  cancel: "Cancel",
@@ -2501,27 +2510,134 @@ function TaskToolbar({
2501
2510
  }
2502
2511
  );
2503
2512
  }
2504
- var labelClass2 = "text-xs font-medium uppercase tracking-wide text-muted-foreground";
2505
2513
  var fieldClass2 = "w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring";
2514
+ function CommentComposer({
2515
+ placeholder,
2516
+ submitLabel,
2517
+ pendingLabel,
2518
+ members,
2519
+ autoFocus,
2520
+ onSubmit
2521
+ }) {
2522
+ const [text, setText] = useState("");
2523
+ const [pending, setPending] = useState(false);
2524
+ const [mentioned, setMentioned] = useState([]);
2525
+ const [query, setQuery] = useState(null);
2526
+ const taRef = useRef(null);
2527
+ const suggestions = useMemo(() => {
2528
+ if (query == null || !members || members.length === 0) return [];
2529
+ const q = query.toLowerCase();
2530
+ return members.filter((m) => m.name.toLowerCase().includes(q)).slice(0, 6);
2531
+ }, [query, members]);
2532
+ const detectMention = (value) => {
2533
+ const caret = taRef.current?.selectionStart ?? value.length;
2534
+ const m = /(?:^|\s)@([\p{L}\p{N} ._-]*)$/u.exec(value.slice(0, caret));
2535
+ setQuery(m ? m[1] : null);
2536
+ };
2537
+ const onChange = (value) => {
2538
+ setText(value);
2539
+ detectMention(value);
2540
+ };
2541
+ const pickMention = (member) => {
2542
+ const el = taRef.current;
2543
+ const caret = el?.selectionStart ?? text.length;
2544
+ const before = text.slice(0, caret);
2545
+ const after = text.slice(caret);
2546
+ const replaced = before.replace(
2547
+ /@([\p{L}\p{N} ._-]*)$/u,
2548
+ `@${member.name} `
2549
+ );
2550
+ setText(replaced + after);
2551
+ setQuery(null);
2552
+ setMentioned(
2553
+ (prev) => prev.some((p) => p.id === member.id) ? prev : [...prev, member]
2554
+ );
2555
+ requestAnimationFrame(() => el?.focus());
2556
+ };
2557
+ const submit = async (e) => {
2558
+ e.preventDefault();
2559
+ const body = text.trim();
2560
+ if (!body || pending) return;
2561
+ const ids = mentioned.filter((m) => body.includes(`@${m.name}`)).map((m) => m.id);
2562
+ setPending(true);
2563
+ try {
2564
+ await onSubmit({ text: body, mentionUserIds: ids });
2565
+ setText("");
2566
+ setMentioned([]);
2567
+ setQuery(null);
2568
+ } finally {
2569
+ setPending(false);
2570
+ }
2571
+ };
2572
+ return /* @__PURE__ */ jsxs("form", { onSubmit: submit, className: "relative space-y-2", children: [
2573
+ /* @__PURE__ */ jsx(
2574
+ "textarea",
2575
+ {
2576
+ ref: taRef,
2577
+ className: cn(fieldClass2, "min-h-[60px]"),
2578
+ rows: 2,
2579
+ value: text,
2580
+ autoFocus,
2581
+ onChange: (e) => onChange(e.target.value),
2582
+ onKeyUp: (e) => detectMention(e.target.value),
2583
+ onClick: (e) => detectMention(e.target.value),
2584
+ placeholder,
2585
+ "aria-label": placeholder
2586
+ }
2587
+ ),
2588
+ suggestions.length > 0 && /* @__PURE__ */ jsx("ul", { className: "absolute left-0 top-full z-20 mt-1 max-h-48 w-56 overflow-auto rounded-md border border-border bg-popover p-1 shadow-md", children: suggestions.map((m) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
2589
+ "button",
2590
+ {
2591
+ type: "button",
2592
+ onMouseDown: (e) => {
2593
+ e.preventDefault();
2594
+ pickMention(m);
2595
+ },
2596
+ className: "flex w-full items-center rounded px-2 py-1 text-left text-sm text-foreground hover:bg-muted",
2597
+ children: [
2598
+ "@",
2599
+ m.name
2600
+ ]
2601
+ }
2602
+ ) }, m.id)) }),
2603
+ /* @__PURE__ */ jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx(
2604
+ "button",
2605
+ {
2606
+ type: "submit",
2607
+ disabled: pending || !text.trim(),
2608
+ className: cn(
2609
+ "rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground",
2610
+ (pending || !text.trim()) && "cursor-not-allowed opacity-50"
2611
+ ),
2612
+ children: pending ? pendingLabel : submitLabel
2613
+ }
2614
+ ) })
2615
+ ] });
2616
+ }
2617
+ var labelClass2 = "text-xs font-medium uppercase tracking-wide text-muted-foreground";
2618
+ var escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2506
2619
  function TaskCommentsCore({
2507
2620
  entityType,
2508
2621
  entityId,
2509
2622
  currentActorId,
2510
2623
  resolveAssignee,
2624
+ members,
2511
2625
  readonly,
2512
2626
  className
2513
2627
  }) {
2514
2628
  const L = useTaskLabels();
2515
- const [text, setText] = useState("");
2516
2629
  const [error, setError] = useState(null);
2630
+ const [replyingTo, setReplyingTo] = useState(null);
2517
2631
  const { data, loading, refetch } = useQuery(
2518
2632
  GET_ENTITY_COMMENTS,
2519
2633
  {
2634
+ // No parentCommentId → the service returns EVERY comment (all levels)
2635
+ // flat; we build the thread tree client-side via threadRootId.
2520
2636
  variables: {
2521
2637
  input: {
2522
2638
  entityType,
2523
2639
  entityId,
2524
- take: 100,
2640
+ take: 200,
2525
2641
  sortBy: "createdAt",
2526
2642
  sortOrder: "asc"
2527
2643
  }
@@ -2530,16 +2646,30 @@ function TaskCommentsCore({
2530
2646
  errorPolicy: "all"
2531
2647
  }
2532
2648
  );
2533
- const comments = data?.getEntityComments?.items ?? [];
2534
- const [createComment, { loading: posting }] = useMutation(CREATE_ENTITY_COMMENT);
2649
+ const all = data?.getEntityComments?.items ?? [];
2650
+ const { topLevel, repliesByRoot } = useMemo(() => {
2651
+ const top = [];
2652
+ const map = /* @__PURE__ */ new Map();
2653
+ for (const c of all) {
2654
+ if (!c.parentCommentId) {
2655
+ top.push(c);
2656
+ } else {
2657
+ const root = c.threadRootId ?? c.parentCommentId;
2658
+ const arr = map.get(root);
2659
+ if (arr) arr.push(c);
2660
+ else map.set(root, [c]);
2661
+ }
2662
+ }
2663
+ return { topLevel: top, repliesByRoot: map };
2664
+ }, [all]);
2665
+ const [createComment] = useMutation(
2666
+ CREATE_ENTITY_COMMENT
2667
+ );
2535
2668
  const [deleteComment] = useMutation(
2536
2669
  DELETE_ENTITY_COMMENT
2537
2670
  );
2538
2671
  const authorName = (id) => id === currentActorId ? L.you : resolveAssignee?.(id)?.name ?? void 0;
2539
- const handlePost = async (e) => {
2540
- e.preventDefault();
2541
- const body = text.trim();
2542
- if (!body) return;
2672
+ const post = async ({ text, mentionUserIds }, parentCommentId) => {
2543
2673
  setError(null);
2544
2674
  try {
2545
2675
  await createComment({
@@ -2547,15 +2677,18 @@ function TaskCommentsCore({
2547
2677
  input: {
2548
2678
  entityType,
2549
2679
  entityId,
2550
- content: { text: body },
2551
- contentType: "TEXT"
2680
+ content: { text },
2681
+ contentType: "TEXT",
2682
+ ...parentCommentId ? { parentCommentId } : {},
2683
+ ...mentionUserIds.length ? { mentionUserIds } : {}
2552
2684
  }
2553
2685
  }
2554
2686
  });
2555
- setText("");
2556
2687
  await refetch();
2557
- } catch {
2688
+ setReplyingTo(null);
2689
+ } catch (err) {
2558
2690
  setError(L.failedToComment);
2691
+ throw err;
2559
2692
  }
2560
2693
  };
2561
2694
  const handleDelete = async (id) => {
@@ -2567,60 +2700,89 @@ function TaskCommentsCore({
2567
2700
  setError(L.failedToDeleteComment);
2568
2701
  }
2569
2702
  };
2570
- return /* @__PURE__ */ jsxs("div", { className: cn("space-y-3", className), children: [
2571
- /* @__PURE__ */ jsxs("div", { className: labelClass2, children: [
2572
- L.commentsLabel,
2573
- comments.length > 0 ? ` (${comments.length})` : ""
2574
- ] }),
2575
- /* @__PURE__ */ jsx("ul", { className: "space-y-3", children: comments.length === 0 && !loading ? /* @__PURE__ */ jsx("li", { className: "text-sm text-muted-foreground", children: L.noComments }) : comments.map((c) => /* @__PURE__ */ jsxs("li", { className: "flex gap-2", children: [
2576
- /* @__PURE__ */ jsx(Avatar, { id: c.authorId, name: authorName(c.authorId), size: "sm" }),
2577
- /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
2578
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
2579
- /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-foreground", children: authorName(c.authorId) ?? c.authorId.slice(0, 8) }),
2580
- /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: formatDateFull(c.createdAt) }),
2581
- c.edited && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: L.editedTag }),
2582
- !readonly && c.authorId === currentActorId && /* @__PURE__ */ jsx(
2583
- "button",
2584
- {
2585
- type: "button",
2586
- onClick: () => handleDelete(c.id),
2587
- "aria-label": L.del,
2588
- title: L.del,
2589
- className: "ml-auto rounded p-1 text-muted-foreground hover:text-destructive",
2590
- children: /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5" })
2591
- }
2592
- )
2593
- ] }),
2594
- /* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap break-words text-sm text-foreground", children: c.content?.text })
2595
- ] })
2596
- ] }, c.id)) }),
2597
- error && /* @__PURE__ */ jsx("p", { className: "text-sm text-destructive", children: error }),
2598
- !readonly && /* @__PURE__ */ jsxs("form", { onSubmit: handlePost, className: "space-y-2", children: [
2599
- /* @__PURE__ */ jsx(
2600
- "textarea",
2703
+ const renderBody = (c) => {
2704
+ const text = c.content?.text ?? "";
2705
+ const names = (c.mentions ?? []).map((m) => resolveAssignee?.(m.mentionedUserId)?.name).filter((n) => Boolean(n));
2706
+ if (names.length === 0) return text;
2707
+ const tokens = new Set(names.map((n) => `@${n}`));
2708
+ const re = new RegExp(`(@(?:${names.map(escapeRe).join("|")}))`, "g");
2709
+ return text.split(re).map(
2710
+ (part, i) => tokens.has(part) ? /* @__PURE__ */ jsx(
2711
+ "span",
2601
2712
  {
2602
- className: cn(fieldClass2, "min-h-[60px]"),
2603
- rows: 2,
2604
- value: text,
2605
- onChange: (e) => setText(e.target.value),
2606
- placeholder: L.commentPlaceholder,
2607
- "aria-label": L.commentsLabel
2608
- }
2609
- ),
2610
- /* @__PURE__ */ jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx(
2713
+ className: "rounded bg-primary/10 px-0.5 font-medium text-primary",
2714
+ children: part
2715
+ },
2716
+ i
2717
+ ) : /* @__PURE__ */ jsx("span", { children: part }, i)
2718
+ );
2719
+ };
2720
+ const commentBlock = (c, isReply) => /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
2721
+ /* @__PURE__ */ jsx(Avatar, { id: c.authorId, name: authorName(c.authorId), size: "sm" }),
2722
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
2723
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
2724
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-foreground", children: authorName(c.authorId) ?? c.authorId.slice(0, 8) }),
2725
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: formatDateFull(c.createdAt) }),
2726
+ c.edited && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: L.editedTag }),
2727
+ !readonly && c.authorId === currentActorId && /* @__PURE__ */ jsx(
2728
+ "button",
2729
+ {
2730
+ type: "button",
2731
+ onClick: () => handleDelete(c.id),
2732
+ "aria-label": L.del,
2733
+ title: L.del,
2734
+ className: "ml-auto rounded p-1 text-muted-foreground hover:text-destructive",
2735
+ children: /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5" })
2736
+ }
2737
+ )
2738
+ ] }),
2739
+ /* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap break-words text-sm text-foreground", children: renderBody(c) }),
2740
+ !isReply && !readonly && /* @__PURE__ */ jsx(
2611
2741
  "button",
2612
2742
  {
2613
- type: "submit",
2614
- disabled: posting || !text.trim(),
2615
- className: cn(
2616
- "rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground",
2617
- (posting || !text.trim()) && "cursor-not-allowed opacity-50"
2618
- ),
2619
- children: posting ? L.posting : L.postComment
2743
+ type: "button",
2744
+ onClick: () => setReplyingTo((prev) => prev === c.id ? null : c.id),
2745
+ className: "mt-1 text-xs font-medium text-muted-foreground hover:text-foreground",
2746
+ children: L.reply
2620
2747
  }
2621
- ) })
2748
+ )
2622
2749
  ] })
2623
2750
  ] });
2751
+ return /* @__PURE__ */ jsxs("div", { className: cn("space-y-4", className), children: [
2752
+ /* @__PURE__ */ jsxs("div", { className: labelClass2, children: [
2753
+ L.commentsLabel,
2754
+ all.length > 0 ? ` (${all.length})` : ""
2755
+ ] }),
2756
+ /* @__PURE__ */ jsx("ul", { className: "space-y-4", children: topLevel.length === 0 && !loading ? /* @__PURE__ */ jsx("li", { className: "text-sm text-muted-foreground", children: L.noComments }) : topLevel.map((c) => {
2757
+ const replies = repliesByRoot.get(c.id) ?? [];
2758
+ return /* @__PURE__ */ jsxs("li", { className: "space-y-2", children: [
2759
+ commentBlock(c, false),
2760
+ replies.length > 0 && /* @__PURE__ */ jsx("ul", { className: "ml-8 space-y-2 border-l border-border pl-3", children: replies.map((r) => /* @__PURE__ */ jsx("li", { children: commentBlock(r, true) }, r.id)) }),
2761
+ !readonly && replyingTo === c.id && /* @__PURE__ */ jsx("div", { className: "ml-8", children: /* @__PURE__ */ jsx(
2762
+ CommentComposer,
2763
+ {
2764
+ placeholder: L.replyPlaceholder,
2765
+ submitLabel: L.reply,
2766
+ pendingLabel: L.posting,
2767
+ members,
2768
+ autoFocus: true,
2769
+ onSubmit: (p) => post(p, c.id)
2770
+ }
2771
+ ) })
2772
+ ] }, c.id);
2773
+ }) }),
2774
+ error && /* @__PURE__ */ jsx("p", { className: "text-sm text-destructive", children: error }),
2775
+ !readonly && /* @__PURE__ */ jsx(
2776
+ CommentComposer,
2777
+ {
2778
+ placeholder: L.commentPlaceholder,
2779
+ submitLabel: L.postComment,
2780
+ pendingLabel: L.posting,
2781
+ members,
2782
+ onSubmit: (p) => post(p)
2783
+ }
2784
+ )
2785
+ ] });
2624
2786
  }
2625
2787
  var fieldClass3 = "w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring";
2626
2788
  var labelClass3 = "text-xs font-medium uppercase tracking-wide text-muted-foreground";
@@ -2947,7 +3109,8 @@ function TaskDetailCore({
2947
3109
  entityType: commentsEntityType,
2948
3110
  entityId: item.id,
2949
3111
  currentActorId,
2950
- resolveAssignee
3112
+ resolveAssignee,
3113
+ members: assigneeOptions
2951
3114
  }
2952
3115
  ) })
2953
3116
  ] }) : /* @__PURE__ */ jsxs("form", { onSubmit: handleSave, className: "space-y-4", children: [