diff --git a/backend/internal/api/file_handlers.go b/backend/internal/api/file_handlers.go index 1a6e943..dc98752 100644 --- a/backend/internal/api/file_handlers.go +++ b/backend/internal/api/file_handlers.go @@ -120,19 +120,19 @@ func DeleteFile(fs *filesystem.FileSystem) http.HandlerFunc { func GetLastOpenedFile(db *db.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - userID, _, err := getUserAndWorkspaceIDs(r) + _, workspaceID, err := getUserAndWorkspaceIDs(r) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } - user, err := db.GetUserByID(userID) + filePath, err := db.GetLastOpenedFile(workspaceID) if err != nil { - http.Error(w, "Failed to get user", http.StatusInternalServerError) + http.Error(w, "Failed to get last opened file", http.StatusInternalServerError) return } - respondJSON(w, map[string]string{"lastOpenedFile": user.LastOpenedFilePath}) + respondJSON(w, map[string]string{"lastOpenedFilePath": filePath}) } } @@ -153,14 +153,15 @@ func UpdateLastOpenedFile(db *db.DB, fs *filesystem.FileSystem) http.HandlerFunc return } - // Validate that the file path is valid within the workspace - _, err = fs.ValidatePath(userID, workspaceID, requestBody.FilePath) - if err != nil { - http.Error(w, "Invalid file path", http.StatusBadRequest) - return + // Validate the file path exists in the workspace + if requestBody.FilePath != "" { + if _, err := fs.ValidatePath(userID, workspaceID, requestBody.FilePath); err != nil { + http.Error(w, "Invalid file path", http.StatusBadRequest) + return + } } - if err := db.UpdateLastOpenedFile(userID, requestBody.FilePath); err != nil { + if err := db.UpdateLastOpenedFile(workspaceID, requestBody.FilePath); err != nil { http.Error(w, "Failed to update last opened file", http.StatusInternalServerError) return } diff --git a/backend/internal/db/migrations.go b/backend/internal/db/migrations.go index 4eb9402..4b24d9c 100644 --- a/backend/internal/db/migrations.go +++ b/backend/internal/db/migrations.go @@ -22,8 +22,7 @@ var migrations = []Migration{ password_hash TEXT NOT NULL, role TEXT NOT NULL CHECK(role IN ('admin', 'editor', 'viewer')), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - last_workspace_id INTEGER, - last_opened_file_path TEXT + last_workspace_id INTEGER ); -- Create workspaces table with integrated settings @@ -32,6 +31,7 @@ var migrations = []Migration{ user_id INTEGER NOT NULL, name TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + last_opened_file_path TEXT, -- Settings fields theme TEXT NOT NULL DEFAULT 'light' CHECK(theme IN ('light', 'dark')), auto_save BOOLEAN NOT NULL DEFAULT 0, diff --git a/backend/internal/db/users.go b/backend/internal/db/users.go index 4a0251d..b2c3709 100644 --- a/backend/internal/db/users.go +++ b/backend/internal/db/users.go @@ -81,15 +81,13 @@ func (db *DB) createWorkspaceTx(tx *sql.Tx, workspace *models.Workspace) error { func (db *DB) GetUserByID(id int) (*models.User, error) { user := &models.User{} err := db.QueryRow(` - SELECT - u.id, u.email, u.display_name, u.role, u.created_at, - u.last_workspace_id, u.last_opened_file_path, - COALESCE(w.id, 0) as workspace_id - FROM users u - LEFT JOIN workspaces w ON w.id = u.last_workspace_id - WHERE u.id = ?`, id). + SELECT + id, email, display_name, role, created_at, + last_workspace_id + FROM users + WHERE id = ?`, id). Scan(&user.ID, &user.Email, &user.DisplayName, &user.Role, &user.CreatedAt, - &user.LastWorkspaceID, &user.LastOpenedFilePath, &user.LastWorkspaceID) + &user.LastWorkspaceID) if err != nil { return nil, err } @@ -98,34 +96,27 @@ func (db *DB) GetUserByID(id int) (*models.User, error) { func (db *DB) GetUserByEmail(email string) (*models.User, error) { user := &models.User{} - var lastOpenedFilePath sql.NullString err := db.QueryRow(` - SELECT - u.id, u.email, u.display_name, u.password_hash, u.role, u.created_at, - u.last_workspace_id, u.last_opened_file_path, - COALESCE(w.id, 0) as workspace_id - FROM users u - LEFT JOIN workspaces w ON w.id = u.last_workspace_id - WHERE u.email = ?`, email). + SELECT + id, email, display_name, password_hash, role, created_at, + last_workspace_id + FROM users + WHERE email = ?`, email). Scan(&user.ID, &user.Email, &user.DisplayName, &user.PasswordHash, &user.Role, &user.CreatedAt, - &user.LastWorkspaceID, &lastOpenedFilePath, &user.LastWorkspaceID) + &user.LastWorkspaceID) if err != nil { return nil, err } - if lastOpenedFilePath.Valid { - user.LastOpenedFilePath = lastOpenedFilePath.String - } else { - user.LastOpenedFilePath = "" - } + return user, nil } func (db *DB) UpdateUser(user *models.User) error { _, err := db.Exec(` UPDATE users - SET email = ?, display_name = ?, role = ?, last_workspace_id = ?, last_opened_file_path = ? + SET email = ?, display_name = ?, role = ?, last_workspace_id = ? WHERE id = ?`, - user.Email, user.DisplayName, user.Role, user.LastWorkspaceID, user.LastOpenedFilePath, user.ID) + user.Email, user.DisplayName, user.Role, user.LastWorkspaceID, user.ID) return err } @@ -134,11 +125,6 @@ func (db *DB) UpdateLastWorkspace(userID, workspaceID int) error { return err } -func (db *DB) UpdateLastOpenedFile(userID int, filePath string) error { - _, err := db.Exec("UPDATE users SET last_opened_file_path = ? WHERE id = ?", filePath, userID) - return err -} - func (db *DB) DeleteUser(id int) error { tx, err := db.Begin() if err != nil { diff --git a/backend/internal/db/workspaces.go b/backend/internal/db/workspaces.go index 344e4e2..b3264ae 100644 --- a/backend/internal/db/workspaces.go +++ b/backend/internal/db/workspaces.go @@ -160,3 +160,20 @@ 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 } + +func (db *DB) UpdateLastOpenedFile(workspaceID int, filePath string) error { + _, err := db.Exec("UPDATE workspaces SET last_opened_file_path = ? WHERE id = ?", filePath, workspaceID) + return err +} + +func (db *DB) GetLastOpenedFile(workspaceID int) (string, error) { + var filePath sql.NullString + err := db.QueryRow("SELECT last_opened_file_path FROM workspaces WHERE id = ?", workspaceID).Scan(&filePath) + if err != nil { + return "", err + } + if !filePath.Valid { + return "", nil + } + return filePath.String, nil +} diff --git a/backend/internal/models/user.go b/backend/internal/models/user.go index 8881601..e2efcbb 100644 --- a/backend/internal/models/user.go +++ b/backend/internal/models/user.go @@ -17,14 +17,13 @@ const ( ) type User struct { - ID int `json:"id" validate:"required,min=1"` - Email string `json:"email" validate:"required,email"` - DisplayName string `json:"displayName"` - PasswordHash string `json:"-"` - Role UserRole `json:"role" validate:"required,oneof=admin editor viewer"` - CreatedAt time.Time `json:"createdAt"` - LastWorkspaceID int `json:"lastWorkspaceId"` - LastOpenedFilePath string `json:"lastOpenedFilePath"` + ID int `json:"id" validate:"required,min=1"` + Email string `json:"email" validate:"required,email"` + DisplayName string `json:"displayName"` + PasswordHash string `json:"-"` + Role UserRole `json:"role" validate:"required,oneof=admin editor viewer"` + CreatedAt time.Time `json:"createdAt"` + LastWorkspaceID int `json:"lastWorkspaceId"` } func (u *User) Validate() error { diff --git a/backend/internal/models/workspace.go b/backend/internal/models/workspace.go index 28f1bc5..16e9081 100644 --- a/backend/internal/models/workspace.go +++ b/backend/internal/models/workspace.go @@ -5,10 +5,11 @@ import ( ) type Workspace struct { - ID int `json:"id" validate:"required,min=1"` - UserID int `json:"userId" validate:"required,min=1"` - Name string `json:"name" validate:"required"` - CreatedAt time.Time `json:"createdAt"` + ID int `json:"id" validate:"required,min=1"` + UserID int `json:"userId" validate:"required,min=1"` + Name string `json:"name" validate:"required"` + CreatedAt time.Time `json:"createdAt"` + LastOpenedFilePath string `json:"lastOpenedFilePath"` // Integrated settings Theme string `json:"theme" validate:"oneof=light dark"` diff --git a/backend/internal/user/user.go b/backend/internal/user/user.go index 6c9e2be..dbfce37 100644 --- a/backend/internal/user/user.go +++ b/backend/internal/user/user.go @@ -1,6 +1,7 @@ package user import ( + "database/sql" "fmt" "log" @@ -28,7 +29,7 @@ func (s *UserService) SetupAdminUser(adminEmail, adminPassword string) (*models. adminUser, err := s.DB.GetUserByEmail(adminEmail) if adminUser != nil { return adminUser, nil // Admin user already exists - } else if err != nil { + } else if err != sql.ErrNoRows { return nil, err } diff --git a/frontend/src/hooks/useFileNavigation.js b/frontend/src/hooks/useFileNavigation.js index 15928b7..0803e64 100644 --- a/frontend/src/hooks/useFileNavigation.js +++ b/frontend/src/hooks/useFileNavigation.js @@ -1,18 +1,28 @@ -import { useState, useCallback, useEffect } from 'react'; // Added useEffect +import { useState, useCallback, useEffect } from 'react'; import { notifications } from '@mantine/notifications'; import { lookupFileByName } from '../services/api'; import { DEFAULT_FILE } from '../utils/constants'; import { useWorkspace } from '../contexts/WorkspaceContext'; +import { useLastOpenedFile } from './useLastOpenedFile'; export const useFileNavigation = () => { const [selectedFile, setSelectedFile] = useState(DEFAULT_FILE.path); const [isNewFile, setIsNewFile] = useState(true); const { currentWorkspace } = useWorkspace(); + const { loadLastOpenedFile, saveLastOpenedFile } = useLastOpenedFile(); - const handleFileSelect = useCallback((filePath) => { - setSelectedFile(filePath || DEFAULT_FILE.path); - setIsNewFile(filePath ? false : true); - }, []); + const handleFileSelect = useCallback( + async (filePath) => { + const newPath = filePath || DEFAULT_FILE.path; + setSelectedFile(newPath); + setIsNewFile(!filePath); + + if (filePath) { + await saveLastOpenedFile(filePath); + } + }, + [saveLastOpenedFile] + ); const handleLinkClick = useCallback( async (filename) => { @@ -41,10 +51,19 @@ export const useFileNavigation = () => { [currentWorkspace, handleFileSelect] ); - // Reset to default file when workspace changes + // Load last opened file when workspace changes useEffect(() => { - handleFileSelect(null); - }, [currentWorkspace, handleFileSelect]); + const initializeFile = async () => { + const lastFile = await loadLastOpenedFile(); + if (lastFile) { + handleFileSelect(lastFile); + } else { + handleFileSelect(null); + } + }; + + initializeFile(); + }, [currentWorkspace, loadLastOpenedFile, handleFileSelect]); return { handleLinkClick, selectedFile, isNewFile, handleFileSelect }; }; diff --git a/frontend/src/hooks/useLastOpenedFile.js b/frontend/src/hooks/useLastOpenedFile.js new file mode 100644 index 0000000..1844fd6 --- /dev/null +++ b/frontend/src/hooks/useLastOpenedFile.js @@ -0,0 +1,37 @@ +import { useCallback } from 'react'; +import { getLastOpenedFile, updateLastOpenedFile } from '../services/api'; +import { useWorkspace } from '../contexts/WorkspaceContext'; + +export const useLastOpenedFile = () => { + const { currentWorkspace } = useWorkspace(); + + const loadLastOpenedFile = useCallback(async () => { + if (!currentWorkspace) return null; + + try { + const response = await getLastOpenedFile(currentWorkspace.id); + return response.lastOpenedFilePath || null; + } catch (error) { + console.error('Failed to load last opened file:', error); + return null; + } + }, [currentWorkspace]); + + const saveLastOpenedFile = useCallback( + async (filePath) => { + if (!currentWorkspace) return; + + try { + await updateLastOpenedFile(currentWorkspace.id, filePath); + } catch (error) { + console.error('Failed to save last opened file:', error); + } + }, + [currentWorkspace] + ); + + return { + loadLastOpenedFile, + saveLastOpenedFile, + }; +};