Skip to content

feat: Categories Editor — browse, search, and view preset category sets (closes #143) - #147

Merged
luandro merged 68 commits into
mainfrom
agent/comapeo-cloud-app/issue-143
Jul 27, 2026
Merged

feat: Categories Editor — browse, search, and view preset category sets (closes #143)#147
luandro merged 68 commits into
mainfrom
agent/comapeo-cloud-app/issue-143

Conversation

@luandro

@luandro luandro commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Closes #143

What

Implements the Categories Editor screen at /categories — browse and search preset categories from the server.

Note: Category set import (.comapeocat upload) is not included in this PR. The dialog and data layer exist but the import flow is not connected end-to-end. Tracked in #151.

Changes

  • Valibot schemas for server wire format (preset v1) and import format (.comapeocat)
  • Data layer: useApiPresets (TanStack Query), normalizeCategories (grouping/search/locale/field labels)
  • Categories Editor screen: grid view, search, category detail panel
  • Latest/All toggle for time-clustered preset sets
  • Server routing via project sourceId (multi-archive support)
  • Full i18n (en/pt/es)
  • Responsive: mobile detail takeover, desktop split view
  • Comprehensive test suite

Import Status

The ImportSetDialog component, categories-db.ts IndexedDB layer, and comapeoCatSchema are included but not wired into the UI. Import functionality requires connecting the IndexedDB write path to the server-backed read path. See #151.

Greptile Summary

This PR adds a server-backed Categories Editor for browsing and searching preset category sets. The main changes are:

  • Category grid, search, detail panel, and latest/all filtering.
  • Server-routed preset and field queries for multi-archive projects.
  • Runtime schemas and normalization for presets, fields, and category imports.
  • Responsive layouts and translations for English, Portuguese, and Spanish.
  • Unit and end-to-end coverage for the new category workflows.

Confidence Score: 5/5

This looks safe to merge.

  • The preset response is unwrapped before category normalization.
  • Field labels now come from the server field query.
  • Field-query failures expose retry actions in both result states.
  • No blocking issues remain in the reviewed fixes.

Important Files Changed

Filename Overview
src/hooks/useApiPresets.ts Fetches server presets, unwraps the response envelope, and deduplicates preset versions.
src/hooks/useApiFields.ts Fetches fields from the owning archive and filters and deduplicates the results.
src/hooks/useCategories.ts Groups and searches categories using preset labels and resolved field labels.
src/screens/CategoriesEditor/index.tsx Coordinates category queries, search, filtering, detail rendering, and query recovery states.
src/lib/fields/normalize.ts Normalizes server field formats and builds field lookups for category details and search.

Reviews (46): Last reviewed commit: "refactor: comment out Import Category Se..." | Re-trigger Greptile

Context used:

  • Context used - CLAUDE.md (source)
  • Context used - AGENTS.md (source)

Terrastories added 4 commits July 22, 2026 14:50
…egory format

- Add import-file schemas: metadataSchema, fieldSchema, categorySchema, comapeoCatSchema
- Vendor 5 representative category + 10 field fixtures from comapeo-default-categories v1.1.2
- Add loadDefaultCategoryFixtures() fixture loader
- Export new schemas from barrel index
- 54 tests covering valid/malformed/edge cases for all schemas
- usePresets: TanStack Query hook fetching presets from server API with Valibot validation
- normalizeCategories: pure function for grouping by tags.type, locale-aware search, deterministic ordering
- 17 tests covering success/error/validation/locale/grouping/search/edge cases
- Register /categories and /categories/ routes
- Add Categories nav item with grid icon to primary rail
- Create screen shell with loading/error/empty/search states
- Add en/pt/es i18n messages for all strings
- 4 screen tests covering loading/error/empty/search states
- CategoryCard: colored icon fallback, color accent strip, field count, semantic article
- CategoryGrid: section headers by tags.type, responsive 2-4 column grid
- Extended Category model with color/iconRef pass-through
- 8 new tests covering group headings, cards, accents, accessibility, empty state
Comment thread src/screens/CategoriesEditor/index.tsx
Comment thread src/hooks/useCategories.ts
Terrastories added 7 commits July 22, 2026 15:28
…elect unwrap

- Add iconRef to PresetInput interface in useCategories
- Remove unused Card import from CategoryCard
- Fix duplicate useNavigate mock in test
- Add select: (data) => data.data to usePresets for backward compat
- Fix test assertions for unwrapped data shape
…refs

- Remove label from PresetInput.fieldRefs (API returns {docId,versionId,url} only)
- Add optional fieldLabels: Map<string,string> param to normalizeCategories
- Resolve field labels in search and output from the field lookup map
- Add test for field label resolution
- FieldViewer: read-only previews for text/selectOne/selectMultiple/date/number/unknown field types
- CategoryDetail: icon, color swatch, field refs with resolved labels, back nav
- Integrated useFields hook for field label resolution
- Responsive layout: grid + sticky sidebar on desktop, full-width on mobile
- CategoryCard: selection state with aria-pressed and keyboard support
- 20 new tests (10 FieldViewer, 10 CategoryDetail), 31 total CategoriesEditor tests
- categories-db: Dexie DB with versioned schema, importCategorySet/getCategorySet/getCategorySets
- useCategorySets: hook for listing imported category sets
- ImportSetDialog: file upload, JSON parsing, Valibot validation, replace confirmation
- 16 tests covering valid/malformed/replace/retrieve/dialog lifecycle
- Revert usePresets to use getPresets (data-layer) for backward compat
- Create useApiPresets with apiClient + select unwrap for CategoriesEditor
- Fix ESLint: remove unused Category import, move useMemo before early returns
- Fix TypeScript: resolve consumer hooks' Preset type mismatch
- Update test mocks for new hook structure
@luandro
luandro force-pushed the agent/comapeo-cloud-app/issue-143 branch from 7f3506b to 312003e Compare July 22, 2026 19:26
@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Preview deployment ready: https://agent-comapeo-cloud-app-issu-62kr.comapeo-cloud-app.pages.dev

Commit: 1724eb2

… a11y

- useCategorySets: add error state + .catch on toArray() and refresh
- ImportSetDialog: useEffect cleanup for auto-close timer, try/catch on replace
- categories-db: remove dead getCategoriesDb singleton
- CategoriesEditor: add aria-label to search input
Terrastories added 7 commits July 22, 2026 16:58
- CategoriesEditor: show 'Select a project' message when no project selected (stops eternal skeleton)
- FieldViewer: fix select_one/select_multiple → selectOne/selectMultiple to match Valibot enum
- ImportSetDialog: wrap first-time import in try/catch, add failedToRead/importError i18n messages
- CategoryCard: use intl.formatMessage for field count (was hardcoded English)
- FieldViewer: use intl.formatMessage for Date/Number/Unknown type badges (was hardcoded)
- index.tsx: add selectProject message definition
- Add categories.* i18n keys to en/es/pt: fieldCount, fieldType.*, importSet.{failedToRead,importError}, selectProject, untitledProject
The presetRefSchema requires {docId, versionId, url} but the CoMapeo
preset schema v1 defines ref items as {docId, versionId} only — no url.
The server omits url from fieldRefs and iconRef in preset responses,
causing v.parse() to throw → preset query isError → 'Failed to load
categories' screen.

Switch to docRefSchema (which already exists for this exact reason —
the comment says 'a single missing field would previously have silently
dropped ALL tracks for the project') with optional versionId and url.
Verify that presetSchema accepts fieldRefs and iconRef without a 'url'
field — the comapeo-schema preset v1 defines ref items as {docId, versionId}
only. The server omits 'url' from these, and the strict presetRefSchema was
the root cause of the 'Failed to load categories' runtime error.
- i18n: 'Uncategorized' group header (useCategories sentinel '' + CategoryGrid translate)
- ImportSetDialog: wrap getCategorySet conflict-check in try/catch
- ImportSetDialog: replace-flow uses importError i18n key (was hardcoded English with wrong validationError key)
- Static import getCategorySet (drop redundant dynamic import)
- CategoriesEditor index: test happy path (cards render), no-project state, empty state
- useCategories: test real diacritics (Água, São Paulo, Ação) against unaccented queries
- useCategories: test accented query against plain target name
@luandro luandro changed the title feat: Categories Editor screen — schema, data layer, route, card grid feat: Categories Editor — browse, search, and import .comapeocat category sets (closes #143) Jul 22, 2026
…, iconRef/color, replace flow, useCategorySets

Addresses all 4 MEDIUM findings from claude-qwen test-coverage review:
- CategoryCard: click/keyboard selection + aria-pressed tests (10 tests)
- useCategories: iconRef & color branch coverage (4 tests)
- ImportSetDialog: replace-execution + file-read-failure paths (2 tests)
- useCategorySets: new hook test file (4 tests — empty, populated, error, refresh)
…ML semantics, serverConfig hygiene

- Add missing pt/es translations for new field-type messages; remove stale
  uncategorized/fieldCount/fieldType.unknown keys from pt/es
- Gate resolvedFields on fieldsQuery.isPending to avoid 'Field unavailable'
  flash during initial fields load
- Replace invalid <h3> inside <button> with <span> (valid content model)
- Stop threading full {baseUrl, token} through UI props; CategoryIcon and
  the chain now take serverBaseUrl (string | null) only
- Add tests: unresolved fieldRef → 'Field unavailable' (no raw docId),
  fields-pending hold, empty geometry:[] exclusion
…onse

- ObservationCategoryIcon: add m-auto to the icon <img> and fallback
  letter so the SVG is centered inside its circle. PR #147 switched sizing
  to fixed width/height inside an absolute inset-0 box, which anchored the
  icon to the top-left corner, breaking horizontal/vertical centering.
- fieldSchema: a real archive /field response can omit optional keys such
  as "universal" (and may carry extra keys like "geometry"). The previous
  strict v.object rejected those fields, throwing on parse and making the
  entire fields fetch fail to load. Make "universal" a tolerant
  v.fallback(boolean, false) so a missing key defaults to false while the
  output type stays boolean and downstream consumers are unaffected.

RED to GREEN via tests: field.test.ts (real server-shaped field) and
ObservationCategoryIcon centering test.
@luandro

luandro commented Jul 26, 2026

Copy link
Copy Markdown
Contributor Author

PR Readiness Worker — Run 2026-07-26T20:03:21Z

  • Actions taken: Medium-model review failed (infrastructure error)
  • Remaining blockers: - Medium review failed: all providers exhausted
  • Review tier completed: medium
  • Reviewer confidence: low
  • Next recommended step: Will retry on next cycle

@luandro luandro added agent:pr-readiness-in-progress PR lifecycle label and removed agent:pr-readiness-in-progress PR lifecycle label labels Jul 26, 2026
@luandro

luandro commented Jul 26, 2026

Copy link
Copy Markdown
Contributor Author

Latest fix: TDD for two field/icon regressions (commit dee74fa)

Followed the mandatory TDD flow (RED → GREEN → verify) for two regressions reported against the Categories screen:

1. Fields failed to load (threw on parse)

  • Root cause: fieldSchema (src/lib/schemas/field.ts) used a strict v.object with universal: v.boolean() required. Real archive /field responses omit optional keys like universal (and may carry extra keys like geometry). valibot rejected the missing key, so the entire fields fetch threw and the detail panel showed nothing.
  • Fix: universal: v.fallback(v.boolean(), false) — a missing key defaults to false and the output type stays boolean (no downstream breakage). v.object already strips unknown extra keys.
  • Test added: tests/unit/lib/schemas/field.test.ts asserts a real server-shaped field (omits universal, carries geometry) parses.

2. Icons not centered in their circle

  • Root cause: PR feat: Categories Editor — browse, search, and view preset category sets (closes #143) #147 switched icon sizing from h-full w-full (fills circle, naturally centered) to a fixed width/height + absolute inset-0 box, which anchors the SVG to the top-left corner; flex centering does not apply to an absolutely-positioned child.
  • Fix: added m-auto to the <img> (and fallback letter) in ObservationCategoryIcon.
  • Test added: centering assertion in tests/unit/components/shared/ObservationCategoryIcon.test.tsx (also fixed the test mock so it forwards className/style).

Greptile P1 review status (verified against current code, not just commit claims):

  • Preset Envelope Passed As Array — FIXED (select: data => deduplicatePresetVersions(data.data) in useApiPresets)
  • Field Labels Are Never Available — FIXED (fieldLabels map built from fields lookup, passed to normalizeCategories)
  • Field Query Errors Hide Labels / Field Failure Hides Retry — FIXED (banner + retry in results and no-results states)
  • Live Presets Use Stale Fields — ACKNOWLEDGED out-of-scope. Premise is also stale: useApiFields now fetches directly from the archive server API (not local Dexie), so field labels resolve from the live server response.

Checks: tsc clean, ESLint clean, Prettier clean, check:i18n clean, pre-push full validation passed, and remote CI (unit-tests/coverage, lint, typecheck, visual-regression, build-e2e) is green on this commit.

Note: a couple of unrelated unit-test files (MapScreen) intermittently fail only under the full parallel suite locally (jsdom pointer-events: none flake) — they pass 15/15 in isolation and CI passes them. Not caused by this change.

@luandro luandro added agent:pr-readiness-in-progress PR lifecycle label agent:pr-blocked PR lifecycle label and removed agent:pr-readiness-in-progress PR lifecycle label labels Jul 26, 2026
Real Mapeo archive server returns `tagKey` (not `key`) and camelCase
field types (`selectOne`/`selectMultiple`). The previous fieldSchema required
`key` + snake_case, so v.parse threw on every /field response in prod,
getFields rejected, useApiFields errored, and CategoriesEditor showed
"Field labels unavailable".

- schemas/field.ts: fieldSchema accepts tagKey and key (both optional),
  accept selectOne/selectMultiple alongside select_one/select_multiple
- fields/normalize.ts: API_TYPE_MAP maps camelCase types; normalizeApiField
  reads tagKey ?? key ?? ''
- remote-archive.ts pullFields: map archive tagKey into Dexie `key` column
  (no migration)
- fixtures: real-archive-response.json (4-field capture incl. 1 deleted);
  presets fieldsFixture key -> tagKey
- tests: field/normalize/useApiFields unit coverage + field-resolution proof
  test (real MSW /field response: labels render, no error banner, deleted
  field excluded)

Reviewed by Opus 5 (Round 2): APPROVED, no pre-merge blockers. Closes #143.
Gap A — Flaky LoginScreen test ("window is not defined" unhandled rejection):
  guard setSubmitState behind a mountedRef so async handleSubmit cannot
  schedule a React update after unmount/teardown.

Gap B — fieldSchema required createdAt/updatedAt; now v.fallback(v.string(),'')
  to match remote-archive.ts pullFields (which already writes ?? '').
  Removes the dead/tolerant mismatch; output type stays string.

Gap C — buildFieldLookup now skips deleted fields by default
  (includeDeleted opt-in); adds regression tests in normalize.test.ts.

Gap D — document that Dexie Field.type is stored un-normalized camelCase.

Verified: tsc --noEmit clean, prettier clean, 57 unit tests pass
(incl. field-resolution proof + new gap tests). screenshots/ left untouched.
1. ObservationCategoryIcon size regression: inline style overrides
   Tailwind classes. Two pre-existing callers (DataScreen h-12 w-12,
   ObservationsMap h-7 w-7) rendered at default 32px. Changed to
   pass explicit size={48} / size={28}. Added regression test.

2. Wire ImportSetDialog into CategoriesEditor toolbar. The dialog
   was fully implemented + tested but unreachable. Added Import
   button, isImportOpen state, i18n key (en/pt/es). Added tests
   for button render + dialog open on click.

Addresses Opus 5 final readiness gate findings (PR #147).
…buttons

- Replace dead hover:bg-primary-dark (undefined CSS var) with hover:bg-primary-hover on all 3 buttons
- Add min-h-[44px] to Import button for mobile touch-target compliance
@luandro luandro changed the title feat: Categories Editor — browse, search, and import .comapeocat category sets (closes #143) feat: Categories Editor — browse, search, and view preset category sets (closes #143) Jul 27, 2026
The import flow writes to IndexedDB but the screen reads from the
server API — the two data sources are disconnected. Commenting out
the UI until the full flow is implemented.

Tracked in #151.

- Comment out Import button + ImportSetDialog render in CategoriesEditorScreen
- Comment out 2 import dialog wiring tests
- Update PR title/description to remove 'import' from scope
@luandro
luandro merged commit 5fb7c93 into main Jul 27, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent:pr-blocked PR lifecycle label agent:pr-needs-review PR lifecycle label

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: implement Categories Editor screen (/categories) with mobile-app-style preset browser

1 participant