Files
scalesApp/.claude/plans/reorder-nomenclatures-drag-drop.md
T
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

6.9 KiB
Raw Blame History

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.