fix(hurtig tilføjelse): fire ting, der blev læst forkert eller tabt i stilhed
Fundet under arbejdet med genopbygnings-prompten, hvor den rigtige parser blev kørt over en tabel af sætninger. - "at" før et 12-timers klokkeslæt blev stående i titlen: "file the VAT return tomorrow at 10am" blev opgaven "file the VAT return at". Markøren (at, kl, klokken) hører nu med til tiden, også foran "7:30 pm". - "køb 1/2 liter fløde" fik forfaldsdato 1. februar. En d/m efterfulgt af en mængdeenhed (liter, dl, kg, stk, tsk, kop, del, …) er en mængde. Uden enhed er det stadig en dato — "3/4" alene, "møde 3/4 med Anders" og også "3/4 kage", fordi intet i "kage" skiller tredje april fra tre fjerdedele, og en dato, der stille forsvinder, er værre end en, man selv sletter. - Et "#navn" eller "/navn", der ikke fandtes, blev klippet ud af titlen, og opgaven landede et andet sted uden et ord. Serveren har længe sendt unknown_project og unknown_section; fladen siger dem nu i en besked, også når en underopgave beholder sin opgaves projekt. - Titlen i opgaveruden blev læst om, men gentagelsen blev læst under `parsed.recurrence`, et felt der ikke findes, så "hver mandag" forsvandt. Den læser nu `recurrence_rule` og tager også klokkeslæt, projekt og alarm med, og siger gentagelsen på læserens sprog i stedet for et fast "gentagelse". Alarmen lægges før rettelsen, så den skrevne ikke stables oven på standardalarmen. Prøver: to nye Go-prøver i quickadd og to e2e-prøver. Fem plantede fejl, hver fanget af sin egen prøve. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ec5cf8f9f9
commit
ed30eb4c36
@@ -304,22 +304,28 @@ func extractPriority(input string, spans []Span) (int, []Span) {
|
|||||||
var (
|
var (
|
||||||
// An explicit marker: "kl 10", "kl. 10:30", "klokken 16", "at 9.15".
|
// An explicit marker: "kl 10", "kl. 10:30", "klokken 16", "at 9.15".
|
||||||
reTimeMarked = regexp.MustCompile(`(?i)(?:^|\s)(kl\.?|klokken|at)\s*(\d{1,2})(?:[:.](\d{2}))?\s*(am|pm)?(?:\s|$)`)
|
reTimeMarked = regexp.MustCompile(`(?i)(?:^|\s)(kl\.?|klokken|at)\s*(\d{1,2})(?:[:.](\d{2}))?\s*(am|pm)?(?:\s|$)`)
|
||||||
// A 12-hour suffix carries its own marker: "10am", "7:30 pm".
|
// A 12-hour suffix carries its own marker: "10am", "7:30 pm" — and may still
|
||||||
reTimeMeridiem = regexp.MustCompile(`(?i)(?:^|\s)(\d{1,2})(?::(\d{2}))?\s*(am|pm)(?:\s|$)`)
|
// be written after one, "at 10am", "kl 7 pm". The marker is part of the time:
|
||||||
|
// left behind, "tomorrow at 10am" became the title "… at".
|
||||||
|
reTimeMeridiem = regexp.MustCompile(`(?i)(?:^|\s)((?:at|kl\.?|klokken)\s*)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)(?:\s|$)`)
|
||||||
// A bare colon time: "16:30". Only ':' — never '.', because "15.3" is a date.
|
// A bare colon time: "16:30". Only ':' — never '.', because "15.3" is a date.
|
||||||
reTimeColon = regexp.MustCompile(`(?:^|\s)(\d{1,2}):(\d{2})(?:\s|$)`)
|
reTimeColon = regexp.MustCompile(`(?:^|\s)(\d{1,2}):(\d{2})(?:\s|$)`)
|
||||||
)
|
)
|
||||||
|
|
||||||
func extractTime(input string, spans []Span, locale string) (int, int, bool, []Span) {
|
func extractTime(input string, spans []Span, locale string) (int, int, bool, []Span) {
|
||||||
if m := reTimeMeridiem.FindStringSubmatchIndex(input); m != nil {
|
if m := reTimeMeridiem.FindStringSubmatchIndex(input); m != nil {
|
||||||
hour := atoi(input, m[2], m[3])
|
hour := atoi(input, m[4], m[5])
|
||||||
minute := 0
|
minute := 0
|
||||||
if m[4] >= 0 {
|
if m[6] >= 0 {
|
||||||
minute = atoi(input, m[4], m[5])
|
minute = atoi(input, m[6], m[7])
|
||||||
}
|
}
|
||||||
hour = applyMeridiem(hour, strings.ToLower(input[m[6]:m[7]]))
|
hour = applyMeridiem(hour, strings.ToLower(input[m[8]:m[9]]))
|
||||||
if valid(hour, minute) {
|
if valid(hour, minute) {
|
||||||
return hour, minute, true, append(spans, Span{Start: m[2], End: m[7], Kind: KindTime})
|
start := m[4]
|
||||||
|
if m[2] >= 0 {
|
||||||
|
start = m[2]
|
||||||
|
}
|
||||||
|
return hour, minute, true, append(spans, Span{Start: start, End: m[9], Kind: KindTime})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,6 +435,37 @@ var (
|
|||||||
reNumeric = regexp.MustCompile(`(?:^|\s)(\d{1,2})[/.](\d{1,2})(?:[-/](\d{2,4}))?(?:\s|$)`)
|
reNumeric = regexp.MustCompile(`(?:^|\s)(\d{1,2})[/.](\d{1,2})(?:[-/](\d{2,4}))?(?:\s|$)`)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// fractionUnits are the words that make "3/4" a quantity rather than a day.
|
||||||
|
//
|
||||||
|
// "køb 1/2 liter fløde" is half a litre, not the first of February — and it is
|
||||||
|
// the ordinary shape of a shopping list or a recipe. Without a unit after it the
|
||||||
|
// slash form stays a date: "3/4" alone, "møde 3/4 med Anders", and also
|
||||||
|
// "3/4 kage", because nothing in "kage" tells the third of April from three
|
||||||
|
// quarters, and a date quietly dropped is worse than one somebody removes.
|
||||||
|
var fractionUnits = map[string]bool{
|
||||||
|
"l": true, "liter": true, "dl": true, "cl": true, "ml": true,
|
||||||
|
"kg": true, "g": true, "gram": true, "pund": true,
|
||||||
|
"tsk": true, "spsk": true, "kop": true, "kopper": true, "glas": true,
|
||||||
|
"stk": true, "stk.": true, "del": true, "dele": true, "pakke": true, "pakker": true,
|
||||||
|
"time": true, "timer": true, "km": true, "m": true, "cm": true,
|
||||||
|
"cup": true, "cups": true, "tsp": true, "tbsp": true, "lb": true, "oz": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// firstNumericDate is the first "d/m" in the input that is not a quantity — see
|
||||||
|
// fractionUnits. A written year always makes it a date.
|
||||||
|
func firstNumericDate(input string) []int {
|
||||||
|
for _, m := range reNumeric.FindAllStringSubmatchIndex(input, -1) {
|
||||||
|
if m[6] < 0 {
|
||||||
|
rest := strings.Fields(input[m[5]:])
|
||||||
|
if len(rest) > 0 && fractionUnits[strings.ToLower(rest[0])] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func extractDate(input string, spans []Span, now time.Time) (time.Time, bool, []Span) {
|
func extractDate(input string, spans []Span, now time.Time) (time.Time, bool, []Span) {
|
||||||
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||||
|
|
||||||
@@ -441,7 +478,7 @@ func extractDate(input string, spans []Span, now time.Time) (time.Time, bool, []
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if m := reNumeric.FindStringSubmatchIndex(input); m != nil {
|
if m := firstNumericDate(input); m != nil {
|
||||||
day, mo := atoi(input, m[2], m[3]), atoi(input, m[4], m[5])
|
day, mo := atoi(input, m[2], m[3]), atoi(input, m[4], m[5])
|
||||||
year := today.Year()
|
year := today.Year()
|
||||||
explicitYear := m[6] >= 0
|
explicitYear := m[6] >= 0
|
||||||
|
|||||||
@@ -784,3 +784,50 @@ func TestImpossibleReminderClockIsNotKept(t *testing.T) {
|
|||||||
t.Errorf("ReminderOffsetMin = %d, want none", *got.ReminderOffsetMin)
|
t.Errorf("ReminderOffsetMin = %d, want none", *got.ReminderOffsetMin)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A marker written before a 12-hour time belongs to the time. Left behind, it
|
||||||
|
// became the end of the title: "file the VAT return tomorrow at 10am" was the
|
||||||
|
// task "file the VAT return at".
|
||||||
|
func TestMarkerBeforeAMeridiemTimeIsConsumed(t *testing.T) {
|
||||||
|
now := ref(t)
|
||||||
|
for _, tc := range []struct{ in, content, due, time string }{
|
||||||
|
{"file the VAT return tomorrow at 10am", "file the VAT return", "2026-03-11", "10:00"},
|
||||||
|
{"call at 7:30 pm", "call", "2026-03-10", "19:30"},
|
||||||
|
{"call the dentist tomorrow at 9am remind me an hour before", "call the dentist", "2026-03-11", "09:00"},
|
||||||
|
{"møde i morgen kl 7 pm", "møde", "2026-03-11", "19:00"},
|
||||||
|
// Control: a meridiem time with no marker still works, and "at" that is
|
||||||
|
// not in front of a time is a word.
|
||||||
|
{"standup tomorrow 9am", "standup", "2026-03-11", "09:00"},
|
||||||
|
{"look at the report tomorrow", "look at the report", "2026-03-11", ""},
|
||||||
|
} {
|
||||||
|
got := Parse(tc.in, now, "en")
|
||||||
|
if got.Content != tc.content || got.DueDate != tc.due || got.DueTime != tc.time {
|
||||||
|
t.Errorf("%q → %q %s %s, want %q %s %s", tc.in, got.Content, got.DueDate, got.DueTime,
|
||||||
|
tc.content, tc.due, tc.time)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// "1/2 liter" is half a litre. A day/month followed by a unit is a quantity and
|
||||||
|
// stays in the title; without a unit it is still a date, because "3/4" alone and
|
||||||
|
// "møde 3/4 med Anders" are the third of April.
|
||||||
|
func TestAFractionWithAUnitIsNotADate(t *testing.T) {
|
||||||
|
now := ref(t)
|
||||||
|
for _, tc := range []struct{ in, content, due string }{
|
||||||
|
{"køb 1/2 liter fløde", "køb 1/2 liter fløde", ""},
|
||||||
|
{"tilsæt 3/4 dl mælk", "tilsæt 3/4 dl mælk", ""},
|
||||||
|
{"bag med 1/2 kg mel i morgen", "bag med 1/2 kg mel", "2026-03-11"},
|
||||||
|
// A quantity first, a real date after it: the date is still found.
|
||||||
|
{"køb 1/2 liter fløde 15/3", "køb 1/2 liter fløde", "2026-03-15"},
|
||||||
|
// Controls: no unit, or a year — a date.
|
||||||
|
{"møde 3/4", "møde", "2026-04-03"},
|
||||||
|
{"møde 3/4 med Anders", "møde med Anders", "2026-04-03"},
|
||||||
|
{"halvdelen 3/4 kage", "halvdelen kage", "2026-04-03"},
|
||||||
|
{"frist 1/2-2027 liter", "frist liter", "2027-02-01"},
|
||||||
|
} {
|
||||||
|
got := Parse(tc.in, now, "da")
|
||||||
|
if got.Content != tc.content || got.DueDate != tc.due {
|
||||||
|
t.Errorf("%q → %q %q, want %q %q", tc.in, got.Content, got.DueDate, tc.content, tc.due)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6089,3 +6089,74 @@ test('knappen findes ikke i urd-ansigtet', async ({ page }) => {
|
|||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
await expect(page.getByRole('button', { name: 'Tal en opgave ind' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Tal en opgave ind' })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** Calls the API as the signed-in page does, and returns the parsed body. */
|
||||||
|
async function apiCall(page, method, path, body) {
|
||||||
|
return page.evaluate(
|
||||||
|
async ({ method, path, body }) => {
|
||||||
|
const init = { method, credentials: 'include', headers: { 'Sec-Fetch-Site': 'same-origin' } };
|
||||||
|
if (body !== undefined) {
|
||||||
|
init.headers['Content-Type'] = 'application/json';
|
||||||
|
init.body = JSON.stringify(body);
|
||||||
|
}
|
||||||
|
const r = await fetch('/api/v1' + path, init);
|
||||||
|
return r.status === 204 ? null : r.json();
|
||||||
|
},
|
||||||
|
{ method, path, body }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Et "#navn", der ikke findes, er klippet ud af titlen af parseren og lagt et
|
||||||
|
* andet sted af serveren. Uden en besked forsvandt ordet bare.
|
||||||
|
*/
|
||||||
|
test('et ukendt projekt i hurtig tilføjelse bliver sagt højt', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
const name = `Findesikke${Date.now()}`;
|
||||||
|
const box = page.getByLabel('Ny opgave');
|
||||||
|
await box.fill(`køb mælk i dag #${name}`);
|
||||||
|
await box.press('Enter');
|
||||||
|
|
||||||
|
await expect(page.locator('.toast').filter({ hasText: name })).toContainText(
|
||||||
|
'Der er intet projekt'
|
||||||
|
);
|
||||||
|
await expect(page.locator('.toast').filter({ hasText: name })).toContainText('Indbakke');
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* At skrive i en opgaves titel læses som hurtig tilføjelse læser: dato,
|
||||||
|
* klokkeslæt, gentagelse, projekt og alarm. Gentagelsen blev engang læst under
|
||||||
|
* et felt, der ikke findes, og forsvandt.
|
||||||
|
*/
|
||||||
|
test('en titel, der rettes i opgaveruden, tager gentagelse, tid, projekt og alarm med', async ({
|
||||||
|
page
|
||||||
|
}) => {
|
||||||
|
await page.goto('/');
|
||||||
|
const project = await apiCall(page, 'POST', '/projects', { name: `Titelprojekt ${Date.now()}` });
|
||||||
|
const task = await apiCall(page, 'POST', '/tasks', { content: 'omskriv titel' });
|
||||||
|
|
||||||
|
await page.goto('/opgave/' + task.id);
|
||||||
|
const drawer = page.getByRole('complementary', { name: 'Opgave' });
|
||||||
|
const title = drawer.getByLabel('Opgavens tekst');
|
||||||
|
await expect(title).toHaveValue('omskriv titel');
|
||||||
|
|
||||||
|
const saved = opgaveGemt(page);
|
||||||
|
await title.fill(`omskriv titel hver mandag kl 9 #"${project.name}" mind mig en time før`);
|
||||||
|
await title.press('Enter');
|
||||||
|
await saved;
|
||||||
|
|
||||||
|
await expect(title).toHaveValue('omskriv titel');
|
||||||
|
await expect(page.locator('.toast').filter({ hasText: 'Læst som' })).toBeVisible();
|
||||||
|
|
||||||
|
// Målt på serveren, ikke på skærmen: det er dér, det skal stå.
|
||||||
|
await expect
|
||||||
|
.poll(async () => (await apiCall(page, 'GET', '/tasks/' + task.id)).recurrence_rule)
|
||||||
|
.toBe('FREQ=WEEKLY;BYDAY=MO');
|
||||||
|
const after = await apiCall(page, 'GET', '/tasks/' + task.id);
|
||||||
|
expect(after.content).toBe('omskriv titel');
|
||||||
|
expect(after.project_id).toBe(project.id);
|
||||||
|
expect(new Date(after.due_datetime).getHours()).toBe(9);
|
||||||
|
expect(new Date(after.due_datetime).getDay()).toBe(1);
|
||||||
|
const { reminders } = await apiCall(page, 'GET', `/tasks/${task.id}/reminders`);
|
||||||
|
expect(reminders.map((r) => r.offset_min)).toEqual([-60]);
|
||||||
|
});
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
import { app, projectName, clockOf } from '$lib/stores.svelte.js';
|
import { app, projectName, clockOf } from '$lib/stores.svelte.js';
|
||||||
import { focusOnMount } from '$lib/focus.js';
|
import { focusOnMount } from '$lib/focus.js';
|
||||||
import { t, tag } from '$lib/i18n.svelte.js';
|
import { t, tag } from '$lib/i18n.svelte.js';
|
||||||
import { REPEATS, reminderOffsetLabel } from '$lib/when.js';
|
import { REPEATS, reminderOffsetLabel, repeatLabel, shortDate } from '$lib/when.js';
|
||||||
import { readList } from '$lib/pastelist.js';
|
import { readList } from '$lib/pastelist.js';
|
||||||
|
|
||||||
let { task, onclose } = $props();
|
let { task, onclose } = $props();
|
||||||
@@ -215,24 +215,83 @@
|
|||||||
|
|
||||||
const patch = { content: trimmed };
|
const patch = { content: trimmed };
|
||||||
const took = [];
|
const took = [];
|
||||||
|
// Det, der ikke kan stå i en PATCH: alarmen er sin egen række.
|
||||||
|
let reminder = null;
|
||||||
if (parsed && parsed.content && parsed.content !== trimmed) {
|
if (parsed && parsed.content && parsed.content !== trimmed) {
|
||||||
patch.content = parsed.content;
|
patch.content = parsed.content;
|
||||||
|
// Samme felter som hurtig tilføjelse læser, og med de navne serveren
|
||||||
|
// sender dem under. Feltet hedder `recurrence_rule`; det læste en gang
|
||||||
|
// `parsed.recurrence`, som aldrig findes, så "hver mandag" i en titel
|
||||||
|
// forsvandt fra titlen og blev ingen steder.
|
||||||
if (parsed.due_date) {
|
if (parsed.due_date) {
|
||||||
patch.due_date = parsed.due_date;
|
patch.due_date = parsed.due_date;
|
||||||
took.push(parsed.due_date);
|
took.push(shortDate(parsed.due_date));
|
||||||
|
}
|
||||||
|
// Parseren fylder altid datoen ud, når den har læst et klokkeslæt, så
|
||||||
|
// due_date er sat ovenfor, når due_time er.
|
||||||
|
if (parsed.due_time) {
|
||||||
|
patch.due_time = parsed.due_time;
|
||||||
|
took.push(parsed.due_time);
|
||||||
}
|
}
|
||||||
if (parsed.priority && parsed.priority < 4) {
|
if (parsed.priority && parsed.priority < 4) {
|
||||||
patch.priority = parsed.priority;
|
patch.priority = parsed.priority;
|
||||||
took.push(`P${parsed.priority}`);
|
took.push(`P${parsed.priority}`);
|
||||||
}
|
}
|
||||||
if (parsed.recurrence) {
|
if (parsed.recurrence_rule) {
|
||||||
patch.recurrence_rule = parsed.recurrence;
|
patch.recurrence_rule = parsed.recurrence_rule;
|
||||||
took.push('gentagelse');
|
took.push(repeatLabel(parsed.recurrence_rule, parsed.recurrence_text));
|
||||||
}
|
}
|
||||||
if (parsed.labels?.length) {
|
if (parsed.labels?.length) {
|
||||||
patch.labels = [...new Set([...(task.labels ?? []), ...parsed.labels])];
|
patch.labels = [...new Set([...(task.labels ?? []), ...parsed.labels])];
|
||||||
took.push(parsed.labels.map((l) => `@${l}`).join(' '));
|
took.push(parsed.labels.map((l) => `@${l}`).join(' '));
|
||||||
}
|
}
|
||||||
|
if (parsed.project) {
|
||||||
|
// Navnet slås op som hurtig tilføjelse gør: uden forskel på store og
|
||||||
|
// små bogstaver. Et navn, der ikke findes, siges højt — det er klippet
|
||||||
|
// ud af titlen, og uden en besked var ordet bare væk.
|
||||||
|
const wanted = parsed.project.toLowerCase();
|
||||||
|
const found = app.projects.find((p) => p.name.toLowerCase() === wanted);
|
||||||
|
if (!found) {
|
||||||
|
app.toast(
|
||||||
|
t('task.unknownProject', {
|
||||||
|
name: parsed.project,
|
||||||
|
where: projectName(app.projectById(task.project_id))
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else if (found.id !== task.project_id && !task.parent_id) {
|
||||||
|
patch.project_id = found.id;
|
||||||
|
took.push(`#${found.name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (parsed.reminder_at) {
|
||||||
|
// Vægurstid i læserens zone, ligesom feltet nedenfor giver den.
|
||||||
|
reminder = { remind_at: new Date(parsed.reminder_at).toISOString() };
|
||||||
|
} else if (parsed.reminder_offset_min !== undefined && parsed.reminder_offset_min !== null) {
|
||||||
|
// En forskydning regnes fra klokkeslættet. Uden et ville rækken aldrig
|
||||||
|
// ringe, og intet bagefter ville afsløre det.
|
||||||
|
if (patch.due_time || task.due_datetime) {
|
||||||
|
reminder = { offset_min: parsed.reminder_offset_min };
|
||||||
|
} else {
|
||||||
|
app.toast(t('task.alarmNeedsATime'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alarmen før rettelsen: får opgaven sit klokkeslæt i samme gemning, lægger
|
||||||
|
// serveren standardalarmen på — medmindre opgaven allerede har en. Den,
|
||||||
|
// der blev skrevet, skal være den, og ikke stå oven på standarden.
|
||||||
|
if (reminder) {
|
||||||
|
try {
|
||||||
|
const created = await api.createReminder(task.id, reminder);
|
||||||
|
reminders = [...reminders, created];
|
||||||
|
took.push(
|
||||||
|
reminder.offset_min !== undefined
|
||||||
|
? reminderOffsetLabel(reminder.offset_min)
|
||||||
|
: parsed.reminder_at.slice(11)
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
app.toast(humanMessage(e));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await save(patch);
|
await save(patch);
|
||||||
@@ -244,6 +303,8 @@
|
|||||||
if (took.length) {
|
if (took.length) {
|
||||||
content = patch.content;
|
content = patch.content;
|
||||||
if (patch.due_date) dueDate = patch.due_date;
|
if (patch.due_date) dueDate = patch.due_date;
|
||||||
|
if (patch.due_time) dueTime = patch.due_time;
|
||||||
|
if (patch.project_id) projectId = patch.project_id;
|
||||||
if (patch.priority) priority = patch.priority;
|
if (patch.priority) priority = patch.priority;
|
||||||
if (patch.recurrence_rule) recurrence = patch.recurrence_rule;
|
if (patch.recurrence_rule) recurrence = patch.recurrence_rule;
|
||||||
if (patch.labels) labels = patch.labels.join(', ');
|
if (patch.labels) labels = patch.labels.join(', ');
|
||||||
|
|||||||
@@ -272,6 +272,9 @@ export const da = {
|
|||||||
// Diktat, ikke tale: programmet laver ingen talegenkendelse, det stiller
|
// Diktat, ikke tale: programmet laver ingen talegenkendelse, det stiller
|
||||||
// feltet frem, så tastaturets egen mikrofon kan bruges. Hjælpelinjen siger
|
// feltet frem, så tastaturets egen mikrofon kan bruges. Hjælpelinjen siger
|
||||||
// det, fordi en knap med en mikrofon på lover noget andet.
|
// det, fordi en knap med en mikrofon på lover noget andet.
|
||||||
|
'task.unknownProject': 'Der er intet projekt, der hedder "{name}". Opgaven ligger i {where}.',
|
||||||
|
'task.unknownSection': 'Der er ingen sektion, der hedder "{name}" i {where}. Opgaven ligger uden sektion.',
|
||||||
|
'task.subtaskKeepsProject': 'En underopgave ligger i sin opgaves projekt, {where} — "{name}" blev ikke brugt.',
|
||||||
'task.alarmNeedsATime':
|
'task.alarmNeedsATime':
|
||||||
'Opgaven er oprettet. Alarmen er ikke — "en time før" skal have et klokkeslæt at regne tilbage fra.',
|
'Opgaven er oprettet. Alarmen er ikke — "en time før" skal have et klokkeslæt at regne tilbage fra.',
|
||||||
'task.speak': 'Tal en opgave ind',
|
'task.speak': 'Tal en opgave ind',
|
||||||
|
|||||||
@@ -266,6 +266,9 @@ export const en = {
|
|||||||
'task.pastedAdd': 'Add {n} tasks',
|
'task.pastedAdd': 'Add {n} tasks',
|
||||||
'task.pastedCancel': 'Cancel',
|
'task.pastedCancel': 'Cancel',
|
||||||
'task.new': 'New task',
|
'task.new': 'New task',
|
||||||
|
'task.unknownProject': 'There is no project called "{name}". The task is in {where}.',
|
||||||
|
'task.unknownSection': 'There is no section called "{name}" in {where}. The task has no section.',
|
||||||
|
'task.subtaskKeepsProject': 'A sub-task stays in its task\'s project, {where} — "{name}" was not used.',
|
||||||
'task.alarmNeedsATime':
|
'task.alarmNeedsATime':
|
||||||
'The task is created. The alarm is not — "an hour before" needs a time to count back from.',
|
'The task is created. The alarm is not — "an hour before" needs a time to count back from.',
|
||||||
'task.speak': 'Speak a task',
|
'task.speak': 'Speak a task',
|
||||||
|
|||||||
@@ -729,6 +729,20 @@ class AppState {
|
|||||||
// slags påmindelse, hvis fejl intet bagefter afslører — der står ingen
|
// slags påmindelse, hvis fejl intet bagefter afslører — der står ingen
|
||||||
// række at savne, og man opdager den først, da den ikke ringede.
|
// række at savne, og man opdager den først, da den ikke ringede.
|
||||||
if (task?.reminder_not_set) this.toast(t('task.alarmNeedsATime'));
|
if (task?.reminder_not_set) this.toast(t('task.alarmNeedsATime'));
|
||||||
|
// Et "#navn" eller "/navn", der ikke ramte noget, er klippet ud af titlen
|
||||||
|
// af parseren og lagt et andet sted af serveren. Uden de her linjer
|
||||||
|
// forsvandt ordet bare, og opgaven lå et sted, ingen havde bedt om.
|
||||||
|
const where = projectName(this.projectById(task?.project_id));
|
||||||
|
if (task?.unknown_project) {
|
||||||
|
this.toast(
|
||||||
|
parentId
|
||||||
|
? t('task.subtaskKeepsProject', { name: task.unknown_project, where })
|
||||||
|
: t('task.unknownProject', { name: task.unknown_project, where })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (task?.unknown_section) {
|
||||||
|
this.toast(t('task.unknownSection', { name: task.unknown_section, where }));
|
||||||
|
}
|
||||||
return task;
|
return task;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.toast(humanMessage(e));
|
this.toast(humanMessage(e));
|
||||||
|
|||||||
Reference in New Issue
Block a user