Storage refactor: tickets in column subdirs, drop redundant column frontmatter
- TicketStore rewritten: tickets live in .project/kanban/<column>/<id>.md - _findTicketFile() searches all column subdirs (one level deep) - update() moves the file when column changes - delete() cleans up attachments/<id>/ if present - _nextNumber() scans all subdirs including archive - Ticket.fromFileContent() now takes column as explicit parameter - Ticket.toFileContent() drops column field (derived from path) - _yamlQuote() added to safely quote titles containing ': ' - dew.yaml: columns changed to backlog/doing/done (alphabetical order) - Existing tickets migrated to .project/kanban/backlog/ Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
08f4d5c7cf
commit
951f0d8bc8
19 changed files with 223 additions and 54 deletions
|
|
@ -16,11 +16,11 @@ dew:
|
||||||
- id: "spike"
|
- id: "spike"
|
||||||
name: "Spike"
|
name: "Spike"
|
||||||
columns:
|
columns:
|
||||||
- id: "todo"
|
- id: "backlog"
|
||||||
name: "To Do"
|
name: "Backlog"
|
||||||
color: "blue"
|
color: "blue"
|
||||||
- id: "in-progress"
|
- id: "doing"
|
||||||
name: "In Progress"
|
name: "Doing"
|
||||||
color: "yellow"
|
color: "yellow"
|
||||||
- id: "done"
|
- id: "done"
|
||||||
name: "Done"
|
name: "Done"
|
||||||
|
|
|
||||||
8
.project/kanban/backlog/DEW-0001.md
Normal file
8
.project/kanban/backlog/DEW-0001.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
id: DEW-0001
|
||||||
|
title: Engineering Quality
|
||||||
|
type: epic
|
||||||
|
created: 2026-04-23T23:37:10.461762Z
|
||||||
|
---
|
||||||
|
|
||||||
|
Refactor and harden the core infrastructure before layering features on top. Covers config architecture, ticket storage model, and documentation accuracy.
|
||||||
8
.project/kanban/backlog/DEW-0002.md
Normal file
8
.project/kanban/backlog/DEW-0002.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
id: DEW-0002
|
||||||
|
title: Bootstrapping
|
||||||
|
type: epic
|
||||||
|
created: 2026-04-23T23:37:10.465324Z
|
||||||
|
---
|
||||||
|
|
||||||
|
Make it easy to initialize a new Dew project. `dew init` scaffolds the .project/ directory and each module creates its own folders via init hooks.
|
||||||
8
.project/kanban/backlog/DEW-0003.md
Normal file
8
.project/kanban/backlog/DEW-0003.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
id: DEW-0003
|
||||||
|
title: Milestones & Labels
|
||||||
|
type: epic
|
||||||
|
created: 2026-04-23T23:37:10.468947Z
|
||||||
|
---
|
||||||
|
|
||||||
|
Add freeform milestone and label fields to tickets for grouping and filtering. Both are List<String> in ticket frontmatter.
|
||||||
8
.project/kanban/backlog/DEW-0004.md
Normal file
8
.project/kanban/backlog/DEW-0004.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
id: DEW-0004
|
||||||
|
title: Output Polish
|
||||||
|
type: epic
|
||||||
|
created: 2026-04-23T23:37:10.472323Z
|
||||||
|
---
|
||||||
|
|
||||||
|
Improve human-facing CLI output. Grouped list by column, enhanced search filters, and an ASCII board view.
|
||||||
8
.project/kanban/backlog/DEW-0005.md
Normal file
8
.project/kanban/backlog/DEW-0005.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
id: DEW-0005
|
||||||
|
title: Column Transitions
|
||||||
|
type: epic
|
||||||
|
created: 2026-04-23T23:37:10.476017Z
|
||||||
|
---
|
||||||
|
|
||||||
|
Optional config-driven validation of which column moves are allowed. E.g. prevent moving directly from todo to done.
|
||||||
8
.project/kanban/backlog/DEW-0006.md
Normal file
8
.project/kanban/backlog/DEW-0006.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
id: DEW-0006
|
||||||
|
title: Archiving
|
||||||
|
type: epic
|
||||||
|
created: 2026-04-23T23:37:10.479959Z
|
||||||
|
---
|
||||||
|
|
||||||
|
Soft-delete tickets to an archive folder rather than permanently destroying them. Archived tickets are excluded from normal list/search but can be browsed.
|
||||||
8
.project/kanban/backlog/DEW-0007.md
Normal file
8
.project/kanban/backlog/DEW-0007.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
id: DEW-0007
|
||||||
|
title: "Config refactor: DewConfig.raw + per-package extensions"
|
||||||
|
type: story
|
||||||
|
created: 2026-04-23T23:37:36.822736Z
|
||||||
|
---
|
||||||
|
|
||||||
|
DewConfig in core becomes a thin wrapper: `class DewConfig { final YamlMap raw; }`. Each package adds a Dart extension with a typed getter (e.g. KanbanDewConfig, McpDewConfig). KanbanConfig/ColumnConfig/TicketTypeConfig move to dew_kanban; McpConfig moves to dew_mcp. Call sites unchanged: context.config.kanban.prefix etc.
|
||||||
8
.project/kanban/backlog/DEW-0008.md
Normal file
8
.project/kanban/backlog/DEW-0008.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
id: DEW-0008
|
||||||
|
title: "Ticket storage refactor: column subdirectories"
|
||||||
|
type: story
|
||||||
|
created: 2026-04-23T23:37:36.826965Z
|
||||||
|
---
|
||||||
|
|
||||||
|
Move ticket .md files from flat .project/kanban/ into column subdirectories (.project/kanban/todo/DEW-0001.md). Attachments live at .project/kanban/attachments/<ticket-id>/ (stable path, unaffected by moves). TicketStore changes: _filePath needs column, findById searches all column dirs, move physically moves the .md file, delete removes .md + attachments/<id>/ if present, create writes into correct column dir.
|
||||||
8
.project/kanban/backlog/DEW-0009.md
Normal file
8
.project/kanban/backlog/DEW-0009.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
id: DEW-0009
|
||||||
|
title: Update stale documentation
|
||||||
|
type: task
|
||||||
|
created: 2026-04-23T23:37:36.830655Z
|
||||||
|
---
|
||||||
|
|
||||||
|
docs/mcp.md only lists 5 of 12 tools; update to reflect current tool set. docs/index.md references old interface names. docs/kanban.md is a stub; flesh it out. Update tool list, command reference, and architecture notes.
|
||||||
8
.project/kanban/backlog/DEW-0010.md
Normal file
8
.project/kanban/backlog/DEW-0010.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
id: DEW-0010
|
||||||
|
title: dew init command with module init hooks
|
||||||
|
type: story
|
||||||
|
created: 2026-04-23T23:37:36.834286Z
|
||||||
|
---
|
||||||
|
|
||||||
|
Add `dew init [path]` command. Core defines DewInitHook interface with onInit(root, options). Options include bool gitkeep (default true). Each package registers a hook. Kanban hook creates: column dirs (from config), archive/, attachments/ under .project/kanban/. --[no-]gitkeep flag adds .gitkeep to empty dirs. Must run after storage refactor so dir structure matches new layout.
|
||||||
8
.project/kanban/backlog/DEW-0011.md
Normal file
8
.project/kanban/backlog/DEW-0011.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
id: DEW-0011
|
||||||
|
title: Add milestones and labels to Ticket model
|
||||||
|
type: story
|
||||||
|
created: 2026-04-23T23:37:36.838770Z
|
||||||
|
---
|
||||||
|
|
||||||
|
Add milestones: List<String> and labels: List<String> to the Ticket model and YAML frontmatter. Support --milestone and --label flags on create/update. Add --label and --milestone filter flags to list and search commands. Depends on storage refactor.
|
||||||
8
.project/kanban/backlog/DEW-0012.md
Normal file
8
.project/kanban/backlog/DEW-0012.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
id: DEW-0012
|
||||||
|
title: Grouped list view and ASCII board
|
||||||
|
type: story
|
||||||
|
created: 2026-04-23T23:37:36.842362Z
|
||||||
|
---
|
||||||
|
|
||||||
|
dew kanban list groups output by column with counts. Add `dew kanban board` subcommand for a full ASCII kanban board view showing all columns and their tickets side by side (or stacked).
|
||||||
8
.project/kanban/backlog/DEW-0013.md
Normal file
8
.project/kanban/backlog/DEW-0013.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
id: DEW-0013
|
||||||
|
title: Enhanced search filters
|
||||||
|
type: story
|
||||||
|
created: 2026-04-23T23:37:36.846212Z
|
||||||
|
---
|
||||||
|
|
||||||
|
Add --column, --type, --label, --milestone filter flags to `dew kanban search`. Currently only matches on text. Filters should be combinable (AND semantics).
|
||||||
8
.project/kanban/backlog/DEW-0014.md
Normal file
8
.project/kanban/backlog/DEW-0014.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
id: DEW-0014
|
||||||
|
title: Column transition validation
|
||||||
|
type: story
|
||||||
|
created: 2026-04-23T23:37:36.849741Z
|
||||||
|
---
|
||||||
|
|
||||||
|
Add optional allowed_transitions map to column config in dew.yaml. When configured, MoveCommand validates the requested move is permitted and errors with a helpful message listing allowed next columns. Unconfigured = all moves allowed (current behaviour).
|
||||||
8
.project/kanban/backlog/DEW-0015.md
Normal file
8
.project/kanban/backlog/DEW-0015.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
id: DEW-0015
|
||||||
|
title: "Archive command: soft-delete tickets"
|
||||||
|
type: story
|
||||||
|
created: 2026-04-23T23:37:36.853236Z
|
||||||
|
---
|
||||||
|
|
||||||
|
Add `dew kanban archive --id <id>` that moves a ticket to .project/kanban/archive/ (and its attachments stay at attachments/<id>/). Archived tickets excluded from list/search by default; add --include-archived flag to opt in. Depends on storage refactor.
|
||||||
|
|
@ -71,6 +71,9 @@ class Ticket {
|
||||||
|
|
||||||
/// Serialises the ticket to markdown with YAML frontmatter.
|
/// Serialises the ticket to markdown with YAML frontmatter.
|
||||||
///
|
///
|
||||||
|
/// The column is intentionally omitted — it is derived from the containing
|
||||||
|
/// directory name and would be redundant (and stale after a move).
|
||||||
|
///
|
||||||
/// Format:
|
/// Format:
|
||||||
/// ```
|
/// ```
|
||||||
/// ---
|
/// ---
|
||||||
|
|
@ -89,9 +92,8 @@ class Ticket {
|
||||||
final buf = StringBuffer();
|
final buf = StringBuffer();
|
||||||
buf.writeln('---');
|
buf.writeln('---');
|
||||||
buf.writeln('id: $id');
|
buf.writeln('id: $id');
|
||||||
buf.writeln('title: $title');
|
buf.writeln('title: ${_yamlQuote(title)}');
|
||||||
buf.writeln('type: $type');
|
buf.writeln('type: $type');
|
||||||
buf.writeln('column: $column');
|
|
||||||
buf.writeln('created: ${created.toUtc().toIso8601String()}');
|
buf.writeln('created: ${created.toUtc().toIso8601String()}');
|
||||||
if (links.isNotEmpty) {
|
if (links.isNotEmpty) {
|
||||||
buf.writeln('links:');
|
buf.writeln('links:');
|
||||||
|
|
@ -114,7 +116,9 @@ class Ticket {
|
||||||
return buf.toString();
|
return buf.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
static Ticket fromFileContent(String id, String content) {
|
/// Parses a ticket from file content. [column] must be supplied by the
|
||||||
|
/// caller (derived from the containing directory name).
|
||||||
|
static Ticket fromFileContent(String id, String content, String column) {
|
||||||
if (!content.startsWith('---\n')) {
|
if (!content.startsWith('---\n')) {
|
||||||
throw FormatException('Ticket file $id does not start with ---');
|
throw FormatException('Ticket file $id does not start with ---');
|
||||||
}
|
}
|
||||||
|
|
@ -146,12 +150,25 @@ class Ticket {
|
||||||
id: id,
|
id: id,
|
||||||
title: fm['title'] as String,
|
title: fm['title'] as String,
|
||||||
type: fm['type'] as String,
|
type: fm['type'] as String,
|
||||||
column: fm['column'] as String,
|
column: column,
|
||||||
created: DateTime.parse(fm['created'] as String),
|
created: DateTime.parse(fm['created'] as String),
|
||||||
body: sections.isNotEmpty ? sections[0] : '',
|
body: sections.isNotEmpty ? sections[0] : '',
|
||||||
comments: sections.length > 1 ? sections.sublist(1) : const [],
|
comments: sections.length > 1 ? sections.sublist(1) : const [],
|
||||||
links: links,
|
links: links,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wraps [value] in double quotes if it contains characters that would
|
||||||
|
/// confuse a YAML parser (colon-space, leading/trailing whitespace, etc.).
|
||||||
|
static String _yamlQuote(String value) {
|
||||||
|
final needsQuoting = value.contains(': ') ||
|
||||||
|
value.contains(' #') ||
|
||||||
|
value.startsWith('"') ||
|
||||||
|
value.startsWith("'") ||
|
||||||
|
value.startsWith(' ') ||
|
||||||
|
value.endsWith(' ');
|
||||||
|
if (!needsQuoting) return value;
|
||||||
|
return '"${value.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,8 @@ class TicketStore {
|
||||||
required String column,
|
required String column,
|
||||||
String body = '',
|
String body = '',
|
||||||
}) async {
|
}) async {
|
||||||
await Directory(kanbanDir).create(recursive: true);
|
final columnDir = Directory(p.join(kanbanDir, column));
|
||||||
|
await columnDir.create(recursive: true);
|
||||||
final id = _formatId(await _nextNumber());
|
final id = _formatId(await _nextNumber());
|
||||||
final ticket = Ticket(
|
final ticket = Ticket(
|
||||||
id: id,
|
id: id,
|
||||||
|
|
@ -27,14 +28,14 @@ class TicketStore {
|
||||||
body: body,
|
body: body,
|
||||||
comments: const [],
|
comments: const [],
|
||||||
);
|
);
|
||||||
await File(_filePath(id)).writeAsString(ticket.toFileContent());
|
await File(p.join(columnDir.path, '$id.md')).writeAsString(ticket.toFileContent());
|
||||||
return ticket;
|
return ticket;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Ticket?> findById(String id) async {
|
Future<Ticket?> findById(String id) async {
|
||||||
final file = File(_filePath(id));
|
final found = await _findTicketFile(id);
|
||||||
if (!await file.exists()) return null;
|
if (found == null) return null;
|
||||||
return Ticket.fromFileContent(id, await file.readAsString());
|
return Ticket.fromFileContent(id, await found.file.readAsString(), found.column);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Ticket>> list() async {
|
Future<List<Ticket>> list() async {
|
||||||
|
|
@ -43,11 +44,15 @@ class TicketStore {
|
||||||
final pattern = RegExp(r'^' + RegExp.escape(prefix) + r'-\d{4}\.md$');
|
final pattern = RegExp(r'^' + RegExp.escape(prefix) + r'-\d{4}\.md$');
|
||||||
final tickets = <Ticket>[];
|
final tickets = <Ticket>[];
|
||||||
await for (final entity in dir.list()) {
|
await for (final entity in dir.list()) {
|
||||||
final name = p.basename(entity.path);
|
if (entity is! Directory) continue;
|
||||||
if (pattern.hasMatch(name)) {
|
final col = p.basename(entity.path);
|
||||||
|
if (col == 'archive' || col == 'attachments') continue;
|
||||||
|
await for (final file in entity.list()) {
|
||||||
|
if (file is! File) continue;
|
||||||
|
final name = p.basename(file.path);
|
||||||
|
if (!pattern.hasMatch(name)) continue;
|
||||||
final id = p.basenameWithoutExtension(name);
|
final id = p.basenameWithoutExtension(name);
|
||||||
final ticket = await findById(id);
|
tickets.add(Ticket.fromFileContent(id, await file.readAsString(), col));
|
||||||
if (ticket != null) tickets.add(ticket);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tickets.sort((a, b) => a.id.compareTo(b.id));
|
tickets.sort((a, b) => a.id.compareTo(b.id));
|
||||||
|
|
@ -55,12 +60,11 @@ class TicketStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Ticket> addComment(String id, String comment) async {
|
Future<Ticket> addComment(String id, String comment) async {
|
||||||
final ticket = await findById(id);
|
final found = await _findTicketFile(id);
|
||||||
if (ticket == null) throw ArgumentError('Ticket $id not found.');
|
if (found == null) throw ArgumentError('Ticket $id not found.');
|
||||||
final updated = ticket.copyWith(
|
final ticket = Ticket.fromFileContent(id, await found.file.readAsString(), found.column);
|
||||||
comments: [...ticket.comments, comment],
|
final updated = ticket.copyWith(comments: [...ticket.comments, comment]);
|
||||||
);
|
await found.file.writeAsString(updated.toFileContent());
|
||||||
await File(_filePath(id)).writeAsString(updated.toFileContent());
|
|
||||||
return updated;
|
return updated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -81,7 +85,8 @@ class TicketStore {
|
||||||
final updated = ticket.copyWith(
|
final updated = ticket.copyWith(
|
||||||
links: [...ticket.links, TicketLink(targetId: targetId, type: type)],
|
links: [...ticket.links, TicketLink(targetId: targetId, type: type)],
|
||||||
);
|
);
|
||||||
await File(_filePath(id)).writeAsString(updated.toFileContent());
|
final found = (await _findTicketFile(id))!;
|
||||||
|
await found.file.writeAsString(updated.toFileContent());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inverse link on the target.
|
// Inverse link on the target.
|
||||||
|
|
@ -90,29 +95,34 @@ class TicketStore {
|
||||||
final updatedTarget = target.copyWith(
|
final updatedTarget = target.copyWith(
|
||||||
links: [...target.links, TicketLink(targetId: id, type: inverseType)],
|
links: [...target.links, TicketLink(targetId: id, type: inverseType)],
|
||||||
);
|
);
|
||||||
await File(_filePath(targetId)).writeAsString(updatedTarget.toFileContent());
|
final foundTarget = (await _findTicketFile(targetId))!;
|
||||||
|
await foundTarget.file.writeAsString(updatedTarget.toFileContent());
|
||||||
}
|
}
|
||||||
|
|
||||||
return (await findById(id))!;
|
return (await findById(id))!;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Ticket> unlinkTickets(String id, String targetId) async {
|
Future<Ticket> unlinkTickets(String id, String targetId) async {
|
||||||
final ticket = await findById(id);
|
final found = await _findTicketFile(id);
|
||||||
if (ticket == null) throw ArgumentError('Ticket $id not found.');
|
if (found == null) throw ArgumentError('Ticket $id not found.');
|
||||||
|
final ticket = Ticket.fromFileContent(id, await found.file.readAsString(), found.column);
|
||||||
// Remove forward link.
|
|
||||||
final updated = ticket.copyWith(
|
final updated = ticket.copyWith(
|
||||||
links: ticket.links.where((l) => l.targetId != targetId).toList(),
|
links: ticket.links.where((l) => l.targetId != targetId).toList(),
|
||||||
);
|
);
|
||||||
await File(_filePath(id)).writeAsString(updated.toFileContent());
|
await found.file.writeAsString(updated.toFileContent());
|
||||||
|
|
||||||
// Remove inverse link on target (if it exists).
|
// Remove inverse link on target (if it exists).
|
||||||
final target = await findById(targetId);
|
final foundTarget = await _findTicketFile(targetId);
|
||||||
if (target != null) {
|
if (foundTarget != null) {
|
||||||
|
final target = Ticket.fromFileContent(
|
||||||
|
targetId,
|
||||||
|
await foundTarget.file.readAsString(),
|
||||||
|
foundTarget.column,
|
||||||
|
);
|
||||||
final updatedTarget = target.copyWith(
|
final updatedTarget = target.copyWith(
|
||||||
links: target.links.where((l) => l.targetId != id).toList(),
|
links: target.links.where((l) => l.targetId != id).toList(),
|
||||||
);
|
);
|
||||||
await File(_filePath(targetId)).writeAsString(updatedTarget.toFileContent());
|
await foundTarget.file.writeAsString(updatedTarget.toFileContent());
|
||||||
}
|
}
|
||||||
|
|
||||||
return (await findById(id))!;
|
return (await findById(id))!;
|
||||||
|
|
@ -137,22 +147,43 @@ class TicketStore {
|
||||||
String? column,
|
String? column,
|
||||||
String? body,
|
String? body,
|
||||||
}) async {
|
}) async {
|
||||||
final ticket = await findById(id);
|
final found = await _findTicketFile(id);
|
||||||
if (ticket == null) throw ArgumentError('Ticket $id not found.');
|
if (found == null) throw ArgumentError('Ticket $id not found.');
|
||||||
final updated = ticket.copyWith(
|
final ticket = Ticket.fromFileContent(id, await found.file.readAsString(), found.column);
|
||||||
title: title,
|
final updated = ticket.copyWith(title: title, type: type, column: column, body: body);
|
||||||
type: type,
|
if (column != null && column != ticket.column) {
|
||||||
column: column,
|
// Column changed — move the file to the new column directory.
|
||||||
body: body,
|
await found.file.delete();
|
||||||
);
|
final newColDir = Directory(p.join(kanbanDir, column));
|
||||||
await File(_filePath(id)).writeAsString(updated.toFileContent());
|
await newColDir.create(recursive: true);
|
||||||
|
await File(p.join(newColDir.path, '$id.md')).writeAsString(updated.toFileContent());
|
||||||
|
} else {
|
||||||
|
await found.file.writeAsString(updated.toFileContent());
|
||||||
|
}
|
||||||
return updated;
|
return updated;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> delete(String id) async {
|
Future<void> delete(String id) async {
|
||||||
final file = File(_filePath(id));
|
final found = await _findTicketFile(id);
|
||||||
if (!await file.exists()) throw ArgumentError('Ticket $id not found.');
|
if (found == null) throw ArgumentError('Ticket $id not found.');
|
||||||
await file.delete();
|
await found.file.delete();
|
||||||
|
// Clean up per-ticket attachment directory if present.
|
||||||
|
final attachmentsDir = Directory(p.join(kanbanDir, 'attachments', id));
|
||||||
|
if (await attachmentsDir.exists()) await attachmentsDir.delete(recursive: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Searches all column subdirectories (one level deep) for a ticket file.
|
||||||
|
/// Skips the [attachments] directory. Includes [archive].
|
||||||
|
Future<({File file, String column})?> _findTicketFile(String id) async {
|
||||||
|
final dir = Directory(kanbanDir);
|
||||||
|
if (!await dir.exists()) return null;
|
||||||
|
await for (final entity in dir.list()) {
|
||||||
|
if (entity is! Directory) continue;
|
||||||
|
if (p.basename(entity.path) == 'attachments') continue;
|
||||||
|
final file = File(p.join(entity.path, '$id.md'));
|
||||||
|
if (await file.exists()) return (file: file, column: p.basename(entity.path));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> _nextNumber() async {
|
Future<int> _nextNumber() async {
|
||||||
|
|
@ -161,16 +192,17 @@ class TicketStore {
|
||||||
final pattern = RegExp(r'^' + RegExp.escape(prefix) + r'-(\d+)\.md$');
|
final pattern = RegExp(r'^' + RegExp.escape(prefix) + r'-(\d+)\.md$');
|
||||||
var max = 0;
|
var max = 0;
|
||||||
await for (final entity in dir.list()) {
|
await for (final entity in dir.list()) {
|
||||||
final match = pattern.firstMatch(p.basename(entity.path));
|
if (entity is! Directory || p.basename(entity.path) == 'attachments') continue;
|
||||||
|
await for (final file in entity.list()) {
|
||||||
|
final match = pattern.firstMatch(p.basename(file.path));
|
||||||
if (match != null) {
|
if (match != null) {
|
||||||
final n = int.parse(match.group(1)!);
|
final n = int.parse(match.group(1)!);
|
||||||
if (n > max) max = n;
|
if (n > max) max = n;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return max + 1;
|
return max + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
String _formatId(int n) => '$prefix-${n.toString().padLeft(4, '0')}';
|
String _formatId(int n) => '$prefix-${n.toString().padLeft(4, '0')}';
|
||||||
|
|
||||||
String _filePath(String id) => p.join(kanbanDir, '$id.md');
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,7 @@ dew:
|
||||||
body: 'Some body.',
|
body: 'Some body.',
|
||||||
comments: ['First comment.', 'Second comment.'],
|
comments: ['First comment.', 'Second comment.'],
|
||||||
);
|
);
|
||||||
final parsed = Ticket.fromFileContent(t.id, t.toFileContent());
|
final parsed = Ticket.fromFileContent(t.id, t.toFileContent(), t.column);
|
||||||
expect(parsed.id, t.id);
|
expect(parsed.id, t.id);
|
||||||
expect(parsed.title, t.title);
|
expect(parsed.title, t.title);
|
||||||
expect(parsed.type, t.type);
|
expect(parsed.type, t.type);
|
||||||
|
|
@ -159,7 +159,7 @@ dew:
|
||||||
body: '',
|
body: '',
|
||||||
comments: const [],
|
comments: const [],
|
||||||
);
|
);
|
||||||
final parsed = Ticket.fromFileContent(t.id, t.toFileContent());
|
final parsed = Ticket.fromFileContent(t.id, t.toFileContent(), t.column);
|
||||||
expect(parsed.body, '');
|
expect(parsed.body, '');
|
||||||
expect(parsed.comments, isEmpty);
|
expect(parsed.comments, isEmpty);
|
||||||
});
|
});
|
||||||
|
|
@ -178,7 +178,7 @@ dew:
|
||||||
TicketLink(targetId: 'TEST-0002', type: 'relates_to'),
|
TicketLink(targetId: 'TEST-0002', type: 'relates_to'),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
final parsed = Ticket.fromFileContent(t.id, t.toFileContent());
|
final parsed = Ticket.fromFileContent(t.id, t.toFileContent(), t.column);
|
||||||
expect(parsed.links, hasLength(2));
|
expect(parsed.links, hasLength(2));
|
||||||
expect(parsed.links[0].targetId, 'TEST-0001');
|
expect(parsed.links[0].targetId, 'TEST-0001');
|
||||||
expect(parsed.links[0].type, 'blocks');
|
expect(parsed.links[0].type, 'blocks');
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue