diff --git a/web/e2e/smoke.spec.js b/web/e2e/smoke.spec.js index 9009a41..965df77 100644 --- a/web/e2e/smoke.spec.js +++ b/web/e2e/smoke.spec.js @@ -3303,3 +3303,78 @@ test('en opgave kan få et klokkeslæt, og beholder det når den flyttes', async expect(trouble).toEqual([]); }); + +/** + * Notelisten står i grupper. + * + * Den føltes tæt, og grunden var ikke afstanden mellem rækkerne: hver række sagde + * tre ting, og to af dem gentog sig selv nedad. Datoen stod på hver eneste række, + * også når tyve noter deler dag, og projektet fik en linje for sig selv. + * + * Datoen er flyttet op i en overskrift, favoritterne har fået deres egen gruppe — + * de lå allerede først, der stod bare ikke hvorfor — og arkivet grupperes på samme + * måde, for det er dér, de tolv hundrede importerede ligger. + */ +test('notelisten grupperer favoritter og datoer, også i arkivet', async ({ page }) => { + const trouble = watchForTrouble(page); + await page.goto('/noter'); + + for (const title of ['Gruppe én', 'Gruppe to']) { + await page.getByRole('button', { name: 'Ny note' }).click(); + const ed = page.getByRole('textbox', { name: 'Notens tekst' }); + await ed.click(); + await page.keyboard.type(title); + await page.keyboard.press('Enter'); + await page.keyboard.type('Brødtekst i ' + title); + await expect(ed.locator('h1')).toHaveText(title); + await page.waitForTimeout(800); + } + await page.reload(); + + // Skrevet i dag, så de står under dagens overskrift. + const today = page.getByRole('heading', { name: /^I dag/ }); + await expect(today, 'der er ingen datogruppe').toBeVisible(); + + // En favorit får sin egen gruppe. Uden overskriften ligger den bare først, og + // der står ikke hvorfor. + await expect(page.getByRole('heading', { name: /Favoritter/i })).toBeHidden(); + const row = page.locator('li').filter({ hasText: 'Gruppe én' }).first(); + await row.hover(); + await row.getByRole('button', { name: /favorit/i }).click(); + const favourites = page.getByRole('heading', { name: /Favoritter/i }); + await expect(favourites).toBeVisible(); + + // Og den ligger under den, ikke under dagen. + const inFavourites = page + .locator('h3.group', { hasText: /Favoritter/i }) + .locator('+ ul li') + .filter({ hasText: 'Gruppe én' }); + await expect(inFavourites).toBeVisible(); + + // Grupper kan foldes sammen. Ikke husket på tværs af besøg: en gruppe, der + // åbner foldet uden at nogen bad om det, er en liste, der ser tom ud. + await favourites.getByRole('button').click(); + await expect(inFavourites).toBeHidden(); + await favourites.getByRole('button').click(); + await expect(inFavourites).toBeVisible(); + + // En søgning grupperes ikke: serveren har sorteret efter hvor godt hver note + // matchede, og at dele det op i måneder ville stille rækkefølgen om efter noget + // andet end det, der blev spurgt om. + await page.getByPlaceholder('Søg i noter').fill('Brødtekst'); + await expect(page.locator('li').filter({ hasText: 'Gruppe to' }).first()).toBeVisible(); + await expect(page.locator('h3.group')).toHaveCount(0); + await page.getByPlaceholder('Søg i noter').fill(''); + + // Arkivet grupperes på samme måde. Det er dér, det betyder mest. + await row.hover(); + await row.getByRole('button', { name: /Arkivér/i }).click(); + await page.getByRole('button', { name: 'Vis arkivet' }).click(); + await expect( + page.locator('h3.group'), + 'arkivet står ugrupperet' + ).not.toHaveCount(0); + await expect(page.locator('li').filter({ hasText: 'Gruppe én' }).first()).toBeVisible(); + + expect(trouble).toEqual([]); +}); diff --git a/web/src/lib/locales/da.js b/web/src/lib/locales/da.js index 2489d52..0edd7e7 100644 --- a/web/src/lib/locales/da.js +++ b/web/src/lib/locales/da.js @@ -75,6 +75,11 @@ export const da = { 'notes.syntaxProject': 'projekt', 'notes.syntaxNote': 'en anden note', 'notes.yesterday': 'i går', + 'notes.groupFavourites': 'Favoritter', + 'notes.groupToday': 'I dag', + 'notes.groupYesterday': 'I går', + 'notes.groupWeek': 'Denne uge', + 'notes.groupUnknown': 'Uden dato', 'notes.title': 'Noter', 'notes.new': 'Ny note', 'notes.search': 'Søg i noter', diff --git a/web/src/lib/locales/en.js b/web/src/lib/locales/en.js index 187fe92..4797581 100644 --- a/web/src/lib/locales/en.js +++ b/web/src/lib/locales/en.js @@ -71,6 +71,11 @@ export const en = { 'notes.syntaxProject': 'project', 'notes.syntaxNote': 'another note', 'notes.yesterday': 'yesterday', + 'notes.groupFavourites': 'Favourites', + 'notes.groupToday': 'Today', + 'notes.groupYesterday': 'Yesterday', + 'notes.groupWeek': 'This week', + 'notes.groupUnknown': 'No date', 'notes.title': 'Notes', 'notes.new': 'New note', 'notes.search': 'Search notes', diff --git a/web/src/routes/noter/+page.svelte b/web/src/routes/noter/+page.svelte index dbe11cc..7b24a32 100644 --- a/web/src/routes/noter/+page.svelte +++ b/web/src/routes/noter/+page.svelte @@ -16,6 +16,7 @@ import { page } from '$app/stores'; import { t, tag } from '$lib/i18n.svelte.js'; import { NOTE, startDrag } from '$lib/dnd.js'; + import { colorVar } from '$lib/colors.js'; import NoteEditor from '$lib/components/NoteEditor.svelte'; let notes = $state([]); @@ -71,6 +72,100 @@ const field = order === 'created' ? 'created_at' : 'updated_at'; return new Date(b[field]).getTime() - new Date(a[field]).getTime(); }); + + // --- listen i grupper ---------------------------------------------------------- + // + // Listen føltes tæt, og grunden var ikke afstanden mellem rækkerne: hver række + // sagde tre ting, og to af dem gentog sig selv nedad. Datoen stod på hver eneste + // række — også når tyve noter deler dag — og projektet fik en linje for sig selv. + // Tre linjer pr. note, gange tolv hundrede. + // + // Gentagelserne flyttes derhen, hvor de siger noget. Datoen bliver til + // overskriften over en gruppe, og så forsvinder den fra rækken; projektet bliver + // en lille mærkat ude til højre. Noten går fra tre linjer til to, og luften + // kommer af, at der er mindre at skrive — ikke af at listen bliver længere. + + /** Grupper, der er foldet sammen. Kun i denne session. */ + let folded = $state(new Set()); + + function toggleGroup(key) { + const next = new Set(folded); + next.has(key) ? next.delete(key) : next.add(key); + folded = next; + } + + /** + * Hvilken dato en gruppe måles på: den, der er sorteret efter. + * + * Ellers ville listen stå i én rækkefølge og være grupperet efter en anden, og + * så er overskrifterne løgn — en note ville stå under "August" mellem to fra + * juli, fordi den var *rørt* i august og *oprettet* i juli. + */ + const groupDate = (note) => (order === 'created' ? note.created_at : note.updated_at); + + /** + * Hvad en gruppe hedder. I dag, i går, denne uge — og derefter måneden. + * + * Nøglen og etiketten er to ting: etiketten er sproget, og nøglen er den, der + * afgør om to noter hører sammen. Måneden som nøgle må bære året med sig, eller + * ville august 2026 og august 2025 blive én bunke. + */ + function bucketOf(note) { + if (order === 'title') { + // Sorteret på navn er der ingen dato at gruppere på, så det bliver + // forbogstavet — og datoen kommer tilbage i rækken, hvor den så er den + // eneste, der siger noget. + const first = (note.title || '').trim().charAt(0).toUpperCase(); + const letter = /\p{L}|\p{N}/u.test(first) ? first : '#'; + return { key: 'a-' + letter, label: letter }; + } + + const at = new Date(groupDate(note)); + if (Number.isNaN(at.getTime())) return { key: 'ukendt', label: t('notes.groupUnknown') }; + + const now = new Date(); + const midnight = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + const days = Math.floor((midnight - new Date(at.getFullYear(), at.getMonth(), at.getDate())) / 86400000); + + if (days <= 0) return { key: 'i-dag', label: t('notes.groupToday') }; + if (days === 1) return { key: 'i-gaar', label: t('notes.groupYesterday') }; + if (days < 7) return { key: 'ugen', label: t('notes.groupWeek') }; + + const label = at.toLocaleDateString(tag(), { month: 'long', year: 'numeric' }); + return { key: `m-${at.getFullYear()}-${at.getMonth()}`, label }; + } + + /** + * Listen, delt op. + * + * En søgning grupperes ikke: serveren har sorteret efter hvor godt hver note + * matchede, og at dele det op i måneder ville stille rækkefølgen om efter noget, + * der ikke er svaret på det, der blev spurgt om. + */ + let groups = $derived.by(() => { + if (query.trim()) return [{ key: 'fundet', label: '', notes: ordered, plain: true }]; + + const out = []; + const favourites = ordered.filter((n) => n.pinned); + if (favourites.length) { + out.push({ key: 'favoritter', label: t('notes.groupFavourites'), notes: favourites, star: true }); + } + + let current = null; + for (const note of ordered) { + if (note.pinned) continue; + const bucket = bucketOf(note); + if (!current || current.key !== bucket.key) { + current = { key: bucket.key, label: bucket.label, notes: [] }; + out.push(current); + } + current.notes.push(note); + } + return out; + }); + + /** Projektets egen farve, som i sidebjælken. */ + const projectColour = (id) => colorVar(app.projects.find((p) => p.id === id)?.color); let selected = $state(null); // Hvilken række der er valgt. Se open(): editoren venter på hele noten. let selectedId = $state(null); @@ -258,6 +353,25 @@ * which is exactly the thing the editor was built to stop showing. What is * wanted here is the sentence, not the notation. */ + /** + * Uddraget, uden titlen foran. + * + * Titlen *er* første linje — det er hele reglen for, hvad en note hedder — så et + * uddrag, der begynder fra toppen, siger navnet én gang til. Det var svært at se, + * så længe en dato stod imellem dem; nu står de oven på hinanden, og så står der + * "Verdande" og under det "Verdande Forbindelse til connectoren". + */ + function preview(note) { + const body = note.preview ?? note.body ?? ''; + const nl = body.indexOf('\n'); + const rest = nl === -1 ? '' : body.slice(nl + 1); + // Kun hvis den første linje faktisk *er* titlen. Et uddrag fra serveren er + // afkortet, så en lang første linje kan være hele uddraget — og så er der + // ikke andet at vise end den. + const withoutTitle = plain(rest); + return withoutTitle || plain(body); + } + function plain(body) { return (body ?? '') // Et billede har ingen ord i sig. Uden det her stod der @@ -515,69 +629,99 @@ {query ? t('notes.noneFound') : showArchive ? t('notes.emptyArchive') : t('notes.noneYet')}
{:else} -