diff --git a/backend/internal/api/workspace_handlers.go b/backend/internal/api/workspace_handlers.go index 0d1ad0f..c1c23c6 100644 --- a/backend/internal/api/workspace_handlers.go +++ b/backend/internal/api/workspace_handlers.go @@ -143,24 +143,57 @@ func DeleteWorkspace(db *db.DB) http.HandlerFunc { return } - workspace, err := db.GetWorkspaceByID(workspaceID) + // Check if this is the user's last workspace + workspaces, err := db.GetWorkspacesByUserID(userID) if err != nil { - http.Error(w, "Workspace not found", http.StatusNotFound) + http.Error(w, "Failed to get workspaces", http.StatusInternalServerError) return } - if workspace.UserID != userID { - http.Error(w, "Unauthorized access to workspace", http.StatusForbidden) + if len(workspaces) <= 1 { + http.Error(w, "Cannot delete the last workspace", http.StatusBadRequest) return } - if err := db.DeleteWorkspace(workspaceID); err != nil { + // Find another workspace to set as last + var nextWorkspaceID int + for _, ws := range workspaces { + if ws.ID != workspaceID { + nextWorkspaceID = ws.ID + break + } + } + + // Start transaction + tx, err := db.Begin() + if err != nil { + http.Error(w, "Failed to start transaction", http.StatusInternalServerError) + return + } + defer tx.Rollback() + + // Update last workspace ID first + err = db.UpdateLastWorkspaceTx(tx, userID, nextWorkspaceID) + if err != nil { + http.Error(w, "Failed to update last workspace", http.StatusInternalServerError) + return + } + + // Delete the workspace + err = db.DeleteWorkspaceTx(tx, workspaceID) + if err != nil { http.Error(w, "Failed to delete workspace", http.StatusInternalServerError) return } - w.WriteHeader(http.StatusOK) - w.Write([]byte("Workspace deleted successfully")) + // Commit transaction + if err = tx.Commit(); err != nil { + http.Error(w, "Failed to commit transaction", http.StatusInternalServerError) + return + } + + // Return the next workspace ID in the response so frontend knows where to redirect + respondJSON(w, map[string]int{"nextWorkspaceId": nextWorkspaceID}) } } diff --git a/backend/internal/db/workspaces.go b/backend/internal/db/workspaces.go index 7fb2038..344e4e2 100644 --- a/backend/internal/db/workspaces.go +++ b/backend/internal/db/workspaces.go @@ -1,6 +1,7 @@ package db import ( + "database/sql" "novamd/internal/models" ) @@ -149,3 +150,13 @@ func (db *DB) DeleteWorkspace(id int) error { _, err := db.Exec("DELETE FROM workspaces WHERE id = ?", id) return err } + +func (db *DB) DeleteWorkspaceTx(tx *sql.Tx, id int) error { + _, err := tx.Exec("DELETE FROM workspaces WHERE id = ?", id) + return err +} + +func (db *DB) UpdateLastWorkspaceTx(tx *sql.Tx, userID, workspaceID int) error { + _, err := tx.Exec("UPDATE users SET last_workspace_id = ? WHERE id = ?", workspaceID, userID) + return err +} diff --git a/frontend/src/components/settings/DangerZoneSettings.js b/frontend/src/components/settings/DangerZoneSettings.js index 8c328e9..2f23517 100644 --- a/frontend/src/components/settings/DangerZoneSettings.js +++ b/frontend/src/components/settings/DangerZoneSettings.js @@ -2,14 +2,18 @@ import React, { useState } from 'react'; import { Box, Button, Title } from '@mantine/core'; import DeleteWorkspaceModal from '../modals/DeleteWorkspaceModal'; import { useWorkspace } from '../../contexts/WorkspaceContext'; +import { useModalContext } from '../../contexts/ModalContext'; const DangerZoneSettings = () => { - const { currentWorkspace } = useWorkspace(); + const { currentWorkspace, workspaces, deleteCurrentWorkspace } = + useWorkspace(); + const { setSettingsModalVisible } = useModalContext(); const [deleteModalOpened, setDeleteModalOpened] = useState(false); - const handleDelete = () => { - // TODO: Implement delete functionality + const handleDelete = async () => { + await deleteCurrentWorkspace(); setDeleteModalOpened(false); + setSettingsModalVisible(false); }; return ( @@ -23,6 +27,12 @@ const DangerZoneSettings = () => { variant="light" onClick={() => setDeleteModalOpened(true)} fullWidth + disabled={workspaces.length <= 1} + title={ + workspaces.length <= 1 + ? 'Cannot delete the last workspace' + : 'Delete this workspace' + } > Delete Workspace diff --git a/frontend/src/contexts/WorkspaceContext.js b/frontend/src/contexts/WorkspaceContext.js index 2bae145..f7804e2 100644 --- a/frontend/src/contexts/WorkspaceContext.js +++ b/frontend/src/contexts/WorkspaceContext.js @@ -12,6 +12,8 @@ import { getWorkspace, updateWorkspace, updateLastWorkspace, + deleteWorkspace, + listWorkspaces, } from '../services/api'; import { DEFAULT_WORKSPACE_SETTINGS } from '../utils/constants'; @@ -19,9 +21,26 @@ const WorkspaceContext = createContext(); export const WorkspaceProvider = ({ children }) => { const [currentWorkspace, setCurrentWorkspace] = useState(null); + const [workspaces, setWorkspaces] = useState([]); const [loading, setLoading] = useState(true); const { colorScheme, setColorScheme } = useMantineColorScheme(); + const loadWorkspaces = useCallback(async () => { + try { + const workspaceList = await listWorkspaces(); + setWorkspaces(workspaceList); + return workspaceList; + } catch (error) { + console.error('Failed to load workspaces:', error); + notifications.show({ + title: 'Error', + message: 'Failed to load workspaces list', + color: 'red', + }); + return []; + } + }, []); + const loadWorkspaceData = useCallback(async (workspaceId) => { try { const workspace = await getWorkspace(workspaceId); @@ -37,6 +56,24 @@ export const WorkspaceProvider = ({ children }) => { } }, []); + const loadFirstAvailableWorkspace = useCallback(async () => { + try { + const allWorkspaces = await listWorkspaces(); + if (allWorkspaces.length > 0) { + const firstWorkspace = allWorkspaces[0]; + await updateLastWorkspace(firstWorkspace.id); + await loadWorkspaceData(firstWorkspace.id); + } + } catch (error) { + console.error('Failed to load first available workspace:', error); + notifications.show({ + title: 'Error', + message: 'Failed to load workspace', + color: 'red', + }); + } + }, []); + useEffect(() => { const initializeWorkspace = async () => { try { @@ -44,10 +81,12 @@ export const WorkspaceProvider = ({ children }) => { if (lastWorkspaceId) { await loadWorkspaceData(lastWorkspaceId); } else { - console.warn('No last workspace found'); + await loadFirstAvailableWorkspace(); } + await loadWorkspaces(); } catch (error) { console.error('Failed to initialize workspace:', error); + await loadFirstAvailableWorkspace(); } finally { setLoading(false); } @@ -61,6 +100,7 @@ export const WorkspaceProvider = ({ children }) => { setLoading(true); await updateLastWorkspace(workspaceId); await loadWorkspaceData(workspaceId); + await loadWorkspaces(); } catch (error) { console.error('Failed to switch workspace:', error); notifications.show({ @@ -73,6 +113,44 @@ export const WorkspaceProvider = ({ children }) => { } }, []); + const deleteCurrentWorkspace = useCallback(async () => { + if (!currentWorkspace) return; + + try { + const allWorkspaces = await loadWorkspaces(); + if (allWorkspaces.length <= 1) { + notifications.show({ + title: 'Error', + message: + 'Cannot delete the last workspace. At least one workspace must exist.', + color: 'red', + }); + return; + } + + // Delete workspace and get the next workspace ID + const response = await deleteWorkspace(currentWorkspace.id); + + // Load the new workspace data + await loadWorkspaceData(response.nextWorkspaceId); + + notifications.show({ + title: 'Success', + message: 'Workspace deleted successfully', + color: 'green', + }); + + await loadWorkspaces(); + } catch (error) { + console.error('Failed to delete workspace:', error); + notifications.show({ + title: 'Error', + message: 'Failed to delete workspace', + color: 'red', + }); + } + }, [currentWorkspace]); + const updateSettings = useCallback( async (newSettings) => { if (!currentWorkspace) return; @@ -89,27 +167,32 @@ export const WorkspaceProvider = ({ children }) => { ); setCurrentWorkspace(response); setColorScheme(response.theme); + await loadWorkspaces(); } catch (error) { console.error('Failed to save settings:', error); throw error; } }, - [currentWorkspace] + [currentWorkspace, setColorScheme] ); - // Update just the color scheme without saving to backend - const updateColorScheme = useCallback((newTheme) => { - setColorScheme(newTheme); - }, []); + const updateColorScheme = useCallback( + (newTheme) => { + setColorScheme(newTheme); + }, + [setColorScheme] + ); const value = { currentWorkspace, + workspaces, settings: currentWorkspace || DEFAULT_WORKSPACE_SETTINGS, updateSettings, loading, colorScheme, updateColorScheme, switchWorkspace, + deleteCurrentWorkspace, }; return (