Files
scalesApp/.claude/plans/reorder-nomenclatures-drag-drop.md
gitea fe9fb71860 Add implementation plans and remove stray Word temp file
Plans record the decisions behind each feature and, for the protocol and
licensing work, what remains unvalidated against real hardware.

~WRL0003.tmp was a Word autosave artifact committed by accident.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 18:58:38 +03:00

116 lines
6.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.