From 3b59bce9731a34d8a74899059be957f5e6f505ed Mon Sep 17 00:00:00 2001 From: "arthur.bassi" Date: Wed, 5 Aug 2026 11:55:39 -0300 Subject: [PATCH] fix(work-orders): harden comment mentions and clear draft on success --- .../view/work-order-activity-panel.tsx | 7 +++-- .../_hooks/use-work-order-view-page.ts | 4 +-- .../common/comment-mention-composer.tsx | 7 +++-- .../work-orders/utils/comment-mentions.ts | 23 ++++++++------- .../utils/comment-mentions.test.ts | 29 +++++++++++++++++++ 5 files changed, 53 insertions(+), 17 deletions(-) diff --git a/src/app/(protected)/workorders/_components/view/work-order-activity-panel.tsx b/src/app/(protected)/workorders/_components/view/work-order-activity-panel.tsx index 4c1742b5..61d3d060 100644 --- a/src/app/(protected)/workorders/_components/view/work-order-activity-panel.tsx +++ b/src/app/(protected)/workorders/_components/view/work-order-activity-panel.tsx @@ -20,7 +20,7 @@ type WorkOrderActivityPanelProps = { auditLog: WorkOrderAuditEntry[]; mentionUsers: MentionUser[]; isSubmitting: boolean; - onSubmitComment: (text: string) => void; + onSubmitComment: (text: string) => void | Promise; }; export function WorkOrderActivityPanel({ @@ -75,8 +75,9 @@ export function WorkOrderActivityPanel({ if (!trimmed) { return; } - onSubmitComment(trimmed); - setCommentText(""); + void Promise.resolve(onSubmitComment(trimmed)).then(() => { + setCommentText(""); + }); }} className="mt-4 flex flex-col gap-2" > diff --git a/src/app/(protected)/workorders/_hooks/use-work-order-view-page.ts b/src/app/(protected)/workorders/_hooks/use-work-order-view-page.ts index 954170ae..d2dc9183 100644 --- a/src/app/(protected)/workorders/_hooks/use-work-order-view-page.ts +++ b/src/app/(protected)/workorders/_hooks/use-work-order-view-page.ts @@ -42,9 +42,9 @@ export function useWorkOrderViewPage(workOrderId: string) { [users], ); - const submitComment = (text: string) => { + const submitComment = async (text: string) => { const mentions = extractMentionIds(text); - commentMutation.mutate({ + await commentMutation.mutateAsync({ text, mentions: mentions.length > 0 ? mentions : undefined, commentType: "internal", diff --git a/src/components/common/comment-mention-composer.tsx b/src/components/common/comment-mention-composer.tsx index 566c1a5b..815969c4 100644 --- a/src/components/common/comment-mention-composer.tsx +++ b/src/components/common/comment-mention-composer.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Box, ClickAwayListener, @@ -49,6 +49,10 @@ export function CommentMentionComposer({ const showMenu = Boolean(menuOpen && activeQuery && filteredCandidates.length > 0); + useEffect(() => { + setHighlightIndex(0); + }, [activeQuery?.query, activeQuery?.start]); + const syncCursor = useCallback(() => { const element = inputRef.current; if (!element) { @@ -58,7 +62,6 @@ export function CommentMentionComposer({ const nextCursor = element.selectionStart ?? value.length; setCursor(nextCursor); setMenuOpen(Boolean(findActiveMentionQuery(value, nextCursor))); - setHighlightIndex(0); }, [value]); const handleChange = (nextValue: string) => { diff --git a/src/domain/work-orders/utils/comment-mentions.ts b/src/domain/work-orders/utils/comment-mentions.ts index 7945bfb5..21302f1a 100644 --- a/src/domain/work-orders/utils/comment-mentions.ts +++ b/src/domain/work-orders/utils/comment-mentions.ts @@ -57,16 +57,12 @@ function resolveMentionNames( users: MentionLookup[] | undefined, ): Map { const byId = new Map(); + const usersById = new Map((users ?? []).map((user) => [String(user.id), user.name] as const)); - for (const user of users ?? []) { - byId.set(String(user.id), user.name); - } - + // Only ids that were actually mentioned — never seed the full dispatcher roster. for (const mentionId of mentionIds ?? []) { const id = String(mentionId); - if (!byId.has(id)) { - byId.set(id, id); - } + byId.set(id, usersById.get(id) ?? id); } return byId; @@ -110,12 +106,21 @@ export function buildCommentBodySegments( const segments: CommentBodySegment[] = []; const encodedPattern = new RegExp(MENTION_TOKEN_PATTERN.source, "g"); + const mentionNameById = resolveMentionNames(mentionIds, users); + const encodedLabels: string[] = []; + for (const match of text.matchAll(new RegExp(MENTION_TOKEN_PATTERN.source, "g"))) { + const label = match[2]?.trim(); + if (label) encodedLabels.push(label); + } + const mentionNames = [...new Set([...mentionNameById.values(), ...encodedLabels])].filter( + Boolean, + ); let cursor = 0; for (const match of text.matchAll(encodedPattern)) { const index = match.index ?? 0; const plainBetween = text.slice(cursor, index); - appendPlainMentionMatches(segments, plainBetween, []); + appendPlainMentionMatches(segments, plainBetween, mentionNames); const userId = match[1] ?? ""; const label = match[2] ?? userId; @@ -124,8 +129,6 @@ export function buildCommentBodySegments( } const trailingPlain = text.slice(cursor); - const mentionNameById = resolveMentionNames(mentionIds, users); - const mentionNames = [...new Set(mentionNameById.values())].filter(Boolean); appendPlainMentionMatches(segments, trailingPlain, mentionNames); return segments.length > 0 ? segments : [{ type: "text", value: text }]; diff --git a/src/test/domain/work-orders/utils/comment-mentions.test.ts b/src/test/domain/work-orders/utils/comment-mentions.test.ts index 4305bcf5..c1f9dd3d 100644 --- a/src/test/domain/work-orders/utils/comment-mentions.test.ts +++ b/src/test/domain/work-orders/utils/comment-mentions.test.ts @@ -41,6 +41,35 @@ describe("comment-mentions", () => { ]); }); + it("does not highlight casual @names from the full user roster without mentions", () => { + const text = "See @Jane Doe later"; + + expect(buildCommentBodySegments(text, undefined, [{ id: "user-1", name: "Jane Doe" }])).toEqual( + [{ type: "text", value: "See @Jane Doe later" }], + ); + }); + + it("highlights plain @names before encoded tokens when mentioned", () => { + const encoded = encodeMentionToken("2", "Bob"); + const text = `Hi @Jane Doe and ${encoded}`; + + expect( + buildCommentBodySegments( + text, + ["user-1", "2"], + [ + { id: "user-1", name: "Jane Doe" }, + { id: "2", name: "Bob" }, + ], + ), + ).toEqual([ + { type: "text", value: "Hi " }, + { type: "mention", userId: "Jane Doe", label: "Jane Doe" }, + { type: "text", value: " and " }, + { type: "mention", userId: "2", label: "Bob" }, + ]); + }); + it("finds active mention query at cursor", () => { const text = "Hello @ja"; const query = findActiveMentionQuery(text, text.length);