144 lines
6.1 KiB
JavaScript
144 lines
6.1 KiB
JavaScript
import { post } from "./api.js";
|
|
import { errorText, h, replace, showAlert } from "./dom.js";
|
|
import { openSettings } from "./settings.js";
|
|
import { activeTasks, currentSession, focusedProject, focusProject, focusTab, focusTask, lastVisits, onEach, state } from "./state.js";
|
|
import { openStatus } from "./status.js";
|
|
export function mountNavbar(root) {
|
|
const status = h("button", { class: "nav-link" }, "Status");
|
|
status.onclick = openStatus;
|
|
const settings = h("button", { class: "nav-link" }, "Settings");
|
|
settings.onclick = openSettings;
|
|
const conn = h("span", { class: "connection down", title: "connecting" });
|
|
const connLabel = h("span", { class: "connection-label" }, "reconnecting…");
|
|
const me = h("span", { class: "me" });
|
|
const render = () => {
|
|
conn.className = state.connected ? "connection up" : "connection down";
|
|
conn.title = state.connected ? "live" : "reconnecting";
|
|
connLabel.hidden = state.connected;
|
|
replace(me, state.me?.display_name ?? "");
|
|
};
|
|
onEach(["me", "connection"], render);
|
|
replace(root, h("a", { href: "#/", class: "brand" }, "byre"), h("span", { class: "spacer" }), status, settings, connLabel, conn, me);
|
|
render();
|
|
addEventListener("keydown", (e) => {
|
|
if (!(e.metaKey || e.ctrlKey) || document.querySelector("dialog[open]")) {
|
|
return;
|
|
}
|
|
if (e.key === "k") {
|
|
e.preventDefault();
|
|
openSwitcher();
|
|
}
|
|
else if (e.key === "p") {
|
|
e.preventDefault();
|
|
focusTab("project");
|
|
}
|
|
});
|
|
}
|
|
export function openSwitcher() {
|
|
if (document.querySelector(".switcher")) {
|
|
return;
|
|
}
|
|
const input = h("input", { placeholder: "Go to a task, or type a title to create one…", autocomplete: "off" });
|
|
const list = h("ul", { class: "switcher-list" });
|
|
const backdrop = h("div", { class: "switcher-backdrop" });
|
|
const box = h("div", { class: "switcher" }, input, list);
|
|
let selected = 0;
|
|
let rows = [];
|
|
const offs = onEach(["projects", "tasks", "sessions", "inbox"], () => render());
|
|
const onEscape = (e) => {
|
|
if (e.key !== "Escape") {
|
|
return;
|
|
}
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
close();
|
|
};
|
|
addEventListener("keydown", onEscape, { capture: true });
|
|
const close = () => {
|
|
offs();
|
|
removeEventListener("keydown", onEscape, { capture: true });
|
|
backdrop.remove();
|
|
box.remove();
|
|
};
|
|
const go = (taskId) => {
|
|
close();
|
|
focusTask(taskId);
|
|
};
|
|
const create = async (projectId, title) => {
|
|
close();
|
|
try {
|
|
const task = await post("/api/projects/{projectId}/tasks", { path: { projectId }, body: { title } });
|
|
go(task.id);
|
|
}
|
|
catch (err) {
|
|
void showAlert(errorText(err));
|
|
}
|
|
};
|
|
const render = () => {
|
|
const title = input.value.trim();
|
|
const q = title.toLowerCase();
|
|
const inbox = (taskId) => state.inbox.filter((item) => item.task_id === taskId).length;
|
|
const visits = lastVisits();
|
|
const matches = activeTasks()
|
|
.filter(({ project, task }) => `${project.name} ${task.title}`.toLowerCase().includes(q))
|
|
.map((m) => ({ ...m, count: inbox(m.task.id), visited: visits[m.task.id] ?? 0 }))
|
|
.sort((a, b) => Number(b.count > 0) - Number(a.count > 0) || a.visited - b.visited);
|
|
rows = matches.map(({ project, task, count }) => {
|
|
const sess = currentSession(task.id);
|
|
const el = h("li", {}, h("span", { class: `dot ${sess?.state ?? "none"}`, title: sess?.state ?? "no session" }), h("span", { class: "task-title" }, h("span", { class: "project-name" }, project.name), " ", task.title), count > 0 && h("span", { class: "badge", title: "inbox items" }, count), h("span", { class: "task-state" }, sess?.state ?? "no agent"));
|
|
return { el, pick: () => go(task.id) };
|
|
});
|
|
for (const p of state.projects.filter((p) => `${p.name} project`.toLowerCase().includes(q))) {
|
|
const el = h("li", {}, h("span", { class: "dot none" }), h("span", { class: "task-title" }, h("span", { class: "project-name" }, p.name)), h("span", { class: "task-state" }, "project"));
|
|
rows.push({
|
|
el,
|
|
pick: () => {
|
|
close();
|
|
focusProject(p.id);
|
|
},
|
|
});
|
|
}
|
|
const project = focusedProject() ?? state.projects[0];
|
|
if (title && project) {
|
|
const el = h("li", { class: "new-task" }, h("span", { class: "task-title" }, "New task in ", h("span", { class: "project-name" }, project.name), ": ", title));
|
|
rows.push({ el, pick: () => void create(project.id, title) });
|
|
}
|
|
selected = Math.min(selected, Math.max(rows.length - 1, 0));
|
|
rows.forEach((row, i) => {
|
|
row.el.classList.toggle("selected", i === selected);
|
|
row.el.onmousedown = (e) => {
|
|
e.preventDefault();
|
|
row.pick();
|
|
};
|
|
});
|
|
replace(list, ...rows.map((r) => r.el), rows.length === 0 && h("li", { class: "empty-row" }, "No active tasks. Type a title to create one."));
|
|
};
|
|
input.oninput = () => {
|
|
selected = 0;
|
|
render();
|
|
};
|
|
input.onkeydown = (e) => {
|
|
if (e.key === "ArrowDown") {
|
|
e.preventDefault();
|
|
selected = Math.min(selected + 1, rows.length - 1);
|
|
render();
|
|
}
|
|
else if (e.key === "ArrowUp") {
|
|
e.preventDefault();
|
|
selected = Math.max(selected - 1, 0);
|
|
render();
|
|
}
|
|
else if (e.key === "Enter") {
|
|
e.preventDefault();
|
|
rows[selected]?.pick();
|
|
}
|
|
};
|
|
backdrop.onclick = close;
|
|
const anchor = document.querySelector(".task-picker").getBoundingClientRect();
|
|
box.style.top = `${anchor.bottom + 4}px`;
|
|
box.style.right = `${document.documentElement.clientWidth - anchor.right}px`;
|
|
document.body.append(backdrop, box);
|
|
render();
|
|
input.focus();
|
|
}
|
|
//# sourceMappingURL=navbar.js.map
|