/** * GoingsOn - Saved Views Module * * Save, apply, pin, and delete named presets of the task-list filters. Wired to the * "Save View" menu item (Cmd/Ctrl+S). Tasks view only (Run #27 UX remediation: the * backend commands shipped with no frontend). The DOM task filters map onto the * backend `ViewFilters`; the milestone filter is not part of a saved view. */ (function() { 'use strict'; const esc = GoingsOn.utils.escapeHtml; const escAttr = GoingsOn.utils.escapeAttrValue; const escArg = GoingsOn.utils.escapeHandlerArg; // DOM single-value filter controls <-> backend ViewFilters (Vec-valued) enums. const STATUS_TO_ENUM = { pending: 'Pending', started: 'Started', completed: 'Completed' }; const ENUM_TO_STATUS = { Pending: 'pending', Started: 'started', Completed: 'completed' }; const PRIORITY_TO_ENUM = { H: 'High', M: 'Medium', L: 'Low' }; const ENUM_TO_PRIORITY = { High: 'H', Medium: 'M', Low: 'L' }; // Last-listed views, so the inline onclick handlers can resolve by id. let viewCache = []; /** Map the current task filter DOM controls to a backend ViewFilters object. */ function currentFiltersToViewFilters() { const f = GoingsOn.tasks.getFilters(); const vf = {}; if (f.status && STATUS_TO_ENUM[f.status]) vf.status = [STATUS_TO_ENUM[f.status]]; if (f.projectId) vf.projectId = f.projectId; if (f.priority && PRIORITY_TO_ENUM[f.priority]) vf.priority = [PRIORITY_TO_ENUM[f.priority]]; if (f.showSnoozed) vf.isSnoozed = true; if (f.waitingOnly) vf.isWaiting = true; return vf; } /** Apply a saved view's filters back onto the task filter DOM, then reload. */ function applyView(view) { if (!view) return; const vf = view.filters || {}; const setVal = (id, val) => { const el = document.getElementById(id); if (el) el.value = val; }; const setChk = (id, val) => { const el = document.getElementById(id); if (el) el.checked = !!val; }; const status = Array.isArray(vf.status) && vf.status.length ? (ENUM_TO_STATUS[vf.status[0]] || '') : ''; const priority = Array.isArray(vf.priority) && vf.priority.length ? (ENUM_TO_PRIORITY[vf.priority[0]] || '') : ''; setVal('filter-status', status); setVal('filter-project', vf.projectId || ''); setVal('filter-priority', priority); setVal('filter-milestone', ''); setChk('filter-snoozed', vf.isSnoozed); setChk('filter-waiting', vf.isWaiting); GoingsOn.ui.closeModal(); GoingsOn.tasks.applyFilters(); GoingsOn.ui.showToast(`Applied view "${view.name}"`, 'success'); } /** Build the modal markup: a save row, then the list of existing views. */ function buildModalHtml() { let html = `
`; if (viewCache.length) { html += `
`; for (const v of viewCache) { const pinLabel = v.isPinned ? 'Unpin' : 'Pin'; html += `
`; } html += `
`; } else { html += `

No saved views yet. Set your task filters, then save them here.

`; } html += `
`; return html; } /** Refresh the view list and (re)render the modal. openModal just resets * #modal-content, so re-calling it is a safe in-place re-render. */ async function refresh() { try { viewCache = await GoingsOn.api.tasks.savedViews.list(); } catch (err) { viewCache = []; } GoingsOn.ui.openModal('Saved Views', buildModalHtml()); } /** Entry point: the "Save View" menu item / Cmd+S. Tasks view only. */ async function openSaveModal() { const view = GoingsOn.getCurrentView ? GoingsOn.getCurrentView() : 'tasks'; if (view !== 'tasks') { GoingsOn.ui.showToast('Saved views are available on the Tasks view', 'info'); return; } await refresh(); } async function _save() { const nameEl = document.getElementById('saved-view-name'); const name = (nameEl?.value || '').trim(); if (!name) { GoingsOn.ui.showToast('Enter a name for the view', 'warning'); return; } const isPinned = document.getElementById('saved-view-pin')?.checked || false; const input = { name, viewType: 'tasks', filters: currentFiltersToViewFilters(), sortBy: null, sortOrder: null, isPinned, }; try { await GoingsOn.api.tasks.savedViews.create(input); GoingsOn.ui.showToast(`Saved view "${name}"`, 'success'); await refresh(); } catch (err) { GoingsOn.ui.showToast(GoingsOn.utils.getErrorMessage(err, 'Failed to save view'), 'error'); } } function _apply(id) { applyView(viewCache.find(v => v.id === id)); } async function _togglePin(id) { try { await GoingsOn.api.tasks.savedViews.togglePinned(id); await refresh(); } catch (err) { GoingsOn.ui.showToast(GoingsOn.utils.getErrorMessage(err, 'Failed to update view'), 'error'); } } async function _delete(id) { const view = viewCache.find(v => v.id === id); const confirmed = await GoingsOn.ui.confirmDelete( 'Delete Saved View', `Delete "${view ? view.name : 'this view'}"? This cannot be undone.` ); if (!confirmed) return; try { await GoingsOn.api.tasks.savedViews.delete(id); await refresh(); } catch (err) { GoingsOn.ui.showToast(GoingsOn.utils.getErrorMessage(err, 'Failed to delete view'), 'error'); } } GoingsOn.savedViews = { openSaveModal, _save, _apply, _togglePin, _delete, }; })();