Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ed6f49589 | ||
|
|
de673260a3 | ||
|
|
804c81f74e | ||
|
|
87054ac8ca | ||
|
|
c62dcc3d7c |
@@ -819,6 +819,15 @@
|
||||
color: var(--accent);
|
||||
}
|
||||
</style>
|
||||
<!-- Besøgstælling for denne side — ikke for programmet, som melder det, der
|
||||
står under "It counts itself". Plausible på vores egen maskine: ingen
|
||||
cookie, ingen gemt adresse, ingen tredjepart, og derfor ingen bjælke at
|
||||
møde folk med. `defer`, så tælleren aldrig holder siden tilbage. -->
|
||||
<script
|
||||
defer
|
||||
data-domain="verdandetodo.com"
|
||||
src="https://plausible.yggdrasilpanel.com/js/script.outbound-links.js"
|
||||
></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
+135
-8
@@ -4657,16 +4657,23 @@ test('en søgning i ⌘K efterlader de træffere, den fandt', async ({ page }) =
|
||||
// Gemt ved at forlade feltet, som resten af fladen gør det — der er ingen
|
||||
// Gem-knap at huske.
|
||||
//
|
||||
// Der ventes på "Gemt", før den næste note laves. Uden det klikkes "Ny note",
|
||||
// mens den forrige stadig er undervejs, og listen når ikke at få den med — det
|
||||
// fejlede i CI én gang ud af tre kørsler, og bestod hver gang her på maskinen.
|
||||
// En prøve, der er hurtigere end programmet, måler maskinen den kører på.
|
||||
// Der ventes på et TOMT tekstfelt, før der skrives. Uden det kan udfyldningen
|
||||
// ramme den forrige notes felt, mens den nye er undervejs, og så bliver teksten
|
||||
// enten skrevet på den forkerte note eller tabt undervejs.
|
||||
//
|
||||
// Og der ventes IKKE på "Gemt": den tekst er hvilestanden, ikke et signal.
|
||||
// Feltet siger "Gemmer" mens en gemning kører og "Gemt" ellers — altså også før
|
||||
// den første gemning overhovedet er begyndt. En prøve, der venter på den, får
|
||||
// grønt med det samme og måler ingenting. Det var den fejl, der gjorde den her
|
||||
// prøve ustabil: rettelsen var en ventetid, der ikke kunne vente på noget.
|
||||
// Rækken i listen er det rigtige tegn — den kommer først, når serveren har
|
||||
// svaret.
|
||||
for (const title of ['Fyrreskov nord', 'Fyrreskov syd', 'Egeskov']) {
|
||||
await page.getByRole('button', { name: 'Ny note' }).click();
|
||||
const body = page.getByLabel('Notens tekst');
|
||||
await expect(body).toHaveText('');
|
||||
await body.fill(`${title}\n\nnoget om den`);
|
||||
await body.blur();
|
||||
await expect(page.getByText('Gemt', { exact: true })).toBeVisible();
|
||||
await expect(page.locator('.list').getByText(title, { exact: true })).toBeVisible();
|
||||
}
|
||||
|
||||
@@ -4687,14 +4694,60 @@ test('en søgning i ⌘K efterlader de træffere, den fandt', async ({ page }) =
|
||||
await expect(list.getByText('Fyrreskov syd')).toBeVisible();
|
||||
await expect(list.getByText('Egeskov')).toHaveCount(0);
|
||||
|
||||
// Og adressen lyver ikke, når feltet tømmes: uden det ville et genbesøg hente
|
||||
// en søgning tilbage, man selv havde ryddet væk.
|
||||
// Feltet kan tømmes, og listen kommer hel igen. Adressen beholder sit `q` —
|
||||
// den skrives kun af navigation. Den første udgave skrev den ved hvert
|
||||
// tastetryk, så den kunne deles, og det kostede bogstaver ud af feltet; se
|
||||
// prøven nedenfor.
|
||||
await page.getByPlaceholder('Søg i noter').fill('');
|
||||
await expect(page).not.toHaveURL(/[?&]q=/);
|
||||
await expect(list.getByText('Egeskov')).toBeVisible();
|
||||
|
||||
expect(trouble).toEqual([]);
|
||||
});
|
||||
|
||||
/**
|
||||
* En ny note åbnes med det samme, uden at editoren forsvinder undervejs.
|
||||
*
|
||||
* Serveren tømmer `body` på listerækker med vilje, og siden brugte den tomme krop
|
||||
* som tegn på "det her er en række, hent noten hel". En nyoprettet note er også
|
||||
* tom — og hel — så den blev sendt samme vej: editoren blev pillet af skærmen,
|
||||
* noten hentet igen, og editoren sat op på ny.
|
||||
*
|
||||
* Det hul var ikke kun et blink. Mens det stod åbent, var der ingen valgt note, og
|
||||
* gemningen går tavst tilbage uden en. Skrev man i det sekund, var teksten væk —
|
||||
* ingen fejl, ingen toast, bare en tom note. Det er også det, der gjorde
|
||||
* søgeprøven ovenfor ustabil: i CI var maskinen langsom nok til at ramme hullet.
|
||||
*
|
||||
* Prøven tæller opslagene frem for at måle et blink. Et blink er en tidsting og
|
||||
* ville kræve, at prøven var hurtigere end programmet — præcis den fejl, den er
|
||||
* skrevet for at lukke. Et opslag, der ikke sker, er et tal.
|
||||
*/
|
||||
test('en ny note hentes ikke igen, når den lige er lavet', async ({ page }) => {
|
||||
const trouble = watchForTrouble(page);
|
||||
|
||||
const fetched = [];
|
||||
page.on('request', (r) => {
|
||||
// Selve noten, ikke dens delinger: /notes/<id> og ikke /notes/<id>/shares.
|
||||
const m = new URL(r.url()).pathname.match(/^\/api\/v1\/notes\/([0-9a-f-]+)$/);
|
||||
if (r.method() === 'GET' && m) fetched.push(m[1]);
|
||||
});
|
||||
|
||||
await page.goto('/noter');
|
||||
await page.getByRole('button', { name: 'Ny note' }).click();
|
||||
|
||||
// Editoren er der, og den er tom — ikke væk og på vej tilbage.
|
||||
const body = page.getByLabel('Notens tekst');
|
||||
await expect(body).toHaveText('');
|
||||
|
||||
await body.fill('Grantræ\n\nnoget om det');
|
||||
await body.blur();
|
||||
await expect(page.locator('.list').getByText('Grantræ', { exact: true })).toBeVisible();
|
||||
|
||||
// Noten blev aldrig hentet igen: svaret på oprettelsen var den hele.
|
||||
expect(fetched).toEqual([]);
|
||||
|
||||
expect(trouble).toEqual([]);
|
||||
});
|
||||
|
||||
/**
|
||||
* Et indsat billede kan ses i fuld størrelse og kopieres videre.
|
||||
*
|
||||
@@ -4821,3 +4874,77 @@ test('en opgaves tekst kan læses færdig på en telefon', async ({ page }) => {
|
||||
|
||||
expect(trouble).toEqual([]);
|
||||
});
|
||||
/**
|
||||
* Søgefeltet taber ikke bogstaver, og listen viser det, der blev spurgt om.
|
||||
*
|
||||
* Fra en skærmoptagelse, to billeder i sekundet:
|
||||
*
|
||||
* 2,5 s feltet "verd" listen ufiltreret
|
||||
* 3,0 s feltet "nde" listen filtreret
|
||||
* 6,0 s feltet "verdande" listen ufiltreret
|
||||
* 6,5 s feltet tomt listen filtreret
|
||||
*
|
||||
* To fejl oven i hinanden.
|
||||
*
|
||||
* Bogstaverne forsvandt, fordi adressen blev skrevet ved hvert tastetryk:
|
||||
* `replaceState` opdaterer `$page.url` et øjeblik efter, den kaldes, så en effekt
|
||||
* læste den forrige adresse og skrev den gamle værdi tilbage i feltet.
|
||||
*
|
||||
* Og resultaterne landede for sent, fordi to hentninger kunne være undervejs på
|
||||
* én gang uden at nogen holdt styr på, hvilken der gjaldt. Den langsomste vandt.
|
||||
*/
|
||||
test('søgefeltet taber ikke bogstaver, og det nyeste svar vinder', async ({ page }) => {
|
||||
const trouble = watchForTrouble(page);
|
||||
await page.goto('/noter');
|
||||
|
||||
// Sin egen note at finde. Et ord fra en anden prøve ville gøre den her
|
||||
// afhængig af rækkefølgen, og den kobling har allerede narret mig i denne fil.
|
||||
await page.getByRole('button', { name: 'Ny note' }).click();
|
||||
const krop = page.getByLabel('Notens tekst');
|
||||
await krop.fill('Kaffemølle\n\nnoget om den');
|
||||
await krop.blur();
|
||||
await expect(page.getByText('Gemt', { exact: true })).toBeVisible();
|
||||
await expect(page.locator('.list').getByText('Kaffemølle', { exact: true })).toBeVisible();
|
||||
|
||||
// Skrevet i et tempo, et menneske kan skrive i. Det var hastigheden, der
|
||||
// afslørede det: en pause mellem hvert bogstav skjuler kapløbet helt.
|
||||
const felt = page.getByPlaceholder('Søg i noter');
|
||||
await felt.click();
|
||||
await page.keyboard.type('kaffemølle', { delay: 40 });
|
||||
await expect(felt).toHaveValue('kaffemølle');
|
||||
|
||||
// Og det bliver stående. Fejlen viste sig et halvt sekund efter, ikke med det
|
||||
// samme — adressen skal nå at komme tilbage, før den kan skrive over noget.
|
||||
await page.waitForTimeout(1500);
|
||||
await expect(felt).toHaveValue('kaffemølle');
|
||||
|
||||
// Det nyeste svar vinder, også når det kommer først.
|
||||
//
|
||||
// Den første søgning holdes tilbage, den anden slippes igennem. Uden vagten
|
||||
// lander den gamle sidst og overskriver listen med svaret på et spørgsmål, der
|
||||
// ikke længere bliver stillet.
|
||||
let held;
|
||||
await page.route('**/api/v1/notes?*', async (route) => {
|
||||
const q = new URL(route.request().url()).searchParams.get('q');
|
||||
if (q === 'aaaaa' && !held) {
|
||||
held = route;
|
||||
return; // holdes, indtil den næste er kommet igennem
|
||||
}
|
||||
await route.continue();
|
||||
});
|
||||
|
||||
await felt.fill('');
|
||||
await felt.fill('aaaaa');
|
||||
await page.waitForTimeout(500);
|
||||
await felt.fill('kaffemølle');
|
||||
await page.waitForTimeout(900);
|
||||
if (held) await held.continue();
|
||||
await page.waitForTimeout(700);
|
||||
|
||||
// Feltet og listen er enige: det, der står, er det, der blev spurgt om.
|
||||
await expect(felt).toHaveValue('kaffemølle');
|
||||
await expect(page.locator('.list').getByText('Kaffemølle', { exact: true })).toBeVisible();
|
||||
|
||||
await page.unroute('**/api/v1/notes?*');
|
||||
expect(trouble).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
import { app } from '$lib/stores.svelte.js';
|
||||
import { page } from '$app/stores';
|
||||
import { untrack } from 'svelte';
|
||||
import { goto, replaceState } from '$app/navigation';
|
||||
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';
|
||||
@@ -485,48 +485,32 @@
|
||||
});
|
||||
|
||||
/**
|
||||
* En søgning, man kommer med, og en søgning, man kan gå videre med.
|
||||
* En søgning, man kommer med.
|
||||
*
|
||||
* ⌘K viste noter, der passede, i sin egen liste — og når man valgte en, åbnede
|
||||
* den noten på en side, hvis liste stod uændret. Så var de andre træffere væk,
|
||||
* og det eneste sted, de havde været, var en popup, der lige var lukket. "Jeg
|
||||
* får kun præsenteret en popup med resultater, og trykker jeg retur, kommer jeg
|
||||
* tilbage til den normale liste."
|
||||
* ⌘K sender `?note=…&q=…`, så listen ved siden af den åbne note er de andre
|
||||
* træffere frem for den almindelige liste.
|
||||
*
|
||||
* Nu bærer adressen søgningen, så listen ved siden af den åbne note er de andre
|
||||
* træffere. Og den skrives tilbage, når feltet ændres, så adressen kan gemmes og
|
||||
* deles — og frem for alt ikke lyver, når man har tømt feltet.
|
||||
* Læst, aldrig skrevet. Den første udgave skrev også søgningen tilbage i
|
||||
* adressen ved hvert tastetryk, så den kunne gemmes og deles — og det kostede
|
||||
* bogstaver. `replaceState` opdaterer `$page.url` et øjeblik efter, den kaldes,
|
||||
* så to hurtige tastetryk gav en effekt, der læste den *forrige* adresse, fandt
|
||||
* den uenig med det skrevne, og skrev den gamle værdi tilbage i feltet. Man
|
||||
* skrev "verd", og der stod "nde". Målt på en skærmoptagelse, ikke gættet.
|
||||
*
|
||||
* `settled` er det, de to retninger er blevet enige om — og den læses med
|
||||
* `untrack`, hvilket er hele forskellen på at virke og ikke.
|
||||
*
|
||||
* Uden den: den nederste effekt sætter `settled`, det vækker den øverste, og
|
||||
* `replaceState` har endnu ikke opdateret `$page.url`. Den øverste læser derfor
|
||||
* den *gamle* adresse, finder en søgning, der ikke er enig med `settled`, og
|
||||
* skriver den tilbage i feltet. Man tømmer søgefeltet, og ordet kommer igen.
|
||||
* Hver effekt skal reagere på sin egen kilde — adressen og feltet — og aldrig
|
||||
* på den fælles markør.
|
||||
* En markør, der skal holde to retninger enige, er en markør, der taber
|
||||
* kapløbet et sted. Adressen skrives derfor kun af navigation, og det her er
|
||||
* den eneste, der læser den.
|
||||
*/
|
||||
let settled = $state(null);
|
||||
let cameWith = $state(null);
|
||||
|
||||
$effect(() => {
|
||||
const inURL = $page.url.searchParams.get('q') ?? '';
|
||||
if (inURL === untrack(() => settled)) return;
|
||||
settled = inURL;
|
||||
if (inURL === untrack(() => cameWith)) return;
|
||||
cameWith = inURL;
|
||||
draftQuery = inURL;
|
||||
query = inURL;
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
const now = query;
|
||||
if (now === untrack(() => settled)) return;
|
||||
settled = now;
|
||||
const url = new URL(untrack(() => $page.url));
|
||||
if (now) url.searchParams.set('q', now);
|
||||
else url.searchParams.delete('q');
|
||||
replaceState(url, {});
|
||||
});
|
||||
|
||||
// Arriving from somewhere that names a note — a task's panel, a project's page —
|
||||
// opens it rather than dropping the person on a list to find it again.
|
||||
let asked = $derived($page.url.searchParams.get('note'));
|
||||
@@ -537,13 +521,30 @@
|
||||
else api.note(asked).then(open).catch(() => {});
|
||||
});
|
||||
|
||||
/**
|
||||
* Hvilken hentning der gælder.
|
||||
*
|
||||
* To søgninger kan være undervejs på én gang, og den langsomste kan lande
|
||||
* sidst. Uden det her betød det, at listen viste svaret på et spørgsmål, der
|
||||
* ikke længere blev stillet — på optagelsen stod resultaterne for "verdande" i
|
||||
* listen, efter feltet var tømt. Pausen på 250 ms gjorde det sjældnere og kunne
|
||||
* ikke gøre det umuligt.
|
||||
*/
|
||||
let newest = 0;
|
||||
|
||||
async function load(q) {
|
||||
const mine = ++newest;
|
||||
try {
|
||||
const params = q ? { q } : showArchive ? { archived: '1' } : {};
|
||||
notes = (await api.notes(params)).notes ?? [];
|
||||
const got = (await api.notes(params)).notes ?? [];
|
||||
// Et svar, der er blevet overhalet, kastes væk. Det er ikke forkert —
|
||||
// det er bare ikke længere svaret på det, der blev spurgt om.
|
||||
if (mine !== newest) return;
|
||||
notes = got;
|
||||
status = 'ready';
|
||||
if (selectedId && !notes.some((n) => n.id === selectedId)) open(notes[0] ?? null);
|
||||
} catch (e) {
|
||||
if (mine !== newest) return;
|
||||
status = 'failed';
|
||||
app.toast(humanMessage(e));
|
||||
}
|
||||
@@ -560,7 +561,17 @@
|
||||
* Derfor hentes den hele her. Rækken vises med det samme, så det ikke føles som
|
||||
* at vente; teksten kommer, når den er der.
|
||||
*/
|
||||
async function open(note) {
|
||||
/**
|
||||
* `whole` siger, at noten allerede er hel, og at der ikke skal hentes.
|
||||
*
|
||||
* En tom krop bruges ellers som tegn på, at det her er en listerække — serveren
|
||||
* tømmer `body` på dem med vilje. Men en *nyoprettet* note er også tom, og den
|
||||
* er hel. Uden det her blev den lukket ud som en halv note: editoren blev pillet
|
||||
* af skærmen, noten hentet igen, og editoren sat op på ny. I det hul står
|
||||
* `selected` som null, og `save()` går tavst tilbage uden at gemme — så tekst
|
||||
* skrevet der forsvandt sporløst.
|
||||
*/
|
||||
async function open(note, { whole = false } = {}) {
|
||||
clearTimeout(timer);
|
||||
if (!note) {
|
||||
selectedId = null;
|
||||
@@ -576,9 +587,9 @@
|
||||
// id'et være det samme begge gange, så den anden aldrig blev vist. Man ville
|
||||
// sidde med en tom note, der lige havde haft en titel.
|
||||
selectedId = note.id;
|
||||
if (note.body) {
|
||||
if (whole || note.body) {
|
||||
selected = note;
|
||||
draft = note.body;
|
||||
draft = note.body ?? '';
|
||||
return;
|
||||
}
|
||||
selected = null;
|
||||
@@ -690,7 +701,8 @@
|
||||
try {
|
||||
const note = await api.createNote({ body: '' });
|
||||
notes = [note, ...notes];
|
||||
open(note);
|
||||
// Svaret er hele noten, så den skal ikke hentes igen — se open().
|
||||
await open(note, { whole: true });
|
||||
document.querySelector('.editor textarea')?.focus();
|
||||
} catch (e) {
|
||||
app.toast(humanMessage(e));
|
||||
|
||||
Reference in New Issue
Block a user