feat: dashboard - add filters
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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**
|
||||
|
||||
|
||||
Reference in New Issue
Block a user