@tenonhq/dovetail-dashboard 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -0
- package/package.json +26 -0
- package/public/app.js +1001 -0
- package/public/index.html +79 -0
- package/public/styles.css +819 -0
- package/server.js +785 -0
package/public/app.js
ADDED
|
@@ -0,0 +1,1001 @@
|
|
|
1
|
+
// --- State ---
|
|
2
|
+
|
|
3
|
+
var scopesData = [];
|
|
4
|
+
var updateSetsCache = {};
|
|
5
|
+
var modalScopeKey = null;
|
|
6
|
+
var modalScopeSysId = null;
|
|
7
|
+
|
|
8
|
+
// ClickUp state
|
|
9
|
+
var activeTask = null;
|
|
10
|
+
var clickupTasks = {};
|
|
11
|
+
var clickupConfigured = false;
|
|
12
|
+
var availableStatuses = [];
|
|
13
|
+
var activeStatuses = ["in progress"];
|
|
14
|
+
var sidebarOpen = false;
|
|
15
|
+
var tasksLoading = false;
|
|
16
|
+
var taskSearchQuery = "";
|
|
17
|
+
var myTasksOnly = false;
|
|
18
|
+
var clickupUserId = null;
|
|
19
|
+
|
|
20
|
+
// --- API helpers ---
|
|
21
|
+
|
|
22
|
+
async function api(method, path, body) {
|
|
23
|
+
var opts = {
|
|
24
|
+
method: method,
|
|
25
|
+
headers: { "Content-Type": "application/json" },
|
|
26
|
+
};
|
|
27
|
+
if (body) opts.body = JSON.stringify(body);
|
|
28
|
+
var resp = await fetch(path, opts);
|
|
29
|
+
if (!resp.ok) {
|
|
30
|
+
var err = await resp.json().catch(function () {
|
|
31
|
+
return { error: "Request failed" };
|
|
32
|
+
});
|
|
33
|
+
throw new Error(err.error || "Request failed");
|
|
34
|
+
}
|
|
35
|
+
return resp.json();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// --- Toast ---
|
|
39
|
+
|
|
40
|
+
function toast(message, type) {
|
|
41
|
+
type = type || "success";
|
|
42
|
+
var container = document.getElementById("toast-container");
|
|
43
|
+
var el = document.createElement("div");
|
|
44
|
+
el.className = "toast " + type;
|
|
45
|
+
el.textContent = message;
|
|
46
|
+
container.appendChild(el);
|
|
47
|
+
setTimeout(function () {
|
|
48
|
+
el.remove();
|
|
49
|
+
}, 4000);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// --- Load scopes ---
|
|
53
|
+
|
|
54
|
+
async function loadScopes() {
|
|
55
|
+
try {
|
|
56
|
+
var data = await api("GET", "/api/scopes");
|
|
57
|
+
scopesData = data.scopes;
|
|
58
|
+
renderScopes();
|
|
59
|
+
} catch (e) {
|
|
60
|
+
document.getElementById("scope-grid").innerHTML =
|
|
61
|
+
'<div class="loading">Failed to load scopes: ' + e.message + "</div>";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function loadConfig() {
|
|
66
|
+
try {
|
|
67
|
+
var data = await api("GET", "/api/config");
|
|
68
|
+
document.getElementById("instance-badge").textContent = data.instance;
|
|
69
|
+
} catch (e) {
|
|
70
|
+
// ignore
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// --- Render Scopes ---
|
|
75
|
+
|
|
76
|
+
function renderScopes() {
|
|
77
|
+
var grid = document.getElementById("scope-grid");
|
|
78
|
+
grid.innerHTML = "";
|
|
79
|
+
|
|
80
|
+
scopesData.forEach(function (scope) {
|
|
81
|
+
var card = document.createElement("div");
|
|
82
|
+
card.className = "scope-card" + (scope.selected_update_set ? " has-selection" : "");
|
|
83
|
+
card.id = "card-" + scope.scope;
|
|
84
|
+
|
|
85
|
+
var selectedName = scope.selected_update_set ? scope.selected_update_set.name : "None";
|
|
86
|
+
var selectedId = scope.selected_update_set ? scope.selected_update_set.sys_id : "";
|
|
87
|
+
|
|
88
|
+
// Build activate button HTML if there's an active task
|
|
89
|
+
var activateHtml = "";
|
|
90
|
+
if (activeTask && scope.sys_id) {
|
|
91
|
+
var scopeActivated = activeTask.scopes && activeTask.scopes[scope.scope];
|
|
92
|
+
if (scopeActivated) {
|
|
93
|
+
activateHtml =
|
|
94
|
+
'<button class="btn-activate activated" disabled>Activated</button>';
|
|
95
|
+
} else {
|
|
96
|
+
activateHtml =
|
|
97
|
+
'<button class="btn-activate" onclick="activateScope(\'' +
|
|
98
|
+
scope.scope + "', '" + scope.sys_id +
|
|
99
|
+
"')\">Activate CU-" + activeTask.taskId + "</button>";
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Build task badge if scope is activated for current task
|
|
104
|
+
var taskBadgeHtml = "";
|
|
105
|
+
if (activeTask && activeTask.scopes && activeTask.scopes[scope.scope]) {
|
|
106
|
+
taskBadgeHtml =
|
|
107
|
+
'<div class="scope-task-badge">CU-' + activeTask.taskId + "</div>";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
card.innerHTML =
|
|
111
|
+
'<div class="scope-header">' +
|
|
112
|
+
'<span class="scope-name">' + scope.scope + "</span>" +
|
|
113
|
+
'<span class="scope-id">' + (scope.sys_id ? scope.sys_id.substring(0, 8) + "..." : "not found") + "</span>" +
|
|
114
|
+
"</div>" +
|
|
115
|
+
'<div class="display-name">' + scope.display_name + "</div>" +
|
|
116
|
+
'<select class="update-set-select" id="select-' + scope.scope + '" onchange="onSelectChange(\'' + scope.scope + '\')">' +
|
|
117
|
+
'<option value="">-- No Update Set --</option>' +
|
|
118
|
+
'<option value="__loading" disabled>Loading...</option>' +
|
|
119
|
+
"</select>" +
|
|
120
|
+
'<div class="card-actions">' +
|
|
121
|
+
'<button class="btn btn-primary" onclick="openCreateModal(\'' + scope.scope + "', '" + scope.sys_id + "')\" " + (!scope.sys_id ? "disabled" : "") + ">New</button>" +
|
|
122
|
+
'<button class="btn btn-danger" id="close-btn-' + scope.scope + '" onclick="closeUpdateSet(\'' + scope.scope + '\')" disabled>Close</button>' +
|
|
123
|
+
'<button class="btn btn-clear" id="clear-btn-' + scope.scope + '" onclick="clearSelection(\'' + scope.scope + '\')" ' + (!scope.selected_update_set ? "disabled" : "") + ">Clear</button>" +
|
|
124
|
+
activateHtml +
|
|
125
|
+
"</div>" +
|
|
126
|
+
'<div class="quick-create">' +
|
|
127
|
+
'<input type="text" id="quick-name-' + scope.scope + '" placeholder="Update set name..." class="quick-create-input" onkeydown="if(event.key===\'Enter\')quickCreateUpdateSet(\'' + scope.scope + "', '" + scope.sys_id + "')\" />" +
|
|
128
|
+
'<button class="btn btn-primary btn-small" onclick="quickCreateUpdateSet(\'' + scope.scope + "', '" + scope.sys_id + "')\" " + (!scope.sys_id ? "disabled" : "") + ">Create</button>" +
|
|
129
|
+
"</div>" +
|
|
130
|
+
(scope.selected_update_set ? '<div class="selected-badge">Active for push</div>' : "") +
|
|
131
|
+
taskBadgeHtml;
|
|
132
|
+
|
|
133
|
+
grid.appendChild(card);
|
|
134
|
+
loadUpdateSets(scope.scope, selectedId);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function loadUpdateSets(scope, selectedId) {
|
|
139
|
+
try {
|
|
140
|
+
var data = await api("GET", "/api/update-sets/" + scope);
|
|
141
|
+
updateSetsCache[scope] = data.update_sets;
|
|
142
|
+
|
|
143
|
+
var select = document.getElementById("select-" + scope);
|
|
144
|
+
select.innerHTML = '<option value="">-- No Update Set --</option>';
|
|
145
|
+
|
|
146
|
+
data.update_sets.forEach(function (us) {
|
|
147
|
+
var option = document.createElement("option");
|
|
148
|
+
option.value = us.sys_id;
|
|
149
|
+
option.textContent = us.name;
|
|
150
|
+
if (us.sys_id === selectedId) option.selected = true;
|
|
151
|
+
select.appendChild(option);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
var closeBtn = document.getElementById("close-btn-" + scope);
|
|
155
|
+
if (closeBtn) closeBtn.disabled = !selectedId;
|
|
156
|
+
} catch (e) {
|
|
157
|
+
var selectEl = document.getElementById("select-" + scope);
|
|
158
|
+
if (selectEl) {
|
|
159
|
+
selectEl.innerHTML = '<option value="" disabled>Failed to load</option>';
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// --- Update Set Actions ---
|
|
165
|
+
|
|
166
|
+
async function onSelectChange(scope) {
|
|
167
|
+
var select = document.getElementById("select-" + scope);
|
|
168
|
+
var sysId = select.value;
|
|
169
|
+
var name = select.options[select.selectedIndex] ? select.options[select.selectedIndex].textContent : "";
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
await api("POST", "/api/select-update-set", {
|
|
173
|
+
scope: scope,
|
|
174
|
+
update_set_sys_id: sysId,
|
|
175
|
+
update_set_name: sysId ? name : null,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
var scopeData = scopesData.find(function (s) { return s.scope === scope; });
|
|
179
|
+
if (scopeData) {
|
|
180
|
+
scopeData.selected_update_set = sysId ? { sys_id: sysId, name: name } : null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
var card = document.getElementById("card-" + scope);
|
|
184
|
+
if (card) {
|
|
185
|
+
if (sysId) {
|
|
186
|
+
card.classList.add("has-selection");
|
|
187
|
+
} else {
|
|
188
|
+
card.classList.remove("has-selection");
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
var closeBtn = document.getElementById("close-btn-" + scope);
|
|
193
|
+
if (closeBtn) closeBtn.disabled = !sysId;
|
|
194
|
+
var clearBtn = document.getElementById("clear-btn-" + scope);
|
|
195
|
+
if (clearBtn) clearBtn.disabled = !sysId;
|
|
196
|
+
|
|
197
|
+
var badge = card ? card.querySelector(".selected-badge") : null;
|
|
198
|
+
if (sysId && !badge) {
|
|
199
|
+
var b = document.createElement("div");
|
|
200
|
+
b.className = "selected-badge";
|
|
201
|
+
b.textContent = "Active for push";
|
|
202
|
+
card.appendChild(b);
|
|
203
|
+
} else if (!sysId && badge) {
|
|
204
|
+
badge.remove();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
toast(sysId ? "Selected: " + name : "Cleared selection for " + scope);
|
|
208
|
+
} catch (e) {
|
|
209
|
+
toast("Failed to save: " + e.message, "error");
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function clearSelection(scope) {
|
|
214
|
+
var select = document.getElementById("select-" + scope);
|
|
215
|
+
if (select) select.value = "";
|
|
216
|
+
await onSelectChange(scope);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function closeUpdateSet(scope) {
|
|
220
|
+
var select = document.getElementById("select-" + scope);
|
|
221
|
+
var sysId = select ? select.value : "";
|
|
222
|
+
if (!sysId) return;
|
|
223
|
+
|
|
224
|
+
var name = select.options[select.selectedIndex] ? select.options[select.selectedIndex].textContent : "";
|
|
225
|
+
if (!confirm("Close update set \"" + name + "\"?")) return;
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
await api("PATCH", "/api/update-set/" + sysId + "/close");
|
|
229
|
+
|
|
230
|
+
await api("POST", "/api/select-update-set", {
|
|
231
|
+
scope: scope,
|
|
232
|
+
update_set_sys_id: "",
|
|
233
|
+
update_set_name: null,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
var scopeData = scopesData.find(function (s) { return s.scope === scope; });
|
|
237
|
+
if (scopeData) scopeData.selected_update_set = null;
|
|
238
|
+
|
|
239
|
+
toast("Closed: " + name);
|
|
240
|
+
loadUpdateSets(scope, "");
|
|
241
|
+
|
|
242
|
+
var card = document.getElementById("card-" + scope);
|
|
243
|
+
if (card) {
|
|
244
|
+
card.classList.remove("has-selection");
|
|
245
|
+
var badge = card.querySelector(".selected-badge");
|
|
246
|
+
if (badge) badge.remove();
|
|
247
|
+
}
|
|
248
|
+
var clearBtn = document.getElementById("clear-btn-" + scope);
|
|
249
|
+
if (clearBtn) clearBtn.disabled = true;
|
|
250
|
+
var closeBtn = document.getElementById("close-btn-" + scope);
|
|
251
|
+
if (closeBtn) closeBtn.disabled = true;
|
|
252
|
+
} catch (e) {
|
|
253
|
+
toast("Failed to close: " + e.message, "error");
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// --- Modal ---
|
|
258
|
+
|
|
259
|
+
function openCreateModal(scope, scopeSysId) {
|
|
260
|
+
modalScopeKey = scope;
|
|
261
|
+
modalScopeSysId = scopeSysId;
|
|
262
|
+
document.getElementById("modal-scope").value = scope;
|
|
263
|
+
document.getElementById("modal-name").value = activeTask ? activeTask.updateSetName : "";
|
|
264
|
+
document.getElementById("modal-description").value = activeTask ? activeTask.description : "";
|
|
265
|
+
document.getElementById("create-modal").classList.add("active");
|
|
266
|
+
document.getElementById("modal-name").focus();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function closeModal() {
|
|
270
|
+
document.getElementById("create-modal").classList.remove("active");
|
|
271
|
+
modalScopeKey = null;
|
|
272
|
+
modalScopeSysId = null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function createUpdateSet() {
|
|
276
|
+
var name = document.getElementById("modal-name").value.trim();
|
|
277
|
+
if (!name) {
|
|
278
|
+
toast("Name is required", "error");
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
var btn = document.getElementById("modal-create-btn");
|
|
283
|
+
btn.disabled = true;
|
|
284
|
+
btn.textContent = "Creating...";
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
var data = await api("POST", "/api/update-set", {
|
|
288
|
+
name: name,
|
|
289
|
+
scope: modalScopeKey,
|
|
290
|
+
scope_sys_id: modalScopeSysId,
|
|
291
|
+
description: document.getElementById("modal-description").value.trim(),
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
var newSysId = data.update_set.sys_id;
|
|
295
|
+
|
|
296
|
+
await api("POST", "/api/select-update-set", {
|
|
297
|
+
scope: modalScopeKey,
|
|
298
|
+
update_set_sys_id: newSysId,
|
|
299
|
+
update_set_name: name,
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
var scopeData = scopesData.find(function (s) { return s.scope === modalScopeKey; });
|
|
303
|
+
if (scopeData) {
|
|
304
|
+
scopeData.selected_update_set = { sys_id: newSysId, name: name };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
toast("Created: " + name);
|
|
308
|
+
closeModal();
|
|
309
|
+
|
|
310
|
+
loadUpdateSets(modalScopeKey, newSysId);
|
|
311
|
+
|
|
312
|
+
var card = document.getElementById("card-" + modalScopeKey);
|
|
313
|
+
if (card) {
|
|
314
|
+
card.classList.add("has-selection");
|
|
315
|
+
var badge = card.querySelector(".selected-badge");
|
|
316
|
+
if (!badge) {
|
|
317
|
+
var b = document.createElement("div");
|
|
318
|
+
b.className = "selected-badge";
|
|
319
|
+
b.textContent = "Active for push";
|
|
320
|
+
card.appendChild(b);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
var closeBtn = document.getElementById("close-btn-" + modalScopeKey);
|
|
324
|
+
if (closeBtn) closeBtn.disabled = false;
|
|
325
|
+
var clearBtn = document.getElementById("clear-btn-" + modalScopeKey);
|
|
326
|
+
if (clearBtn) clearBtn.disabled = false;
|
|
327
|
+
} catch (e) {
|
|
328
|
+
toast("Failed to create: " + e.message, "error");
|
|
329
|
+
} finally {
|
|
330
|
+
btn.disabled = false;
|
|
331
|
+
btn.textContent = "Create";
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// --- Quick Create (inline per-scope) ---
|
|
336
|
+
|
|
337
|
+
async function quickCreateUpdateSet(scope, scopeSysId) {
|
|
338
|
+
var input = document.getElementById("quick-name-" + scope);
|
|
339
|
+
var name = input ? input.value.trim() : "";
|
|
340
|
+
if (!name) {
|
|
341
|
+
toast("Enter a name first", "error");
|
|
342
|
+
if (input) input.focus();
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Find and disable the create button
|
|
347
|
+
var card = document.getElementById("card-" + scope);
|
|
348
|
+
var btn = card ? card.querySelector(".quick-create .btn") : null;
|
|
349
|
+
if (btn) {
|
|
350
|
+
btn.disabled = true;
|
|
351
|
+
btn.textContent = "Creating...";
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
var data = await api("POST", "/api/update-set", {
|
|
356
|
+
name: name,
|
|
357
|
+
scope: scope,
|
|
358
|
+
scope_sys_id: scopeSysId,
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
var newSysId = data.update_set.sys_id;
|
|
362
|
+
|
|
363
|
+
await api("POST", "/api/select-update-set", {
|
|
364
|
+
scope: scope,
|
|
365
|
+
update_set_sys_id: newSysId,
|
|
366
|
+
update_set_name: name,
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
var scopeData = scopesData.find(function (s) { return s.scope === scope; });
|
|
370
|
+
if (scopeData) {
|
|
371
|
+
scopeData.selected_update_set = { sys_id: newSysId, name: name };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
toast("Created: " + name);
|
|
375
|
+
if (input) input.value = "";
|
|
376
|
+
|
|
377
|
+
loadUpdateSets(scope, newSysId);
|
|
378
|
+
|
|
379
|
+
if (card) {
|
|
380
|
+
card.classList.add("has-selection");
|
|
381
|
+
var badge = card.querySelector(".selected-badge");
|
|
382
|
+
if (!badge) {
|
|
383
|
+
var b = document.createElement("div");
|
|
384
|
+
b.className = "selected-badge";
|
|
385
|
+
b.textContent = "Active for push";
|
|
386
|
+
card.appendChild(b);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
var closeBtn = document.getElementById("close-btn-" + scope);
|
|
390
|
+
if (closeBtn) closeBtn.disabled = false;
|
|
391
|
+
var clearBtn = document.getElementById("clear-btn-" + scope);
|
|
392
|
+
if (clearBtn) clearBtn.disabled = false;
|
|
393
|
+
} catch (e) {
|
|
394
|
+
toast("Failed to create: " + e.message, "error");
|
|
395
|
+
} finally {
|
|
396
|
+
if (btn) {
|
|
397
|
+
btn.disabled = false;
|
|
398
|
+
btn.textContent = "Create";
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// =============================================================================
|
|
404
|
+
// ClickUp Sidebar
|
|
405
|
+
// =============================================================================
|
|
406
|
+
|
|
407
|
+
// --- Sidebar toggle ---
|
|
408
|
+
|
|
409
|
+
function toggleSidebar() {
|
|
410
|
+
var sidebar = document.getElementById("task-sidebar");
|
|
411
|
+
var toggleBtn = document.getElementById("sidebar-toggle");
|
|
412
|
+
|
|
413
|
+
sidebarOpen = !sidebarOpen;
|
|
414
|
+
if (sidebarOpen) {
|
|
415
|
+
sidebar.classList.remove("collapsed");
|
|
416
|
+
toggleBtn.classList.add("active");
|
|
417
|
+
if (Object.keys(clickupTasks).length === 0 && activeStatuses.length > 0) {
|
|
418
|
+
loadClickUpTasks();
|
|
419
|
+
}
|
|
420
|
+
} else {
|
|
421
|
+
sidebar.classList.add("collapsed");
|
|
422
|
+
toggleBtn.classList.remove("active");
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// --- Load ClickUp status ---
|
|
427
|
+
|
|
428
|
+
async function loadClickUpStatus() {
|
|
429
|
+
try {
|
|
430
|
+
var data = await api("GET", "/api/clickup/status");
|
|
431
|
+
clickupConfigured = data.configured;
|
|
432
|
+
|
|
433
|
+
if (clickupConfigured) {
|
|
434
|
+
document.getElementById("sidebar-toggle").style.display = "";
|
|
435
|
+
renderTaskToggles();
|
|
436
|
+
|
|
437
|
+
// Fetch current user for "My Tasks" filter
|
|
438
|
+
if (!clickupUserId) {
|
|
439
|
+
try {
|
|
440
|
+
var me = await api("GET", "/api/clickup/me");
|
|
441
|
+
clickupUserId = me.id;
|
|
442
|
+
} catch (meErr) {
|
|
443
|
+
// Non-critical — My Tasks filter just won't work
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (data.activeTask) {
|
|
449
|
+
activeTask = data.activeTask;
|
|
450
|
+
renderActiveTaskBanner();
|
|
451
|
+
renderActiveTaskChip();
|
|
452
|
+
renderScopes();
|
|
453
|
+
fillQuickCreateDefaults();
|
|
454
|
+
}
|
|
455
|
+
} catch (e) {
|
|
456
|
+
// ClickUp not available — just hide the button
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// --- Load ClickUp tasks ---
|
|
461
|
+
|
|
462
|
+
async function loadClickUpTasks() {
|
|
463
|
+
if (tasksLoading) return;
|
|
464
|
+
tasksLoading = true;
|
|
465
|
+
|
|
466
|
+
var taskList = document.getElementById("task-list");
|
|
467
|
+
taskList.innerHTML = '<div class="sidebar-loading">Loading tasks...</div>';
|
|
468
|
+
|
|
469
|
+
try {
|
|
470
|
+
var statusParam = activeStatuses.join(",");
|
|
471
|
+
var data = await api("GET", "/api/clickup/tasks?statuses=" + encodeURIComponent(statusParam));
|
|
472
|
+
clickupTasks = data.byStatus || {};
|
|
473
|
+
|
|
474
|
+
// Merge discovered statuses with what we know
|
|
475
|
+
var newStatuses = data.statuses || [];
|
|
476
|
+
newStatuses.forEach(function (s) {
|
|
477
|
+
if (availableStatuses.indexOf(s) === -1) {
|
|
478
|
+
availableStatuses.push(s);
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
renderStatusFilters();
|
|
483
|
+
renderTaskList();
|
|
484
|
+
} catch (e) {
|
|
485
|
+
taskList.innerHTML =
|
|
486
|
+
'<div class="sidebar-loading">Failed to load: ' + e.message + "</div>";
|
|
487
|
+
} finally {
|
|
488
|
+
tasksLoading = false;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// --- Render status filters ---
|
|
493
|
+
|
|
494
|
+
function renderStatusFilters() {
|
|
495
|
+
var container = document.getElementById("status-filters");
|
|
496
|
+
|
|
497
|
+
// Ensure common statuses are always available
|
|
498
|
+
var defaults = ["in progress", "open", "review", "to do"];
|
|
499
|
+
defaults.forEach(function (s) {
|
|
500
|
+
if (availableStatuses.indexOf(s) === -1) {
|
|
501
|
+
availableStatuses.push(s);
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
container.innerHTML = "";
|
|
506
|
+
availableStatuses.forEach(function (status) {
|
|
507
|
+
var chip = document.createElement("button");
|
|
508
|
+
chip.className = "filter-chip" + (activeStatuses.indexOf(status) !== -1 ? " active" : "");
|
|
509
|
+
chip.textContent = status;
|
|
510
|
+
chip.onclick = function () {
|
|
511
|
+
var idx = activeStatuses.indexOf(status);
|
|
512
|
+
if (idx !== -1) {
|
|
513
|
+
activeStatuses.splice(idx, 1);
|
|
514
|
+
} else {
|
|
515
|
+
activeStatuses.push(status);
|
|
516
|
+
}
|
|
517
|
+
chip.classList.toggle("active");
|
|
518
|
+
loadClickUpTasks();
|
|
519
|
+
};
|
|
520
|
+
container.appendChild(chip);
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// --- Render task list ---
|
|
525
|
+
|
|
526
|
+
function filterTasks(tasks) {
|
|
527
|
+
var filtered = tasks;
|
|
528
|
+
var query = taskSearchQuery.toLowerCase();
|
|
529
|
+
if (query) {
|
|
530
|
+
filtered = filtered.filter(function (task) {
|
|
531
|
+
var name = task.name.toLowerCase();
|
|
532
|
+
var id = (task.customId || task.id || "").toLowerCase();
|
|
533
|
+
return name.indexOf(query) !== -1 || id.indexOf(query) !== -1;
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
if (myTasksOnly && clickupUserId) {
|
|
537
|
+
filtered = filtered.filter(function (task) {
|
|
538
|
+
return task.assignees && task.assignees.some(function (a) {
|
|
539
|
+
return String(a.id) === String(clickupUserId);
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
return filtered;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function renderTaskList() {
|
|
547
|
+
var container = document.getElementById("task-list");
|
|
548
|
+
container.innerHTML = "";
|
|
549
|
+
|
|
550
|
+
var statusKeys = Object.keys(clickupTasks);
|
|
551
|
+
if (statusKeys.length === 0) {
|
|
552
|
+
container.innerHTML = '<div class="sidebar-loading">No tasks found</div>';
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
var totalVisible = 0;
|
|
557
|
+
|
|
558
|
+
statusKeys.forEach(function (status) {
|
|
559
|
+
var tasks = filterTasks(clickupTasks[status] || []);
|
|
560
|
+
if (tasks.length === 0) return;
|
|
561
|
+
|
|
562
|
+
totalVisible += tasks.length;
|
|
563
|
+
|
|
564
|
+
var group = document.createElement("div");
|
|
565
|
+
group.className = "task-status-group";
|
|
566
|
+
|
|
567
|
+
var label = document.createElement("div");
|
|
568
|
+
label.className = "task-status-label";
|
|
569
|
+
label.textContent = status + " (" + tasks.length + ")";
|
|
570
|
+
group.appendChild(label);
|
|
571
|
+
|
|
572
|
+
tasks.forEach(function (task) {
|
|
573
|
+
var card = document.createElement("div");
|
|
574
|
+
card.className = "task-card" + (activeTask && activeTask.taskId === task.id ? " selected" : "");
|
|
575
|
+
card.onclick = function () {
|
|
576
|
+
selectTask(task);
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
var priorityHtml = "";
|
|
580
|
+
if (task.priority) {
|
|
581
|
+
var colors = { urgent: "#f50057", high: "#ff7043", normal: "#ffab40", low: "#29b6f6" };
|
|
582
|
+
var color = colors[task.priority] || "#888";
|
|
583
|
+
priorityHtml = '<span class="task-priority-dot" style="background:' + color + '"></span>';
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
var assigneeHtml = "";
|
|
587
|
+
if (task.assignees && task.assignees.length > 0) {
|
|
588
|
+
var initials = task.assignees.map(function (a) {
|
|
589
|
+
return a.initials || (a.username || "?").substring(0, 2).toUpperCase();
|
|
590
|
+
}).join(", ");
|
|
591
|
+
assigneeHtml = '<span class="task-card-assignee">' + escapeHtml(initials) + "</span>";
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
card.innerHTML =
|
|
595
|
+
'<div class="task-card-name">' + escapeHtml(task.name) + "</div>" +
|
|
596
|
+
'<div class="task-card-meta">' +
|
|
597
|
+
'<span class="task-card-id">' + (task.customId || task.id) + "</span>" +
|
|
598
|
+
priorityHtml +
|
|
599
|
+
assigneeHtml +
|
|
600
|
+
"</div>";
|
|
601
|
+
|
|
602
|
+
group.appendChild(card);
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
container.appendChild(group);
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
if (totalVisible === 0) {
|
|
609
|
+
container.innerHTML = '<div class="sidebar-loading">No matching tasks</div>';
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// --- Select task ---
|
|
614
|
+
|
|
615
|
+
async function selectTask(task) {
|
|
616
|
+
// If already selected, deselect
|
|
617
|
+
if (activeTask && activeTask.taskId === task.id) {
|
|
618
|
+
await deselectTask();
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
try {
|
|
623
|
+
var data = await api("POST", "/api/clickup/select-task", {
|
|
624
|
+
taskId: task.id,
|
|
625
|
+
taskName: task.name,
|
|
626
|
+
taskDescription: task.description || "",
|
|
627
|
+
taskUrl: task.url || "",
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
activeTask = data.activeTask;
|
|
631
|
+
toast("Task selected: " + task.name);
|
|
632
|
+
|
|
633
|
+
renderActiveTaskBanner();
|
|
634
|
+
renderActiveTaskChip();
|
|
635
|
+
renderTaskList();
|
|
636
|
+
renderScopes();
|
|
637
|
+
|
|
638
|
+
// Fill quick-create inputs with the generated update set name
|
|
639
|
+
fillQuickCreateDefaults();
|
|
640
|
+
} catch (e) {
|
|
641
|
+
toast("Failed to select task: " + e.message, "error");
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// --- Deselect task ---
|
|
646
|
+
|
|
647
|
+
async function deselectTask() {
|
|
648
|
+
if (!confirm("Deselect active task? Update sets will remain.")) return;
|
|
649
|
+
|
|
650
|
+
try {
|
|
651
|
+
await api("POST", "/api/clickup/deselect-task");
|
|
652
|
+
activeTask = null;
|
|
653
|
+
toast("Task deselected");
|
|
654
|
+
|
|
655
|
+
clearQuickCreateDefaults();
|
|
656
|
+
renderActiveTaskBanner();
|
|
657
|
+
renderActiveTaskChip();
|
|
658
|
+
renderTaskList();
|
|
659
|
+
renderScopes();
|
|
660
|
+
} catch (e) {
|
|
661
|
+
toast("Failed to deselect: " + e.message, "error");
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// --- Render active task banner (sidebar) ---
|
|
666
|
+
|
|
667
|
+
function renderActiveTaskBanner() {
|
|
668
|
+
var banner = document.getElementById("active-task-banner");
|
|
669
|
+
if (!activeTask) {
|
|
670
|
+
banner.style.display = "none";
|
|
671
|
+
banner.innerHTML = "";
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
banner.style.display = "";
|
|
676
|
+
|
|
677
|
+
var scopeKeys = activeTask.scopes ? Object.keys(activeTask.scopes) : [];
|
|
678
|
+
var scopeText = scopeKeys.length > 0
|
|
679
|
+
? "<span>" + scopeKeys.join(", ") + "</span>"
|
|
680
|
+
: "None activated yet";
|
|
681
|
+
|
|
682
|
+
banner.innerHTML =
|
|
683
|
+
'<div class="active-task-name">' + escapeHtml(activeTask.taskName) + "</div>" +
|
|
684
|
+
'<div class="active-task-us-name">' + escapeHtml(activeTask.updateSetName) + "</div>" +
|
|
685
|
+
'<div class="active-task-scopes">Scopes: ' + scopeText + "</div>" +
|
|
686
|
+
'<button class="btn-deselect" onclick="deselectTask()">Deselect</button>';
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// --- Render active task chip (header) ---
|
|
690
|
+
|
|
691
|
+
function renderActiveTaskChip() {
|
|
692
|
+
var chip = document.getElementById("active-task-chip");
|
|
693
|
+
if (!activeTask) {
|
|
694
|
+
chip.style.display = "none";
|
|
695
|
+
chip.textContent = "";
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
chip.style.display = "";
|
|
700
|
+
chip.textContent = "CU-" + activeTask.taskId;
|
|
701
|
+
chip.title = activeTask.updateSetName;
|
|
702
|
+
chip.onclick = function () {
|
|
703
|
+
if (!sidebarOpen) toggleSidebar();
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// --- Activate scope for current task ---
|
|
708
|
+
|
|
709
|
+
async function activateScope(scope, scopeSysId) {
|
|
710
|
+
if (!activeTask) {
|
|
711
|
+
toast("No active task selected", "error");
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Find and disable the activate button
|
|
716
|
+
var card = document.getElementById("card-" + scope);
|
|
717
|
+
var activateBtn = card ? card.querySelector(".btn-activate") : null;
|
|
718
|
+
if (activateBtn) {
|
|
719
|
+
activateBtn.disabled = true;
|
|
720
|
+
activateBtn.textContent = "Activating...";
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
try {
|
|
724
|
+
var data = await api("POST", "/api/clickup/activate-scope", {
|
|
725
|
+
scope: scope,
|
|
726
|
+
scope_sys_id: scopeSysId,
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
var us = data.update_set;
|
|
730
|
+
var verb = data.created ? "Created" : "Found";
|
|
731
|
+
toast(verb + " update set for " + scope + ": " + us.name);
|
|
732
|
+
|
|
733
|
+
// Update activeTask scopes
|
|
734
|
+
if (!activeTask.scopes) activeTask.scopes = {};
|
|
735
|
+
activeTask.scopes[scope] = { sys_id: us.sys_id, name: us.name };
|
|
736
|
+
|
|
737
|
+
// Update scopesData so the dropdown reflects the new selection
|
|
738
|
+
var scopeData = scopesData.find(function (s) { return s.scope === scope; });
|
|
739
|
+
if (scopeData) {
|
|
740
|
+
scopeData.selected_update_set = { sys_id: us.sys_id, name: us.name };
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// Re-render to update all UI elements
|
|
744
|
+
renderActiveTaskBanner();
|
|
745
|
+
renderScopes();
|
|
746
|
+
} catch (e) {
|
|
747
|
+
toast("Failed to activate scope: " + e.message, "error");
|
|
748
|
+
if (activateBtn) {
|
|
749
|
+
activateBtn.disabled = false;
|
|
750
|
+
activateBtn.textContent = "Activate CU-" + activeTask.taskId;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// --- Auto-activate all scopes ---
|
|
756
|
+
|
|
757
|
+
async function autoActivateAllScopes() {
|
|
758
|
+
if (!activeTask) return;
|
|
759
|
+
|
|
760
|
+
// Disable all activate buttons and show progress
|
|
761
|
+
scopesData.forEach(function (scope) {
|
|
762
|
+
var card = document.getElementById("card-" + scope.scope);
|
|
763
|
+
var btn = card ? card.querySelector(".btn-activate") : null;
|
|
764
|
+
if (btn && !btn.classList.contains("activated")) {
|
|
765
|
+
btn.disabled = true;
|
|
766
|
+
btn.textContent = "Activating...";
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
try {
|
|
771
|
+
var data = await api("POST", "/api/clickup/activate-all-scopes");
|
|
772
|
+
var results = data.results || [];
|
|
773
|
+
|
|
774
|
+
// Update activeTask from server response
|
|
775
|
+
if (data.activeTask) {
|
|
776
|
+
activeTask = data.activeTask;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
var created = 0;
|
|
780
|
+
var found = 0;
|
|
781
|
+
var errors = 0;
|
|
782
|
+
|
|
783
|
+
results.forEach(function (r) {
|
|
784
|
+
if (r.error) {
|
|
785
|
+
errors++;
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
if (r.created) {
|
|
789
|
+
created++;
|
|
790
|
+
} else {
|
|
791
|
+
found++;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// Update scopesData
|
|
795
|
+
var scopeData = scopesData.find(function (s) { return s.scope === r.scope; });
|
|
796
|
+
if (scopeData) {
|
|
797
|
+
scopeData.selected_update_set = { sys_id: r.update_set.sys_id, name: r.update_set.name };
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
var parts = [];
|
|
802
|
+
if (created > 0) parts.push(created + " created");
|
|
803
|
+
if (found > 0) parts.push(found + " found");
|
|
804
|
+
if (errors > 0) parts.push(errors + " failed");
|
|
805
|
+
toast("Update sets: " + parts.join(", "));
|
|
806
|
+
|
|
807
|
+
renderActiveTaskBanner();
|
|
808
|
+
renderScopes();
|
|
809
|
+
} catch (e) {
|
|
810
|
+
toast("Failed to auto-activate scopes: " + e.message, "error");
|
|
811
|
+
renderScopes();
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// --- Task toggles (My Tasks) ---
|
|
816
|
+
|
|
817
|
+
function renderTaskToggles() {
|
|
818
|
+
var container = document.getElementById("task-toggles");
|
|
819
|
+
container.innerHTML = "";
|
|
820
|
+
|
|
821
|
+
if (!clickupConfigured) return;
|
|
822
|
+
|
|
823
|
+
var btn = document.createElement("button");
|
|
824
|
+
btn.className = "filter-chip" + (myTasksOnly ? " active" : "");
|
|
825
|
+
btn.textContent = "my tasks";
|
|
826
|
+
btn.onclick = function () {
|
|
827
|
+
myTasksOnly = !myTasksOnly;
|
|
828
|
+
btn.classList.toggle("active");
|
|
829
|
+
renderTaskList();
|
|
830
|
+
};
|
|
831
|
+
container.appendChild(btn);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// --- Quick-create defaults from active task ---
|
|
835
|
+
|
|
836
|
+
function fillQuickCreateDefaults() {
|
|
837
|
+
if (!activeTask) return;
|
|
838
|
+
scopesData.forEach(function (scope) {
|
|
839
|
+
var input = document.getElementById("quick-name-" + scope.scope);
|
|
840
|
+
if (input && !input.value.trim()) {
|
|
841
|
+
input.value = activeTask.updateSetName;
|
|
842
|
+
}
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function clearQuickCreateDefaults() {
|
|
847
|
+
scopesData.forEach(function (scope) {
|
|
848
|
+
var input = document.getElementById("quick-name-" + scope.scope);
|
|
849
|
+
if (input) {
|
|
850
|
+
input.value = "";
|
|
851
|
+
}
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// --- Recent Edits ---
|
|
856
|
+
|
|
857
|
+
var recentEdits = [];
|
|
858
|
+
var recentEditsInterval = null;
|
|
859
|
+
|
|
860
|
+
async function loadRecentEdits() {
|
|
861
|
+
try {
|
|
862
|
+
var data = await api("GET", "/api/recent-edits");
|
|
863
|
+
recentEdits = data.edits || [];
|
|
864
|
+
renderRecentEdits();
|
|
865
|
+
} catch (e) {
|
|
866
|
+
// Silently fail — panel just stays hidden or stale
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function timeAgo(timestamp) {
|
|
871
|
+
var now = new Date();
|
|
872
|
+
var then = new Date(timestamp);
|
|
873
|
+
var seconds = Math.floor((now - then) / 1000);
|
|
874
|
+
if (seconds < 60) return seconds + "s ago";
|
|
875
|
+
var minutes = Math.floor(seconds / 60);
|
|
876
|
+
if (minutes < 60) return minutes + "m ago";
|
|
877
|
+
var hours = Math.floor(minutes / 60);
|
|
878
|
+
if (hours < 24) return hours + "h ago";
|
|
879
|
+
return Math.floor(hours / 24) + "d ago";
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function renderRecentEdits() {
|
|
883
|
+
var panel = document.getElementById("recent-edits-panel");
|
|
884
|
+
var list = document.getElementById("recent-edits-list");
|
|
885
|
+
|
|
886
|
+
if (recentEdits.length === 0) {
|
|
887
|
+
panel.style.display = "none";
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
panel.style.display = "";
|
|
892
|
+
list.innerHTML = "";
|
|
893
|
+
|
|
894
|
+
// Build a lookup of expected update sets from scopesData
|
|
895
|
+
var expectedUpdateSets = {};
|
|
896
|
+
scopesData.forEach(function (scope) {
|
|
897
|
+
if (scope.selected_update_set) {
|
|
898
|
+
expectedUpdateSets[scope.scope] = scope.selected_update_set.name;
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
recentEdits.forEach(function (edit) {
|
|
903
|
+
var row = document.createElement("div");
|
|
904
|
+
row.className = "recent-edit-row";
|
|
905
|
+
|
|
906
|
+
var nameEl = document.createElement("span");
|
|
907
|
+
nameEl.className = "recent-edit-name";
|
|
908
|
+
nameEl.textContent = edit.name;
|
|
909
|
+
nameEl.title = edit.tableName + "/" + edit.name;
|
|
910
|
+
|
|
911
|
+
var scopeEl = document.createElement("span");
|
|
912
|
+
scopeEl.className = "recent-edit-scope";
|
|
913
|
+
scopeEl.textContent = edit.scope;
|
|
914
|
+
|
|
915
|
+
var updateSetEl = document.createElement("span");
|
|
916
|
+
var isDefault = edit.updateSet.toLowerCase().indexOf("default") !== -1;
|
|
917
|
+
var expected = expectedUpdateSets[edit.scope];
|
|
918
|
+
var isMismatch = expected && edit.updateSet !== expected && edit.updateSet !== "unknown";
|
|
919
|
+
updateSetEl.className = "recent-edit-update-set" + (isDefault || isMismatch ? " warning" : "");
|
|
920
|
+
updateSetEl.textContent = edit.updateSet;
|
|
921
|
+
if (isMismatch && expected) {
|
|
922
|
+
updateSetEl.title = "Expected: " + expected;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
var timeEl = document.createElement("span");
|
|
926
|
+
timeEl.className = "recent-edit-time";
|
|
927
|
+
timeEl.textContent = timeAgo(edit.timestamp);
|
|
928
|
+
|
|
929
|
+
row.appendChild(nameEl);
|
|
930
|
+
row.appendChild(scopeEl);
|
|
931
|
+
row.appendChild(updateSetEl);
|
|
932
|
+
row.appendChild(timeEl);
|
|
933
|
+
list.appendChild(row);
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// --- Utility ---
|
|
938
|
+
|
|
939
|
+
function escapeHtml(str) {
|
|
940
|
+
var div = document.createElement("div");
|
|
941
|
+
div.appendChild(document.createTextNode(str));
|
|
942
|
+
return div.innerHTML;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// --- Keyboard ---
|
|
946
|
+
|
|
947
|
+
document.addEventListener("keydown", function (e) {
|
|
948
|
+
if (e.key === "Escape") {
|
|
949
|
+
closeModal();
|
|
950
|
+
if (sidebarOpen) toggleSidebar();
|
|
951
|
+
}
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
// --- Refresh ---
|
|
955
|
+
|
|
956
|
+
async function refreshDashboard() {
|
|
957
|
+
var btn = document.getElementById("refresh-btn");
|
|
958
|
+
btn.disabled = true;
|
|
959
|
+
btn.textContent = "Refreshing...";
|
|
960
|
+
|
|
961
|
+
try {
|
|
962
|
+
await loadScopes();
|
|
963
|
+
await loadRecentEdits();
|
|
964
|
+
if (clickupConfigured && activeTask) {
|
|
965
|
+
await loadClickUpStatus();
|
|
966
|
+
}
|
|
967
|
+
toast("Dashboard refreshed");
|
|
968
|
+
} catch (e) {
|
|
969
|
+
toast("Refresh failed: " + e.message, "error");
|
|
970
|
+
} finally {
|
|
971
|
+
btn.disabled = false;
|
|
972
|
+
btn.textContent = "Refresh";
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// --- Init ---
|
|
977
|
+
|
|
978
|
+
loadConfig();
|
|
979
|
+
loadScopes();
|
|
980
|
+
loadRecentEdits();
|
|
981
|
+
loadClickUpStatus();
|
|
982
|
+
|
|
983
|
+
// Poll recent edits every 10 seconds
|
|
984
|
+
recentEditsInterval = setInterval(loadRecentEdits, 60000);
|
|
985
|
+
|
|
986
|
+
// Wire up sidebar toggle and close buttons
|
|
987
|
+
document.getElementById("sidebar-toggle").addEventListener("click", toggleSidebar);
|
|
988
|
+
document.getElementById("sidebar-close").addEventListener("click", toggleSidebar);
|
|
989
|
+
document.getElementById("refresh-btn").addEventListener("click", refreshDashboard);
|
|
990
|
+
|
|
991
|
+
// Render initial status filter chips
|
|
992
|
+
renderStatusFilters();
|
|
993
|
+
|
|
994
|
+
// Wire up task search input
|
|
995
|
+
document.getElementById("task-search").addEventListener("input", function (e) {
|
|
996
|
+
taskSearchQuery = e.target.value;
|
|
997
|
+
renderTaskList();
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
// Render task toggles (My Tasks)
|
|
1001
|
+
renderTaskToggles();
|