Implement workspace deletion

This commit is contained in:
2024-10-27 21:19:42 +01:00
parent b679af08e7
commit ba4a0dadca
4 changed files with 153 additions and 16 deletions

View File

@@ -143,24 +143,57 @@ func DeleteWorkspace(db *db.DB) http.HandlerFunc {
return return
} }
workspace, err := db.GetWorkspaceByID(workspaceID) // Check if this is the user's last workspace
workspaces, err := db.GetWorkspacesByUserID(userID)
if err != nil { if err != nil {
http.Error(w, "Workspace not found", http.StatusNotFound) http.Error(w, "Failed to get workspaces", http.StatusInternalServerError)
return return
} }
if workspace.UserID != userID { if len(workspaces) <= 1 {
http.Error(w, "Unauthorized access to workspace", http.StatusForbidden) http.Error(w, "Cannot delete the last workspace", http.StatusBadRequest)
return 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) http.Error(w, "Failed to delete workspace", http.StatusInternalServerError)
return return
} }
w.WriteHeader(http.StatusOK) // Commit transaction
w.Write([]byte("Workspace deleted successfully")) 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})
} }
} }

View File

@@ -1,6 +1,7 @@
package db package db
import ( import (
"database/sql"
"novamd/internal/models" "novamd/internal/models"
) )
@@ -149,3 +150,13 @@ func (db *DB) DeleteWorkspace(id int) error {
_, err := db.Exec("DELETE FROM workspaces WHERE id = ?", id) _, err := db.Exec("DELETE FROM workspaces WHERE id = ?", id)
return err 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
}

View File

@@ -2,14 +2,18 @@ import React, { useState } from 'react';
import { Box, Button, Title } from '@mantine/core'; import { Box, Button, Title } from '@mantine/core';
import DeleteWorkspaceModal from '../modals/DeleteWorkspaceModal'; import DeleteWorkspaceModal from '../modals/DeleteWorkspaceModal';
import { useWorkspace } from '../../contexts/WorkspaceContext'; import { useWorkspace } from '../../contexts/WorkspaceContext';
import { useModalContext } from '../../contexts/ModalContext';
const DangerZoneSettings = () => { const DangerZoneSettings = () => {
const { currentWorkspace } = useWorkspace(); const { currentWorkspace, workspaces, deleteCurrentWorkspace } =
useWorkspace();
const { setSettingsModalVisible } = useModalContext();
const [deleteModalOpened, setDeleteModalOpened] = useState(false); const [deleteModalOpened, setDeleteModalOpened] = useState(false);
const handleDelete = () => { const handleDelete = async () => {
// TODO: Implement delete functionality await deleteCurrentWorkspace();
setDeleteModalOpened(false); setDeleteModalOpened(false);
setSettingsModalVisible(false);
}; };
return ( return (
@@ -23,6 +27,12 @@ const DangerZoneSettings = () => {
variant="light" variant="light"
onClick={() => setDeleteModalOpened(true)} onClick={() => setDeleteModalOpened(true)}
fullWidth fullWidth
disabled={workspaces.length <= 1}
title={
workspaces.length <= 1
? 'Cannot delete the last workspace'
: 'Delete this workspace'
}
> >
Delete Workspace Delete Workspace
</Button> </Button>

View File

@@ -12,6 +12,8 @@ import {
getWorkspace, getWorkspace,
updateWorkspace, updateWorkspace,
updateLastWorkspace, updateLastWorkspace,
deleteWorkspace,
listWorkspaces,
} from '../services/api'; } from '../services/api';
import { DEFAULT_WORKSPACE_SETTINGS } from '../utils/constants'; import { DEFAULT_WORKSPACE_SETTINGS } from '../utils/constants';
@@ -19,9 +21,26 @@ const WorkspaceContext = createContext();
export const WorkspaceProvider = ({ children }) => { export const WorkspaceProvider = ({ children }) => {
const [currentWorkspace, setCurrentWorkspace] = useState(null); const [currentWorkspace, setCurrentWorkspace] = useState(null);
const [workspaces, setWorkspaces] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const { colorScheme, setColorScheme } = useMantineColorScheme(); 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) => { const loadWorkspaceData = useCallback(async (workspaceId) => {
try { try {
const workspace = await getWorkspace(workspaceId); 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(() => { useEffect(() => {
const initializeWorkspace = async () => { const initializeWorkspace = async () => {
try { try {
@@ -44,10 +81,12 @@ export const WorkspaceProvider = ({ children }) => {
if (lastWorkspaceId) { if (lastWorkspaceId) {
await loadWorkspaceData(lastWorkspaceId); await loadWorkspaceData(lastWorkspaceId);
} else { } else {
console.warn('No last workspace found'); await loadFirstAvailableWorkspace();
} }
await loadWorkspaces();
} catch (error) { } catch (error) {
console.error('Failed to initialize workspace:', error); console.error('Failed to initialize workspace:', error);
await loadFirstAvailableWorkspace();
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -61,6 +100,7 @@ export const WorkspaceProvider = ({ children }) => {
setLoading(true); setLoading(true);
await updateLastWorkspace(workspaceId); await updateLastWorkspace(workspaceId);
await loadWorkspaceData(workspaceId); await loadWorkspaceData(workspaceId);
await loadWorkspaces();
} catch (error) { } catch (error) {
console.error('Failed to switch workspace:', error); console.error('Failed to switch workspace:', error);
notifications.show({ 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( const updateSettings = useCallback(
async (newSettings) => { async (newSettings) => {
if (!currentWorkspace) return; if (!currentWorkspace) return;
@@ -89,27 +167,32 @@ export const WorkspaceProvider = ({ children }) => {
); );
setCurrentWorkspace(response); setCurrentWorkspace(response);
setColorScheme(response.theme); setColorScheme(response.theme);
await loadWorkspaces();
} catch (error) { } catch (error) {
console.error('Failed to save settings:', error); console.error('Failed to save settings:', error);
throw error; throw error;
} }
}, },
[currentWorkspace] [currentWorkspace, setColorScheme]
); );
// Update just the color scheme without saving to backend const updateColorScheme = useCallback(
const updateColorScheme = useCallback((newTheme) => { (newTheme) => {
setColorScheme(newTheme); setColorScheme(newTheme);
}, []); },
[setColorScheme]
);
const value = { const value = {
currentWorkspace, currentWorkspace,
workspaces,
settings: currentWorkspace || DEFAULT_WORKSPACE_SETTINGS, settings: currentWorkspace || DEFAULT_WORKSPACE_SETTINGS,
updateSettings, updateSettings,
loading, loading,
colorScheme, colorScheme,
updateColorScheme, updateColorScheme,
switchWorkspace, switchWorkspace,
deleteCurrentWorkspace,
}; };
return ( return (