fe9fb71860
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>
116 lines
6.9 KiB
Markdown
116 lines
6.9 KiB
Markdown
# 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.
|