Skip to content

Add conversation reference links to private notes - #475

Open
fredrikblau wants to merge 1 commit into
abhinavxd:mainfrom
fredrikblau:feat/private-note-ticket-links
Open

Add conversation reference links to private notes#475
fredrikblau wants to merge 1 commit into
abhinavxd:mainfrom
fredrikblau:feat/private-note-ticket-links

Conversation

@fredrikblau

@fredrikblau fredrikblau commented Aug 6, 2026

Copy link
Copy Markdown

Summary

  • add # conversation suggestions to the private-note editor
  • search accessible conversations by reference number
  • insert the selected reference as a link to the conversation
  • keep ticket suggestions disabled for public replies

Testing

  • pnpm exec vitest run
  • pnpm exec vite build --mode main
  • live browser test of insertion, saved link navigation, and Reply mode

Fixes #468

image

Summary by CodeRabbit

  • New Features
    • Added ticket references in private notes using # search.
    • Search suggestions support mouse selection and keyboard navigation.
    • Referenced tickets render as clickable links that open in a new tab.
    • Suggestions require at least three characters and display helpful empty-state messages.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds ticket-reference support to the editor. Private notes can search conversations with #, display keyboard-navigable suggestions, insert linked references, and open references in protected new tabs.

Changes

Ticket reference support

Layer / File(s) Summary
Reference search and URL mapping
frontend/apps/main/src/components/editor/ticketReference.js, frontend/apps/main/src/components/editor/ticketReference.test.js
Queries shorter than three characters return no suggestions. Valid queries search conversations and map results to reference data and conversation URLs.
Suggestion popup and reference rendering
frontend/apps/main/src/components/editor/TicketReferenceList.vue, frontend/apps/main/src/components/editor/ticketReferenceSuggestion.js, frontend/apps/main/src/components/editor/TextEditor.vue
The editor renders # references as links. The suggestion popup supports mouse selection, keyboard navigation, Escape dismissal, lifecycle cleanup, and viewport-aware positioning.
Editor and private-note integration
frontend/apps/main/src/components/editor/TextEditor.vue, frontend/apps/main/src/features/conversation/ReplyBoxContent.vue
TextEditor accepts ticket-reference settings and opens reference links in protected new tabs. ReplyBoxContent enables references for private notes and supplies the conversation search callback.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes satisfy issue #468 by enabling #conversation references in private notes and linking them to accessible conversations.
Out of Scope Changes check ✅ Passed All code changes support private-note conversation references, including suggestions, linking, editor integration, and tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding conversation reference links to private notes.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/apps/main/src/components/editor/TextEditor.vue`:
- Around line 291-315: Update TicketReference to parse its rendered
a.ld-ticket-reference markup on HTML reload: add a parseHTML rule matching the
ticket-reference anchor, map its href into the node’s href attribute, and render
a data-type marker consistent with the parser. Add a round-trip test that
serializes and reloads a ticket reference, preserving its node type, label, and
href.

In `@frontend/apps/main/src/components/editor/TicketReferenceList.vue`:
- Around line 26-28: Move the minimum-query guidance from the hardcoded text in
TicketReferenceList’s query-length hint into the locale messages, defining a
translated message that accepts the minimum length as a count parameter. Update
the span to render that message through $t while passing
MIN_REFERENCE_QUERY_LENGTH, preserving the existing visibility condition.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: da4e04da-985d-408e-b2ef-cd6fa8a9aeb3

📥 Commits

Reviewing files that changed from the base of the PR and between 51a7b95 and 197b7f9.

📒 Files selected for processing (6)
  • frontend/apps/main/src/components/editor/TextEditor.vue
  • frontend/apps/main/src/components/editor/TicketReferenceList.vue
  • frontend/apps/main/src/components/editor/ticketReference.js
  • frontend/apps/main/src/components/editor/ticketReference.test.js
  • frontend/apps/main/src/components/editor/ticketReferenceSuggestion.js
  • frontend/apps/main/src/features/conversation/ReplyBoxContent.vue

Comment on lines +291 to +315
const TicketReference = Mention.extend({
name: 'ticketReference',

addAttributes() {
return {
...this.parent?.(),
href: { default: null }
}
},

renderText({ node }) {
return `#${node.attrs.label}`
},

