From b2522ef7d6c5e5e9b690ea2c6f0c1eb3181872da Mon Sep 17 00:00:00 2001 From: OpenVelo Agent Date: Thu, 23 Jul 2026 10:58:19 +0000 Subject: [PATCH] feat: Admin Area Blog and Blog Page 1.01 --- .kilo/plans/914.md | 65 ------------------- .kilo/plans/915.md | 48 ++++++++++++++ .../app/features/admin/blog/blog.component.ts | 2 +- tests/frontend/blog-admin-delete.spec.ts | 27 ++++++++ 4 files changed, 76 insertions(+), 66 deletions(-) delete mode 100644 .kilo/plans/914.md create mode 100644 .kilo/plans/915.md create mode 100644 tests/frontend/blog-admin-delete.spec.ts diff --git a/.kilo/plans/914.md b/.kilo/plans/914.md deleted file mode 100644 index 5346245..0000000 --- a/.kilo/plans/914.md +++ /dev/null @@ -1,65 +0,0 @@ -# Implementation Plan: Job 914 — Escape must close the New post overlay without saving - -## 1. Architectural Reconnaissance - -- **Codebase style & conventions:** - - Angular 17+ standalone components, `ChangeDetectionStrategy.OnPush`, signal inputs (`input()`), signal outputs (`output()`), the new `@if` control flow, and `effect()` for reactive side effects. - - Backend is NestJS with TypeORM; out of scope for this Job (no DB/API changes). - - The frontend admin blog feature lives under `frontend/src/app/features/admin/blog/`. The pure helpers live in `blog-form.pure.ts`; the modal is `blog-form-modal.component.ts`; the smart page is `blog.component.ts`. - - **Escape-to-close is the established convention for every modal in this codebase.** Every other modal uses `@HostListener('document:keydown.escape')` on the component class and emits its `cancel` output: - - `frontend/src/app/features/setup/setup-create-admin.component.ts:103` - - `frontend/src/app/features/admin/player-delete-modal.component.ts:61` - - `frontend/src/app/features/shell/change-password/change-password-modal.component.ts:156` - - `frontend/src/app/features/challenges/challenge-modal.component.ts:174` - - `frontend/src/app/features/admin/challenges/challenge-import-modal.component.ts:105` - - `frontend/src/app/features/admin/challenges/challenge-form-modal.component.ts:404` - - `frontend/src/app/features/admin/challenges/challenge-delete-modal.component.ts:56` - - `frontend/src/app/features/landing/landing.component.ts:102` - - The very similar `change-password-modal.component.ts:156–159` guards the handler with `if (this.open() && !this.submitting()) this.onCancel();`. We will mirror that exact contract for the blog form modal (swapping `submitting()` for `saving()`). - - **The blog form modal is the only modal missing this listener.** This is the bug described in the Job. -- **Data Layer:** No database changes — this is purely a frontend modal interaction fix. -- **Test Framework & Structure:** - - Jest 29 with `ts-jest`, frontend project runs in `jsdom` (`tests/jest.config.js`). - - Single test entry: `npm test` runs both backend and frontend projects; `npm run test:frontend` runs only frontend. - - Tests live under `tests/frontend/` (front-end) and `tests/backend/` (back-end). Source code lives under `frontend/src/app/` and `backend/src/`. Tests must NEVER live inside source folders. - - Existing frontend tests are predominantly pure-helper tests (no TestBed). The `challenge-card-accessibility.spec.ts` pattern uses a `readFileSync` source-text assertion against the component template, which is the safest pattern for testing that a `@HostListener` is wired into the component without standing up the full Angular runtime. -- **Required Tools & Dependencies:** None. No new packages, globals, or `setup.sh` changes are needed — `@angular/core` already exports `HostListener`, and Jest/jsdom is already configured. - -## 2. Impacted Files - -- **To Modify:** - - `frontend/src/app/features/admin/blog/blog-form-modal.component.ts` — add the `@HostListener('document:keydown.escape')` handler that calls the existing `onCancel()` when the modal is open and not currently saving. -- **To Create:** - - `tests/frontend/blog-form-modal-escape.spec.ts` — minimal, focused regression test that verifies the Escape listener is wired and routed correctly. Mirrors the existing source-text snapshot pattern from `tests/frontend/challenge-card-accessibility.spec.ts` so it stays free of TestBed/Angular runtime setup and runs instantly under `npm test`. - -## 3. Proposed Changes - -1. **Add an Escape handler to `BlogFormModalComponent`** in `frontend/src/app/features/admin/blog/blog-form-modal.component.ts`: - - Import `HostListener` from `@angular/core` alongside the existing `ChangeDetectionStrategy`, `ChangeDetectorRef`, `Component`, `DestroyRef`, `effect`, `inject`, `input`, `output`, `signal` imports. - - Add the following method on the class (mirroring `change-password-modal.component.ts:156–159`): - ```ts - @HostListener('document:keydown.escape') - onEscape(): void { - if (this.open() && !this.saving()) this.onCancel(); - } - ``` - - This reuses the existing `onCancel()` (`blog-form-modal.component.ts:210`) which already emits `cancel`. The parent `AdminBlogComponent.closeModal()` (`blog.component.ts:188`) clears `modalMode`, `editing`, `formError`, `saving`, etc., so no post is created or modified. - - Rationale for the `open() && !saving()` guard: the modal is conditionally rendered via `@if (open())` in the template, so the listener will fire when the modal is mounted regardless of `open()`'s value; we explicitly check `open()` for safety and parity with `change-password-modal.component.ts`, and we skip the handler while `saving()` is true so an in-flight create/publish cannot be cancelled mid-request (the existing `Publish`/`Save draft` buttons remain `[disabled]` while saving and the parent also blocks the toolbar via `actionInFlight`). - - No template, styles, or HTML attribute changes are needed. The `Cancel` button, backdrop click, and the new Escape keypress all flow through the same `onCancel()` → `cancel` output. - -2. **No backend / API changes.** The bug is purely a missing keyboard handler on the client. The REST endpoints (`POST /api/v1/admin/blog/posts`, `PATCH /api/v1/admin/blog/posts/:id`) are already correct; the failing test scenario was a *frontend* interaction that never called the API. - -3. **No `setup.sh` / dependency changes.** `HostListener` is part of `@angular/core`, which is already a dependency. - -## 4. Test Strategy - -- **Target Unit Test File:** `tests/frontend/blog-form-modal-escape.spec.ts` (new file, in the dedicated `tests/frontend/` folder, runnable via `npm test` from the repo root). -- **Mocking Strategy:** No runtime mocking required. The test uses the same source-text assertion pattern as `tests/frontend/challenge-card-accessibility.spec.ts`: read `frontend/src/app/features/admin/blog/blog-form-modal.component.ts` once with `fs.readFileSync` and assert that: - 1. `HostListener` is imported from `@angular/core` (presence of the symbol in the import block). - 2. The exact decorator `@HostListener('document:keydown.escape')` is present on the class. - 3. The decorated handler method calls `this.onCancel()`. - 4. The handler is guarded by `this.open()` and `this.saving()` (to prevent Escape from cancelling an in-flight save, matching the `change-password-modal` contract). - 5. The existing `onCancel()` method still emits `cancel` (regression guard that the wiring remains intact). -- The test must import `'@angular/compiler'` at the top for parity with the rest of the frontend suite (see `tests/frontend/blog-admin-form.spec.ts:1`). -- Avoid standing up TestBed, ComponentFixture, or any DOM/jest-environment-jsdom behavior — keep the test pure and instant, per the `IMPORTANT RULE CONCERNING TESTS` instructions. -- All tests must pass via `npm test` (or `npm run test:frontend`) inside the container without any UI/browser interaction. diff --git a/.kilo/plans/915.md b/.kilo/plans/915.md new file mode 100644 index 0000000..166570c --- /dev/null +++ b/.kilo/plans/915.md @@ -0,0 +1,48 @@ +# Implementation Plan: Job 915 — Enable Admin Blog Post Deletion Confirmation + +## Status + +The requested job is **not fully implemented**. The backend DELETE endpoint, persistence service, Angular API client, parent delete workflow, and confirmation modal already exist, but the parent binds the modal's disabled state to the selected post rather than the request-in-flight state. The UI therefore cannot complete an otherwise wired deletion flow. + +## 1. Architectural Reconnaissance + +- **Codebase style & conventions:** Node.js npm-workspace monorepo using TypeScript, NestJS 10 on the backend, and Angular 17 standalone components on the frontend. Angular admin pages use smart/container components with `signal()` state, signal inputs/outputs, `ChangeDetectionStrategy.OnPush`, and Angular control-flow syntax. The admin blog parent owns service calls and modal lifecycle; `BlogDeleteModalComponent` is presentational and only receives inputs/emits cancel/confirm events. +- **Data Layer:** TypeORM `BlogPostEntity` backed by SQLite via `better-sqlite3`. Blog deletion is already implemented in `backend/src/modules/blog/admin-blog.service.ts:65-69` with a repository lookup, not-found error, and `delete({ id })`; no schema or migration change is required. +- **Test Framework & Structure:** Root Jest multi-project configuration in `tests/jest.config.js`, with frontend tests under the dedicated `tests/frontend/` folder and backend tests under `tests/backend/`. The root `npm test` command runs all projects; focused frontend execution is available through `npm run test:frontend`. Existing blog tests are mostly pure-logic/source-contract tests, while the API suite already covers DELETE persistence and 404 behavior. +- **Required Tools & Dependencies:** No new package or system dependency is required. Existing Node/npm, Angular, Jest, jsdom, ts-jest, and repository dependencies are sufficient. `setup.sh` needs no change; it already installs dependencies, rebuilds `better-sqlite3`, and builds both workspaces. No `/data` fixture or persistent test asset is needed because this is a frontend state-binding fix and existing backend tests use in-memory SQLite. + +## 2. Impacted Files + +- **To Modify:** + - `frontend/src/app/features/admin/blog/blog.component.ts` — bind the delete modal's `deleting` input to `actionInFlight()` rather than `deleting() !== null`, preserving the selected post while enabling confirmation before the request starts and disabling it during the request. + - `tests/frontend/blog-admin-delete.spec.ts` — add a minimal focused regression test in the dedicated frontend test folder covering the modal binding and deletion state contract. +- **To Create:** + - `tests/frontend/blog-admin-delete.spec.ts` — new focused frontend regression test file, if no existing delete-modal/admin-page test is suitable for extension. + +No backend controller, service, entity, DTO, route, database migration, API client, delete modal component, documentation, dependency manifest, or setup script needs modification. + +## 3. Proposed Changes + +1. **Database / Schema Migration:** + - No change. `AdminBlogService.remove()` already deletes the requested `blog_post` row and returns not-found for a missing ID. + - Do not add migration or seed data. The expected persistence behavior is already provided by the existing DELETE API and is covered by `tests/backend/blog-admin.spec.ts:220-240`. + +2. **Backend Logic & APIs:** + - No change. `AdminBlogController.remove()` already exposes `DELETE /api/v1/admin/blog/posts/:id`, applies the admin guard/role protection, and returns HTTP 204. + - `BlogApiService.deletePost()` already calls the endpoint with the encoded ID and credentials. + - The parent `AdminBlogComponent.onDeleteConfirm()` already sets `actionInFlight` to true before awaiting the DELETE request, closes the modal after success, reloads the complete admin list, reports `Post deleted.`, and resets the in-flight signal in `finally`. Its error path keeps the modal open and exposes the server message. + +3. **Frontend UI Integration:** + - In `frontend/src/app/features/admin/blog/blog.component.ts:111-118`, change only the `[deleting]` binding from `[deleting]="deleting() !== null"` to `[deleting]="actionInFlight()"`. + - Keep `[post]="deleting()"` unchanged so the modal continues displaying the selected title and the parent retains the target ID until success or cancel. + - Keep the existing `actionInFlight` signal shared by create, edit, and delete operations. When `openDelete()` sets the selected post and opens the modal, `actionInFlight()` remains false, so `post-delete-ok` is enabled. When `onDeleteConfirm()` begins, it becomes true and the modal disables Delete while awaiting the request. On success, `closeModal()` clears the selection and `load()` refreshes the table; on failure, the selection/modal remain visible and the button is re-enabled after `finally` so retry is possible. + - Do not change `BlogDeleteModalComponent`: its `[disabled]="deleting()"` behavior is already correct when supplied the proper request-state signal. + +## 4. Test Strategy + +- **Target Unit Test File:** Add `tests/frontend/blog-admin-delete.spec.ts` under the existing dedicated Jest frontend test root. The test should be minimal and focused on the regression contract, not a browser/UI visual test. +- **Test cases:** + 1. Read/verify the admin component template contract that the delete modal receives `actionInFlight()` for its `deleting` input, and that it no longer derives the disabled state from the selected `deleting()` post. This matches the repository's existing lightweight source-contract testing style and directly catches reintroduction of the bug. + 2. Optionally instantiate the standalone `BlogDeleteModalComponent` with signal inputs using the existing Jest/jsdom Angular setup, set `deleting` false and true via `fixture.componentRef.setInput`, run `detectChanges()`, and assert the confirm button is enabled before the request and disabled during it. Keep this only if the current test environment supports standalone component compilation without introducing infrastructure; otherwise the source-contract test is sufficient for this one-line wiring regression. +- **Mocking Strategy:** Do not involve HTTP, the backend, SQLite, authentication, or real navigation in the focused frontend test. If component instantiation is used, provide only the modal's standalone imports and use signal input APIs; no service mock is needed because the modal emits outputs and contains no service dependency. The existing backend DELETE integration test remains the persistence/error boundary test. +- **Verification:** Run the single root command `npm test` after implementation so both existing backend API coverage and the new frontend regression test execute. The implementer should also run the repository's available frontend-focused Jest project if needed during iteration; no new command or setup change is required. diff --git a/frontend/src/app/features/admin/blog/blog.component.ts b/frontend/src/app/features/admin/blog/blog.component.ts index e2e3c5e..de4f7d3 100644 --- a/frontend/src/app/features/admin/blog/blog.component.ts +++ b/frontend/src/app/features/admin/blog/blog.component.ts @@ -112,7 +112,7 @@ type ModalMode = 'create' | 'edit' | null; [open]="deleteOpen()" [post]="deleting()" [errorMessage]="deleteError()" - [deleting]="deleting() !== null" + [deleting]="actionInFlight()" (cancel)="closeModal()" (confirm)="onDeleteConfirm()" /> diff --git a/tests/frontend/blog-admin-delete.spec.ts b/tests/frontend/blog-admin-delete.spec.ts new file mode 100644 index 0000000..2030588 --- /dev/null +++ b/tests/frontend/blog-admin-delete.spec.ts @@ -0,0 +1,27 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +const componentPath = resolve( + __dirname, + '../../frontend/src/app/features/admin/blog/blog.component.ts', +); +const source = readFileSync(componentPath, 'utf8'); + +describe('AdminBlogComponent — delete confirmation disabled binding', () => { + it('binds the delete modal deleting input to actionInFlight() so Delete is enabled on open', () => { + const modalTag = extractBlogDeleteModalTag(source); + expect(modalTag).not.toBeNull(); + expect(modalTag).toMatch(/\[deleting\]\s*=\s*"actionInFlight\(\)"/); + }); + + it('no longer derives the delete modal disabled state from the selected post', () => { + const modalTag = extractBlogDeleteModalTag(source); + expect(modalTag).not.toBeNull(); + expect(modalTag).not.toMatch(/\[deleting\]\s*=\s*"deleting\(\)\s*!==\s*null"/); + }); +}); + +function extractBlogDeleteModalTag(src: string): string | null { + const m = src.match(//); + return m ? m[0] : null; +} \ No newline at end of file -- 2.52.0