diff --git a/frontend/src/components/NomenclatureManager/NomenclatureManager.css b/frontend/src/components/NomenclatureManager/NomenclatureManager.css index ac9c65a..056b359 100644 --- a/frontend/src/components/NomenclatureManager/NomenclatureManager.css +++ b/frontend/src/components/NomenclatureManager/NomenclatureManager.css @@ -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; diff --git a/frontend/src/components/NomenclatureManager/NomenclatureManager.jsx b/frontend/src/components/NomenclatureManager/NomenclatureManager.jsx index 334f08d..523f515 100644 --- a/frontend/src/components/NomenclatureManager/NomenclatureManager.jsx +++ b/frontend/src/components/NomenclatureManager/NomenclatureManager.jsx @@ -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 &&