Internal
Public Access
Add sidebar Tags section to offline mode
Offline mode had tag chips and per-task tag assignment, but was missing the dashboard sidebar's Tags section entirely: filtering the task list by tag, per-tag task counts, and tag create/rename/delete. Ports that section over (reusing the same tag modal pattern as the online create/edit flow), combinable with the existing status filters the same way it works online. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
ae9ab596bb
commit
3fbab2b831
+176
-4
@@ -13,8 +13,10 @@ const TAG_COLOR_PRESETS = ['#3b82f6', '#ef4444', '#10b981', '#f59e0b', '#8b5cf6'
|
||||
|
||||
let currentFilter = 'all';
|
||||
let currentSort = 'due_date';
|
||||
let currentTagId = null;
|
||||
let openTaskSyncId = null;
|
||||
let timerDisplayInterval = null;
|
||||
let currentTagModalSyncId = null;
|
||||
|
||||
/* ============================================
|
||||
Date/format helpers
|
||||
@@ -64,6 +66,11 @@ function applyFilter(tasks, filter) {
|
||||
}
|
||||
}
|
||||
|
||||
function applyTagFilter(tasks, tagId) {
|
||||
if (!tagId) return tasks;
|
||||
return tasks.filter((t) => (t.tag_sync_ids || []).includes(tagId));
|
||||
}
|
||||
|
||||
function applySort(tasks, sort) {
|
||||
const sorted = [...tasks];
|
||||
const dueOrDefault = (t) => t.due_date || '9999-99-99';
|
||||
@@ -217,7 +224,17 @@ async function renderOfflineTasks() {
|
||||
// Only top-level tasks in the main list - subtasks appear inside their
|
||||
// parent's detail modal, matching the online dashboard.
|
||||
const topLevel = allTasks.filter((t) => !t.is_deleted && !t.parent_sync_id);
|
||||
const visibleTasks = applySort(applyFilter(topLevel, currentFilter), currentSort);
|
||||
const statusFiltered = applyFilter(topLevel, currentFilter);
|
||||
const visibleTasks = applySort(applyTagFilter(statusFiltered, currentTagId), currentSort);
|
||||
|
||||
setActiveFilterNav(currentFilter);
|
||||
renderTagSidebarNav(allTags, topLevel);
|
||||
|
||||
const titleEl = document.getElementById('offline-page-title');
|
||||
if (titleEl) {
|
||||
const tagName = currentTagId && tagsBySyncId[currentTagId] ? tagsBySyncId[currentTagId].name : null;
|
||||
titleEl.textContent = (FILTER_TITLES[currentFilter] || 'All Tasks') + (tagName ? ` • ${tagName}` : '');
|
||||
}
|
||||
|
||||
listEl.innerHTML = '';
|
||||
|
||||
@@ -244,8 +261,150 @@ function setActiveFilterNav(filter) {
|
||||
document.querySelectorAll('#offline-filter-nav .sidebar-item').forEach((item) => {
|
||||
item.classList.toggle('active', item.dataset.filter === filter);
|
||||
});
|
||||
const titleEl = document.getElementById('offline-page-title');
|
||||
if (titleEl) titleEl.textContent = FILTER_TITLES[filter] || 'All Tasks';
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Sidebar Tags section (filter-by-tag + manage)
|
||||
============================================ */
|
||||
|
||||
function renderTagSidebarNav(allTags, topLevelTasks) {
|
||||
const nav = document.getElementById('offline-tag-nav');
|
||||
if (!nav) return;
|
||||
|
||||
const visibleTags = allTags.filter((t) => !t.is_deleted && !t.is_archived);
|
||||
// Counts match the sidebar Filters' "all" semantics: top-level, not completed.
|
||||
const countable = topLevelTasks.filter((t) => t.status !== 'completed');
|
||||
|
||||
nav.innerHTML = visibleTags.map((tag) => {
|
||||
const count = countable.filter((t) => (t.tag_sync_ids || []).includes(tag.sync_id)).length;
|
||||
return `
|
||||
<div class="sidebar-group-row">
|
||||
<a href="#" class="sidebar-item sidebar-group-item ${currentTagId === tag.sync_id ? 'active' : ''}" data-tag-id="${tag.sync_id}">
|
||||
<span class="group-color-dot" style="background-color: ${tag.color}"></span>
|
||||
${escapeHtml(tag.name)}
|
||||
${count ? `<span class="sidebar-item-count">${count}</span>` : ''}
|
||||
</a>
|
||||
<button type="button" class="sidebar-group-edit" title="Edit tag" data-edit-tag-id="${tag.sync_id}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}).join('') + `
|
||||
<a href="#" class="sidebar-item sidebar-group-item ${!currentTagId ? 'active' : ''}" id="offline-all-tags-link">
|
||||
<span class="group-color-dot" style="background-color: var(--text-muted)"></span>
|
||||
All Tags
|
||||
</a>
|
||||
`;
|
||||
|
||||
nav.querySelectorAll('.sidebar-group-item[data-tag-id]').forEach((link) => {
|
||||
link.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
currentTagId = link.dataset.tagId;
|
||||
renderOfflineTasks();
|
||||
});
|
||||
});
|
||||
nav.querySelectorAll('.sidebar-group-edit').forEach((btn) => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const tag = visibleTags.find((t) => t.sync_id === btn.dataset.editTagId);
|
||||
if (tag) openTagModal(tag.sync_id, tag.name, tag.color);
|
||||
});
|
||||
});
|
||||
const allTagsLink = document.getElementById('offline-all-tags-link');
|
||||
if (allTagsLink) {
|
||||
allTagsLink.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
currentTagId = null;
|
||||
renderOfflineTasks();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function renderTagColorPresets(selectedColor) {
|
||||
const container = document.getElementById('tag-modal-color-presets');
|
||||
if (!container) return;
|
||||
container.innerHTML = TAG_COLOR_PRESETS.map((c) => `
|
||||
<button type="button" class="color-preset ${c.toLowerCase() === selectedColor.toLowerCase() ? 'selected' : ''}" data-color="${c}" style="background-color: ${c}"></button>
|
||||
`).join('');
|
||||
container.querySelectorAll('.color-preset').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.getElementById('tag-modal-color').value = btn.dataset.color;
|
||||
container.querySelectorAll('.color-preset').forEach((b) => b.classList.toggle('selected', b === btn));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function openTagModal(tagSyncId = null, name = '', color = '#3b82f6') {
|
||||
currentTagModalSyncId = tagSyncId;
|
||||
document.getElementById('tag-modal-title').textContent = tagSyncId ? 'Edit Tag' : 'New Tag';
|
||||
document.getElementById('tag-modal-name').value = name;
|
||||
document.getElementById('tag-modal-color').value = color;
|
||||
document.getElementById('tag-modal-submit-btn').textContent = tagSyncId ? 'Save' : 'Create';
|
||||
document.getElementById('tag-modal-delete-btn').style.display = tagSyncId ? 'inline-flex' : 'none';
|
||||
renderTagColorPresets(color);
|
||||
document.getElementById('tag-modal-backdrop').classList.add('open');
|
||||
document.getElementById('tag-modal-name').focus();
|
||||
}
|
||||
|
||||
function closeTagModal() {
|
||||
document.getElementById('tag-modal-backdrop').classList.remove('open');
|
||||
currentTagModalSyncId = null;
|
||||
}
|
||||
|
||||
async function saveTagModal() {
|
||||
const name = document.getElementById('tag-modal-name').value.trim();
|
||||
if (!name) return;
|
||||
const color = document.getElementById('tag-modal-color').value;
|
||||
const now = new Date().toISOString();
|
||||
|
||||
if (currentTagModalSyncId) {
|
||||
const tags = await getAllTags();
|
||||
const tag = tags.find((t) => t.sync_id === currentTagModalSyncId);
|
||||
if (tag) {
|
||||
tag.name = name;
|
||||
tag.color = color;
|
||||
tag.updated_at = now;
|
||||
tag._dirty = true;
|
||||
await putLocalTag(tag);
|
||||
}
|
||||
} else {
|
||||
const syncId = crypto.randomUUID();
|
||||
await putLocalTag({
|
||||
id: syncId, sync_id: syncId, name, description: '', color, icon: '',
|
||||
sort_order: 0, is_archived: false, is_deleted: false,
|
||||
created_at: now, updated_at: now, _dirty: true, _pending_conflict_id: null,
|
||||
});
|
||||
}
|
||||
|
||||
closeTagModal();
|
||||
renderOfflineTasks();
|
||||
if (openTaskSyncId) {
|
||||
renderTaskDetailPane();
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteCurrentTag() {
|
||||
if (!currentTagModalSyncId) return;
|
||||
if (!confirm('Are you sure you want to delete this tag?\n\nTasks with this tag will not be deleted.')) {
|
||||
return;
|
||||
}
|
||||
const tags = await getAllTags();
|
||||
const tag = tags.find((t) => t.sync_id === currentTagModalSyncId);
|
||||
if (tag) {
|
||||
tag.is_deleted = true;
|
||||
tag.updated_at = new Date().toISOString();
|
||||
tag._dirty = true;
|
||||
await putLocalTag(tag);
|
||||
}
|
||||
if (currentTagId === currentTagModalSyncId) {
|
||||
currentTagId = null;
|
||||
}
|
||||
closeTagModal();
|
||||
renderOfflineTasks();
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
@@ -569,7 +728,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
item.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
currentFilter = item.dataset.filter;
|
||||
setActiveFilterNav(currentFilter);
|
||||
renderOfflineTasks();
|
||||
});
|
||||
});
|
||||
@@ -582,6 +740,20 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
renderOfflineTasks();
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('offline-add-tag-btn').addEventListener('click', () => openTagModal());
|
||||
document.getElementById('tag-modal-close-btn').addEventListener('click', closeTagModal);
|
||||
document.getElementById('tag-modal-cancel-btn').addEventListener('click', closeTagModal);
|
||||
document.getElementById('tag-modal-delete-btn').addEventListener('click', deleteCurrentTag);
|
||||
document.getElementById('tag-edit-form').addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
saveTagModal();
|
||||
});
|
||||
document.getElementById('tag-modal-backdrop').addEventListener('click', (e) => {
|
||||
if (e.target.id === 'tag-modal-backdrop') {
|
||||
closeTagModal();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/* ============================================
|
||||
|
||||
Reference in New Issue
Block a user