renderHTML({ node, HTMLAttributes }) {
return [
'a',
{
...HTMLAttributes,
href: node.attrs.href,
class: [HTMLAttributes.class, 'ld-ticket-reference'].filter(Boolean).join(' ')
},
`#${node.attrs.label}`
]
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the declared TipTap versions and existing round-trip coverage.
fd -HI -t f 'package.json' frontend | while IFS= read -r file; do
  rg -n -C2 '"`@tiptap/`(extension-mention|extension-link|vue-3)"' "$file" || true
done

# Locate tests and editor paths that should cover HTML serialization and reload.
rg -n -C3 \
  --glob '*.{test,spec}.{js,ts,vue}' \
  'ticketReference|ld-ticket-reference|setContent|getHTML|handleDOMEvents' \
  frontend/apps/main || true

Repository: abhinavxd/libredesk

Length of output: 1549


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== referenced file outline =="
ast-grep outline frontend/apps/main/src/components/editor/TextEditor.vue --match TicketReference --view expanded || true

echo "== TextEditor relevant sections =="
sed -n '260,420p' frontend/apps/main/src/components/editor/TextEditor.vue

echo "== package versions (all package.json) =="
python3 - <<'PY'
import json, pathlib
for p in pathlib.Path('frontend/apps/main').rglob('package.json'):
    data=json.loads(p.read_text())
    deps={}
    for k in ['dependencies','devDependencies','peerDependencies','resolutions?']:
        if k in data:
            for n,v in data[k].items():
                if n.startswith('`@tiptap/`'):
                    deps[n]=v
    if deps:
        print(p)
        for k,v in deps.items(): print(f'  {k}: {v}')
PY

echo "== node_modules mention/link source availability =="
fd 'mention\.js$|link\.js$|mention\.ts$|link\.ts$' . -d 3 | sed -n '1,40p'

Repository: abhinavxd/libredesk

Length of output: 4429


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== git status/stat =="
git status --short
git diff --stat || true

echo "== TextEditor around TicketReference with line numbers =="
rg -n -C4 '\bTicketReference\b|CustomMention|handleDOMEvents|ld-ticket-reference' frontend/apps/main/src/components/editor/TextEditor.vue

echo "== editor tests around ticketReference =="
sed -n '1,240p' frontend/apps/main/src/components/editor/ticketReference.test.js

echo "== search for TicketReference tests/usages =="
rg -n -C3 'TicketReference|ticketReference|ld-ticket-reference|ticket-reference' frontend/apps/main || true

Repository: abhinavxd/libredesk

Length of output: 15557


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Fetch `@tiptap/extension-mention` 2.27.1 =="
python3 - <<'PY'
import urllib.request, json, textwrap
url='https://registry.npmjs.org/@tiptap/extension-mention/2.27.1'
try:
    data=json.load(urllib.request.urlopen(url, timeout=20))
    dist=data['dist']
    print('version', data.get('version'))
    print('tarball', dist.get('tarball'))
except Exception as e:
    print('registry_fetch_error', e)
PY

TARBALL='https://registry.npmjs.org/@tiptap/extension-mention/2.27.1'
tmp="$(mktemp -d)"
cd "$tmp"
curl -Lfs "$TARBALL/package.tgz" -o mention.tgz 2>/dev/null || curl -Lfs "$TARBALL" -o mention.tgz
tar -tzf mention.tgz | sed -n '1,80p'
echo "-- src/mention.ts --"
tar -xOzf mention.tgz package/src/mention.ts | sed -n '1,260p'
echo "-- package/dist/esm/*.d.ts (links) --"
tar -tzf mention.tgz | rg '(^package/dist/esm/|\.d\.ts$)' | rg 'mention|link|node' | sed -n '1,120p'
echo "-- tar export of likely mention/link files --"
for f in package/src/mention.ts package/src/mention-extension.ts package/src/utilities.ts; do
  tar -xOzf mention.tgz "$f" 2>/dev/null | sed -n '1,220p' || true
done

Repository: abhinavxd/libredesk

Length of output: 463


🌐 Web query:

@tiptap/extension-mention parseHTML data-type mention node 2.27.1

💡 Result:

In Tiptap, the @tiptap/extension-mention node identifies and parses mentions by looking for a specific HTML structure, primarily the data-type="mention" attribute on a span element [1][2]. When the Tiptap editor parses HTML (e.g., via setContent), it uses the parseHTML method defined in the Mention extension [3][1]. By default, this is implemented as: parseHTML { return [ { tag: 'span[data-type="mention"]', }, ] }, To ensure that your HTML is correctly parsed back into a mention node, your serialized HTML must include this attribute [2]. For example: @John Doe Key implementation details regarding this process include: 1. Identification: The parseHTML rule specifically targets span elements with the attribute data-type="mention" [1][2]. If your custom implementation or older versions do not include this attribute in the rendered output, Tiptap will treat the element as plain text rather than a mention node [2][4]. 2. Attribute Parsing: While the core mention node identifies itself via data-type, individual attributes (like id or label) are handled by the addAttributes method [1]. These are mapped to data-attributes (e.g., data-id, data-label) in the HTML [1]. 3. Customization: If you need to support a different HTML structure (e.g., an anchor tag or different data attributes), you can extend the Mention node and override the parseHTML and renderHTML methods to match your specific requirements [5][3][2]. 4. Versioning Note: Issues regarding data attributes not being correctly included or parsed have been identified in specific version releases (e.g., related to regressions in how attributes are passed to renderHTML) [6][4]. Ensure you are using an up-to-date version of the @tiptap/extension-mention package to avoid these known defects [4]. If you are experiencing issues where mentions are pasted or loaded as plain text, verify that the source HTML contains the data-type="mention" attribute exactly as expected by the extension's parseHTML rule [2].

Citations:


🌐 Web query:

github ueberdosis/tiptap packages extension-mention src mention.ts 2.27.1

💡 Result:

The file mention.ts located at packages/extension-mention/src/mention.ts in the ueberdosis/tiptap repository is a core component of the Tiptap Mention extension [1][2]. This extension enables users to insert mentions (typically triggered by a character like '@') into the editor [1]. In version 2.27.1 of Tiptap, this file underwent minor updates as part of the package's maintenance [3]. The source code defines the Mention node, which manages attributes such as id, label, and mentionSuggestionChar, and provides methods for rendering these mentions in both text and HTML formats [1][4]. You can view the current version of the source code directly on GitHub [1]. Detailed official documentation and usage instructions for the Mention extension are available on the Tiptap website [2].

Citations:


🌐 Web query:

github tip tip extension-mention 2.27.1 mention.ts parseHTML renderHTML

💡 Result:

The Tiptap Mention extension (found in mention.ts) manages how mention nodes are parsed from and rendered to HTML [1][2]. ParseHTML The extension uses a parseHTML method to identify mention nodes in the DOM [1][2]. It looks for span elements that have a data-type attribute matching the extension's name (which defaults to "mention"): parseHTML { return [ { tag: 'span[data-type="${this.name}"]', }, ] }, Specific attributes like id, label, and mentionSuggestionChar are parsed via the addAttributes configuration, which extracts values from corresponding data attributes (e.g., data-id, data-label) [1][2]. RenderHTML The renderHTML method controls how the mention node is converted back to HTML [1][2]. It retrieves the suggestion configuration based on the mentionSuggestionChar, then prioritizes a custom renderHTML function provided via the extension's options if defined [1][2][3]. If no custom renderer is provided, it defaults to rendering a span element [1][2]: renderHTML({ node, HTMLAttributes }) { const suggestion = getSuggestionFromChar(this, node.attrs.mentionSuggestionChar) //... (custom rendering logic) const mergedOptions = {...this.options } //... (attributes merging) const html = this.options.renderHTML({ options: mergedOptions, node, suggestion }) if (typeof html === 'string') { return ['span', mergeAttributes({ 'data-type': this.name }, this.options.HTMLAttributes, HTMLAttributes), html] } return html }, This architecture allows developers to customize the rendered output (e.g., changing the span to an anchor tag or adding specific CSS classes) by configuring the extension [4]. Note that direct usage of renderLabel is deprecated in favor of this integrated renderHTML and renderText approach [1][2][3].

Citations:


🌐 Web query:

@tiptap/extension-link 2.11.2 Link parser href CSS class html

💡 Result:

In Tiptap version 2.11.2, the @tiptap/extension-link extension provides built-in mechanisms to handle HTML attributes, including CSS classes, both globally and per-link [1][2][3]. Global Configuration You can define a default CSS class for all links by configuring the extension during editor initialization. This class will be applied to every rendered tag [1][3]. import Link from '@tiptap/extension-link'; Link.configure({ HTMLAttributes: { class: 'my-custom-class', }, }); Dynamic Per-Link Attributes You can also set or change the CSS class for specific links dynamically when using the setLink or toggleLink commands [1][3][4]. // Example of setting a class dynamically on a specific link editor.chain.focus.setLink({ href: 'https://example.com', class: 'my-specific-class' }).run; HTML Parsing The extension handles HTML parsing via the parseHTML method, which identifies tags in your input HTML [2]. It preserves the href attribute by default [2]. Regarding custom classes, because the class attribute is included in the extension's addAttributes definition, Tiptap automatically parses and maintains the class attribute present on existing tags in your source HTML [2]. Note on TypeScript Support: While the class attribute has been functional in the underlying extension logic for some time, if you encounter issues with TypeScript autocompletion or type errors when passing the class attribute to setLink, ensure you are aware that explicit type definitions for class in the Command interface were formalized in later updates [5]. In version 2.11.2, if type-checking is strict, you may need to cast the attributes object or ensure your project dependencies are correctly aligned with the version's capabilities [5].

Citations:


🌐 Web query:

tiptap 2.11.2 link extension parseHTML data-type href class

💡 Result:

In Tiptap 2.11.2, the Link extension handles href, class, and other attributes through the addAttributes() method [1][2]. By default, the extension defines these attributes to ensure they are correctly parsed from and rendered to HTML [1][2]. To handle custom attributes such as data-type, you must extend the Link extension and add them to the addAttributes() configuration [3]. ### Managing Attributes in Link Extension The Link extension uses addAttributes() to define how specific attributes are stored in the editor's state and synchronized with the DOM [1][2]. 1. Default Attributes: The href, target, rel, class, and title attributes are included by default [1][2]. The parseHTML function for href specifically extracts the value using element.getAttribute('href') [1][2]. 2. Adding Custom Attributes: To support custom attributes like data-type, you should extend the base Link extension. You must define both parseHTML (to read the attribute from existing HTML) and renderHTML (to write the attribute back to the DOM) [3]. Example of extending the Link extension for custom attributes: javascript import Link from '`@tiptap/extension-link`'; const CustomLink = Link.extend({ addAttributes() { return { // Keep default attributes ...this.parent?.(), // Add custom attribute 'data-type': { default: null, parseHTML: (element) => element.getAttribute('data-type'), renderHTML: (attributes) => { if (!attributes['data-type']) { return {}; } return { 'data-type': attributes['data-type'], }; }, }, }; }, }); ### Important Considerations * Parsing Logic: The main parseHTML() method in the Link extension is primarily responsible for identifying which DOM nodes should be treated as links (using the tag: 'a[href]' rule) and performing XSS validation [1][2]. Attribute-level parsing (like href or class) is handled inside addAttributes() [1][2]. * Attribute Persistence: When adding custom attributes, ensure you include them in both parseHTML and renderHTML within addAttributes to prevent them from being lost during editor updates or content synchronization [3]. * HTMLAttributes Option: For global settings (like a default CSS class for all links), you can use the HTMLAttributes option in the extension configuration [4][2][5], which is merged automatically during rendering [1].

Citations:


Preserve ticket-reference nodes across HTML reload.

TicketReference renders as <a class="ld-ticket-reference" href="...">#<label></a>, but the inherited Mention parser only recognizes custom mentions from span[data-type="<extension-name>"] markup. When saved HTML is reloaded, the anchor can be parsed as a regular link and bypass the closest('a.ld-ticket-reference') click handler.

Add a parseHTML rule for ticket-reference anchors, render a marker such as data-type, parse href into node attributes, and add a reload round-trip test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/apps/main/src/components/editor/TextEditor.vue` around lines 291 -
315, Update TicketReference to parse its rendered a.ld-ticket-reference markup
on HTML reload: add a parseHTML rule matching the ticket-reference anchor, map
its href into the node’s href attribute, and render a data-type marker
consistent with the parser. Add a round-trip test that serializes and reloads a
ticket reference, preserving its node type, label, and href.

Comment on lines +26 to +28
<span v-if="query.length < MIN_REFERENCE_QUERY_LENGTH" class="text-sm text-muted-foreground">
Type at least {{ MIN_REFERENCE_QUERY_LENGTH }} digits to search tickets
</span>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Move the minimum-query guidance into the locale messages.

Line 27 always renders English text. Add a translated message with a count parameter, then render it through $t. This keeps the private-note editor usable in non-English locales.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/apps/main/src/components/editor/TicketReferenceList.vue` around
lines 26 - 28, Move the minimum-query guidance from the hardcoded text in
TicketReferenceList’s query-length hint into the locale messages, defining a
translated message that accepts the minimum length as a count parameter. Update
the span to render that message through $t while passing
MIN_REFERENCE_QUERY_LENGTH, preserving the existing visibility condition.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request] Link to ticket by reference number in private note

1 participant