test: update tests, add eslint rules
This commit is contained in:
@@ -1,5 +1,8 @@
|
|||||||
# Vitest + Clean Architecture Refactoring
|
# Vitest + Clean Architecture Refactoring
|
||||||
|
|
||||||
|
> **Статус: выполнено / архив**
|
||||||
|
> План закрыт, дальше не ведём и не обновляем. Исторический снимок задачи.
|
||||||
|
|
||||||
## Цель
|
## Цель
|
||||||
|
|
||||||
Установить Vitest, переделать архитектуру ближе к чистой (порты/адаптеры),
|
Установить Vitest, переделать архитектуру ближе к чистой (порты/адаптеры),
|
||||||
|
|||||||
@@ -37,6 +37,25 @@ export default defineConfig(
|
|||||||
caughtErrorsIgnorePattern: "^_",
|
caughtErrorsIgnorePattern: "^_",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"@typescript-eslint/parameter-properties": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
prefer: "class-property",
|
||||||
|
allow: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"@typescript-eslint/naming-convention": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
selector: "classProperty",
|
||||||
|
modifiers: ["private"],
|
||||||
|
format: null,
|
||||||
|
custom: {
|
||||||
|
regex: "^.*$",
|
||||||
|
match: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,11 +4,18 @@ import { beforeEach, describe, expect, it } from "vitest";
|
|||||||
import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem";
|
import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem";
|
||||||
import { FsTaskRepository } from "./FsTaskRepository";
|
import { FsTaskRepository } from "./FsTaskRepository";
|
||||||
|
|
||||||
|
const UUID_RE =
|
||||||
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||||
|
|
||||||
describe("FsTaskRepository", () => {
|
describe("FsTaskRepository", () => {
|
||||||
let fs: InMemoryFileSystem;
|
let fs: InMemoryFileSystem;
|
||||||
let repo: ITaskRepository;
|
let repo: ITaskRepository;
|
||||||
|
let clockStep: number;
|
||||||
|
|
||||||
const folder = "/tasks";
|
const folder = "/tasks";
|
||||||
|
const t0 = "2026-01-01T00:00:00.000Z";
|
||||||
|
const t1 = "2026-01-01T00:00:01.000Z";
|
||||||
|
const t2 = "2026-01-01T00:00:02.000Z";
|
||||||
|
|
||||||
const sampleTask = {
|
const sampleTask = {
|
||||||
title: "Fix login bug",
|
title: "Fix login bug",
|
||||||
@@ -21,35 +28,41 @@ describe("FsTaskRepository", () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fs = new InMemoryFileSystem();
|
fs = new InMemoryFileSystem();
|
||||||
repo = new FsTaskRepository(fs, ".task.md");
|
clockStep = 0;
|
||||||
|
const timestamps = [t0, t1, t2];
|
||||||
|
repo = new FsTaskRepository(fs, ".task.md", () => {
|
||||||
|
const value = timestamps[clockStep] ?? t2;
|
||||||
|
clockStep += 1;
|
||||||
|
return value;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("creates a task and writes it as a .task.md file", async () => {
|
it("creates a task and writes it as a .task.md file", async () => {
|
||||||
const task = await repo.create(folder, sampleTask);
|
const task = await repo.create(folder, sampleTask);
|
||||||
|
|
||||||
expect(task.id).toBeTruthy();
|
expect(task.id).toMatch(UUID_RE);
|
||||||
expect(task.title).toBe("Fix login bug");
|
expect(task.title).toBe("Fix login bug");
|
||||||
expect(task.created).toBeTruthy();
|
expect(task.created).toBe(t0);
|
||||||
expect(task.updated).toBe(task.created);
|
expect(task.updated).toBe(t0);
|
||||||
|
|
||||||
const files = await fs.readdir(folder);
|
const files = await fs.readdir(folder);
|
||||||
expect(files).toHaveLength(1);
|
expect(files).toEqual(["fix-login-bug.task.md"]);
|
||||||
expect(files[0]).toMatch(/\.task\.md$/);
|
|
||||||
|
|
||||||
const content = await fs.readFile(`${folder}/${files[0]}`);
|
const content = await fs.readFile(`${folder}/fix-login-bug.task.md`);
|
||||||
expect(content).toContain("Fix login bug");
|
expect(content).toContain("Fix login bug");
|
||||||
expect(content).toContain("status: todo");
|
expect(content).toContain("status: todo");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("list returns created tasks sorted by updated desc", async () => {
|
it("list returns created tasks sorted by updated desc", async () => {
|
||||||
await repo.create(folder, { ...sampleTask, title: "A" });
|
await repo.create(folder, { ...sampleTask, title: "A" });
|
||||||
await delay(10);
|
|
||||||
await repo.create(folder, { ...sampleTask, title: "B" });
|
await repo.create(folder, { ...sampleTask, title: "B" });
|
||||||
|
|
||||||
const tasks = await repo.list(folder);
|
const tasks = await repo.list(folder);
|
||||||
expect(tasks).toHaveLength(2);
|
expect(tasks).toHaveLength(2);
|
||||||
expect(tasks[0].title).toBe("B");
|
expect(tasks[0].title).toBe("B");
|
||||||
|
expect(tasks[0].updated).toBe(t1);
|
||||||
expect(tasks[1].title).toBe("A");
|
expect(tasks[1].title).toBe("A");
|
||||||
|
expect(tasks[1].updated).toBe(t0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("getById returns the correct task", async () => {
|
it("getById returns the correct task", async () => {
|
||||||
@@ -63,19 +76,50 @@ describe("FsTaskRepository", () => {
|
|||||||
expect(found).toBeUndefined();
|
expect(found).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("update modifies the file and updates timestamp", async () => {
|
it("update modifies fields and bumps updated timestamp", async () => {
|
||||||
const task = await repo.create(folder, sampleTask);
|
const task = await repo.create(folder, sampleTask);
|
||||||
await delay(5);
|
const updated: Task = { ...task, status: "done" };
|
||||||
const updated: Task = { ...task, title: "Fixed!", status: "done" };
|
|
||||||
|
|
||||||
await repo.update(folder, updated);
|
await repo.update(folder, updated);
|
||||||
const found = await repo.getById(folder, task.id);
|
const found = await repo.getById(folder, task.id);
|
||||||
|
|
||||||
expect(found!.title).toBe("Fixed!");
|
|
||||||
expect(found!.status).toBe("done");
|
expect(found!.status).toBe("done");
|
||||||
expect(new Date(found!.updated).getTime()).toBeGreaterThan(
|
expect(found!.title).toBe(task.title);
|
||||||
new Date(task.updated).getTime(),
|
expect(found!.updated).toBe(t1);
|
||||||
);
|
expect(found!.created).toBe(t0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("update renames file when title changes", async () => {
|
||||||
|
const task = await repo.create(folder, sampleTask);
|
||||||
|
expect(await fs.readdir(folder)).toEqual(["fix-login-bug.task.md"]);
|
||||||
|
|
||||||
|
await repo.update(folder, { ...task, title: "Fixed!" });
|
||||||
|
|
||||||
|
const files = await fs.readdir(folder);
|
||||||
|
expect(files).toEqual(["fixed.task.md"]);
|
||||||
|
const found = await repo.getById(folder, task.id);
|
||||||
|
expect(found!.title).toBe("Fixed!");
|
||||||
|
expect(found!.updated).toBe(t1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("update of unknown id writes a new file", async () => {
|
||||||
|
const orphan: Task = {
|
||||||
|
...sampleTask,
|
||||||
|
id: "orphan-id",
|
||||||
|
title: "Orphan",
|
||||||
|
created: t0,
|
||||||
|
updated: t0,
|
||||||
|
};
|
||||||
|
|
||||||
|
await repo.update(folder, orphan);
|
||||||
|
|
||||||
|
const found = await repo.getById(folder, "orphan-id");
|
||||||
|
expect(found).toMatchObject({
|
||||||
|
id: "orphan-id",
|
||||||
|
title: "Orphan",
|
||||||
|
updated: t0,
|
||||||
|
});
|
||||||
|
expect(await fs.readdir(folder)).toEqual(["orphan.task.md"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("delete removes the task file", async () => {
|
it("delete removes the task file", async () => {
|
||||||
@@ -84,6 +128,7 @@ describe("FsTaskRepository", () => {
|
|||||||
|
|
||||||
const tasks = await repo.list(folder);
|
const tasks = await repo.list(folder);
|
||||||
expect(tasks).toHaveLength(0);
|
expect(tasks).toHaveLength(0);
|
||||||
|
expect(await fs.readdir(folder)).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("delete is a no-op for unknown id", async () => {
|
it("delete is a no-op for unknown id", async () => {
|
||||||
@@ -96,8 +141,8 @@ describe("FsTaskRepository", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("filters files by extension", async () => {
|
it("filters files by extension", async () => {
|
||||||
fs.writeFile(`${folder}/notes.txt`, "hello");
|
await fs.writeFile(`${folder}/notes.txt`, "hello");
|
||||||
fs.writeFile(`${folder}/chore.md`, "---\nid: x\n---\n");
|
await fs.writeFile(`${folder}/chore.md`, "---\nid: x\n---\n");
|
||||||
|
|
||||||
await repo.create(folder, sampleTask);
|
await repo.create(folder, sampleTask);
|
||||||
|
|
||||||
@@ -105,15 +150,41 @@ describe("FsTaskRepository", () => {
|
|||||||
expect(tasks).toHaveLength(1);
|
expect(tasks).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("skips unparseable .task.md files silently", async () => {
|
it("list returns empty when only non-matching extensions exist", async () => {
|
||||||
fs.writeFile(`${folder}/corrupt.task.md`, "not valid frontmatter");
|
await fs.writeFile(`${folder}/notes.txt`, "hello");
|
||||||
|
await fs.writeFile(`${folder}/chore.md`, "---\nid: x\n---\n");
|
||||||
|
|
||||||
|
const tasks = await repo.list(folder);
|
||||||
|
expect(tasks).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips files without id", async () => {
|
||||||
|
await fs.writeFile(`${folder}/no-id.task.md`, "not valid frontmatter");
|
||||||
|
await fs.writeFile(
|
||||||
|
`${folder}/empty-id.task.md`,
|
||||||
|
"---\ntitle: No Id\n---\n",
|
||||||
|
);
|
||||||
|
|
||||||
await repo.create(folder, sampleTask);
|
await repo.create(folder, sampleTask);
|
||||||
const tasks = await repo.list(folder);
|
const tasks = await repo.list(folder);
|
||||||
expect(tasks).toHaveLength(1);
|
expect(tasks).toHaveLength(1);
|
||||||
});
|
expect(tasks[0].title).toBe("Fix login bug");
|
||||||
});
|
});
|
||||||
|
|
||||||
function delay(ms: number): Promise<void> {
|
it("create with same title overwrites the previous file", async () => {
|
||||||
return new Promise((r) => setTimeout(r, ms));
|
const first = await repo.create(folder, sampleTask);
|
||||||
}
|
const second = await repo.create(folder, {
|
||||||
|
...sampleTask,
|
||||||
|
description: "second",
|
||||||
|
});
|
||||||
|
|
||||||
|
const files = await fs.readdir(folder);
|
||||||
|
expect(files).toEqual(["fix-login-bug.task.md"]);
|
||||||
|
|
||||||
|
const tasks = await repo.list(folder);
|
||||||
|
expect(tasks).toHaveLength(1);
|
||||||
|
expect(tasks[0].id).toBe(second.id);
|
||||||
|
expect(tasks[0].id).not.toBe(first.id);
|
||||||
|
expect(tasks[0].description).toBe("second");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -5,27 +5,38 @@ import { generateId } from "@/utils/uuid";
|
|||||||
|
|
||||||
const MAX_SAFE_NAME_LENGTH = 80;
|
const MAX_SAFE_NAME_LENGTH = 80;
|
||||||
|
|
||||||
|
export type NowFn = () => string;
|
||||||
|
|
||||||
export class FsTaskRepository implements ITaskRepository {
|
export class FsTaskRepository implements ITaskRepository {
|
||||||
|
#fs: IFileSystem;
|
||||||
|
#fileExtension: string;
|
||||||
|
#now: NowFn;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private fs: IFileSystem,
|
fs: IFileSystem,
|
||||||
private fileExtension: string,
|
fileExtension: string,
|
||||||
) {}
|
now: NowFn = () => new Date().toISOString(),
|
||||||
|
) {
|
||||||
|
this.#fs = fs;
|
||||||
|
this.#fileExtension = fileExtension;
|
||||||
|
this.#now = now;
|
||||||
|
}
|
||||||
|
|
||||||
async list(folderPath: string): Promise<Task[]> {
|
async list(folderPath: string): Promise<Task[]> {
|
||||||
let entries: string[];
|
let entries: string[];
|
||||||
try {
|
try {
|
||||||
entries = await this.fs.readdir(folderPath);
|
entries = await this.#fs.readdir(folderPath);
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const files = entries.filter((name) => name.endsWith(this.fileExtension));
|
const files = entries.filter((name) => name.endsWith(this.#fileExtension));
|
||||||
const tasks: Task[] = [];
|
const tasks: Task[] = [];
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
try {
|
try {
|
||||||
const fullPath = this.joinPath(folderPath, file);
|
const fullPath = this.joinPath(folderPath, file);
|
||||||
const content = await this.fs.readFile(fullPath);
|
const content = await this.#fs.readFile(fullPath);
|
||||||
const task = parseTaskFile(content);
|
const task = parseTaskFile(content);
|
||||||
if (!task.id) continue;
|
if (!task.id) continue;
|
||||||
tasks.push(task);
|
tasks.push(task);
|
||||||
@@ -48,7 +59,7 @@ export class FsTaskRepository implements ITaskRepository {
|
|||||||
folderPath: string,
|
folderPath: string,
|
||||||
task: Omit<Task, "id" | "created" | "updated">,
|
task: Omit<Task, "id" | "created" | "updated">,
|
||||||
): Promise<Task> {
|
): Promise<Task> {
|
||||||
const now = new Date().toISOString();
|
const now = this.#now();
|
||||||
const newTask: Task = {
|
const newTask: Task = {
|
||||||
...task,
|
...task,
|
||||||
id: generateId(),
|
id: generateId(),
|
||||||
@@ -56,26 +67,26 @@ export class FsTaskRepository implements ITaskRepository {
|
|||||||
updated: now,
|
updated: now,
|
||||||
};
|
};
|
||||||
|
|
||||||
await this.fs.mkdir(folderPath);
|
await this.#fs.mkdir(folderPath);
|
||||||
const filePath = this.taskFilePath(folderPath, newTask);
|
const filePath = this.taskFilePath(folderPath, newTask);
|
||||||
await this.fs.writeFile(filePath, serializeTask(newTask));
|
await this.#fs.writeFile(filePath, serializeTask(newTask));
|
||||||
|
|
||||||
return newTask;
|
return newTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(folderPath: string, task: Task): Promise<void> {
|
async update(folderPath: string, task: Task): Promise<void> {
|
||||||
const existing = await this.getById(folderPath, task.id);
|
const existing = await this.getById(folderPath, task.id);
|
||||||
const updatedTask = { ...task, updated: new Date().toISOString() };
|
const updatedTask = { ...task, updated: this.#now() };
|
||||||
const newPath = this.taskFilePath(folderPath, updatedTask);
|
const newPath = this.taskFilePath(folderPath, updatedTask);
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
const oldPath = this.taskFilePath(folderPath, existing);
|
const oldPath = this.taskFilePath(folderPath, existing);
|
||||||
if (oldPath !== newPath) {
|
if (oldPath !== newPath) {
|
||||||
await this.fs.deleteFile(oldPath).catch(() => {});
|
await this.#fs.deleteFile(oldPath).catch(() => {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.fs.writeFile(newPath, serializeTask(updatedTask));
|
await this.#fs.writeFile(newPath, serializeTask(updatedTask));
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(folderPath: string, id: string): Promise<void> {
|
async delete(folderPath: string, id: string): Promise<void> {
|
||||||
@@ -84,7 +95,7 @@ export class FsTaskRepository implements ITaskRepository {
|
|||||||
|
|
||||||
const filePath = this.taskFilePath(folderPath, task);
|
const filePath = this.taskFilePath(folderPath, task);
|
||||||
try {
|
try {
|
||||||
await this.fs.deleteFile(filePath);
|
await this.#fs.deleteFile(filePath);
|
||||||
} catch {
|
} catch {
|
||||||
// already gone
|
// already gone
|
||||||
}
|
}
|
||||||
@@ -97,7 +108,7 @@ export class FsTaskRepository implements ITaskRepository {
|
|||||||
.replace(/^-|-$/g, "")
|
.replace(/^-|-$/g, "")
|
||||||
.slice(0, MAX_SAFE_NAME_LENGTH);
|
.slice(0, MAX_SAFE_NAME_LENGTH);
|
||||||
|
|
||||||
return this.joinPath(folderPath, `${safeName}${this.fileExtension}`);
|
return this.joinPath(folderPath, `${safeName}${this.#fileExtension}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
private joinPath(...segments: string[]): string {
|
private joinPath(...segments: string[]): string {
|
||||||
|
|||||||
+30
-12
@@ -1,6 +1,8 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
|
||||||
import { parseTaskFile, serializeTask } from "./markdown";
|
|
||||||
import { Task } from "@/model/task";
|
import { Task } from "@/model/task";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { parseTaskFile, serializeTask } from "./markdown";
|
||||||
|
|
||||||
|
const ISO_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/;
|
||||||
|
|
||||||
const fullTask: Task = {
|
const fullTask: Task = {
|
||||||
id: "550e8400-e29b-41d4-a716-446655440000",
|
id: "550e8400-e29b-41d4-a716-446655440000",
|
||||||
@@ -51,20 +53,17 @@ describe("markdown", () => {
|
|||||||
expect(task.priority).toBe("medium");
|
expect(task.priority).toBe("medium");
|
||||||
expect(task.tags).toEqual([]);
|
expect(task.tags).toEqual([]);
|
||||||
expect(task.assignee).toBe("");
|
expect(task.assignee).toBe("");
|
||||||
expect(task.created).toBeTruthy();
|
expect(task.created).toMatch(ISO_RE);
|
||||||
expect(task.updated).toBeTruthy();
|
expect(task.updated).toMatch(ISO_RE);
|
||||||
});
|
|
||||||
|
|
||||||
it("serializeTask produces valid frontmatter", () => {
|
|
||||||
const result = serializeTask(fullTask);
|
|
||||||
expect(result).toContain("---");
|
|
||||||
expect(result).toContain("title: Пофиксить баг логина");
|
|
||||||
expect(result).toContain("status: in-progress");
|
|
||||||
expect(result).toContain("Ошибка валидации токена");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("round-trip: serialize → parse produces the same task", () => {
|
it("round-trip: serialize → parse produces the same task", () => {
|
||||||
const serialized = serializeTask(fullTask);
|
const serialized = serializeTask(fullTask);
|
||||||
|
expect(serialized).toContain("---");
|
||||||
|
expect(serialized).toContain("title: Пофиксить баг логина");
|
||||||
|
expect(serialized).toContain("status: in-progress");
|
||||||
|
expect(serialized).toContain("Ошибка валидации токена");
|
||||||
|
|
||||||
const parsed = parseTaskFile(serialized);
|
const parsed = parseTaskFile(serialized);
|
||||||
expect(parsed).toEqual(fullTask);
|
expect(parsed).toEqual(fullTask);
|
||||||
});
|
});
|
||||||
@@ -78,4 +77,23 @@ describe("markdown", () => {
|
|||||||
const parsed = parseTaskFile(serialized);
|
const parsed = parseTaskFile(serialized);
|
||||||
expect(parsed.description).toBe(task.description);
|
expect(parsed.description).toBe(task.description);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("converts Date values from YAML to ISO strings", () => {
|
||||||
|
const created = new Date("2026-07-12T10:00:00.000Z");
|
||||||
|
const updated = new Date("2026-07-12T11:00:00.000Z");
|
||||||
|
const content = [
|
||||||
|
"---",
|
||||||
|
'id: "date-task"',
|
||||||
|
'title: "With dates"',
|
||||||
|
`created: ${created.toISOString()}`,
|
||||||
|
`updated: ${updated.toISOString()}`,
|
||||||
|
"---",
|
||||||
|
"",
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
// front-matter/js-yaml may return Date for unquoted ISO-like timestamps
|
||||||
|
const task = parseTaskFile(content);
|
||||||
|
expect(task.created).toBe("2026-07-12T10:00:00.000Z");
|
||||||
|
expect(task.updated).toBe("2026-07-12T11:00:00.000Z");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as vscode from "vscode";
|
|
||||||
import { Task, TaskStatus } from "@/model/task";
|
import { Task, TaskStatus } from "@/model/task";
|
||||||
import { IConfigProvider, ITaskRepository } from "@/ports";
|
import { IConfigProvider, ITaskRepository } from "@/ports";
|
||||||
|
import * as vscode from "vscode";
|
||||||
|
|
||||||
const STATUS_ORDER: TaskStatus[] = ["todo", "in-progress", "done", "cancelled"];
|
const STATUS_ORDER: TaskStatus[] = ["todo", "in-progress", "done", "cancelled"];
|
||||||
|
|
||||||
@@ -12,30 +12,32 @@ const STATUS_ICONS: Record<TaskStatus, vscode.ThemeIcon> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class TaskGroupTreeItem extends vscode.TreeItem {
|
class TaskGroupTreeItem extends vscode.TreeItem {
|
||||||
constructor(
|
readonly tasks: Task[];
|
||||||
label: string,
|
|
||||||
public readonly tasks: Task[],
|
constructor(label: string, tasks: Task[], icon: vscode.ThemeIcon) {
|
||||||
icon: vscode.ThemeIcon,
|
|
||||||
) {
|
|
||||||
super(label, vscode.TreeItemCollapsibleState.Collapsed);
|
super(label, vscode.TreeItemCollapsibleState.Collapsed);
|
||||||
this.iconPath = icon;
|
this.iconPath = icon;
|
||||||
|
this.tasks = tasks;
|
||||||
this.description = `(${tasks.length})`;
|
this.description = `(${tasks.length})`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TaskTreeProvider implements vscode.TreeDataProvider<vscode.TreeItem> {
|
export class TaskTreeProvider implements vscode.TreeDataProvider<vscode.TreeItem> {
|
||||||
private _onDidChangeTreeData = new vscode.EventEmitter<
|
#onDidChangeTreeData = new vscode.EventEmitter<
|
||||||
vscode.TreeItem | undefined | null | void
|
vscode.TreeItem | undefined | null | void
|
||||||
>();
|
>();
|
||||||
readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
|
readonly onDidChangeTreeData = this.#onDidChangeTreeData.event;
|
||||||
|
|
||||||
constructor(
|
#taskStore: ITaskRepository;
|
||||||
private taskStore: ITaskRepository,
|
#configStore: IConfigProvider;
|
||||||
private configStore: IConfigProvider,
|
|
||||||
) {}
|
constructor(taskStore: ITaskRepository, configStore: IConfigProvider) {
|
||||||
|
this.#taskStore = taskStore;
|
||||||
|
this.#configStore = configStore;
|
||||||
|
}
|
||||||
|
|
||||||
refresh(): void {
|
refresh(): void {
|
||||||
this._onDidChangeTreeData.fire();
|
this.#onDidChangeTreeData.fire();
|
||||||
}
|
}
|
||||||
|
|
||||||
getTreeItem(element: vscode.TreeItem): vscode.TreeItem {
|
getTreeItem(element: vscode.TreeItem): vscode.TreeItem {
|
||||||
@@ -50,18 +52,22 @@ export class TaskTreeProvider implements vscode.TreeDataProvider<vscode.TreeItem
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async getRootChildren(): Promise<vscode.TreeItem[]> {
|
private async getRootChildren(): Promise<vscode.TreeItem[]> {
|
||||||
const projectPath = this.configStore.getProjectTaskPath();
|
const projectPath = this.#configStore.getProjectTaskPath();
|
||||||
if (!projectPath) {
|
if (!projectPath) {
|
||||||
return [new vscode.TreeItem("Open a workspace folder to start")];
|
return [new vscode.TreeItem("Open a workspace folder to start")];
|
||||||
}
|
}
|
||||||
|
|
||||||
const tasks = await this.taskStore.list(projectPath);
|
const tasks = await this.#taskStore.list(projectPath);
|
||||||
|
|
||||||
return STATUS_ORDER.map((status) => {
|
return STATUS_ORDER.map((status) => {
|
||||||
const groupTasks = tasks.filter((t) => t.status === status);
|
const groupTasks = tasks.filter((t) => t.status === status);
|
||||||
const label = status === "todo" ? "To Do"
|
const label =
|
||||||
: status === "in-progress" ? "In Progress"
|
status === "todo"
|
||||||
: status === "done" ? "Done"
|
? "To Do"
|
||||||
|
: status === "in-progress"
|
||||||
|
? "In Progress"
|
||||||
|
: status === "done"
|
||||||
|
? "Done"
|
||||||
: "Cancelled";
|
: "Cancelled";
|
||||||
const item = new TaskGroupTreeItem(
|
const item = new TaskGroupTreeItem(
|
||||||
label,
|
label,
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { IFileSystem } from "@/ports";
|
import { IFileSystem } from "@/ports";
|
||||||
|
|
||||||
export class InMemoryFileSystem implements IFileSystem {
|
export class InMemoryFileSystem implements IFileSystem {
|
||||||
private files = new Map<string, string>();
|
#files = new Map<string, string>();
|
||||||
|
|
||||||
reset(): void {
|
reset(): void {
|
||||||
this.files.clear();
|
this.#files.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
async readFile(path: string): Promise<string> {
|
async readFile(path: string): Promise<string> {
|
||||||
const content = this.files.get(path);
|
const content = this.#files.get(path);
|
||||||
if (content === undefined) {
|
if (content === undefined) {
|
||||||
throw new Error(`ENOENT: no such file: ${path}`);
|
throw new Error(`ENOENT: no such file: ${path}`);
|
||||||
}
|
}
|
||||||
@@ -16,17 +16,17 @@ export class InMemoryFileSystem implements IFileSystem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async writeFile(path: string, content: string): Promise<void> {
|
async writeFile(path: string, content: string): Promise<void> {
|
||||||
this.files.set(path, content);
|
this.#files.set(path, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteFile(path: string): Promise<void> {
|
async deleteFile(path: string): Promise<void> {
|
||||||
this.files.delete(path);
|
this.#files.delete(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
async readdir(path: string): Promise<string[]> {
|
async readdir(path: string): Promise<string[]> {
|
||||||
const prefix = path.endsWith("/") || path.endsWith("\\") ? path : path + "/";
|
const prefix = path.endsWith("/") || path.endsWith("\\") ? path : path + "/";
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
for (const key of this.files.keys()) {
|
for (const key of this.#files.keys()) {
|
||||||
if (key.startsWith(prefix)) {
|
if (key.startsWith(prefix)) {
|
||||||
const relative = key.slice(prefix.length);
|
const relative = key.slice(prefix.length);
|
||||||
const top = relative.split(/[/\\]/)[0];
|
const top = relative.split(/[/\\]/)[0];
|
||||||
|
|||||||
Reference in New Issue
Block a user