Repository navigation
[SE-641] @ mention users in documents - #274
ihordubas99 wants to merge 7 commits into
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Active-user filtering, repeated-query races, hash-search cancellation, keyboard activation, and public setup documentation remain unresolved.
5 open findings
What changed in this PR
Adds project-member @ mentions with OpenProject-compatible serialization, profile links, avatars, menus, and inline-node interactions.
Changes:
- Adds mention search, rendering, clipboard/markdown serialization, and public APIs.
- Extracts shared inline-node, popover, search, avatar, and styling infrastructure.
- Adds comprehensive unit and browser coverage.
| File | Description |
|---|---|
.gitignore |
Ignores Vitest artifacts. |
src/App.tsx |
Enables mentions in the demo. |
lib/index.ts |
Exports mention APIs and triggers. |
lib/server.ts |
Exports server-safe mention support. |
lib/openProjectTypes.ts |
Adds the principal type. |
lib/locales/en.ts |
Adds mention translations. |
lib/services/openProjectApi.ts |
Adds user search, links, and avatars. |
lib/hooks/useDebouncedSearch.ts |
Adds reusable debounced search. |
lib/hooks/useInlineNodeOptions.ts |
Adds shared inline-node options behavior. |
lib/hooks/usePressOutside.ts |
Adds shadow-DOM-aware outside presses. |
lib/hooks/useSuggestionSearch.ts |
Generalizes suggestion searches. |
lib/hooks/useWorkPackageSearch.ts |
Uses shared debouncing. |
lib/utils/html.ts |
Adds HTML escaping helpers. |
lib/utils/inlineChipActions.ts |
Reuses generic inline-node operations. |
lib/utils/inlineNodes.ts |
Adds generic atom-node operations. |
lib/utils/insertInline.ts |
Adds shared inline insertion helpers. |
lib/utils/links.ts |
Adds new-tab link props. |
lib/utils/nodeTypes.ts |
Centralizes node type constants. |
lib/utils/suggestionTrigger.ts |
Adds trigger-text inspection. |
lib/utils/tapActivation.ts |
Preserves nested link activation. |
lib/components/index.ts |
Exports the mention spec. |
lib/components/UserMention/index.ts |
Exports browser mention support. |
lib/components/UserMention/externalHtml.ts |
Serializes and parses mention markup. |
lib/components/UserMention/spec.tsx |
Defines the React mention spec. |
lib/components/UserMention/staticSpec.ts |
Defines server-side serialization. |
lib/components/UserMention/userMentionConfig.ts |
Defines mention schema properties. |
lib/components/UserMention/UserMentionPill.tsx |
Renders interactive mention pills. |
lib/components/MentionMenu/index.ts |
Exports mention-menu APIs. |
lib/components/MentionMenu/MentionMenu.tsx |
Renders mention results. |
lib/components/MentionMenu/mentionTrigger.ts |
Restricts valid @ triggers. |
lib/components/MentionMenu/OpenProjectMentionMenu.tsx |
Wires the menu controller. |
lib/components/MentionMenu/useMentionMenu.ts |
Searches and inserts users. |
lib/components/shared/anchoredPopover.tsx |
Centralizes anchored popovers. |
lib/components/shared/Avatar/index.ts |
Exports avatars. |
lib/components/shared/Avatar/Avatar.tsx |
Renders pictures or initials. |
lib/components/shared/Avatar/avatars.ts |
Computes initials and colors. |
lib/components/shared/inlineNodeStyles.ts |
Shares inline-node styling. |
lib/components/shared/OptionsPopover.tsx |
Shares open/remove controls. |
lib/components/shared/SearchMessage.tsx |
Shares search status styling. |
lib/components/shared/SuggestionListbox.tsx |
Shares suggestion-list rendering. |
lib/components/shared/theme.ts |
Centralizes theme tokens. |
lib/components/WorkPackage/atoms.tsx |
Reuses shared links and styling. |
lib/components/WorkPackage/index.ts |
Removes relocated style exports. |
lib/components/WorkPackage/OptionsPopover.tsx |
Uses shared option controls. |
lib/components/WorkPackage/PreviewPopover.tsx |
Uses shared popover infrastructure. |
lib/components/WorkPackage/SizeMenu.tsx |
Uses relocated shared utilities. |
lib/components/WorkPackage/tokens.ts |
Removes relocated tokens. |
lib/components/WorkPackage/UnavailableCard.tsx |
Preserves card-link behavior. |
lib/components/InlineWorkPackage/chipLayouts.tsx |
Uses shared inline-node styles. |
lib/components/InlineWorkPackage/externalHtml.ts |
Uses centralized node constants. |
lib/components/InlineWorkPackage/inlineConfig.ts |
Uses centralized node constants. |
lib/components/InlineWorkPackage/InlineWorkPackageChip.tsx |
Uses shared option behavior. |
lib/components/BlockWorkPackage/blockConfig.ts |
Uses centralized node constants. |
lib/components/BlockWorkPackage/BlockCards.tsx |
Uses shared theme variables. |
lib/components/BlockWorkPackage/BlockWorkPackage.tsx |
Reuses outside-press handling. |
lib/components/CreateWorkPackage/atoms.ts |
Uses shared theme variables. |
lib/components/CreateWorkPackage/Suggestions.tsx |
Uses shared anchored popovers. |
lib/components/HashMenu/editorUtils.ts |
Reuses inline insertion. |
lib/components/HashMenu/hashTrigger.ts |
Reuses trigger-text inspection. |
lib/components/HashMenu/HashWpMenu.tsx |
Uses the shared listbox. |
lib/components/HashMenu/index.ts |
Exports the hash trigger. |
lib/components/HashMenu/OpenProjectHashMenu.tsx |
Uses shared menu callbacks. |
lib/components/HashMenu/types.ts |
Reuses suggestion item types. |
lib/components/HashMenu/useHashWpMenu.ts |
Uses generic suggestion search. |
lib/components/Search/SearchContainer.tsx |
Uses shared theme and messages. |
lib/components/Search/SearchDropdown.tsx |
Uses shared search messaging. |
lib/components/Search/WorkPackageSearchPopover.tsx |
Uses shared anchored popovers. |
test/helpers/content.ts |
Adds reusable content fixtures. |
test/helpers/editorHelpers.ts |
Adds tap and navigation helpers. |
test/helpers/headlessEditor.ts |
Adds mention-aware editor helpers. |
test/helpers/renderEditor.tsx |
Registers mention support in tests. |
test/mocks/handlers.ts |
Mocks avatar responses. |
test/lib/components/HashMenu/editorUtils.test.ts |
Updates fixture imports. |
test/lib/components/HashMenu/hashTrigger.test.ts |
Updates fixture imports. |
test/lib/components/MentionMenu/mentionTrigger.test.ts |
Tests valid mention positions. |
test/lib/components/UserMention/externalHtml.test.ts |
Tests mention HTML conversion. |
test/lib/components/UserMention/markdown.test.ts |
Pins canonical markdown output. |
test/lib/components/shared/Avatar/avatar.browser.test.tsx |
Tests avatar rendering. |
test/lib/components/shared/Avatar/avatars.test.ts |
Tests initials and colors. |
test/lib/components/integration/editor.drag.browser.test.tsx |
Tests mention dragging. |
test/lib/components/integration/inlineNodeOptionsShadowDom.browser.test.tsx |
Tests shadow-root popovers. |
test/lib/components/integration/userMention.browser.test.tsx |
Tests rendering and paste. |
test/lib/components/integration/userMentionGeometry.browser.test.tsx |
Tests pill geometry. |
test/lib/components/integration/userMentionMenu.browser.test.tsx |
Tests mention searching and insertion. |
test/lib/components/integration/userMentionOptions.browser.test.tsx |
Tests mention actions. |
test/lib/hooks/useDebouncedSearch.test.ts |
Tests debounce semantics. |
test/lib/hooks/useSuggestionSearch.test.ts |
Tests stale search handling. |
test/lib/hooks/useWorkPackageSearch.test.ts |
Tests imperative search cancellation. |
test/lib/services/openProjectApi.test.ts |
Tests user API behavior. |
test/lib/utils/html.test.ts |
Tests HTML escaping. |
test/lib/utils/inlineChipActions.test.ts |
Moves generic removal coverage. |
test/lib/utils/inlineNodes.test.ts |
Tests generic inline-node actions. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
Description of changes in the new commit: Why. Hocuspocus saves the aggregated changes of everyone editing with the token of the first user who connected, so OpenProject can't tell from the save who added a mention. The notification would then name the wrong person. As agreed with Judith, the mention carries its author and the backend reads it from the stored tag. Stored format. The tag gets one more attribute, last and only when an author is known. Without it the tag is unchanged from what CKEditor writes. Who gets recorded.
The extension has to be passed in The author is set by the client, so the backend should check that this user can edit the document. |


