From ce692098207616773a322edcd074415372d9f135 Mon Sep 17 00:00:00 2001 From: Ku6epXBOCTuK Date: Mon, 13 Jul 2026 16:00:25 +0500 Subject: [PATCH] feat: tasks workflow --- .vscode/tasks.json | 2 +- docs/PLAN.md | 118 +++++++++++++++++++++++------- package-lock.json | 29 ++++++++ package.json | 1 + src/commands/changeStatus.test.ts | 76 +++++++++++++++++++ src/commands/changeStatus.ts | 46 ++++++++++++ src/commands/createTask.test.ts | 61 +++++++++++++++ src/commands/createTask.ts | 34 +++++++++ src/commands/deleteTask.test.ts | 58 +++++++++++++++ src/commands/deleteTask.ts | 32 ++++++++ src/commands/openTask.test.ts | 60 +++++++++++++++ src/commands/openTask.ts | 41 +++++++++++ src/error.ts | 20 +++++ src/ports.ts | 2 + src/storage/FsTaskRepository.ts | 10 +-- 15 files changed, 559 insertions(+), 31 deletions(-) create mode 100644 src/commands/changeStatus.test.ts create mode 100644 src/commands/changeStatus.ts create mode 100644 src/commands/createTask.test.ts create mode 100644 src/commands/createTask.ts create mode 100644 src/commands/deleteTask.test.ts create mode 100644 src/commands/deleteTask.ts create mode 100644 src/commands/openTask.test.ts create mode 100644 src/commands/openTask.ts create mode 100644 src/error.ts diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 3793125..d438619 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -3,7 +3,7 @@ "tasks": [ { "type": "npm", - "script": "compile", + "script": "build", "group": { "kind": "build", "isDefault": true diff --git a/docs/PLAN.md b/docs/PLAN.md index 1ba8a5b..d56a5e2 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -6,6 +6,7 @@ ```markdown --- +id: "..." title: "Пофиксить баг логина" status: todo # todo | in-progress | done | cancelled priority: high # low | medium | high | critical @@ -21,7 +22,7 @@ updated: 2026-07-12T10:00:00Z **Структура папок:** - `.vscode/tasks/` — проектные задачи -- `~/project-tasks/` — глобальные задачи (настраивается) +- глобальная папка — настраивается (`projectTasks.globalPath`) Оба пути настраиваются через VS Code settings. @@ -29,50 +30,117 @@ updated: 2026-07-12T10:00:00Z Через **Custom Editor** + **WebView**: -1. **Activity Bar** — своя вкладка с Task Dashboard +1. **Activity Bar** — вкладка Project Tasks 2. **Task Dashboard** — WebView в editor area со split layout: - Левая панель — список задач с фильтрацией/группировкой - Правая панель — содержимое выбранной задачи (редактируемый markdown) -3. **TreeView в Explorer** — быстрый просмотр +3. **TreeView** — быстрый просмотр по статусу ## Архитектура кода +Актуальная раскладка (после рефакторинга портов/адаптеров): + ```txt src/ -├── extension.ts # Активация, регистрация +├── extension.ts # composition root (DI, register) +├── ports.ts # IFileSystem, ITaskRepository, IConfigProvider +├── error.ts # AppError + factories (единый каталог ошибок) +├── model/task.ts ├── storage/ -│ ├── taskStore.ts # CRUD для .task.md (gray-matter парсинг) -│ └── configStore.ts # Настройки -├── model/ -│ └── task.ts # Интерфейс Task + типы -├── views/ -│ ├── taskTreeProvider.ts # TreeView в sidebar -│ ├── taskListProvider.ts # Data provider для WebView -│ └── taskDashboardPanel.ts # WebView split layout -├── commands/ +│ ├── FsTaskRepository.ts +│ ├── NodeFileSystem.ts +│ └── VscodeConfigProvider.ts +├── commands/ # use-cases без vscode UI │ ├── createTask.ts │ ├── openTask.ts │ ├── deleteTask.ts │ └── changeStatus.ts +├── views/ +│ └── taskTreeProvider.ts # (+ dashboard later) └── utils/ ├── uuid.ts - └── markdown.ts + └── markdown.ts # parse/serialize frontmatter + +tests/helpers/InMemoryFileSystem.ts ``` +UI (input box, quick pick, open editor) — тонкие адаптеры; бизнес-логика в +use-cases + `ITaskRepository`. Ошибки — `AppError` из `error.ts`. + ## План реализации -| Фаза | Что делаем | -| ------------------------ | -------------------------------------------------------------- | -| **1. Scaffold** | `yo code`, TypeScript, настройка package.json | -| **2. Storage** | TaskStore — чтение `.task.md`, парсинг через gray-matter, CRUD | -| **3. TreeView** | Боковая панель со списком задач по статусу | -| **4. Команды** | createTask, deleteTask, changeStatus, openTask | -| **5. Task Dashboard** | WebView split layout в editor area | -| **6. Глобальные задачи** | Второй storage (глобальная папка) | -| **7. Kanban** | Drag-n-drop доска | -| **8. Публикация** | vsce publish | +| Фаза | Что делаем | +| ------------------------ | ---------------------------------------------------------------------- | +| **1. Scaffold** | Extension, TypeScript, сборка, package.json | +| **2. Storage** | CRUD `.task.md`, парсинг frontmatter, unit-тесты storage | +| **3. TreeView** | sidebar по статусу, file watcher | +| **4. Команды** | createTask, deleteTask, changeStatus, openTask (use-cases → wiring UI) | +| **5. Task Dashboard** | WebView split layout | +| **6. Глобальные задачи** | использование globalPath | +| **7. Kanban** | drag-n-drop | +| **8. Публикация** | vsce publish | + +### Фаза 4 — команды (use-cases) + +| Команда | Поведение | +| -------------- | -------------------------------------------------------------- | +| `createTask` | title + folder → defaults todo/medium; empty title / no folder | +| `deleteTask` | folder + id → delete; not found | +| `changeStatus` | folder + id + status → update; not found | +| `openTask` | folder + id → path + task для editor; not found | ## Зависимости -- `gray-matter` — парсинг YAML frontmatter +- `front-matter` — разбор YAML frontmatter (body + attributes) +- `js-yaml` — сериализация frontmatter обратно в YAML (`dump`) - `uuid` — генерация ID +- `neverthrow` — `Result` / `ResultAsync` для use-cases +- `vitest` / `eslint` — dev + +## История изменений (решения, не чеклист) + +Краткая летопись смен относительно **изначального** наброска плана. Детали +рефакторинга storage/тестов — в архиве `docs/REFACTOR-AND-TEST.md` (документ +закрыт, не ведём). + +### Изначальный набросок (v0) + +- Парсинг: **`gray-matter`** +- Storage: `taskStore.ts` (CRUD), `configStore.ts` (settings) +- Сборка/скелет: `yo code`-ориентированный scaffold +- Дерево модулей: storage + views (tree/list/dashboard) + commands + utils + +### Рефакторинг storage / чистая архитектура + +- `taskStore` / `configStore` → **порты** (`IFileSystem`, `ITaskRepository`, + `IConfigProvider`) и адаптеры: + - `FsTaskRepository`, `NodeFileSystem`, `VscodeConfigProvider` +- `extension.ts` — composition root (DI), tree зависит от портов +- Unit-тесты: Vitest, colocation, `InMemoryFileSystem` в `tests/helpers/` + +### `gray-matter` → `front-matter` + `js-yaml` + +- **Было в плане:** один пакет `gray-matter` на parse (+ stringify при + необходимости). +- **Стало:** `front-matter` для чтения attributes/body; `js-yaml` (`dump`) для + записи frontmatter в `serializeTask`. +- **Зачем:** `gray-matter` даёт лишние возможности парсинга (eval для JS и + прочая «магия»), которые для задач не нужны; Vite на eval ругается, с точки + зрения безопасности это нежелательно. Поэтому parse через `front-matter`, + serialize через `js-yaml` (`dump`). Формат файла тот же: Markdown + YAML + frontmatter. + +### neverthrow + `AppError` + +- Use-cases возвращают `ResultAsync` вместо кастомных + `{ ok, reason }`. +- Каталог ошибок: `src/error.ts` (`AppErrorCode`, factories `AppError.noFolder()` + и т.д.). + +### Прочее относительно v0 + +- Сборка: Vite (extension bundle), не классический `tsc`-only из yo code +- В модели/файле задачи зафиксирован **`id`** в frontmatter (нужен для + list/get/update/delete) +- Фаза 4: use-cases в `src/commands/*` тестируются без vscode; UI — отдельные + адаптеры diff --git a/package-lock.json b/package-lock.json index 0157582..b5cd5de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "front-matter": "^4.0.2", "js-yaml": "^5.2.1", + "neverthrow": "^8.2.0", "prettier": "^3.9.5", "uuid": "^11.0.0" }, @@ -727,6 +728,22 @@ "dev": true, "license": "MIT" }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -2358,6 +2375,18 @@ "dev": true, "license": "MIT" }, + "node_modules/neverthrow": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/neverthrow/-/neverthrow-8.2.0.tgz", + "integrity": "sha512-kOCT/1MCPAxY5iUV3wytNFUMUolzuwd/VF/1KCx7kf6CutrOsTie+84zTGTpgQycjvfLdBBdvBvFLqFD2c0wkQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@rollup/rollup-linux-x64-gnu": "^4.24.0" + } + }, "node_modules/obug": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", diff --git a/package.json b/package.json index 23d9004..fb003dc 100644 --- a/package.json +++ b/package.json @@ -117,6 +117,7 @@ "dependencies": { "front-matter": "^4.0.2", "js-yaml": "^5.2.1", + "neverthrow": "^8.2.0", "prettier": "^3.9.5", "uuid": "^11.0.0" }, diff --git a/src/commands/changeStatus.test.ts b/src/commands/changeStatus.test.ts new file mode 100644 index 0000000..10f0a00 --- /dev/null +++ b/src/commands/changeStatus.test.ts @@ -0,0 +1,76 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { err } from "neverthrow"; +import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem"; +import { AppError } from "@/error"; +import { FsTaskRepository } from "@/storage/FsTaskRepository"; +import { changeStatus } from "./changeStatus"; + +describe("changeStatus", () => { + const folder = "/tasks"; + let repo: FsTaskRepository; + + beforeEach(() => { + repo = new FsTaskRepository(new InMemoryFileSystem(), ".task.md"); + }); + + it("updates status of an existing task", async () => { + const created = await repo.create(folder, { + title: "Work item", + description: "", + status: "todo", + priority: "medium", + tags: [], + assignee: "", + }); + + const result = await changeStatus(repo, { + folderPath: folder, + id: created.id, + status: "in-progress", + }); + + expect(result.isOk()).toBe(true); + if (result.isErr()) return; + expect(result.value.status).toBe("in-progress"); + expect(result.value.id).toBe(created.id); + + const stored = await repo.getById(folder, created.id); + expect(stored?.status).toBe("in-progress"); + }); + + it("returns not_found for unknown id", async () => { + const result = await changeStatus(repo, { + folderPath: folder, + id: "missing", + status: "done", + }); + expect(result).toEqual(err(AppError.notFound())); + }); + + it("rejects missing folder", async () => { + const result = await changeStatus(repo, { + folderPath: undefined, + id: "x", + status: "done", + }); + expect(result).toEqual(err(AppError.noFolder())); + }); + + it("rejects missing id", async () => { + const result = await changeStatus(repo, { + folderPath: folder, + id: undefined, + status: "done", + }); + expect(result).toEqual(err(AppError.noId())); + }); + + it("rejects missing status", async () => { + const result = await changeStatus(repo, { + folderPath: folder, + id: "x", + status: undefined, + }); + expect(result).toEqual(err(AppError.noStatus())); + }); +}); diff --git a/src/commands/changeStatus.ts b/src/commands/changeStatus.ts new file mode 100644 index 0000000..0eaf651 --- /dev/null +++ b/src/commands/changeStatus.ts @@ -0,0 +1,46 @@ +import { errAsync, okAsync, ResultAsync } from "neverthrow"; +import { AppError } from "@/error"; +import { Task, TaskStatus } from "@/model/task"; +import { ITaskRepository } from "@/ports"; + +export type ChangeStatusInput = { + folderPath?: string; + id?: string; + status?: TaskStatus; +}; + +export function changeStatus( + repo: ITaskRepository, + input: ChangeStatusInput, +): ResultAsync { + if (input.folderPath === undefined) { + return errAsync(AppError.noFolder()); + } + if (input.id === undefined) { + return errAsync(AppError.noId()); + } + if (input.status === undefined) { + return errAsync(AppError.noStatus()); + } + + const folderPath = input.folderPath; + const id = input.id; + const status = input.status; + + return ResultAsync.fromSafePromise(repo.getById(folderPath, id)).andThen( + (existing) => { + if (!existing) { + return errAsync(AppError.notFound()); + } + + const next: Task = { ...existing, status }; + return ResultAsync.fromSafePromise(repo.update(folderPath, next)).andThen( + () => + ResultAsync.fromSafePromise(repo.getById(folderPath, id)).andThen( + (updated) => + updated ? okAsync(updated) : errAsync(AppError.notFound()), + ), + ); + }, + ); +} diff --git a/src/commands/createTask.test.ts b/src/commands/createTask.test.ts new file mode 100644 index 0000000..3efa569 --- /dev/null +++ b/src/commands/createTask.test.ts @@ -0,0 +1,61 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { err } from "neverthrow"; +import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem"; +import { AppError } from "@/error"; +import { FsTaskRepository } from "@/storage/FsTaskRepository"; +import { createTask } from "./createTask"; + +describe("createTask", () => { + const folder = "/tasks"; + let repo: FsTaskRepository; + + beforeEach(() => { + repo = new FsTaskRepository(new InMemoryFileSystem(), ".task.md"); + }); + + it("creates a todo/medium task from title", async () => { + const result = await createTask(repo, { + folderPath: folder, + title: " New feature ", + }); + + expect(result.isOk()).toBe(true); + if (result.isErr()) return; + + expect(result.value.title).toBe("New feature"); + expect(result.value.status).toBe("todo"); + expect(result.value.priority).toBe("medium"); + expect(result.value.description).toBe(""); + expect(result.value.tags).toEqual([]); + expect(result.value.assignee).toBe(""); + + const listed = await repo.list(folder); + expect(listed).toHaveLength(1); + expect(listed[0].id).toBe(result.value.id); + }); + + it("rejects empty title", async () => { + const result = await createTask(repo, { + folderPath: folder, + title: " ", + }); + expect(result).toEqual(err(AppError.emptyTitle())); + expect(await repo.list(folder)).toEqual([]); + }); + + it("rejects missing title", async () => { + const result = await createTask(repo, { + folderPath: folder, + title: undefined, + }); + expect(result).toEqual(err(AppError.emptyTitle())); + }); + + it("rejects missing folder", async () => { + const result = await createTask(repo, { + folderPath: undefined, + title: "Task", + }); + expect(result).toEqual(err(AppError.noFolder())); + }); +}); diff --git a/src/commands/createTask.ts b/src/commands/createTask.ts new file mode 100644 index 0000000..11672bd --- /dev/null +++ b/src/commands/createTask.ts @@ -0,0 +1,34 @@ +import { errAsync, ResultAsync } from "neverthrow"; +import { AppError } from "@/error"; +import { Task } from "@/model/task"; +import { ITaskRepository } from "@/ports"; + +export type CreateTaskInput = { + folderPath?: string; + title?: string; +}; + +export function createTask( + repo: ITaskRepository, + input: CreateTaskInput, +): ResultAsync { + if (input.folderPath === undefined) { + return errAsync(AppError.noFolder()); + } + + const title = input.title?.trim() ?? ""; + if (!title) { + return errAsync(AppError.emptyTitle()); + } + + return ResultAsync.fromSafePromise( + repo.create(input.folderPath, { + title, + description: "", + status: "todo", + priority: "medium", + tags: [], + assignee: "", + }), + ); +} diff --git a/src/commands/deleteTask.test.ts b/src/commands/deleteTask.test.ts new file mode 100644 index 0000000..bdd3c0f --- /dev/null +++ b/src/commands/deleteTask.test.ts @@ -0,0 +1,58 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { err, ok } from "neverthrow"; +import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem"; +import { AppError } from "@/error"; +import { FsTaskRepository } from "@/storage/FsTaskRepository"; +import { deleteTask } from "./deleteTask"; + +describe("deleteTask", () => { + const folder = "/tasks"; + let repo: FsTaskRepository; + + beforeEach(() => { + repo = new FsTaskRepository(new InMemoryFileSystem(), ".task.md"); + }); + + it("deletes an existing task", async () => { + const created = await repo.create(folder, { + title: "To delete", + description: "", + status: "todo", + priority: "medium", + tags: [], + assignee: "", + }); + + const result = await deleteTask(repo, { + folderPath: folder, + id: created.id, + }); + + expect(result).toEqual(ok(undefined)); + expect(await repo.list(folder)).toEqual([]); + }); + + it("returns not_found for unknown id", async () => { + const result = await deleteTask(repo, { + folderPath: folder, + id: "missing", + }); + expect(result).toEqual(err(AppError.notFound())); + }); + + it("rejects missing folder", async () => { + const result = await deleteTask(repo, { + folderPath: undefined, + id: "x", + }); + expect(result).toEqual(err(AppError.noFolder())); + }); + + it("rejects missing id", async () => { + const result = await deleteTask(repo, { + folderPath: folder, + id: undefined, + }); + expect(result).toEqual(err(AppError.noId())); + }); +}); diff --git a/src/commands/deleteTask.ts b/src/commands/deleteTask.ts new file mode 100644 index 0000000..3d27466 --- /dev/null +++ b/src/commands/deleteTask.ts @@ -0,0 +1,32 @@ +import { errAsync, ResultAsync } from "neverthrow"; +import { AppError } from "@/error"; +import { ITaskRepository } from "@/ports"; + +export type DeleteTaskInput = { + folderPath?: string; + id?: string; +}; + +export function deleteTask( + repo: ITaskRepository, + input: DeleteTaskInput, +): ResultAsync { + if (input.folderPath === undefined) { + return errAsync(AppError.noFolder()); + } + if (input.id === undefined) { + return errAsync(AppError.noId()); + } + + const folderPath = input.folderPath; + const id = input.id; + + return ResultAsync.fromSafePromise(repo.getById(folderPath, id)).andThen( + (existing) => { + if (!existing) { + return errAsync(AppError.notFound()); + } + return ResultAsync.fromSafePromise(repo.delete(folderPath, id)); + }, + ); +} diff --git a/src/commands/openTask.test.ts b/src/commands/openTask.test.ts new file mode 100644 index 0000000..5c3d8f1 --- /dev/null +++ b/src/commands/openTask.test.ts @@ -0,0 +1,60 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { err } from "neverthrow"; +import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem"; +import { AppError } from "@/error"; +import { FsTaskRepository } from "@/storage/FsTaskRepository"; +import { openTask } from "./openTask"; + +describe("openTask", () => { + const folder = "/tasks"; + let repo: FsTaskRepository; + + beforeEach(() => { + repo = new FsTaskRepository(new InMemoryFileSystem(), ".task.md"); + }); + + it("returns file path and task for an existing id", async () => { + const created = await repo.create(folder, { + title: "Open me", + description: "body", + status: "todo", + priority: "high", + tags: [], + assignee: "", + }); + + const result = await openTask(repo, { + folderPath: folder, + id: created.id, + }); + + expect(result.isOk()).toBe(true); + if (result.isErr()) return; + expect(result.value.path).toBe("/tasks/open-me.task.md"); + expect(result.value.task).toEqual(created); + }); + + it("returns not_found for unknown id", async () => { + const result = await openTask(repo, { + folderPath: folder, + id: "missing", + }); + expect(result).toEqual(err(AppError.notFound())); + }); + + it("rejects missing folder", async () => { + const result = await openTask(repo, { + folderPath: undefined, + id: "x", + }); + expect(result).toEqual(err(AppError.noFolder())); + }); + + it("rejects missing id", async () => { + const result = await openTask(repo, { + folderPath: folder, + id: undefined, + }); + expect(result).toEqual(err(AppError.noId())); + }); +}); diff --git a/src/commands/openTask.ts b/src/commands/openTask.ts new file mode 100644 index 0000000..253e6f9 --- /dev/null +++ b/src/commands/openTask.ts @@ -0,0 +1,41 @@ +import { errAsync, okAsync, ResultAsync } from "neverthrow"; +import { AppError } from "@/error"; +import { Task } from "@/model/task"; +import { ITaskRepository } from "@/ports"; + +export type OpenTaskInput = { + folderPath?: string; + id?: string; +}; + +export type OpenTaskValue = { + path: string; + task: Task; +}; + +export function openTask( + repo: ITaskRepository, + input: OpenTaskInput, +): ResultAsync { + if (input.folderPath === undefined) { + return errAsync(AppError.noFolder()); + } + if (input.id === undefined) { + return errAsync(AppError.noId()); + } + + const folderPath = input.folderPath; + const id = input.id; + + return ResultAsync.fromSafePromise(repo.getById(folderPath, id)).andThen( + (task) => { + if (!task) { + return errAsync(AppError.notFound()); + } + return okAsync({ + path: repo.getFilePath(folderPath, task), + task, + }); + }, + ); +} diff --git a/src/error.ts b/src/error.ts new file mode 100644 index 0000000..5f6f8a8 --- /dev/null +++ b/src/error.ts @@ -0,0 +1,20 @@ +/** All application-level error codes. */ +export type AppErrorCode = + | "no_folder" + | "no_id" + | "no_status" + | "empty_title" + | "not_found"; + +/** Discriminated app error returned via neverthrow `Result` / `ResultAsync`. */ +export type AppError = { + readonly code: AppErrorCode; +}; + +export const AppError = { + noFolder: (): AppError => ({ code: "no_folder" }), + noId: (): AppError => ({ code: "no_id" }), + noStatus: (): AppError => ({ code: "no_status" }), + emptyTitle: (): AppError => ({ code: "empty_title" }), + notFound: (): AppError => ({ code: "not_found" }), +} as const; diff --git a/src/ports.ts b/src/ports.ts index 01b2e31..101377a 100644 --- a/src/ports.ts +++ b/src/ports.ts @@ -17,6 +17,8 @@ export interface ITaskRepository { ): Promise; update(folderPath: string, task: Task): Promise; delete(folderPath: string, id: string): Promise; + /** Absolute/joined path for the task file in `folderPath`. */ + getFilePath(folderPath: string, task: Task): string; } export interface IConfigProvider { diff --git a/src/storage/FsTaskRepository.ts b/src/storage/FsTaskRepository.ts index a21ceaa..7628552 100644 --- a/src/storage/FsTaskRepository.ts +++ b/src/storage/FsTaskRepository.ts @@ -68,7 +68,7 @@ export class FsTaskRepository implements ITaskRepository { }; await this.#fs.mkdir(folderPath); - const filePath = this.taskFilePath(folderPath, newTask); + const filePath = this.getFilePath(folderPath, newTask); await this.#fs.writeFile(filePath, serializeTask(newTask)); return newTask; @@ -77,10 +77,10 @@ export class FsTaskRepository implements ITaskRepository { async update(folderPath: string, task: Task): Promise { const existing = await this.getById(folderPath, task.id); const updatedTask = { ...task, updated: this.#now() }; - const newPath = this.taskFilePath(folderPath, updatedTask); + const newPath = this.getFilePath(folderPath, updatedTask); if (existing) { - const oldPath = this.taskFilePath(folderPath, existing); + const oldPath = this.getFilePath(folderPath, existing); if (oldPath !== newPath) { await this.#fs.deleteFile(oldPath).catch(() => {}); } @@ -93,7 +93,7 @@ export class FsTaskRepository implements ITaskRepository { const task = await this.getById(folderPath, id); if (!task) return; - const filePath = this.taskFilePath(folderPath, task); + const filePath = this.getFilePath(folderPath, task); try { await this.#fs.deleteFile(filePath); } catch { @@ -101,7 +101,7 @@ export class FsTaskRepository implements ITaskRepository { } } - private taskFilePath(folderPath: string, task: Task): string { + getFilePath(folderPath: string, task: Task): string { const safeName = task.title .toLowerCase() .replace(/[^a-z0-9\u0400-\u04FF]+/g, "-")