Skip to content

feat: Categories Editor — browse, search, and import .comapeocat category sets (closes #143)#147

Open
luandro wants to merge 26 commits into
mainfrom
agent/comapeo-cloud-app/issue-143
Open

feat: Categories Editor — browse, search, and import .comapeocat category sets (closes #143)#147
luandro wants to merge 26 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 — Phase 1-5 complete.

Changes (21 commits, 44 files, +3229/-68 lines)

Core Implementation

  • Valibot schemas for server wire format (preset v1) and import format (.comapeocat)
  • 15 fixtures from comapeo-default-categories v1.1.2
  • Data layer: useApiPresets (TanStack Query), normalizeCategories (grouping/search/locale/field labels)
  • Route + nav + i18n: /categories route, grid icon nav, full en/pt/es coverage
  • CategoryCard + CategoryGrid: grouped by tags.type, alphabetical ordering, diacritic-insensitive search
  • CategoryDetail + FieldViewer: icon, color swatch, field refs with resolved labels, type badges
  • .comapeocat import: Dexie persistence, conflict detection, replace confirmation, ARIA live regions
  • All states handled: loading (skeleton), empty, error+retry, no-project, no-search-results

Review Findings Addressed (this session)

  • CRITICAL: presetRefSchema → docRefSchema — server returns fieldRefs without url per comapeo-schema v1; strict schema caused 'Failed to load categories'
  • HIGH: i18n 'Uncategorized' header (sentinel + translate), conflict-check try/catch, replace-flow wrong error key
  • HIGH: screen success path + real diacritic tests (Água, São Paulo, Ação)
  • 3× claude-qwen full-branch reviews (schemas, components, test coverage) — all findings fixed

Test Results

  • 171 test files, 2158+ tests — all passing
  • CategoriesEditor: 58 tests across 6 files (loading, empty, error, success, no-project, diacritic)
  • Schema tests: preset wire format, import format, upstream fixture validation
  • Full CI: lint ✓ types ✓ coverage ✓ i18n ✓ build ✓

Greptile Summary

This PR adds a Categories Editor for browsing, searching, viewing, and importing category sets. The main changes are:

  • Adds category list and detail routes with navigation.
  • Loads presets and field labels from project data.
  • Adds localized grouping and diacritic-insensitive search.
  • Adds .comapeocat validation, conflict handling, and Dexie persistence.
  • Adds loading, empty, error, and no-project states.

Confidence Score: 5/5

This looks safe to merge.

The preset response is unwrapped before category normalization, and field labels are resolved through the project field data.

No blocking issues were found in the updated code.

T-Rex T-Rex Logs

What T-Rex did

  • The baseline Categories route was captured to establish a reference state before browsing content.
  • The after-capture showed grouped categories Atividades, Lugares, and Recursos naturais containing Ação comunitária, São Paulo, and Água.
  • Searches without diacritics were validated to map to the accented categories, and selecting Água exposed its detail view.
  • Selecting Água produced aria-pressed=true and displayed detail content including Qualidade da água and the color reference #1677FF.
  • Artifacts including video, images, and logs were reviewed to corroborate the described states and interactions.

View all artifacts

T-Rex Ran code and verified through T-Rex

Important Files Changed

Filename Overview
src/hooks/useApiPresets.ts Loads server presets and unwraps the validated response envelope.
src/hooks/useCategories.ts Groups and filters categories while resolving field labels.
src/screens/CategoriesEditor/index.tsx Connects project presets and fields to the category editor states.
src/lib/schemas/preset.ts Adds import schemas and accepts the server's document-reference shape.
src/screens/CategoriesEditor/ImportSetDialog.tsx Adds category-set file import and replacement confirmation.

Reviews (12): Last reviewed commit: "fix(issue-143): remove unsafe 304→{data:..." | Re-trigger Greptile

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: 67f8a6a

… 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)
… localId (hex)

The comapeo-cloud server's /projects/:projectPublicId/preset route validates
the projectPublicId against a base32 regex. useApiPresets was passing the
project's local DB id (hex UUID), causing the server to return an error for
remote archive projects. pullPresets already uses the correct remoteId.

- useApiPresets: renamed parameter projectLocalId→projectRemoteId
- CategoriesEditor: resolves selectedProject.remoteId from projects query
- Local projects without remoteId show empty state (not eternal skeleton)
…in tests

HIGH issues from claude-qwen review:
1. When projectsQuery was pending/error, the !selectedProject?.remoteId guard returned
   empty state instead of loading skeleton or error+retry. Fixed by adding explicit
   projectsQuery.isPending / isError / !selectedProject guards before the remoteId check.
2. Tests mocked useApiPresets but never verified the argument — reverting to localId
   would leave every test green. Added assertions for useApiPresets('base32proj1')
   and useApiPresets(null), plus guard tests for pending/error/not-found.
…cache

The comapeo-cloud server returns 304 Not Modified (ETag cache hit) for
/preset and /field endpoints. The browser's fetch receives the raw 304
without a cached body, and handleResponse treats !response.ok (304 ∉ 200-299)
as an error → ApiError → 'Failed to load categories'.

Two-pronged fix:
1. handleResponse: handle 304 before the !response.ok check, return
   { data: [] } — safe for all list-envelope schemas.
2. getPresets/getFields: add cache: 'no-store' to fetch calls to prevent
   the browser from sending If-None-Match/If-Modified-Since headers.

Tests: 6 new api-client tests covering 304, 404, 500, and success paths.
… all reads

claude-qwen review: the { data: [] } as T cast in handleResponse was
unsafe for non-list endpoints (getProject, getServerInfo) — a 304 would
resolve to wrongly-shaped data, corrupting e.g. pullProjects.

Fix: remove the 304 fallback entirely. Add cache: 'no-store' to ALL
read endpoints (getServerInfo, getProjects, getProject, getObservations,
getTracks, getAlerts, getPresets, getFields). With no-store, the browser
never sends If-None-Match/If-Modified-Since, so 304 is unreachable.

Tests: added getProject 304 regression test asserting it throws (never
resolves to {data:[]}), plus getPresets 304 throw test.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

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