- Foldable left panel (user profile) and right panel (task details) - Clicking a task in the list or graph node selects it and shows details - Both views (task list + graph) always mounted via absolute inset-0 for correct canvas dimensions; tabs toggle visibility with opacity - Graph node selection animation: other nodes repel outward (charge -600), then selected node smoothly slides to center (500ms cubic ease-out), then charge restores to -120 and graph stabilizes - Graph re-fits on tab switch and panel resize via ResizeObserver - Fix UUID string IDs throughout (backend returns UUIDs, not integers) - Add TaskDetailPanel, UserPanel components - Add CLAUDE.md project documentation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
115 lines
2.1 KiB
JavaScript
115 lines
2.1 KiB
JavaScript
"use strict";
|
|
|
|
class QueueItem {
|
|
constructor(onLoad, onError, dependentItem) {
|
|
this.onLoad = onLoad;
|
|
this.onError = onError;
|
|
this.data = null;
|
|
this.error = null;
|
|
this.dependentItem = dependentItem;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* AsyncResourceQueue is the queue in charge of run the async scripts
|
|
* and notify when they finish.
|
|
*/
|
|
module.exports = class AsyncResourceQueue {
|
|
constructor() {
|
|
this.items = new Set();
|
|
this.dependentItems = new Set();
|
|
}
|
|
|
|
count() {
|
|
return this.items.size + this.dependentItems.size;
|
|
}
|
|
|
|
_notify() {
|
|
if (this._listener) {
|
|
this._listener();
|
|
}
|
|
}
|
|
|
|
_check(item) {
|
|
let promise;
|
|
|
|
if (item.onError && item.error) {
|
|
promise = item.onError(item.error);
|
|
} else if (item.onLoad && item.data) {
|
|
promise = item.onLoad(item.data);
|
|
}
|
|
|
|
promise
|
|
.then(() => {
|
|
this.items.delete(item);
|
|
this.dependentItems.delete(item);
|
|
|
|
if (this.count() === 0) {
|
|
this._notify();
|
|
}
|
|
});
|
|
}
|
|
|
|
setListener(listener) {
|
|
this._listener = listener;
|
|
}
|
|
|
|
push(request, onLoad, onError, dependentItem) {
|
|
const q = this;
|
|
|
|
const item = new QueueItem(onLoad, onError, dependentItem);
|
|
|
|
q.items.add(item);
|
|
|
|
return request
|
|
.then(data => {
|
|
item.data = data;
|
|
|
|
if (dependentItem && !dependentItem.finished) {
|
|
q.dependentItems.add(item);
|
|
return q.items.delete(item);
|
|
}
|
|
|
|
if (onLoad) {
|
|
return q._check(item);
|
|
}
|
|
|
|
q.items.delete(item);
|
|
|
|
if (q.count() === 0) {
|
|
q._notify();
|
|
}
|
|
|
|
return null;
|
|
})
|
|
.catch(err => {
|
|
item.error = err;
|
|
|
|
if (dependentItem && !dependentItem.finished) {
|
|
q.dependentItems.add(item);
|
|
return q.items.delete(item);
|
|
}
|
|
|
|
if (onError) {
|
|
return q._check(item);
|
|
}
|
|
|
|
q.items.delete(item);
|
|
|
|
if (q.count() === 0) {
|
|
q._notify();
|
|
}
|
|
|
|
return null;
|
|
});
|
|
}
|
|
|
|
notifyItem(syncItem) {
|
|
for (const item of this.dependentItems) {
|
|
if (item.dependentItem === syncItem) {
|
|
this._check(item);
|
|
}
|
|
}
|
|
}
|
|
};
|