fix(work-orders): harden comment mentions and clear draft on success

This commit is contained in:
arthur.bassi 2026-08-05 11:55:39 -03:00
parent babb314613
commit 3b59bce973
5 changed files with 53 additions and 17 deletions

View file

@ -20,7 +20,7 @@ type WorkOrderActivityPanelProps = {
auditLog: WorkOrderAuditEntry[];
mentionUsers: MentionUser[];
isSubmitting: boolean;
onSubmitComment: (text: string) => void;
onSubmitComment: (text: string) => void | Promise<void>;
};
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"
>

View file

@ -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",

View file

@ -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) => {

View file

@ -57,16 +57,12 @@ function resolveMentionNames(
users: MentionLookup[] | undefined,
): Map<string, string> {
const byId = new Map<string, string>();
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 }];

View file

@ -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);