Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84af63316d | ||
|
|
930522ecf6 | ||
|
|
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>
|
||||
|
||||
+169
-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,94 @@ 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 totalt udfald siger "ingen forbindelse" — ikke "log ind".
|
||||
*
|
||||
* Opstarten spørger serveren, hvem man er. Når svaret aldrig kommer, står `user`
|
||||
* som null, og null betyder "logget ud" alle andre steder i programmet — så et
|
||||
* udfald blev vist som login-skærmen. Appen gav altså sessionen skylden for, at
|
||||
* nettet var nede, og tilbød et kodeordsfelt, der umuligt kunne virke.
|
||||
*
|
||||
* Det er værre end en fejlbesked, fordi det ligner noget, der kunne virke: man
|
||||
* taster kodeordet, der sker ingenting, og man konkluderer, at kontoen er i
|
||||
* stykker. Det skete i virkeligheden 17. september 2026, da instansens DNS-post
|
||||
* blev slettet.
|
||||
*
|
||||
* Prøven afbryder API-kaldene frem for at stoppe serveren: en afbrudt forespørgsel
|
||||
* er præcis det, en manglende DNS-post giver browseren — fetch afviser, uden at der
|
||||
* nogensinde kom en status. En 500 ville være serveren, der svarer, og det er den
|
||||
* anden sag.
|
||||
*/
|
||||
test('et udfald siger ingen forbindelse frem for at bede om kodeordet', async ({ page }) => {
|
||||
await page.route('**/api/v1/**', (route) => route.abort());
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
// Prøvetiden skal kunne rumme hele forsøgsvinduet: forespørgselslaget prøver
|
||||
// igen i femten sekunder, før det kalder det et udfald. Det er med vilje —
|
||||
// et blink på nettet må ikke smide folk ud — så prøven må vente det ud.
|
||||
await expect(page.getByText('Ingen forbindelse', { exact: true })).toBeVisible({
|
||||
timeout: 25000
|
||||
});
|
||||
|
||||
// Og ikke kodeordsfeltet. Det er hele pointen.
|
||||
await expect(page.getByLabel('Adgangskode')).toHaveCount(0);
|
||||
});
|
||||
|
||||
/**
|
||||
* Et indsat billede kan ses i fuld størrelse og kopieres videre.
|
||||
*
|
||||
@@ -4821,3 +4908,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([]);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
<script>
|
||||
/**
|
||||
* What the app shows when it could not reach the server at all.
|
||||
*
|
||||
* Before this, an outage rendered the sign-in screen: the boot asks who you
|
||||
* are, the request never gets an answer, `user` stays null, and null means
|
||||
* "logged out" everywhere else. So the app blamed the session for the network
|
||||
* being down, and offered a password box that could not possibly work. That is
|
||||
* worse than an error message, because it looks like it might work — people
|
||||
* type the password, get nothing, and conclude their account is broken.
|
||||
*
|
||||
* It happened for real on 17 September 2026: the instance's DNS record was
|
||||
* deleted, and what the app showed was a slow-loading login page.
|
||||
*
|
||||
* The text says the data is safe, because that is the question somebody
|
||||
* actually has when their notes vanish behind a login box.
|
||||
*/
|
||||
import { t } from '$lib/i18n.svelte.js';
|
||||
import { app } from '$lib/stores.svelte.js';
|
||||
|
||||
let trying = $state(false);
|
||||
|
||||
async function again() {
|
||||
trying = true;
|
||||
try {
|
||||
await app.load();
|
||||
} finally {
|
||||
trying = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="screen">
|
||||
<div class="card">
|
||||
<div class="brand">
|
||||
<span class="rune" aria-hidden="true">ᚹ</span>
|
||||
<h1>verdande</h1>
|
||||
</div>
|
||||
|
||||
<h2>{t('net.downTitle')}</h2>
|
||||
<p class="lede">{t('net.downBody')}</p>
|
||||
|
||||
<button onclick={again} disabled={trying}>
|
||||
{trying ? t('net.reconnecting') : t('net.downRetry')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.screen {
|
||||
height: 100dvh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: var(--s4);
|
||||
background: var(--ground);
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 100%;
|
||||
max-width: 340px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--s4);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--s3);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.rune {
|
||||
font-size: var(--text-2xl);
|
||||
color: var(--accent);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: var(--text-xl);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: var(--text-lg);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.lede {
|
||||
margin: 0;
|
||||
color: var(--ink-soft);
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
@@ -237,6 +237,9 @@ export const da = {
|
||||
'nav.offlineHint': 'Ændringer fra andre vises ikke lige nu',
|
||||
'net.reconnecting': 'Genopretter forbindelse…',
|
||||
'net.offline': 'Ingen forbindelse',
|
||||
'net.downTitle': 'Ingen forbindelse',
|
||||
'net.downBody': 'Verdande kan ikke nås herfra lige nu. Din tekst er ikke væk — den ligger på serveren, og den kommer igen, når forbindelsen gør.',
|
||||
'net.downRetry': 'Prøv igen',
|
||||
'nav.doItToday': 'Gør det i dag',
|
||||
|
||||
// --- the sidebar's own sections ------------------------------------------------
|
||||
|
||||
@@ -234,6 +234,9 @@ export const en = {
|
||||
'nav.offlineHint': 'Changes from other people are not showing right now',
|
||||
'net.reconnecting': 'Reconnecting…',
|
||||
'net.offline': 'No connection',
|
||||
'net.downTitle': 'No connection',
|
||||
'net.downBody': 'Verdande cannot be reached from here right now. Nothing you wrote is lost — it is on the server, and it comes back when the connection does.',
|
||||
'net.downRetry': 'Try again',
|
||||
'nav.doItToday': 'Do it today',
|
||||
|
||||
// --- the sidebar's own sections ------------------------------------------------
|
||||
|
||||
@@ -29,6 +29,20 @@ class AppState {
|
||||
people = $state([]);
|
||||
tasks = $state([]);
|
||||
loading = $state(true);
|
||||
|
||||
/**
|
||||
* True when the boot never reached the server — as opposed to reaching it and
|
||||
* being told the session is gone.
|
||||
*
|
||||
* The two are indistinguishable from `user === null`, and they are opposite
|
||||
* things to the person looking at the screen: one means "log ind", the other
|
||||
* means "der er intet at logge ind på herfra, og et kodeord hjælper ikke". A
|
||||
* total outage used to show the login screen, so the app blamed the user's
|
||||
* session for the network being down — and a login page that cannot possibly
|
||||
* work is worse than an error, because it looks like it might.
|
||||
*/
|
||||
unreachable = $state(false);
|
||||
|
||||
/** Transient messages: a failed save, a rolled-back change. */
|
||||
toasts = $state([]);
|
||||
connected = $state(false);
|
||||
@@ -96,6 +110,7 @@ class AppState {
|
||||
this.loading = true;
|
||||
try {
|
||||
this.user = await api.me();
|
||||
this.unreachable = false;
|
||||
// The interface's language follows the account, so it is set the moment
|
||||
// the session is known — before anything has drawn a string.
|
||||
i18n.set(this.user.locale);
|
||||
@@ -111,6 +126,10 @@ class AppState {
|
||||
this.loadNotifications();
|
||||
} catch (e) {
|
||||
this.user = null;
|
||||
// `offline` is the one code the request layer makes up itself: it is what
|
||||
// is thrown when the retry window ran out without any status ever
|
||||
// arriving. A 401 is the server answering, and that is a real sign-out.
|
||||
this.unreachable = e?.code === 'offline';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import Sidebar from '$lib/components/Sidebar.svelte';
|
||||
import CommandPalette from '$lib/components/CommandPalette.svelte';
|
||||
import SignIn from '$lib/components/SignIn.svelte';
|
||||
import NoConnection from '$lib/components/NoConnection.svelte';
|
||||
import TaskDetail from '$lib/components/TaskDetail.svelte';
|
||||
import { t } from '$lib/i18n.svelte.js';
|
||||
import { setBadge } from '$lib/badge.js';
|
||||
@@ -166,6 +167,10 @@
|
||||
<div class="booting"></div>
|
||||
{:else if standalone}
|
||||
{@render children()}
|
||||
{:else if app.unreachable}
|
||||
<!-- Ikke login-skærmen: en total udfald er ikke en udløbet session, og et
|
||||
kodeord hjælper ikke mod en manglende forbindelse. -->
|
||||
<NoConnection />
|
||||
{:else if !app.user}
|
||||
<SignIn />
|
||||
{:else}
|
||||
|
||||
@@ -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