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>
6.9 KiB
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 samecanEdit(currentUser?.can_manage_entities) that already gates the edit/add/delete buttons.onDragStart→ recorddragIndex; calle.dataTransfer.setData('text/plain', String(idx))(Firefox refuses to start a drag without data set) ande.dataTransfer.effectAllowed = 'move'.onDragOver→e.preventDefault()(required or no drop event fires) and setdragOverIndex.onDragEnd→ clear both indices (fires even on a cancelled drag, so it is the reliable reset).onDrop→e.preventDefault(), then run the reorder below.classNamegains conditionalnm-list-item--dragging/nm-list-item--drag-overmodifiers.
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):
- Bail if
dragIndex === null || dragIndex === targetIdx. - Build the reordered array with a splice-out/splice-in on a copy of
nomenclatures. - Write
sort_order: ionto every element of the new array, andsetNomenclatures(...)immediately — optimistic update, so the list does not visibly snap back while the requests are in flight. - Diff against the previous array and PATCH only rows whose
sort_orderactually changed:api.patch('/api/nomenclatures/${id}/', { sort_order: i }), all in onePromise.all. PATCH (not PUT) matters —NomenclatureSerializer.updatedeletes and recreates the wholefieldscollection (backend/api/serializers.py:284-287), so a PUT without afieldskey would wipe the field definitions. A partial update leavesfieldsuntouched. - On rejection:
setError('Failed to save order')andawait 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 hascursor: pointer; leave it. Adduser-select: noneso 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-itemhas aborder-bottom(line 122), use an insetbox-shadow: inset 0 2px 0 #667eearather than a border so row height does not shift by 2px during the drag.
Explicitly not in scope
NomenclatureFieldrows (the "Fields" table on the right panel) — they have their ownsort_orderthat 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.NomenclatureEntryordering — entries have nosort_ordercolumn at all; that would need a migration. Not requested.Main.jsx— no change required. Line 31 already sorts bysort_order. Left as is.
Verification
- Start backend (
cd backend && uvicorn scalesapp.asgi:application --reload) and frontend (cd frontend && npm start); log in as a user withcan_manage_entities. - 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.
- Check the Network tab: several
PATCH /api/nomenclatures/{id}/requests, each with a{sort_order: N}body only, all returning 200. - 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(). - 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.
- 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.
- Log in as a user without
can_manage_entitiesand confirm rows are not draggable.