Skip to content

feat(api): let maintainers reorder subcategories within a category - #607

Open
mrivas00 wants to merge 5 commits into
feat/mati/scope3-ghg-orderfrom
feat/mati/subcategory-reorder-maintainer
Open

feat(api): let maintainers reorder subcategories within a category#607
mrivas00 wants to merge 5 commits into
feat/mati/scope3-ghg-orderfrom
feat/mati/subcategory-reorder-maintainer

Conversation

@mrivas00

Copy link
Copy Markdown
Collaborator

Chained PR — targets feat/mati/scope3-ghg-order (PR 606), which introduces subcategory.position. Review that one first; merge order matters.

What & why

  • PR 606 gives Scope 3 a normative order but no way to maintain it: countries edit their own methodology, and a subcategory created from the maintainer lands last with no way to move it. The first insert a country makes degrades the GHG Protocol order.
  • Adds up/down arrows in the subcategories maintainer, the same interaction categories already have.

Approach

Mirrors the existing POST /categories/swap-positions end to end (endpoint, optimistic row swap, ActionButtons arrows) instead of introducing a new reordering idiom — drag-and-drop or an editable position cell would both need their own uniqueness handling, while the swap keeps positions contiguous by construction.

Key changes & decisions

1. POST /subcategories/swap-positions

