feat: dashboard - add filters

This commit is contained in:
2026-07-14 23:44:30 +05:00
parent 4bd3aa0cd1
commit dc37cd8553
2 changed files with 91 additions and 41 deletions
+86 -35
View File
@@ -199,9 +199,11 @@ export function getTaskDashboardHtml(
<option value="status">Status</option>
<option value="priority">Priority</option>
</select>
<button id="resetFilters" type="button" class="secondary" title="Reset filters">Reset</button>
<button id="refresh" type="button" class="secondary" title="Refresh">↻</button>
</div>
<div class="row" id="status-filters" aria-label="Status filters"></div>
<div class="row" id="priority-filters" aria-label="Priority filters"></div>
</div>
<div id="list"></div>
</aside>
@@ -228,8 +230,15 @@ export function getTaskDashboardHtml(
{ value: "cancelled", label: "Cancelled" },
];
const PRIORITIES = [
{ value: "critical", label: "Critical" },
{ value: "high", label: "High" },
{ value: "medium", label: "Medium" },
{ value: "low", label: "Low" },
];
const state = {
filter: { statuses: [], query: "" },
filter: {},
groupBy: "none",
selectedId: undefined,
groups: [],
@@ -242,7 +251,9 @@ export function getTaskDashboardHtml(
query: document.getElementById("query"),
groupBy: document.getElementById("groupBy"),
refresh: document.getElementById("refresh"),
resetFilters: document.getElementById("resetFilters"),
statusFilters: document.getElementById("status-filters"),
priorityFilters: document.getElementById("priority-filters"),
empty: document.getElementById("empty"),
detail: document.getElementById("detail"),
title: document.getElementById("detail-title"),
@@ -261,31 +272,6 @@ export function getTaskDashboardHtml(
el.statusLine.classList.toggle("error", Boolean(isError));
}
function selectedStatuses() {
return state.filter.statuses || [];
}
function renderStatusChips() {
el.statusFilters.innerHTML = "";
for (const s of STATUSES) {
const btn = document.createElement("button");
btn.type = "button";
btn.className = "chip" + (selectedStatuses().includes(s.value) ? " active" : "");
btn.textContent = s.label;
btn.addEventListener("click", () => {
const current = new Set(selectedStatuses());
if (current.has(s.value)) current.delete(s.value);
else current.add(s.value);
state.filter = {
...state.filter,
statuses: Array.from(current),
};
post({ type: "setFilter", filter: normalizeFilter(state.filter) });
});
el.statusFilters.appendChild(btn);
}
}
function normalizeFilter(filter) {
const next = {};
if (filter.statuses && filter.statuses.length) next.statuses = filter.statuses;
@@ -295,6 +281,65 @@ export function getTaskDashboardHtml(
return next;
}
function postFilter() {
post({ type: "setFilter", filter: normalizeFilter(state.filter) });
}
function renderChipRow(container, options, selectedValues, key) {
container.innerHTML = "";
for (const option of options) {
const btn = document.createElement("button");
btn.type = "button";
btn.className = "chip" + (selectedValues.includes(option.value) ? " active" : "");
btn.textContent = option.label;
btn.addEventListener("click", () => {
const current = new Set(selectedValues);
if (current.has(option.value)) current.delete(option.value);
else current.add(option.value);
state.filter = {
...state.filter,
[key]: Array.from(current),
};
postFilter();
});
container.appendChild(btn);
}
}
function renderFilterChips() {
renderChipRow(
el.statusFilters,
STATUSES,
state.filter.statuses || [],
"statuses",
);
renderChipRow(
el.priorityFilters,
PRIORITIES,
state.filter.priorities || [],
"priorities",
);
}
function saveDescription() {
if (!state.selected) return;
post({
type: "saveDescription",
id: state.selected.id,
description: el.description.value,
});
}
function resetFilters() {
state.filter = {};
state.groupBy = "none";
el.query.value = "";
el.groupBy.value = "none";
renderFilterChips();
post({ type: "setFilter", filter: {} });
post({ type: "setGroupBy", groupBy: "none" });
}
function renderList() {
el.list.innerHTML = "";
if (!state.groups.length) {
@@ -367,7 +412,7 @@ export function getTaskDashboardHtml(
state.dirty = false;
el.query.value = state.filter.query || "";
el.groupBy.value = state.groupBy;
renderStatusChips();
renderFilterChips();
renderList();
renderDetail();
setStatus("");
@@ -379,7 +424,7 @@ export function getTaskDashboardHtml(
window.clearTimeout(queryTimer);
queryTimer = window.setTimeout(() => {
state.filter = { ...state.filter, query };
post({ type: "setFilter", filter: normalizeFilter(state.filter) });
postFilter();
}, ${QUERY_DEBOUNCE_MS});
});
@@ -392,18 +437,24 @@ export function getTaskDashboardHtml(
post({ type: "refresh" });
});
el.resetFilters.addEventListener("click", () => {
resetFilters();
});
el.description.addEventListener("input", () => {
state.dirty = true;
setStatus("Unsaved changes");
});
el.save.addEventListener("click", () => {
if (!state.selected) return;
post({
type: "saveDescription",
id: state.selected.id,
description: el.description.value,
});
saveDescription();
});
window.addEventListener("keydown", (event) => {
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "s") {
event.preventDefault();
saveDescription();
}
});
window.addEventListener("message", (event) => {
@@ -421,7 +472,7 @@ export function getTaskDashboardHtml(
}
});
renderStatusChips();
renderFilterChips();
post({ type: "ready" });
</script>
</body>
+5 -6
View File
@@ -2,7 +2,7 @@
## Dashboard
[ ] **Сохранение по Ctrl+S**
[x] **Сохранение по Ctrl+S**
- В detail (textarea description) — hotkey `Ctrl+S` / `Cmd+S` → тот же flow, что
кнопка Save (`saveDescription`).
@@ -29,15 +29,14 @@ Use-case/repo для headless/тестов можно оставить; editor-s
**Пока:** не блокер; format при правке файла в editor уже ок. Ctrl+S в
Dashboard без editor-path format не даст.
[ ] **Кнопка сброса фильтров**
[x] **Кнопка сброса фильтров**
- В toolbar Dashboard: Reset → `filter = {}`, query/chips/groupBy defaults,
`setFilter` + при необходимости сброс groupBy.
`setFilter` + сброс groupBy.
[ ] **Фильтр по приоритету**
[x] **Фильтр по приоритету**
- Chips как у status: low / medium / high / critical`filter.priorities` (уже
есть в `TaskFilter` / `filterTasks`).
- Chips: critical / high / medium / low`filter.priorities`.
[ ] **Dashboard: raw MD ↔ view**