Feat: Add tag browser page at /tags with count-sorted tag list and library deep-link
- Extends GET /api/v1/tags with sort=count_desc and min_count query params - New TagsComponent at /tags (public, no auth guard) shows all tags sorted by image count - Clicking a tag navigates to /?tags=<name> for a pre-filtered library view - LibraryComponent reads ?tags= query param on init to support deep-linking from tag browser - Library header gains a "Browse tags" link to /tags for discoverability - All 15 TDD tasks complete; ruff, ng lint, and ng build clean Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
152
specs/007-tag-browser/tasks.md
Normal file
152
specs/007-tag-browser/tasks.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# Tasks: Tag Browser
|
||||
|
||||
**Input**: Design documents from `specs/007-tag-browser/`
|
||||
**Prerequisites**: plan.md ✅, spec.md ✅, data-model.md ✅, contracts/ ✅, quickstart.md ✅
|
||||
|
||||
**Tests**: TDD is non-negotiable (§5.1). Every implementation task is preceded by a failing-test task. Test tasks MUST be written and confirmed failing before the corresponding implementation task begins.
|
||||
|
||||
**Organization**: Foundational API + service changes first (block all stories), then one phase per user story.
|
||||
|
||||
## Format: `[ID] [P?] [Story] Description`
|
||||
|
||||
- **[P]**: Can run in parallel with other [P] tasks in the same phase
|
||||
- **[Story]**: Which user story this task belongs to
|
||||
- Exact file paths included in every task description
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Setup
|
||||
|
||||
No new project structure required. The existing layout accommodates all changes.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Foundational — API Enhancement & Service Update
|
||||
|
||||
**Purpose**: Extend `GET /api/v1/tags` with `sort` and `min_count` query parameters; update the Angular `TagService` to pass them. All three user stories depend on the API returning tags sorted by count with zero-count tags excluded.
|
||||
|
||||
**⚠️ CRITICAL**: No user story work can begin until this phase is complete.
|
||||
|
||||
- [X] T001 [P] Write failing API integration tests for `sort=count_desc` and `min_count=1` params in `api/tests/integration/test_tags.py` — assert response is ordered highest-count-first and excludes zero-count tags
|
||||
- [X] T002 [P] Write failing spec for updated `TagService.list()` accepting `sort` and `minCount` params in `ui/src/app/services/tag.service.spec.ts` — final signature: `list(prefix = '', limit = 100, offset = 0, sort?: string, minCount?: number)`
|
||||
- [X] T003 Extend `TagRepository.list_tags()` in `api/app/repositories/tag_repo.py` — add `sort: str = "name"` and `min_count: int = 0` params; apply `ORDER BY image_count DESC, name ASC` when `sort="count_desc"`; apply `HAVING image_count >= min_count` filter — run AFTER T001 (TDD)
|
||||
- [X] T004 Expose `sort` and `min_count` as optional query params in `api/app/routers/tags.py` — pass through to `tag_repo.list_tags()` — run AFTER T003
|
||||
- [X] T005 Update `TagService.list()` in `ui/src/app/services/tag.service.ts` — final signature: `list(prefix = '', limit = 100, offset = 0, sort?: string, minCount?: number)`; include `sort` and `min_count` in `HttpParams` when provided — run AFTER T002 (TDD)
|
||||
|
||||
**Execution order**: T001 ∥ T002 → T003 (after T001), T005 (after T002) → T004 (after T003)
|
||||
|
||||
**Checkpoint**: `GET /api/v1/tags?sort=count_desc&min_count=1` returns tags sorted by image count descending with zero-count tags excluded. `TagService.list()` passes the new params.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: User Story 1 — Browse All Tags (Priority: P1) 🎯 MVP
|
||||
|
||||
**Goal**: A `/tags` page that lists every tag (with count ≥ 1) sorted from most-used to least-used, with loading skeleton, empty state, and error state matching the existing design system.
|
||||
|
||||
**Independent Test**: Navigate to `/tags` while logged out. Confirm every tag with at least one image is shown with its count, ordered by count descending. Confirm the empty state appears when no tags exist.
|
||||
|
||||
### Tests for User Story 1
|
||||
|
||||
- [X] T006 [US1] Write failing spec for `TagBrowserComponent` in `ui/src/app/tags/tags.component.spec.ts` covering: (a) skeleton shown while loading, (b) tag list rendered with name and count after load, (c) tags ordered by count descending, (d) empty state shown when tag list is empty, (e) error state shown on fetch failure with retry button, (f) each rendered tag element has an `href` of `/?tags=<tagname>` (FR-005 coverage), (g) component renders when `AuthService` is not present / user is unauthenticated (FR-006 coverage)
|
||||
|
||||
### Implementation for User Story 1
|
||||
|
||||
- [X] T007 [US1] Create `TagBrowserComponent` in `ui/src/app/tags/tags.component.ts` — standalone component; on init call `tagService.list('', 500, 0, 'count_desc', 1)` (positional order matches T005 signature); display tag chips with name + count; each chip is a `routerLink="/"` with `[queryParams]="{tags: tag.name}"` so the href renders as `/?tags=<name>`; include skeleton loading state (reuse `.skeleton` class from global styles), empty state, and error state with retry; apply design tokens throughout
|
||||
- [X] T008 [P] [US1] Add `/tags` lazy route to `ui/src/app/app.routes.ts` — load `TagBrowserComponent`; no auth guard (public route)
|
||||
|
||||
**Checkpoint**: `/tags` renders a sorted, filterable tag list visible without authentication.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: User Story 2 — Navigate from Tag to Library (Priority: P1)
|
||||
|
||||
**Goal**: Clicking a tag on the tag browser navigates to the library pre-filtered to that tag. Requires the library to read `?tags=<name>` from the URL on init and apply it as an active filter before the first image load.
|
||||
|
||||
**Independent Test**: Navigate directly to `/?tags=cat` in the browser. Confirm the library loads showing only images tagged `cat` and the `cat` chip appears in the active filter bar.
|
||||
|
||||
### Tests for User Story 2
|
||||
|
||||
- [X] T009 [US2] Write failing spec for `LibraryComponent` reading `?tags=` query param in `ui/src/app/library/library.component.spec.ts` — assert that when the component initialises with `?tags=cat` in the URL, `activeFilters` contains `['cat']` and `imageService.list` is called with `['cat']`
|
||||
|
||||
### Implementation for User Story 2
|
||||
|
||||
- [X] T010 [US2] Update `LibraryComponent` in `ui/src/app/library/library.component.ts` — inject `ActivatedRoute`; in `ngOnInit`, read `snapshot.queryParamMap.get('tags')`; if present, split by comma, set `activeFilters` before calling `load()` so the first fetch is already filtered
|
||||
|
||||
**Checkpoint**: Navigating to `/?tags=cat` from the tag browser shows the correctly filtered library.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: User Story 3 — Tag Browser Discoverable from Library (Priority: P2)
|
||||
|
||||
**Goal**: A visible "Browse tags" link in the library page header navigates to `/tags`. Makes the tag browser discoverable without requiring the user to type the URL.
|
||||
|
||||
**Independent Test**: Load the library page. Confirm a link to `/tags` is visible in the header and navigates correctly when clicked.
|
||||
|
||||
### Tests for User Story 3
|
||||
|
||||
- [X] T011 [US3] Write failing spec for library nav link to `/tags` in `ui/src/app/library/library.component.spec.ts` — assert a link element with `href="/tags"` is present in the rendered header
|
||||
|
||||
### Implementation for User Story 3
|
||||
|
||||
- [X] T012 [US3] Add "Browse tags" `routerLink="/tags"` link to `LibraryComponent` header in `ui/src/app/library/library.component.ts` — place alongside the existing Upload button; style consistently with the existing header button pattern
|
||||
|
||||
**Checkpoint**: All three user stories are independently functional.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Polish & Cross-Cutting Concerns
|
||||
|
||||
- [X] T013 [P] Run `ruff check api/app/ api/tests/` and fix any violations
|
||||
- [X] T014 [P] Run `ng lint` in `ui/` — zero violations required
|
||||
- [X] T015 Run `ng build` in `ui/` — zero errors required
|
||||
|
||||
---
|
||||
|
||||
## Dependencies & Execution Order
|
||||
|
||||
### Phase Dependencies
|
||||
|
||||
- **Phase 2 (Foundational)**: Blocks all user story phases — must complete first
|
||||
- **Phase 3 (US1)**: Depends on Phase 2 — TagBrowserComponent needs the sorted tag endpoint
|
||||
- **Phase 4 (US2)**: Depends on Phase 2 — library deep-link needs no API change, but should follow US1 for coherent testing
|
||||
- **Phase 5 (US3)**: Depends on Phase 3 (needs the `/tags` route to exist for the link to be meaningful)
|
||||
- **Phase 6 (Polish)**: Depends on all prior phases
|
||||
|
||||
### Within Phase 2
|
||||
|
||||
- T001 ∥ T002 (different repos, both write failing tests)
|
||||
- T003 after T001 (TDD: failing test must exist first)
|
||||
- T005 after T002 (TDD: failing test must exist first)
|
||||
- T003 ∥ T005 (different repos, after their respective tests)
|
||||
- T004 after T003 (router wraps repo)
|
||||
|
||||
### Execution Order (Phase 2)
|
||||
|
||||
```
|
||||
Step 1 (parallel): T001, T002
|
||||
Step 2 (parallel): T003 (after T001), T005 (after T002)
|
||||
Step 3: T004 (after T003)
|
||||
```
|
||||
|
||||
### Parallel Opportunities (Phases 3–5)
|
||||
|
||||
- T007 and T008 are parallel within Phase 3
|
||||
|
||||
---
|
||||
|
||||
## Implementation Strategy
|
||||
|
||||
### MVP (US1 + US2 — both P1)
|
||||
|
||||
1. Complete Phase 2 (Foundational)
|
||||
2. Complete Phase 3 (US1 — TagBrowserComponent)
|
||||
3. Complete Phase 4 (US2 — library deep-link)
|
||||
4. **Validate**: Navigate from tag browser → library → confirm pre-filtered results
|
||||
5. Phases 5–6 add discoverability and polish
|
||||
|
||||
### Incremental Delivery
|
||||
|
||||
- After Phase 3: `/tags` page is live and usable (visitors can browse tags)
|
||||
- After Phase 4: clicking a tag works end-to-end (browse → filtered library)
|
||||
- After Phase 5: tag browser is discoverable from the library without typing the URL
|
||||
- After Phase 6: lint and build clean, ready for merge
|
||||
Reference in New Issue
Block a user