@datum-cloud/datum-ui 0.2.0-alpha.3 → 0.2.0-alpha.5
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 +66 -32
- package/dist/alert/index.mjs +3 -0
- package/dist/alert-BC2Mccfo.mjs +95 -0
- package/dist/autocomplete/index.mjs +7 -0
- package/dist/autocomplete-DZtI97HP.mjs +295 -0
- package/dist/avatar-stack/index.mjs +5 -0
- package/dist/avatar-stack-JCfBlPB9.mjs +80 -0
- package/dist/badge/index.mjs +3 -0
- package/dist/badge-bFgeYceE.mjs +185 -0
- package/dist/breadcrumb/index.mjs +4 -0
- package/dist/breadcrumb-BGYJgom_.mjs +71 -0
- package/dist/button/index.mjs +4 -0
- package/dist/button-AzpnV-WB.mjs +49 -0
- package/dist/button-C1wRfGtT.mjs +230 -0
- package/dist/button-group/index.mjs +5 -0
- package/dist/button-group-C1IB2K5s.mjs +40 -0
- package/dist/calendar/index.mjs +5 -0
- package/dist/calendar-DlIHeWb0.mjs +113 -0
- package/dist/card/index.mjs +4 -0
- package/dist/card-3Kd0VdNf.mjs +63 -0
- package/dist/chart/index.mjs +4 -0
- package/dist/chart-BZqUKpkh.mjs +143 -0
- package/dist/checkbox/index.mjs +4 -0
- package/dist/checkbox-LG1OKTpG.mjs +34 -0
- package/dist/col-lrLMZaTJ.mjs +184 -0
- package/dist/collapsible/index.mjs +3 -0
- package/dist/collapsible-Bt9UYfv3.mjs +9 -0
- package/dist/command/index.mjs +5 -0
- package/dist/command-s0Yv3abE.mjs +86 -0
- package/dist/components/features/date-picker/index.d.ts +3 -0
- package/dist/components/features/date-picker/index.d.ts.map +1 -0
- package/dist/components/features/dropzone/index.d.ts +1 -0
- package/dist/components/features/dropzone/index.d.ts.map +1 -1
- package/dist/components/themes/index.d.ts +1 -1
- package/dist/components/themes/index.d.ts.map +1 -1
- package/dist/components/themes/types.d.ts +0 -2
- package/dist/components/themes/types.d.ts.map +1 -1
- package/dist/date-picker/index.mjs +9 -0
- package/dist/dialog/index.mjs +5 -0
- package/dist/dialog-DXBaT9gA.mjs +86 -0
- package/dist/dialog-bnMMf9GD.mjs +73 -0
- package/dist/dropdown/index.mjs +3 -0
- package/dist/dropdown-DtSa_lqc.mjs +112 -0
- package/dist/dropzone/index.mjs +5 -0
- package/dist/dropzone-BkOnwrS4.mjs +221 -0
- package/dist/empty-content/index.mjs +3 -0
- package/dist/empty-content-BM9rzI13.mjs +196 -0
- package/dist/exports/map.d.ts +3 -0
- package/dist/exports/map.d.ts.map +1 -0
- package/dist/fonts/AllianceNo1-Medium.ttf +0 -0
- package/dist/fonts/AllianceNo1-Regular.ttf +0 -0
- package/dist/fonts/AllianceNo1-SemiBold.ttf +0 -0
- package/dist/form/index.mjs +146 -0
- package/dist/grid/index.mjs +3 -0
- package/dist/hooks/index.mjs +2 -3
- package/dist/hover-card/index.mjs +4 -0
- package/dist/hover-card-CUPfFUqE.mjs +33 -0
- package/dist/icon-wrapper-9ticVbRL.mjs +14 -0
- package/dist/icons/index.mjs +3 -3
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +65 -9
- package/dist/input/index.mjs +5 -0
- package/dist/input-DuyjEKEW.mjs +17 -0
- package/dist/input-fzXBheCN.mjs +17 -0
- package/dist/input-group/index.mjs +7 -0
- package/dist/input-group-CPaFSTEV.mjs +80 -0
- package/dist/input-number/index.mjs +6 -0
- package/dist/input-number-9o62JHRl.mjs +106 -0
- package/dist/input-with-addons/index.mjs +3 -0
- package/dist/input-with-addons-BQn7KCTU.mjs +30 -0
- package/dist/label/index.mjs +4 -0
- package/dist/label-_ste_Re3.mjs +44 -0
- package/dist/link-button-TIF2Zdrk.mjs +36 -0
- package/dist/loader-overlay/index.mjs +3 -0
- package/dist/loader-overlay-DUaQSZQP.mjs +17 -0
- package/dist/map/index.mjs +13 -0
- package/dist/map-WL6jhkSM.mjs +1094 -0
- package/dist/more-actions/index.mjs +5 -0
- package/dist/more-actions-Ch1f6Mh3.mjs +54 -0
- package/dist/nprogress/index.mjs +32 -0
- package/dist/page-title/index.mjs +3 -0
- package/dist/page-title-BJuo81rT.mjs +26 -0
- package/dist/popover/index.mjs +4 -0
- package/dist/popover-SQlKSz6L.mjs +36 -0
- package/dist/radio-group/index.mjs +4 -0
- package/dist/radio-group-Oshv0b-U.mjs +49 -0
- package/dist/select/index.mjs +4 -0
- package/dist/select-DVlEzD2W.mjs +166 -0
- package/dist/separator/index.mjs +4 -0
- package/dist/separator-T2ppyD-8.mjs +18 -0
- package/dist/sheet/index.mjs +5 -0
- package/dist/sheet-BKiCwtNO.mjs +45 -0
- package/dist/sheet-CtnP6gTD.mjs +77 -0
- package/dist/sidebar/index.mjs +11 -0
- package/dist/sidebar-DfqezV8t.mjs +945 -0
- package/dist/skeleton/index.mjs +4 -0
- package/dist/skeleton-vzbxA-DQ.mjs +13 -0
- package/dist/spinner/index.mjs +4 -0
- package/dist/spinner-BE7k2bAD.mjs +16 -0
- package/dist/{icon-wrapper-BgPkifId.mjs → spinner.icon-Bg8zgGh0.mjs} +1 -12
- package/dist/stepper/index.mjs +5 -0
- package/dist/stepper-SWB-u_nM.mjs +323 -0
- package/dist/{style.css → styles.css} +317 -575
- package/dist/styles.mjs +1 -0
- package/dist/switch/index.mjs +4 -0
- package/dist/switch-Calk7Gyw.mjs +32 -0
- package/dist/table/index.mjs +4 -0
- package/dist/table-CsXBcQLI.mjs +68 -0
- package/dist/tabs/index.mjs +3 -0
- package/dist/tabs-D8n-dqnw.mjs +52 -0
- package/dist/tag-input/index.mjs +5 -0
- package/dist/tag-input-Di7SDNbK.mjs +284 -0
- package/dist/task-queue/index.mjs +7 -0
- package/dist/task-queue-dropdown-DW72ikDH.mjs +1356 -0
- package/dist/textarea/index.mjs +5 -0
- package/dist/textarea-CxE3YbC7.mjs +17 -0
- package/dist/textarea-QYRcDEpK.mjs +15 -0
- package/dist/theme/index.mjs +3 -0
- package/dist/{theme.provider-DpFLwtHe.mjs → theme.provider-CzCxEFFh.mjs} +63 -1
- package/dist/to-api-format-C2xjQUcI.mjs +1506 -0
- package/dist/toast/index.mjs +3 -0
- package/dist/tooltip/index.mjs +4 -0
- package/dist/tooltip-Dd3ActSS.mjs +74 -0
- package/dist/typography/index.mjs +3 -0
- package/dist/typography-UA7ZZvgJ.mjs +200 -0
- package/dist/use-copy-to-clipboard-ki-WoTml.mjs +31 -0
- package/dist/use-stepper-BaToCYMs.mjs +2017 -0
- package/dist/{use-copy-to-clipboard-BfrpD6G8.mjs → use-toast-mdn_CqRY.mjs} +34 -27
- package/dist/utils/index.mjs +0 -1
- package/dist/utils-Bfgoe-Gm.mjs +20 -0
- package/dist/visually-hidden/index.mjs +3 -0
- package/dist/visuallyhidden-aaTUk4Yo.mjs +7 -0
- package/package.json +223 -24
- package/dist/components/index.mjs +0 -8
- package/dist/datum.provider-D6VMjSV0.mjs +0 -37
- package/dist/providers/datum.provider.d.ts +0 -20
- package/dist/providers/datum.provider.d.ts.map +0 -1
- package/dist/providers/index.d.ts +0 -3
- package/dist/providers/index.d.ts.map +0 -1
- package/dist/providers/index.mjs +0 -4
- package/dist/theme-script-DHyLk25i.mjs +0 -11128
- /package/dist/{close.icon-chkXPAUC.mjs → close.icon-CMNMoXM_.mjs} +0 -0
- /package/dist/{map-leaflet-imports-OKaoesjZ.mjs → map-leaflet-imports-C4JYls8q.mjs} +0 -0
- /package/dist/{use-debounce-BYB-jPeX.mjs → use-debounce-B6wPrZV8.mjs} +0 -0
|
@@ -0,0 +1,1356 @@
|
|
|
1
|
+
import { t as cn } from "./cn-DWCc1QRE.mjs";
|
|
2
|
+
import { t as Badge } from "./badge-bFgeYceE.mjs";
|
|
3
|
+
import { t as SpinnerIcon } from "./spinner.icon-Bg8zgGh0.mjs";
|
|
4
|
+
import { t as Button } from "./button-C1wRfGtT.mjs";
|
|
5
|
+
import { t as Icon } from "./icon-wrapper-9ticVbRL.mjs";
|
|
6
|
+
import { t as Dialog } from "./dialog-bnMMf9GD.mjs";
|
|
7
|
+
import { c as TableRow, i as TableCell, n as TableBody, o as TableHead, s as TableHeader, t as Table } from "./table-CsXBcQLI.mjs";
|
|
8
|
+
import { a as TooltipTrigger, n as Tooltip, r as TooltipContent, t as Tooltip$1 } from "./tooltip-Dd3ActSS.mjs";
|
|
9
|
+
import { h as DropdownMenuTrigger, r as DropdownMenuContent, t as DropdownMenu } from "./dropdown-DtSa_lqc.mjs";
|
|
10
|
+
import { Ban, CheckCircle2, CircleAlert, CircleCheck, CornerDownRightIcon, FileIcon, ListTodo, X, XCircle } from "lucide-react";
|
|
11
|
+
import { createContext, use, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
12
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
13
|
+
import { formatDistanceToNowStrict } from "date-fns";
|
|
14
|
+
|
|
15
|
+
//#region src/components/features/task-queue/engine/executor.ts
|
|
16
|
+
function createTaskContext(task, callbacks) {
|
|
17
|
+
let cancelled = false;
|
|
18
|
+
let shouldStop = false;
|
|
19
|
+
const cleanupCallbacks = [];
|
|
20
|
+
const updateTask = () => {
|
|
21
|
+
callbacks.onUpdate({
|
|
22
|
+
...task,
|
|
23
|
+
succeededItems: [...task.succeededItems],
|
|
24
|
+
failedItems: [...task.failedItems]
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
return {
|
|
28
|
+
ctx: {
|
|
29
|
+
get items() {
|
|
30
|
+
return task.items ?? [];
|
|
31
|
+
},
|
|
32
|
+
get cancelled() {
|
|
33
|
+
return cancelled || shouldStop;
|
|
34
|
+
},
|
|
35
|
+
get failedItems() {
|
|
36
|
+
return [...task.failedItems];
|
|
37
|
+
},
|
|
38
|
+
succeed(itemId) {
|
|
39
|
+
task.completed += 1;
|
|
40
|
+
if (itemId) task.succeededItems = [...task.succeededItems, itemId];
|
|
41
|
+
updateTask();
|
|
42
|
+
},
|
|
43
|
+
fail(itemId, message) {
|
|
44
|
+
task.failed += 1;
|
|
45
|
+
if (itemId || message) task.failedItems = [...task.failedItems, {
|
|
46
|
+
id: itemId,
|
|
47
|
+
message: message ?? "Unknown error"
|
|
48
|
+
}];
|
|
49
|
+
if (task.errorStrategy === "stop") shouldStop = true;
|
|
50
|
+
updateTask();
|
|
51
|
+
},
|
|
52
|
+
setTitle(title) {
|
|
53
|
+
task.title = title;
|
|
54
|
+
updateTask();
|
|
55
|
+
},
|
|
56
|
+
setResult(result) {
|
|
57
|
+
task.result = result;
|
|
58
|
+
},
|
|
59
|
+
onCancel(cleanup) {
|
|
60
|
+
cleanupCallbacks.push(cleanup);
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
getCancelled: () => cancelled,
|
|
64
|
+
setCancelled: (v) => {
|
|
65
|
+
cancelled = v;
|
|
66
|
+
if (v) cleanupCallbacks.forEach((cleanup) => {
|
|
67
|
+
try {
|
|
68
|
+
cleanup();
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error("[TaskQueue] Cleanup callback error:", error);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
getShouldStop: () => shouldStop
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
async function executeTask(task, callbacks) {
|
|
78
|
+
const processor = task._processor;
|
|
79
|
+
if (!processor) return {
|
|
80
|
+
status: "failed",
|
|
81
|
+
completed: 0,
|
|
82
|
+
failed: 0,
|
|
83
|
+
failedItems: [{ message: "No processor attached" }]
|
|
84
|
+
};
|
|
85
|
+
task.status = "running";
|
|
86
|
+
task.startedAt = Date.now();
|
|
87
|
+
if (!task.succeededItems) task.succeededItems = [];
|
|
88
|
+
callbacks.onUpdate({ ...task });
|
|
89
|
+
const { ctx, getCancelled, setCancelled, getShouldStop } = createTaskContext(task, callbacks);
|
|
90
|
+
callbacks.onCancelReady?.(setCancelled);
|
|
91
|
+
try {
|
|
92
|
+
await processor(ctx);
|
|
93
|
+
if (getCancelled()) task.status = "cancelled";
|
|
94
|
+
else if (getShouldStop() || task.failed > 0) task.status = "failed";
|
|
95
|
+
else task.status = "completed";
|
|
96
|
+
} catch (error) {
|
|
97
|
+
task.status = "failed";
|
|
98
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
99
|
+
task.failedItems = [...task.failedItems, { message }];
|
|
100
|
+
task.failed += 1;
|
|
101
|
+
}
|
|
102
|
+
task.completedAt = Date.now();
|
|
103
|
+
callbacks.onUpdate({ ...task });
|
|
104
|
+
callbacks.onTaskComplete?.();
|
|
105
|
+
return {
|
|
106
|
+
status: task.status,
|
|
107
|
+
completed: task.completed,
|
|
108
|
+
failed: task.failed,
|
|
109
|
+
failedItems: [...task.failedItems],
|
|
110
|
+
result: task.result
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
//#endregion
|
|
115
|
+
//#region src/components/features/task-queue/constants.ts
|
|
116
|
+
const TASK_QUEUE_DEFAULTS = {
|
|
117
|
+
concurrency: 3,
|
|
118
|
+
errorStrategy: "continue",
|
|
119
|
+
cancelable: true,
|
|
120
|
+
retryable: true
|
|
121
|
+
};
|
|
122
|
+
const TASK_STORAGE_KEY = "datum-task-queue";
|
|
123
|
+
|
|
124
|
+
//#endregion
|
|
125
|
+
//#region src/components/features/task-queue/utils/index.ts
|
|
126
|
+
/**
|
|
127
|
+
* Check if code is running in a browser environment.
|
|
128
|
+
* Used for SSR safety in storage and other browser-dependent code.
|
|
129
|
+
*/
|
|
130
|
+
function isBrowser() {
|
|
131
|
+
return typeof window !== "undefined" && typeof localStorage !== "undefined";
|
|
132
|
+
}
|
|
133
|
+
function generateTaskId() {
|
|
134
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return `task_${crypto.randomUUID()}`;
|
|
135
|
+
return `task_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Extract an ID from an item using common patterns.
|
|
139
|
+
* Checks: primitives (string/number) → obj.id → obj.name → obj.key → obj.uuid
|
|
140
|
+
*/
|
|
141
|
+
function extractItemId(item) {
|
|
142
|
+
if (typeof item === "string" || typeof item === "number") return String(item);
|
|
143
|
+
if (item && typeof item === "object") {
|
|
144
|
+
const obj = item;
|
|
145
|
+
if (typeof obj.id === "string" || typeof obj.id === "number") return String(obj.id);
|
|
146
|
+
if (typeof obj.name === "string") return obj.name;
|
|
147
|
+
if (typeof obj.key === "string") return obj.key;
|
|
148
|
+
if (typeof obj.uuid === "string") return obj.uuid;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Create metadata for project-scoped tasks.
|
|
153
|
+
* Use this when enqueueing tasks that operate on project resources.
|
|
154
|
+
*/
|
|
155
|
+
function createProjectMetadata(project, org, extra) {
|
|
156
|
+
return {
|
|
157
|
+
scope: "project",
|
|
158
|
+
projectId: project.id,
|
|
159
|
+
projectName: project.name,
|
|
160
|
+
orgId: org.id,
|
|
161
|
+
orgName: org.name,
|
|
162
|
+
...extra
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Create metadata for organization-scoped tasks.
|
|
167
|
+
* Use this when enqueueing tasks that operate on org-level resources.
|
|
168
|
+
*/
|
|
169
|
+
function createOrgMetadata(org, extra) {
|
|
170
|
+
return {
|
|
171
|
+
scope: "org",
|
|
172
|
+
orgId: org.id,
|
|
173
|
+
orgName: org.name,
|
|
174
|
+
...extra
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Create metadata for user-scoped tasks.
|
|
179
|
+
* Use this when enqueueing tasks that operate on user-level resources.
|
|
180
|
+
*/
|
|
181
|
+
function createUserMetadata(extra) {
|
|
182
|
+
return {
|
|
183
|
+
scope: "user",
|
|
184
|
+
...extra
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
//#endregion
|
|
189
|
+
//#region src/components/features/task-queue/engine/storage/local-storage.ts
|
|
190
|
+
var LocalTaskStorage = class {
|
|
191
|
+
key;
|
|
192
|
+
constructor(key = TASK_STORAGE_KEY) {
|
|
193
|
+
this.key = key;
|
|
194
|
+
}
|
|
195
|
+
getAll() {
|
|
196
|
+
if (!isBrowser()) return [];
|
|
197
|
+
try {
|
|
198
|
+
const raw = localStorage.getItem(this.key);
|
|
199
|
+
return raw ? JSON.parse(raw) : [];
|
|
200
|
+
} catch {
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
get(id) {
|
|
205
|
+
if (!isBrowser()) return void 0;
|
|
206
|
+
return this.getAll().find((t) => t.id === id);
|
|
207
|
+
}
|
|
208
|
+
set(id, task) {
|
|
209
|
+
if (!isBrowser()) return;
|
|
210
|
+
const tasks = this.getAll();
|
|
211
|
+
const index = tasks.findIndex((t) => t.id === id);
|
|
212
|
+
if (index >= 0) tasks[index] = task;
|
|
213
|
+
else tasks.push(task);
|
|
214
|
+
this.persist(tasks);
|
|
215
|
+
}
|
|
216
|
+
remove(id) {
|
|
217
|
+
if (!isBrowser()) return;
|
|
218
|
+
const tasks = this.getAll().filter((t) => t.id !== id);
|
|
219
|
+
this.persist(tasks);
|
|
220
|
+
}
|
|
221
|
+
clear() {
|
|
222
|
+
if (!isBrowser()) return;
|
|
223
|
+
try {
|
|
224
|
+
localStorage.removeItem(this.key);
|
|
225
|
+
} catch {}
|
|
226
|
+
}
|
|
227
|
+
persist(tasks) {
|
|
228
|
+
try {
|
|
229
|
+
const serializable = tasks.map((task) => {
|
|
230
|
+
const { _processor, _icon, _completionActions, ...rest } = task;
|
|
231
|
+
return rest;
|
|
232
|
+
});
|
|
233
|
+
localStorage.setItem(this.key, JSON.stringify(serializable));
|
|
234
|
+
} catch (error) {
|
|
235
|
+
if (error instanceof Error && error.name === "QuotaExceededError") console.warn("[TaskQueue] Storage quota exceeded. Consider dismissing old tasks.");
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
//#endregion
|
|
241
|
+
//#region src/components/features/task-queue/engine/storage/memory-storage.ts
|
|
242
|
+
/**
|
|
243
|
+
* In-memory task storage.
|
|
244
|
+
*
|
|
245
|
+
* Tasks are stored in a Map and lost on page reload.
|
|
246
|
+
* Use with beforeunload warning to alert users of active tasks.
|
|
247
|
+
*
|
|
248
|
+
* SSR-safe: works on both server and client (just empty on server).
|
|
249
|
+
*/
|
|
250
|
+
var MemoryTaskStorage = class {
|
|
251
|
+
tasks = /* @__PURE__ */ new Map();
|
|
252
|
+
getAll() {
|
|
253
|
+
return Array.from(this.tasks.values());
|
|
254
|
+
}
|
|
255
|
+
get(id) {
|
|
256
|
+
return this.tasks.get(id);
|
|
257
|
+
}
|
|
258
|
+
set(id, task) {
|
|
259
|
+
this.tasks.set(id, task);
|
|
260
|
+
}
|
|
261
|
+
remove(id) {
|
|
262
|
+
this.tasks.delete(id);
|
|
263
|
+
}
|
|
264
|
+
clear() {
|
|
265
|
+
this.tasks.clear();
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
//#endregion
|
|
270
|
+
//#region src/components/features/task-queue/engine/storage/redis-storage.ts
|
|
271
|
+
const DEFAULT_TTL = 3600 * 24 * 7;
|
|
272
|
+
var RedisTaskStorage = class {
|
|
273
|
+
client;
|
|
274
|
+
key;
|
|
275
|
+
cache = /* @__PURE__ */ new Map();
|
|
276
|
+
initialized = false;
|
|
277
|
+
initPromise = null;
|
|
278
|
+
ttl;
|
|
279
|
+
constructor(client, key = TASK_STORAGE_KEY, ttl = DEFAULT_TTL) {
|
|
280
|
+
this.client = client;
|
|
281
|
+
this.key = key;
|
|
282
|
+
this.ttl = ttl;
|
|
283
|
+
if (isBrowser()) this.initPromise = this.initialize();
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Wait for initialization to complete.
|
|
287
|
+
* Call this before accessing data if you need guaranteed consistency.
|
|
288
|
+
*/
|
|
289
|
+
async waitForInit() {
|
|
290
|
+
if (this.initPromise) await this.initPromise;
|
|
291
|
+
}
|
|
292
|
+
async initialize() {
|
|
293
|
+
if (this.initialized) return;
|
|
294
|
+
if (!isBrowser()) {
|
|
295
|
+
this.initialized = true;
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
const raw = await this.client.get(this.key);
|
|
300
|
+
if (raw) JSON.parse(raw).forEach((t) => this.cache.set(t.id, t));
|
|
301
|
+
this.initialized = true;
|
|
302
|
+
} catch {
|
|
303
|
+
this.initialized = true;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
getAll() {
|
|
307
|
+
return Array.from(this.cache.values());
|
|
308
|
+
}
|
|
309
|
+
get(id) {
|
|
310
|
+
return this.cache.get(id);
|
|
311
|
+
}
|
|
312
|
+
set(id, task) {
|
|
313
|
+
if (!isBrowser()) return;
|
|
314
|
+
this.cache.set(id, task);
|
|
315
|
+
this.syncToRedis();
|
|
316
|
+
}
|
|
317
|
+
remove(id) {
|
|
318
|
+
if (!isBrowser()) return;
|
|
319
|
+
this.cache.delete(id);
|
|
320
|
+
this.syncToRedis();
|
|
321
|
+
}
|
|
322
|
+
clear() {
|
|
323
|
+
if (!isBrowser()) return;
|
|
324
|
+
this.cache.clear();
|
|
325
|
+
this.client.del(this.key).catch(() => {});
|
|
326
|
+
}
|
|
327
|
+
syncToRedis() {
|
|
328
|
+
const tasks = this.getAll().map((task) => {
|
|
329
|
+
const { _processor, _icon, _completionActions, ...rest } = task;
|
|
330
|
+
return rest;
|
|
331
|
+
});
|
|
332
|
+
const value = JSON.stringify(tasks);
|
|
333
|
+
if ("setex" in this.client && typeof this.client.setex === "function") this.client.setex(this.key, this.ttl, value).catch(() => {});
|
|
334
|
+
else this.client.set(this.key, value).catch(() => {});
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
//#endregion
|
|
339
|
+
//#region src/components/features/task-queue/engine/storage/detect-storage.ts
|
|
340
|
+
/**
|
|
341
|
+
* Auto-detects and returns the appropriate storage backend.
|
|
342
|
+
*
|
|
343
|
+
* Storage types:
|
|
344
|
+
* - `memory` (default): In-memory storage, tasks lost on reload
|
|
345
|
+
* - `local`: Browser localStorage, tasks persist across reloads
|
|
346
|
+
* - `auto`: Uses Redis if available, otherwise localStorage
|
|
347
|
+
*
|
|
348
|
+
* All storage backends are SSR-safe and will return empty data on the server.
|
|
349
|
+
*/
|
|
350
|
+
function detectStorage(options = {}) {
|
|
351
|
+
const { redisClient, storageKey, storageType = "memory" } = options;
|
|
352
|
+
if (storageType === "memory") return new MemoryTaskStorage();
|
|
353
|
+
if (storageType === "local") return new LocalTaskStorage(storageKey);
|
|
354
|
+
if (redisClient && redisClient.status === "ready") return new RedisTaskStorage(redisClient, storageKey);
|
|
355
|
+
return new LocalTaskStorage(storageKey);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
//#endregion
|
|
359
|
+
//#region src/components/features/task-queue/engine/queue.ts
|
|
360
|
+
var TaskQueue = class {
|
|
361
|
+
storage;
|
|
362
|
+
concurrency;
|
|
363
|
+
runningCount = 0;
|
|
364
|
+
listeners = /* @__PURE__ */ new Set();
|
|
365
|
+
taskResolvers = /* @__PURE__ */ new Map();
|
|
366
|
+
snapshot = [];
|
|
367
|
+
notifyScheduled = false;
|
|
368
|
+
processors = /* @__PURE__ */ new Map();
|
|
369
|
+
cancelFunctions = /* @__PURE__ */ new Map();
|
|
370
|
+
onCompleteCallbacks = /* @__PURE__ */ new Map();
|
|
371
|
+
taskTimeouts = /* @__PURE__ */ new Map();
|
|
372
|
+
_activeSummary = null;
|
|
373
|
+
_summaryRenderContent;
|
|
374
|
+
constructor(config = {}) {
|
|
375
|
+
this.concurrency = config.concurrency ?? TASK_QUEUE_DEFAULTS.concurrency;
|
|
376
|
+
this.storage = config.storage ?? detectStorage({
|
|
377
|
+
redisClient: config.redisClient,
|
|
378
|
+
storageKey: config.storageKey,
|
|
379
|
+
storageType: config.storageType
|
|
380
|
+
});
|
|
381
|
+
this._summaryRenderContent = config.summaryRenderContent;
|
|
382
|
+
this.updateSnapshot();
|
|
383
|
+
}
|
|
384
|
+
subscribe = (listener) => {
|
|
385
|
+
this.listeners.add(listener);
|
|
386
|
+
return () => {
|
|
387
|
+
this.listeners.delete(listener);
|
|
388
|
+
};
|
|
389
|
+
};
|
|
390
|
+
getSnapshot = () => {
|
|
391
|
+
return this.snapshot;
|
|
392
|
+
};
|
|
393
|
+
notify() {
|
|
394
|
+
if (this.notifyScheduled) return;
|
|
395
|
+
this.notifyScheduled = true;
|
|
396
|
+
queueMicrotask(() => {
|
|
397
|
+
this.notifyScheduled = false;
|
|
398
|
+
this.updateSnapshot();
|
|
399
|
+
this.listeners.forEach((listener) => listener());
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
updateSnapshot() {
|
|
403
|
+
this.snapshot = this.storage.getAll();
|
|
404
|
+
}
|
|
405
|
+
enqueue = (options) => {
|
|
406
|
+
const id = generateTaskId();
|
|
407
|
+
const items = options.items;
|
|
408
|
+
let processor;
|
|
409
|
+
if ("processItem" in options && options.processItem) processor = this.buildProcessor(options);
|
|
410
|
+
else if ("processor" in options && options.processor) processor = options.processor;
|
|
411
|
+
else throw new Error("[TaskQueue] enqueue: must provide either processor or processItem");
|
|
412
|
+
const cancelable = options.cancelable ?? TASK_QUEUE_DEFAULTS.cancelable;
|
|
413
|
+
const confirmBeforeUnload = options.confirmBeforeUnload ?? true;
|
|
414
|
+
const task = {
|
|
415
|
+
id,
|
|
416
|
+
title: options.title,
|
|
417
|
+
status: "pending",
|
|
418
|
+
icon: options.icon,
|
|
419
|
+
category: options.category,
|
|
420
|
+
metadata: options.metadata,
|
|
421
|
+
items,
|
|
422
|
+
total: items?.length,
|
|
423
|
+
completed: 0,
|
|
424
|
+
failed: 0,
|
|
425
|
+
succeededItems: [],
|
|
426
|
+
failedItems: [],
|
|
427
|
+
errorStrategy: options.errorStrategy ?? TASK_QUEUE_DEFAULTS.errorStrategy,
|
|
428
|
+
cancelable,
|
|
429
|
+
retryable: options.retryable ?? TASK_QUEUE_DEFAULTS.retryable,
|
|
430
|
+
confirmBeforeUnload,
|
|
431
|
+
completionActions: options.completionActions,
|
|
432
|
+
retryCount: 0,
|
|
433
|
+
createdAt: Date.now(),
|
|
434
|
+
_processor: processor,
|
|
435
|
+
_originalItems: items ? [...items] : void 0
|
|
436
|
+
};
|
|
437
|
+
this.processors.set(id, processor);
|
|
438
|
+
if (options.onComplete) this.onCompleteCallbacks.set(id, options.onComplete);
|
|
439
|
+
this.storage.set(id, task);
|
|
440
|
+
this.notify();
|
|
441
|
+
const timeout = options.timeout ?? 3e5;
|
|
442
|
+
const timeoutId = setTimeout(() => {
|
|
443
|
+
this.handleTaskTimeout(id);
|
|
444
|
+
}, timeout);
|
|
445
|
+
this.taskTimeouts.set(id, timeoutId);
|
|
446
|
+
const promise = new Promise((resolve) => {
|
|
447
|
+
this.taskResolvers.set(id, resolve);
|
|
448
|
+
});
|
|
449
|
+
this.drain();
|
|
450
|
+
return {
|
|
451
|
+
id,
|
|
452
|
+
cancel: () => this.cancel(id),
|
|
453
|
+
promise
|
|
454
|
+
};
|
|
455
|
+
};
|
|
456
|
+
cancel = (taskId) => {
|
|
457
|
+
const task = this.storage.get(taskId);
|
|
458
|
+
if (!task || task.status !== "running") return;
|
|
459
|
+
const setCancelled = this.cancelFunctions.get(taskId);
|
|
460
|
+
if (setCancelled) setCancelled(true);
|
|
461
|
+
};
|
|
462
|
+
retry = (taskId) => {
|
|
463
|
+
const task = this.storage.get(taskId);
|
|
464
|
+
if (!task) return;
|
|
465
|
+
if (task.status !== "failed" && task.status !== "cancelled") return;
|
|
466
|
+
const processor = this.processors.get(taskId);
|
|
467
|
+
if (!processor) {
|
|
468
|
+
console.error("[TaskQueue] retry: no processor found for task", taskId);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (task.items && task.items.length > 0) {
|
|
472
|
+
const remainingItems = this.getRetryItems(task);
|
|
473
|
+
if (!remainingItems || remainingItems.length === 0) {
|
|
474
|
+
const succeededItems = task.succeededItems ?? [];
|
|
475
|
+
const originalItems = task._originalItems ?? task.items ?? [];
|
|
476
|
+
if (succeededItems.length >= originalItems.length || task.completed >= (task.total ?? 0)) {
|
|
477
|
+
task.status = "completed";
|
|
478
|
+
task.completedAt = Date.now();
|
|
479
|
+
this.storage.set(taskId, task);
|
|
480
|
+
this.notify();
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
console.warn("[TaskQueue] retry: no remaining items for task", taskId);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
task.status = "pending";
|
|
487
|
+
task.items = remainingItems;
|
|
488
|
+
task.failedItems = [];
|
|
489
|
+
task.failed = 0;
|
|
490
|
+
task.retryCount += 1;
|
|
491
|
+
} else {
|
|
492
|
+
task.status = "pending";
|
|
493
|
+
task.completed = 0;
|
|
494
|
+
task.failed = 0;
|
|
495
|
+
task.succeededItems = [];
|
|
496
|
+
task.failedItems = [];
|
|
497
|
+
task.result = void 0;
|
|
498
|
+
task.retryCount += 1;
|
|
499
|
+
}
|
|
500
|
+
task.startedAt = void 0;
|
|
501
|
+
task.completedAt = void 0;
|
|
502
|
+
this.processors.set(taskId, processor);
|
|
503
|
+
this.storage.set(taskId, task);
|
|
504
|
+
this.notify();
|
|
505
|
+
this.drain();
|
|
506
|
+
};
|
|
507
|
+
getRetryItems(task) {
|
|
508
|
+
const originalItems = task._originalItems ?? task.items;
|
|
509
|
+
if (!originalItems || originalItems.length === 0) return void 0;
|
|
510
|
+
const succeededItems = task.succeededItems ?? [];
|
|
511
|
+
const failedItems = task.failedItems ?? [];
|
|
512
|
+
if (task.status === "cancelled" && succeededItems.length > 0) {
|
|
513
|
+
const remaining = originalItems.filter((item) => {
|
|
514
|
+
const itemId = this.resolveItemId(item);
|
|
515
|
+
if (!itemId) return true;
|
|
516
|
+
return !succeededItems.includes(itemId);
|
|
517
|
+
});
|
|
518
|
+
return remaining.length > 0 ? remaining : void 0;
|
|
519
|
+
}
|
|
520
|
+
if (task.status === "failed" && failedItems.length > 0) {
|
|
521
|
+
const failed = originalItems.filter((item) => {
|
|
522
|
+
const itemId = this.resolveItemId(item);
|
|
523
|
+
if (!itemId) return false;
|
|
524
|
+
return failedItems.some((fi) => fi.id === itemId);
|
|
525
|
+
});
|
|
526
|
+
return failed.length > 0 ? failed : void 0;
|
|
527
|
+
}
|
|
528
|
+
return originalItems;
|
|
529
|
+
}
|
|
530
|
+
/** Extract ID from an item using custom extractor or shared utility */
|
|
531
|
+
resolveItemId(item, getItemId) {
|
|
532
|
+
if (getItemId) return getItemId(item);
|
|
533
|
+
const id = extractItemId(item);
|
|
534
|
+
if (id !== void 0) return id;
|
|
535
|
+
try {
|
|
536
|
+
return JSON.stringify(item);
|
|
537
|
+
} catch {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
dismiss = (taskId) => {
|
|
542
|
+
const task = this.storage.get(taskId);
|
|
543
|
+
if (!task) return;
|
|
544
|
+
if (task.status === "running" || task.status === "pending") return;
|
|
545
|
+
this.storage.remove(taskId);
|
|
546
|
+
this.processors.delete(taskId);
|
|
547
|
+
this.onCompleteCallbacks.delete(taskId);
|
|
548
|
+
this.notify();
|
|
549
|
+
};
|
|
550
|
+
dismissAll = () => {
|
|
551
|
+
this.storage.getAll().forEach((task) => {
|
|
552
|
+
if (task.status !== "running" && task.status !== "pending") {
|
|
553
|
+
this.storage.remove(task.id);
|
|
554
|
+
this.processors.delete(task.id);
|
|
555
|
+
this.onCompleteCallbacks.delete(task.id);
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
this.notify();
|
|
559
|
+
};
|
|
560
|
+
showSummary = (title, items, options) => {
|
|
561
|
+
this._activeSummary = {
|
|
562
|
+
title,
|
|
563
|
+
items,
|
|
564
|
+
renderContent: options?.renderContent
|
|
565
|
+
};
|
|
566
|
+
this.notify();
|
|
567
|
+
};
|
|
568
|
+
closeSummary = () => {
|
|
569
|
+
this._activeSummary = null;
|
|
570
|
+
this.notify();
|
|
571
|
+
};
|
|
572
|
+
getActiveSummary = () => {
|
|
573
|
+
return this._activeSummary;
|
|
574
|
+
};
|
|
575
|
+
getSummaryRenderContent = () => {
|
|
576
|
+
return this._summaryRenderContent;
|
|
577
|
+
};
|
|
578
|
+
buildProcessor(options) {
|
|
579
|
+
const { processItem, itemConcurrency = 1, getItemId, errorStrategy = "continue" } = options;
|
|
580
|
+
return async (ctx) => {
|
|
581
|
+
const pending = [...ctx.items];
|
|
582
|
+
const inFlight = [];
|
|
583
|
+
while (pending.length > 0 || inFlight.length > 0) {
|
|
584
|
+
if (ctx.cancelled) break;
|
|
585
|
+
if (errorStrategy === "stop" && ctx.failedItems.length > 0) break;
|
|
586
|
+
while (inFlight.length < itemConcurrency && pending.length > 0) {
|
|
587
|
+
const item = pending.shift();
|
|
588
|
+
const itemId = this.resolveItemId(item, getItemId) ?? String(item);
|
|
589
|
+
const promise = this.processOneItem(item, itemId, processItem, ctx).finally(() => {
|
|
590
|
+
const idx = inFlight.indexOf(promise);
|
|
591
|
+
if (idx !== -1) inFlight.splice(idx, 1);
|
|
592
|
+
});
|
|
593
|
+
inFlight.push(promise);
|
|
594
|
+
}
|
|
595
|
+
if (inFlight.length > 0) await Promise.race(inFlight);
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
async processOneItem(item, itemId, processItem, ctx) {
|
|
600
|
+
let manuallyHandled = false;
|
|
601
|
+
const itemCtx = {
|
|
602
|
+
get cancelled() {
|
|
603
|
+
return ctx.cancelled;
|
|
604
|
+
},
|
|
605
|
+
succeed: (id) => {
|
|
606
|
+
manuallyHandled = true;
|
|
607
|
+
ctx.succeed(id ?? itemId);
|
|
608
|
+
},
|
|
609
|
+
fail: (id, message) => {
|
|
610
|
+
manuallyHandled = true;
|
|
611
|
+
ctx.fail(id ?? itemId, message);
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
try {
|
|
615
|
+
await processItem(item, itemCtx);
|
|
616
|
+
if (!manuallyHandled) ctx.succeed(itemId);
|
|
617
|
+
} catch (error) {
|
|
618
|
+
if (!manuallyHandled) {
|
|
619
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
620
|
+
ctx.fail(itemId, message);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
drain() {
|
|
625
|
+
const pending = this.storage.getAll().filter((t) => t.status === "pending");
|
|
626
|
+
while (this.runningCount < this.concurrency && pending.length > 0) {
|
|
627
|
+
const next = pending.shift();
|
|
628
|
+
if (!next) break;
|
|
629
|
+
this.runTask(next);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
async runTask(task) {
|
|
633
|
+
this.runningCount += 1;
|
|
634
|
+
const processor = this.processors.get(task.id);
|
|
635
|
+
if (!processor) {
|
|
636
|
+
console.error("[TaskQueue] No processor found for task", task.id);
|
|
637
|
+
this.runningCount -= 1;
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
task._processor = processor;
|
|
641
|
+
try {
|
|
642
|
+
const outcome = await executeTask(task, {
|
|
643
|
+
onUpdate: (updated) => {
|
|
644
|
+
this.storage.set(updated.id, updated);
|
|
645
|
+
this.notify();
|
|
646
|
+
},
|
|
647
|
+
onCancelReady: (setCancelled) => {
|
|
648
|
+
this.cancelFunctions.set(task.id, setCancelled);
|
|
649
|
+
},
|
|
650
|
+
onTaskComplete: () => {
|
|
651
|
+
this.clearTaskTimeout(task.id);
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
const onComplete = this.onCompleteCallbacks.get(task.id);
|
|
655
|
+
if (onComplete) try {
|
|
656
|
+
await onComplete(outcome);
|
|
657
|
+
} catch (error) {
|
|
658
|
+
console.error("[TaskQueue] onComplete callback error:", error);
|
|
659
|
+
}
|
|
660
|
+
const resolver = this.taskResolvers.get(task.id);
|
|
661
|
+
if (resolver) {
|
|
662
|
+
resolver(outcome);
|
|
663
|
+
this.taskResolvers.delete(task.id);
|
|
664
|
+
}
|
|
665
|
+
if (outcome.status === "completed") {
|
|
666
|
+
this.processors.delete(task.id);
|
|
667
|
+
this.onCompleteCallbacks.delete(task.id);
|
|
668
|
+
}
|
|
669
|
+
} finally {
|
|
670
|
+
this.runningCount -= 1;
|
|
671
|
+
this.cancelFunctions.delete(task.id);
|
|
672
|
+
this.drain();
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
handleTaskTimeout(taskId) {
|
|
676
|
+
const task = this.storage.get(taskId);
|
|
677
|
+
if (!task || task.status !== "running") return;
|
|
678
|
+
const setCancelled = this.cancelFunctions.get(taskId);
|
|
679
|
+
if (setCancelled) setCancelled(true);
|
|
680
|
+
task.status = "failed";
|
|
681
|
+
task.completedAt = Date.now();
|
|
682
|
+
task.failedItems = [...task.failedItems, { message: "Task timeout: exceeded maximum execution time" }];
|
|
683
|
+
task.failed += 1;
|
|
684
|
+
this.storage.set(taskId, task);
|
|
685
|
+
this.notify();
|
|
686
|
+
const resolver = this.taskResolvers.get(taskId);
|
|
687
|
+
if (resolver) {
|
|
688
|
+
resolver({
|
|
689
|
+
status: "failed",
|
|
690
|
+
completed: task.completed,
|
|
691
|
+
failed: task.failed,
|
|
692
|
+
failedItems: [...task.failedItems],
|
|
693
|
+
result: task.result
|
|
694
|
+
});
|
|
695
|
+
this.taskResolvers.delete(taskId);
|
|
696
|
+
}
|
|
697
|
+
this.processors.delete(taskId);
|
|
698
|
+
this.onCompleteCallbacks.delete(taskId);
|
|
699
|
+
this.cancelFunctions.delete(taskId);
|
|
700
|
+
this.taskTimeouts.delete(taskId);
|
|
701
|
+
}
|
|
702
|
+
clearTaskTimeout(taskId) {
|
|
703
|
+
const timeoutId = this.taskTimeouts.get(taskId);
|
|
704
|
+
if (timeoutId) {
|
|
705
|
+
clearTimeout(timeoutId);
|
|
706
|
+
this.taskTimeouts.delete(taskId);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
//#endregion
|
|
712
|
+
//#region src/components/features/task-queue/provider/task-queue-provider.tsx
|
|
713
|
+
const TaskQueueContext = createContext(null);
|
|
714
|
+
function TaskQueueProvider({ children, config }) {
|
|
715
|
+
const queueRef = useRef(null);
|
|
716
|
+
if (!queueRef.current) queueRef.current = new TaskQueue(config);
|
|
717
|
+
useEffect(() => {
|
|
718
|
+
const queue = queueRef.current;
|
|
719
|
+
return () => {
|
|
720
|
+
if (!queue) return;
|
|
721
|
+
queue.getSnapshot().forEach((task) => {
|
|
722
|
+
if (task.status === "running") queue.cancel(task.id);
|
|
723
|
+
});
|
|
724
|
+
};
|
|
725
|
+
}, []);
|
|
726
|
+
useEffect(() => {
|
|
727
|
+
const handleBeforeUnload = (e) => {
|
|
728
|
+
if ((queueRef.current?.getSnapshot() ?? []).some((t) => (t.status === "running" || t.status === "pending") && t.confirmBeforeUnload !== false)) {
|
|
729
|
+
e.preventDefault();
|
|
730
|
+
e.returnValue = "Tasks in progress will be lost.";
|
|
731
|
+
return e.returnValue;
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
window.addEventListener("beforeunload", handleBeforeUnload);
|
|
735
|
+
return () => window.removeEventListener("beforeunload", handleBeforeUnload);
|
|
736
|
+
}, []);
|
|
737
|
+
return /* @__PURE__ */ jsx(TaskQueueContext, {
|
|
738
|
+
value: useMemo(() => ({ queue: queueRef.current }), []),
|
|
739
|
+
children
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
//#endregion
|
|
744
|
+
//#region src/components/features/task-queue/hooks/use-task-queue.ts
|
|
745
|
+
const EMPTY_TASKS = [];
|
|
746
|
+
function useTaskQueue(options) {
|
|
747
|
+
const context = use(TaskQueueContext);
|
|
748
|
+
if (!context) throw new Error("useTaskQueue must be used within a TaskQueueProvider");
|
|
749
|
+
const { queue } = context;
|
|
750
|
+
const allTasks = useSyncExternalStore(queue.subscribe, queue.getSnapshot, () => EMPTY_TASKS);
|
|
751
|
+
const tasks = useMemo(() => {
|
|
752
|
+
if (!options?.status) return allTasks;
|
|
753
|
+
return allTasks.filter((t) => t.status === options.status);
|
|
754
|
+
}, [allTasks, options?.status]);
|
|
755
|
+
const activeSummary = useSyncExternalStore(queue.subscribe, queue.getActiveSummary, () => null);
|
|
756
|
+
const summaryRenderContent = queue.getSummaryRenderContent();
|
|
757
|
+
return useMemo(() => ({
|
|
758
|
+
enqueue: queue.enqueue,
|
|
759
|
+
cancel: queue.cancel,
|
|
760
|
+
retry: queue.retry,
|
|
761
|
+
dismiss: queue.dismiss,
|
|
762
|
+
dismissAll: queue.dismissAll,
|
|
763
|
+
showSummary: queue.showSummary,
|
|
764
|
+
closeSummary: queue.closeSummary,
|
|
765
|
+
activeSummary,
|
|
766
|
+
summaryRenderContent,
|
|
767
|
+
tasks
|
|
768
|
+
}), [
|
|
769
|
+
tasks,
|
|
770
|
+
activeSummary,
|
|
771
|
+
summaryRenderContent
|
|
772
|
+
]);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
//#endregion
|
|
776
|
+
//#region src/components/features/task-queue/hooks/use-task-scope.ts
|
|
777
|
+
/**
|
|
778
|
+
* Detect the current scope from URL params.
|
|
779
|
+
* Returns project scope if projectId is present, org scope if only orgId, otherwise global.
|
|
780
|
+
*
|
|
781
|
+
* @param params - Route params (e.g. from useParams() in react-router)
|
|
782
|
+
*/
|
|
783
|
+
function useCurrentScope(params) {
|
|
784
|
+
return useMemo(() => {
|
|
785
|
+
if (params.projectId) return {
|
|
786
|
+
type: "project",
|
|
787
|
+
projectId: params.projectId,
|
|
788
|
+
orgId: params.orgId
|
|
789
|
+
};
|
|
790
|
+
if (params.orgId) return {
|
|
791
|
+
type: "org",
|
|
792
|
+
orgId: params.orgId
|
|
793
|
+
};
|
|
794
|
+
return { type: "global" };
|
|
795
|
+
}, [params.projectId, params.orgId]);
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Check if task metadata matches the current scope.
|
|
799
|
+
*/
|
|
800
|
+
function matchesCurrentScope(metadata, scope) {
|
|
801
|
+
if (!metadata) return false;
|
|
802
|
+
if (scope.type === "project") return metadata.projectId === scope.projectId;
|
|
803
|
+
if (scope.type === "org") return metadata.orgId === scope.orgId && !metadata.projectId;
|
|
804
|
+
return false;
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Get the context label to display for a task.
|
|
808
|
+
* Returns formatted label with scope type: "Project: Name", "Org: Name", etc.
|
|
809
|
+
*/
|
|
810
|
+
function getContextLabel(metadata) {
|
|
811
|
+
if (!metadata) return void 0;
|
|
812
|
+
const scope = metadata.scope;
|
|
813
|
+
if (scope === "project" && metadata.projectName) return `Project: ${metadata.projectName}`;
|
|
814
|
+
if (scope === "org" && metadata.orgName) return `Org: ${metadata.orgName}`;
|
|
815
|
+
if (scope === "user") return "User";
|
|
816
|
+
if (scope === "edge" && metadata.projectName) return `AI Edge: ${metadata.projectName}`;
|
|
817
|
+
if (metadata.projectName) return `Project: ${metadata.projectName}`;
|
|
818
|
+
if (metadata.orgName) return `Org: ${metadata.orgName}`;
|
|
819
|
+
if (scope) return scope.charAt(0).toUpperCase() + scope.slice(1);
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* Hook that returns tasks with a flag indicating whether to show context labels.
|
|
823
|
+
* Labels are shown when:
|
|
824
|
+
* - User is on a global page (account, etc.)
|
|
825
|
+
* - Tasks are from mixed scopes (not all match current scope)
|
|
826
|
+
*
|
|
827
|
+
* @param tasks - Array of tasks to evaluate
|
|
828
|
+
* @param params - Route params (e.g. from useParams() in react-router)
|
|
829
|
+
*/
|
|
830
|
+
function useTasksWithLabels(tasks, params) {
|
|
831
|
+
const currentScope = useCurrentScope(params);
|
|
832
|
+
return {
|
|
833
|
+
tasks,
|
|
834
|
+
showLabels: useMemo(() => {
|
|
835
|
+
if (tasks.length === 0) return false;
|
|
836
|
+
if (currentScope.type === "global") return true;
|
|
837
|
+
return !tasks.every((task) => matchesCurrentScope(task.metadata, currentScope));
|
|
838
|
+
}, [tasks, currentScope])
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
//#endregion
|
|
843
|
+
//#region src/components/features/task-queue/core/task-panel-header.tsx
|
|
844
|
+
function TaskPanelHeader() {
|
|
845
|
+
return /* @__PURE__ */ jsx("div", {
|
|
846
|
+
className: "border-border flex items-center border-b px-4 py-3",
|
|
847
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
848
|
+
className: "text-sm font-medium",
|
|
849
|
+
children: "Tasks"
|
|
850
|
+
})
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
//#endregion
|
|
855
|
+
//#region src/components/features/task-queue/core/task-panel-actions.tsx
|
|
856
|
+
function TaskPanelActions({ task }) {
|
|
857
|
+
const { showSummary } = useTaskQueue();
|
|
858
|
+
const actions = resolveActions(task);
|
|
859
|
+
const hasBatch = task.total != null;
|
|
860
|
+
const hasFailedMessages = task.failedItems.length > 0 && task.failedItems.some((f) => f.message);
|
|
861
|
+
if (!hasBatch && task.status === "failed" && hasFailedMessages && actions.length === 0) return /* @__PURE__ */ jsx("div", {
|
|
862
|
+
className: "flex items-center justify-end gap-1.5",
|
|
863
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
864
|
+
htmlType: "button",
|
|
865
|
+
type: "quaternary",
|
|
866
|
+
theme: "outline",
|
|
867
|
+
size: "xs",
|
|
868
|
+
onClick: () => showSummary(task.title, task.failedItems.map((item, i) => ({
|
|
869
|
+
id: item.id ?? `error-${i}`,
|
|
870
|
+
label: item.id ?? "Error",
|
|
871
|
+
status: "failed",
|
|
872
|
+
message: item.message
|
|
873
|
+
}))),
|
|
874
|
+
children: "Details"
|
|
875
|
+
})
|
|
876
|
+
});
|
|
877
|
+
if (actions.length === 0) return null;
|
|
878
|
+
return /* @__PURE__ */ jsx("div", {
|
|
879
|
+
className: "flex items-center justify-end gap-1.5",
|
|
880
|
+
children: actions.map((action, i) => /* @__PURE__ */ jsx(Button, {
|
|
881
|
+
htmlType: "button",
|
|
882
|
+
...action
|
|
883
|
+
}, i))
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
const TERMINAL_STATUSES = [
|
|
887
|
+
"completed",
|
|
888
|
+
"failed",
|
|
889
|
+
"cancelled"
|
|
890
|
+
];
|
|
891
|
+
function resolveActions(task) {
|
|
892
|
+
if (!task.completionActions) return [];
|
|
893
|
+
if (!TERMINAL_STATUSES.includes(task.status)) return [];
|
|
894
|
+
if (typeof task.completionActions === "function") {
|
|
895
|
+
const originalItems = task._originalItems ?? task.items ?? [];
|
|
896
|
+
const failedMap = new Map(task.failedItems.filter((f) => f.id).map((f) => [f.id, f]));
|
|
897
|
+
const succeededSet = new Set(task.succeededItems);
|
|
898
|
+
const items = originalItems.map((item) => {
|
|
899
|
+
const id = extractItemId(item);
|
|
900
|
+
if (!id) return null;
|
|
901
|
+
const failed = failedMap.get(id);
|
|
902
|
+
if (failed) return {
|
|
903
|
+
id,
|
|
904
|
+
status: "failed",
|
|
905
|
+
message: failed.message,
|
|
906
|
+
data: item
|
|
907
|
+
};
|
|
908
|
+
if (succeededSet.has(id)) return {
|
|
909
|
+
id,
|
|
910
|
+
status: "succeeded",
|
|
911
|
+
data: item
|
|
912
|
+
};
|
|
913
|
+
return null;
|
|
914
|
+
}).filter((item) => item !== null);
|
|
915
|
+
return task.completionActions(task.result, {
|
|
916
|
+
status: task.status,
|
|
917
|
+
completed: task.completed,
|
|
918
|
+
failed: task.failed,
|
|
919
|
+
items
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
return task.completionActions;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
//#endregion
|
|
926
|
+
//#region src/components/features/task-queue/core/task-panel-counter.tsx
|
|
927
|
+
function TaskPanelCounter({ total, completed, failed = 0, status }) {
|
|
928
|
+
const hasBatch = total != null;
|
|
929
|
+
const parts = [];
|
|
930
|
+
if (status === "completed") parts.push({ text: "Completed" });
|
|
931
|
+
else if (status === "failed") if (hasBatch) {
|
|
932
|
+
if (completed > 0) parts.push({ text: `${completed} completed` });
|
|
933
|
+
parts.push({
|
|
934
|
+
text: `${failed} failed`,
|
|
935
|
+
destructive: true
|
|
936
|
+
});
|
|
937
|
+
} else parts.push({
|
|
938
|
+
text: "Failed",
|
|
939
|
+
destructive: true
|
|
940
|
+
});
|
|
941
|
+
else if (status === "cancelled") {
|
|
942
|
+
parts.push({ text: "Cancelled" });
|
|
943
|
+
if (hasBatch && completed > 0) parts.push({ text: `${completed} completed` });
|
|
944
|
+
} else if (hasBatch) {
|
|
945
|
+
parts.push({ text: `${completed + failed}/${total}` });
|
|
946
|
+
if (failed > 0) parts.push({
|
|
947
|
+
text: `${failed} failed`,
|
|
948
|
+
destructive: true
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
if (parts.length === 0) return null;
|
|
952
|
+
return /* @__PURE__ */ jsx("span", {
|
|
953
|
+
className: "text-muted-foreground text-xs",
|
|
954
|
+
children: parts.map((part, i) => /* @__PURE__ */ jsxs("span", { children: [i > 0 && ", ", /* @__PURE__ */ jsx("span", {
|
|
955
|
+
className: cn(part.destructive && "text-destructive"),
|
|
956
|
+
children: part.text
|
|
957
|
+
})] }, i))
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
//#endregion
|
|
962
|
+
//#region src/components/features/task-queue/core/task-panel-item.tsx
|
|
963
|
+
function TaskPanelItem({ task, contextLabel, onCancel }) {
|
|
964
|
+
const hasBatch = task.total != null;
|
|
965
|
+
const isTerminal = task.status === "completed" || task.status === "failed" || task.status === "cancelled";
|
|
966
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
967
|
+
className: "group border-border flex flex-col gap-1 px-4 py-3 not-first:border-t",
|
|
968
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
969
|
+
className: "flex items-start gap-3",
|
|
970
|
+
children: [
|
|
971
|
+
/* @__PURE__ */ jsx("div", {
|
|
972
|
+
className: "mt-0.5 flex shrink-0 items-center",
|
|
973
|
+
children: /* @__PURE__ */ jsx(TaskIcon, { task })
|
|
974
|
+
}),
|
|
975
|
+
/* @__PURE__ */ jsxs("div", {
|
|
976
|
+
className: "flex min-w-0 flex-1 flex-col gap-0.5",
|
|
977
|
+
children: [
|
|
978
|
+
/* @__PURE__ */ jsx("span", {
|
|
979
|
+
className: "truncate text-sm font-medium",
|
|
980
|
+
children: task.title
|
|
981
|
+
}),
|
|
982
|
+
task.status === "pending" && /* @__PURE__ */ jsx("span", {
|
|
983
|
+
className: "text-muted-foreground text-xs",
|
|
984
|
+
children: "Waiting..."
|
|
985
|
+
}),
|
|
986
|
+
hasBatch && task.status === "running" && /* @__PURE__ */ jsx(TaskPanelCounter, {
|
|
987
|
+
total: task.total,
|
|
988
|
+
completed: task.completed,
|
|
989
|
+
failed: task.failed
|
|
990
|
+
}),
|
|
991
|
+
isTerminal && /* @__PURE__ */ jsx(TaskPanelCounter, {
|
|
992
|
+
total: task.total,
|
|
993
|
+
completed: task.completed,
|
|
994
|
+
failed: task.failed,
|
|
995
|
+
status: task.status
|
|
996
|
+
}),
|
|
997
|
+
contextLabel && /* @__PURE__ */ jsxs("span", {
|
|
998
|
+
className: "text-muted-foreground flex items-center gap-1 text-xs",
|
|
999
|
+
children: [/* @__PURE__ */ jsx(Icon, {
|
|
1000
|
+
icon: CornerDownRightIcon,
|
|
1001
|
+
className: "size-3 shrink-0 opacity-60"
|
|
1002
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
1003
|
+
className: "truncate",
|
|
1004
|
+
children: contextLabel
|
|
1005
|
+
})]
|
|
1006
|
+
})
|
|
1007
|
+
]
|
|
1008
|
+
}),
|
|
1009
|
+
/* @__PURE__ */ jsx("div", {
|
|
1010
|
+
className: "flex shrink-0 items-center",
|
|
1011
|
+
children: /* @__PURE__ */ jsx(TaskStatusAction, {
|
|
1012
|
+
task,
|
|
1013
|
+
onCancel
|
|
1014
|
+
})
|
|
1015
|
+
})
|
|
1016
|
+
]
|
|
1017
|
+
}), /* @__PURE__ */ jsx(TaskPanelActions, { task })]
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
/** Left-side task icon - shows task icon when running, status icon when complete */
|
|
1021
|
+
function TaskIcon({ task }) {
|
|
1022
|
+
if (task.status === "running" || task.status === "pending") {
|
|
1023
|
+
if (task.icon) return /* @__PURE__ */ jsx("span", {
|
|
1024
|
+
className: "text-muted-foreground [&>svg]:size-4",
|
|
1025
|
+
children: task.icon
|
|
1026
|
+
});
|
|
1027
|
+
return /* @__PURE__ */ jsx(Icon, {
|
|
1028
|
+
icon: FileIcon,
|
|
1029
|
+
className: "text-muted-foreground size-4"
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
if (task.status === "completed" && task.failed > 0) return /* @__PURE__ */ jsx(Icon, {
|
|
1033
|
+
icon: CircleAlert,
|
|
1034
|
+
className: "size-4 text-amber-500"
|
|
1035
|
+
});
|
|
1036
|
+
if (task.status === "completed") return /* @__PURE__ */ jsx(Icon, {
|
|
1037
|
+
icon: CircleCheck,
|
|
1038
|
+
className: "size-4 text-green-600 dark:text-green-400"
|
|
1039
|
+
});
|
|
1040
|
+
if (task.status === "failed") return /* @__PURE__ */ jsx(Icon, {
|
|
1041
|
+
icon: XCircle,
|
|
1042
|
+
className: "text-destructive size-4"
|
|
1043
|
+
});
|
|
1044
|
+
if (task.status === "cancelled") return /* @__PURE__ */ jsx(Icon, {
|
|
1045
|
+
icon: Ban,
|
|
1046
|
+
className: "text-muted-foreground size-4"
|
|
1047
|
+
});
|
|
1048
|
+
return /* @__PURE__ */ jsx(Icon, {
|
|
1049
|
+
icon: FileIcon,
|
|
1050
|
+
className: "text-muted-foreground size-4"
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
/** Format completedAt timestamp as human-readable relative time */
|
|
1054
|
+
function formatCompletedAt(completedAt) {
|
|
1055
|
+
if (!completedAt) return "";
|
|
1056
|
+
if (Date.now() - completedAt < 1e3) return "just now";
|
|
1057
|
+
return `${formatDistanceToNowStrict(new Date(completedAt), { addSuffix: false })} ago`;
|
|
1058
|
+
}
|
|
1059
|
+
/** Right-side: spinner/cancel for active tasks, timestamp for terminal tasks */
|
|
1060
|
+
function TaskStatusAction({ task, onCancel }) {
|
|
1061
|
+
if (task.status === "running") return /* @__PURE__ */ jsxs("div", {
|
|
1062
|
+
className: "relative size-5",
|
|
1063
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
1064
|
+
className: "pointer-events-none absolute inset-0 flex items-center justify-center transition-opacity group-hover:opacity-0",
|
|
1065
|
+
children: /* @__PURE__ */ jsx(SpinnerIcon, { size: "sm" })
|
|
1066
|
+
}), task.cancelable && /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
|
|
1067
|
+
asChild: true,
|
|
1068
|
+
children: /* @__PURE__ */ jsx("button", {
|
|
1069
|
+
type: "button",
|
|
1070
|
+
onClick: onCancel,
|
|
1071
|
+
className: cn("flex size-5 items-center justify-center rounded-md transition-colors", "text-muted-foreground hover:bg-accent hover:text-foreground", "absolute inset-0 opacity-0 transition-opacity group-hover:opacity-100"),
|
|
1072
|
+
"aria-label": "Cancel task",
|
|
1073
|
+
children: /* @__PURE__ */ jsx(Icon, {
|
|
1074
|
+
icon: X,
|
|
1075
|
+
className: "size-4"
|
|
1076
|
+
})
|
|
1077
|
+
})
|
|
1078
|
+
}), /* @__PURE__ */ jsx(TooltipContent, {
|
|
1079
|
+
side: "left",
|
|
1080
|
+
children: "Cancel"
|
|
1081
|
+
})] })]
|
|
1082
|
+
});
|
|
1083
|
+
if (task.status === "pending") return /* @__PURE__ */ jsx("div", {
|
|
1084
|
+
className: "flex size-5 items-center justify-center",
|
|
1085
|
+
children: /* @__PURE__ */ jsx(SpinnerIcon, {
|
|
1086
|
+
size: "sm",
|
|
1087
|
+
className: "opacity-50"
|
|
1088
|
+
})
|
|
1089
|
+
});
|
|
1090
|
+
return /* @__PURE__ */ jsx("span", {
|
|
1091
|
+
className: "text-muted-foreground/60 text-xs whitespace-nowrap",
|
|
1092
|
+
children: formatCompletedAt(task.completedAt)
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
//#endregion
|
|
1097
|
+
//#region src/components/features/task-queue/core/task-panel.tsx
|
|
1098
|
+
/**
|
|
1099
|
+
* @deprecated Use TaskQueueDropdown instead — renders as a header-anchored dropdown.
|
|
1100
|
+
* TaskPanel is kept for backward compatibility but is no longer used in the main layout.
|
|
1101
|
+
*/
|
|
1102
|
+
function TaskPanel() {
|
|
1103
|
+
const { tasks, cancel } = useTaskQueue();
|
|
1104
|
+
if (tasks.length === 0) return null;
|
|
1105
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1106
|
+
className: cn("bg-background fixed right-4 bottom-4 z-50 w-96 overflow-hidden", "border-border/50 rounded-xl border shadow-xl shadow-black/10", "animate-in slide-in-from-bottom-4 fade-in duration-200"),
|
|
1107
|
+
children: [/* @__PURE__ */ jsx(TaskPanelHeader, {}), /* @__PURE__ */ jsx("div", {
|
|
1108
|
+
className: "max-h-80 overflow-y-auto",
|
|
1109
|
+
children: tasks.map((task) => /* @__PURE__ */ jsx(TaskPanelItem, {
|
|
1110
|
+
task,
|
|
1111
|
+
contextLabel: getContextLabel(task.metadata),
|
|
1112
|
+
onCancel: () => cancel(task.id)
|
|
1113
|
+
}, task.id))
|
|
1114
|
+
})]
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
//#endregion
|
|
1119
|
+
//#region src/components/features/task-queue/core/task-queue-trigger.tsx
|
|
1120
|
+
function TaskQueueTrigger({ ref, tasks, ...props }) {
|
|
1121
|
+
const runningCount = tasks.filter((t) => t.status === "running").length;
|
|
1122
|
+
const activeCount = runningCount + tasks.filter((t) => t.status === "pending").length;
|
|
1123
|
+
const hasRunning = runningCount > 0;
|
|
1124
|
+
const isAllComplete = tasks.length > 0 && activeCount === 0 && tasks.every((t) => t.status === "completed" || t.status === "failed" || t.status === "cancelled");
|
|
1125
|
+
const [flash, setFlash] = useState(false);
|
|
1126
|
+
const prevAllComplete = useRef(false);
|
|
1127
|
+
useEffect(() => {
|
|
1128
|
+
if (isAllComplete && !prevAllComplete.current) {
|
|
1129
|
+
prevAllComplete.current = true;
|
|
1130
|
+
setFlash(true);
|
|
1131
|
+
const timer = setTimeout(() => setFlash(false), 5e3);
|
|
1132
|
+
return () => clearTimeout(timer);
|
|
1133
|
+
}
|
|
1134
|
+
if (!isAllComplete) prevAllComplete.current = false;
|
|
1135
|
+
}, [isAllComplete]);
|
|
1136
|
+
useEffect(() => {
|
|
1137
|
+
if (tasks.length === 0) {
|
|
1138
|
+
setFlash(false);
|
|
1139
|
+
prevAllComplete.current = false;
|
|
1140
|
+
}
|
|
1141
|
+
}, [tasks.length]);
|
|
1142
|
+
return /* @__PURE__ */ jsx(Tooltip$1, {
|
|
1143
|
+
message: "Tasks",
|
|
1144
|
+
children: /* @__PURE__ */ jsxs(Button, {
|
|
1145
|
+
ref,
|
|
1146
|
+
type: "quaternary",
|
|
1147
|
+
theme: "borderless",
|
|
1148
|
+
size: "small",
|
|
1149
|
+
className: cn("hover:bg-sidebar-accent relative h-7 w-7 rounded-lg p-0 transition-colors duration-300", flash && "bg-primary/10"),
|
|
1150
|
+
"aria-label": `Tasks${activeCount > 0 ? ` (${activeCount} active)` : ""}`,
|
|
1151
|
+
...props,
|
|
1152
|
+
children: [
|
|
1153
|
+
/* @__PURE__ */ jsx(Icon, {
|
|
1154
|
+
icon: ListTodo,
|
|
1155
|
+
className: cn("text-icon-header size-4", flash ? "text-primary" : "text-icon-header")
|
|
1156
|
+
}),
|
|
1157
|
+
hasRunning && /* @__PURE__ */ jsx("span", { className: "border-t-primary pointer-events-none absolute inset-[-3px] animate-spin rounded-full border-2 border-transparent" }),
|
|
1158
|
+
activeCount > 0 && /* @__PURE__ */ jsx(Badge, {
|
|
1159
|
+
type: "tertiary",
|
|
1160
|
+
theme: "solid",
|
|
1161
|
+
className: "bg-primary text-primary-foreground text-2xs absolute -top-1 -right-1 flex size-4 items-center justify-center rounded-full p-0 leading-0",
|
|
1162
|
+
children: activeCount > 99 ? "99+" : activeCount
|
|
1163
|
+
})
|
|
1164
|
+
]
|
|
1165
|
+
})
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
//#endregion
|
|
1170
|
+
//#region src/components/features/task-queue/core/task-summary-dialog.tsx
|
|
1171
|
+
function getStatusConfig(status) {
|
|
1172
|
+
switch (status) {
|
|
1173
|
+
case "success": return {
|
|
1174
|
+
icon: CircleCheck,
|
|
1175
|
+
label: "Success",
|
|
1176
|
+
className: "text-green-600"
|
|
1177
|
+
};
|
|
1178
|
+
case "failed": return {
|
|
1179
|
+
icon: XCircle,
|
|
1180
|
+
label: "Failed",
|
|
1181
|
+
className: "text-destructive"
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
function StatusCell({ item }) {
|
|
1186
|
+
const config = getStatusConfig(item.status);
|
|
1187
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1188
|
+
className: "flex flex-col gap-0.5",
|
|
1189
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1190
|
+
className: "flex items-center gap-1.5",
|
|
1191
|
+
children: [/* @__PURE__ */ jsx(Icon, {
|
|
1192
|
+
icon: config.icon,
|
|
1193
|
+
className: cn("size-4", config.className)
|
|
1194
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
1195
|
+
className: cn("text-xs font-medium", config.className),
|
|
1196
|
+
children: config.label
|
|
1197
|
+
})]
|
|
1198
|
+
}), item.message && item.status !== "success" && /* @__PURE__ */ jsx("span", {
|
|
1199
|
+
className: "text-muted-foreground pl-5.5 text-xs text-wrap",
|
|
1200
|
+
children: item.message
|
|
1201
|
+
})]
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
function DefaultTableContent({ items }) {
|
|
1205
|
+
const sorted = useMemo(() => [...items].sort((a, b) => {
|
|
1206
|
+
if (a.status === "failed" && b.status !== "failed") return -1;
|
|
1207
|
+
if (a.status !== "failed" && b.status === "failed") return 1;
|
|
1208
|
+
return 0;
|
|
1209
|
+
}), [items]);
|
|
1210
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1211
|
+
className: "max-h-[400px] overflow-auto rounded-xl border",
|
|
1212
|
+
children: /* @__PURE__ */ jsxs(Table, { children: [/* @__PURE__ */ jsx(TableHeader, {
|
|
1213
|
+
className: "bg-muted/50 sticky top-0",
|
|
1214
|
+
children: /* @__PURE__ */ jsxs(TableRow, { children: [/* @__PURE__ */ jsx(TableHead, { children: "Item" }), /* @__PURE__ */ jsx(TableHead, {
|
|
1215
|
+
className: "max-w-80",
|
|
1216
|
+
children: "Status"
|
|
1217
|
+
})] })
|
|
1218
|
+
}), /* @__PURE__ */ jsx(TableBody, { children: sorted.length === 0 ? /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(TableCell, {
|
|
1219
|
+
colSpan: 2,
|
|
1220
|
+
className: "text-muted-foreground py-6 text-center",
|
|
1221
|
+
children: "No items"
|
|
1222
|
+
}) }) : sorted.map((item) => /* @__PURE__ */ jsxs(TableRow, { children: [/* @__PURE__ */ jsx(TableCell, {
|
|
1223
|
+
className: "font-medium",
|
|
1224
|
+
children: item.label
|
|
1225
|
+
}), /* @__PURE__ */ jsx(TableCell, {
|
|
1226
|
+
className: "max-w-80 break-all text-wrap whitespace-normal",
|
|
1227
|
+
children: /* @__PURE__ */ jsx(StatusCell, { item })
|
|
1228
|
+
})] }, item.id)) })] })
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
function useTaskSummaryItems(taskId, getItemLabel) {
|
|
1232
|
+
const { tasks } = useTaskQueue();
|
|
1233
|
+
return useMemo(() => {
|
|
1234
|
+
if (!taskId || !getItemLabel) return [];
|
|
1235
|
+
const task = tasks.find((t) => t.id === taskId);
|
|
1236
|
+
if (!task) return [];
|
|
1237
|
+
const succeeded = task.succeededItems.map((id) => ({
|
|
1238
|
+
id,
|
|
1239
|
+
label: getItemLabel(id),
|
|
1240
|
+
status: "success"
|
|
1241
|
+
}));
|
|
1242
|
+
return [...task.failedItems.map((item) => ({
|
|
1243
|
+
id: item.id ?? "",
|
|
1244
|
+
label: getItemLabel(item.id ?? ""),
|
|
1245
|
+
status: "failed",
|
|
1246
|
+
message: item.message
|
|
1247
|
+
})), ...succeeded];
|
|
1248
|
+
}, [
|
|
1249
|
+
taskId,
|
|
1250
|
+
getItemLabel,
|
|
1251
|
+
tasks
|
|
1252
|
+
]);
|
|
1253
|
+
}
|
|
1254
|
+
function TaskSummaryDialog(props) {
|
|
1255
|
+
const { open, onOpenChange, title, description, actions, renderContent } = props;
|
|
1256
|
+
const taskIdItems = useTaskSummaryItems("taskId" in props ? props.taskId : void 0, "getItemLabel" in props ? props.getItemLabel : void 0);
|
|
1257
|
+
const resolvedItems = props.items ?? taskIdItems;
|
|
1258
|
+
const successCount = resolvedItems.filter((i) => i.status === "success").length;
|
|
1259
|
+
const failedCount = resolvedItems.filter((i) => i.status === "failed").length;
|
|
1260
|
+
return /* @__PURE__ */ jsx(Dialog, {
|
|
1261
|
+
open,
|
|
1262
|
+
onOpenChange,
|
|
1263
|
+
children: /* @__PURE__ */ jsxs(Dialog.Content, {
|
|
1264
|
+
className: "w-full sm:max-w-[774px]",
|
|
1265
|
+
children: [
|
|
1266
|
+
/* @__PURE__ */ jsx(Dialog.Header, {
|
|
1267
|
+
title,
|
|
1268
|
+
description: description ?? `${successCount} succeeded, ${failedCount} failed`,
|
|
1269
|
+
onClose: () => onOpenChange(false),
|
|
1270
|
+
className: "border-b-0"
|
|
1271
|
+
}),
|
|
1272
|
+
/* @__PURE__ */ jsx(Dialog.Body, {
|
|
1273
|
+
className: "px-5 py-0",
|
|
1274
|
+
children: renderContent ? renderContent(resolvedItems) : /* @__PURE__ */ jsx(DefaultTableContent, { items: resolvedItems })
|
|
1275
|
+
}),
|
|
1276
|
+
/* @__PURE__ */ jsxs(Dialog.Footer, {
|
|
1277
|
+
className: "border-t-0",
|
|
1278
|
+
children: [actions, /* @__PURE__ */ jsx(Button, {
|
|
1279
|
+
type: "primary",
|
|
1280
|
+
theme: "solid",
|
|
1281
|
+
onClick: () => onOpenChange(false),
|
|
1282
|
+
children: "Done"
|
|
1283
|
+
})]
|
|
1284
|
+
})
|
|
1285
|
+
]
|
|
1286
|
+
})
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
//#endregion
|
|
1291
|
+
//#region src/components/features/task-queue/core/task-queue-dropdown.tsx
|
|
1292
|
+
const autoOpenedForIds = /* @__PURE__ */ new Set();
|
|
1293
|
+
function TaskQueueDropdown() {
|
|
1294
|
+
const { tasks, cancel, dismissAll, activeSummary, closeSummary, summaryRenderContent } = useTaskQueue();
|
|
1295
|
+
const [open, setOpen] = useState(() => tasks.some((t) => !autoOpenedForIds.has(t.id)));
|
|
1296
|
+
useEffect(() => {
|
|
1297
|
+
if (tasks.some((t) => !autoOpenedForIds.has(t.id))) setOpen(true);
|
|
1298
|
+
for (const t of tasks) autoOpenedForIds.add(t.id);
|
|
1299
|
+
if (autoOpenedForIds.size > tasks.length) {
|
|
1300
|
+
const currentIds = new Set(tasks.map((t) => t.id));
|
|
1301
|
+
for (const id of autoOpenedForIds) if (!currentIds.has(id)) autoOpenedForIds.delete(id);
|
|
1302
|
+
}
|
|
1303
|
+
}, [tasks]);
|
|
1304
|
+
const hasDismissable = tasks.some((t) => t.status !== "running" && t.status !== "pending");
|
|
1305
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs(DropdownMenu, {
|
|
1306
|
+
open,
|
|
1307
|
+
onOpenChange: setOpen,
|
|
1308
|
+
children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
|
|
1309
|
+
asChild: true,
|
|
1310
|
+
children: /* @__PURE__ */ jsx(TaskQueueTrigger, { tasks })
|
|
1311
|
+
}), /* @__PURE__ */ jsxs(DropdownMenuContent, {
|
|
1312
|
+
align: "end",
|
|
1313
|
+
className: "w-96 rounded-lg p-0",
|
|
1314
|
+
onCloseAutoFocus: (e) => e.preventDefault(),
|
|
1315
|
+
children: [
|
|
1316
|
+
/* @__PURE__ */ jsx(TaskPanelHeader, {}),
|
|
1317
|
+
/* @__PURE__ */ jsx("div", {
|
|
1318
|
+
className: "max-h-[350px] overflow-y-auto",
|
|
1319
|
+
children: tasks.length === 0 && !activeSummary ? /* @__PURE__ */ jsxs("div", {
|
|
1320
|
+
className: "flex flex-col items-center justify-center px-4 py-12 text-center",
|
|
1321
|
+
children: [/* @__PURE__ */ jsx(CheckCircle2, { className: "text-muted-foreground/30 mb-3 h-12 w-12" }), /* @__PURE__ */ jsx("p", {
|
|
1322
|
+
className: "text-muted-foreground text-sm",
|
|
1323
|
+
children: "No tasks currently scheduled"
|
|
1324
|
+
})]
|
|
1325
|
+
}) : tasks.map((task) => /* @__PURE__ */ jsx(TaskPanelItem, {
|
|
1326
|
+
task,
|
|
1327
|
+
contextLabel: getContextLabel(task.metadata),
|
|
1328
|
+
onCancel: () => cancel(task.id)
|
|
1329
|
+
}, task.id))
|
|
1330
|
+
}),
|
|
1331
|
+
hasDismissable && /* @__PURE__ */ jsx("button", {
|
|
1332
|
+
type: "button",
|
|
1333
|
+
onClick: () => {
|
|
1334
|
+
dismissAll();
|
|
1335
|
+
},
|
|
1336
|
+
className: "border-border hover:bg-accent flex w-full cursor-pointer items-center justify-center gap-2 border-t px-3 py-2 transition-colors",
|
|
1337
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
1338
|
+
className: "text-destructive text-xs",
|
|
1339
|
+
children: "Clear tasks"
|
|
1340
|
+
})
|
|
1341
|
+
})
|
|
1342
|
+
]
|
|
1343
|
+
})]
|
|
1344
|
+
}), activeSummary && /* @__PURE__ */ jsx(TaskSummaryDialog, {
|
|
1345
|
+
open: true,
|
|
1346
|
+
onOpenChange: (isOpen) => {
|
|
1347
|
+
if (!isOpen) closeSummary();
|
|
1348
|
+
},
|
|
1349
|
+
title: activeSummary.title,
|
|
1350
|
+
items: activeSummary.items,
|
|
1351
|
+
renderContent: activeSummary.renderContent ?? summaryRenderContent
|
|
1352
|
+
})] });
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
//#endregion
|
|
1356
|
+
export { RedisTaskStorage as _, TaskPanelItem as a, createProjectMetadata as b, TaskPanelHeader as c, useCurrentScope as d, useTasksWithLabels as f, detectStorage as g, TaskQueue as h, TaskPanel as i, getContextLabel as l, TaskQueueProvider as m, TaskSummaryDialog as n, TaskPanelCounter as o, useTaskQueue as p, TaskQueueTrigger as r, TaskPanelActions as s, TaskQueueDropdown as t, matchesCurrentScope as u, LocalTaskStorage as v, createUserMetadata as x, createOrgMetadata as y };
|