Add drag-to-reorder for nomenclatures

Nomenclatures can be reordered by dragging in the left-hand list, which
PATCHes sort_order on the rows that moved. PATCH rather than PUT, since a
full update recreates the nested fields collection.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-18 18:56:21 +03:00
parent 19945dae19
commit 183908cbab
2 changed files with 78 additions and 2 deletions
@@ -121,6 +121,7 @@
cursor: pointer;
border-bottom: 1px solid #f0f0f0;
transition: background 0.15s;
user-select: none;
}
.nm-list-item:hover {
@@ -132,6 +133,15 @@
border-left: 3px solid #667eea;
}
.nm-list-item--dragging {
opacity: 0.4;
}
/* Inset shadow rather than a border, so the drop line adds no row height */
.nm-list-item--drag-over {
box-shadow: inset 0 2px 0 #667eea;
}
.nm-list-item-info {
flex: 1;
min-width: 0;
@@ -22,6 +22,8 @@ function NomenclatureManager({ onClose }) {
const [isLoading, setIsLoading] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [error, setError] = useState('');
const [dragIndex, setDragIndex] = useState(null);
const [dragOverIndex, setDragOverIndex] = useState(null);
// Load nomenclature list
const loadNomenclatures = useCallback(async () => {
@@ -164,6 +166,60 @@ function NomenclatureManager({ onClose }) {
}));
};
// --- Drag to reorder the nomenclature list ---
const handleDragStart = (index, e) => {
setDragIndex(index);
e.dataTransfer.effectAllowed = 'move';
// Firefox will not start a drag unless some data is set
e.dataTransfer.setData('text/plain', String(index));
};
const handleDragOver = (index, e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
setDragOverIndex(index);
};
const handleDragEnd = () => {
setDragIndex(null);
setDragOverIndex(null);
};
const handleDrop = async (targetIndex, e) => {
e.preventDefault();
const sourceIndex = dragIndex;
setDragIndex(null);
setDragOverIndex(null);
if (sourceIndex === null || sourceIndex === targetIndex) return;
const previous = nomenclatures;
const reordered = [...previous];
const [moved] = reordered.splice(sourceIndex, 1);
reordered.splice(targetIndex, 0, moved);
// Renumber and apply optimistically so the list does not snap back
const withOrder = reordered.map((nom, i) => ({ ...nom, sort_order: i }));
setNomenclatures(withOrder);
// PATCH only the rows whose position actually changed. PATCH (not PUT) is
// required: a full update recreates the nested fields collection.
const changed = withOrder.filter(
(nom, i) => previous[i]?.id !== nom.id || previous[i]?.sort_order !== i
);
try {
await Promise.all(
changed.map(nom =>
api.patch(`/api/nomenclatures/${nom.id}/`, { sort_order: nom.sort_order })
)
);
} catch {
setError('Failed to save order');
await loadNomenclatures();
}
};
const handleOverlayClick = (e) => {
if (e.target.className === 'nm-overlay') {
onClose();
@@ -187,11 +243,21 @@ function NomenclatureManager({ onClose }) {
{isLoading && <div className="nm-loading">Loading...</div>}
<div className="nm-list">
{nomenclatures.map(nom => (
{nomenclatures.map((nom, idx) => (
<div
key={nom.id}
className={`nm-list-item ${selectedId === nom.id ? 'nm-list-item--active' : ''}`}
className={[
'nm-list-item',
selectedId === nom.id ? 'nm-list-item--active' : '',
dragIndex === idx ? 'nm-list-item--dragging' : '',
dragOverIndex === idx && dragIndex !== idx ? 'nm-list-item--drag-over' : '',
].filter(Boolean).join(' ')}
onClick={() => handleSelect(nom)}
draggable={canEdit}
onDragStart={e => handleDragStart(idx, e)}
onDragOver={e => handleDragOver(idx, e)}
onDragEnd={handleDragEnd}
onDrop={e => handleDrop(idx, e)}
>
<div className="nm-list-item-info">
<span className="nm-list-item-name">{nom.name}</span>