mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-07 16:19:01 +00:00
fix(work-orders): harden comment mentions and clear draft on success
This commit is contained in:
parent
babb314613
commit
3b59bce973
5 changed files with 53 additions and 17 deletions
|
|
@ -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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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 }];
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue