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[];
|
auditLog: WorkOrderAuditEntry[];
|
||||||
mentionUsers: MentionUser[];
|
mentionUsers: MentionUser[];
|
||||||
isSubmitting: boolean;
|
isSubmitting: boolean;
|
||||||
onSubmitComment: (text: string) => void;
|
onSubmitComment: (text: string) => void | Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function WorkOrderActivityPanel({
|
export function WorkOrderActivityPanel({
|
||||||
|
|
@ -75,8 +75,9 @@ export function WorkOrderActivityPanel({
|
||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onSubmitComment(trimmed);
|
void Promise.resolve(onSubmitComment(trimmed)).then(() => {
|
||||||
setCommentText("");
|
setCommentText("");
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
className="mt-4 flex flex-col gap-2"
|
className="mt-4 flex flex-col gap-2"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -42,9 +42,9 @@ export function useWorkOrderViewPage(workOrderId: string) {
|
||||||
[users],
|
[users],
|
||||||
);
|
);
|
||||||
|
|
||||||
const submitComment = (text: string) => {
|
const submitComment = async (text: string) => {
|
||||||
const mentions = extractMentionIds(text);
|
const mentions = extractMentionIds(text);
|
||||||
commentMutation.mutate({
|
await commentMutation.mutateAsync({
|
||||||
text,
|
text,
|
||||||
mentions: mentions.length > 0 ? mentions : undefined,
|
mentions: mentions.length > 0 ? mentions : undefined,
|
||||||
commentType: "internal",
|
commentType: "internal",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useCallback, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
ClickAwayListener,
|
ClickAwayListener,
|
||||||
|
|
@ -49,6 +49,10 @@ export function CommentMentionComposer({
|
||||||
|
|
||||||
const showMenu = Boolean(menuOpen && activeQuery && filteredCandidates.length > 0);
|
const showMenu = Boolean(menuOpen && activeQuery && filteredCandidates.length > 0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setHighlightIndex(0);
|
||||||
|
}, [activeQuery?.query, activeQuery?.start]);
|
||||||
|
|
||||||
const syncCursor = useCallback(() => {
|
const syncCursor = useCallback(() => {
|
||||||
const element = inputRef.current;
|
const element = inputRef.current;
|
||||||
if (!element) {
|
if (!element) {
|
||||||
|
|
@ -58,7 +62,6 @@ export function CommentMentionComposer({
|
||||||
const nextCursor = element.selectionStart ?? value.length;
|
const nextCursor = element.selectionStart ?? value.length;
|
||||||
setCursor(nextCursor);
|
setCursor(nextCursor);
|
||||||
setMenuOpen(Boolean(findActiveMentionQuery(value, nextCursor)));
|
setMenuOpen(Boolean(findActiveMentionQuery(value, nextCursor)));
|
||||||
setHighlightIndex(0);
|
|
||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
const handleChange = (nextValue: string) => {
|
const handleChange = (nextValue: string) => {
|
||||||
|
|
|
||||||
|
|
@ -57,16 +57,12 @@ function resolveMentionNames(
|
||||||
users: MentionLookup[] | undefined,
|
users: MentionLookup[] | undefined,
|
||||||
): Map<string, string> {
|
): Map<string, string> {
|
||||||
const byId = new 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 ?? []) {
|
// Only ids that were actually mentioned — never seed the full dispatcher roster.
|
||||||
byId.set(String(user.id), user.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const mentionId of mentionIds ?? []) {
|
for (const mentionId of mentionIds ?? []) {
|
||||||
const id = String(mentionId);
|
const id = String(mentionId);
|
||||||
if (!byId.has(id)) {
|
byId.set(id, usersById.get(id) ?? id);
|
||||||
byId.set(id, id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return byId;
|
return byId;
|
||||||
|
|
@ -110,12 +106,21 @@ export function buildCommentBodySegments(
|
||||||
|
|
||||||
const segments: CommentBodySegment[] = [];
|
const segments: CommentBodySegment[] = [];
|
||||||
const encodedPattern = new RegExp(MENTION_TOKEN_PATTERN.source, "g");
|
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;
|
let cursor = 0;
|
||||||
|
|
||||||
for (const match of text.matchAll(encodedPattern)) {
|
for (const match of text.matchAll(encodedPattern)) {
|
||||||
const index = match.index ?? 0;
|
const index = match.index ?? 0;
|
||||||
const plainBetween = text.slice(cursor, index);
|
const plainBetween = text.slice(cursor, index);
|
||||||
appendPlainMentionMatches(segments, plainBetween, []);
|
appendPlainMentionMatches(segments, plainBetween, mentionNames);
|
||||||
|
|
||||||
const userId = match[1] ?? "";
|
const userId = match[1] ?? "";
|
||||||
const label = match[2] ?? userId;
|
const label = match[2] ?? userId;
|
||||||
|
|
@ -124,8 +129,6 @@ export function buildCommentBodySegments(
|
||||||
}
|
}
|
||||||
|
|
||||||
const trailingPlain = text.slice(cursor);
|
const trailingPlain = text.slice(cursor);
|
||||||
const mentionNameById = resolveMentionNames(mentionIds, users);
|
|
||||||
const mentionNames = [...new Set(mentionNameById.values())].filter(Boolean);
|
|
||||||
appendPlainMentionMatches(segments, trailingPlain, mentionNames);
|
appendPlainMentionMatches(segments, trailingPlain, mentionNames);
|
||||||
|
|
||||||
return segments.length > 0 ? segments : [{ type: "text", value: text }];
|
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", () => {
|
it("finds active mention query at cursor", () => {
|
||||||
const text = "Hello @ja";
|
const text = "Hello @ja";
|
||||||
const query = findActiveMentionQuery(text, text.length);
|
const query = findActiveMentionQuery(text, text.length);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue