feat(api): let maintainers reorder subcategories within a category - #607
Open
mrivas00 wants to merge 5 commits into
Open
feat(api): let maintainers reorder subcategories within a category#607mrivas00 wants to merge 5 commits into
mrivas00 wants to merge 5 commits into
Conversation
mrivas00
force-pushed
the
feat/mati/scope3-ghg-order
branch
from
August 24, 2026 14:35
309e196 to
5d2bfd5
Compare
mrivas00
force-pushed
the
feat/mati/subcategory-reorder-maintainer
branch
2 times, most recently
from
August 24, 2026 14:40
4d06751 to
a2b963a
Compare
mrivas00
force-pushed
the
feat/mati/scope3-ghg-order
branch
from
August 25, 2026 13:31
5d2bfd5 to
f0aca17
Compare
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
force-pushed
the
feat/mati/subcategory-reorder-maintainer
branch
from
August 25, 2026 19:46
a2b963a to
4b4f350
Compare
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
force-pushed
the
feat/mati/scope3-ghg-order
branch
from
August 25, 2026 19:57
f0aca17 to
557de74
Compare
mrivas00
force-pushed
the
feat/mati/subcategory-reorder-maintainer
branch
from
August 25, 2026 19:57
4b4f350 to
f35d752
Compare
mrivas00
force-pushed
the
feat/mati/scope3-ghg-order
branch
from
August 25, 2026 21:20
557de74 to
0c58625
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Approach
Mirrors the existing
POST /categories/swap-positionsend to end (endpoint, optimistic row swap,ActionButtonsarrows) 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-positionsfeatures/subcategories/swapSubcategoryPositions/*,errors.ts,routes/api/subcategories/index.ts·a30ed3beSAME_SUBCATEGORY), missing/deleted rows (404) and subcategories from different categories (422SUBCATEGORIES_FROM_DIFFERENT_CATEGORIES).(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/:idwithcategoryId, which appends at the destination).2.
positionexposed on subcategory responsesbaseSchemas/subcategory.ts,getAllSubcategories,createSubcategory,updateSubcategoryschemas + services ·a30ed3bepositionto the base schema and to the three response shapes; the swap response returns only{ id, categoryId, name, position }.SubcategoryBaseSchemafrom the swap (as the category endpoint does) would mean writing a subcategory mapper with no other caller, for fields the client already has.SubcategoryFormSchemaallowsposition: 0for 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) ·d2f4976ehandleMovepicks the neighbour among same-category siblings sorted by position; the arrows are disabled on the first/last row of each category group.handleAddRowprepended it to.Test plan
pnpm format:check,pnpm lint,pnpm type-checkpnpm test:api -- /subcategories --coverage=false— 48 tests, includes the newswapSubcategoryPositionsfile: swap + persistence, GET order after swap, siblings untouched, non-adjacent swap, 404 missing, 404 deleted, 422 same id, 422 cross-categorypnpm test:api -- /methodologies --coverage=false(81) — response shapes with the new fieldpnpm test:web(39 files / 723 tests, coverage floor met)Deliberate deviations
Risk / blast radius
positiononly, inside a transaction; no effect on inventory lines or factors.