features/subcategories/swapSubcategoryPositions/*, errors.ts, routes/api/subcategories/index.ts · a30ed3be

  • Trigger — no write path could change a position; the column was seed-only.
  • Change — swaps two positions inside one transaction via a temporary position (max + 1), refusing equal IDs (422 SAME_SUBCATEGORY), missing/deleted rows (404) and subcategories from different categories (422 SUBCATEGORIES_FROM_DIFFERENT_CATEGORIES).
  • Why this way — the temp-position hop is required: (category_id, position) is uniquely indexed, so a direct two-row update would violate it mid-swap. Cross-category swaps are rejected rather than supported because a position only means something within its category; moving between categories already has a path (PATCH /subcategories/:id with categoryId, which appends at the destination).

2. position exposed on subcategory responses

baseSchemas/subcategory.ts, getAllSubcategories, createSubcategory, updateSubcategory schemas + services · a30ed3be

  • Trigger — the UI needs the current positions to know which row is first/last in its category and to apply the optimistic swap.
  • Change — added position to the base schema and to the three response shapes; the swap response returns only { id, categoryId, name, position }.
  • Why this way — returning the full SubcategoryBaseSchema from the swap (as the category endpoint does) would mean writing a subcategory mapper with no other caller, for fields the client already has. SubcategoryFormSchema allows position: 0 for grid rows not yet persisted, since the real position is only known once the server appends the row.

3. Arrows constrained to the row's own category

SubcategoriesMaintainerScreen.tsx, useSubcategoryColumns.tsx, useSubcategories.ts (+2) · d2f4976e

  • Trigger — unlike the categories grid, this grid lists the subcategories of every category at once, so "first row" and "last row" of the grid are the wrong boundaries.
  • ChangehandleMove picks the neighbour among same-category siblings sorted by position; the arrows are disabled on the first/last row of each category group.
  • Why this way — the boundary has to match the endpoint's constraint (same category), otherwise the only feedback for an impossible move would be a 422 snackbar. The optimistic update swaps the two rows in place instead of re-sorting the grid, so a pending new row keeps the top slot handleAddRow prepended it to.

Test plan

  • pnpm format:check, pnpm lint, pnpm type-check
  • pnpm test:api -- /subcategories --coverage=false — 48 tests, includes the new swapSubcategoryPositions file: swap + persistence, GET order after swap, siblings untouched, non-adjacent swap, 404 missing, 404 deleted, 422 same id, 422 cross-category
  • pnpm test:api -- /methodologies --coverage=false (81) — response shapes with the new field
  • pnpm test:web (39 files / 723 tests, coverage floor met)
  • Reviewer, maintainer → Sub-categorías: move a middle row up → the row swaps with the one above, order survives a reload; the ↑ arrow is disabled on the first row of each category and ↓ on the last; arrows are disabled while another row is being edited and on an unsaved new row.
  • Reviewer: with a row in edit mode, confirm no arrow is clickable (avoids a swap racing an in-flight save).

Deliberate deviations

  • No automated coverage for the move handler itself — the web suite has no test for this screen and adding one would mean building the grid harness. The endpoint it calls is covered; the UI boundary logic is verified manually (checklist above). Called out rather than implied.
  • Not smoke-tested in a running app yet: local Docker was down for part of this work, and the app containers run built images, so the arrows were exercised through types/lint/tests only. Worth a click-through before merge.

Risk / blast radius

  • Additive API surface (one endpoint) plus one new field on three existing response shapes; no consumer breaks.
  • Reordering writes to position only, inside a transaction; no effect on inventory lines or factors.

@mrivas00 mrivas00 added type: feature new user-facing capability or enhancement area: web apps/web (React frontend) area: api apps/api (Fastify backend) area: packages shared packages/* priority: high High priority labels Aug 19, 2026
@mrivas00
mrivas00 force-pushed the feat/mati/scope3-ghg-order branch from 309e196 to 5d2bfd5 Compare August 24, 2026 14:35
@mrivas00
mrivas00 force-pushed the feat/mati/subcategory-reorder-maintainer branch 2 times, most recently from 4d06751 to a2b963a Compare August 24, 2026 14:40
@mrivas00
mrivas00 force-pushed the feat/mati/scope3-ghg-order branch from 5d2bfd5 to f0aca17 Compare August 25, 2026 13:31
Subcategories were ordered alphabetically, which cannot express the GHG
Protocol numbering that the Scope 3 category must follow. Ordering moves
to a position column with a CHECK and a partial unique index per
category, mirroring how category already works.

Existing rows are backfilled with the alphabetical order they were
already displayed in, so the migration on its own changes nothing
visible: the new ordering ships as seed data.
Every subcategory read (methodology tree, methodology export, emission
capture, maintainer list) now sorts by position instead of name, so the
order defined by the methodology data is what users see.

Positions are never supplied by the client: a new subcategory is
appended last inside its category, and one moved to another category is
appended last in its destination — keeping its old position could
collide with a subcategory already sitting there. Duplicating a
methodology carries positions over.
The 11 subcategories of the Scope 3 category now follow the GHG Protocol
Scope 3 category numbering (purchased goods, water, upstream transport,
waste, business travel, commuting, downstream transport, use of sold
products, other sources last) instead of being listed alphabetically,
and each description opens with the GHG Protocol and ISO 14064-1
category it maps to, so the equivalence is visible where the user picks
the subcategory.

Scope 1 and 2 keep the order already authored in the data file, which
reads better than alphabetical: "Emisiones provenientes de otras
fuentes" now closes the list instead of sitting in the middle.
@mrivas00
mrivas00 force-pushed the feat/mati/subcategory-reorder-maintainer branch from a2b963a to 4b4f350 Compare August 25, 2026 19:46
Countries edit their own methodology from the maintainer, so a
subcategory added there landed last with no way to move it — the GHG
Protocol order of the Scope 3 category degraded with the first insert.

POST /subcategories/swap-positions mirrors the category endpoint: it
swaps two positions atomically through a temporary position, and refuses
subcategories from different categories, since positions are only unique
(and only meaningful) within a category.

Subcategory responses now carry `position` so the caller can order and
optimistically reorder its rows. The swap response returns just the
identifying fields plus the new positions instead of the full base
schema — the full shape is already served by GET /subcategories, and
building it here would mean a mapper with no other caller.
Up/down arrows in the subcategories maintainer, same interaction as the
categories screen. Moves are constrained to the row's own category — the
arrows are disabled on the first and last row of each category group,
not of the grid — because positions are unique per category and the grid
lists every category at once.

The optimistic update swaps the two rows in place rather than re-sorting
the whole grid, so a pending new row keeps the top slot it was prepended
to.
@mrivas00
mrivas00 force-pushed the feat/mati/scope3-ghg-order branch from f0aca17 to 557de74 Compare August 25, 2026 19:57
@mrivas00
mrivas00 force-pushed the feat/mati/subcategory-reorder-maintainer branch from 4b4f350 to f35d752 Compare August 25, 2026 19:57
@mrivas00
mrivas00 force-pushed the feat/mati/scope3-ghg-order branch from 557de74 to 0c58625 Compare August 25, 2026 21:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: api apps/api (Fastify backend) area: packages shared packages/* area: web apps/web (React frontend) priority: high High priority type: feature new user-facing capability or enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant