Send pick-any checks with Other text; keep inbox focus across updates
This commit is contained in:
Vendored
+47
-23
@@ -10,6 +10,8 @@ export function mountInbox(root) {
|
||||
const tabs = h("nav", { class: "scope-tabs" });
|
||||
const list = h("div", { class: "inbox-list" });
|
||||
const cards = new Map();
|
||||
const groups = new Map();
|
||||
const empty = h("div", { class: "empty" }, "Nothing waiting on you.");
|
||||
const visible = () => {
|
||||
const task = state.selectedTaskId ? findTask(state.selectedTaskId) : undefined;
|
||||
if (scope === "task" && task) {
|
||||
@@ -61,12 +63,28 @@ export function mountInbox(root) {
|
||||
cards.set(item.id, { sig, el });
|
||||
return el;
|
||||
};
|
||||
const els = groupByTask(items).map((g) => {
|
||||
const task = findTask(g.taskId);
|
||||
const project = state.projects.find((p) => p.id === g.projectId);
|
||||
return h("section", { class: `task-group ${g.blocked ? "blocked" : ""}` }, h("h3", {}, h("span", { class: "project-name" }, project?.name ?? g.projectId, " › "), h("a", { href: `#/task/${g.taskId}` }, task?.title ?? g.taskId)), ...g.items.map(cardFor));
|
||||
const shown = groupByTask(items);
|
||||
const keepGroups = new Set(shown.map((g) => g.taskId));
|
||||
for (const id of groups.keys()) {
|
||||
if (!keepGroups.has(id)) {
|
||||
groups.delete(id);
|
||||
}
|
||||
}
|
||||
const els = shown.map((g) => {
|
||||
const projectName = state.projects.find((p) => p.id === g.projectId)?.name ?? g.projectId;
|
||||
const title = findTask(g.taskId)?.title ?? g.taskId;
|
||||
const head = JSON.stringify([projectName, title]);
|
||||
let group = groups.get(g.taskId);
|
||||
if (!group || group.head !== head) {
|
||||
const h3 = h("h3", {}, h("span", { class: "project-name" }, projectName, " › "), h("a", { href: `#/task/${g.taskId}` }, title));
|
||||
group = { head, h3, el: group?.el ?? h("section", {}) };
|
||||
groups.set(g.taskId, group);
|
||||
}
|
||||
group.el.className = `task-group ${g.blocked ? "blocked" : ""}`;
|
||||
reconcile(group.el, [group.h3, ...g.items.map(cardFor)]);
|
||||
return group.el;
|
||||
});
|
||||
replace(list, ...(els.length > 0 ? els : [h("div", { class: "empty" }, "Nothing waiting on you.")]));
|
||||
reconcile(list, els.length > 0 ? els : [empty]);
|
||||
};
|
||||
const off = onEach(["inbox", "selection", "tasks"], render);
|
||||
const changes = h("section", { class: "inbox-changes" });
|
||||
@@ -78,6 +96,17 @@ export function mountInbox(root) {
|
||||
offChanges();
|
||||
};
|
||||
}
|
||||
function reconcile(parent, nodes) {
|
||||
nodes.forEach((node, i) => {
|
||||
const at = parent.childNodes[i];
|
||||
if (at !== node) {
|
||||
parent.insertBefore(node, at ?? null);
|
||||
}
|
||||
});
|
||||
while (parent.childNodes.length > nodes.length) {
|
||||
parent.lastChild.remove();
|
||||
}
|
||||
}
|
||||
function groupByTask(items) {
|
||||
const groups = new Map();
|
||||
for (const item of [...items].sort((a, b) => a.created_at.localeCompare(b.created_at))) {
|
||||
@@ -156,15 +185,12 @@ function card(item) {
|
||||
const now = (checked.get(item.id) ?? []).filter((o) => o !== text);
|
||||
if (input.checked) {
|
||||
now.push(text);
|
||||
texts.delete(item.id);
|
||||
}
|
||||
checked.set(item.id, options.filter((o) => now.includes(o)));
|
||||
renderActions();
|
||||
};
|
||||
boxes.push(input);
|
||||
return h("label", { class: "option" }, input, " ", inlineMarkdown(text), tag);
|
||||
};
|
||||
const boxes = [];
|
||||
const textEntry = (label, placeholder, shared) => {
|
||||
const open = texts.has(item.id);
|
||||
const text = h("textarea", { rows: 3, placeholder, value: texts.get(item.id) ?? "" });
|
||||
@@ -175,9 +201,12 @@ function card(item) {
|
||||
block.hidden = true;
|
||||
editor.hidden = false;
|
||||
text.focus();
|
||||
shared?.onOpen();
|
||||
});
|
||||
const go = () => {
|
||||
if (shared) {
|
||||
shared.onSubmit();
|
||||
return;
|
||||
}
|
||||
if (text.value.trim()) {
|
||||
void submit({ item_id: item.id, type: "other", text: text.value });
|
||||
}
|
||||
@@ -223,23 +252,18 @@ function card(item) {
|
||||
const refresh = () => {
|
||||
send.disabled = !typed() && (checked.get(item.id) ?? []).length === 0;
|
||||
};
|
||||
const other = textEntry("Other…", placeholder, {
|
||||
onOpen: () => {
|
||||
checked.delete(item.id);
|
||||
for (const box of boxes) {
|
||||
box.checked = false;
|
||||
}
|
||||
refresh();
|
||||
},
|
||||
onInput: refresh,
|
||||
});
|
||||
send.onclick = () => {
|
||||
if (typed()) {
|
||||
void submit({ item_id: item.id, type: "other", text: texts.get(item.id) });
|
||||
const go = () => {
|
||||
const options = checked.get(item.id) ?? [];
|
||||
if (options.length > 0) {
|
||||
void submit({ item_id: item.id, type: "option", options, text: typed() ? texts.get(item.id) : undefined });
|
||||
return;
|
||||
}
|
||||
void submit({ item_id: item.id, type: "option", options: checked.get(item.id) ?? [] });
|
||||
if (typed()) {
|
||||
void submit({ item_id: item.id, type: "other", text: texts.get(item.id) });
|
||||
}
|
||||
};
|
||||
const other = textEntry("Other…", placeholder, { onInput: refresh, onSubmit: go });
|
||||
send.onclick = go;
|
||||
refresh();
|
||||
replace(actions, other, h("div", { class: "submit-row" }, send));
|
||||
return;
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -739,7 +739,7 @@ export interface components {
|
||||
type: components["schemas"]["InboxResponseType"];
|
||||
/** @description option: the chosen options; exactly one for a pick-one question, one or more for a pick-any question. */
|
||||
options?: string[];
|
||||
/** @description other: the text sent to the agent. */
|
||||
/** @description other: the text sent to the agent. option on a pick-any question: optional text sent along with the checked options. */
|
||||
text?: string;
|
||||
attachments?: components["schemas"]["Attachment"][];
|
||||
};
|
||||
|
||||
+50
-29
@@ -14,6 +14,8 @@ export function mountInbox(root: HTMLElement): () => void {
|
||||
const tabs = h("nav", { class: "scope-tabs" });
|
||||
const list = h("div", { class: "inbox-list" });
|
||||
const cards = new Map<string, { sig: string; el: HTMLElement }>();
|
||||
const groups = new Map<string, { head: string; h3: HTMLElement; el: HTMLElement }>();
|
||||
const empty = h("div", { class: "empty" }, "Nothing waiting on you.");
|
||||
|
||||
const visible = (): InboxItem[] => {
|
||||
const task = state.selectedTaskId ? findTask(state.selectedTaskId) : undefined;
|
||||
@@ -70,18 +72,30 @@ export function mountInbox(root: HTMLElement): () => void {
|
||||
return el;
|
||||
};
|
||||
|
||||
const els = groupByTask(items).map((g) => {
|
||||
const task = findTask(g.taskId);
|
||||
const project = state.projects.find((p) => p.id === g.projectId);
|
||||
return h(
|
||||
"section",
|
||||
{ class: `task-group ${g.blocked ? "blocked" : ""}` },
|
||||
h("h3", {}, h("span", { class: "project-name" }, project?.name ?? g.projectId, " › "), h("a", { href: `#/task/${g.taskId}` }, task?.title ?? g.taskId)),
|
||||
...g.items.map(cardFor),
|
||||
);
|
||||
const shown = groupByTask(items);
|
||||
const keepGroups = new Set(shown.map((g) => g.taskId));
|
||||
for (const id of groups.keys()) {
|
||||
if (!keepGroups.has(id)) {
|
||||
groups.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
const els = shown.map((g) => {
|
||||
const projectName = state.projects.find((p) => p.id === g.projectId)?.name ?? g.projectId;
|
||||
const title = findTask(g.taskId)?.title ?? g.taskId;
|
||||
const head = JSON.stringify([projectName, title]);
|
||||
let group = groups.get(g.taskId);
|
||||
if (!group || group.head !== head) {
|
||||
const h3 = h("h3", {}, h("span", { class: "project-name" }, projectName, " › "), h("a", { href: `#/task/${g.taskId}` }, title));
|
||||
group = { head, h3, el: group?.el ?? h("section", {}) };
|
||||
groups.set(g.taskId, group);
|
||||
}
|
||||
group.el.className = `task-group ${g.blocked ? "blocked" : ""}`;
|
||||
reconcile(group.el, [group.h3, ...g.items.map(cardFor)]);
|
||||
return group.el;
|
||||
});
|
||||
|
||||
replace(list, ...(els.length > 0 ? els : [h("div", { class: "empty" }, "Nothing waiting on you.")]));
|
||||
reconcile(list, els.length > 0 ? els : [empty]);
|
||||
};
|
||||
|
||||
const off = onEach(["inbox", "selection", "tasks"], render);
|
||||
@@ -95,6 +109,18 @@ export function mountInbox(root: HTMLElement): () => void {
|
||||
};
|
||||
}
|
||||
|
||||
function reconcile(parent: Element, nodes: Node[]): void {
|
||||
nodes.forEach((node, i) => {
|
||||
const at = parent.childNodes[i];
|
||||
if (at !== node) {
|
||||
parent.insertBefore(node, at ?? null);
|
||||
}
|
||||
});
|
||||
while (parent.childNodes.length > nodes.length) {
|
||||
parent.lastChild!.remove();
|
||||
}
|
||||
}
|
||||
|
||||
type Group = { taskId: string; projectId: string; blocked: boolean; items: InboxItem[] };
|
||||
|
||||
function groupByTask(items: InboxItem[]): Group[] {
|
||||
@@ -194,7 +220,6 @@ function card(item: InboxItem): HTMLElement {
|
||||
const now = (checked.get(item.id) ?? []).filter((o) => o !== text);
|
||||
if (input.checked) {
|
||||
now.push(text);
|
||||
texts.delete(item.id);
|
||||
}
|
||||
checked.set(
|
||||
item.id,
|
||||
@@ -202,12 +227,10 @@ function card(item: InboxItem): HTMLElement {
|
||||
);
|
||||
renderActions();
|
||||
};
|
||||
boxes.push(input);
|
||||
return h("label", { class: "option" }, input, " ", inlineMarkdown(text), tag);
|
||||
};
|
||||
const boxes: HTMLInputElement[] = [];
|
||||
|
||||
const textEntry = (label: string, placeholder: string, shared?: { onOpen: () => void; onInput: () => void }) => {
|
||||
const textEntry = (label: string, placeholder: string, shared?: { onInput: () => void; onSubmit: () => void }) => {
|
||||
const open = texts.has(item.id);
|
||||
const text = h("textarea", { rows: 3, placeholder, value: texts.get(item.id) ?? "" });
|
||||
const send = h("button", { type: "button", class: "primary", disabled: !text.value.trim() }, "Submit");
|
||||
@@ -217,9 +240,12 @@ function card(item: InboxItem): HTMLElement {
|
||||
block.hidden = true;
|
||||
editor.hidden = false;
|
||||
text.focus();
|
||||
shared?.onOpen();
|
||||
});
|
||||
const go = () => {
|
||||
if (shared) {
|
||||
shared.onSubmit();
|
||||
return;
|
||||
}
|
||||
if (text.value.trim()) {
|
||||
void submit({ item_id: item.id, type: "other", text: text.value });
|
||||
}
|
||||
@@ -266,23 +292,18 @@ function card(item: InboxItem): HTMLElement {
|
||||
const refresh = () => {
|
||||
send.disabled = !typed() && (checked.get(item.id) ?? []).length === 0;
|
||||
};
|
||||
const other = textEntry("Other…", placeholder, {
|
||||
onOpen: () => {
|
||||
checked.delete(item.id);
|
||||
for (const box of boxes) {
|
||||
box.checked = false;
|
||||
}
|
||||
refresh();
|
||||
},
|
||||
onInput: refresh,
|
||||
});
|
||||
send.onclick = () => {
|
||||
if (typed()) {
|
||||
void submit({ item_id: item.id, type: "other", text: texts.get(item.id)! });
|
||||
const go = () => {
|
||||
const options = checked.get(item.id) ?? [];
|
||||
if (options.length > 0) {
|
||||
void submit({ item_id: item.id, type: "option", options, text: typed() ? texts.get(item.id) : undefined });
|
||||
return;
|
||||
}
|
||||
void submit({ item_id: item.id, type: "option", options: checked.get(item.id) ?? [] });
|
||||
if (typed()) {
|
||||
void submit({ item_id: item.id, type: "other", text: texts.get(item.id)! });
|
||||
}
|
||||
};
|
||||
const other = textEntry("Other…", placeholder, { onInput: refresh, onSubmit: go });
|
||||
send.onclick = go;
|
||||
refresh();
|
||||
replace(actions, other, h("div", { class: "submit-row" }, send));
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user