# Plan: Drag-to-reorder nomenclatures, reflected in vehicle "Additional Data" ## User Request > in nomenclatures overlay, make the rows in the list draggable, so they can be rearranged. then in vehicle, show the fields in this order Clarified with screenshots: the target is the **left-hand list in the Nomenclatures overlay** (Агент, Товар, Товародател, № Контейнер, Корабособственик). The client wants to be able to move e.g. "Агент" to the bottom of the list, and the vehicle screen's "Additional Data" dropdowns should then appear in that same order. ## Plan ### Context The backend already supports this fully: `Nomenclature.sort_order` exists (`backend/nomenclatures/models.py:29`), `Meta.ordering = ["sort_order", "code"]` (`models.py:32`), the viewset orders by it (`backend/api/views.py:260`), the serializer exposes it as writable (`backend/api/serializers.py:264`), and `Main.jsx:31` already sorts `vehicleNomenclatures` by it. **No backend change and no migration are needed.** The reason the order looks fixed today is that every existing row has `sort_order = 0` — the model default — so the sort is a no-op and rows fall back to `code` order. Assigning real values on the first drag fixes that permanently. Decisions taken with the user: - Persist by PATCHing each moved row (frontend-only; no new endpoint). - Save immediately on drop — no Save-button staging. - The whole row is draggable (no separate grip handle). ### Scope Two files change. Both are frontend. #### 1. `frontend/src/components/NomenclatureManager/NomenclatureManager.jsx` Add native HTML5 drag-and-drop to the left list (lines 189–217). No library — `package.json` has no DnD dependency and adding one for a five-row list is not warranted. **State:** two new pieces of local state — `dragIndex` (index of the row being dragged, `null` when idle) and `dragOverIndex` (index the row would drop into, for the drop-line indicator). Plus reuse of the existing `error` state for failure messages. **Row markup** (`.nm-list-item`, line 191): add - `draggable={canEdit}` — reordering is an edit, so gate it on the same `canEdit` (`currentUser?.can_manage_entities`) that already gates the edit/add/delete buttons. - `onDragStart` → record `dragIndex`; call `e.dataTransfer.setData('text/plain', String(idx))` (Firefox refuses to start a drag without data set) and `e.dataTransfer.effectAllowed = 'move'`. - `onDragOver` → `e.preventDefault()` (required or no drop event fires) and set `dragOverIndex`. - `onDragEnd` → clear both indices (fires even on a cancelled drag, so it is the reliable reset). - `onDrop` → `e.preventDefault()`, then run the reorder below. - `className` gains conditional `nm-list-item--dragging` / `nm-list-item--drag-over` modifiers. Change the `.map(nom => ...)` at line 190 to `.map((nom, idx) => ...)` so handlers have the index. The React `key={nom.id}` at line 192 is already stable and correct — leave it. **Reorder handler** `handleDrop(targetIdx)`: 1. Bail if `dragIndex === null || dragIndex === targetIdx`. 2. Build the reordered array with a splice-out/splice-in on a copy of `nomenclatures`. 3. Write `sort_order: i` onto every element of the new array, and `setNomenclatures(...)` immediately — optimistic update, so the list does not visibly snap back while the requests are in flight. 4. Diff against the previous array and PATCH only rows whose `sort_order` actually changed: `api.patch('/api/nomenclatures/${id}/', { sort_order: i })`, all in one `Promise.all`. PATCH (not PUT) matters — `NomenclatureSerializer.update` deletes and recreates the whole `fields` collection (`backend/api/serializers.py:284-287`), so a PUT without a `fields` key would wipe the field definitions. A partial update leaves `fields` untouched. 5. On rejection: `setError('Failed to save order')` and `await loadNomenclatures()` to resync from the server. Note the first drag will PATCH nearly every row, because they all currently sit at `sort_order = 0` and the diff sees every position as changed. That is correct and self-healing — subsequent drags only touch the rows between the source and target. **One caveat to handle:** each PATCH triggers `sse_broadcast_update('nomenclature', 'update', ...)` (`backend/api/views.py:268`), and `NomenclatureDataContext.jsx:82-85` responds to any `nomenclature` event with a full `loadAll()` refetch. For a 5-row reorder that is ~5 events collapsing into a few refetches of a small payload — acceptable, and it is what makes the vehicle screen update live without a page reload. No debounce needed at this scale; revisit only if the list ever grows large. #### 2. `frontend/src/components/NomenclatureManager/NomenclatureManager.css` - `.nm-list-item` (line 117) — the existing rule already has `cursor: pointer`; leave it. Add `user-select: none` so dragging does not paint a text selection across the row. - `.nm-list-item--dragging` — `opacity: 0.4`, to show which row is in flight. - `.nm-list-item--drag-over` — a drop indicator in the accent already used by `.nm-list-item--active` (line 132). Because `.nm-list-item` has a `border-bottom` (line 122), use an inset `box-shadow: inset 0 2px 0 #667eea` rather than a border so row height does not shift by 2px during the drag. ### Explicitly not in scope - `NomenclatureField` rows (the "Fields" table on the right panel) — they have their own `sort_order` that the save payload currently drops, but the user asked about the left list only. Flagged to the user as a separate known gap, not fixed here. - `NomenclatureEntry` ordering — entries have no `sort_order` column at all; that would need a migration. Not requested. - `Main.jsx` — **no change required.** Line 31 already sorts by `sort_order`. Left as is. ### Verification 1. Start backend (`cd backend && uvicorn scalesapp.asgi:application --reload`) and frontend (`cd frontend && npm start`); log in as a user with `can_manage_entities`. 2. Open the Nomenclatures overlay. Drag **Агент** from the top to the bottom of the left list. Verify: the row follows the cursor with a drop indicator, and lands last on release. 3. Check the Network tab: several `PATCH /api/nomenclatures/{id}/` requests, each with a `{sort_order: N}` body only, all returning 200. 4. Without reloading, look at the vehicle screen's "Additional Data" section — **АГЕНТ should now be the last dropdown**, moved there live by the SSE-driven `loadAll()`. 5. Hard-reload the page (F5) and confirm both the overlay list and Additional Data still show Агент last — this proves it persisted rather than only living in local state. 6. Re-open the overlay, click a nomenclature to edit it, and confirm its **Fields table still lists its fields** — this is the PUT-vs-PATCH regression guard from step 4 of the handler. 7. Log in as a user without `can_manage_entities` and confirm rows are not draggable.