Add create task modal
This commit is contained in:
+56
-30
@@ -1,5 +1,11 @@
|
||||
const states = ["Backlog", "In Progress", "Review", "Done"];
|
||||
const priorities = ["Low", "Normal", "High", "Urgent"];
|
||||
const priorities = ["Low", "Normal", "Medium", "High", "Urgent"];
|
||||
const assignees = [
|
||||
{ value: "bao", label: "Bao" },
|
||||
{ value: "maxi", label: "Maxi" },
|
||||
{ value: "luna", label: "Luna" },
|
||||
{ value: "iris", label: "Iris" }
|
||||
];
|
||||
|
||||
const app = document.querySelector("#app");
|
||||
const state = {
|
||||
@@ -132,10 +138,9 @@ function boardView() {
|
||||
<span class="status-pill"><span class="dot ${connected ? "ok" : ""}"></span>OpenClaw ${connected ? "verbunden" : "pruefen"}</span>
|
||||
</div>
|
||||
</header>
|
||||
<form class="quick-create" id="quick-create">
|
||||
<input name="title" autocomplete="off" maxlength="180" placeholder="New task title">
|
||||
<button type="submit">Create task</button>
|
||||
</form>
|
||||
<div class="quick-create">
|
||||
<button class="primary" type="button" id="open-create-task">Create task</button>
|
||||
</div>
|
||||
<section class="kanban">
|
||||
${states.map((name) => column(name)).join("")}
|
||||
</section>
|
||||
@@ -186,23 +191,23 @@ function taskDialog() {
|
||||
<label class="field">Titel
|
||||
<input name="title" required>
|
||||
</label>
|
||||
<label class="field">Details
|
||||
<label class="field">Beschreibung
|
||||
<textarea name="detail"></textarea>
|
||||
</label>
|
||||
<div class="grid">
|
||||
<label class="field">Status
|
||||
<label class="field status-field">Status
|
||||
<select name="state">${states.map((x) => `<option>${x}</option>`).join("")}</select>
|
||||
</label>
|
||||
<label class="field">Prioritaet
|
||||
<label class="field">Priorität
|
||||
<select name="priority">${priorities.map((x) => `<option>${x}</option>`).join("")}</select>
|
||||
</label>
|
||||
</div>
|
||||
<label class="field">Assignee
|
||||
<input name="assignee" value="${escapeHtml(state.settings?.defaultAssignee || "luna")}">
|
||||
<label class="field">Zuweisen an
|
||||
<select name="assignee">${assigneeOptions(state.settings?.defaultAssignee || "luna")}</select>
|
||||
</label>
|
||||
<div class="dialog-actions">
|
||||
<button class="ghost" type="button" id="cancel-task">Abbrechen</button>
|
||||
<button class="primary" type="submit">Speichern</button>
|
||||
<button class="primary" type="submit" id="save-task">Speichern</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
@@ -214,41 +219,36 @@ function bindBoard() {
|
||||
const form = document.querySelector("#task-form");
|
||||
const dialogTitle = document.querySelector("#dialog-title");
|
||||
const dialogMeta = document.querySelector("#dialog-meta");
|
||||
const saveTaskButton = document.querySelector("#save-task");
|
||||
let draggedTaskId = null;
|
||||
const resetDialog = () => {
|
||||
form.reset();
|
||||
form.elements.id.value = "";
|
||||
form.elements.assignee.value = state.settings?.defaultAssignee || "luna";
|
||||
form.elements.state.value = "Backlog";
|
||||
form.elements.priority.value = "Medium";
|
||||
form.elements.assignee.value = normalizeAssignee(state.settings?.defaultAssignee || "luna");
|
||||
form.classList.add("task-form-create");
|
||||
dialogTitle.textContent = "Neue Aufgabe";
|
||||
dialogMeta.textContent = "";
|
||||
dialogMeta.textContent = "Erstelle eine neue Aufgabe fuer das Mission-Control-Board.";
|
||||
saveTaskButton.textContent = "Aufgabe erstellen";
|
||||
};
|
||||
const openTaskDialog = (task) => {
|
||||
form.classList.remove("task-form-create");
|
||||
form.elements.id.value = task.id;
|
||||
form.elements.title.value = task.title;
|
||||
form.elements.detail.value = task.detail || "";
|
||||
form.elements.state.value = task.state;
|
||||
form.elements.priority.value = task.priority;
|
||||
form.elements.assignee.value = task.assignee || "";
|
||||
ensureAssigneeOption(form.elements.assignee, task.assignee || state.settings?.defaultAssignee || "luna");
|
||||
form.elements.assignee.value = normalizeAssignee(task.assignee || state.settings?.defaultAssignee || "luna");
|
||||
dialogTitle.textContent = task.title;
|
||||
dialogMeta.textContent = `Status ${task.state} · Aktualisiert ${formatDate(task.updatedAt)} · Erstellt ${formatDate(task.createdAt)}`;
|
||||
saveTaskButton.textContent = "Speichern";
|
||||
dialog.showModal();
|
||||
};
|
||||
document.querySelector("#quick-create").addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
const formData = new FormData(event.currentTarget);
|
||||
const title = String(formData.get("title") || "").trim();
|
||||
if (!title) return;
|
||||
await api("/api/tasks", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
title,
|
||||
state: "Backlog",
|
||||
priority: "Normal",
|
||||
assignee: state.settings?.defaultAssignee || "luna"
|
||||
})
|
||||
});
|
||||
await loadTasks();
|
||||
renderApp();
|
||||
document.querySelector("#open-create-task").addEventListener("click", () => {
|
||||
resetDialog();
|
||||
dialog.showModal();
|
||||
});
|
||||
document.querySelector("#cancel-task").addEventListener("click", () => dialog.close());
|
||||
dialog.addEventListener("click", (event) => {
|
||||
@@ -388,4 +388,30 @@ function formatDate(value) {
|
||||
return new Date(value).toLocaleString("de-DE", { dateStyle: "short", timeStyle: "short" });
|
||||
}
|
||||
|
||||
function normalizeAssignee(value) {
|
||||
return String(value || "").trim().toLowerCase();
|
||||
}
|
||||
|
||||
function assigneeLabel(value) {
|
||||
const normalized = normalizeAssignee(value);
|
||||
const known = assignees.find((x) => x.value === normalized);
|
||||
if (known) return known.label;
|
||||
return normalized ? normalized.charAt(0).toUpperCase() + normalized.slice(1) : "Luna";
|
||||
}
|
||||
|
||||
function assigneeOptions(selected) {
|
||||
const normalized = normalizeAssignee(selected);
|
||||
const options = [...assignees];
|
||||
if (normalized && !options.some((x) => x.value === normalized)) {
|
||||
options.push({ value: normalized, label: assigneeLabel(normalized) });
|
||||
}
|
||||
return options.map((x) => `<option value="${escapeHtml(x.value)}" ${x.value === normalized ? "selected" : ""}>${escapeHtml(x.label)}</option>`).join("");
|
||||
}
|
||||
|
||||
function ensureAssigneeOption(select, value) {
|
||||
const normalized = normalizeAssignee(value);
|
||||
if (!normalized || Array.from(select.options).some((option) => option.value === normalized)) return;
|
||||
select.add(new Option(assigneeLabel(normalized), normalized));
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
+2
-2
@@ -4,10 +4,10 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Maxi Mission Control</title>
|
||||
<link rel="stylesheet" href="/styles.css?v=20260626-nexus-board">
|
||||
<link rel="stylesheet" href="/styles.css?v=20260626-create-modal">
|
||||
</head>
|
||||
<body>
|
||||
<main id="app"></main>
|
||||
<script type="module" src="/app.js?v=20260626-nexus-board"></script>
|
||||
<script type="module" src="/app.js?v=20260626-create-modal"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+14
-18
@@ -255,25 +255,13 @@ input:focus, textarea:focus, select:focus {
|
||||
}
|
||||
|
||||
.quick-create {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(180px, 360px) max-content;
|
||||
gap: 8px;
|
||||
align-items: stretch;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.quick-create input {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.quick-create button {
|
||||
min-width: 100px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid #443d7c;
|
||||
border-radius: 8px;
|
||||
background: #211d39;
|
||||
color: #c1b8ff;
|
||||
font-size: 9px;
|
||||
min-width: 112px;
|
||||
}
|
||||
|
||||
.kanban {
|
||||
@@ -467,9 +455,9 @@ input:focus, textarea:focus, select:focus {
|
||||
|
||||
dialog {
|
||||
width: min(560px, calc(100vw - 32px));
|
||||
padding: 22px;
|
||||
padding: 24px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(145deg, rgba(18, 21, 29, 0.98), rgba(10, 12, 18, 0.98));
|
||||
color: var(--text);
|
||||
box-shadow: var(--shadow);
|
||||
@@ -482,7 +470,7 @@ dialog::backdrop {
|
||||
|
||||
dialog h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
@@ -499,6 +487,14 @@ dialog h2 {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.task-form-create .status-field {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.task-form-create .grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.app-shell {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
@@ -20,7 +20,7 @@ const openClawBaseUrl = (process.env.OPENCLAW_BASE_URL || "http://openclaw-gatew
|
||||
const openClawToken = process.env.OPENCLAW_GATEWAY_TOKEN || "";
|
||||
|
||||
const states = ["Backlog", "In Progress", "Review", "Done"];
|
||||
const priorities = ["Low", "Normal", "High", "Urgent"];
|
||||
const priorities = ["Low", "Normal", "Medium", "High", "Urgent"];
|
||||
let db;
|
||||
|
||||
function mustEnv(name) {
|
||||
|
||||
Reference in New Issue
Block a user