13 KiB
Implementation Plan: Admin Area General Settings and Categories 1.14 (Job 896)
0. Status — NOT yet implemented
The required behaviour for this Job is NOT fully implemented in this
repository. The category edit modal opens but the Name, Abbreviation,
and Description reactive-form controls are not populated with the
selected category's existing values when the user clicks the edit button
(e.g. cat-edit-CRY), and the abbreviation input is therefore not marked
readonly for system rows. The bug is reproduced every time an existing
category is opened for edit, regardless of whether the user previously
opened it in create mode or not.
Evidence (read-only inspection):
frontend/src/app/features/admin/categories/category-form-modal.component.ts:91-110declares a constructoreffect()that intends to callthis.form.patchValue({ name, abbreviation, description })andthis.abbreviationReadonly.set(c.isSystem)whenmode() === 'edit' && category()is truthy. The rest of the source tree (parent component, service, backend controller/service/DTO, schemas, migrations, and the existing backend Jest suite) already implements everything the Guide describes; only the frontend modal's edit-prefill step is broken.
Because the fix lives entirely in one frontend component (and a new
Jest test), no DB migration, backend code change, dependency addition,
or setup.sh change is required.
1. Architectural Reconnaissance
- Codebase style & conventions:
- Frontend: Angular 17.3 (
/repo/frontend/package.json), standalone components,ChangeDetectionStrategy.OnPush, signal inputs / outputs, reactive forms viafb.nonNullable.group. - Backend: NestJS 10 + Express, modular layout under
backend/src/modules/admin/**, TypeORM +better-sqlite3. - Tests: Jest 29 with two projects (
backend=ts-jest+node,frontend=ts-jest+jsdom) configured in/repo/tests/jest.config.js. A singlenpm testfrom the repo root runs every spec. Frontend specs live undertests/frontend/alongside existing admin specs.
- Frontend: Angular 17.3 (
- Data Layer:
- SQLite via TypeORM;
categorytable hasid,system_key,name,abbreviation,description,icon_path,created_at,updated_atcolumns (backend/src/database/entities/category.entity.ts). - Categories are CRUD-ed via
AdminCategoriesServicewhich already uppercases the abbreviation, protects system rows from abbreviation edits, and rejects duplicate abbreviations (backend/src/modules/admin/categories.service.ts:39-78). - User-supplied persistent state is stored under
/data/hipctf(created bysetup.sh); this job reads no project-shared data.
- SQLite via TypeORM;
- Test Framework & Structure:
- Jest 29 (
ts-jest). Two projects configured intests/jest.config.js. Frontend specs intests/frontend/, backend specs intests/backend/. Singlenpm test(also wired intopackage.jsonat the repo root) executes everything. - No DOM harness library is configured; existing frontend specs
query the rendered DOM through
fixture.nativeElement.querySelectoron the rendered template (see e.g.tests/frontend/admin-general-pure.spec.ts).
- Jest 29 (
- Required Tools & Dependencies: No new dependencies are needed.
All tooling required to fix and verify this job is already
installed by
setup.shand the rootpackage.json/tests/package.json. The implementer does not need to touchsetup.sh.
Component wiring already present in the repo
| Layer | File | Status |
|---|---|---|
| Edit modal | frontend/src/app/features/admin/categories/category-form-modal.component.ts |
buggy — edit pre-fill does not reach the DOM |
| Categories list + edit dispatcher | frontend/src/app/features/admin/categories/categories.component.ts:109-114 |
correct (openEdit(c) sets editing and modalMode) |
| General page (embeds categories) | frontend/src/app/features/admin/general.component.ts:195 |
already imports AdminCategoriesComponent |
| Admin service | frontend/src/app/core/services/admin.service.ts:96-122 |
correct (listCategories, updateCategory) |
| Backend controller | backend/src/modules/admin/admin-categories.controller.ts |
correct |
| Backend service + DTO | backend/src/modules/admin/categories.service.ts, backend/src/modules/admin/dto/categories.dto.ts |
correct |
| Backend tests | tests/backend/admin-categories-service.spec.ts |
correct (system-row, dup, etc.) |
| Guide | docs/guides/admin-categories.md |
correct (states the modal must pre-fill) |
2. Impacted Files
To Modify (1 file)
frontend/src/app/features/admin/categories/category-form-modal.component.ts— replace the broken constructoreffect()with a robust Angular 17 reactive-form prefill that always populatesname,abbreviation,description, setsabbreviationReadonlyfromc.isSystem, and resetsiconPreview/iconFilewhenever the parent changes thecategoryormodeinput. Must keep the existing[data-testid]selectors intact.
To Create (1 file)
tests/frontend/admin-categories-form-modal.spec.ts— Jest spec that mountsCategoryFormModalComponentwithTestBed, simulates the parent passingopen=true,mode='edit', and a fully-populatedAdminCategory(includingisSystem: true), then asserts that:input[data-testid="cf-name"]value equals the row'sname,input[data-testid="cf-abbr"]value equals the row'sabbreviationand has thereadonlyattribute whenisSystem === true,textarea[data-testid="cf-desc"]value equals the row'sdescription,- switching the inputs to a different category (a second
setInput('category', ...)/setInput('mode', 'edit')call) re-populates the form to the new row's values, - switching back to
mode='create'clears the form.
3. Proposed Changes
1. Fix the edit-prefill in the modal (category-form-modal.component.ts)
The bug: the existing constructor effect() reads
this.category() and this.mode() and calls
this.form.patchValue(...) inside an OnPush component. In Angular 17
the value-update flow works, but the display of the patched value
inside the <input formControlName="…"> element is not reaching the
DOM because the change originates from a constructor
effect() while change-detection is sitting idle, and the
formControlName directive's write-back into the native <input>
relies on the OnPush view being marked dirty. The same code path also
fails to mark abbreviationReadonly, which is exactly what the bug
report observes (abbreviation is not readonly and the three fields
read '').
Replace the constructor body so that:
- We keep reading both inputs inside the effect:
const c = this.category(); const m = this.mode();. - We synchronise the form using individual
FormControl.setValuecalls (notpatchValueon the group), so the directive chain inside eachFormControlNameruns theDefaultValueAccessor.writeValuepath that reaches the native input. - After every prefill (edit or create-reset), we explicitly call
inject(ChangeDetectorRef).markForCheck()so the OnPush view repaints. - We still set
abbreviationReadonly.set(c.isSystem)andiconPreview.set(c.iconPath || null)inside theeditbranch, andabbreviationReadonly.set(false)/iconPreview.set(null)in theelsebranch. - We also clear
iconFile(already done in current code) and reset thedescriptioncontrol in the create branch. - We add
allowSignalWrites: trueto theeffect()options so that the signal writes inside it are tolerated.
Pseudocode for the replacement constructor:
private readonly cdr = inject(ChangeDetectorRef);
constructor() {
effect(
() => {
const c = this.category();
const m = this.mode();
if (m === 'edit' && c) {
this.form.controls.name.setValue(c.name, { emitEvent: false });
this.form.controls.abbreviation.setValue(c.abbreviation, { emitEvent: false });
this.form.controls.description.setValue(c.description ?? '', { emitEvent: false });
this.abbreviationReadonly.set(!!c.isSystem);
this.iconPreview.set(c.iconPath || null);
} else {
this.form.controls.name.setValue('', { emitEvent: false });
this.form.controls.abbreviation.setValue('', { emitEvent: false });
this.form.controls.description.setValue('', { emitEvent: false });
this.abbreviationReadonly.set(false);
this.iconPreview.set(null);
}
this.iconFile.set(null);
this.form.controls.name.markAsUntouched();
this.form.controls.name.markAsPristine();
this.form.controls.abbreviation.markAsUntouched();
this.form.controls.abbreviation.markAsPristine();
this.form.controls.description.markAsUntouched();
this.form.controls.description.markAsPristine();
this.cdr.markForCheck();
},
{ allowSignalWrites: true },
);
}
All other parts of the modal (template selectors cf-name,
cf-abbr, cf-desc, cf-icon, cf-cancel, cf-ok,
cat-form-modal, cat-form-backdrop; the CategoryFormSubmit
output; the icon FileReader; the readonly binding on the abbreviation
input) stay exactly the same, so the parent
AdminCategoriesComponent and its submit handler do not need to
change.
2. Add a frontend Jest spec (admin-categories-form-modal.spec.ts)
The spec file lives under tests/frontend/. It runs with the existing
tests/jest.config.js frontend project (jest + jsdom), is picked up
by npm test from the repo root, requires no UI, and asserts the
behaviour the bug report says is broken.
Mocking strategy:
- No HTTP mocks are required — the modal is rendered in isolation with
its three inputs (
open,mode,category) bound directly viafixture.componentRef.setInput(...).AdminServiceis unused by the modal. - Construct
CategoryFormModalComponentthroughTestBedand target by querying the existingdata-testidattributes (cf-name,cf-abbr,cf-desc). No CSS selectors are introduced. - Provide
FormBuilderandDestroyRefvia DI (already provided viainject(FormBuilder)andinject(DestroyRef)becauseTestBed.createComponentresolves Angular's DI tree).
Spec assertions (single describe('edit prefill'), four its):
populates name, abbreviation, description for a system row— setopen=true,mode='edit',category = { id:'x', name:'Cryptography', abbreviation:'CRY', description:'Cryptographic challenges', iconPath:'', isSystem:true }, callfixture.detectChanges(), read each input's.value, expect the row's values, and expectcf-abbrto carry thereadonlyattribute.populates name, abbreviation, description for a user row— same shape butisSystem: false; expectcf-abbrto NOT bereadonly.re-populates when the parent switches to another row—setInput('mode','edit')with row A,detectChanges(), assert values, thensetInput('category', rowB),detectChanges(), assert new values.clears the form when the parent switches back to create— after the edit-prefill assertions,setInput('mode','create'),setInput('category', null),detectChanges(), assert empty values.
The spec is intentionally minimal (5 assertions per case, no async timers, no DOM harnesses) so it compiles and runs instantly.
4. Test Strategy
- Target spec file:
tests/frontend/admin-categories-form-modal.spec.ts(new). - Mocking strategy: no HTTP mocks, no service mocks. The modal is
component-only;
fixture.componentRef.setInput(...)simulates the parent setting theopen,mode,categorysignals, andfixture.detectChanges()flushes the patched values to the DOM via the OnPush view marker we add. - Coverage scope: the four bullets above plus a smoke check that
submitandcanceloutputs are not emitted by the modal alone (the parent owns the network call). - Run command:
npm testfrom the repository root (tests/jest.config.jsis the active config). The new spec runs inside thefrontendproject and finishes in well under a second with no network or DB activity. - Existing tests: re-run
tests/backend/admin-categories-service.spec.tsto confirm the system-row / dup / CRUD rules still pass; the frontend modal change has no backend impact.
5. Files Touched — Summary
| Path | Action |
|---|---|
frontend/src/app/features/admin/categories/category-form-modal.component.ts |
edit — replace constructor effect() body with the explicit-setValue + markForCheck version above; add inject(ChangeDetectorRef) and { allowSignalWrites: true } option |
tests/frontend/admin-categories-form-modal.spec.ts |
create — the four it()s described in §3.2 |
No backend, schema, DTO, controller, service, route, migration, or
dependency change is required, and setup.sh does not need to be
modified.