Ticket
https://community.openproject.org/wp/SE-641
What are you trying to accomplish?
Users can mention people in a document. Typing
@opens a menu with the active members of the document's project. Picking someone inserts an inline mention with their avatar and name, linking to their profile.The mention is stored in the document markdown in exactly the form CKEditor uses. OpenProject therefore renders it as a user link with a hover card and can send notifications for it, without special cases. Notifications themselves are handled separately in OpenProject.
Inside the editor a mention behaves like an inline work package. Clicking it selects it and opens a popover to open the profile or remove the mention. A selected mention can be removed with Backspace and moved with drag and drop.
Screenshots
What approach did you choose and why?
Stored format. BlockNote's markdown export keeps only the text of elements it does not know, so a real
<mention>element would be saved as a plain"@Name"and lose its id. Text nodes are copied verbatim, so the server-side spec writes the canonical tag as text. A markdown test pins the exact output in case BlockNote ever starts escaping text. In the browser the clipboard gets a real<mention>element, and a mention copied from a CKEditor field pastes as a mention.Search. The menu asks
/api/v3/principalsfor active users who are members of the document's project, matching name, login or email. Like CKEditor, it only opens where a word starts, so typing an email address does not open it.Shared building blocks. A mention needs what the inline work package already had, such as the options popover, node selection, closing on an outside press and a suggestion menu. Instead of copying these, they moved into
lib/components/sharedand a few generic hooks, and the work package code now uses them too. That is why the diff touches many work package files. The first three commits are this refactoring without behaviour changes and can be reviewed on their own. The last two add the feature.Two small fixes come with the refactoring. Clicking an open inline work package again now closes its popover inside a shadow root, as in OpenProject, instead of reopening it. A blank
#search cancels a pending search again.Public API. Only additions. The client gets
openProjectUserMentionSpec,OpenProjectMentionMenuand the trigger constants. The server entry getsopenProjectUserMentionStaticSpecand the stored-format helpers for hocuspocus. The OpenProject side is a separate PR opf/openproject#25917Merge checklist