fix: support typed Todo reminder times

This commit is contained in:
mirivlad 2026-07-14 20:50:14 +08:00
parent dfc53f1a5f
commit bee365ad22
4 changed files with 62 additions and 39 deletions

View File

@ -18,7 +18,7 @@
'.todo-title{font-size:.86rem;font-weight:600}.todo-count,.todo-status,.todo-scope{font-size:.72rem;color:var(--vt-color-text-muted,#7f8aa3)}.todo-spacer{flex:1}', '.todo-title{font-size:.86rem;font-weight:600}.todo-count,.todo-status,.todo-scope{font-size:.72rem;color:var(--vt-color-text-muted,#7f8aa3)}.todo-spacer{flex:1}',
'.todo-filters{display:flex;align-items:center;gap:.35rem;min-width:0;flex:1;flex-wrap:wrap}', '.todo-filters{display:flex;align-items:center;gap:.35rem;min-width:0;flex:1;flex-wrap:wrap}',
'.todo-input,.todo-select{box-sizing:border-box;min-height:1.9rem;border:1px solid var(--vt-color-border-strong,#2c456a);border-radius:var(--vt-radius-sm,4px);background:var(--vt-color-surface,#15152c);color:var(--vt-color-text-primary,#f4f7fb);color-scheme:dark;font:inherit;font-size:.78rem;padding:.32rem .45rem}', '.todo-input,.todo-select{box-sizing:border-box;min-height:1.9rem;border:1px solid var(--vt-color-border-strong,#2c456a);border-radius:var(--vt-radius-sm,4px);background:var(--vt-color-surface,#15152c);color:var(--vt-color-text-primary,#f4f7fb);color-scheme:dark;font:inherit;font-size:.78rem;padding:.32rem .45rem}',
'.todo-input.search{width:min(15rem,100%)}.todo-input.textarea{min-height:6.5rem;resize:vertical;line-height:1.4}.todo-select{max-width:12rem;appearance:none;background-color:var(--vt-color-surface,#15152c);background-image:linear-gradient(45deg,transparent 50%,var(--vt-color-text-muted,#7f8aa3) 50%),linear-gradient(135deg,var(--vt-color-text-muted,#7f8aa3) 50%,transparent 50%);background-position:calc(100% - 14px) 50%,calc(100% - 9px) 50%;background-size:5px 5px,5px 5px;background-repeat:no-repeat;padding-right:1.7rem}.todo-select option{background:var(--vt-color-surface,#15152c);color:var(--vt-color-text-primary,#f4f7fb)}.todo-time-picker{display:grid;grid-template-columns:1fr 1fr;gap:.4rem}.todo-time-select{width:100%;max-width:none}', '.todo-input.search{width:min(15rem,100%)}.todo-input.textarea{min-height:6.5rem;resize:vertical;line-height:1.4}.todo-input.todo-time-input{width:100%;font-variant-numeric:tabular-nums}.todo-select{max-width:12rem;appearance:none;background-color:var(--vt-color-surface,#15152c);background-image:linear-gradient(45deg,transparent 50%,var(--vt-color-text-muted,#7f8aa3) 50%),linear-gradient(135deg,var(--vt-color-text-muted,#7f8aa3) 50%,transparent 50%);background-position:calc(100% - 14px) 50%,calc(100% - 9px) 50%;background-size:5px 5px,5px 5px;background-repeat:no-repeat;padding-right:1.7rem}.todo-select option{background:var(--vt-color-surface,#15152c);color:var(--vt-color-text-primary,#f4f7fb)}',
'.todo-btn{min-height:1.9rem;padding:.32rem .62rem;border:1px solid var(--vt-color-border-strong,#2c456a);border-radius:var(--vt-radius-md,6px);background:var(--vt-color-surface-hover,#1b2440);color:var(--vt-color-text-secondary,#b7c0d4);font-size:.78rem;cursor:pointer}.todo-btn:hover{border-color:var(--vt-color-accent,#4ecca3);color:var(--vt-color-text-primary,#f4f7fb)}.todo-btn.primary{background:var(--vt-color-accent,#4ecca3);border-color:var(--vt-color-accent,#4ecca3);color:#101827}.todo-btn.danger{border-color:rgba(233,69,96,.5);color:#ff9a9a}.todo-btn:disabled{opacity:.45;cursor:default}', '.todo-btn{min-height:1.9rem;padding:.32rem .62rem;border:1px solid var(--vt-color-border-strong,#2c456a);border-radius:var(--vt-radius-md,6px);background:var(--vt-color-surface-hover,#1b2440);color:var(--vt-color-text-secondary,#b7c0d4);font-size:.78rem;cursor:pointer}.todo-btn:hover{border-color:var(--vt-color-accent,#4ecca3);color:var(--vt-color-text-primary,#f4f7fb)}.todo-btn.primary{background:var(--vt-color-accent,#4ecca3);border-color:var(--vt-color-accent,#4ecca3);color:#101827}.todo-btn.danger{border-color:rgba(233,69,96,.5);color:#ff9a9a}.todo-btn:disabled{opacity:.45;cursor:default}',
'.todo-list{flex:1;min-height:0;overflow:auto;padding:.5rem .75rem .85rem}.todo-empty{height:100%;display:flex;align-items:center;justify-content:center;padding:2rem;text-align:center;color:var(--vt-color-text-muted,#7f8aa3);font-size:.86rem}', '.todo-list{flex:1;min-height:0;overflow:auto;padding:.5rem .75rem .85rem}.todo-empty{height:100%;display:flex;align-items:center;justify-content:center;padding:2rem;text-align:center;color:var(--vt-color-text-muted,#7f8aa3);font-size:.86rem}',
'.todo-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:.75rem;align-items:start;margin-top:.5rem;padding:.75rem .85rem;border:1px solid var(--vt-color-border,#202b46);border-radius:var(--vt-radius-lg,8px);background:var(--vt-color-surface,#15152c)}.todo-row:hover{background:var(--vt-color-surface-hover,#1b2440)}.todo-row.done .todo-row-title{text-decoration:line-through;color:var(--vt-color-text-muted,#7f8aa3)}', '.todo-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:.75rem;align-items:start;margin-top:.5rem;padding:.75rem .85rem;border:1px solid var(--vt-color-border,#202b46);border-radius:var(--vt-radius-lg,8px);background:var(--vt-color-surface,#15152c)}.todo-row:hover{background:var(--vt-color-surface-hover,#1b2440)}.todo-row.done .todo-row-title{text-decoration:line-through;color:var(--vt-color-text-muted,#7f8aa3)}',
@ -132,12 +132,20 @@
return match ? { date: match[1], time: match[2] } : { date: '', time: '' }; return match ? { date: match[1], time: match[2] } : { date: '', time: '' };
} }
function joinReminderDateTime(date, hour, minute) { function cleanReminderTime(value) {
var match = /^(\d{1,2}):(\d{2})$/.exec(text(value).trim());
if (!match) return '';
var hour = Number(match[1]);
var minute = Number(match[2]);
if (hour > 23 || minute > 59) return '';
return String(hour).padStart(2, '0') + ':' + String(minute).padStart(2, '0');
}
function joinReminderDateTime(date, time) {
date = cleanDate(date); date = cleanDate(date);
hour = text(hour).trim(); time = cleanReminderTime(time);
minute = text(minute).trim(); if (!date || !time) return '';
if (!date || !/^\d{2}$/.test(hour) || !/^\d{2}$/.test(minute)) return ''; return cleanDateTime(date + 'T' + time);
return cleanDateTime(date + 'T' + hour + ':' + minute);
} }
function todoId(workspaceRoot, title) { function todoId(workspaceRoot, title) {
@ -147,6 +155,8 @@
function normalizeTodo(value) { function normalizeTodo(value) {
value = value || {}; value = value || {};
var status = cleanStatus(value.status); var status = cleanStatus(value.status);
var reminderAt = cleanDateTime(value.reminderAt || value.reminderDateTime);
var reminder = splitReminderDateTime(reminderAt);
var createdAt = text(value.createdAt).trim() || now(); var createdAt = text(value.createdAt).trim() || now();
var completedAt = status === 'done' ? (text(value.completedAt).trim() || createdAt) : ''; var completedAt = status === 'done' ? (text(value.completedAt).trim() || createdAt) : '';
return { return {
@ -158,7 +168,8 @@
status: status, status: status,
priority: cleanPriority(value.priority), priority: cleanPriority(value.priority),
dueAt: cleanDate(value.dueAt || value.dueDate), dueAt: cleanDate(value.dueAt || value.dueDate),
reminderAt: cleanDateTime(value.reminderAt || value.reminderDateTime), reminderDate: cleanDate(value.reminderDate) || reminder.date,
reminderAt: reminderAt,
createdAt: createdAt, createdAt: createdAt,
updatedAt: text(value.updatedAt).trim() || createdAt, updatedAt: text(value.updatedAt).trim() || createdAt,
completedAt: completedAt, completedAt: completedAt,
@ -187,6 +198,7 @@
status: todo.status, status: todo.status,
priority: todo.priority, priority: todo.priority,
dueAt: todo.dueAt, dueAt: todo.dueAt,
reminderDate: todo.reminderDate,
reminderAt: todo.reminderAt, reminderAt: todo.reminderAt,
createdAt: todo.createdAt, createdAt: todo.createdAt,
updatedAt: todo.updatedAt, updatedAt: todo.updatedAt,
@ -330,15 +342,6 @@
return el('option', { value: value, textContent: label }); return el('option', { value: value, textContent: label });
} }
function timeOptions(limit, emptyLabel) {
var options = [option('', emptyLabel)];
for (var value = 0; value < limit; value += 1) {
var formatted = String(value).padStart(2, '0');
options.push(option(formatted, formatted));
}
return options;
}
function cleanStatusFilter(value) { function cleanStatusFilter(value) {
value = text(value).trim().toLowerCase(); value = text(value).trim().toLowerCase();
return value === 'all' || STATUS_VALUES.indexOf(value) !== -1 ? value : 'all'; return value === 'all' || STATUS_VALUES.indexOf(value) !== -1 ? value : 'all';
@ -457,12 +460,8 @@
priorityInput.value = editing ? existingTodo.priority : 'normal'; priorityInput.value = editing ? existingTodo.priority : 'normal';
var dueInput = el('input', { className: 'todo-input', type: 'date', value: editing ? existingTodo.dueAt : '', 'data-todo-input': 'dueAt' }); var dueInput = el('input', { className: 'todo-input', type: 'date', value: editing ? existingTodo.dueAt : '', 'data-todo-input': 'dueAt' });
var reminder = splitReminderDateTime(editing ? existingTodo.reminderAt : ''); var reminder = splitReminderDateTime(editing ? existingTodo.reminderAt : '');
var reminderDateInput = el('input', { className: 'todo-input', type: 'date', value: reminder.date, 'data-todo-input': 'reminderDate' }); var reminderDateInput = el('input', { className: 'todo-input', type: 'date', value: editing ? existingTodo.reminderDate || reminder.date : '', 'data-todo-input': 'reminderDate' });
var reminderTime = reminder.time.split(':'); var reminderTimeInput = el('input', { className: 'todo-input todo-time-input', type: 'text', inputmode: 'numeric', maxlength: '5', placeholder: tr('ui.reminderTimePlaceholder', null, '14:30'), value: reminder.time, 'data-todo-input': 'reminderTime', 'aria-label': tr('ui.field.reminderTime', null, 'Reminder time') });
var reminderHourInput = el('select', { className: 'todo-select todo-time-select', 'data-todo-input': 'reminderHour', 'aria-label': tr('ui.field.reminderHour', null, 'Hour') }, timeOptions(24, tr('ui.time.hourPlaceholder', null, 'Hour')));
var reminderMinuteInput = el('select', { className: 'todo-select todo-time-select', 'data-todo-input': 'reminderMinute', 'aria-label': tr('ui.field.reminderMinute', null, 'Minute') }, timeOptions(60, tr('ui.time.minutePlaceholder', null, 'Minute')));
reminderHourInput.value = reminderTime[0] || '';
reminderMinuteInput.value = reminderTime[1] || '';
var workspaceInput = null; var workspaceInput = null;
var workspace = editing ? existingTodo.workspaceRootPath : scope.workspaceRoot; var workspace = editing ? existingTodo.workspaceRootPath : scope.workspaceRoot;
if (scope.mode === 'global') { if (scope.mode === 'global') {
@ -483,6 +482,20 @@
return; return;
} }
var workspaceRoot = scope.mode === 'workspace' ? scope.workspaceRoot : cleanWorkspace(workspaceInput && workspaceInput.value); var workspaceRoot = scope.mode === 'workspace' ? scope.workspaceRoot : cleanWorkspace(workspaceInput && workspaceInput.value);
var reminderDate = cleanDate(reminderDateInput.value);
var reminderTime = cleanReminderTime(reminderTimeInput.value);
if (text(reminderTimeInput.value).trim() && !reminderTime) {
statusText = tr('ui.reminderTimeInvalid', null, 'Enter a valid reminder time (HH:MM)');
statusClass = 'error';
render();
return;
}
if (reminderTime && !reminderDate) {
statusText = tr('ui.reminderDateRequired', null, 'Choose a reminder date before setting its time');
statusClass = 'error';
render();
return;
}
var timestamp = now(); var timestamp = now();
var next = normalizeTodo({ var next = normalizeTodo({
id: editing ? existingTodo.id : todoId(workspaceRoot, title), id: editing ? existingTodo.id : todoId(workspaceRoot, title),
@ -493,7 +506,8 @@
status: editing ? existingTodo.status : 'open', status: editing ? existingTodo.status : 'open',
priority: priorityInput.value, priority: priorityInput.value,
dueAt: dueInput.value, dueAt: dueInput.value,
reminderAt: joinReminderDateTime(reminderDateInput.value, reminderHourInput.value, reminderMinuteInput.value), reminderDate: reminderDate,
reminderAt: joinReminderDateTime(reminderDate, reminderTime),
createdAt: editing ? existingTodo.createdAt : timestamp, createdAt: editing ? existingTodo.createdAt : timestamp,
updatedAt: timestamp, updatedAt: timestamp,
completedAt: editing ? existingTodo.completedAt : '', completedAt: editing ? existingTodo.completedAt : '',
@ -519,7 +533,7 @@
el('label', { className: 'todo-field' }, [tr('ui.field.priority', null, 'Priority'), priorityInput]), el('label', { className: 'todo-field' }, [tr('ui.field.priority', null, 'Priority'), priorityInput]),
el('label', { className: 'todo-field' }, [tr('ui.field.due', null, 'Due date'), dueInput]), el('label', { className: 'todo-field' }, [tr('ui.field.due', null, 'Due date'), dueInput]),
el('label', { className: 'todo-field' }, [tr('ui.field.reminderDate', null, 'Reminder date'), reminderDateInput]), el('label', { className: 'todo-field' }, [tr('ui.field.reminderDate', null, 'Reminder date'), reminderDateInput]),
el('div', { className: 'todo-field' }, [tr('ui.field.reminderTime', null, 'Reminder time'), el('div', { className: 'todo-time-picker' }, [reminderHourInput, reminderMinuteInput])]) el('label', { className: 'todo-field' }, [tr('ui.field.reminderTime', null, 'Reminder time'), reminderTimeInput])
]; ];
if (workspaceInput) fields.push(el('label', { className: 'todo-field' }, [tr('ui.field.workspace', null, 'Workspace'), workspaceInput])); if (workspaceInput) fields.push(el('label', { className: 'todo-field' }, [tr('ui.field.workspace', null, 'Workspace'), workspaceInput]));
else fields.push(el('div', { className: 'todo-field', textContent: tr('ui.workspaceValue', { workspace: scope.workspaceRoot }, 'Workspace: ' + scope.workspaceRoot) })); else fields.push(el('div', { className: 'todo-field', textContent: tr('ui.workspaceValue', { workspace: scope.workspaceRoot }, 'Workspace: ' + scope.workspaceRoot) }));
@ -600,6 +614,7 @@
meta.push(el('span', { className: 'todo-badge', textContent: tr('ui.status.' + todo.status, null, todo.status) })); meta.push(el('span', { className: 'todo-badge', textContent: tr('ui.status.' + todo.status, null, todo.status) }));
if (todo.dueAt) meta.push(el('span', { className: 'todo-badge ' + due, textContent: tr('ui.dueValue', { prefix: due === 'overdue' ? tr('ui.overduePrefix', null, 'Overdue · ') : (due === 'due-soon' ? tr('ui.dueSoonPrefix', null, 'Due soon · ') : ''), date: formatDate(todo.dueAt) }, (due === 'overdue' ? 'Overdue · ' : (due === 'due-soon' ? 'Due soon · ' : '')) + 'Due ' + formatDate(todo.dueAt)) })); if (todo.dueAt) meta.push(el('span', { className: 'todo-badge ' + due, textContent: tr('ui.dueValue', { prefix: due === 'overdue' ? tr('ui.overduePrefix', null, 'Overdue · ') : (due === 'due-soon' ? tr('ui.dueSoonPrefix', null, 'Due soon · ') : ''), date: formatDate(todo.dueAt) }, (due === 'overdue' ? 'Overdue · ' : (due === 'due-soon' ? 'Due soon · ' : '')) + 'Due ' + formatDate(todo.dueAt)) }));
if (todo.reminderAt) meta.push(el('span', { className: 'todo-badge ' + (reminderDue ? 'reminder-due' : ''), textContent: tr(reminderDue ? 'ui.reminderDueValue' : 'ui.reminderValue', { date: formatDate(todo.reminderAt) }, (reminderDue ? 'Reminder due ' : 'Reminder ') + formatDate(todo.reminderAt)) })); if (todo.reminderAt) meta.push(el('span', { className: 'todo-badge ' + (reminderDue ? 'reminder-due' : ''), textContent: tr(reminderDue ? 'ui.reminderDueValue' : 'ui.reminderValue', { date: formatDate(todo.reminderAt) }, (reminderDue ? 'Reminder due ' : 'Reminder ') + formatDate(todo.reminderAt)) }));
else if (todo.reminderDate) meta.push(el('span', { className: 'todo-badge', textContent: tr('ui.reminderDateValue', { date: formatDate(todo.reminderDate) }, 'Reminder date ' + formatDate(todo.reminderDate)) }));
return el('div', { className: 'todo-row-meta' }, meta); return el('div', { className: 'todo-row-meta' }, meta);
} }

View File

@ -37,10 +37,9 @@
"ui.field.reminder": "Reminder", "ui.field.reminder": "Reminder",
"ui.field.reminderDate": "Reminder date", "ui.field.reminderDate": "Reminder date",
"ui.field.reminderTime": "Reminder time", "ui.field.reminderTime": "Reminder time",
"ui.field.reminderHour": "Hour", "ui.reminderTimePlaceholder": "14:30",
"ui.field.reminderMinute": "Minute", "ui.reminderTimeInvalid": "Enter a valid reminder time (HH:MM)",
"ui.time.hourPlaceholder": "Hour", "ui.reminderDateRequired": "Choose a reminder date before setting its time",
"ui.time.minutePlaceholder": "Minute",
"ui.field.workspace": "Workspace", "ui.field.workspace": "Workspace",
"ui.workspaceValue": "Workspace: {workspace}", "ui.workspaceValue": "Workspace: {workspace}",
"ui.edit": "Edit Todo", "ui.edit": "Edit Todo",
@ -56,6 +55,7 @@
"ui.dueValue": "{prefix}Due {date}", "ui.dueValue": "{prefix}Due {date}",
"ui.reminderDueValue": "Reminder due {date}", "ui.reminderDueValue": "Reminder due {date}",
"ui.reminderValue": "Reminder {date}", "ui.reminderValue": "Reminder {date}",
"ui.reminderDateValue": "Reminder date {date}",
"ui.noMatches": "No todos match the current filters.", "ui.noMatches": "No todos match the current filters.",
"ui.empty": "No todos yet.", "ui.empty": "No todos yet.",
"ui.openWorkspace": "Open workspace", "ui.openWorkspace": "Open workspace",

View File

@ -37,10 +37,9 @@
"ui.field.reminder": "Напоминание", "ui.field.reminder": "Напоминание",
"ui.field.reminderDate": "Дата напоминания", "ui.field.reminderDate": "Дата напоминания",
"ui.field.reminderTime": "Время напоминания", "ui.field.reminderTime": "Время напоминания",
"ui.field.reminderHour": "Часы", "ui.reminderTimePlaceholder": "14:30",
"ui.field.reminderMinute": "Минуты", "ui.reminderTimeInvalid": "Введите корректное время напоминания (ЧЧ:ММ)",
"ui.time.hourPlaceholder": "Часы", "ui.reminderDateRequired": "Сначала выберите дату напоминания",
"ui.time.minutePlaceholder": "Минуты",
"ui.field.workspace": "Рабочее пространство", "ui.field.workspace": "Рабочее пространство",
"ui.workspaceValue": "Рабочее пространство: {workspace}", "ui.workspaceValue": "Рабочее пространство: {workspace}",
"ui.edit": "Изменить задачу", "ui.edit": "Изменить задачу",
@ -56,6 +55,7 @@
"ui.dueValue": "{prefix}Срок: {date}", "ui.dueValue": "{prefix}Срок: {date}",
"ui.reminderDueValue": "Пора напомнить: {date}", "ui.reminderDueValue": "Пора напомнить: {date}",
"ui.reminderValue": "Напоминание: {date}", "ui.reminderValue": "Напоминание: {date}",
"ui.reminderDateValue": "Дата напоминания: {date}",
"ui.noMatches": "Нет задач, соответствующих фильтрам.", "ui.noMatches": "Нет задач, соответствующих фильтрам.",
"ui.empty": "Задач пока нет.", "ui.empty": "Задач пока нет.",
"ui.openWorkspace": "Открыть рабочее пространство", "ui.openWorkspace": "Открыть рабочее пространство",

View File

@ -233,14 +233,11 @@ async function mountWithApi(apiState, props, emittedEvents = [], document = make
byData(container, 'data-todo-input', 'priority').value = 'high'; byData(container, 'data-todo-input', 'priority').value = 'high';
byData(container, 'data-todo-input', 'dueAt').value = '01/02/2000'; byData(container, 'data-todo-input', 'dueAt').value = '01/02/2000';
const reminderDate = byData(container, 'data-todo-input', 'reminderDate'); const reminderDate = byData(container, 'data-todo-input', 'reminderDate');
const reminderHour = byData(container, 'data-todo-input', 'reminderHour'); const reminderTime = byData(container, 'data-todo-input', 'reminderTime');
const reminderMinute = byData(container, 'data-todo-input', 'reminderMinute');
if (!reminderDate || reminderDate.getAttribute('type') !== 'date') throw new Error('Todo reminder date input was not rendered'); if (!reminderDate || reminderDate.getAttribute('type') !== 'date') throw new Error('Todo reminder date input was not rendered');
if (!reminderHour || reminderHour.tagName !== 'SELECT') throw new Error('Todo reminder hour selector was not rendered'); if (!reminderTime || reminderTime.getAttribute('type') !== 'text') throw new Error('Todo reminder time must be a keyboard-editable text input');
if (!reminderMinute || reminderMinute.tagName !== 'SELECT') throw new Error('Todo reminder minute selector was not rendered');
reminderDate.value = '01/02/2000'; reminderDate.value = '01/02/2000';
reminderHour.value = '09'; reminderTime.value = '09:30';
reminderMinute.value = '30';
byData(container, 'data-todo-action', 'save').click(); byData(container, 'data-todo-action', 'save').click();
await flush(); await flush();
@ -249,7 +246,7 @@ async function mountWithApi(apiState, props, emittedEvents = [], document = make
const createdTodo = storedAfterCreate[0]; const createdTodo = storedAfterCreate[0];
if (createdTodo.workspaceRootPath !== 'Project') throw new Error('workspace Todo did not keep the Project root path'); if (createdTodo.workspaceRootPath !== 'Project') throw new Error('workspace Todo did not keep the Project root path');
if (createdTodo.status !== 'open' || createdTodo.priority !== 'high') throw new Error('Todo status or priority was not stored'); if (createdTodo.status !== 'open' || createdTodo.priority !== 'high') throw new Error('Todo status or priority was not stored');
if (createdTodo.dueAt !== '2000-01-02' || createdTodo.reminderAt !== '2000-01-02T09:30') throw new Error('Todo due/reminder metadata was not stored'); if (createdTodo.dueAt !== '2000-01-02' || createdTodo.reminderDate !== '2000-01-02' || createdTodo.reminderAt !== '2000-01-02T09:30') throw new Error('Todo due/reminder metadata was not stored');
if (!container.textContent.includes('Overdue') || !container.textContent.includes('Reminder due')) throw new Error('due/reminder indicators were not rendered'); if (!container.textContent.includes('Overdue') || !container.textContent.includes('Reminder due')) throw new Error('due/reminder indicators were not rendered');
const scheduledAfterCreate = apiState.notificationCalls.at(-1) || []; const scheduledAfterCreate = apiState.notificationCalls.at(-1) || [];
if (scheduledAfterCreate.length !== 1 if (scheduledAfterCreate.length !== 1
@ -269,9 +266,20 @@ async function mountWithApi(apiState, props, emittedEvents = [], document = make
byData(container, 'data-todo-action', 'edit').click(); byData(container, 'data-todo-action', 'edit').click();
byData(container, 'data-todo-input', 'title').value = 'Prepare project review updated'; byData(container, 'data-todo-input', 'title').value = 'Prepare project review updated';
byData(container, 'data-todo-input', 'reminderTime').value = '29:30';
byData(container, 'data-todo-action', 'save').click();
await flush();
if (apiState.settings['todos:global'][0].title !== 'Prepare project review' || !container.textContent.includes('Enter a valid reminder time')) {
throw new Error('invalid reminder time was saved instead of showing a human-readable error');
}
byData(container, 'data-todo-input', 'reminderTime').value = '';
byData(container, 'data-todo-action', 'save').click(); byData(container, 'data-todo-action', 'save').click();
await flush(); await flush();
if (apiState.settings['todos:global'][0].title !== 'Prepare project review updated') throw new Error('Todo edit was not persisted'); if (apiState.settings['todos:global'][0].title !== 'Prepare project review updated') throw new Error('Todo edit was not persisted');
if (apiState.settings['todos:global'][0].reminderDate !== '2000-01-02' || apiState.settings['todos:global'][0].reminderAt !== '') {
throw new Error('clearing reminder time did not preserve its date while cancelling the reminder');
}
if ((apiState.notificationCalls.at(-1) || []).length !== 0) throw new Error('clearing reminder time did not remove the native notification');
byData(container, 'data-todo-action', 'mark-done').click(); byData(container, 'data-todo-action', 'mark-done').click();
await flush(); await flush();