From 46943445cff65e2ffe9c388b6cabfe9a34e4889a Mon Sep 17 00:00:00 2001 From: DarrellRoberts Date: Wed, 10 Jun 2026 16:37:01 +0200 Subject: [PATCH 1/3] replaces endpoint of /user/:id to /users --- .../sections/Comments/common/Autocomplete.tsx | 13 +++++---- .../sections/Comments/common/Comment.tsx | 1 - .../sections/Comments/common/CommentEdit.tsx | 3 +-- .../Comments/common/EntityComments.tsx | 19 +++++-------- .../sections/Comments/common/helpers.ts | 27 ------------------- .../Comments/common/hooks/useCommentTag.tsx | 10 +++---- 6 files changed, 21 insertions(+), 52 deletions(-) delete mode 100644 src/components/Dashboard/Profile/sections/Comments/common/helpers.ts diff --git a/src/components/Dashboard/Profile/sections/Comments/common/Autocomplete.tsx b/src/components/Dashboard/Profile/sections/Comments/common/Autocomplete.tsx index 4a4a80c8..658bdc13 100644 --- a/src/components/Dashboard/Profile/sections/Comments/common/Autocomplete.tsx +++ b/src/components/Dashboard/Profile/sections/Comments/common/Autocomplete.tsx @@ -7,7 +7,7 @@ import { getImageUrl } from "@/utils"; import getCaretCoordinates from "textarea-caret"; type Props = { - handleTagAdd: (userId: number, fullName: string) => void; + handleTagAdd: (userId: number, fullName: string, personId: number) => void; newCommentText: string; textAreaRef: React.RefObject; activeRowIndex: number; @@ -55,7 +55,10 @@ export default function Autocomplete({ if (!filteredUsers) return; const activeUser = filteredUsers[activeRowIndex]; if (activeUser) { - setOnSelectTrigger(() => () => handleTagAdd(activeUser.id, activeUser.fullName.replaceAll(/ /g, ""))); + setOnSelectTrigger( + () => () => + handleTagAdd(activeUser.id, activeUser.fullName.replaceAll(/ /g, ""), activeUser.personId as number), + ); } else { setOnSelectTrigger(null); } @@ -83,8 +86,8 @@ export default function Autocomplete({ } }, [activeRowIndex, filteredUsers]); - const handleUserSelect = (userId: number, fullName: string) => { - handleTagAdd(userId, fullName.replaceAll(/ /g, "")); + const handleUserSelect = (userId: number, fullName: string, personId: number) => { + handleTagAdd(userId, fullName.replaceAll(/ /g, ""), personId); }; const resolvedAvatarUrl = (url: string | null | undefined) => { @@ -127,7 +130,7 @@ export default function Autocomplete({ key={user.id} role="option" aria-selected={isActive} - onClick={() => handleUserSelect(user.id, user.fullName)} + onClick={() => handleUserSelect(user.id, user.fullName, user.personId as number)} style={{ backgroundColor: isActive ? "var(--editableField-optionRow-selectedBg)" : "transparent", cursor: "pointer", diff --git a/src/components/Dashboard/Profile/sections/Comments/common/Comment.tsx b/src/components/Dashboard/Profile/sections/Comments/common/Comment.tsx index 000ad904..8a2e5329 100644 --- a/src/components/Dashboard/Profile/sections/Comments/common/Comment.tsx +++ b/src/components/Dashboard/Profile/sections/Comments/common/Comment.tsx @@ -9,7 +9,6 @@ type EditState = { text: string; canSave: boolean; isUpdating: boolean; - isTagFetch: boolean; onTextChange: (text: string) => void; onKeyPress: (e: React.KeyboardEvent) => void; onSave: () => void; diff --git a/src/components/Dashboard/Profile/sections/Comments/common/CommentEdit.tsx b/src/components/Dashboard/Profile/sections/Comments/common/CommentEdit.tsx index 02291037..8b78617c 100644 --- a/src/components/Dashboard/Profile/sections/Comments/common/CommentEdit.tsx +++ b/src/components/Dashboard/Profile/sections/Comments/common/CommentEdit.tsx @@ -15,7 +15,6 @@ type EditState = { text: string; canSave: boolean; isUpdating: boolean; - isTagFetch: boolean; onTextChange: (text: string) => void; onKeyPress: (e: React.KeyboardEvent) => void; onSave: () => void; @@ -86,7 +85,7 @@ export function CommentEdit({ commentId, edit }: Props) { {t("dashboard.commentsSection.saveEdit")} diff --git a/src/components/Dashboard/Profile/sections/Comments/common/EntityComments.tsx b/src/components/Dashboard/Profile/sections/Comments/common/EntityComments.tsx index 8495ec7f..7d929512 100644 --- a/src/components/Dashboard/Profile/sections/Comments/common/EntityComments.tsx +++ b/src/components/Dashboard/Profile/sections/Comments/common/EntityComments.tsx @@ -14,7 +14,6 @@ import { useCommentMenu } from "./hooks/useCommentMenu"; import { AddCommentButton, Container, NewCommentSection, TagOverlay, TextArea } from "./styles"; import { useCommentTag } from "./hooks/useCommentTag"; import Autocomplete from "./Autocomplete"; -import { getPersonIds } from "./helpers"; type Props = { entityId: Id; @@ -27,7 +26,6 @@ export function EntityComments({ entityId, entityType, comments, testId }: Props const { t } = useTranslation(); const { mutate: createComment, isPending: isCreating } = useCreateComment(entityId, entityType); const [newCommentText, setNewCommentText] = useState(""); - const [isTagFetch, setIsTagFetch] = useState(false); const textAreaRef = useRef(null); const overlayRef = useRef(null); @@ -62,16 +60,15 @@ export function EntityComments({ entityId, entityType, comments, testId }: Props if (!newCommentText.trim()) return; let formattedText = newCommentText; - const taggedUserIds: number[] = []; + const taggedPersonIds: number[] = []; tags.forEach((tag) => { formattedText = formattedText.replace(`@${tag.name}`, `<@${tag.id}>`); - if (formattedText.includes(`<@${tag.id}>`) && !taggedUserIds.includes(tag.id)) { - taggedUserIds.push(tag.id); + if (formattedText.includes(`<@${tag.id}>`) && !taggedPersonIds.includes(tag.personId)) { + taggedPersonIds.push(tag.personId); } }); - const taggedPersonIds = await getPersonIds(taggedUserIds, setIsTagFetch, t); createComment( { text: formattedText.trim(), @@ -104,17 +101,16 @@ export function EntityComments({ entityId, entityType, comments, testId }: Props if (!edit.editText.trim() || !edit.editingCommentId) return; const currentTags = initTags(edit.editText); - const taggedUserIds: number[] = []; + const taggedPersonIds: number[] = []; let formattedText = edit.editText; currentTags?.forEach((tag) => { formattedText = formattedText.replace(`@${tag.name}`, `<@${tag.id}>`); - if (formattedText.includes(`<@${tag.id}>`) && !taggedUserIds.includes(tag.id)) { - taggedUserIds.push(tag.id); + if (formattedText.includes(`<@${tag.id}>`) && !taggedPersonIds.includes(tag.personId)) { + taggedPersonIds.push(tag.personId); } }); - const taggedPersonIds = await getPersonIds(taggedUserIds, setIsTagFetch, t); updateComment( { text: formattedText.trim(), taggedPersonIds }, @@ -154,7 +150,6 @@ export function EntityComments({ entityId, entityType, comments, testId }: Props text: edit.editText, canSave: edit.canSave, isUpdating, - isTagFetch, onTextChange: edit.updateEditText, onKeyPress: (e) => edit.handleKeyPress(e, handleSaveEdit), onSave: handleSaveEdit, @@ -203,7 +198,7 @@ export function EntityComments({ entityId, entityType, comments, testId }: Props {t("dashboard.commentsSection.addComment")} diff --git a/src/components/Dashboard/Profile/sections/Comments/common/helpers.ts b/src/components/Dashboard/Profile/sections/Comments/common/helpers.ts deleted file mode 100644 index c695b626..00000000 --- a/src/components/Dashboard/Profile/sections/Comments/common/helpers.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { apiPathUser } from "@/config/constants"; -import axios from "axios"; -import { TFunction } from "i18next"; -import { toast } from "react-toastify"; - -export const getPersonIds = async ( - taggedUserIds: number[], - setIsTagFetch: (arg: boolean) => void, - t: TFunction<"translation", undefined>, -) => { - if (!taggedUserIds.length) return; - const promiseArray = taggedUserIds.map(async (id) => { - setIsTagFetch(true); - try { - const response = await axios.get(`${apiPathUser}/${id}`); - return response.data.data.personId; - } catch (err) { - console.error(err); - toast.error(t("dashboard.commentsSection.errorTagging")); - return null; - } finally { - setIsTagFetch(false); - } - }); - const resolvedPersonIds = await Promise.all(promiseArray); - return resolvedPersonIds?.filter((id): id is number => id !== null); -}; diff --git a/src/components/Dashboard/Profile/sections/Comments/common/hooks/useCommentTag.tsx b/src/components/Dashboard/Profile/sections/Comments/common/hooks/useCommentTag.tsx index 6630ca5c..c633da4d 100644 --- a/src/components/Dashboard/Profile/sections/Comments/common/hooks/useCommentTag.tsx +++ b/src/components/Dashboard/Profile/sections/Comments/common/hooks/useCommentTag.tsx @@ -8,7 +8,7 @@ export function useCommentTag( setNewCommentText?: (text: string) => void, textAreaRef?: React.RefObject | null, ) { - const [tags, setTags] = useState<{ id: number; name: string }[]>([]); + const [tags, setTags] = useState<{ id: number; name: string; personId: number }[]>([]); const [showAutocomplete, setShowAutocomplete] = useState(false); const [activeRowIndex, setActiveRowIndex] = useState(0); const [filteredListLength, setFilteredListLength] = useState(0); @@ -76,7 +76,7 @@ export function useCommentTag( return elements; }, [value, tags, users]); - const handleTagAdd = (userId: number, fullName: string) => { + const handleTagAdd = (userId: number, fullName: string, personId: number) => { if (!value || !textAreaRef?.current) return null; const cursorPosition = textAreaRef.current.selectionStart; const textBeforeCaret = value.substring(0, cursorPosition); @@ -85,7 +85,7 @@ export function useCommentTag( const newText = textBeforeCaret.substring(0, lastAtIndex) + `@${fullName} ` + textAfterCaret; setNewCommentText?.(newText); - setTags((prev) => [...prev, { id: userId, name: fullName }]); + setTags((prev) => [...prev, { id: userId, name: fullName, personId }]); setShowAutocomplete(false); }; @@ -122,14 +122,14 @@ export function useCommentTag( if (!value || !users) return; const regexTag = /(<@\d+>)|((?<=^|\s)@[\w\s]+?)(?=\s|$)/g; const matches = Array.from(value.matchAll(regexTag)); - const freshlyFoundTags: { id: number; name: string }[] = []; + const freshlyFoundTags: { id: number; name: string; personId: number }[] = []; matches.forEach((match) => { const username = match[0]; const user = users.find((u) => `@${u.fullName.replaceAll(/ /g, "")}` === username); if (user) { const cleanName = user.fullName.replace(/\s/g, ""); - freshlyFoundTags.push({ id: user.id, name: cleanName }); + freshlyFoundTags.push({ id: user.id, name: cleanName, personId: user.personId as number }); } }); if (freshlyFoundTags.length > 0) { From 043b2f1702647e1e2e99e61d5f9ad17904965673 Mon Sep 17 00:00:00 2001 From: DarrellRoberts Date: Wed, 10 Jun 2026 20:55:47 +0200 Subject: [PATCH 2/3] fixes edge cases --- .../sections/Comments/common/Autocomplete.tsx | 15 +++++++++------ .../Profile/sections/Comments/common/Comment.tsx | 1 + .../sections/Comments/common/CommentEdit.tsx | 3 ++- .../sections/Comments/common/EntityComments.tsx | 4 +++- .../Comments/common/hooks/useCommentTag.tsx | 14 +++++++++----- 5 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/components/Dashboard/Profile/sections/Comments/common/Autocomplete.tsx b/src/components/Dashboard/Profile/sections/Comments/common/Autocomplete.tsx index 658bdc13..88e9210a 100644 --- a/src/components/Dashboard/Profile/sections/Comments/common/Autocomplete.tsx +++ b/src/components/Dashboard/Profile/sections/Comments/common/Autocomplete.tsx @@ -42,7 +42,7 @@ export default function Autocomplete({ const filteredUsers = useMemo(() => { if (userFilter === null) return; - return users?.filter((user) => user?.fullName?.toLowerCase().includes(userFilter)); + return users?.filter((user) => user?.fullName?.toLowerCase().includes(userFilter) && user?.personId != null); }, [userFilter, users]); useEffect(() => { @@ -55,10 +55,10 @@ export default function Autocomplete({ if (!filteredUsers) return; const activeUser = filteredUsers[activeRowIndex]; if (activeUser) { - setOnSelectTrigger( - () => () => - handleTagAdd(activeUser.id, activeUser.fullName.replaceAll(/ /g, ""), activeUser.personId as number), - ); + setOnSelectTrigger(() => () => { + if (activeUser.personId == null) return; + handleTagAdd(activeUser.id, activeUser.fullName.replaceAll(/ /g, ""), activeUser.personId); + }); } else { setOnSelectTrigger(null); } @@ -130,7 +130,10 @@ export default function Autocomplete({ key={user.id} role="option" aria-selected={isActive} - onClick={() => handleUserSelect(user.id, user.fullName, user.personId as number)} + onClick={() => { + if (user.personId == null) return; + handleUserSelect(user.id, user.fullName, user.personId); + }} style={{ backgroundColor: isActive ? "var(--editableField-optionRow-selectedBg)" : "transparent", cursor: "pointer", diff --git a/src/components/Dashboard/Profile/sections/Comments/common/Comment.tsx b/src/components/Dashboard/Profile/sections/Comments/common/Comment.tsx index 8a2e5329..b6b06879 100644 --- a/src/components/Dashboard/Profile/sections/Comments/common/Comment.tsx +++ b/src/components/Dashboard/Profile/sections/Comments/common/Comment.tsx @@ -9,6 +9,7 @@ type EditState = { text: string; canSave: boolean; isUpdating: boolean; + isUsersLoading: boolean; onTextChange: (text: string) => void; onKeyPress: (e: React.KeyboardEvent) => void; onSave: () => void; diff --git a/src/components/Dashboard/Profile/sections/Comments/common/CommentEdit.tsx b/src/components/Dashboard/Profile/sections/Comments/common/CommentEdit.tsx index 8b78617c..e2823412 100644 --- a/src/components/Dashboard/Profile/sections/Comments/common/CommentEdit.tsx +++ b/src/components/Dashboard/Profile/sections/Comments/common/CommentEdit.tsx @@ -15,6 +15,7 @@ type EditState = { text: string; canSave: boolean; isUpdating: boolean; + isUsersLoading: boolean; onTextChange: (text: string) => void; onKeyPress: (e: React.KeyboardEvent) => void; onSave: () => void; @@ -85,7 +86,7 @@ export function CommentEdit({ commentId, edit }: Props) { {t("dashboard.commentsSection.saveEdit")} diff --git a/src/components/Dashboard/Profile/sections/Comments/common/EntityComments.tsx b/src/components/Dashboard/Profile/sections/Comments/common/EntityComments.tsx index 7d929512..9bbc0b40 100644 --- a/src/components/Dashboard/Profile/sections/Comments/common/EntityComments.tsx +++ b/src/components/Dashboard/Profile/sections/Comments/common/EntityComments.tsx @@ -44,6 +44,7 @@ export function EntityComments({ entityId, entityType, comments, testId }: Props handleKeyDown, initTags, users, + isUsersLoading, } = useCommentTag(newCommentText, setNewCommentText, textAreaRef); const { mutate: updateComment, isPending: isUpdating } = useUpdateComment( @@ -150,6 +151,7 @@ export function EntityComments({ entityId, entityType, comments, testId }: Props text: edit.editText, canSave: edit.canSave, isUpdating, + isUsersLoading, onTextChange: edit.updateEditText, onKeyPress: (e) => edit.handleKeyPress(e, handleSaveEdit), onSave: handleSaveEdit, @@ -198,7 +200,7 @@ export function EntityComments({ entityId, entityType, comments, testId }: Props {t("dashboard.commentsSection.addComment")} diff --git a/src/components/Dashboard/Profile/sections/Comments/common/hooks/useCommentTag.tsx b/src/components/Dashboard/Profile/sections/Comments/common/hooks/useCommentTag.tsx index c633da4d..767320b6 100644 --- a/src/components/Dashboard/Profile/sections/Comments/common/hooks/useCommentTag.tsx +++ b/src/components/Dashboard/Profile/sections/Comments/common/hooks/useCommentTag.tsx @@ -14,7 +14,7 @@ export function useCommentTag( const [filteredListLength, setFilteredListLength] = useState(0); const [onSelectTrigger, setOnSelectTrigger] = useState<(() => void) | null>(null); - const { data: users } = useGetQuery({ + const { data: users, isLoading: isUsersLoading } = useGetQuery({ queryKey: ["users", "coordinators"], apiPath: apiPathUser, params: { @@ -112,7 +112,7 @@ export function useCommentTag( if (!text || !users) return text; return text.replace(/<@(\d+)>/g, (match, userId) => { const user = users.find((u) => u.id === Number(userId)); - return user ? `@${user.fullName.replaceAll(/ /g, "")}` : "@user"; + return user ? `@${user.fullName.replaceAll(/ /g, "")}` : `@user:${userId}`; }); }, [users], @@ -120,16 +120,19 @@ export function useCommentTag( const initTags = (value: string) => { if (!value || !users) return; - const regexTag = /(<@\d+>)|((?<=^|\s)@[\w\s]+?)(?=\s|$)/g; + const regexTag = /(<@\d+>)|((?<=^|\s)@[\w\s]+?(?::\d+)?)(?=\s|$)/g; const matches = Array.from(value.matchAll(regexTag)); const freshlyFoundTags: { id: number; name: string; personId: number }[] = []; matches.forEach((match) => { const username = match[0]; const user = users.find((u) => `@${u.fullName.replaceAll(/ /g, "")}` === username); - if (user) { + if (user && user.personId) { const cleanName = user.fullName.replace(/\s/g, ""); - freshlyFoundTags.push({ id: user.id, name: cleanName, personId: user.personId as number }); + freshlyFoundTags.push({ id: user.id, name: cleanName, personId: user.personId }); + } else if (username.startsWith("@user:")) { + const extractedId = Number(username.split(":")[1]); + freshlyFoundTags.push({ id: extractedId, name: "user", personId: -1 }); } }); if (freshlyFoundTags.length > 0) { @@ -187,5 +190,6 @@ export function useCommentTag( convertDbTextToEditable, initTags, users, + isUsersLoading, }; } From 035eeea24bb4b87da34b4bc184eacda44826d6c8 Mon Sep 17 00:00:00 2001 From: DarrellRoberts Date: Mon, 15 Jun 2026 15:26:03 +0200 Subject: [PATCH 3/3] removes else block & isUsersLoading --- .../Profile/sections/Comments/common/Comment.tsx | 1 - .../Profile/sections/Comments/common/CommentEdit.tsx | 3 +-- .../Profile/sections/Comments/common/EntityComments.tsx | 4 +--- .../sections/Comments/common/hooks/useCommentTag.tsx | 8 ++------ 4 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/components/Dashboard/Profile/sections/Comments/common/Comment.tsx b/src/components/Dashboard/Profile/sections/Comments/common/Comment.tsx index b6b06879..8a2e5329 100644 --- a/src/components/Dashboard/Profile/sections/Comments/common/Comment.tsx +++ b/src/components/Dashboard/Profile/sections/Comments/common/Comment.tsx @@ -9,7 +9,6 @@ type EditState = { text: string; canSave: boolean; isUpdating: boolean; - isUsersLoading: boolean; onTextChange: (text: string) => void; onKeyPress: (e: React.KeyboardEvent) => void; onSave: () => void; diff --git a/src/components/Dashboard/Profile/sections/Comments/common/CommentEdit.tsx b/src/components/Dashboard/Profile/sections/Comments/common/CommentEdit.tsx index e2823412..8b78617c 100644 --- a/src/components/Dashboard/Profile/sections/Comments/common/CommentEdit.tsx +++ b/src/components/Dashboard/Profile/sections/Comments/common/CommentEdit.tsx @@ -15,7 +15,6 @@ type EditState = { text: string; canSave: boolean; isUpdating: boolean; - isUsersLoading: boolean; onTextChange: (text: string) => void; onKeyPress: (e: React.KeyboardEvent) => void; onSave: () => void; @@ -86,7 +85,7 @@ export function CommentEdit({ commentId, edit }: Props) { {t("dashboard.commentsSection.saveEdit")} diff --git a/src/components/Dashboard/Profile/sections/Comments/common/EntityComments.tsx b/src/components/Dashboard/Profile/sections/Comments/common/EntityComments.tsx index 9bbc0b40..7d929512 100644 --- a/src/components/Dashboard/Profile/sections/Comments/common/EntityComments.tsx +++ b/src/components/Dashboard/Profile/sections/Comments/common/EntityComments.tsx @@ -44,7 +44,6 @@ export function EntityComments({ entityId, entityType, comments, testId }: Props handleKeyDown, initTags, users, - isUsersLoading, } = useCommentTag(newCommentText, setNewCommentText, textAreaRef); const { mutate: updateComment, isPending: isUpdating } = useUpdateComment( @@ -151,7 +150,6 @@ export function EntityComments({ entityId, entityType, comments, testId }: Props text: edit.editText, canSave: edit.canSave, isUpdating, - isUsersLoading, onTextChange: edit.updateEditText, onKeyPress: (e) => edit.handleKeyPress(e, handleSaveEdit), onSave: handleSaveEdit, @@ -200,7 +198,7 @@ export function EntityComments({ entityId, entityType, comments, testId }: Props {t("dashboard.commentsSection.addComment")} diff --git a/src/components/Dashboard/Profile/sections/Comments/common/hooks/useCommentTag.tsx b/src/components/Dashboard/Profile/sections/Comments/common/hooks/useCommentTag.tsx index 767320b6..fa515502 100644 --- a/src/components/Dashboard/Profile/sections/Comments/common/hooks/useCommentTag.tsx +++ b/src/components/Dashboard/Profile/sections/Comments/common/hooks/useCommentTag.tsx @@ -14,7 +14,7 @@ export function useCommentTag( const [filteredListLength, setFilteredListLength] = useState(0); const [onSelectTrigger, setOnSelectTrigger] = useState<(() => void) | null>(null); - const { data: users, isLoading: isUsersLoading } = useGetQuery({ + const { data: users } = useGetQuery({ queryKey: ["users", "coordinators"], apiPath: apiPathUser, params: { @@ -127,12 +127,9 @@ export function useCommentTag( matches.forEach((match) => { const username = match[0]; const user = users.find((u) => `@${u.fullName.replaceAll(/ /g, "")}` === username); - if (user && user.personId) { + if (user && user.personId != null) { const cleanName = user.fullName.replace(/\s/g, ""); freshlyFoundTags.push({ id: user.id, name: cleanName, personId: user.personId }); - } else if (username.startsWith("@user:")) { - const extractedId = Number(username.split(":")[1]); - freshlyFoundTags.push({ id: extractedId, name: "user", personId: -1 }); } }); if (freshlyFoundTags.length > 0) { @@ -190,6 +187,5 @@ export function useCommentTag( convertDbTextToEditable, initTags, users, - isUsersLoading, }; }