@fayz-ai/plugin-tasks 0.9.4 → 0.10.1
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/dist/components/TasksDrawer.d.ts +1 -1
- package/dist/components/TasksDrawer.d.ts.map +1 -1
- package/dist/components/TasksLauncher.d.ts +9 -13
- package/dist/components/TasksLauncher.d.ts.map +1 -1
- package/dist/context.d.ts +0 -11
- package/dist/context.d.ts.map +1 -1
- package/dist/data/registries.d.ts.map +1 -1
- package/dist/index.d.ts +5 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +86 -864
- package/dist/index.js.map +1 -1
- package/dist/migrations/index.d.ts +1 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/package.json +6 -5
- package/dist/components/TaskCard.d.ts +0 -10
- package/dist/components/TaskCard.d.ts.map +0 -1
- package/dist/components/TaskDetail.d.ts +0 -5
- package/dist/components/TaskDetail.d.ts.map +0 -1
- package/dist/components/TaskLabelBadge.d.ts +0 -9
- package/dist/components/TaskLabelBadge.d.ts.map +0 -1
- package/dist/components/TaskPriorityBadge.d.ts +0 -7
- package/dist/components/TaskPriorityBadge.d.ts.map +0 -1
- package/dist/components/TaskQuickAdd.d.ts +0 -5
- package/dist/components/TaskQuickAdd.d.ts.map +0 -1
- package/dist/components/TaskStatusGroup.d.ts +0 -7
- package/dist/components/TaskStatusGroup.d.ts.map +0 -1
- package/dist/components/TasksFilterBar.d.ts +0 -3
- package/dist/components/TasksFilterBar.d.ts.map +0 -1
- package/dist/components/TasksPanel.d.ts +0 -7
- package/dist/components/TasksPanel.d.ts.map +0 -1
- package/dist/data/mock.d.ts +0 -3
- package/dist/data/mock.d.ts.map +0 -1
- package/dist/data/supabase.d.ts +0 -3
- package/dist/data/supabase.d.ts.map +0 -1
- package/dist/data/tables.d.ts +0 -5
- package/dist/data/tables.d.ts.map +0 -1
- package/dist/data/types.d.ts +0 -13
- package/dist/data/types.d.ts.map +0 -1
- package/dist/lib/tenant.d.ts +0 -3
- package/dist/lib/tenant.d.ts.map +0 -1
- package/dist/store.d.ts +0 -27
- package/dist/store.d.ts.map +0 -1
- package/dist/types.d.ts +0 -68
- package/dist/types.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,375 +1,12 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
import { createStore } from 'zustand/vanilla';
|
|
1
|
+
import { registerTranslations, useTranslation } from '@fayz-ai/core';
|
|
2
|
+
import { getTasksProvider, registerNotesTasksAdapter, useTasksStore, NOTE_LABEL_PREFIX, useTenantPluginSettings, useRightRailStore, useWidgetSurface, useTasksSummaryLine, RightRailPage, TasksPanel } from '@fayz-ai/admin';
|
|
3
|
+
export { TaskCard, TaskDetail, TaskQuickAdd, TasksFilterBar, TasksPanel, createMockTasksProvider, createSupabaseTasksProvider, useTasksStore, useTasksSummaryLine } from '@fayz-ai/admin';
|
|
5
4
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
5
|
+
import { useEffect } from 'react';
|
|
6
|
+
import { CheckSquare } from 'lucide-react';
|
|
7
|
+
import { useLayoutStore, useRailRow, cn, SheetBody } from '@fayz-ai/ui';
|
|
9
8
|
|
|
10
9
|
// src/index.ts
|
|
11
|
-
var ctx = createPluginContext("TasksContextProvider");
|
|
12
|
-
var TasksContextProvider = ctx.ContextProvider;
|
|
13
|
-
ctx.useConfig;
|
|
14
|
-
ctx.useProvider;
|
|
15
|
-
var useTasksStore = ctx.useStore;
|
|
16
|
-
|
|
17
|
-
// src/data/mock.ts
|
|
18
|
-
function uid() {
|
|
19
|
-
return crypto.randomUUID?.() ?? String(Date.now());
|
|
20
|
-
}
|
|
21
|
-
function now() {
|
|
22
|
-
return (/* @__PURE__ */ new Date()).toISOString();
|
|
23
|
-
}
|
|
24
|
-
function today() {
|
|
25
|
-
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
26
|
-
}
|
|
27
|
-
var SEED_LABELS = [
|
|
28
|
-
{ id: "lbl-bug", name: "Bug", color: "#ef4444", isActive: true, tenantId: "mock", createdAt: now() },
|
|
29
|
-
{ id: "lbl-feature", name: "Feature", color: "#3b82f6", isActive: true, tenantId: "mock", createdAt: now() },
|
|
30
|
-
{ id: "lbl-urgent", name: "Urgent", color: "#f59e0b", isActive: true, tenantId: "mock", createdAt: now() },
|
|
31
|
-
{ id: "lbl-personal", name: "Personal", color: "#8b5cf6", isActive: true, tenantId: "mock", createdAt: now() }
|
|
32
|
-
];
|
|
33
|
-
function makeSeedTasks() {
|
|
34
|
-
const base = {
|
|
35
|
-
tenantId: "mock",
|
|
36
|
-
description: null,
|
|
37
|
-
assignedToId: null,
|
|
38
|
-
assignedToName: null,
|
|
39
|
-
parentId: null,
|
|
40
|
-
labels: [],
|
|
41
|
-
createdById: null,
|
|
42
|
-
createdByName: null,
|
|
43
|
-
createdAt: now(),
|
|
44
|
-
updatedAt: now()
|
|
45
|
-
};
|
|
46
|
-
return [
|
|
47
|
-
{ ...base, id: uid(), title: "Setup payment gateway", status: "todo", priority: "high", dueDate: today(), position: 0 },
|
|
48
|
-
{ ...base, id: uid(), title: "Update menu photos", status: "todo", priority: "medium", dueDate: null, position: 1 },
|
|
49
|
-
{ ...base, id: uid(), title: "Review monthly report", status: "in_progress", priority: "medium", dueDate: null, position: 0 },
|
|
50
|
-
{ ...base, id: uid(), title: "Fix login bug", status: "in_progress", priority: "urgent", dueDate: today(), position: 1, labels: ["lbl-bug"] },
|
|
51
|
-
{ ...base, id: uid(), title: "Onboard new employee", status: "done", priority: "low", dueDate: null, position: 0 }
|
|
52
|
-
];
|
|
53
|
-
}
|
|
54
|
-
function matchesQuery(task, query) {
|
|
55
|
-
if (query.status) {
|
|
56
|
-
const statuses = Array.isArray(query.status) ? query.status : [query.status];
|
|
57
|
-
if (!statuses.includes(task.status)) return false;
|
|
58
|
-
}
|
|
59
|
-
if (query.priority) {
|
|
60
|
-
const priorities = Array.isArray(query.priority) ? query.priority : [query.priority];
|
|
61
|
-
if (!priorities.includes(task.priority)) return false;
|
|
62
|
-
}
|
|
63
|
-
if (query.assignedToId && task.assignedToId !== query.assignedToId) return false;
|
|
64
|
-
if (query.parentId !== void 0) {
|
|
65
|
-
if (query.parentId === null && task.parentId !== null) return false;
|
|
66
|
-
if (query.parentId !== null && task.parentId !== query.parentId) return false;
|
|
67
|
-
}
|
|
68
|
-
if (query.search) {
|
|
69
|
-
const s = query.search.toLowerCase();
|
|
70
|
-
if (!task.title.toLowerCase().includes(s)) return false;
|
|
71
|
-
}
|
|
72
|
-
if (query.overdue && task.dueDate) {
|
|
73
|
-
if (task.dueDate >= today()) return false;
|
|
74
|
-
}
|
|
75
|
-
return true;
|
|
76
|
-
}
|
|
77
|
-
function createMockTasksProvider() {
|
|
78
|
-
let tasks = makeSeedTasks();
|
|
79
|
-
const labels = [...SEED_LABELS];
|
|
80
|
-
return {
|
|
81
|
-
async getTasks(query) {
|
|
82
|
-
return tasks.filter((t) => matchesQuery(t, query)).sort((a, b) => a.position - b.position);
|
|
83
|
-
},
|
|
84
|
-
async getTaskById(id) {
|
|
85
|
-
return tasks.find((t) => t.id === id) ?? null;
|
|
86
|
-
},
|
|
87
|
-
async getSubtasks(parentId) {
|
|
88
|
-
return tasks.filter((t) => t.parentId === parentId).sort((a, b) => a.position - b.position);
|
|
89
|
-
},
|
|
90
|
-
async createTask(input) {
|
|
91
|
-
const statusTasks = tasks.filter((t) => t.status === (input.status ?? "todo") && t.parentId === (input.parentId ?? null));
|
|
92
|
-
const maxPos = statusTasks.reduce((max, t) => Math.max(max, t.position), -1);
|
|
93
|
-
const task = {
|
|
94
|
-
id: uid(),
|
|
95
|
-
title: input.title,
|
|
96
|
-
description: input.description ?? null,
|
|
97
|
-
status: input.status ?? "todo",
|
|
98
|
-
priority: input.priority ?? "medium",
|
|
99
|
-
dueDate: input.dueDate ?? null,
|
|
100
|
-
assignedToId: input.assignedToId ?? null,
|
|
101
|
-
assignedToName: null,
|
|
102
|
-
parentId: input.parentId ?? null,
|
|
103
|
-
labels: input.labels ?? [],
|
|
104
|
-
position: maxPos + 1,
|
|
105
|
-
createdById: null,
|
|
106
|
-
createdByName: null,
|
|
107
|
-
tenantId: "mock",
|
|
108
|
-
createdAt: now(),
|
|
109
|
-
updatedAt: now()
|
|
110
|
-
};
|
|
111
|
-
tasks.push(task);
|
|
112
|
-
return task;
|
|
113
|
-
},
|
|
114
|
-
async updateTask(id, input) {
|
|
115
|
-
const idx = tasks.findIndex((t) => t.id === id);
|
|
116
|
-
if (idx === -1) throw new Error("Task not found");
|
|
117
|
-
tasks[idx] = { ...tasks[idx], ...input, updatedAt: now() };
|
|
118
|
-
return tasks[idx];
|
|
119
|
-
},
|
|
120
|
-
async deleteTask(id) {
|
|
121
|
-
tasks = tasks.filter((t) => t.id !== id && t.parentId !== id);
|
|
122
|
-
},
|
|
123
|
-
async reorderTask(id, newPosition, status) {
|
|
124
|
-
const task = tasks.find((t) => t.id === id);
|
|
125
|
-
if (!task) return;
|
|
126
|
-
task.status = status;
|
|
127
|
-
task.position = newPosition;
|
|
128
|
-
task.updatedAt = now();
|
|
129
|
-
},
|
|
130
|
-
async getLabels() {
|
|
131
|
-
return labels.filter((l) => l.isActive);
|
|
132
|
-
},
|
|
133
|
-
async getSummary() {
|
|
134
|
-
const topLevel = tasks.filter((t) => t.parentId === null);
|
|
135
|
-
const d = today();
|
|
136
|
-
return {
|
|
137
|
-
total: topLevel.length,
|
|
138
|
-
todo: topLevel.filter((t) => t.status === "todo").length,
|
|
139
|
-
inProgress: topLevel.filter((t) => t.status === "in_progress").length,
|
|
140
|
-
done: topLevel.filter((t) => t.status === "done").length,
|
|
141
|
-
overdue: topLevel.filter((t) => t.dueDate != null && t.dueDate < d && t.status !== "done" && t.status !== "cancelled").length,
|
|
142
|
-
dueToday: topLevel.filter((t) => t.dueDate === d && t.status !== "done" && t.status !== "cancelled").length,
|
|
143
|
-
highPriority: topLevel.filter((t) => (t.priority === "high" || t.priority === "urgent") && t.status !== "done" && t.status !== "cancelled").length
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// src/data/tables.ts
|
|
150
|
-
var T = {
|
|
151
|
-
tasks: "plg_tasks_tasks",
|
|
152
|
-
labels: "plg_tasks_labels"
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
// src/data/supabase.ts
|
|
156
|
-
function getTenantId() {
|
|
157
|
-
return getActiveTenantId();
|
|
158
|
-
}
|
|
159
|
-
function getClient() {
|
|
160
|
-
const supabase = getSupabaseClientOptional();
|
|
161
|
-
if (!supabase) throw new Error("Supabase not initialized");
|
|
162
|
-
return supabase;
|
|
163
|
-
}
|
|
164
|
-
function snakeToCamel(obj) {
|
|
165
|
-
const result = {};
|
|
166
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
167
|
-
result[key.replace(/_([a-z])/g, (_, c) => c.toUpperCase())] = value;
|
|
168
|
-
}
|
|
169
|
-
return result;
|
|
170
|
-
}
|
|
171
|
-
function toTask(row) {
|
|
172
|
-
const t = snakeToCamel(row);
|
|
173
|
-
if (!Array.isArray(t.labels)) t.labels = [];
|
|
174
|
-
return t;
|
|
175
|
-
}
|
|
176
|
-
function createSupabaseTasksProvider() {
|
|
177
|
-
return {
|
|
178
|
-
async getTasks(query) {
|
|
179
|
-
const tenantId = getTenantId();
|
|
180
|
-
if (!tenantId) return [];
|
|
181
|
-
let q = getClient().from(T.tasks).select("*").eq("tenant_id", tenantId);
|
|
182
|
-
if (query.parentId === null) {
|
|
183
|
-
q = q.is("parent_id", null);
|
|
184
|
-
} else if (query.parentId) {
|
|
185
|
-
q = q.eq("parent_id", query.parentId);
|
|
186
|
-
}
|
|
187
|
-
if (query.status) {
|
|
188
|
-
const statuses = Array.isArray(query.status) ? query.status : [query.status];
|
|
189
|
-
q = q.in("status", statuses);
|
|
190
|
-
}
|
|
191
|
-
if (query.priority) {
|
|
192
|
-
const priorities = Array.isArray(query.priority) ? query.priority : [query.priority];
|
|
193
|
-
q = q.in("priority", priorities);
|
|
194
|
-
}
|
|
195
|
-
if (query.assignedToId) {
|
|
196
|
-
q = q.eq("assigned_to_id", query.assignedToId);
|
|
197
|
-
}
|
|
198
|
-
if (query.search) {
|
|
199
|
-
q = q.ilike("title", `%${query.search}%`);
|
|
200
|
-
}
|
|
201
|
-
if (query.overdue) {
|
|
202
|
-
q = q.lt("due_date", (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)).not("status", "in", '("done","cancelled")');
|
|
203
|
-
}
|
|
204
|
-
q = q.order("position", { ascending: true }).order("created_at", { ascending: false });
|
|
205
|
-
const { data } = await q;
|
|
206
|
-
return (data ?? []).map(toTask);
|
|
207
|
-
},
|
|
208
|
-
async getTaskById(id) {
|
|
209
|
-
const { data } = await getClient().from(T.tasks).select("*").eq("id", id).maybeSingle();
|
|
210
|
-
return data ? toTask(data) : null;
|
|
211
|
-
},
|
|
212
|
-
async getSubtasks(parentId) {
|
|
213
|
-
const { data } = await getClient().from(T.tasks).select("*").eq("parent_id", parentId).order("position", { ascending: true });
|
|
214
|
-
return (data ?? []).map(toTask);
|
|
215
|
-
},
|
|
216
|
-
async createTask(input) {
|
|
217
|
-
const tenantId = getTenantId();
|
|
218
|
-
if (!tenantId) throw new Error("No active tenant");
|
|
219
|
-
const { data: maxData } = await getClient().from(T.tasks).select("position").eq("tenant_id", tenantId).eq("status", input.status ?? "todo").is("parent_id", input.parentId ?? null).order("position", { ascending: false }).limit(1);
|
|
220
|
-
const maxPos = maxData?.[0]?.position ?? -1;
|
|
221
|
-
const { data, error } = await getClient().from(T.tasks).insert({
|
|
222
|
-
tenant_id: tenantId,
|
|
223
|
-
title: input.title,
|
|
224
|
-
description: input.description ?? null,
|
|
225
|
-
status: input.status ?? "todo",
|
|
226
|
-
priority: input.priority ?? "medium",
|
|
227
|
-
due_date: input.dueDate ?? null,
|
|
228
|
-
assigned_to_id: input.assignedToId ?? null,
|
|
229
|
-
parent_id: input.parentId ?? null,
|
|
230
|
-
labels: input.labels ?? [],
|
|
231
|
-
position: maxPos + 1
|
|
232
|
-
}).select("*").single();
|
|
233
|
-
if (error) throw error;
|
|
234
|
-
return toTask(data);
|
|
235
|
-
},
|
|
236
|
-
async updateTask(id, input) {
|
|
237
|
-
const payload = { updated_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
238
|
-
if (input.title !== void 0) payload.title = input.title;
|
|
239
|
-
if (input.description !== void 0) payload.description = input.description;
|
|
240
|
-
if (input.status !== void 0) payload.status = input.status;
|
|
241
|
-
if (input.priority !== void 0) payload.priority = input.priority;
|
|
242
|
-
if (input.dueDate !== void 0) payload.due_date = input.dueDate;
|
|
243
|
-
if (input.assignedToId !== void 0) payload.assigned_to_id = input.assignedToId;
|
|
244
|
-
if (input.labels !== void 0) payload.labels = input.labels;
|
|
245
|
-
if (input.position !== void 0) payload.position = input.position;
|
|
246
|
-
const { data, error } = await getClient().from(T.tasks).update(payload).eq("id", id).select("*").single();
|
|
247
|
-
if (error) throw error;
|
|
248
|
-
return toTask(data);
|
|
249
|
-
},
|
|
250
|
-
async deleteTask(id) {
|
|
251
|
-
await getClient().from(T.tasks).delete().eq("id", id);
|
|
252
|
-
},
|
|
253
|
-
async reorderTask(id, newPosition, status) {
|
|
254
|
-
await getClient().from(T.tasks).update({ position: newPosition, status, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", id);
|
|
255
|
-
},
|
|
256
|
-
async getLabels() {
|
|
257
|
-
const tenantId = getTenantId();
|
|
258
|
-
if (!tenantId) return [];
|
|
259
|
-
const { data } = await getClient().from(T.labels).select("*").eq("tenant_id", tenantId).eq("is_active", true).order("name");
|
|
260
|
-
return (data ?? []).map((r) => snakeToCamel(r));
|
|
261
|
-
},
|
|
262
|
-
async getSummary() {
|
|
263
|
-
const tenantId = getTenantId();
|
|
264
|
-
if (!tenantId) return { total: 0, todo: 0, inProgress: 0, done: 0, overdue: 0, dueToday: 0, highPriority: 0 };
|
|
265
|
-
const { data } = await getClient().from(T.tasks).select("status, priority, due_date").eq("tenant_id", tenantId).is("parent_id", null);
|
|
266
|
-
const tasks = data ?? [];
|
|
267
|
-
const today2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
268
|
-
return {
|
|
269
|
-
total: tasks.length,
|
|
270
|
-
todo: tasks.filter((t) => t.status === "todo").length,
|
|
271
|
-
inProgress: tasks.filter((t) => t.status === "in_progress").length,
|
|
272
|
-
done: tasks.filter((t) => t.status === "done").length,
|
|
273
|
-
overdue: tasks.filter((t) => t.due_date && t.due_date < today2 && t.status !== "done" && t.status !== "cancelled").length,
|
|
274
|
-
dueToday: tasks.filter((t) => t.due_date === today2 && t.status !== "done" && t.status !== "cancelled").length,
|
|
275
|
-
highPriority: tasks.filter((t) => (t.priority === "high" || t.priority === "urgent") && t.status !== "done" && t.status !== "cancelled").length
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
function createTasksStore(provider) {
|
|
281
|
-
return createStore((set, get) => ({
|
|
282
|
-
tasks: [],
|
|
283
|
-
tasksLoading: false,
|
|
284
|
-
labels: [],
|
|
285
|
-
summary: null,
|
|
286
|
-
summaryLoading: false,
|
|
287
|
-
drawerOpen: false,
|
|
288
|
-
selectedTaskId: null,
|
|
289
|
-
activeFilter: "all",
|
|
290
|
-
// ----- Data actions -----
|
|
291
|
-
async fetchTasks(query) {
|
|
292
|
-
return dedup("tasks:list", async () => {
|
|
293
|
-
set({ tasksLoading: true });
|
|
294
|
-
try {
|
|
295
|
-
const filter = get().activeFilter;
|
|
296
|
-
const q = {
|
|
297
|
-
parentId: null,
|
|
298
|
-
...query,
|
|
299
|
-
...filter !== "all" ? { status: filter } : {}
|
|
300
|
-
};
|
|
301
|
-
const tasks = await provider.getTasks(q);
|
|
302
|
-
set({ tasks, tasksLoading: false });
|
|
303
|
-
} catch {
|
|
304
|
-
set({ tasksLoading: false });
|
|
305
|
-
}
|
|
306
|
-
});
|
|
307
|
-
},
|
|
308
|
-
async fetchLabels() {
|
|
309
|
-
return dedup("tasks:labels", async () => {
|
|
310
|
-
try {
|
|
311
|
-
const labels = await provider.getLabels();
|
|
312
|
-
set({ labels });
|
|
313
|
-
} catch {
|
|
314
|
-
}
|
|
315
|
-
});
|
|
316
|
-
},
|
|
317
|
-
async fetchSummary() {
|
|
318
|
-
return dedup("tasks:summary", async () => {
|
|
319
|
-
set({ summaryLoading: true });
|
|
320
|
-
try {
|
|
321
|
-
const summary = await provider.getSummary();
|
|
322
|
-
set({ summary, summaryLoading: false });
|
|
323
|
-
} catch {
|
|
324
|
-
set({ summaryLoading: false });
|
|
325
|
-
}
|
|
326
|
-
});
|
|
327
|
-
},
|
|
328
|
-
async createTask(input) {
|
|
329
|
-
const task = await provider.createTask(input);
|
|
330
|
-
await get().fetchTasks();
|
|
331
|
-
await get().fetchSummary();
|
|
332
|
-
return task;
|
|
333
|
-
},
|
|
334
|
-
async updateTask(id, input) {
|
|
335
|
-
const task = await provider.updateTask(id, input);
|
|
336
|
-
set((s) => ({ tasks: s.tasks.map((t) => t.id === id ? task : t) }));
|
|
337
|
-
await get().fetchSummary();
|
|
338
|
-
return task;
|
|
339
|
-
},
|
|
340
|
-
async deleteTask(id) {
|
|
341
|
-
await provider.deleteTask(id);
|
|
342
|
-
set((s) => ({ tasks: s.tasks.filter((t) => t.id !== id), selectedTaskId: null }));
|
|
343
|
-
await get().fetchSummary();
|
|
344
|
-
},
|
|
345
|
-
async reorderTask(id, newPosition, status) {
|
|
346
|
-
await provider.reorderTask(id, newPosition, status);
|
|
347
|
-
await get().fetchTasks();
|
|
348
|
-
},
|
|
349
|
-
// ----- UI actions -----
|
|
350
|
-
openDrawer() {
|
|
351
|
-
set({ drawerOpen: true });
|
|
352
|
-
get().fetchTasks();
|
|
353
|
-
get().fetchLabels();
|
|
354
|
-
get().fetchSummary();
|
|
355
|
-
},
|
|
356
|
-
closeDrawer() {
|
|
357
|
-
set({ drawerOpen: false, selectedTaskId: null });
|
|
358
|
-
},
|
|
359
|
-
toggleDrawer() {
|
|
360
|
-
const { drawerOpen } = get();
|
|
361
|
-
if (drawerOpen) get().closeDrawer();
|
|
362
|
-
else get().openDrawer();
|
|
363
|
-
},
|
|
364
|
-
selectTask(id) {
|
|
365
|
-
set({ selectedTaskId: id });
|
|
366
|
-
},
|
|
367
|
-
setFilter(filter) {
|
|
368
|
-
set({ activeFilter: filter });
|
|
369
|
-
get().fetchTasks();
|
|
370
|
-
}
|
|
371
|
-
}));
|
|
372
|
-
}
|
|
373
10
|
|
|
374
11
|
// src/locales/en.ts
|
|
375
12
|
var en = {
|
|
@@ -466,6 +103,7 @@ var tasksLocales = {
|
|
|
466
103
|
};
|
|
467
104
|
|
|
468
105
|
// src/data/registries.ts
|
|
106
|
+
var LABELS_TABLE = "plg_tasks_labels";
|
|
469
107
|
var taskLabelEntity = {
|
|
470
108
|
name: "Task Label",
|
|
471
109
|
namePlural: "Task Labels",
|
|
@@ -477,7 +115,7 @@ var taskLabelEntity = {
|
|
|
477
115
|
{ key: "color", label: "Color", type: "text", showInTable: true, defaultValue: "#6366f1" },
|
|
478
116
|
{ key: "isActive", label: "Active", type: "boolean", showInTable: true, defaultValue: true }
|
|
479
117
|
],
|
|
480
|
-
data: { table:
|
|
118
|
+
data: { table: LABELS_TABLE, tenantScoped: true }
|
|
481
119
|
};
|
|
482
120
|
var tasksRegistries = [
|
|
483
121
|
{
|
|
@@ -524,496 +162,37 @@ function TasksGeneralSettings() {
|
|
|
524
162
|
] }) })
|
|
525
163
|
] });
|
|
526
164
|
}
|
|
527
|
-
|
|
528
|
-
{ key: "all", labelKey: "tasks.filter.all" },
|
|
529
|
-
{ key: "todo", labelKey: "tasks.filter.todo" },
|
|
530
|
-
{ key: "in_progress", labelKey: "tasks.filter.inProgress" },
|
|
531
|
-
{ key: "done", labelKey: "tasks.filter.done" }
|
|
532
|
-
];
|
|
533
|
-
function TasksFilterBar() {
|
|
534
|
-
const t = useTranslation();
|
|
535
|
-
const activeFilter = useTasksStore((s) => s.activeFilter);
|
|
536
|
-
const setFilter = useTasksStore((s) => s.setFilter);
|
|
537
|
-
const summary = useTasksStore((s) => s.summary);
|
|
538
|
-
const getCount = (key) => {
|
|
539
|
-
if (!summary) return void 0;
|
|
540
|
-
switch (key) {
|
|
541
|
-
case "all":
|
|
542
|
-
return summary.total;
|
|
543
|
-
case "todo":
|
|
544
|
-
return summary.todo;
|
|
545
|
-
case "in_progress":
|
|
546
|
-
return summary.inProgress;
|
|
547
|
-
case "done":
|
|
548
|
-
return summary.done;
|
|
549
|
-
default:
|
|
550
|
-
return void 0;
|
|
551
|
-
}
|
|
552
|
-
};
|
|
553
|
-
return /* @__PURE__ */ jsx("div", { className: "flex items-center gap-1 px-3 py-2 border-b overflow-x-auto", children: FILTERS.map(({ key, labelKey }) => {
|
|
554
|
-
const active = activeFilter === key;
|
|
555
|
-
const count = getCount(key);
|
|
556
|
-
return /* @__PURE__ */ jsxs(
|
|
557
|
-
"button",
|
|
558
|
-
{
|
|
559
|
-
type: "button",
|
|
560
|
-
onClick: () => setFilter(key),
|
|
561
|
-
className: `inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-medium transition-colors whitespace-nowrap ${active ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:bg-muted hover:text-foreground"}`,
|
|
562
|
-
children: [
|
|
563
|
-
t(labelKey),
|
|
564
|
-
count != null && /* @__PURE__ */ jsx("span", { className: `text-[10px] ${active ? "text-primary-foreground/80" : "text-muted-foreground"}`, children: count })
|
|
565
|
-
]
|
|
566
|
-
},
|
|
567
|
-
key
|
|
568
|
-
);
|
|
569
|
-
}) });
|
|
570
|
-
}
|
|
571
|
-
function TaskQuickAdd({ parentId }) {
|
|
165
|
+
function TasksDrawer() {
|
|
572
166
|
const t = useTranslation();
|
|
573
|
-
const
|
|
574
|
-
const
|
|
575
|
-
const
|
|
576
|
-
const handleSubmit = useCallback(async (e) => {
|
|
577
|
-
e.preventDefault();
|
|
578
|
-
const trimmed = title.trim();
|
|
579
|
-
if (!trimmed || loading) return;
|
|
580
|
-
setLoading(true);
|
|
581
|
-
try {
|
|
582
|
-
await createTask({
|
|
583
|
-
title: trimmed,
|
|
584
|
-
parentId: parentId ?? null
|
|
585
|
-
});
|
|
586
|
-
setTitle("");
|
|
587
|
-
} finally {
|
|
588
|
-
setLoading(false);
|
|
589
|
-
}
|
|
590
|
-
}, [title, loading, createTask, parentId]);
|
|
591
|
-
return /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, className: "flex items-center gap-2 px-3 py-2 border-b", children: [
|
|
592
|
-
/* @__PURE__ */ jsx(Plus, { className: "h-4 w-4 shrink-0 text-muted-foreground" }),
|
|
593
|
-
/* @__PURE__ */ jsx(
|
|
594
|
-
"input",
|
|
595
|
-
{
|
|
596
|
-
type: "text",
|
|
597
|
-
value: title,
|
|
598
|
-
onChange: (e) => setTitle(e.target.value),
|
|
599
|
-
placeholder: parentId ? t("tasks.detail.addSubtask") : t("tasks.quickAdd.placeholder"),
|
|
600
|
-
className: "flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground/60",
|
|
601
|
-
disabled: loading
|
|
602
|
-
}
|
|
603
|
-
)
|
|
604
|
-
] });
|
|
605
|
-
}
|
|
606
|
-
var PRIORITY_CONFIG = {
|
|
607
|
-
low: { color: "text-muted-foreground", bg: "bg-muted", icon: ArrowDown },
|
|
608
|
-
medium: { color: "text-info-soft-foreground", bg: "bg-info-soft", icon: Minus },
|
|
609
|
-
high: { color: "text-warning-soft-foreground", bg: "bg-warning-soft", icon: ArrowUp },
|
|
610
|
-
urgent: { color: "text-destructive-soft-foreground", bg: "bg-destructive-soft", icon: AlertTriangle }
|
|
611
|
-
};
|
|
612
|
-
function TaskPriorityBadge({ priority, showLabel }) {
|
|
613
|
-
const cfg = PRIORITY_CONFIG[priority];
|
|
614
|
-
const Icon = cfg.icon;
|
|
615
|
-
return /* @__PURE__ */ jsxs("span", { className: `inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs font-medium ${cfg.color} ${cfg.bg}`, children: [
|
|
616
|
-
/* @__PURE__ */ jsx(Icon, { className: "h-3 w-3" }),
|
|
617
|
-
showLabel && /* @__PURE__ */ jsx("span", { className: "capitalize", children: priority })
|
|
618
|
-
] });
|
|
619
|
-
}
|
|
620
|
-
function TaskLabelBadge({ labelId }) {
|
|
621
|
-
const labels = useTasksStore((s) => s.labels);
|
|
622
|
-
const label = labels.find((l) => l.id === labelId);
|
|
623
|
-
if (!label) return null;
|
|
624
|
-
return /* @__PURE__ */ jsx(
|
|
625
|
-
"span",
|
|
626
|
-
{
|
|
627
|
-
className: "inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium leading-none",
|
|
628
|
-
style: {
|
|
629
|
-
backgroundColor: label.color + "20",
|
|
630
|
-
color: label.color
|
|
631
|
-
},
|
|
632
|
-
children: label.name
|
|
633
|
-
}
|
|
634
|
-
);
|
|
635
|
-
}
|
|
636
|
-
function TaskLabelBadges({ labelIds, max = 2 }) {
|
|
637
|
-
if (labelIds.length === 0) return null;
|
|
638
|
-
const visible = labelIds.slice(0, max);
|
|
639
|
-
const overflow = labelIds.length - max;
|
|
640
|
-
return /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1", children: [
|
|
641
|
-
visible.map((id) => /* @__PURE__ */ jsx(TaskLabelBadge, { labelId: id }, id)),
|
|
642
|
-
overflow > 0 && /* @__PURE__ */ jsxs("span", { className: "text-[10px] text-muted-foreground", children: [
|
|
643
|
-
"+",
|
|
644
|
-
overflow
|
|
645
|
-
] })
|
|
646
|
-
] });
|
|
647
|
-
}
|
|
648
|
-
function isOverdue(dueDate) {
|
|
649
|
-
if (!dueDate) return false;
|
|
650
|
-
return dueDate < (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
651
|
-
}
|
|
652
|
-
function isToday(dueDate) {
|
|
653
|
-
if (!dueDate) return false;
|
|
654
|
-
return dueDate === (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
655
|
-
}
|
|
656
|
-
function formatDueDate(dueDate) {
|
|
657
|
-
const d = /* @__PURE__ */ new Date(dueDate + "T00:00:00");
|
|
658
|
-
return d.toLocaleDateString(void 0, { month: "short", day: "numeric" });
|
|
659
|
-
}
|
|
660
|
-
function TaskCard({ task, subtaskCount }) {
|
|
661
|
-
const selectTask = useTasksStore((s) => s.selectTask);
|
|
662
|
-
const updateTask = useTasksStore((s) => s.updateTask);
|
|
663
|
-
const isDone = task.status === "done";
|
|
664
|
-
const overdue = !isDone && isOverdue(task.dueDate);
|
|
665
|
-
const dueToday = !isDone && isToday(task.dueDate);
|
|
666
|
-
const handleCheck = (e) => {
|
|
667
|
-
e.stopPropagation();
|
|
668
|
-
updateTask(task.id, { status: isDone ? "todo" : "done" });
|
|
669
|
-
};
|
|
167
|
+
const drawerOpen = useTasksStore((s) => s.drawerOpen);
|
|
168
|
+
const closeDrawer = useTasksStore((s) => s.closeDrawer);
|
|
169
|
+
const summaryLine = useTasksSummaryLine();
|
|
670
170
|
return /* @__PURE__ */ jsxs(
|
|
671
|
-
|
|
171
|
+
RightRailPage,
|
|
672
172
|
{
|
|
673
|
-
|
|
674
|
-
|
|
173
|
+
title: t("tasks.drawer.title"),
|
|
174
|
+
open: drawerOpen,
|
|
175
|
+
onClose: closeDrawer,
|
|
176
|
+
width: "max-w-lg",
|
|
675
177
|
children: [
|
|
676
|
-
/* @__PURE__ */ jsx("
|
|
677
|
-
|
|
678
|
-
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1 space-y-1", children: [
|
|
679
|
-
/* @__PURE__ */ jsx("p", { className: `text-sm leading-snug ${isDone ? "line-through text-muted-foreground" : ""}`, children: task.title }),
|
|
680
|
-
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-1.5", children: [
|
|
681
|
-
/* @__PURE__ */ jsx(TaskPriorityBadge, { priority: task.priority }),
|
|
682
|
-
task.dueDate && /* @__PURE__ */ jsxs("span", { className: `inline-flex items-center gap-1 text-[11px] ${overdue ? "text-destructive font-medium" : dueToday ? "text-warning font-medium" : "text-muted-foreground"}`, children: [
|
|
683
|
-
/* @__PURE__ */ jsx(Calendar, { className: "h-3 w-3" }),
|
|
684
|
-
formatDueDate(task.dueDate)
|
|
685
|
-
] }),
|
|
686
|
-
subtaskCount && subtaskCount.total > 0 && /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1 text-[11px] text-muted-foreground", children: [
|
|
687
|
-
/* @__PURE__ */ jsx(ListChecks, { className: "h-3 w-3" }),
|
|
688
|
-
subtaskCount.done,
|
|
689
|
-
"/",
|
|
690
|
-
subtaskCount.total
|
|
691
|
-
] }),
|
|
692
|
-
/* @__PURE__ */ jsx(TaskLabelBadges, { labelIds: task.labels, max: 2 })
|
|
693
|
-
] })
|
|
694
|
-
] }),
|
|
695
|
-
task.assignedToName && /* @__PURE__ */ jsx("div", { className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-primary/10 text-[10px] font-medium text-primary", title: task.assignedToName, children: task.assignedToName.split(" ").map((n) => n[0]).join("").slice(0, 2).toUpperCase() })
|
|
178
|
+
/* @__PURE__ */ jsx("p", { className: "shrink-0 border-b border-border/40 px-5 py-2 text-xs text-muted-foreground", children: summaryLine || t("tasks.drawer.subtitle") }),
|
|
179
|
+
/* @__PURE__ */ jsx(SheetBody, { className: "flex flex-col px-0 py-0", children: /* @__PURE__ */ jsx(TasksPanel, {}) })
|
|
696
180
|
]
|
|
697
181
|
}
|
|
698
182
|
);
|
|
699
183
|
}
|
|
700
|
-
|
|
701
|
-
todo: "bg-muted-foreground",
|
|
702
|
-
in_progress: "bg-info",
|
|
703
|
-
done: "bg-success",
|
|
704
|
-
cancelled: "bg-muted-foreground/60"
|
|
705
|
-
};
|
|
706
|
-
function TaskStatusGroup({ status, tasks }) {
|
|
707
|
-
const t = useTranslation();
|
|
708
|
-
const [collapsed, setCollapsed] = useState(status === "cancelled");
|
|
709
|
-
if (tasks.length === 0) return null;
|
|
710
|
-
const Chevron = collapsed ? ChevronRight : ChevronDown;
|
|
711
|
-
return /* @__PURE__ */ jsxs("div", { className: "space-y-0.5", children: [
|
|
712
|
-
/* @__PURE__ */ jsxs(
|
|
713
|
-
"button",
|
|
714
|
-
{
|
|
715
|
-
type: "button",
|
|
716
|
-
onClick: () => setCollapsed(!collapsed),
|
|
717
|
-
className: "flex w-full items-center gap-2 px-3 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wider hover:text-foreground transition-colors",
|
|
718
|
-
children: [
|
|
719
|
-
/* @__PURE__ */ jsx("span", { className: `h-2 w-2 rounded-full ${STATUS_COLORS[status]}` }),
|
|
720
|
-
/* @__PURE__ */ jsx("span", { children: t(`tasks.status.${status}`) }),
|
|
721
|
-
/* @__PURE__ */ jsxs("span", { className: "text-[10px] font-normal", children: [
|
|
722
|
-
"(",
|
|
723
|
-
tasks.length,
|
|
724
|
-
")"
|
|
725
|
-
] }),
|
|
726
|
-
/* @__PURE__ */ jsx(Chevron, { className: "ml-auto h-3.5 w-3.5" })
|
|
727
|
-
]
|
|
728
|
-
}
|
|
729
|
-
),
|
|
730
|
-
!collapsed && /* @__PURE__ */ jsx("div", { children: tasks.map((task) => /* @__PURE__ */ jsx(TaskCard, { task }, task.id)) })
|
|
731
|
-
] });
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
// src/types.ts
|
|
735
|
-
var TASK_STATUSES = ["todo", "in_progress", "done", "cancelled"];
|
|
736
|
-
var TASK_PRIORITIES = ["low", "medium", "high", "urgent"];
|
|
737
|
-
var STATUS_DOT = {
|
|
738
|
-
todo: "bg-muted-foreground",
|
|
739
|
-
in_progress: "bg-info",
|
|
740
|
-
done: "bg-success",
|
|
741
|
-
cancelled: "bg-muted-foreground/60"
|
|
742
|
-
};
|
|
743
|
-
function formatDateTime(iso) {
|
|
744
|
-
const d = new Date(iso);
|
|
745
|
-
const sameDay = d.toDateString() === (/* @__PURE__ */ new Date()).toDateString();
|
|
746
|
-
return sameDay ? d.toLocaleTimeString(void 0, { hour: "2-digit", minute: "2-digit" }) : d.toLocaleDateString(void 0, { day: "2-digit", month: "2-digit", year: "2-digit" });
|
|
747
|
-
}
|
|
748
|
-
function TaskDetail({ taskId }) {
|
|
749
|
-
const t = useTranslation();
|
|
750
|
-
const tasks = useTasksStore((s) => s.tasks);
|
|
751
|
-
const selectTask = useTasksStore((s) => s.selectTask);
|
|
752
|
-
const updateTask = useTasksStore((s) => s.updateTask);
|
|
753
|
-
const deleteTask = useTasksStore((s) => s.deleteTask);
|
|
754
|
-
const task = tasks.find((tt) => tt.id === taskId);
|
|
755
|
-
const delegate = useDelegateToAssistant();
|
|
756
|
-
const [title, setTitle] = useState("");
|
|
757
|
-
const [description, setDescription] = useState("");
|
|
758
|
-
const [subtasks, setSubtasks] = useState([]);
|
|
759
|
-
useEffect(() => {
|
|
760
|
-
if (task) {
|
|
761
|
-
setTitle(task.title);
|
|
762
|
-
setDescription(task.description ?? "");
|
|
763
|
-
}
|
|
764
|
-
}, [task]);
|
|
765
|
-
useEffect(() => {
|
|
766
|
-
setSubtasks([]);
|
|
767
|
-
}, [taskId]);
|
|
768
|
-
const handleTitleBlur = useCallback(() => {
|
|
769
|
-
if (task && title.trim() && title !== task.title) {
|
|
770
|
-
updateTask(task.id, { title: title.trim() });
|
|
771
|
-
}
|
|
772
|
-
}, [task, title, updateTask]);
|
|
773
|
-
const handleDescriptionBlur = useCallback(() => {
|
|
774
|
-
if (task && description !== (task.description ?? "")) {
|
|
775
|
-
updateTask(task.id, { description: description || null });
|
|
776
|
-
}
|
|
777
|
-
}, [task, description, updateTask]);
|
|
778
|
-
const handleDelete = useCallback(() => {
|
|
779
|
-
if (task && confirm(t("tasks.detail.deleteConfirm"))) {
|
|
780
|
-
deleteTask(task.id);
|
|
781
|
-
}
|
|
782
|
-
}, [task, deleteTask, t]);
|
|
783
|
-
if (!task) {
|
|
784
|
-
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-8 text-muted-foreground", children: [
|
|
785
|
-
/* @__PURE__ */ jsx("p", { className: "text-sm", children: "Task not found" }),
|
|
786
|
-
/* @__PURE__ */ jsx("button", { type: "button", onClick: () => selectTask(null), className: "mt-2 text-xs text-primary hover:underline", children: t("tasks.detail.back") })
|
|
787
|
-
] });
|
|
788
|
-
}
|
|
789
|
-
return (
|
|
790
|
-
// Same frame as the Notes editor: header row with back + icon actions,
|
|
791
|
-
// full-bleed px-4 content, sectioned by hairlines, thin timestamp footer.
|
|
792
|
-
/* @__PURE__ */ jsxs("div", { className: "flex min-h-full flex-col", children: [
|
|
793
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 border-b border-border/40 px-2 py-1.5", children: [
|
|
794
|
-
/* @__PURE__ */ jsxs(
|
|
795
|
-
"button",
|
|
796
|
-
{
|
|
797
|
-
type: "button",
|
|
798
|
-
onClick: () => selectTask(null),
|
|
799
|
-
className: "inline-flex items-center gap-1 rounded-lg px-1.5 py-1 text-[11.5px] text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
|
|
800
|
-
children: [
|
|
801
|
-
/* @__PURE__ */ jsx(ArrowLeft, { className: "h-3.5 w-3.5" }),
|
|
802
|
-
t("tasks.drawer.title")
|
|
803
|
-
]
|
|
804
|
-
}
|
|
805
|
-
),
|
|
806
|
-
/* @__PURE__ */ jsx("span", { className: "flex-1" }),
|
|
807
|
-
/* @__PURE__ */ jsx(
|
|
808
|
-
"button",
|
|
809
|
-
{
|
|
810
|
-
type: "button",
|
|
811
|
-
onClick: handleDelete,
|
|
812
|
-
"aria-label": t("tasks.detail.delete"),
|
|
813
|
-
title: t("tasks.detail.delete"),
|
|
814
|
-
className: "inline-flex h-7 w-7 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-destructive",
|
|
815
|
-
children: /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5" })
|
|
816
|
-
}
|
|
817
|
-
)
|
|
818
|
-
] }),
|
|
819
|
-
/* @__PURE__ */ jsx(
|
|
820
|
-
"input",
|
|
821
|
-
{
|
|
822
|
-
type: "text",
|
|
823
|
-
value: title,
|
|
824
|
-
onChange: (e) => setTitle(e.target.value),
|
|
825
|
-
onBlur: handleTitleBlur,
|
|
826
|
-
onKeyDown: (e) => {
|
|
827
|
-
if (e.key === "Enter") e.currentTarget.blur();
|
|
828
|
-
},
|
|
829
|
-
className: "bg-transparent px-4 pt-3 text-sm font-semibold text-foreground outline-none"
|
|
830
|
-
}
|
|
831
|
-
),
|
|
832
|
-
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-1.5 px-4 pt-2", children: [
|
|
833
|
-
/* @__PURE__ */ jsxs("div", { className: "relative", children: [
|
|
834
|
-
/* @__PURE__ */ jsx(
|
|
835
|
-
"select",
|
|
836
|
-
{
|
|
837
|
-
value: task.status,
|
|
838
|
-
onChange: (e) => updateTask(task.id, { status: e.target.value }),
|
|
839
|
-
className: "appearance-none rounded-full border bg-background pl-5 pr-6 py-1 text-xs font-medium cursor-pointer hover:border-primary/40 transition-colors",
|
|
840
|
-
children: TASK_STATUSES.map((s) => /* @__PURE__ */ jsx("option", { value: s, children: t(`tasks.status.${s}`) }, s))
|
|
841
|
-
}
|
|
842
|
-
),
|
|
843
|
-
/* @__PURE__ */ jsx("span", { className: `absolute left-2 top-1/2 -translate-y-1/2 h-2 w-2 rounded-full ${STATUS_DOT[task.status]}` }),
|
|
844
|
-
/* @__PURE__ */ jsx(ChevronDown, { className: "absolute right-1.5 top-1/2 -translate-y-1/2 h-3 w-3 text-muted-foreground pointer-events-none" })
|
|
845
|
-
] }),
|
|
846
|
-
/* @__PURE__ */ jsxs("div", { className: "relative", children: [
|
|
847
|
-
/* @__PURE__ */ jsx(
|
|
848
|
-
"select",
|
|
849
|
-
{
|
|
850
|
-
value: task.priority,
|
|
851
|
-
onChange: (e) => updateTask(task.id, { priority: e.target.value }),
|
|
852
|
-
className: "appearance-none rounded-full border bg-background pl-2 pr-6 py-1 text-xs font-medium cursor-pointer hover:border-primary/40 transition-colors",
|
|
853
|
-
children: TASK_PRIORITIES.map((p) => /* @__PURE__ */ jsx("option", { value: p, children: t(`tasks.priority.${p}`) }, p))
|
|
854
|
-
}
|
|
855
|
-
),
|
|
856
|
-
/* @__PURE__ */ jsx(ChevronDown, { className: "absolute right-1.5 top-1/2 -translate-y-1/2 h-3 w-3 text-muted-foreground pointer-events-none" })
|
|
857
|
-
] }),
|
|
858
|
-
/* @__PURE__ */ jsxs("div", { className: "relative", children: [
|
|
859
|
-
/* @__PURE__ */ jsx(
|
|
860
|
-
"input",
|
|
861
|
-
{
|
|
862
|
-
type: "date",
|
|
863
|
-
value: task.dueDate ?? "",
|
|
864
|
-
onChange: (e) => updateTask(task.id, { dueDate: e.target.value || null }),
|
|
865
|
-
className: "appearance-none rounded-full border bg-background pl-6 pr-2 py-1 text-xs font-medium cursor-pointer hover:border-primary/40 transition-colors w-[120px]"
|
|
866
|
-
}
|
|
867
|
-
),
|
|
868
|
-
/* @__PURE__ */ jsx(Calendar, { className: "absolute left-2 top-1/2 -translate-y-1/2 h-3 w-3 text-muted-foreground pointer-events-none" })
|
|
869
|
-
] }),
|
|
870
|
-
task.labels.length > 0 && /* @__PURE__ */ jsx(TaskLabelBadges, { labelIds: task.labels, max: 3 })
|
|
871
|
-
] }),
|
|
872
|
-
/* @__PURE__ */ jsx(
|
|
873
|
-
"textarea",
|
|
874
|
-
{
|
|
875
|
-
value: description,
|
|
876
|
-
onChange: (e) => setDescription(e.target.value),
|
|
877
|
-
onBlur: handleDescriptionBlur,
|
|
878
|
-
placeholder: t("tasks.detail.descriptionPlaceholder"),
|
|
879
|
-
className: "min-h-[72px] flex-1 resize-none bg-transparent px-4 py-3 text-sm leading-relaxed text-foreground outline-none placeholder:text-muted-foreground/60"
|
|
880
|
-
}
|
|
881
|
-
),
|
|
882
|
-
/* @__PURE__ */ jsx("div", { className: "px-4 pb-2", children: /* @__PURE__ */ jsxs(
|
|
883
|
-
"button",
|
|
884
|
-
{
|
|
885
|
-
type: "button",
|
|
886
|
-
onClick: () => delegate(
|
|
887
|
-
`${t("tasks.detail.chatWithAi.prompt")}
|
|
888
|
-
|
|
889
|
-
"${task.title}"${task.description ? `
|
|
890
|
-
${task.description}` : ""}${task.dueDate ? `
|
|
891
|
-
(${t("tasks.detail.dueDate")}: ${task.dueDate})` : ""}`
|
|
892
|
-
),
|
|
893
|
-
className: "group flex w-full items-center justify-center gap-2 rounded-xl bg-gradient-to-r from-primary via-primary/80 to-primary bg-[length:200%_100%] bg-left px-3 py-2 text-[13px] font-semibold text-primary-foreground shadow-sm transition-all duration-500 hover:bg-right hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
894
|
-
children: [
|
|
895
|
-
/* @__PURE__ */ jsx(Sparkles, { className: "h-4 w-4 transition-transform duration-300 group-hover:rotate-12" }),
|
|
896
|
-
t("tasks.detail.chatWithAi")
|
|
897
|
-
]
|
|
898
|
-
}
|
|
899
|
-
) }),
|
|
900
|
-
/* @__PURE__ */ jsxs("div", { className: "border-t border-border/40 px-4 py-2.5", children: [
|
|
901
|
-
/* @__PURE__ */ jsxs("p", { className: "pb-1.5 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground/60", children: [
|
|
902
|
-
t("tasks.detail.subtasks"),
|
|
903
|
-
subtasks.length > 0 ? ` \xB7 ${subtasks.length}` : ""
|
|
904
|
-
] }),
|
|
905
|
-
/* @__PURE__ */ jsx("div", { className: "-mx-3", children: /* @__PURE__ */ jsx(TaskQuickAdd, { parentId: task.id }) }),
|
|
906
|
-
subtasks.length > 0 && /* @__PURE__ */ jsx("div", { className: "divide-y divide-border/40", children: subtasks.map((st) => /* @__PURE__ */ jsx(TaskCard, { task: st }, st.id)) })
|
|
907
|
-
] }),
|
|
908
|
-
/* @__PURE__ */ jsxs("p", { className: "border-t border-border/40 px-4 py-1.5 text-[10px] text-muted-foreground/60", children: [
|
|
909
|
-
t("tasks.detail.created"),
|
|
910
|
-
" ",
|
|
911
|
-
formatDateTime(task.createdAt),
|
|
912
|
-
" \xB7 ",
|
|
913
|
-
t("tasks.detail.updated"),
|
|
914
|
-
" ",
|
|
915
|
-
formatDateTime(task.updatedAt)
|
|
916
|
-
] })
|
|
917
|
-
] })
|
|
918
|
-
);
|
|
919
|
-
}
|
|
920
|
-
var STATUS_ORDER = ["todo", "in_progress", "done", "cancelled"];
|
|
921
|
-
function TasksPanel() {
|
|
922
|
-
const t = useTranslation();
|
|
923
|
-
const selectedTaskId = useTasksStore((s) => s.selectedTaskId);
|
|
924
|
-
const tasks = useTasksStore((s) => s.tasks);
|
|
925
|
-
const tasksLoading = useTasksStore((s) => s.tasksLoading);
|
|
926
|
-
const activeFilter = useTasksStore((s) => s.activeFilter);
|
|
927
|
-
const fetchTasks = useTasksStore((s) => s.fetchTasks);
|
|
928
|
-
const fetchLabels = useTasksStore((s) => s.fetchLabels);
|
|
929
|
-
const fetchSummary = useTasksStore((s) => s.fetchSummary);
|
|
930
|
-
React4.useEffect(() => {
|
|
931
|
-
void fetchTasks();
|
|
932
|
-
void fetchLabels();
|
|
933
|
-
void fetchSummary();
|
|
934
|
-
}, [fetchTasks, fetchLabels, fetchSummary]);
|
|
935
|
-
const grouped = React4.useMemo(() => {
|
|
936
|
-
const map = /* @__PURE__ */ new Map();
|
|
937
|
-
for (const s of STATUS_ORDER) map.set(s, []);
|
|
938
|
-
for (const task of tasks) {
|
|
939
|
-
const group = map.get(task.status);
|
|
940
|
-
if (group) group.push(task);
|
|
941
|
-
}
|
|
942
|
-
return map;
|
|
943
|
-
}, [tasks]);
|
|
944
|
-
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
945
|
-
!selectedTaskId && /* @__PURE__ */ jsx(TasksFilterBar, {}),
|
|
946
|
-
!selectedTaskId && /* @__PURE__ */ jsx(TaskQuickAdd, {}),
|
|
947
|
-
/* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 overflow-y-auto", children: selectedTaskId ? /* @__PURE__ */ jsx(TaskDetail, { taskId: selectedTaskId }) : tasksLoading && tasks.length === 0 ? /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center py-16", children: /* @__PURE__ */ jsx("div", { className: "h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent" }) }) : tasks.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-16 text-muted-foreground", children: [
|
|
948
|
-
/* @__PURE__ */ jsx(ClipboardList, { className: "mb-3 h-10 w-10 opacity-40" }),
|
|
949
|
-
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: t("tasks.empty") }),
|
|
950
|
-
/* @__PURE__ */ jsx("p", { className: "mt-1 text-xs", children: t("tasks.empty.description") })
|
|
951
|
-
] }) : activeFilter !== "all" ? /* @__PURE__ */ jsx("div", { className: "py-1", children: tasks.map((task) => /* @__PURE__ */ jsx("div", { className: "px-2", children: /* @__PURE__ */ jsx(TaskCard, { task }) }, task.id)) }) : /* @__PURE__ */ jsx("div", { className: "space-y-2 py-2", children: STATUS_ORDER.filter((s) => s !== "cancelled" || (grouped.get(s)?.length ?? 0) > 0).map((status) => /* @__PURE__ */ jsx(TaskStatusGroup, { status, tasks: grouped.get(status) ?? [] }, status)) }) })
|
|
952
|
-
] });
|
|
953
|
-
}
|
|
954
|
-
function useTasksSummaryLine() {
|
|
955
|
-
const t = useTranslation();
|
|
956
|
-
const summary = useTasksStore((s) => s.summary);
|
|
957
|
-
return React4.useMemo(() => {
|
|
958
|
-
if (!summary) return "";
|
|
959
|
-
const parts = [];
|
|
960
|
-
if (summary.overdue > 0) parts.push(`${summary.overdue} ${t("tasks.summary.overdue")}`);
|
|
961
|
-
if (summary.dueToday > 0) parts.push(`${summary.dueToday} ${t("tasks.summary.dueToday")}`);
|
|
962
|
-
return parts.join(" \xB7 ");
|
|
963
|
-
}, [summary, t]);
|
|
964
|
-
}
|
|
965
|
-
function TasksDrawer() {
|
|
966
|
-
const t = useTranslation();
|
|
967
|
-
const drawerOpen = useTasksStore((s) => s.drawerOpen);
|
|
968
|
-
const closeDrawer = useTasksStore((s) => s.closeDrawer);
|
|
969
|
-
const summaryLine = useTasksSummaryLine();
|
|
970
|
-
return /* @__PURE__ */ jsx(Sheet, { open: drawerOpen, onOpenChange: (v) => {
|
|
971
|
-
if (!v) closeDrawer();
|
|
972
|
-
}, children: /* @__PURE__ */ jsxs(SheetContent, { width: "max-w-lg", overlay: "dim", children: [
|
|
973
|
-
/* @__PURE__ */ jsxs(SheetHeader, { children: [
|
|
974
|
-
/* @__PURE__ */ jsx(SheetTitle, { children: t("tasks.drawer.title") }),
|
|
975
|
-
/* @__PURE__ */ jsx(SheetDescription, { children: summaryLine || t("tasks.drawer.subtitle") })
|
|
976
|
-
] }),
|
|
977
|
-
/* @__PURE__ */ jsx(SheetBody, { className: "flex flex-col px-0 py-0", children: /* @__PURE__ */ jsx(TasksPanel, {}) })
|
|
978
|
-
] }) });
|
|
979
|
-
}
|
|
980
|
-
function TasksLauncher({ config }) {
|
|
981
|
-
const { tasksConfig, tasksProvider, tasksStore } = config;
|
|
982
|
-
const t = useTranslation();
|
|
184
|
+
function TasksLauncher() {
|
|
983
185
|
const railMounted = useRightRailStore((s) => s.mounted);
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
const Panel = React4.useMemo(() => {
|
|
987
|
-
const RailPanel = () => /* @__PURE__ */ jsx(TasksContextProvider, { config: tasksConfig, provider: tasksProvider, store: tasksStore, children: /* @__PURE__ */ jsx(TasksPanel, {}) });
|
|
988
|
-
RailPanel.displayName = "TasksRailPanel";
|
|
989
|
-
return RailPanel;
|
|
990
|
-
}, [tasksConfig, tasksProvider, tasksStore]);
|
|
991
|
-
useRightRailPanel(
|
|
992
|
-
React4.useMemo(
|
|
993
|
-
() => railMounted ? {
|
|
994
|
-
id: "tasks",
|
|
995
|
-
label: t("tasks.drawer.title") || "Tasks",
|
|
996
|
-
icon: CheckSquare,
|
|
997
|
-
order: 20,
|
|
998
|
-
badge: { count: openCount, tone: "destructive" },
|
|
999
|
-
Component: Panel
|
|
1000
|
-
} : null,
|
|
1001
|
-
[railMounted, t, Panel, openCount]
|
|
1002
|
-
)
|
|
1003
|
-
);
|
|
1004
|
-
return /* @__PURE__ */ jsxs(TasksContextProvider, { config: tasksConfig, provider: tasksProvider, store: tasksStore, children: [
|
|
1005
|
-
/* @__PURE__ */ jsx(TasksLauncherRow, { store: tasksStore, railMounted }),
|
|
186
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
187
|
+
/* @__PURE__ */ jsx(TasksLauncherRow, { railMounted }),
|
|
1006
188
|
!railMounted && /* @__PURE__ */ jsx(TasksDrawer, {})
|
|
1007
189
|
] });
|
|
1008
190
|
}
|
|
1009
|
-
function TasksLauncherRow({
|
|
1010
|
-
store,
|
|
1011
|
-
railMounted
|
|
1012
|
-
}) {
|
|
191
|
+
function TasksLauncherRow({ railMounted }) {
|
|
1013
192
|
const t = useTranslation();
|
|
1014
|
-
const toggleDrawer =
|
|
1015
|
-
const fetchSummary =
|
|
1016
|
-
const summary =
|
|
193
|
+
const toggleDrawer = useTasksStore((s) => s.toggleDrawer);
|
|
194
|
+
const fetchSummary = useTasksStore((s) => s.fetchSummary);
|
|
195
|
+
const summary = useTasksStore((s) => s.summary);
|
|
1017
196
|
const togglePanel = useRightRailStore((s) => s.togglePanel);
|
|
1018
197
|
useEffect(() => {
|
|
1019
198
|
fetchSummary();
|
|
@@ -1152,8 +331,56 @@ DROP POLICY IF EXISTS plg_tasks_labels_delete ON public.plg_tasks_labels;
|
|
|
1152
331
|
CREATE POLICY plg_tasks_labels_delete ON public.plg_tasks_labels FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
1153
332
|
GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_tasks_labels TO authenticated;
|
|
1154
333
|
`;
|
|
334
|
+
var MIGRATION_002_TASKS_TO_SPINE = `-- 002_tasks_to_spine.sql \u2014 carry this plugin's rows into the spine's tasks table.
|
|
335
|
+
--
|
|
336
|
+
-- public.tasks (db migration 047) is now the one table admin work lives in: the
|
|
337
|
+
-- shell's getting-started checklist writes it, so it has to exist in apps that
|
|
338
|
+
-- never installed this plugin. This file moves what plg_tasks_tasks already has.
|
|
339
|
+
--
|
|
340
|
+
-- WHY THE COPY IS HERE AND NOT IN 047. \`fayz db apply\` runs the spine BEFORE
|
|
341
|
+
-- plugins (cli/src/lib/migration-plan.ts). On a fresh pool 047 runs while
|
|
342
|
+
-- plg_tasks_tasks does not exist yet, and a checksum-frozen file cannot try
|
|
343
|
+
-- again later. Here, both tables are guaranteed to exist.
|
|
344
|
+
--
|
|
345
|
+
-- plg_tasks_tasks is NOT dropped. It is retired in place; a later migration
|
|
346
|
+
-- removes it once the fleet has been verified on the new table.
|
|
347
|
+
--
|
|
348
|
+
-- Idempotent.
|
|
1155
349
|
|
|
1156
|
-
|
|
350
|
+
DO $$
|
|
351
|
+
BEGIN
|
|
352
|
+
-- An app on an older @fayz-ai/db has no spine table to copy into. Say so and
|
|
353
|
+
-- leave: the plugin still works against its own table until db is upgraded.
|
|
354
|
+
IF to_regclass('public.tasks') IS NULL THEN
|
|
355
|
+
RAISE NOTICE 'plugin-tasks 002: public.tasks not found \u2014 upgrade @fayz-ai/db to move tasks to the spine';
|
|
356
|
+
RETURN;
|
|
357
|
+
END IF;
|
|
358
|
+
|
|
359
|
+
IF to_regclass('public.plg_tasks_tasks') IS NULL THEN
|
|
360
|
+
RETURN; -- fresh pool: nothing to carry
|
|
361
|
+
END IF;
|
|
362
|
+
|
|
363
|
+
-- The id is preserved deliberately. The notes bridge keys a note's todos by
|
|
364
|
+
-- task id and stores the link as a 'note:<id>' string in \`labels\`; a new id
|
|
365
|
+
-- here would orphan every note todo in the fleet.
|
|
366
|
+
--
|
|
367
|
+
-- Roots before children so the self-referencing parent_id always resolves.
|
|
368
|
+
-- auth.uid() is NULL in a migration session and both of 047's stamping
|
|
369
|
+
-- triggers COALESCE, so these explicit values win.
|
|
370
|
+
INSERT INTO public.tasks (
|
|
371
|
+
id, tenant_id, title, description, status, priority, due_date,
|
|
372
|
+
assigned_to_id, assigned_to_name, parent_id, labels, position,
|
|
373
|
+
created_by, created_by_name, owner_id, created_at, updated_at
|
|
374
|
+
)
|
|
375
|
+
SELECT
|
|
376
|
+
src.id, src.tenant_id, src.title, src.description, src.status, src.priority, src.due_date,
|
|
377
|
+
src.assigned_to_id, src.assigned_to_name, src.parent_id, src.labels, src.position,
|
|
378
|
+
src.created_by_id, src.created_by_name, src.created_by_id, src.created_at, src.updated_at
|
|
379
|
+
FROM public.plg_tasks_tasks src
|
|
380
|
+
ORDER BY src.parent_id NULLS FIRST
|
|
381
|
+
ON CONFLICT (id) DO NOTHING;
|
|
382
|
+
END $$;
|
|
383
|
+
`;
|
|
1157
384
|
var DEFAULT_LABELS = {
|
|
1158
385
|
drawerTitle: "Tasks",
|
|
1159
386
|
settingsTitle: "Tasks",
|
|
@@ -1167,11 +394,8 @@ function resolveConfig(options) {
|
|
|
1167
394
|
function createTasksPlugin(options) {
|
|
1168
395
|
const config = resolveConfig(options);
|
|
1169
396
|
registerTranslations(tasksLocales);
|
|
1170
|
-
const provider = options?.dataProvider ??
|
|
1171
|
-
|
|
1172
|
-
() => createMockTasksProvider()
|
|
1173
|
-
);
|
|
1174
|
-
const store = createTasksStore(provider);
|
|
397
|
+
const provider = options?.dataProvider ?? getTasksProvider();
|
|
398
|
+
const store = useTasksStore;
|
|
1175
399
|
registerNotesTasksAdapter({
|
|
1176
400
|
async createTodo({ title, noteId, noteTitle }) {
|
|
1177
401
|
const task = await provider.createTask({
|
|
@@ -1198,6 +422,7 @@ function createTasksPlugin(options) {
|
|
|
1198
422
|
});
|
|
1199
423
|
return {
|
|
1200
424
|
id: "tasks",
|
|
425
|
+
defaultAgentRole: "setup",
|
|
1201
426
|
name: config.labels.drawerTitle,
|
|
1202
427
|
icon: "CheckSquare",
|
|
1203
428
|
version: "1.0.0",
|
|
@@ -1217,8 +442,7 @@ function createTasksPlugin(options) {
|
|
|
1217
442
|
order: 10,
|
|
1218
443
|
props: {
|
|
1219
444
|
tasksConfig: config,
|
|
1220
|
-
tasksProvider: provider
|
|
1221
|
-
tasksStore: store
|
|
445
|
+
tasksProvider: provider
|
|
1222
446
|
}
|
|
1223
447
|
}
|
|
1224
448
|
],
|
|
@@ -1253,15 +477,7 @@ function createTasksPlugin(options) {
|
|
|
1253
477
|
id: "tasks",
|
|
1254
478
|
label: config.labels.settingsTitle,
|
|
1255
479
|
icon: "CheckSquare",
|
|
1256
|
-
component:
|
|
1257
|
-
const Tab = () => React4.createElement(
|
|
1258
|
-
TasksContextProvider,
|
|
1259
|
-
{ config, provider, store },
|
|
1260
|
-
React4.createElement(TasksGeneralSettings)
|
|
1261
|
-
);
|
|
1262
|
-
Tab.displayName = "TasksSettingsTab";
|
|
1263
|
-
return Tab;
|
|
1264
|
-
})(),
|
|
480
|
+
component: TasksGeneralSettings,
|
|
1265
481
|
order: 14
|
|
1266
482
|
}
|
|
1267
483
|
],
|
|
@@ -1277,6 +493,12 @@ function createTasksPlugin(options) {
|
|
|
1277
493
|
version: "1.0.0",
|
|
1278
494
|
sql: MIGRATION_001_TASKS_BASE,
|
|
1279
495
|
description: "Create plg_tasks_tasks and plg_tasks_labels"
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
id: "tasks-002-to-spine",
|
|
499
|
+
version: "1.1.0",
|
|
500
|
+
sql: MIGRATION_002_TASKS_TO_SPINE,
|
|
501
|
+
description: "Copy plg_tasks_tasks rows into the spine public.tasks (ids preserved; the old table is retired in place, not dropped)"
|
|
1280
502
|
}
|
|
1281
503
|
],
|
|
1282
504
|
locales: tasksLocales
|