Files
espresso_frame/server/app/static/widget_dialog_tasks.js
T
Thomas Faour b5c52004c8
Build and push server image / test (push) Successful in 24s
Build and push server image / build-and-push (push) Successful in 2m1s
Build and push server image / deploy (push) Successful in 56s
Split the tasks feature out of the calendar widget into its own widget type
Task lists used to be a week-view-only sub-feature bolted onto calendar
widgets (CalendarWidgetConfig.tasks_*), so a task list could only exist
tied to a calendar's view and only inside its footprint. Tasks are now
a standalone widget type (TaskWidgetConfig, app/widgets/tasks.py) that
can be placed and sized independently, same as photos/calendar/
whiteboard -- no separate "enabled" flag either, since being on the
grid at all is the on/off switch, matching every other widget type.

Migration 17 creates task_widget_configs, extracts any existing
calendar widget's configured task source into a new sibling tasks
widget (auto-placed in open grid space, source dropped+logged if truly
none is left), then drops calendar_widget_configs' now-dead tasks_*
columns in the same migration -- this project's usual same-migration-
drop convention. Also handles the rarer case of a database jumping
straight from before the widget system existed to after this split in
one boot, via the legacy Frame.calendar_tasks_* columns.

Verified live in the browser at desktop and mobile widths: adding a
Tasks widget, its own dialog (task-list source picker + preview), and
confirming the calendar widget's dialog no longer mentions tasks at
all. Full test suite (180 tests, including new coverage for the widget
render/actions, the migration's data-extraction path, and the
permission-boundary shape for tasks-source) passes.
2026-07-25 02:11:45 +00:00

92 lines
3.4 KiB
JavaScript

// Tasks widget dialog: pick one of the viewer's own CalDAV task lists as
// this widget's source, plus the rendered preview. Not a page-load
// script -- frame_layout.js fetches this widget's dialog HTML fragment,
// injects it into the shared <dialog>, points window.FRAME_API at this
// specific widget (/api/frames/{id}/widgets/{widget_id}), then calls
// initTasksDialog().
// Rewrites #tasks-current-source in place instead of telling the user to
// reload -- the API always assigns a successful "set" to the caller
// (see api_widget_tasks_source), so after either action we already know
// exactly what the new state is without asking the server again.
function renderTasksCurrentSource(label) {
const container = document.getElementById('tasks-current-source');
container.innerHTML = '';
if (!label) {
container.innerHTML = '<p class="sub" style="margin-top: 10px;">No task list configured yet.</p>';
return;
}
const p = document.createElement('p');
p.className = 'sub';
p.style.marginTop = '10px';
p.append('Currently using your ');
const labelEl = document.createElement('strong');
labelEl.textContent = label;
p.append(labelEl, ' list. ');
const clearBtn = document.createElement('button');
clearBtn.type = 'button';
clearBtn.className = 'btn-inline secondary';
clearBtn.id = 'tasks-source-clear';
clearBtn.textContent = 'Clear';
clearBtn.addEventListener('click', clearTasksSource);
p.append(clearBtn);
container.append(p);
}
async function clearTasksSource() {
try {
const resp = await fetch(`${window.FRAME_API}/tasks-source`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ calendar_key: null }),
});
if (!resp.ok) throw new Error(await apiError(resp));
showStatus(true, 'Task list cleared.');
renderTasksCurrentSource(null);
document.querySelectorAll('.tasks-source-choice').forEach((r) => { r.checked = false; });
loadTasksPreview();
} catch (e) {
showStatus(false, e.message);
}
}
function loadTasksPreview() {
document.getElementById('tasks-preview').src = `${window.FRAME_API}/preview/tasks?_=${Date.now()}`;
}
function initTasksDialog() {
// Choosing one of your own CalDAV task lists as this widget's source --
// owner-only (see api_widget_tasks_source), so these radios only ever
// render for the viewer's own calendars anyway.
document.querySelectorAll('.tasks-source-choice').forEach((el) => {
el.addEventListener('change', async () => {
try {
const resp = await fetch(`${window.FRAME_API}/tasks-source`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ calendar_key: el.dataset.key }),
});
if (!resp.ok) throw new Error(await apiError(resp));
showStatus(true, 'Task list saved.');
const labelEl = el.closest('li').querySelector('label');
renderTasksCurrentSource(labelEl ? labelEl.textContent.trim() : '');
loadTasksPreview();
} catch (e) {
showStatus(false, e.message);
}
});
});
const tasksSourceClear = document.getElementById('tasks-source-clear');
if (tasksSourceClear) {
tasksSourceClear.addEventListener('click', clearTasksSource);
}
document.getElementById('tasks-preview-refresh').addEventListener('click', loadTasksPreview);
loadTasksPreview();
}
function closeTasksDialog() {
// Nothing to tear down -- no poll interval, unlike the photos dialog.
}