From de95df44c1e889e3916897c1581734da99942ba6 Mon Sep 17 00:00:00 2001 From: kristianwind Date: Mon, 24 Aug 2026 13:11:46 +0200 Subject: [PATCH] fix(notes): a [[note]] link is one you can follow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Picking a note from the [[ completion wrote [[Title]] and the server made a real link from it — but the link only ever showed in the panel under the note as plain text. It said the note pointed somewhere and gave no way to go there, which reads as "it didn't become a link at all". The links in that panel are buttons now. A [[note]] resolves by its title to the note — the one already on the list, or looked up if not — and opens it in place; a #project goes to its page; a task link to the task. Titles with punctuation work, brackets and parentheses included, because the match is on the whole title, not a parse of it. e2e: after linking a note, clicking its chip opens the note it names. Co-Authored-By: Claude Opus 5 --- web/e2e/smoke.spec.js | 4 +++ web/src/lib/locales/da.js | 2 ++ web/src/lib/locales/en.js | 2 ++ web/src/routes/noter/+page.svelte | 51 ++++++++++++++++++++++++++++++- 4 files changed, 58 insertions(+), 1 deletion(-) diff --git a/web/e2e/smoke.spec.js b/web/e2e/smoke.spec.js index 39e6bb4..3bfaf02 100644 --- a/web/e2e/smoke.spec.js +++ b/web/e2e/smoke.spec.js @@ -2651,6 +2651,10 @@ test('[[ foreslår en note, og linket lander i noten', async ({ page }) => { await ed.blur(); await expect(page.locator('.links')).toContainText('Rejseplan til Berlin'); + // And it is a link you can follow: clicking the chip opens the note it names. + await page.locator('.links .link', { hasText: 'Rejseplan til Berlin' }).click(); + await expect(ed.locator('h1')).toHaveText('Rejseplan til Berlin'); + expect(trouble).toEqual([]); }); diff --git a/web/src/lib/locales/da.js b/web/src/lib/locales/da.js index 8ce9897..41d1b34 100644 --- a/web/src/lib/locales/da.js +++ b/web/src/lib/locales/da.js @@ -22,6 +22,8 @@ export const da = { 'notes.unshared': 'Noten er din igen.', // Deling med enkeltpersoner, ved siden af projektet. 'notes.groupSharedWithMe': 'Delt med mig', + 'notes.linkOpen': 'Åbn', + 'notes.linkMissing': 'Noten blev ikke fundet.', 'notes.sharedBy': 'Delt af {name}', 'notes.sharePeople': 'Del med personer', 'notes.sharePick': 'Vælg en person …', diff --git a/web/src/lib/locales/en.js b/web/src/lib/locales/en.js index dab402b..f7ad590 100644 --- a/web/src/lib/locales/en.js +++ b/web/src/lib/locales/en.js @@ -18,6 +18,8 @@ export const en = { 'notes.unshared': 'The note is yours again.', // Sharing with individual people, alongside the project. 'notes.groupSharedWithMe': 'Shared with me', + 'notes.linkOpen': 'Open', + 'notes.linkMissing': 'That note could not be found.', 'notes.sharedBy': 'Shared by {name}', 'notes.sharePeople': 'Share with people', 'notes.sharePick': 'Choose someone …', diff --git a/web/src/routes/noter/+page.svelte b/web/src/routes/noter/+page.svelte index 5f38220..b6dc2bb 100644 --- a/web/src/routes/noter/+page.svelte +++ b/web/src/routes/noter/+page.svelte @@ -14,6 +14,7 @@ import { api, humanMessage } from '$lib/api.js'; import { app } from '$lib/stores.svelte.js'; import { page } from '$app/stores'; + import { goto } from '$app/navigation'; import { t, tag } from '$lib/i18n.svelte.js'; import { NOTE, startDrag } from '$lib/dnd.js'; import { colorVar } from '$lib/colors.js'; @@ -653,6 +654,44 @@ // What the note points at, for the panel under the editor. let links = $derived(selected?.links ?? []); + /** + * Follows a link from the panel to the thing it names. + * + * A [[note]] resolves by its title to a note — the one on the list if it is + * there, otherwise looked up — and opens it in place. A #project and a task go + * to their own pages. Until now these were text and nothing else: the note said + * it pointed somewhere and there was no way to go. + */ + async function openLink(link) { + if (link.kind === 'note') { + const wanted = link.target_id.toLowerCase(); + const here = notes.find((n) => (n.title ?? '').toLowerCase() === wanted); + if (here) { + open(here); + return; + } + try { + const found = (await api.notes({ q: link.target_id })).notes ?? []; + const hit = found.find((n) => (n.title ?? '').toLowerCase() === wanted) ?? found[0]; + if (hit) open(hit); + else app.toast(t('notes.linkMissing')); + } catch (e) { + app.toast(humanMessage(e)); + } + return; + } + if (link.kind === 'project') { + const project = app.projects.find( + (p) => p.name.toLowerCase() === link.target_id.toLowerCase() + ); + if (project) goto(`/projekt/${project.id}`); + return; + } + if (link.kind === 'task') { + goto(`/opgave/${link.target_id}`); + } + } + @@ -889,7 +928,9 @@ {#if links.length} {#each links as link} - {label(link)} + {/each} {/if} @@ -1615,9 +1656,17 @@ .link { color: var(--ink-muted); background: var(--surface); + border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 0 var(--s1); font-family: var(--mono, ui-monospace, monospace); + font-size: inherit; + cursor: pointer; + } + .link:hover { + color: var(--ink); + background: var(--surface-raised); + border-color: var(--line-strong); } /* Sharing lives in a popover above the Del button, so the footer stays a status