Add Workspace context

This commit is contained in:
2024-10-19 13:48:37 +02:00
parent 3b7bf83073
commit 6eb3eecb24
16 changed files with 356 additions and 210 deletions

View File

@@ -3,19 +3,13 @@ import { MantineProvider, ColorSchemeScript } from '@mantine/core';
import { Notifications } from '@mantine/notifications';
import { ModalsProvider } from '@mantine/modals';
import Layout from './components/Layout';
import { SettingsProvider, useSettings } from './contexts/SettingsContext';
import { WorkspaceProvider } from './contexts/WorkspaceContext';
import { ModalProvider } from './contexts/ModalContext';
import '@mantine/core/styles.css';
import '@mantine/notifications/styles.css';
import './App.scss';
function AppContent() {
const { loading } = useSettings();
if (loading) {
return <div>Loading...</div>;
}
return <Layout />;
}
@@ -26,11 +20,11 @@ function App() {
<MantineProvider defaultColorScheme="light">
<Notifications />
<ModalsProvider>
<SettingsProvider>
<WorkspaceProvider>
<ModalProvider>
<AppContent />
</ModalProvider>
</SettingsProvider>
</WorkspaceProvider>
</ModalsProvider>
</MantineProvider>
</>

View File

@@ -5,10 +5,10 @@ import { EditorView, keymap } from '@codemirror/view';
import { markdown } from '@codemirror/lang-markdown';
import { defaultKeymap } from '@codemirror/commands';
import { oneDark } from '@codemirror/theme-one-dark';
import { useSettings } from '../contexts/SettingsContext';
import { useWorkspace } from '../contexts/WorkspaceContext';
const Editor = ({ content, handleContentChange, handleSave, selectedFile }) => {
const { settings } = useSettings();
const { settings } = useWorkspace();
const editorRef = useRef();
const viewRef = useRef();

View File

@@ -6,11 +6,11 @@ import {
IconGitPullRequest,
IconGitCommit,
} from '@tabler/icons-react';
import { useSettings } from '../contexts/SettingsContext';
import { useModalContext } from '../contexts/ModalContext';
import { useWorkspace } from '../contexts/WorkspaceContext';
const FileActions = ({ handlePullChanges, selectedFile }) => {
const { settings } = useSettings();
const { settings } = useWorkspace();
const {
setNewFileModalVisible,
setDeleteFileModalVisible,

View File

@@ -1,16 +1,30 @@
import React from 'react';
import { AppShell, Container } from '@mantine/core';
import { AppShell, Container, Loader, Center } from '@mantine/core';
import Header from './Header';
import Sidebar from './Sidebar';
import MainContent from './MainContent';
import { useFileNavigation } from '../hooks/useFileNavigation';
import { useFileList } from '../hooks/useFileList';
import { useWorkspace } from '../contexts/WorkspaceContext';
const Layout = () => {
const { currentWorkspace, loading: workspaceLoading } = useWorkspace();
const { selectedFile, handleFileSelect, handleLinkClick } =
useFileNavigation();
const { files, loadFileList } = useFileList();
if (workspaceLoading) {
return (
<Center style={{ height: '100vh' }}>
<Loader size="xl" />
</Center>
);
}
if (!currentWorkspace) {
return <div>No workspace found. Please create a workspace.</div>;
}
return (
<AppShell header={{ height: 60 }} padding="md">
<AppShell.Header>
@@ -22,8 +36,8 @@ const Layout = () => {
p={0}
style={{
display: 'flex',
height: 'calc(100vh - 60px - 2rem)', // Subtracting header height and vertical padding
overflow: 'hidden', // Prevent scrolling in the container
height: 'calc(100vh - 60px - 2rem)',
overflow: 'hidden',
}}
>
<Sidebar

View File

@@ -10,7 +10,7 @@ import CommitMessageModal from './modals/CommitMessageModal';
import { useFileContent } from '../hooks/useFileContent';
import { useFileOperations } from '../hooks/useFileOperations';
import { useGitOperations } from '../hooks/useGitOperations';
import { useSettings } from '../contexts/SettingsContext';
import { useWorkspace } from '../contexts/WorkspaceContext';
const MainContent = ({
selectedFile,
@@ -19,7 +19,7 @@ const MainContent = ({
loadFileList,
}) => {
const [activeTab, setActiveTab] = useState('source');
const { settings } = useSettings();
const { settings } = useWorkspace();
const {
content,
hasUnsavedChanges,

View File

@@ -1,7 +1,7 @@
import React, { useReducer, useEffect, useCallback, useRef } from 'react';
import { Modal, Badge, Button, Group, Title } from '@mantine/core';
import { notifications } from '@mantine/notifications';
import { useSettings } from '../contexts/SettingsContext';
import { useWorkspace } from '../contexts/WorkspaceContext';
import AppearanceSettings from './settings/AppearanceSettings';
import EditorSettings from './settings/EditorSettings';
import GitSettings from './settings/GitSettings';
@@ -50,7 +50,7 @@ function settingsReducer(state, action) {
}
const Settings = () => {
const { settings, updateSettings, colorScheme } = useSettings();
const { settings, updateSettings, colorScheme } = useWorkspace();
const { settingsModalVisible, setSettingsModalVisible } = useModalContext();
const [state, dispatch] = useReducer(settingsReducer, initialState);
const isInitialMount = useRef(true);

View File

@@ -3,10 +3,10 @@ import { Box } from '@mantine/core';
import FileActions from './FileActions';
import FileTree from './FileTree';
import { useGitOperations } from '../hooks/useGitOperations';
import { useSettings } from '../contexts/SettingsContext';
import { useWorkspace } from '../contexts/WorkspaceContext';
const Sidebar = ({ selectedFile, handleFileSelect, files, loadFileList }) => {
const { settings } = useSettings();
const { settings } = useWorkspace();
const { handlePull } = useGitOperations(settings.gitEnabled);
useEffect(() => {

View File

@@ -1,9 +1,9 @@
import React from 'react';
import { Text, Switch, Group, Box, Title } from '@mantine/core';
import { useSettings } from '../../contexts/SettingsContext';
import { useWorkspace } from '../../contexts/WorkspaceContext';
const AppearanceSettings = ({ onThemeChange }) => {
const { colorScheme, toggleColorScheme } = useSettings();
const { colorScheme, toggleColorScheme } = useWorkspace();
const handleThemeChange = () => {
toggleColorScheme();

View File

@@ -1,79 +0,0 @@
import React, {
createContext,
useContext,
useEffect,
useMemo,
useCallback,
useState,
} from 'react';
import { useMantineColorScheme } from '@mantine/core';
import { fetchUserSettings, saveUserSettings } from '../services/api';
import { DEFAULT_SETTINGS } from '../utils/constants';
const SettingsContext = createContext();
export const useSettings = () => useContext(SettingsContext);
export const SettingsProvider = ({ children }) => {
const { colorScheme, setColorScheme } = useMantineColorScheme();
const [settings, setSettings] = useState(DEFAULT_SETTINGS);
const [loading, setLoading] = useState(true);
useEffect(() => {
const loadSettings = async () => {
try {
const userSettings = await fetchUserSettings(1);
setSettings(userSettings.settings);
setColorScheme(userSettings.settings.theme);
} catch (error) {
console.error('Failed to load user settings:', error);
} finally {
setLoading(false);
}
};
loadSettings();
}, []);
const updateSettings = useCallback(
async (newSettings) => {
try {
await saveUserSettings({
userId: 1,
settings: newSettings,
});
setSettings(newSettings);
if (newSettings.theme) {
setColorScheme(newSettings.theme);
}
} catch (error) {
console.error('Failed to save settings:', error);
throw error;
}
},
[setColorScheme]
);
const toggleColorScheme = useCallback(() => {
const newTheme = colorScheme === 'dark' ? 'light' : 'dark';
setColorScheme(newTheme);
updateSettings({ ...settings, theme: newTheme });
}, [colorScheme, settings, setColorScheme, updateSettings]);
const contextValue = useMemo(
() => ({
settings,
updateSettings,
toggleColorScheme,
loading,
colorScheme,
}),
[settings, updateSettings, toggleColorScheme, loading, colorScheme]
);
return (
<SettingsContext.Provider value={contextValue}>
{children}
</SettingsContext.Provider>
);
};

View File

@@ -0,0 +1,92 @@
import React, {
createContext,
useContext,
useState,
useEffect,
useCallback,
} from 'react';
import { useMantineColorScheme } from '@mantine/core';
import {
fetchLastWorkspace,
fetchWorkspaceSettings,
saveWorkspaceSettings,
} from '../services/api';
import { DEFAULT_SETTINGS } from '../utils/constants';
const WorkspaceContext = createContext();
export const WorkspaceProvider = ({ children }) => {
const [currentWorkspace, setCurrentWorkspace] = useState(null);
const [settings, setSettings] = useState(DEFAULT_SETTINGS);
const [loading, setLoading] = useState(true);
const { colorScheme, setColorScheme } = useMantineColorScheme();
useEffect(() => {
const loadWorkspaceAndSettings = async () => {
try {
const workspace = await fetchLastWorkspace();
setCurrentWorkspace(workspace);
if (workspace) {
const workspaceSettings = await fetchWorkspaceSettings(workspace.id);
setSettings(workspaceSettings.settings);
setColorScheme(workspaceSettings.settings.theme);
}
} catch (error) {
console.error('Failed to load workspace or settings:', error);
} finally {
setLoading(false);
}
};
loadWorkspaceAndSettings();
}, [setColorScheme]);
const updateSettings = useCallback(
async (newSettings) => {
if (!currentWorkspace) return;
try {
await saveWorkspaceSettings(currentWorkspace.id, newSettings);
setSettings(newSettings);
if (newSettings.theme) {
setColorScheme(newSettings.theme);
}
} catch (error) {
console.error('Failed to save settings:', error);
throw error;
}
},
[currentWorkspace, setColorScheme]
);
const toggleColorScheme = useCallback(() => {
const newTheme = colorScheme === 'dark' ? 'light' : 'dark';
setColorScheme(newTheme);
updateSettings({ ...settings, theme: newTheme });
}, [colorScheme, settings, setColorScheme, updateSettings]);
const value = {
currentWorkspace,
setCurrentWorkspace,
settings,
updateSettings,
toggleColorScheme,
loading,
colorScheme,
};
return (
<WorkspaceContext.Provider value={value}>
{children}
</WorkspaceContext.Provider>
);
};
export const useWorkspace = () => {
const context = useContext(WorkspaceContext);
if (context === undefined) {
throw new Error('useWorkspace must be used within a WorkspaceProvider');
}
return context;
};

View File

@@ -2,19 +2,24 @@ import { useState, useCallback, useEffect } from 'react';
import { fetchFileContent } from '../services/api';
import { isImageFile } from '../utils/fileHelpers';
import { DEFAULT_FILE } from '../utils/constants';
import { useWorkspace } from '../contexts/WorkspaceContext';
export const useFileContent = (selectedFile) => {
const { currentWorkspace } = useWorkspace();
const [content, setContent] = useState(DEFAULT_FILE.content);
const [originalContent, setOriginalContent] = useState(DEFAULT_FILE.content);
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const loadFileContent = useCallback(async (filePath) => {
const loadFileContent = useCallback(
async (filePath) => {
if (!currentWorkspace) return;
try {
let newContent;
if (filePath === DEFAULT_FILE.path) {
newContent = DEFAULT_FILE.content;
} else if (!isImageFile(filePath)) {
newContent = await fetchFileContent(filePath);
newContent = await fetchFileContent(currentWorkspace.id, filePath);
} else {
newContent = ''; // Set empty content for image files
}
@@ -27,13 +32,15 @@ export const useFileContent = (selectedFile) => {
setOriginalContent('');
setHasUnsavedChanges(false);
}
}, []);
},
[currentWorkspace]
);
useEffect(() => {
if (selectedFile) {
if (selectedFile && currentWorkspace) {
loadFileContent(selectedFile);
}
}, [selectedFile, loadFileContent]);
}, [selectedFile, currentWorkspace, loadFileContent]);
const handleContentChange = useCallback(
(newContent) => {

View File

@@ -1,12 +1,16 @@
import { useState, useCallback } from 'react';
import { fetchFileList } from '../services/api';
import { useWorkspace } from '../contexts/WorkspaceContext';
export const useFileList = () => {
const [files, setFiles] = useState([]);
const { currentWorkspace } = useWorkspace();
const loadFileList = useCallback(async () => {
if (!currentWorkspace) return;
try {
const fileList = await fetchFileList();
const fileList = await fetchFileList(currentWorkspace.id);
if (Array.isArray(fileList)) {
setFiles(fileList);
} else {
@@ -15,7 +19,7 @@ export const useFileList = () => {
} catch (error) {
console.error('Failed to load file list:', error);
}
}, []);
}, [currentWorkspace]);
return { files, loadFileList };
};

View File

@@ -2,10 +2,12 @@ import { useState, useCallback } from 'react';
import { notifications } from '@mantine/notifications';
import { lookupFileByName } from '../services/api';
import { DEFAULT_FILE } from '../utils/constants';
import { useWorkspace } from '../contexts/WorkspaceContext';
export const useFileNavigation = () => {
const [selectedFile, setSelectedFile] = useState(DEFAULT_FILE.path);
const [isNewFile, setIsNewFile] = useState(true);
const { currentWorkspace } = useWorkspace();
const handleFileSelect = useCallback((filePath) => {
setSelectedFile(filePath);
@@ -14,8 +16,10 @@ export const useFileNavigation = () => {
const handleLinkClick = useCallback(
async (filename) => {
if (!currentWorkspace) return;
try {
const filePaths = await lookupFileByName(filename);
const filePaths = await lookupFileByName(currentWorkspace.id, filename);
if (filePaths.length >= 1) {
handleFileSelect(filePaths[0]);
} else {
@@ -34,7 +38,7 @@ export const useFileNavigation = () => {
});
}
},
[handleFileSelect]
[currentWorkspace, handleFileSelect]
);
return { handleLinkClick, selectedFile, isNewFile, handleFileSelect };

View File

@@ -1,12 +1,12 @@
import { useCallback } from 'react';
import { notifications } from '@mantine/notifications';
import { saveFileContent, deleteFile } from '../services/api';
import { useSettings } from '../contexts/SettingsContext';
import { useWorkspace } from '../contexts/WorkspaceContext';
import { useGitOperations } from './useGitOperations';
export const useFileOperations = () => {
const { settings } = useSettings();
const { handleCommitAndPush } = useGitOperations(settings.gitEnabled);
const { currentWorkspace, settings } = useWorkspace();
const { handleCommitAndPush } = useGitOperations();
const autoCommit = useCallback(
async (filePath, action) => {
@@ -15,7 +15,6 @@ export const useFileOperations = () => {
.replace('${filename}', filePath)
.replace('${action}', action);
// Capitalize the first letter of the commit message
commitMessage =
commitMessage.charAt(0).toUpperCase() + commitMessage.slice(1);
@@ -27,8 +26,10 @@ export const useFileOperations = () => {
const handleSave = useCallback(
async (filePath, content) => {
if (!currentWorkspace) return false;
try {
await saveFileContent(filePath, content);
await saveFileContent(currentWorkspace.id, filePath, content);
notifications.show({
title: 'Success',
message: 'File saved successfully',
@@ -46,13 +47,15 @@ export const useFileOperations = () => {
return false;
}
},
[autoCommit]
[currentWorkspace, autoCommit]
);
const handleDelete = useCallback(
async (filePath) => {
if (!currentWorkspace) return false;
try {
await deleteFile(filePath);
await deleteFile(currentWorkspace.id, filePath);
notifications.show({
title: 'Success',
message: 'File deleted successfully',
@@ -70,13 +73,15 @@ export const useFileOperations = () => {
return false;
}
},
[autoCommit]
[currentWorkspace, autoCommit]
);
const handleCreate = useCallback(
async (fileName, initialContent = '') => {
if (!currentWorkspace) return false;
try {
await saveFileContent(fileName, initialContent);
await saveFileContent(currentWorkspace.id, fileName, initialContent);
notifications.show({
title: 'Success',
message: 'File created successfully',
@@ -94,7 +99,7 @@ export const useFileOperations = () => {
return false;
}
},
[autoCommit]
[currentWorkspace, autoCommit]
);
return { handleSave, handleDelete, handleCreate };

View File

@@ -1,12 +1,16 @@
import { useCallback } from 'react';
import { notifications } from '@mantine/notifications';
import { pullChanges, commitAndPush } from '../services/api';
import { useWorkspace } from '../contexts/WorkspaceContext';
export const useGitOperations = () => {
const { currentWorkspace, settings } = useWorkspace();
export const useGitOperations = (gitEnabled) => {
const handlePull = useCallback(async () => {
if (!gitEnabled) return false;
if (!currentWorkspace || !settings.gitEnabled) return false;
try {
await pullChanges();
await pullChanges(currentWorkspace.id);
notifications.show({
title: 'Success',
message: 'Successfully pulled latest changes',
@@ -22,13 +26,14 @@ export const useGitOperations = (gitEnabled) => {
});
return false;
}
}, [gitEnabled]);
}, [currentWorkspace, settings.gitEnabled]);
const handleCommitAndPush = useCallback(
async (message) => {
if (!gitEnabled) return false;
if (!currentWorkspace || !settings.gitEnabled) return false;
try {
await commitAndPush(message);
await commitAndPush(currentWorkspace.id, message);
notifications.show({
title: 'Success',
message: 'Successfully committed and pushed changes',
@@ -45,7 +50,7 @@ export const useGitOperations = (gitEnabled) => {
return false;
}
},
[gitEnabled]
[currentWorkspace, settings.gitEnabled]
);
return { handlePull, handleCommitAndPush };

View File

@@ -16,76 +16,176 @@ const apiCall = async (url, options = {}) => {
}
};
export const fetchFileList = async () => {
const response = await apiCall(`${API_BASE_URL}/files`);
export const fetchLastWorkspace = async () => {
const response = await apiCall(`${API_BASE_URL}/users/1/workspaces/last`);
return response.json();
};
export const fetchFileContent = async (filePath) => {
const response = await apiCall(`${API_BASE_URL}/files/${filePath}`);
export const fetchFileList = async (workspaceId) => {
const response = await apiCall(
`${API_BASE_URL}/users/1/workspaces/${workspaceId}/files`
);
return response.json();
};
export const fetchFileContent = async (workspaceId, filePath) => {
const response = await apiCall(
`${API_BASE_URL}/users/1/workspaces/${workspaceId}/files/${filePath}`
);
return response.text();
};
export const saveFileContent = async (filePath, content) => {
const response = await apiCall(`${API_BASE_URL}/files/${filePath}`, {
export const saveFileContent = async (workspaceId, filePath, content) => {
const response = await apiCall(
`${API_BASE_URL}/users/1/workspaces/${workspaceId}/files/${filePath}`,
{
method: 'POST',
headers: {
'Content-Type': 'text/plain',
},
body: content,
});
}
);
return response.text();
};
export const deleteFile = async (filePath) => {
const response = await apiCall(`${API_BASE_URL}/files/${filePath}`, {
export const deleteFile = async (workspaceId, filePath) => {
const response = await apiCall(
`${API_BASE_URL}/users/1/workspaces/${workspaceId}/files/${filePath}`,
{
method: 'DELETE',
});
}
);
return response.text();
};
export const fetchUserSettings = async (userId) => {
const response = await apiCall(`${API_BASE_URL}/settings?userId=${userId}`);
export const fetchWorkspaceSettings = async (workspaceId) => {
const response = await apiCall(
`${API_BASE_URL}/users/1/workspaces/${workspaceId}/settings`
);
return response.json();
};
export const saveUserSettings = async (settings) => {
const response = await apiCall(`${API_BASE_URL}/settings`, {
method: 'POST',
export const saveWorkspaceSettings = async (workspaceId, settings) => {
const response = await apiCall(
`${API_BASE_URL}/users/1/workspaces/${workspaceId}/settings`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(settings),
});
body: JSON.stringify({ settings }),
}
);
return response.json();
};
export const pullChanges = async () => {
const response = await apiCall(`${API_BASE_URL}/git/pull`, {
export const pullChanges = async (workspaceId) => {
const response = await apiCall(
`${API_BASE_URL}/users/1/workspaces/${workspaceId}/git/pull`,
{
method: 'POST',
});
}
);
return response.json();
};
export const commitAndPush = async (message) => {
const response = await apiCall(`${API_BASE_URL}/git/commit`, {
export const commitAndPush = async (workspaceId, message) => {
const response = await apiCall(
`${API_BASE_URL}/users/1/workspaces/${workspaceId}/git/commit`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ message }),
});
}
);
return response.json();
};
export const getFileUrl = (filePath) => {
return `${API_BASE_URL}/files/${filePath}`;
export const getFileUrl = (workspaceId, filePath) => {
return `${API_BASE_URL}/users/1/workspaces/${workspaceId}/files/${filePath}`;
};
export const lookupFileByName = async (filename) => {
export const lookupFileByName = async (workspaceId, filename) => {
const response = await apiCall(
`${API_BASE_URL}/files/lookup?filename=${encodeURIComponent(filename)}`
`${API_BASE_URL}/users/1/workspaces/${workspaceId}/files/lookup?filename=${encodeURIComponent(
filename
)}`
);
const data = await response.json();
return data.paths;
};
export const updateLastOpenedFile = async (workspaceId, filePath) => {
const response = await apiCall(
`${API_BASE_URL}/users/1/workspaces/${workspaceId}/files/last`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ filePath }),
}
);
return response.json();
};
export const getLastOpenedFile = async (workspaceId) => {
const response = await apiCall(
`${API_BASE_URL}/users/1/workspaces/${workspaceId}/files/last`
);
return response.json();
};
export const listWorkspaces = async () => {
const response = await apiCall(`${API_BASE_URL}/users/1/workspaces`);
return response.json();
};
export const createWorkspace = async (name) => {
const response = await apiCall(`${API_BASE_URL}/users/1/workspaces`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name }),
});
return response.json();
};
export const updateWorkspace = async (workspaceId, name) => {
const response = await apiCall(
`${API_BASE_URL}/users/1/workspaces/${workspaceId}`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name }),
}
);
return response.json();
};
export const deleteWorkspace = async (workspaceId) => {
const response = await apiCall(
`${API_BASE_URL}/users/1/workspaces/${workspaceId}`,
{
method: 'DELETE',
}
);
return response.json();
};
export const updateLastWorkspace = async (workspaceId) => {
const response = await apiCall(`${API_BASE_URL}/users/1/workspaces/last`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ workspaceId }),
});
return response.json();
};