Files
box-manifest-android/web/app.js
T

194 lines
5.5 KiB
JavaScript

const state = {
root: null,
nodes: new Map(),
selectedId: null,
};
const elements = {
status: document.querySelector("#status"),
tree: document.querySelector("#tree"),
selected: document.querySelector("#selected"),
refresh: document.querySelector("#refresh"),
createForm: document.querySelector("#create-form"),
createParent: document.querySelector("#create-parent"),
createName: document.querySelector("#create-name"),
createQuantity: document.querySelector("#create-quantity"),
updateForm: document.querySelector("#update-form"),
updateName: document.querySelector("#update-name"),
updateQuantity: document.querySelector("#update-quantity"),
moveForm: document.querySelector("#move-form"),
moveParent: document.querySelector("#move-parent"),
moveChildren: document.querySelector("#move-children"),
deleteButton: document.querySelector("#delete"),
};
async function api(path, options = {}) {
const response = await fetch(path, {
...options,
headers: options.body ? { "Content-Type": "application/json" } : undefined,
});
if (response.status === 204) {
return null;
}
const body = await response.json();
if (!response.ok) {
throw new Error(body.message || `Request failed with status ${response.status}`);
}
return body;
}
async function loadTree() {
try {
state.root = await api("/api/tree");
state.nodes.clear();
indexNode(state.root);
const requestedCode = new URLSearchParams(window.location.search).get("code")?.toUpperCase();
if (requestedCode) {
state.selectedId = [...state.nodes.values()].find((node) => node.lookupCode === requestedCode)?.id || null;
}
if (state.selectedId && !state.nodes.has(state.selectedId)) {
state.selectedId = null;
}
render();
setStatus("");
} catch (error) {
setStatus(error.message);
}
}
function indexNode(node) {
state.nodes.set(node.id, node);
for (const child of node.children) {
indexNode(child);
}
}
function render() {
elements.tree.replaceChildren(renderNode(state.root));
renderSelection();
}
function renderNode(node) {
const list = document.createElement("ul");
const item = document.createElement("li");
const button = document.createElement("button");
button.type = "button";
button.textContent = node.quantity === null ? node.name : `${node.name} (${node.quantity})`;
button.addEventListener("click", () => {
state.selectedId = node.id;
renderSelection();
});
item.append(button);
if (node.children.length > 0) {
for (const child of node.children) {
item.append(renderNode(child));
}
}
list.append(item);
return list;
}
function renderSelection() {
const node = state.nodes.get(state.selectedId);
const hasSelection = Boolean(node);
const protectedNode = node?.id === "root" || node?.id === "unsorted";
elements.selected.textContent = node ? `${node.name} [${node.lookupCode}]` : "None";
elements.createParent.textContent = `Parent: ${node?.name || "Unsorted"}`;
elements.updateName.value = node?.name || "";
elements.updateQuantity.value = node?.quantity ?? "";
for (const control of elements.updateForm.elements) {
control.disabled = !hasSelection || protectedNode;
}
for (const control of elements.moveForm.elements) {
control.disabled = !hasSelection || protectedNode;
}
elements.deleteButton.disabled = !hasSelection || protectedNode;
elements.moveParent.replaceChildren();
for (const candidate of state.nodes.values()) {
if (candidate.id === state.selectedId) {
continue;
}
const option = document.createElement("option");
option.value = candidate.id;
option.textContent = candidate.name;
elements.moveParent.append(option);
}
}
function quantityFrom(input) {
return input.value === "" ? null : Number(input.value);
}
elements.refresh.addEventListener("click", loadTree);
elements.createForm.addEventListener("submit", async (event) => {
event.preventDefault();
const request = {
name: elements.createName.value,
quantity: quantityFrom(elements.createQuantity),
};
if (state.selectedId) {
request.parentId = state.selectedId;
}
await perform(async () => {
const node = await api("/api/nodes", {
method: "POST",
body: JSON.stringify(request),
});
state.selectedId = node.id;
elements.createForm.reset();
});
});
elements.updateForm.addEventListener("submit", async (event) => {
event.preventDefault();
await perform(() => api(`/api/nodes/${state.selectedId}`, {
method: "PATCH",
body: JSON.stringify({
name: elements.updateName.value,
quantity: quantityFrom(elements.updateQuantity),
}),
}));
});
elements.moveForm.addEventListener("submit", async (event) => {
event.preventDefault();
await perform(() => api(`/api/nodes/${state.selectedId}/move`, {
method: "POST",
body: JSON.stringify({
targetParentId: elements.moveParent.value,
childHandling: elements.moveChildren.value,
}),
}));
});
elements.deleteButton.addEventListener("click", async () => {
const node = state.nodes.get(state.selectedId);
if (!node || !window.confirm(`Delete ${node.name}?`)) {
return;
}
await perform(async () => {
await api(`/api/nodes/${node.id}`, { method: "DELETE" });
state.selectedId = null;
});
});
async function perform(action) {
try {
setStatus("");
await action();
await loadTree();
} catch (error) {
setStatus(error.message);
}
}
function setStatus(message) {
elements.status.textContent = message;
}
loadTree();