Skip to content

feat(editor): render same-origin task links as task pills - #3544

Merged
kolaente merged 6 commits into
mainfrom
jumpy-bullfrog
Aug 20, 2026
Merged

feat(editor): render same-origin task links as task pills#3544
kolaente merged 6 commits into
mainfrom
jumpy-bullfrog

Conversation

@tink-bot

Copy link
Copy Markdown
Collaborator

Links to /tasks/:id on own instance render as bare urls in descriptions and comments — no title, no done state, no way to tell what is behind them without clicking. Now: same-origin task url anchors (text == href) upgrade in-memory to inline taskLink node rendered as pill (identifier, title, done state, project prefix when task lives in another project, glance tooltip on hover). Stored HTML stays plain <a href>, byte-stable on re-save. Links to tasks user cannot see (403/404) degrade to plain link. Pasting bare task url inserts pill immediately.

Side fixes: StarterKit no longer registers its own link/underline copies (duplicate Link had openOnClick: true, opened new tab when clicking link in edit mode); TaskGlanceTooltip teleports into open <dialog> so it is visible inside task detail modal.

How to verify

  1. Create a project with tasks A, B, C. Mark C done. Create a second project with task D.
  2. Edit the description of task A and paste the full url of task B (e.g. http://localhost:4173/tasks/2).
  3. Expected: the url turns into a pill showing B's identifier and title immediately, still in edit mode. Save. Reload the page.
  4. Expected: the pill is still shown in preview mode; the stored description (GET /api/v1/tasks/<A>) contains only <a href="...">...</a>, no custom markup.
  5. Add links to C and D the same way.
  6. Expected: C's pill shows a strikethrough title and a check icon; D's pill is prefixed with the second project's name.
  7. Hover a pill for about a second, both on the task page and when the task is opened as a modal from the kanban view.
  8. Expected: the task glance card appears above the content in both cases.
  9. Click a pill in preview mode. Expected: navigates to that task. Click a pill in edit mode. Expected: it is selected (outline), no navigation, no new tab; Backspace deletes it.
  10. As another user without access to task B, add a link to B in one of your own tasks.
  11. Expected: the link renders as a plain, clickable link with the url as text.
  12. Add a link with custom text (<a href="/tasks/2">see B</a> via the link toolbar) and a link to another origin's /tasks/1.
  13. Expected: both stay plain links.

@tink-bot tink-bot added the pr-swarm/started PR Swarm run is currently active on this PR label Aug 18, 2026
@github-actions github-actions Bot added area/comments Task comments and discussion area/frontend Frontend-only code or UI work area/task-editor Task detail view, inline editing, rich text labels Aug 18, 2026
@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown

Preview Deployment

Preview deployments for this PR are available at:

URL Tag Commit
https://pr-3544.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:pr-3544 latest
https://sha-13fd9fedc9e808268dac4e035e4a6f91082c5999.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:sha-13fd9fedc9e808268dac4e035e4a6f91082c5999 13fd9fe
https://sha-3b3d44a1d3a2e2edfffc90fdb16e8c58f9b8ae48.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:sha-3b3d44a1d3a2e2edfffc90fdb16e8c58f9b8ae48 3b3d44a
https://sha-5d7b13d69324e2cc307671bc8c56f33f169c70ec.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:sha-5d7b13d69324e2cc307671bc8c56f33f169c70ec 5d7b13d
https://sha-e265b452c64d28ac969180637c90a81f0c97aaaa.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:sha-e265b452c64d28ac969180637c90a81f0c97aaaa e265b45
https://sha-1aa9aefddd245d83646ee3ab1e9a616d33700f96.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:sha-1aa9aefddd245d83646ee3ab1e9a616d33700f96 1aa9aef
https://sha-38488859277e5fc5016c4b6b0ff7090ec6c572fd.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:sha-38488859277e5fc5016c4b6b0ff7090ec6c572fd 3848885
https://sha-1fdf4431356fbba21492969eecf1bfc6d12c0e58.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:sha-1fdf4431356fbba21492969eecf1bfc6d12c0e58 1fdf443
https://sha-23013ee7a4daebf80f4a47172307198fcfdfdfda.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:sha-23013ee7a4daebf80f4a47172307198fcfdfdfda 23013ee
https://sha-1acc0c176e723a5e349369e5ce98e56c23e77a3f.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:sha-1acc0c176e723a5e349369e5ce98e56c23e77a3f 1acc0c1
https://sha-e6eb5eddfca169eef1c5533d6630316435c85c4c.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:sha-e6eb5eddfca169eef1c5533d6630316435c85c4c e6eb5ed

The preview environment will start automatically on first visit. Subsequent pushes to this PR will update the pr-3544 image — the preview picks up the new version on restart. The per-commit URLs point to a specific version and will not change.

Run locally with Docker
docker pull ghcr.io/go-vikunja/vikunja:pr-3544
docker run -p 3456:3456 ghcr.io/go-vikunja/vikunja:pr-3544

Last updated for commit 13fd9fe

@tink-bot tink-bot left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🤖 PR Swarm — automated review, round 1. See inline comments.

Comment thread frontend/src/helpers/taskLinkCache.ts Outdated
Comment thread frontend/src/helpers/taskLinkCache.ts
Comment thread frontend/src/components/input/editor/taskLink.ts
Comment thread frontend/src/components/input/editor/taskLink.ts
Comment thread frontend/src/components/input/editor/taskLink.ts
Comment thread frontend/src/components/input/editor/TaskLinkPill.vue
Comment thread frontend/src/components/input/editor/TaskLinkPill.vue
Comment thread frontend/src/components/input/editor/taskLinkContext.ts Outdated
Comment thread frontend/src/helpers/getTopLayerContainer.ts
Comment thread frontend/src/helpers/parseTaskIdFromUrl.ts
tink-bot pushed a commit that referenced this pull request Aug 18, 2026
@tink-bot

tink-bot commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator Author

🤖 PR Swarm — automated review. Panel: bughunter · security · conventions · design · verifier — decision round @ 5d7b13d (+553/−480 this round, PR +1641/−327 after rebase onto main)

Verdict: ✅ APPROVE

Decision round: maintainer answered all 5 deferred items — 2 implemented here, 1 split into a follow-up PR, 2 accepted as-is. Branch rebased onto main (23 commits, no merge commit).

Round 3 (before fixes): 4 MEDIUM on the round-2 refetch path (identity change kept stale task visible, no post-await race guard, failed refetch blanked pill, loader-injection shape) — all fixed. Round cap reached (3 rounds); remaining open items are the design/perf calls below. Rounds 1→3 grew the PR from +819 to +1260 (tests, helpers, cache lifecycle) — no core reshaping.

❓ Needs your input (0)

All decided:

  • fetch fan-out — accepted as is (maintainer)
  • no TTL — accepted as is (maintainer)
  • TimeEntryList duplicate fetch — consolidated: cache renamed helpers/taskCache.ts + helpers/fetchTaskById.ts; TimeEntryList resolves tasks through it 9f73ec2, 535a10e
  • tests on bare StarterKit — fixed: createEditorExtensions(deps) extracted from TipTap.vue (pure move); taskLink tests run on the real config; new paste-order test fails without priority: 200 (distinguishing signal: only TaskLink's paste path applies target/rel) 1c874ad, 5d7b13d
  • hover-only glance tooltip — follow-up PR (maintainer)

✅ Fixed this round (7)

  • TaskLinkPill.vue — separate identity signal drops the shown task on identity change (no refetch) 3848885
  • TaskLinkPill.vue — post-await guard discards superseded results (id / cache version / identity version) 3848885
  • TaskLinkPill.vue — failed refetch keeps stale task; unchanged task object skips re-render 3848885
  • helpers/taskLinkCache.ts (state leaf) + helpers/fetchTaskForLink.ts (single-arg fetch) replace loader injection; identity-checked eviction; tests restored to mock @/services/task 3848885
  • stores/auth.ts — cache cleared from a watch on user id/type (covers login, logout, link-share, cross-tab adoption); helpers/auth.ts back to a leaf 67c0d93
  • services/task.tsinvalidateTaskLink(model.id) 3848885
  • TaskLinkPill.vue — errored pills stay on screen across unrelated refetches 1aa9aef

💤 Resolved as nit (3)

  • global taskLinkCacheVersion (vs per-id) — unrelated pills hit the cached promise, no extra requests; identical-object check removes render churn. YAGNI. [design]
  • clearTaskLinkCache bump causing 401 burst on logout — moot: identity signal no longer refetches. [design + bughunter]
  • comment tightening (2-line whys, // separator) — applied in 3848885; vi.mock('@/services/task') in pill test removed with the split. [conventions]

🔎 Runtime verification (round 3, re-driven)

  • ✅ stale pill after edit in modal → back: pill updates title + done + un-done, no reload (still-mounted editor)
  • ✅ kanban done-bucket drag → pill done on next mount (TaskBucketService.update hook)
  • ✅ favorite toggle → exactly one refetch, unrelated pill no request, no error state
  • ✅ logout → login as user without access, same tab: fallback link + real 403, no leaked title
  • ✅ regressions: paste pill (stored anchor carries target/rel), external link attrs unchanged, glance tooltip body/dialog, prefix suppression, ctrl+click + middle-click new tab, delete linked task → fallback on next mount
  • ✅ console: no new errors (linkifyjs warnings pre-existing, reproduced on pill-free control)
  • ❌→✅ errored pill flicker on unrelated updates — reproduced, fixed in 1aa9aef (not re-driven)
  • ⚠️ round-3 code landed mid-run; flows 1/3/4 re-driven on the new HEAD, results unchanged; final 1aa9aef covered by unit tests only

pill updated after modal edit
user2 fallback

more screenshots

kanban drag done
pasted pill saved
tooltip in modal

Reviewer summaries

Reviewer Assessment
🐛 bughunter Service-layer hooks + removeToken placement verified; found refetch race + error-on-refetch — fixed.
🛡 security Identity-change clear complete on all in-tab paths; identity must force-drop + post-await guard — fixed; cross-tab adoption now covered by the identity watch.
📏 conventions Layering/tests clean; comment nits — applied.
📐 design Loader injection wrong seam → module split; auth helper back to leaf; per-id versioning YAGNI.
🔎 verifier All 6 flows pass on HEAD; one errored-pill flicker → fixed.
Previous rounds (2) round 2 @ 1fdf443 — 💬 APPROVE WITH NITS: 9 fixed (identity-change cache clear, reactive invalidation, service-layer hooks, cache moved to helpers, a11y nits, pinia guard, shared link attrs, prop rename), 5 nits, 5 deferred. round 1 @ 23013ee⚠️ REQUEST CHANGES: 17 fixed (cache logout/invalidation, paste priority, modifier clicks, backdropView, union state, a11y/contrast, dialog helper, frontendUrl base, TipTap provide), 3 nits, 5 deferred.

@tink-bot tink-bot left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🤖 PR Swarm — automated review, round 2. See inline comments.

Comment thread frontend/src/helpers/auth.ts Outdated
Comment thread frontend/src/helpers/taskLinkCache.ts Outdated
Comment thread frontend/src/services/task.ts Outdated
Comment thread frontend/src/helpers/taskLinkCache.ts Outdated
Comment thread frontend/src/helpers/parseTaskIdFromUrl.ts
Comment thread frontend/src/components/input/editor/taskLink.ts
Comment thread frontend/src/components/input/editor/TipTap.vue
Comment thread frontend/src/components/input/editor/TaskLinkPill.vue
Comment thread frontend/src/components/input/editor/TaskLinkPill.vue
tink-bot pushed a commit that referenced this pull request Aug 18, 2026

@tink-bot tink-bot left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🤖 PR Swarm — automated review, round 3. See inline comments.

Comment thread frontend/src/components/input/editor/TaskLinkPill.vue Outdated
Comment thread frontend/src/components/input/editor/TaskLinkPill.vue
Comment thread frontend/src/components/input/editor/TaskLinkPill.vue
Comment thread frontend/src/helpers/fetchTaskForLink.ts Outdated
Comment thread frontend/src/stores/auth.ts Outdated
Comment thread frontend/src/services/task.ts Outdated
Comment thread frontend/src/components/input/editor/TaskLinkPill.vue
tink-bot pushed a commit that referenced this pull request Aug 18, 2026
@tink-bot tink-bot added pr-swarm/needs-input PR Swarm finished, deferred items are waiting on maintainer input and removed pr-swarm/started PR Swarm run is currently active on this PR labels Aug 18, 2026
@tink-bot tink-bot added pr-swarm/started PR Swarm run is currently active on this PR pr-swarm/done PR Swarm finished, nothing deferred and removed pr-swarm/needs-input PR Swarm finished, deferred items are waiting on maintainer input pr-swarm/started PR Swarm run is currently active on this PR labels Aug 19, 2026
@kolaente
kolaente enabled auto-merge (rebase) August 19, 2026 19:50
@kolaente
kolaente disabled auto-merge August 19, 2026 19:50
Recognises the current origin plus base path and the configured
frontendUrl (comment permalinks, notification mails).
…glance tooltip

TaskGlanceTooltip now teleports into an open <dialog> so it is visible
above the task detail modal.
One request per task id shared by all consumers; 403/404 stay cached,
transient failures are evicted. TaskService/TaskBucketService
invalidate on update/delete, an auth-store watch clears the cache when
the identity changes, and a version ref lets mounted consumers refetch.
TimeEntryList resolves tasks through it instead of its own cache.
Same-origin task url anchors whose text equals their href are upgraded
in memory to an inline atom node and serialized back to the same plain
anchor, so stored HTML never carries special markup. The node view
fetches the task through the shared cache and renders identifier,
title, done state, a project prefix for foreign projects and the glance
tooltip; 403/404 degrade to a plain link. Pasting a bare task url
inserts a pill immediately.
…Extensions

Registers TaskLink, shares the Link mark's HTMLAttributes with pasted
pills, drops StarterKit's duplicate link/underline copies and renames
the mentionProjectId prop to projectId since it now also provides the
host project for task link pills.
…onfig

Includes a paste-order test that fails without the explicit TaskLink
priority over the markdown paste handler.
@kolaente
kolaente merged commit 1ae361c into main Aug 20, 2026
41 checks passed
@kolaente
kolaente deleted the jumpy-bullfrog branch August 20, 2026 06:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/comments Task comments and discussion area/frontend Frontend-only code or UI work area/task-editor Task detail view, inline editing, rich text pr-swarm/done PR Swarm finished, nothing deferred

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants