@allpepper/task-orchestrator-tui 1.3.0 → 2.0.0
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/package.json +2 -2
- package/src/tui/components/status-actions.tsx +59 -21
- package/src/tui/screens/dashboard.tsx +2 -69
- package/src/tui/screens/feature-detail.tsx +77 -49
- package/src/tui/screens/project-detail.tsx +8 -65
- package/src/tui/screens/project-view.tsx +76 -80
- package/src/tui/screens/task-detail.tsx +46 -19
- package/src/ui/adapters/direct.ts +323 -94
- package/src/ui/adapters/types.ts +75 -72
- package/src/ui/hooks/use-data.ts +71 -199
- package/src/ui/hooks/use-feature-kanban.ts +45 -30
- package/src/ui/hooks/use-kanban.ts +35 -27
- package/src/ui/index.ts +1 -0
- package/src/ui/lib/colors.ts +27 -24
- package/src/ui/lib/types.ts +0 -1
- package/src/ui/themes/dark.ts +10 -28
- package/src/ui/themes/light.ts +16 -34
- package/src/ui/themes/types.ts +14 -26
package/src/ui/hooks/use-data.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback, useMemo } from 'react';
|
|
2
2
|
import { useAdapter } from '../context/adapter-context';
|
|
3
|
-
import type { Project, Task, Feature, Section, EntityType,
|
|
3
|
+
import type { Project, Task, Feature, Section, EntityType, Priority } from '@allpepper/task-orchestrator';
|
|
4
4
|
import type { FeatureWithTasks, ProjectOverview, SearchResults, DependencyInfo, BoardCard, BoardTask } from '../lib/types';
|
|
5
5
|
import type { TreeRow } from '../../tui/components/tree-view';
|
|
6
|
+
import { isCompletedStatus } from '../lib/colors';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Task counts structure
|
|
@@ -18,7 +19,7 @@ export interface TaskCounts {
|
|
|
18
19
|
export function calculateTaskCounts(tasks: Task[]): TaskCounts {
|
|
19
20
|
return {
|
|
20
21
|
total: tasks.length,
|
|
21
|
-
completed: tasks.filter(t => t.status
|
|
22
|
+
completed: tasks.filter(t => isCompletedStatus(t.status)).length,
|
|
22
23
|
};
|
|
23
24
|
}
|
|
24
25
|
|
|
@@ -32,7 +33,7 @@ export function calculateTaskCountsByProject(tasks: Task[]): Map<string, TaskCou
|
|
|
32
33
|
if (task.projectId) {
|
|
33
34
|
const counts = countsByProject.get(task.projectId) || { total: 0, completed: 0 };
|
|
34
35
|
counts.total++;
|
|
35
|
-
if (task.status
|
|
36
|
+
if (isCompletedStatus(task.status)) {
|
|
36
37
|
counts.completed++;
|
|
37
38
|
}
|
|
38
39
|
countsByProject.set(task.projectId, counts);
|
|
@@ -50,11 +51,6 @@ export interface FeatureCounts {
|
|
|
50
51
|
completed: number;
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
/**
|
|
54
|
-
* Terminal feature statuses considered "completed"
|
|
55
|
-
*/
|
|
56
|
-
const COMPLETED_FEATURE_STATUSES = ['COMPLETED', 'DEPLOYED'];
|
|
57
|
-
|
|
58
54
|
/**
|
|
59
55
|
* Group features by project ID and calculate counts for each
|
|
60
56
|
*/
|
|
@@ -65,7 +61,7 @@ export function calculateFeatureCountsByProject(featureList: Feature[]): Map<str
|
|
|
65
61
|
if (feature.projectId) {
|
|
66
62
|
const counts = countsByProject.get(feature.projectId) || { total: 0, completed: 0 };
|
|
67
63
|
counts.total++;
|
|
68
|
-
if (
|
|
64
|
+
if (isCompletedStatus(feature.status)) {
|
|
69
65
|
counts.completed++;
|
|
70
66
|
}
|
|
71
67
|
countsByProject.set(feature.projectId, counts);
|
|
@@ -85,14 +81,6 @@ export interface ProjectWithCounts extends Project {
|
|
|
85
81
|
|
|
86
82
|
/**
|
|
87
83
|
* Hook for fetching and managing the list of projects for the dashboard.
|
|
88
|
-
* Includes task counts for each project.
|
|
89
|
-
*
|
|
90
|
-
* @returns Project list state with loading/error states and refresh function
|
|
91
|
-
*
|
|
92
|
-
* @example
|
|
93
|
-
* ```tsx
|
|
94
|
-
* const { projects, loading, error, refresh } = useProjects();
|
|
95
|
-
* ```
|
|
96
84
|
*/
|
|
97
85
|
export function useProjects() {
|
|
98
86
|
const { adapter } = useAdapter();
|
|
@@ -104,11 +92,10 @@ export function useProjects() {
|
|
|
104
92
|
setLoading(true);
|
|
105
93
|
setError(null);
|
|
106
94
|
|
|
107
|
-
// Fetch projects, tasks, and features in parallel
|
|
108
95
|
const [projectsResult, tasksResult, featuresResult] = await Promise.all([
|
|
109
96
|
adapter.getProjects(),
|
|
110
|
-
adapter.getTasks({ limit: 1000 }),
|
|
111
|
-
adapter.getFeatures({ limit: 1000 }),
|
|
97
|
+
adapter.getTasks({ limit: 1000 }),
|
|
98
|
+
adapter.getFeatures({ limit: 1000 }),
|
|
112
99
|
]);
|
|
113
100
|
|
|
114
101
|
if (!projectsResult.success) {
|
|
@@ -117,17 +104,14 @@ export function useProjects() {
|
|
|
117
104
|
return;
|
|
118
105
|
}
|
|
119
106
|
|
|
120
|
-
// Build task counts by project using shared utility
|
|
121
107
|
const taskCountsByProject = tasksResult.success
|
|
122
108
|
? calculateTaskCountsByProject(tasksResult.data)
|
|
123
109
|
: new Map<string, TaskCounts>();
|
|
124
110
|
|
|
125
|
-
// Build feature counts by project using shared utility
|
|
126
111
|
const featureCountsByProject = featuresResult.success
|
|
127
112
|
? calculateFeatureCountsByProject(featuresResult.data)
|
|
128
113
|
: new Map<string, FeatureCounts>();
|
|
129
114
|
|
|
130
|
-
// Merge task and feature counts into projects
|
|
131
115
|
const projectsWithCounts: ProjectWithCounts[] = projectsResult.data.map(project => ({
|
|
132
116
|
...project,
|
|
133
117
|
taskCounts: taskCountsByProject.get(project.id) || { total: 0, completed: 0 },
|
|
@@ -152,14 +136,6 @@ export function useProjects() {
|
|
|
152
136
|
|
|
153
137
|
/**
|
|
154
138
|
* Hook for fetching a single project with its overview statistics.
|
|
155
|
-
*
|
|
156
|
-
* @param id - The project ID
|
|
157
|
-
* @returns Project and overview state with loading/error states and refresh function
|
|
158
|
-
*
|
|
159
|
-
* @example
|
|
160
|
-
* ```tsx
|
|
161
|
-
* const { project, overview, loading, error, refresh } = useProjectOverview('proj-123');
|
|
162
|
-
* ```
|
|
163
139
|
*/
|
|
164
140
|
export function useProjectOverview(id: string) {
|
|
165
141
|
const { adapter } = useAdapter();
|
|
@@ -186,7 +162,6 @@ export function useProjectOverview(id: string) {
|
|
|
186
162
|
if (overviewResult.success) {
|
|
187
163
|
setOverview(overviewResult.data);
|
|
188
164
|
} else if (!error) {
|
|
189
|
-
// Only set error if project fetch didn't already fail
|
|
190
165
|
setError(overviewResult.error);
|
|
191
166
|
}
|
|
192
167
|
|
|
@@ -207,16 +182,16 @@ export function useProjectOverview(id: string) {
|
|
|
207
182
|
}
|
|
208
183
|
|
|
209
184
|
/**
|
|
210
|
-
*
|
|
185
|
+
* v2 pipeline status order for task columns
|
|
186
|
+
* Tasks: NEW → ACTIVE → TO_BE_TESTED → READY_TO_PROD → CLOSED (+ WILL_NOT_IMPLEMENT)
|
|
211
187
|
*/
|
|
212
|
-
const
|
|
213
|
-
'
|
|
214
|
-
'
|
|
215
|
-
'
|
|
216
|
-
'
|
|
217
|
-
'
|
|
218
|
-
'
|
|
219
|
-
'CANCELLED' as TaskStatus,
|
|
188
|
+
const TASK_STATUS_ORDER: string[] = [
|
|
189
|
+
'NEW',
|
|
190
|
+
'ACTIVE',
|
|
191
|
+
'TO_BE_TESTED',
|
|
192
|
+
'READY_TO_PROD',
|
|
193
|
+
'CLOSED',
|
|
194
|
+
'WILL_NOT_IMPLEMENT',
|
|
220
195
|
];
|
|
221
196
|
|
|
222
197
|
/**
|
|
@@ -228,65 +203,43 @@ const PRIORITY_ORDER: Record<Priority, number> = {
|
|
|
228
203
|
LOW: 1,
|
|
229
204
|
};
|
|
230
205
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
'
|
|
236
|
-
'
|
|
206
|
+
/**
|
|
207
|
+
* Board status order (subset for kanban view)
|
|
208
|
+
*/
|
|
209
|
+
const BOARD_STATUS_ORDER: string[] = [
|
|
210
|
+
'NEW',
|
|
211
|
+
'ACTIVE',
|
|
212
|
+
'TO_BE_TESTED',
|
|
213
|
+
'READY_TO_PROD',
|
|
214
|
+
'CLOSED',
|
|
237
215
|
];
|
|
238
216
|
|
|
239
217
|
/**
|
|
240
|
-
* Display names for
|
|
218
|
+
* Display names for v2 pipeline statuses
|
|
241
219
|
*/
|
|
242
220
|
const STATUS_DISPLAY_NAMES: Record<string, string> = {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
READY_FOR_QA: 'Ready for QA',
|
|
250
|
-
INVESTIGATING: 'Investigating',
|
|
251
|
-
BLOCKED: 'Blocked',
|
|
252
|
-
ON_HOLD: 'On Hold',
|
|
253
|
-
DEPLOYED: 'Deployed',
|
|
254
|
-
COMPLETED: 'Completed',
|
|
255
|
-
CANCELLED: 'Cancelled',
|
|
256
|
-
DEFERRED: 'Deferred',
|
|
257
|
-
DRAFT: 'Draft',
|
|
258
|
-
PLANNING: 'Planning',
|
|
259
|
-
IN_DEVELOPMENT: 'In Development',
|
|
260
|
-
VALIDATING: 'Validating',
|
|
261
|
-
PENDING_REVIEW: 'Pending Review',
|
|
262
|
-
ARCHIVED: 'Archived',
|
|
221
|
+
NEW: 'New',
|
|
222
|
+
ACTIVE: 'Active',
|
|
223
|
+
TO_BE_TESTED: 'To Be Tested',
|
|
224
|
+
READY_TO_PROD: 'Ready to Prod',
|
|
225
|
+
CLOSED: 'Closed',
|
|
226
|
+
WILL_NOT_IMPLEMENT: 'Will Not Implement',
|
|
263
227
|
};
|
|
264
228
|
|
|
265
229
|
/**
|
|
266
|
-
* Feature status order for grouping features
|
|
230
|
+
* Feature status order for grouping features
|
|
231
|
+
* Features: NEW → ACTIVE → READY_TO_PROD → CLOSED (+ WILL_NOT_IMPLEMENT)
|
|
267
232
|
*/
|
|
268
|
-
const FEATURE_STATUS_ORDER:
|
|
269
|
-
'
|
|
270
|
-
'
|
|
271
|
-
'
|
|
272
|
-
'
|
|
273
|
-
'
|
|
274
|
-
'PENDING_REVIEW' as FeatureStatus,
|
|
275
|
-
'BLOCKED' as FeatureStatus,
|
|
276
|
-
'ON_HOLD' as FeatureStatus,
|
|
277
|
-
'DEPLOYED' as FeatureStatus,
|
|
278
|
-
'COMPLETED' as FeatureStatus,
|
|
279
|
-
'ARCHIVED' as FeatureStatus,
|
|
233
|
+
const FEATURE_STATUS_ORDER: string[] = [
|
|
234
|
+
'NEW',
|
|
235
|
+
'ACTIVE',
|
|
236
|
+
'READY_TO_PROD',
|
|
237
|
+
'CLOSED',
|
|
238
|
+
'WILL_NOT_IMPLEMENT',
|
|
280
239
|
];
|
|
281
240
|
|
|
282
241
|
/**
|
|
283
242
|
* Build status-grouped tree rows for tasks
|
|
284
|
-
* Groups tasks by status, then by feature within each status
|
|
285
|
-
*
|
|
286
|
-
* @param tasks - All tasks to group
|
|
287
|
-
* @param features - Features to lookup task feature info
|
|
288
|
-
* @param expandedGroups - Set of expanded group IDs (both status groups and composite feature keys)
|
|
289
|
-
* @returns TreeRow[] grouped by status → feature → tasks
|
|
290
243
|
*/
|
|
291
244
|
function buildStatusGroupedRows(
|
|
292
245
|
tasks: Task[],
|
|
@@ -301,39 +254,14 @@ function buildStatusGroupedRows(
|
|
|
301
254
|
}
|
|
302
255
|
|
|
303
256
|
// Group tasks by status
|
|
304
|
-
const tasksByStatus = new Map<
|
|
257
|
+
const tasksByStatus = new Map<string, Task[]>();
|
|
305
258
|
for (const task of tasks) {
|
|
306
|
-
const
|
|
307
|
-
const group = tasksByStatus.get(status) || [];
|
|
259
|
+
const group = tasksByStatus.get(task.status) || [];
|
|
308
260
|
group.push(task);
|
|
309
|
-
tasksByStatus.set(status, group);
|
|
261
|
+
tasksByStatus.set(task.status, group);
|
|
310
262
|
}
|
|
311
263
|
|
|
312
|
-
|
|
313
|
-
switch (status) {
|
|
314
|
-
case 'COMPLETED':
|
|
315
|
-
case 'DEPLOYED':
|
|
316
|
-
return 'COMPLETED' as TaskStatus;
|
|
317
|
-
case 'BLOCKED':
|
|
318
|
-
return 'BLOCKED' as TaskStatus;
|
|
319
|
-
case 'ON_HOLD':
|
|
320
|
-
return 'ON_HOLD' as TaskStatus;
|
|
321
|
-
case 'ARCHIVED':
|
|
322
|
-
return 'CANCELLED' as TaskStatus;
|
|
323
|
-
case 'PENDING_REVIEW':
|
|
324
|
-
return 'IN_REVIEW' as TaskStatus;
|
|
325
|
-
case 'IN_DEVELOPMENT':
|
|
326
|
-
case 'TESTING':
|
|
327
|
-
case 'VALIDATING':
|
|
328
|
-
return 'IN_PROGRESS' as TaskStatus;
|
|
329
|
-
case 'PLANNING':
|
|
330
|
-
case 'DRAFT':
|
|
331
|
-
default:
|
|
332
|
-
return 'PENDING' as TaskStatus;
|
|
333
|
-
}
|
|
334
|
-
};
|
|
335
|
-
|
|
336
|
-
// Only inject empty features into status buckets to avoid duplicate feature rows across statuses.
|
|
264
|
+
// Track which features have tasks
|
|
337
265
|
const featureHasTasks = new Set<string>();
|
|
338
266
|
for (const task of tasks) {
|
|
339
267
|
if (task.featureId) {
|
|
@@ -341,20 +269,33 @@ function buildStatusGroupedRows(
|
|
|
341
269
|
}
|
|
342
270
|
}
|
|
343
271
|
|
|
344
|
-
//
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
272
|
+
// Map feature status to the closest task status for empty features
|
|
273
|
+
const featureStatusToTaskBucket = (featureStatus: string): string => {
|
|
274
|
+
switch (featureStatus) {
|
|
275
|
+
case 'CLOSED':
|
|
276
|
+
case 'WILL_NOT_IMPLEMENT':
|
|
277
|
+
return featureStatus;
|
|
278
|
+
case 'ACTIVE':
|
|
279
|
+
case 'READY_TO_PROD':
|
|
280
|
+
return featureStatus;
|
|
281
|
+
case 'NEW':
|
|
282
|
+
default:
|
|
283
|
+
return 'NEW';
|
|
349
284
|
}
|
|
350
|
-
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// Group empty features by their mapped status bucket
|
|
288
|
+
const featuresByStatus = new Map<string, FeatureWithTasks[]>();
|
|
289
|
+
for (const feature of features) {
|
|
290
|
+
if (featureHasTasks.has(feature.id)) continue;
|
|
291
|
+
const mappedStatus = featureStatusToTaskBucket(feature.status);
|
|
351
292
|
const group = featuresByStatus.get(mappedStatus) || [];
|
|
352
293
|
group.push(feature);
|
|
353
294
|
featuresByStatus.set(mappedStatus, group);
|
|
354
295
|
}
|
|
355
296
|
|
|
356
297
|
// Build rows in status order
|
|
357
|
-
for (const status of
|
|
298
|
+
for (const status of TASK_STATUS_ORDER) {
|
|
358
299
|
const statusTasks = tasksByStatus.get(status) || [];
|
|
359
300
|
const statusFeatures = featuresByStatus.get(status) || [];
|
|
360
301
|
if (statusTasks.length === 0 && statusFeatures.length === 0) continue;
|
|
@@ -363,7 +304,6 @@ function buildStatusGroupedRows(
|
|
|
363
304
|
const statusExpanded = expandedGroups.has(statusGroupId);
|
|
364
305
|
const statusExpandable = statusTasks.length > 0 || statusFeatures.length > 0;
|
|
365
306
|
|
|
366
|
-
// Add status group row (depth 0)
|
|
367
307
|
rows.push({
|
|
368
308
|
type: 'group',
|
|
369
309
|
id: statusGroupId,
|
|
@@ -375,9 +315,8 @@ function buildStatusGroupedRows(
|
|
|
375
315
|
expandable: statusExpandable,
|
|
376
316
|
});
|
|
377
317
|
|
|
378
|
-
// If status group is expanded, group tasks by feature
|
|
379
318
|
if (statusExpanded) {
|
|
380
|
-
// Group tasks by featureId
|
|
319
|
+
// Group tasks by featureId
|
|
381
320
|
const tasksByFeature = new Map<string | null, Task[]>();
|
|
382
321
|
for (const task of statusTasks) {
|
|
383
322
|
const featureId = task.featureId || null;
|
|
@@ -386,7 +325,7 @@ function buildStatusGroupedRows(
|
|
|
386
325
|
tasksByFeature.set(featureId, group);
|
|
387
326
|
}
|
|
388
327
|
|
|
389
|
-
// Sort tasks within each feature by priority
|
|
328
|
+
// Sort tasks within each feature by priority then title
|
|
390
329
|
for (const [_, featureTasks] of tasksByFeature.entries()) {
|
|
391
330
|
featureTasks.sort((a, b) => {
|
|
392
331
|
const priorityDiff = PRIORITY_ORDER[b.priority] - PRIORITY_ORDER[a.priority];
|
|
@@ -396,7 +335,6 @@ function buildStatusGroupedRows(
|
|
|
396
335
|
}
|
|
397
336
|
|
|
398
337
|
// Build feature sub-groups
|
|
399
|
-
// First, collect features that have tasks in this status (sorted by creation date)
|
|
400
338
|
const tasksByFeatureId = new Map<string, Task[]>();
|
|
401
339
|
for (const [featureId, featureTasks] of tasksByFeature.entries()) {
|
|
402
340
|
if (featureId !== null) {
|
|
@@ -404,7 +342,7 @@ function buildStatusGroupedRows(
|
|
|
404
342
|
}
|
|
405
343
|
}
|
|
406
344
|
|
|
407
|
-
// Sort
|
|
345
|
+
// Sort features by creation date
|
|
408
346
|
const statusFeatureMap = new Map<string, FeatureWithTasks>();
|
|
409
347
|
for (const feature of statusFeatures) {
|
|
410
348
|
statusFeatureMap.set(feature.id, feature);
|
|
@@ -420,7 +358,6 @@ function buildStatusGroupedRows(
|
|
|
420
358
|
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
|
421
359
|
);
|
|
422
360
|
|
|
423
|
-
// Add feature sub-group rows
|
|
424
361
|
for (const feature of sortedStatusFeatures) {
|
|
425
362
|
const featureId = feature.id;
|
|
426
363
|
const featureTasks = tasksByFeatureId.get(featureId) || [];
|
|
@@ -428,7 +365,6 @@ function buildStatusGroupedRows(
|
|
|
428
365
|
const featureExpandable = featureTasks.length > 0;
|
|
429
366
|
const featureExpanded = featureExpandable && expandedGroups.has(compositeFeatureId);
|
|
430
367
|
|
|
431
|
-
// Add feature group row (depth 1)
|
|
432
368
|
rows.push({
|
|
433
369
|
type: 'group',
|
|
434
370
|
id: compositeFeatureId,
|
|
@@ -441,7 +377,6 @@ function buildStatusGroupedRows(
|
|
|
441
377
|
featureId,
|
|
442
378
|
});
|
|
443
379
|
|
|
444
|
-
// Add task rows if feature is expanded (depth 1)
|
|
445
380
|
if (featureExpanded) {
|
|
446
381
|
featureTasks.forEach((task, index) => {
|
|
447
382
|
const isLast = index === featureTasks.length - 1;
|
|
@@ -450,37 +385,33 @@ function buildStatusGroupedRows(
|
|
|
450
385
|
task,
|
|
451
386
|
isLast,
|
|
452
387
|
depth: 2,
|
|
453
|
-
// No featureName needed - tasks are nested under their feature
|
|
454
388
|
});
|
|
455
389
|
});
|
|
456
390
|
}
|
|
457
391
|
}
|
|
458
392
|
|
|
459
|
-
// Add unassigned tasks
|
|
393
|
+
// Add unassigned tasks
|
|
460
394
|
const unassignedTasks = tasksByFeature.get(null);
|
|
461
395
|
if (unassignedTasks && unassignedTasks.length > 0) {
|
|
462
396
|
const unassignedId = `${status}:unassigned`;
|
|
463
397
|
const unassignedExpanded = expandedGroups.has(unassignedId);
|
|
464
398
|
|
|
465
|
-
// Sort unassigned tasks by priority (descending) then title
|
|
466
399
|
unassignedTasks.sort((a, b) => {
|
|
467
400
|
const priorityDiff = PRIORITY_ORDER[b.priority] - PRIORITY_ORDER[a.priority];
|
|
468
401
|
if (priorityDiff !== 0) return priorityDiff;
|
|
469
402
|
return a.title.localeCompare(b.title);
|
|
470
403
|
});
|
|
471
404
|
|
|
472
|
-
// Add unassigned group row (depth 1)
|
|
473
405
|
rows.push({
|
|
474
406
|
type: 'group',
|
|
475
407
|
id: unassignedId,
|
|
476
408
|
label: 'Unassigned',
|
|
477
|
-
status: status,
|
|
409
|
+
status: status,
|
|
478
410
|
taskCount: unassignedTasks.length,
|
|
479
411
|
expanded: unassignedExpanded,
|
|
480
412
|
depth: 1,
|
|
481
413
|
});
|
|
482
414
|
|
|
483
|
-
// Add task rows if unassigned group is expanded (depth 1)
|
|
484
415
|
if (unassignedExpanded) {
|
|
485
416
|
unassignedTasks.forEach((task, index) => {
|
|
486
417
|
const isLast = index === unassignedTasks.length - 1;
|
|
@@ -501,11 +432,6 @@ function buildStatusGroupedRows(
|
|
|
501
432
|
|
|
502
433
|
/**
|
|
503
434
|
* Build feature-status-grouped tree rows
|
|
504
|
-
* Groups features by their feature status, then nests tasks within each feature
|
|
505
|
-
*
|
|
506
|
-
* @param features - All features with their tasks
|
|
507
|
-
* @param expandedGroups - Set of expanded group IDs
|
|
508
|
-
* @returns TreeRow[] grouped by feature status → feature → tasks
|
|
509
435
|
*/
|
|
510
436
|
function buildFeatureStatusGroupedRows(
|
|
511
437
|
features: FeatureWithTasks[],
|
|
@@ -513,16 +439,13 @@ function buildFeatureStatusGroupedRows(
|
|
|
513
439
|
): TreeRow[] {
|
|
514
440
|
const rows: TreeRow[] = [];
|
|
515
441
|
|
|
516
|
-
// Group features by their status
|
|
517
442
|
const featuresByStatus = new Map<string, FeatureWithTasks[]>();
|
|
518
443
|
for (const feature of features) {
|
|
519
|
-
const
|
|
520
|
-
const group = featuresByStatus.get(status) || [];
|
|
444
|
+
const group = featuresByStatus.get(feature.status) || [];
|
|
521
445
|
group.push(feature);
|
|
522
|
-
featuresByStatus.set(status, group);
|
|
446
|
+
featuresByStatus.set(feature.status, group);
|
|
523
447
|
}
|
|
524
448
|
|
|
525
|
-
// Build rows in feature status order
|
|
526
449
|
for (const status of FEATURE_STATUS_ORDER) {
|
|
527
450
|
const statusFeatures = featuresByStatus.get(status) || [];
|
|
528
451
|
if (statusFeatures.length === 0) continue;
|
|
@@ -530,10 +453,6 @@ function buildFeatureStatusGroupedRows(
|
|
|
530
453
|
const statusGroupId = `fs:${status}`;
|
|
531
454
|
const statusExpanded = expandedGroups.has(statusGroupId);
|
|
532
455
|
|
|
533
|
-
// Count total tasks across all features in this status
|
|
534
|
-
const totalTasks = statusFeatures.reduce((sum, f) => sum + f.tasks.length, 0);
|
|
535
|
-
|
|
536
|
-
// Add status group row (depth 0)
|
|
537
456
|
rows.push({
|
|
538
457
|
type: 'group',
|
|
539
458
|
id: statusGroupId,
|
|
@@ -546,7 +465,6 @@ function buildFeatureStatusGroupedRows(
|
|
|
546
465
|
});
|
|
547
466
|
|
|
548
467
|
if (statusExpanded) {
|
|
549
|
-
// Sort features by creation date (oldest first)
|
|
550
468
|
const sortedFeatures = [...statusFeatures].sort(
|
|
551
469
|
(a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
|
552
470
|
);
|
|
@@ -556,7 +474,6 @@ function buildFeatureStatusGroupedRows(
|
|
|
556
474
|
const featureExpandable = feature.tasks.length > 0;
|
|
557
475
|
const featureExpanded = featureExpandable && expandedGroups.has(compositeFeatureId);
|
|
558
476
|
|
|
559
|
-
// Add feature group row (depth 1)
|
|
560
477
|
rows.push({
|
|
561
478
|
type: 'group',
|
|
562
479
|
id: compositeFeatureId,
|
|
@@ -569,9 +486,7 @@ function buildFeatureStatusGroupedRows(
|
|
|
569
486
|
featureId: feature.id,
|
|
570
487
|
});
|
|
571
488
|
|
|
572
|
-
// Add task rows if feature is expanded (depth 2)
|
|
573
489
|
if (featureExpanded) {
|
|
574
|
-
// Sort tasks by priority (descending) then title
|
|
575
490
|
const sortedTasks = [...feature.tasks].sort((a, b) => {
|
|
576
491
|
const priorityDiff = PRIORITY_ORDER[b.priority] - PRIORITY_ORDER[a.priority];
|
|
577
492
|
if (priorityDiff !== 0) return priorityDiff;
|
|
@@ -596,15 +511,6 @@ function buildFeatureStatusGroupedRows(
|
|
|
596
511
|
|
|
597
512
|
/**
|
|
598
513
|
* Hook for fetching a project tree with features and their tasks.
|
|
599
|
-
* Also includes unassigned tasks (tasks without a feature).
|
|
600
|
-
*
|
|
601
|
-
* @param projectId - The project ID
|
|
602
|
-
* @returns Project, features with nested tasks, unassigned tasks, task counts, status-grouped rows, loading/error states, and refresh function
|
|
603
|
-
*
|
|
604
|
-
* @example
|
|
605
|
-
* ```tsx
|
|
606
|
-
* const { project, features, unassignedTasks, taskCounts, statusGroupedRows, loading, error, refresh } = useProjectTree('proj-123');
|
|
607
|
-
* ```
|
|
608
514
|
*/
|
|
609
515
|
export function useProjectTree(projectId: string, expandedGroups: Set<string> = new Set()) {
|
|
610
516
|
const { adapter } = useAdapter();
|
|
@@ -648,18 +554,15 @@ export function useProjectTree(projectId: string, expandedGroups: Set<string> =
|
|
|
648
554
|
const allFeatures = featuresResult.data;
|
|
649
555
|
const tasks = tasksResult.data;
|
|
650
556
|
|
|
651
|
-
// Sort features by creation date ascending (oldest first)
|
|
652
557
|
const sortedFeatures = [...allFeatures].sort(
|
|
653
558
|
(a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
|
654
559
|
);
|
|
655
560
|
|
|
656
|
-
// Build feature tree with nested tasks
|
|
657
561
|
const featuresWithTasks: FeatureWithTasks[] = sortedFeatures.map((feature) => ({
|
|
658
562
|
...feature,
|
|
659
563
|
tasks: tasks.filter((task) => task.featureId === feature.id),
|
|
660
564
|
}));
|
|
661
565
|
|
|
662
|
-
// Find unassigned tasks (no featureId)
|
|
663
566
|
const unassigned = tasks.filter((task) => !task.featureId);
|
|
664
567
|
|
|
665
568
|
setProject(projectData);
|
|
@@ -674,12 +577,10 @@ export function useProjectTree(projectId: string, expandedGroups: Set<string> =
|
|
|
674
577
|
loadProjectTree();
|
|
675
578
|
}, [loadProjectTree]);
|
|
676
579
|
|
|
677
|
-
// Build status-grouped rows using useMemo to recalculate when data changes
|
|
678
580
|
const statusGroupedRows = useMemo(() => {
|
|
679
581
|
return buildStatusGroupedRows(allTasks, features, expandedGroups);
|
|
680
582
|
}, [allTasks, features, expandedGroups]);
|
|
681
583
|
|
|
682
|
-
// Build feature-status-grouped rows
|
|
683
584
|
const featureStatusGroupedRows = useMemo(() => {
|
|
684
585
|
return buildFeatureStatusGroupedRows(features, expandedGroups);
|
|
685
586
|
}, [features, expandedGroups]);
|
|
@@ -699,7 +600,6 @@ export function useProjectTree(projectId: string, expandedGroups: Set<string> =
|
|
|
699
600
|
|
|
700
601
|
/**
|
|
701
602
|
* Hook for fetching board data (kanban columns) for a project.
|
|
702
|
-
* Returns tasks grouped by status with feature labels for each card.
|
|
703
603
|
*/
|
|
704
604
|
export function useBoardData(projectId: string) {
|
|
705
605
|
const { adapter } = useAdapter();
|
|
@@ -778,14 +678,6 @@ export function useBoardData(projectId: string) {
|
|
|
778
678
|
|
|
779
679
|
/**
|
|
780
680
|
* Hook for fetching a single task with its sections and dependencies.
|
|
781
|
-
*
|
|
782
|
-
* @param id - The task ID
|
|
783
|
-
* @returns Task, sections, dependencies, loading/error states, and refresh function
|
|
784
|
-
*
|
|
785
|
-
* @example
|
|
786
|
-
* ```tsx
|
|
787
|
-
* const { task, sections, dependencies, loading, error, refresh } = useTask('task-123');
|
|
788
|
-
* ```
|
|
789
681
|
*/
|
|
790
682
|
export function useTask(id: string) {
|
|
791
683
|
const { adapter } = useAdapter();
|
|
@@ -842,14 +734,6 @@ export function useTask(id: string) {
|
|
|
842
734
|
|
|
843
735
|
/**
|
|
844
736
|
* Hook for fetching a single feature with its tasks and sections.
|
|
845
|
-
*
|
|
846
|
-
* @param id - The feature ID
|
|
847
|
-
* @returns Feature, tasks, sections, loading/error states, and refresh function
|
|
848
|
-
*
|
|
849
|
-
* @example
|
|
850
|
-
* ```tsx
|
|
851
|
-
* const { feature, tasks, sections, loading, error, refresh } = useFeature('feat-123');
|
|
852
|
-
* ```
|
|
853
737
|
*/
|
|
854
738
|
export function useFeature(id: string) {
|
|
855
739
|
const { adapter } = useAdapter();
|
|
@@ -906,17 +790,6 @@ export function useFeature(id: string) {
|
|
|
906
790
|
|
|
907
791
|
/**
|
|
908
792
|
* Hook for performing full-text search across all entities.
|
|
909
|
-
* Use with useDebounce to avoid excessive API calls.
|
|
910
|
-
*
|
|
911
|
-
* @param query - The search query string
|
|
912
|
-
* @returns Search results with loading/error states
|
|
913
|
-
*
|
|
914
|
-
* @example
|
|
915
|
-
* ```tsx
|
|
916
|
-
* const [inputValue, setInputValue] = useState('');
|
|
917
|
-
* const debouncedQuery = useDebounce(inputValue, 300);
|
|
918
|
-
* const { results, loading, error } = useSearch(debouncedQuery);
|
|
919
|
-
* ```
|
|
920
793
|
*/
|
|
921
794
|
export function useSearch(query: string) {
|
|
922
795
|
const { adapter } = useAdapter();
|
|
@@ -925,7 +798,6 @@ export function useSearch(query: string) {
|
|
|
925
798
|
const [error, setError] = useState<string | null>(null);
|
|
926
799
|
|
|
927
800
|
useEffect(() => {
|
|
928
|
-
// Don't search for empty queries
|
|
929
801
|
if (!query.trim()) {
|
|
930
802
|
setResults(null);
|
|
931
803
|
setLoading(false);
|