diff --git a/docs/circuit-list-editor-api.md b/docs/circuit-list-editor-api.md index 2c42938..ee2580c 100644 --- a/docs/circuit-list-editor-api.md +++ b/docs/circuit-list-editor-api.md @@ -156,8 +156,12 @@ These remain for migration/legacy views. New circuit-list interactions must use - returns all linked circuit device rows with distribution-board/circuit context and field differences - `POST /project-devices/projects/:projectId/:projectDeviceId/synchronize` - applies only explicitly selected fields to explicitly selected linked rows +- `POST /project-devices/projects/:projectId/:projectDeviceId/restore` + - restores the captured pre-sync values for session-local undo - `POST /project-devices/projects/:projectId/:projectDeviceId/disconnect` - disconnects explicitly selected rows without changing their local values +- `POST /project-devices/projects/:projectId/:projectDeviceId/reconnect` + - restores a disconnected link if the row has not been linked elsewhere meanwhile `displayName` is included in the comparison but is not selected by default in the UI. Updating a project device never triggers synchronization implicitly. diff --git a/docs/circuit-list-editor-known-limitations.md b/docs/circuit-list-editor-known-limitations.md index 0f88c73..70f289b 100644 --- a/docs/circuit-list-editor-known-limitations.md +++ b/docs/circuit-list-editor-known-limitations.md @@ -9,5 +9,3 @@ - Sorting is view-only until users explicitly apply sorted order. - Cross-section circuit drag-reorder is intentionally blocked. - Legacy consumer and circuit-first paths coexist; migration is transitional and still requires operational discipline. -- Project-device synchronization and disconnect actions are explicit, but are not yet connected to an undo command. -- Multi-row project-device synchronization is sequential and not yet wrapped in one database transaction. diff --git a/src/app/projects/[projectId]/page.tsx b/src/app/projects/[projectId]/page.tsx index 7e889b5..dc6163c 100644 --- a/src/app/projects/[projectId]/page.tsx +++ b/src/app/projects/[projectId]/page.tsx @@ -20,6 +20,8 @@ import { listProjectDevices, listProjects, listRooms, + reconnectProjectDeviceRows, + restoreProjectDeviceRows, synchronizeProjectDeviceRows, updateProjectDevice, updateProjectSettings, @@ -32,6 +34,7 @@ import type { GlobalDeviceDto, ProjectDeviceDto, ProjectDeviceSyncPreviewDto, + ProjectDeviceSyncRestoreRowDto, ProjectDto, RoomDto, } from "../../../frontend/types"; @@ -54,6 +57,10 @@ const projectDeviceSyncFieldLabels: Record = { remark: "Bemerkung", }; +type ProjectDeviceUndoState = + | { kind: "synchronize"; projectDeviceId: string; rows: ProjectDeviceSyncRestoreRowDto[] } + | { kind: "disconnect"; projectDeviceId: string; rowIds: string[] }; + const emptyProjectDevice: CreateProjectDeviceInput = { name: "", displayName: "", @@ -110,6 +117,7 @@ export default function ProjectDetailPage() { const [syncPreview, setSyncPreview] = useState(null); const [selectedSyncRowIds, setSelectedSyncRowIds] = useState([]); const [selectedSyncFields, setSelectedSyncFields] = useState([]); + const [projectDeviceUndo, setProjectDeviceUndo] = useState(null); const [error, setError] = useState(null); const [isSaving, setIsSaving] = useState(false); @@ -347,6 +355,7 @@ export default function ProjectDetailPage() { return; } try { + setProjectDeviceUndo(null); const preview = await getProjectDeviceSyncPreview(projectId, projectDeviceId); setSyncPreview(preview); setSelectedSyncRowIds(preview.rows.filter((row) => row.differences.length > 0).map((row) => row.rowId)); @@ -380,14 +389,21 @@ export default function ProjectDetailPage() { setIsSaving(true); setError(null); try { - const preview = await synchronizeProjectDeviceRows( + const result = await synchronizeProjectDeviceRows( projectId, syncPreview.projectDevice.id, selectedSyncRowIds, selectedSyncFields ); - setSyncPreview(preview); - setSelectedSyncRowIds(preview.rows.filter((row) => row.differences.length > 0).map((row) => row.rowId)); + setSyncPreview(result.preview); + setSelectedSyncRowIds( + result.preview.rows.filter((row) => row.differences.length > 0).map((row) => row.rowId) + ); + setProjectDeviceUndo({ + kind: "synchronize", + projectDeviceId: syncPreview.projectDevice.id, + rows: result.undo.rows, + }); } catch (err) { setError(err instanceof Error ? err.message : "Gerätezeilen konnten nicht synchronisiert werden."); } finally { @@ -402,8 +418,17 @@ export default function ProjectDetailPage() { setIsSaving(true); setError(null); try { - await disconnectProjectDeviceRows(projectId, syncPreview.projectDevice.id, selectedSyncRowIds); + const result = await disconnectProjectDeviceRows( + projectId, + syncPreview.projectDevice.id, + selectedSyncRowIds + ); await openProjectDeviceSyncPreview(syncPreview.projectDevice.id); + setProjectDeviceUndo({ + kind: "disconnect", + projectDeviceId: syncPreview.projectDevice.id, + rowIds: result.undo.rowIds, + }); } catch (err) { setError(err instanceof Error ? err.message : "Verknüpfungen konnten nicht getrennt werden."); } finally { @@ -411,6 +436,35 @@ export default function ProjectDetailPage() { } } + async function handleUndoProjectDeviceOperation() { + if (!projectId || !projectDeviceUndo) { + return; + } + setIsSaving(true); + setError(null); + try { + const preview = + projectDeviceUndo.kind === "synchronize" + ? await restoreProjectDeviceRows( + projectId, + projectDeviceUndo.projectDeviceId, + projectDeviceUndo.rows + ) + : await reconnectProjectDeviceRows( + projectId, + projectDeviceUndo.projectDeviceId, + projectDeviceUndo.rowIds + ); + setSyncPreview(preview); + setSelectedSyncRowIds(preview.rows.filter((row) => row.differences.length > 0).map((row) => row.rowId)); + setProjectDeviceUndo(null); + } catch (err) { + setError(err instanceof Error ? err.message : "Die letzte Aktion konnte nicht rückgängig gemacht werden."); + } finally { + setIsSaving(false); + } + } + async function handleCopyGlobalToProject() { if (!projectId || !selectedGlobalDeviceId) { return; @@ -1039,6 +1093,16 @@ export default function ProjectDetailPage() {
+ {projectDeviceUndo ? ( + + ) : null}