@industry-theme/backlogmd-kanban-panel 0.1.0 → 0.1.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/adapters/BacklogAdapter.d.ts +50 -0
- package/dist/adapters/BacklogAdapter.d.ts.map +1 -0
- package/dist/adapters/backlog-parser.d.ts +24 -0
- package/dist/adapters/backlog-parser.d.ts.map +1 -0
- package/dist/adapters/index.d.ts +10 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/types.d.ts +115 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/panels/KanbanPanel.d.ts.map +1 -1
- package/dist/panels/kanban/components/EmptyState.d.ts +12 -0
- package/dist/panels/kanban/components/EmptyState.d.ts.map +1 -0
- package/dist/panels/kanban/hooks/useKanbanData.d.ts +10 -2
- package/dist/panels/kanban/hooks/useKanbanData.d.ts.map +1 -1
- package/dist/panels.bundle.js +632 -187
- package/dist/panels.bundle.js.map +1 -1
- package/package.json +1 -1
package/dist/panels.bundle.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
-
import React2, { forwardRef, createElement, createContext, useState, useEffect, useContext,
|
|
2
|
+
import React2, { forwardRef, createElement, createContext, useState, useEffect, useContext, useRef, useCallback, useMemo } from "react";
|
|
3
3
|
/**
|
|
4
4
|
* @license lucide-react v0.552.0 - ISC
|
|
5
5
|
*
|
|
@@ -105,12 +105,44 @@ const createLucideIcon = (iconName, iconNode) => {
|
|
|
105
105
|
* This source code is licensed under the ISC license.
|
|
106
106
|
* See the LICENSE file in the root directory of this source tree.
|
|
107
107
|
*/
|
|
108
|
-
const __iconNode$
|
|
108
|
+
const __iconNode$4 = [
|
|
109
109
|
["circle", { cx: "12", cy: "12", r: "10", key: "1mglay" }],
|
|
110
110
|
["line", { x1: "12", x2: "12", y1: "8", y2: "12", key: "1pkeuh" }],
|
|
111
111
|
["line", { x1: "12", x2: "12.01", y1: "16", y2: "16", key: "4dfq90" }]
|
|
112
112
|
];
|
|
113
|
-
const CircleAlert = createLucideIcon("circle-alert", __iconNode$
|
|
113
|
+
const CircleAlert = createLucideIcon("circle-alert", __iconNode$4);
|
|
114
|
+
/**
|
|
115
|
+
* @license lucide-react v0.552.0 - ISC
|
|
116
|
+
*
|
|
117
|
+
* This source code is licensed under the ISC license.
|
|
118
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
119
|
+
*/
|
|
120
|
+
const __iconNode$3 = [
|
|
121
|
+
["path", { d: "M15 3h6v6", key: "1q9fwt" }],
|
|
122
|
+
["path", { d: "M10 14 21 3", key: "gplh6r" }],
|
|
123
|
+
["path", { d: "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6", key: "a6xqqp" }]
|
|
124
|
+
];
|
|
125
|
+
const ExternalLink = createLucideIcon("external-link", __iconNode$3);
|
|
126
|
+
/**
|
|
127
|
+
* @license lucide-react v0.552.0 - ISC
|
|
128
|
+
*
|
|
129
|
+
* This source code is licensed under the ISC license.
|
|
130
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
131
|
+
*/
|
|
132
|
+
const __iconNode$2 = [
|
|
133
|
+
[
|
|
134
|
+
"path",
|
|
135
|
+
{
|
|
136
|
+
d: "M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z",
|
|
137
|
+
key: "1oefj6"
|
|
138
|
+
}
|
|
139
|
+
],
|
|
140
|
+
["path", { d: "M14 2v5a1 1 0 0 0 1 1h5", key: "wfsgrz" }],
|
|
141
|
+
["path", { d: "M10 9H8", key: "b1mrlr" }],
|
|
142
|
+
["path", { d: "M16 13H8", key: "t4e002" }],
|
|
143
|
+
["path", { d: "M16 17H8", key: "z1uh3a" }]
|
|
144
|
+
];
|
|
145
|
+
const FileText = createLucideIcon("file-text", __iconNode$2);
|
|
114
146
|
/**
|
|
115
147
|
* @license lucide-react v0.552.0 - ISC
|
|
116
148
|
*
|
|
@@ -351,211 +383,490 @@ var ThemeProvider = ({
|
|
|
351
383
|
}, children);
|
|
352
384
|
};
|
|
353
385
|
var theme = terminalTheme;
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
}
|
|
371
|
-
{
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
{
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
ordinal: 1,
|
|
406
|
-
filePath: "backlog/task-003.md",
|
|
407
|
-
source: "local"
|
|
408
|
-
},
|
|
409
|
-
{
|
|
410
|
-
id: "task-004",
|
|
411
|
-
title: "Create UI component library",
|
|
412
|
-
status: "In Progress",
|
|
413
|
-
assignee: ["diana@example.com", "eve@example.com"],
|
|
414
|
-
createdDate: "2025-11-04T13:45:00Z",
|
|
415
|
-
updatedDate: "2025-11-17T16:20:00Z",
|
|
416
|
-
labels: ["frontend", "ui"],
|
|
417
|
-
dependencies: [],
|
|
418
|
-
description: "Build reusable React components with TypeScript. Include buttons, forms, modals, and data tables.",
|
|
419
|
-
priority: "medium",
|
|
420
|
-
ordinal: 2,
|
|
421
|
-
acceptanceCriteriaItems: [
|
|
422
|
-
{ index: 0, text: "All components are fully typed", checked: true },
|
|
423
|
-
{ index: 1, text: "Components support theme customization", checked: true },
|
|
424
|
-
{ index: 2, text: "Storybook stories for each component", checked: false },
|
|
425
|
-
{ index: 3, text: "Accessibility audit passes", checked: false }
|
|
426
|
-
],
|
|
427
|
-
filePath: "backlog/task-004.md",
|
|
428
|
-
source: "local"
|
|
429
|
-
},
|
|
430
|
-
{
|
|
431
|
-
id: "task-005",
|
|
432
|
-
title: "Set up CI/CD pipeline",
|
|
433
|
-
status: "Done",
|
|
434
|
-
assignee: ["frank@example.com"],
|
|
435
|
-
createdDate: "2025-11-05T08:30:00Z",
|
|
436
|
-
updatedDate: "2025-11-10T17:00:00Z",
|
|
437
|
-
labels: ["devops", "automation"],
|
|
438
|
-
dependencies: [],
|
|
439
|
-
description: "Configure GitHub Actions for automated testing, building, and deployment.",
|
|
440
|
-
priority: "medium",
|
|
441
|
-
ordinal: 1,
|
|
442
|
-
filePath: "backlog/task-005.md",
|
|
443
|
-
source: "completed"
|
|
444
|
-
},
|
|
445
|
-
{
|
|
446
|
-
id: "task-006",
|
|
447
|
-
title: "Write unit tests",
|
|
448
|
-
status: "Done",
|
|
449
|
-
assignee: ["grace@example.com"],
|
|
450
|
-
createdDate: "2025-11-06T10:00:00Z",
|
|
451
|
-
updatedDate: "2025-11-12T14:30:00Z",
|
|
452
|
-
labels: ["testing", "quality"],
|
|
453
|
-
dependencies: ["task-003"],
|
|
454
|
-
description: "Add comprehensive unit tests for all API endpoints and utility functions. Aim for 80%+ coverage.",
|
|
455
|
-
priority: "medium",
|
|
456
|
-
ordinal: 2,
|
|
457
|
-
filePath: "backlog/task-006.md",
|
|
458
|
-
source: "completed"
|
|
459
|
-
},
|
|
460
|
-
{
|
|
461
|
-
id: "task-007",
|
|
462
|
-
title: "Optimize database queries",
|
|
463
|
-
status: "To Do",
|
|
464
|
-
assignee: ["henry@example.com"],
|
|
465
|
-
createdDate: "2025-11-07T15:20:00Z",
|
|
466
|
-
labels: ["performance", "database"],
|
|
467
|
-
dependencies: ["task-002", "task-003"],
|
|
468
|
-
description: "Profile and optimize slow database queries. Add indexes where needed.",
|
|
469
|
-
priority: "low",
|
|
470
|
-
ordinal: 3,
|
|
471
|
-
filePath: "backlog/task-007.md",
|
|
472
|
-
source: "local"
|
|
473
|
-
},
|
|
474
|
-
{
|
|
475
|
-
id: "task-008",
|
|
476
|
-
title: "Implement real-time notifications",
|
|
477
|
-
status: "To Do",
|
|
478
|
-
assignee: [],
|
|
479
|
-
createdDate: "2025-11-08T09:45:00Z",
|
|
480
|
-
labels: ["feature", "realtime"],
|
|
481
|
-
dependencies: ["task-003"],
|
|
482
|
-
description: "Add WebSocket support for real-time notifications when tasks are updated or assigned.",
|
|
483
|
-
priority: "low",
|
|
484
|
-
ordinal: 4,
|
|
485
|
-
filePath: "backlog/task-008.md",
|
|
486
|
-
source: "local"
|
|
487
|
-
}
|
|
488
|
-
];
|
|
386
|
+
class BacklogAdapterError extends Error {
|
|
387
|
+
constructor(message) {
|
|
388
|
+
super(message);
|
|
389
|
+
this.name = "BacklogAdapterError";
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
function parseYaml(yamlString) {
|
|
393
|
+
const lines = yamlString.split("\n");
|
|
394
|
+
const result = {};
|
|
395
|
+
let currentKey = "";
|
|
396
|
+
let isArray = false;
|
|
397
|
+
let arrayItems = [];
|
|
398
|
+
for (const line of lines) {
|
|
399
|
+
const trimmed = line.trim();
|
|
400
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
if (trimmed.startsWith("- ")) {
|
|
404
|
+
if (isArray) {
|
|
405
|
+
const value = trimmed.slice(2).trim();
|
|
406
|
+
arrayItems.push(value.replace(/^["']|["']$/g, ""));
|
|
407
|
+
}
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
if (isArray && !trimmed.startsWith("- ")) {
|
|
411
|
+
result[currentKey] = arrayItems;
|
|
412
|
+
isArray = false;
|
|
413
|
+
arrayItems = [];
|
|
414
|
+
currentKey = "";
|
|
415
|
+
}
|
|
416
|
+
const colonIndex = trimmed.indexOf(":");
|
|
417
|
+
if (colonIndex > 0) {
|
|
418
|
+
const key = trimmed.slice(0, colonIndex).trim();
|
|
419
|
+
const valueStr = trimmed.slice(colonIndex + 1).trim();
|
|
420
|
+
if (valueStr === "" || valueStr === "[]") {
|
|
421
|
+
currentKey = key;
|
|
422
|
+
if (valueStr === "[]") {
|
|
423
|
+
result[key] = [];
|
|
424
|
+
} else {
|
|
425
|
+
isArray = true;
|
|
426
|
+
arrayItems = [];
|
|
427
|
+
}
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
result[key] = parseYamlValue(valueStr);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (isArray && arrayItems.length > 0) {
|
|
434
|
+
result[currentKey] = arrayItems;
|
|
435
|
+
}
|
|
436
|
+
return result;
|
|
489
437
|
}
|
|
490
|
-
function
|
|
491
|
-
|
|
438
|
+
function parseYamlValue(value) {
|
|
439
|
+
const unquoted = value.replace(/^["']|["']$/g, "");
|
|
440
|
+
if (unquoted === "true") return true;
|
|
441
|
+
if (unquoted === "false") return false;
|
|
442
|
+
if (unquoted === "null" || unquoted === "~") return null;
|
|
443
|
+
if (/^-?\d+\.?\d*$/.test(unquoted)) {
|
|
444
|
+
return Number(unquoted);
|
|
445
|
+
}
|
|
446
|
+
return unquoted;
|
|
492
447
|
}
|
|
493
|
-
function
|
|
494
|
-
const
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
448
|
+
function parseBacklogConfig(content) {
|
|
449
|
+
const parsed = parseYaml(content);
|
|
450
|
+
if (!parsed.project_name || typeof parsed.project_name !== "string") {
|
|
451
|
+
throw new BacklogAdapterError(
|
|
452
|
+
"Invalid config.yml: missing or invalid project_name"
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
if (!parsed.statuses || !Array.isArray(parsed.statuses)) {
|
|
456
|
+
throw new BacklogAdapterError(
|
|
457
|
+
"Invalid config.yml: missing or invalid statuses array"
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
return {
|
|
461
|
+
project_name: parsed.project_name,
|
|
462
|
+
default_status: parsed.default_status || "To Do",
|
|
463
|
+
statuses: parsed.statuses,
|
|
464
|
+
labels: parsed.labels || [],
|
|
465
|
+
milestones: parsed.milestones || [],
|
|
466
|
+
date_format: parsed.date_format || "yyyy-mm-dd hh:mm",
|
|
467
|
+
default_editor: parsed.default_editor,
|
|
468
|
+
auto_commit: parsed.auto_commit,
|
|
469
|
+
zero_padded_ids: parsed.zero_padded_ids
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
function parseTaskFile(content, filepath) {
|
|
473
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
474
|
+
if (!frontmatterMatch) {
|
|
475
|
+
throw new BacklogAdapterError(
|
|
476
|
+
`Invalid task file: ${filepath} - missing YAML frontmatter`
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
const parsed = parseYaml(frontmatterMatch[1]);
|
|
480
|
+
const metadata = validateTaskMetadata(parsed, filepath);
|
|
481
|
+
const body = content.slice(frontmatterMatch[0].length).trim();
|
|
482
|
+
const description = extractSection(body, "Description");
|
|
483
|
+
const acceptanceCriteria = extractAcceptanceCriteria(body);
|
|
484
|
+
const implementationPlan = extractSection(body, "Implementation Plan");
|
|
485
|
+
const notes = extractSection(body, "Implementation Notes");
|
|
486
|
+
return {
|
|
487
|
+
...metadata,
|
|
488
|
+
// Normalize field names to match existing backlog-types
|
|
489
|
+
createdDate: metadata.created_date,
|
|
490
|
+
updatedDate: metadata.updated_date,
|
|
491
|
+
parentTaskId: metadata.parent,
|
|
492
|
+
// Content fields
|
|
493
|
+
body,
|
|
494
|
+
rawContent: body,
|
|
495
|
+
description,
|
|
496
|
+
acceptanceCriteriaItems: acceptanceCriteria,
|
|
497
|
+
implementationPlan,
|
|
498
|
+
implementationNotes: notes,
|
|
499
|
+
// Set source based on filepath
|
|
500
|
+
source: filepath.includes("/completed/") ? "completed" : "local",
|
|
501
|
+
filePath: filepath
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
function validateTaskMetadata(parsed, filepath) {
|
|
505
|
+
if (!parsed.id || typeof parsed.id !== "string") {
|
|
506
|
+
throw new BacklogAdapterError(
|
|
507
|
+
`Invalid task file: ${filepath} - missing or invalid id`
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
if (!parsed.title || typeof parsed.title !== "string") {
|
|
511
|
+
throw new BacklogAdapterError(
|
|
512
|
+
`Invalid task file: ${filepath} - missing or invalid title`
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
if (!parsed.status || typeof parsed.status !== "string") {
|
|
516
|
+
throw new BacklogAdapterError(
|
|
517
|
+
`Invalid task file: ${filepath} - missing or invalid status`
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
if (!parsed.created_date || typeof parsed.created_date !== "string") {
|
|
521
|
+
throw new BacklogAdapterError(
|
|
522
|
+
`Invalid task file: ${filepath} - missing or invalid created_date`
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
return {
|
|
526
|
+
id: parsed.id,
|
|
527
|
+
title: parsed.title,
|
|
528
|
+
status: parsed.status,
|
|
529
|
+
created_date: parsed.created_date,
|
|
530
|
+
assignee: Array.isArray(parsed.assignee) ? parsed.assignee : [],
|
|
531
|
+
updated_date: parsed.updated_date,
|
|
532
|
+
labels: Array.isArray(parsed.labels) ? parsed.labels : [],
|
|
533
|
+
dependencies: Array.isArray(parsed.dependencies) ? parsed.dependencies : [],
|
|
534
|
+
priority: parsed.priority || void 0,
|
|
535
|
+
ordinal: parsed.ordinal,
|
|
536
|
+
parent: parsed.parent
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
function extractSection(body, heading) {
|
|
540
|
+
const regex = new RegExp(
|
|
541
|
+
`## ${heading}\\n\\n([\\s\\S]*?)(?=\\n## |$)`,
|
|
542
|
+
"i"
|
|
543
|
+
);
|
|
544
|
+
const match = body.match(regex);
|
|
545
|
+
return match ? match[1].trim() : void 0;
|
|
546
|
+
}
|
|
547
|
+
function extractAcceptanceCriteria(body) {
|
|
548
|
+
const acMatch = body.match(
|
|
549
|
+
/## Acceptance Criteria\n<!-- AC:BEGIN -->\n([\s\S]*?)\n<!-- AC:END -->/
|
|
550
|
+
);
|
|
551
|
+
if (!acMatch) {
|
|
552
|
+
return void 0;
|
|
553
|
+
}
|
|
554
|
+
const acSection = acMatch[1];
|
|
555
|
+
const lines = acSection.split("\n");
|
|
556
|
+
const criteria = [];
|
|
557
|
+
for (const line of lines) {
|
|
558
|
+
const trimmed = line.trim();
|
|
559
|
+
const match = trimmed.match(/^- \[([ x])\] #(\d+) (.+)$/);
|
|
560
|
+
if (match) {
|
|
561
|
+
const [, checked, idStr, text] = match;
|
|
562
|
+
criteria.push({
|
|
563
|
+
index: parseInt(idStr, 10),
|
|
564
|
+
text: text.trim(),
|
|
565
|
+
checked: checked === "x"
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return criteria.length > 0 ? criteria : void 0;
|
|
570
|
+
}
|
|
571
|
+
function sortTasks(tasks) {
|
|
572
|
+
return tasks.sort((a, b) => {
|
|
573
|
+
if (a.ordinal !== void 0 && b.ordinal !== void 0) {
|
|
574
|
+
return a.ordinal - b.ordinal;
|
|
575
|
+
}
|
|
576
|
+
const priorityOrder = { high: 3, medium: 2, low: 1 };
|
|
577
|
+
const aPriority = priorityOrder[a.priority || "medium"];
|
|
578
|
+
const bPriority = priorityOrder[b.priority || "medium"];
|
|
579
|
+
if (aPriority !== bPriority) {
|
|
580
|
+
return bPriority - aPriority;
|
|
581
|
+
}
|
|
582
|
+
const aDate = new Date(a.created_date).getTime();
|
|
583
|
+
const bDate = new Date(b.created_date).getTime();
|
|
584
|
+
return bDate - aDate;
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
class BacklogAdapter {
|
|
588
|
+
constructor(fileAccess) {
|
|
589
|
+
this.configCache = null;
|
|
590
|
+
this.tasksCache = null;
|
|
591
|
+
this.fileAccess = fileAccess;
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Check if the repository is a Backlog.md project
|
|
595
|
+
*/
|
|
596
|
+
isBacklogProject() {
|
|
597
|
+
const files = this.fileAccess.listFiles();
|
|
598
|
+
return files.some((path) => path === "backlog/config.yml");
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Get the Backlog.md project configuration
|
|
602
|
+
*/
|
|
603
|
+
async getConfig() {
|
|
604
|
+
if (this.configCache) {
|
|
605
|
+
return this.configCache;
|
|
606
|
+
}
|
|
607
|
+
if (!this.isBacklogProject()) {
|
|
608
|
+
throw new BacklogAdapterError(
|
|
609
|
+
"Not a Backlog.md project: backlog/config.yml not found"
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
try {
|
|
613
|
+
const content = await this.fileAccess.fetchFile("backlog/config.yml");
|
|
614
|
+
const config = parseBacklogConfig(content);
|
|
615
|
+
this.configCache = config;
|
|
616
|
+
return config;
|
|
617
|
+
} catch (error) {
|
|
618
|
+
if (error instanceof BacklogAdapterError) {
|
|
619
|
+
throw error;
|
|
620
|
+
}
|
|
621
|
+
throw new BacklogAdapterError(
|
|
622
|
+
`Failed to load config: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Get the list of status columns from config
|
|
628
|
+
*/
|
|
629
|
+
async getStatuses() {
|
|
630
|
+
const config = await this.getConfig();
|
|
631
|
+
return config.statuses;
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Get all tasks from the backlog
|
|
635
|
+
*/
|
|
636
|
+
async getTasks(includeCompleted = true) {
|
|
637
|
+
if (this.tasksCache) {
|
|
638
|
+
return this.tasksCache;
|
|
639
|
+
}
|
|
640
|
+
const files = this.fileAccess.listFiles();
|
|
641
|
+
const taskPaths = this.findTaskFiles(files, includeCompleted);
|
|
642
|
+
if (taskPaths.length === 0) {
|
|
643
|
+
return [];
|
|
644
|
+
}
|
|
645
|
+
try {
|
|
646
|
+
const taskPromises = taskPaths.map(async (path) => {
|
|
647
|
+
try {
|
|
648
|
+
const content = await this.fileAccess.fetchFile(path);
|
|
649
|
+
return parseTaskFile(content, path);
|
|
650
|
+
} catch (error) {
|
|
651
|
+
console.error(`Failed to parse task file ${path}:`, error);
|
|
652
|
+
return null;
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
const taskResults = await Promise.all(taskPromises);
|
|
656
|
+
const tasks = taskResults.filter((task) => task !== null);
|
|
657
|
+
this.tasksCache = tasks;
|
|
658
|
+
return tasks;
|
|
659
|
+
} catch (error) {
|
|
660
|
+
throw new BacklogAdapterError(
|
|
661
|
+
`Failed to load tasks: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Get tasks grouped by status
|
|
667
|
+
*/
|
|
668
|
+
async getTasksByStatus(includeCompleted = true) {
|
|
669
|
+
const [config, tasks] = await Promise.all([
|
|
670
|
+
this.getConfig(),
|
|
671
|
+
this.getTasks(includeCompleted)
|
|
672
|
+
]);
|
|
499
673
|
const grouped = /* @__PURE__ */ new Map();
|
|
500
|
-
for (const status of statuses) {
|
|
674
|
+
for (const status of config.statuses) {
|
|
501
675
|
grouped.set(status, []);
|
|
502
676
|
}
|
|
503
677
|
for (const task of tasks) {
|
|
504
|
-
const statusKey = task.status
|
|
505
|
-
const
|
|
506
|
-
if (
|
|
507
|
-
|
|
678
|
+
const statusKey = task.status || config.default_status;
|
|
679
|
+
const column = grouped.get(statusKey);
|
|
680
|
+
if (column) {
|
|
681
|
+
column.push(task);
|
|
682
|
+
} else {
|
|
683
|
+
console.warn(
|
|
684
|
+
`Task ${task.id} has unknown status: ${task.status}. Placing in default status.`
|
|
685
|
+
);
|
|
686
|
+
const defaultColumn = grouped.get(config.default_status);
|
|
687
|
+
if (defaultColumn) {
|
|
688
|
+
defaultColumn.push(task);
|
|
689
|
+
}
|
|
508
690
|
}
|
|
509
691
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
692
|
+
grouped.forEach((tasks2, status) => {
|
|
693
|
+
grouped.set(status, sortTasks(tasks2));
|
|
694
|
+
});
|
|
695
|
+
return grouped;
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Clear all caches (useful for refresh)
|
|
699
|
+
*/
|
|
700
|
+
clearCache() {
|
|
701
|
+
this.configCache = null;
|
|
702
|
+
this.tasksCache = null;
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Find all task files in the repository
|
|
706
|
+
*/
|
|
707
|
+
findTaskFiles(files, includeCompleted) {
|
|
708
|
+
const taskFiles = [];
|
|
709
|
+
for (const path of files) {
|
|
710
|
+
if (path.startsWith("backlog/tasks/") && this.isTaskFile(path)) {
|
|
711
|
+
taskFiles.push(path);
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
if (includeCompleted && path.startsWith("backlog/completed/") && this.isTaskFile(path)) {
|
|
715
|
+
taskFiles.push(path);
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
return taskFiles;
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Check if a file path is a task file
|
|
723
|
+
*/
|
|
724
|
+
isTaskFile(path) {
|
|
725
|
+
if (!path.endsWith(".md")) {
|
|
726
|
+
return false;
|
|
727
|
+
}
|
|
728
|
+
const filename = path.split("/").pop() || "";
|
|
729
|
+
return /^task-[\d.]+/.test(filename);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
function createBacklogAdapter(fileAccess) {
|
|
733
|
+
return new BacklogAdapter(fileAccess);
|
|
734
|
+
}
|
|
735
|
+
function useKanbanData(options) {
|
|
736
|
+
const { context, actions } = options || {};
|
|
737
|
+
const [tasks, setTasks] = useState([]);
|
|
738
|
+
const [statuses, setStatuses] = useState(["To Do", "In Progress", "Done"]);
|
|
739
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
740
|
+
const [error, setError] = useState(null);
|
|
741
|
+
const [isBacklogProject, setIsBacklogProject] = useState(false);
|
|
742
|
+
const activeFilePathRef = useRef(null);
|
|
743
|
+
const fetchFileContent = useCallback(
|
|
744
|
+
async (path) => {
|
|
745
|
+
if (!actions || !context) {
|
|
746
|
+
throw new Error("PanelContext not available");
|
|
747
|
+
}
|
|
748
|
+
if (activeFilePathRef.current === path) {
|
|
749
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
750
|
+
}
|
|
751
|
+
activeFilePathRef.current = path;
|
|
752
|
+
try {
|
|
753
|
+
if (actions.openFile) {
|
|
754
|
+
await actions.openFile(path);
|
|
755
|
+
} else {
|
|
756
|
+
throw new Error("openFile action not available");
|
|
514
757
|
}
|
|
515
|
-
const
|
|
516
|
-
const
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
return bPriority - aPriority;
|
|
758
|
+
const activeFileSlice = context.getRepositorySlice("active-file");
|
|
759
|
+
const fileData = activeFileSlice == null ? void 0 : activeFileSlice.data;
|
|
760
|
+
if (!(fileData == null ? void 0 : fileData.content)) {
|
|
761
|
+
throw new Error(`Failed to fetch content for ${path}`);
|
|
520
762
|
}
|
|
521
|
-
return
|
|
522
|
-
}
|
|
523
|
-
|
|
763
|
+
return fileData.content;
|
|
764
|
+
} finally {
|
|
765
|
+
activeFilePathRef.current = null;
|
|
766
|
+
}
|
|
767
|
+
},
|
|
768
|
+
[actions, context]
|
|
769
|
+
);
|
|
770
|
+
const loadBacklogData = useCallback(async () => {
|
|
771
|
+
if (!context || !actions) {
|
|
772
|
+
console.log("[useKanbanData] No context provided");
|
|
773
|
+
setIsBacklogProject(false);
|
|
774
|
+
setTasks([]);
|
|
775
|
+
setStatuses(["To Do", "In Progress", "Done"]);
|
|
776
|
+
setIsLoading(false);
|
|
777
|
+
return;
|
|
524
778
|
}
|
|
525
|
-
return grouped;
|
|
526
|
-
}, [tasks, statuses]);
|
|
527
|
-
const refreshData = useCallback(async () => {
|
|
528
779
|
setIsLoading(true);
|
|
529
780
|
setError(null);
|
|
530
781
|
try {
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
782
|
+
const fileTreeSlice = context.getRepositorySlice("fileTree");
|
|
783
|
+
if (!(fileTreeSlice == null ? void 0 : fileTreeSlice.data)) {
|
|
784
|
+
console.log("[useKanbanData] FileTree not available");
|
|
785
|
+
setIsBacklogProject(false);
|
|
786
|
+
setTasks([]);
|
|
787
|
+
setStatuses(["To Do", "In Progress", "Done"]);
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
const files = fileTreeSlice.data.files || [];
|
|
791
|
+
const filePaths = files.map((f) => f.path);
|
|
792
|
+
const adapter = createBacklogAdapter({
|
|
793
|
+
fetchFile: fetchFileContent,
|
|
794
|
+
listFiles: () => filePaths
|
|
795
|
+
});
|
|
796
|
+
if (!adapter.isBacklogProject()) {
|
|
797
|
+
console.log("[useKanbanData] Not a Backlog.md project");
|
|
798
|
+
setIsBacklogProject(false);
|
|
799
|
+
setTasks([]);
|
|
800
|
+
setStatuses(["To Do", "In Progress", "Done"]);
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
console.log("[useKanbanData] Loading Backlog.md data...");
|
|
804
|
+
setIsBacklogProject(true);
|
|
805
|
+
const [loadedStatuses, tasksByStatus2] = await Promise.all([
|
|
806
|
+
adapter.getStatuses(),
|
|
807
|
+
adapter.getTasksByStatus(true)
|
|
808
|
+
]);
|
|
809
|
+
const allTasks = [];
|
|
810
|
+
tasksByStatus2.forEach((tasks2) => {
|
|
811
|
+
allTasks.push(...tasks2);
|
|
812
|
+
});
|
|
813
|
+
console.log(
|
|
814
|
+
`[useKanbanData] Loaded ${allTasks.length} tasks with ${loadedStatuses.length} statuses`
|
|
815
|
+
);
|
|
816
|
+
setStatuses(loadedStatuses);
|
|
817
|
+
setTasks(allTasks);
|
|
534
818
|
} catch (err) {
|
|
535
|
-
|
|
819
|
+
console.error("[useKanbanData] Failed to load Backlog.md data:", err);
|
|
820
|
+
if (err instanceof BacklogAdapterError) {
|
|
821
|
+
setError(err.message);
|
|
822
|
+
} else {
|
|
823
|
+
setError(
|
|
824
|
+
err instanceof Error ? err.message : "Failed to load backlog data"
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
setIsBacklogProject(false);
|
|
828
|
+
setTasks([]);
|
|
829
|
+
setStatuses(["To Do", "In Progress", "Done"]);
|
|
536
830
|
} finally {
|
|
537
831
|
setIsLoading(false);
|
|
538
832
|
}
|
|
539
|
-
}, []);
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
833
|
+
}, [context, actions, fetchFileContent]);
|
|
834
|
+
useEffect(() => {
|
|
835
|
+
loadBacklogData();
|
|
836
|
+
}, [loadBacklogData]);
|
|
837
|
+
const tasksByStatus = useMemo(() => {
|
|
838
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
839
|
+
for (const status of statuses) {
|
|
840
|
+
grouped.set(status, []);
|
|
841
|
+
}
|
|
842
|
+
for (const task of tasks) {
|
|
843
|
+
const statusKey = task.status ?? "";
|
|
844
|
+
const list = grouped.get(statusKey);
|
|
845
|
+
if (list) {
|
|
846
|
+
list.push(task);
|
|
847
|
+
}
|
|
552
848
|
}
|
|
553
|
-
|
|
849
|
+
return grouped;
|
|
850
|
+
}, [tasks, statuses]);
|
|
851
|
+
const refreshData = useCallback(async () => {
|
|
852
|
+
await loadBacklogData();
|
|
853
|
+
}, [loadBacklogData]);
|
|
854
|
+
const updateTaskStatus = useCallback(
|
|
855
|
+
async (taskId, newStatus) => {
|
|
856
|
+
setError(null);
|
|
857
|
+
console.warn(
|
|
858
|
+
"[useKanbanData] Task status updates not yet implemented for Backlog.md"
|
|
859
|
+
);
|
|
860
|
+
setError("Task editing is not yet supported");
|
|
861
|
+
},
|
|
862
|
+
[]
|
|
863
|
+
);
|
|
554
864
|
return {
|
|
555
865
|
tasks,
|
|
556
866
|
statuses,
|
|
557
867
|
isLoading,
|
|
558
868
|
error,
|
|
869
|
+
isBacklogProject,
|
|
559
870
|
tasksByStatus,
|
|
560
871
|
refreshData,
|
|
561
872
|
updateTaskStatus
|
|
@@ -767,8 +1078,141 @@ const KanbanColumn = ({
|
|
|
767
1078
|
}
|
|
768
1079
|
);
|
|
769
1080
|
};
|
|
1081
|
+
const EmptyState = ({
|
|
1082
|
+
message = "No Backlog.md project detected",
|
|
1083
|
+
description = "This repository does not appear to use Backlog.md for task management.",
|
|
1084
|
+
showBacklogLink = true
|
|
1085
|
+
}) => {
|
|
1086
|
+
const { theme: theme2 } = useTheme();
|
|
1087
|
+
return /* @__PURE__ */ jsxs(
|
|
1088
|
+
"div",
|
|
1089
|
+
{
|
|
1090
|
+
style: {
|
|
1091
|
+
display: "flex",
|
|
1092
|
+
flexDirection: "column",
|
|
1093
|
+
alignItems: "center",
|
|
1094
|
+
justifyContent: "center",
|
|
1095
|
+
height: "100%",
|
|
1096
|
+
padding: "48px 24px",
|
|
1097
|
+
textAlign: "center",
|
|
1098
|
+
color: theme2.colors.textMuted
|
|
1099
|
+
},
|
|
1100
|
+
children: [
|
|
1101
|
+
/* @__PURE__ */ jsx(
|
|
1102
|
+
FileText,
|
|
1103
|
+
{
|
|
1104
|
+
size: 64,
|
|
1105
|
+
color: theme2.colors.textMuted,
|
|
1106
|
+
style: { marginBottom: "24px", opacity: 0.5 }
|
|
1107
|
+
}
|
|
1108
|
+
),
|
|
1109
|
+
/* @__PURE__ */ jsx(
|
|
1110
|
+
"h3",
|
|
1111
|
+
{
|
|
1112
|
+
style: {
|
|
1113
|
+
fontSize: theme2.fontSizes[4],
|
|
1114
|
+
fontWeight: 600,
|
|
1115
|
+
color: theme2.colors.text,
|
|
1116
|
+
marginBottom: "12px"
|
|
1117
|
+
},
|
|
1118
|
+
children: message
|
|
1119
|
+
}
|
|
1120
|
+
),
|
|
1121
|
+
/* @__PURE__ */ jsx(
|
|
1122
|
+
"p",
|
|
1123
|
+
{
|
|
1124
|
+
style: {
|
|
1125
|
+
fontSize: theme2.fontSizes[2],
|
|
1126
|
+
color: theme2.colors.textMuted,
|
|
1127
|
+
marginBottom: "32px",
|
|
1128
|
+
maxWidth: "480px",
|
|
1129
|
+
lineHeight: 1.6
|
|
1130
|
+
},
|
|
1131
|
+
children: description
|
|
1132
|
+
}
|
|
1133
|
+
),
|
|
1134
|
+
showBacklogLink && /* @__PURE__ */ jsxs(
|
|
1135
|
+
"a",
|
|
1136
|
+
{
|
|
1137
|
+
href: "https://github.com/MrLesk/Backlog.md",
|
|
1138
|
+
target: "_blank",
|
|
1139
|
+
rel: "noopener noreferrer",
|
|
1140
|
+
style: {
|
|
1141
|
+
display: "inline-flex",
|
|
1142
|
+
alignItems: "center",
|
|
1143
|
+
gap: "8px",
|
|
1144
|
+
padding: "12px 24px",
|
|
1145
|
+
backgroundColor: theme2.colors.primary,
|
|
1146
|
+
color: "#fff",
|
|
1147
|
+
borderRadius: theme2.radii[2],
|
|
1148
|
+
textDecoration: "none",
|
|
1149
|
+
fontSize: theme2.fontSizes[2],
|
|
1150
|
+
fontWeight: 500,
|
|
1151
|
+
transition: "opacity 0.2s"
|
|
1152
|
+
},
|
|
1153
|
+
onMouseEnter: (e) => {
|
|
1154
|
+
e.currentTarget.style.opacity = "0.9";
|
|
1155
|
+
},
|
|
1156
|
+
onMouseLeave: (e) => {
|
|
1157
|
+
e.currentTarget.style.opacity = "1";
|
|
1158
|
+
},
|
|
1159
|
+
children: [
|
|
1160
|
+
/* @__PURE__ */ jsx("span", { children: "Learn about Backlog.md" }),
|
|
1161
|
+
/* @__PURE__ */ jsx(ExternalLink, { size: 16 })
|
|
1162
|
+
]
|
|
1163
|
+
}
|
|
1164
|
+
),
|
|
1165
|
+
/* @__PURE__ */ jsx(
|
|
1166
|
+
"div",
|
|
1167
|
+
{
|
|
1168
|
+
style: {
|
|
1169
|
+
marginTop: "48px",
|
|
1170
|
+
padding: "16px",
|
|
1171
|
+
backgroundColor: `${theme2.colors.primary}10`,
|
|
1172
|
+
borderRadius: theme2.radii[2],
|
|
1173
|
+
maxWidth: "560px"
|
|
1174
|
+
},
|
|
1175
|
+
children: /* @__PURE__ */ jsxs(
|
|
1176
|
+
"p",
|
|
1177
|
+
{
|
|
1178
|
+
style: {
|
|
1179
|
+
fontSize: theme2.fontSizes[1],
|
|
1180
|
+
color: theme2.colors.textSecondary,
|
|
1181
|
+
margin: 0,
|
|
1182
|
+
lineHeight: 1.5
|
|
1183
|
+
},
|
|
1184
|
+
children: [
|
|
1185
|
+
/* @__PURE__ */ jsx("strong", { style: { color: theme2.colors.text }, children: "Want to use this panel?" }),
|
|
1186
|
+
" ",
|
|
1187
|
+
"Initialize Backlog.md in your repository by running",
|
|
1188
|
+
" ",
|
|
1189
|
+
/* @__PURE__ */ jsx(
|
|
1190
|
+
"code",
|
|
1191
|
+
{
|
|
1192
|
+
style: {
|
|
1193
|
+
padding: "2px 6px",
|
|
1194
|
+
backgroundColor: theme2.colors.surface,
|
|
1195
|
+
borderRadius: "4px",
|
|
1196
|
+
fontFamily: theme2.fonts.monospace,
|
|
1197
|
+
fontSize: "0.9em"
|
|
1198
|
+
},
|
|
1199
|
+
children: "backlog init"
|
|
1200
|
+
}
|
|
1201
|
+
),
|
|
1202
|
+
" ",
|
|
1203
|
+
"in your project directory."
|
|
1204
|
+
]
|
|
1205
|
+
}
|
|
1206
|
+
)
|
|
1207
|
+
}
|
|
1208
|
+
)
|
|
1209
|
+
]
|
|
1210
|
+
}
|
|
1211
|
+
);
|
|
1212
|
+
};
|
|
770
1213
|
const KanbanPanelContent = ({
|
|
771
|
-
context
|
|
1214
|
+
context,
|
|
1215
|
+
actions
|
|
772
1216
|
}) => {
|
|
773
1217
|
const { theme: theme2 } = useTheme();
|
|
774
1218
|
const [_selectedTask, setSelectedTask] = useState(null);
|
|
@@ -777,8 +1221,9 @@ const KanbanPanelContent = ({
|
|
|
777
1221
|
tasksByStatus,
|
|
778
1222
|
isLoading,
|
|
779
1223
|
error,
|
|
1224
|
+
isBacklogProject,
|
|
780
1225
|
refreshData
|
|
781
|
-
} = useKanbanData();
|
|
1226
|
+
} = useKanbanData({ context, actions });
|
|
782
1227
|
const handleTaskClick = (task) => {
|
|
783
1228
|
setSelectedTask(task);
|
|
784
1229
|
};
|
|
@@ -867,7 +1312,7 @@ const KanbanPanelContent = ({
|
|
|
867
1312
|
]
|
|
868
1313
|
}
|
|
869
1314
|
),
|
|
870
|
-
/* @__PURE__ */ jsx(
|
|
1315
|
+
!isBacklogProject ? /* @__PURE__ */ jsx(EmptyState, {}) : /* @__PURE__ */ jsx(
|
|
871
1316
|
"div",
|
|
872
1317
|
{
|
|
873
1318
|
style: {
|