feat: dashboard - add filters
This commit is contained in:
@@ -199,9 +199,11 @@ export function getTaskDashboardHtml(
|
|||||||
<option value="status">Status</option>
|
<option value="status">Status</option>
|
||||||
<option value="priority">Priority</option>
|
<option value="priority">Priority</option>
|
||||||
</select>
|
</select>
|
||||||
|
<button id="resetFilters" type="button" class="secondary" title="Reset filters">Reset</button>
|
||||||
<button id="refresh" type="button" class="secondary" title="Refresh">↻</button>
|
<button id="refresh" type="button" class="secondary" title="Refresh">↻</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="row" id="status-filters" aria-label="Status filters"></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>
|
||||||
<div id="list"></div>
|
<div id="list"></div>
|
||||||
</aside>
|
</aside>
|
||||||
@@ -228,8 +230,15 @@ export function getTaskDashboardHtml(
|
|||||||
{ value: "cancelled", label: "Cancelled" },
|
{ value: "cancelled", label: "Cancelled" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const PRIORITIES = [
|
||||||
|
{ value: "critical", label: "Critical" },
|
||||||
|
{ value: "high", label: "High" },
|
||||||
|
{ value: "medium", label: "Medium" },
|
||||||
|
{ value: "low", label: "Low" },
|
||||||
|
];
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
filter: { statuses: [], query: "" },
|
filter: {},
|
||||||
groupBy: "none",
|
groupBy: "none",
|
||||||
selectedId: undefined,
|
selectedId: undefined,
|
||||||
groups: [],
|
groups: [],
|
||||||
@@ -242,7 +251,9 @@ export function getTaskDashboardHtml(
|
|||||||
query: document.getElementById("query"),
|
query: document.getElementById("query"),
|
||||||
groupBy: document.getElementById("groupBy"),
|
groupBy: document.getElementById("groupBy"),
|
||||||
refresh: document.getElementById("refresh"),
|
refresh: document.getElementById("refresh"),
|
||||||
|
resetFilters: document.getElementById("resetFilters"),
|
||||||
statusFilters: document.getElementById("status-filters"),
|
statusFilters: document.getElementById("status-filters"),
|
||||||
|
priorityFilters: document.getElementById("priority-filters"),
|
||||||
empty: document.getElementById("empty"),
|
empty: document.getElementById("empty"),
|
||||||
detail: document.getElementById("detail"),
|
detail: document.getElementById("detail"),
|
||||||
title: document.getElementById("detail-title"),
|
title: document.getElementById("detail-title"),
|
||||||
@@ -261,31 +272,6 @@ export function getTaskDashboardHtml(
|
|||||||
el.statusLine.classList.toggle("error", Boolean(isError));
|
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) {
|
function normalizeFilter(filter) {
|
||||||
const next = {};
|
const next = {};
|
||||||
if (filter.statuses && filter.statuses.length) next.statuses = filter.statuses;
|
if (filter.statuses && filter.statuses.length) next.statuses = filter.statuses;
|
||||||
@@ -295,6 +281,65 @@ export function getTaskDashboardHtml(
|
|||||||
return next;
|
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() {
|
function renderList() {
|
||||||
el.list.innerHTML = "";
|
el.list.innerHTML = "";
|
||||||
if (!state.groups.length) {
|
if (!state.groups.length) {
|
||||||
@@ -367,7 +412,7 @@ export function getTaskDashboardHtml(
|
|||||||
state.dirty = false;
|
state.dirty = false;
|
||||||
el.query.value = state.filter.query || "";
|
el.query.value = state.filter.query || "";
|
||||||
el.groupBy.value = state.groupBy;
|
el.groupBy.value = state.groupBy;
|
||||||
renderStatusChips();
|
renderFilterChips();
|
||||||
renderList();
|
renderList();
|
||||||
renderDetail();
|
renderDetail();
|
||||||
setStatus("");
|
setStatus("");
|
||||||
@@ -379,7 +424,7 @@ export function getTaskDashboardHtml(
|
|||||||
window.clearTimeout(queryTimer);
|
window.clearTimeout(queryTimer);
|
||||||
queryTimer = window.setTimeout(() => {
|
queryTimer = window.setTimeout(() => {
|
||||||
state.filter = { ...state.filter, query };
|
state.filter = { ...state.filter, query };
|
||||||
post({ type: "setFilter", filter: normalizeFilter(state.filter) });
|
postFilter();
|
||||||
}, ${QUERY_DEBOUNCE_MS});
|
}, ${QUERY_DEBOUNCE_MS});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -392,18 +437,24 @@ export function getTaskDashboardHtml(
|
|||||||
post({ type: "refresh" });
|
post({ type: "refresh" });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
el.resetFilters.addEventListener("click", () => {
|
||||||
|
resetFilters();
|
||||||
|
});
|
||||||
|
|
||||||
el.description.addEventListener("input", () => {
|
el.description.addEventListener("input", () => {
|
||||||
state.dirty = true;
|
state.dirty = true;
|
||||||
setStatus("Unsaved changes");
|
setStatus("Unsaved changes");
|
||||||
});
|
});
|
||||||
|
|
||||||
el.save.addEventListener("click", () => {
|
el.save.addEventListener("click", () => {
|
||||||
if (!state.selected) return;
|
saveDescription();
|
||||||
post({
|
});
|
||||||
type: "saveDescription",
|
|
||||||
id: state.selected.id,
|
window.addEventListener("keydown", (event) => {
|
||||||
description: el.description.value,
|
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "s") {
|
||||||
});
|
event.preventDefault();
|
||||||
|
saveDescription();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener("message", (event) => {
|
window.addEventListener("message", (event) => {
|
||||||
@@ -421,7 +472,7 @@ export function getTaskDashboardHtml(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
renderStatusChips();
|
renderFilterChips();
|
||||||
post({ type: "ready" });
|
post({ type: "ready" });
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Dashboard
|
## Dashboard
|
||||||
|
|
||||||
[ ] **Сохранение по Ctrl+S**
|
[x] **Сохранение по Ctrl+S**
|
||||||
|
|
||||||
- В detail (textarea description) — hotkey `Ctrl+S` / `Cmd+S` → тот же flow, что
|
- В detail (textarea description) — hotkey `Ctrl+S` / `Cmd+S` → тот же flow, что
|
||||||
кнопка Save (`saveDescription`).
|
кнопка Save (`saveDescription`).
|
||||||
@@ -29,15 +29,14 @@ Use-case/repo для headless/тестов можно оставить; editor-s
|
|||||||
**Пока:** не блокер; format при правке файла в editor уже ок. Ctrl+S в
|
**Пока:** не блокер; format при правке файла в editor уже ок. Ctrl+S в
|
||||||
Dashboard без editor-path format не даст.
|
Dashboard без editor-path format не даст.
|
||||||
|
|
||||||
[ ] **Кнопка сброса фильтров**
|
[x] **Кнопка сброса фильтров**
|
||||||
|
|
||||||
- В toolbar Dashboard: Reset → `filter = {}`, query/chips/groupBy defaults,
|
- В toolbar Dashboard: Reset → `filter = {}`, query/chips/groupBy defaults,
|
||||||
`setFilter` + при необходимости сброс groupBy.
|
`setFilter` + сброс groupBy.
|
||||||
|
|
||||||
[ ] **Фильтр по приоритету**
|
[x] **Фильтр по приоритету**
|
||||||
|
|
||||||
- Chips как у status: low / medium / high / critical → `filter.priorities` (уже
|
- Chips: critical / high / medium / low → `filter.priorities`.
|
||||||
есть в `TaskFilter` / `filterTasks`).
|
|
||||||
|
|
||||||
[ ] **Dashboard: raw MD ↔ view**
|
[ ] **Dashboard: raw MD ↔ view**
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user