Implement workspace switching

This commit is contained in:
2024-10-26 23:15:23 +02:00
parent fd313c1d7f
commit 12312137b7
8 changed files with 189 additions and 91 deletions

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { Group, Text, Avatar } from '@mantine/core';
import WorkspaceMenu from './WorkspaceMenu';
import WorkspaceSwitcher from './WorkspaceSwitcher';
import Settings from './Settings';
const Header = () => {
@@ -10,8 +10,8 @@ const Header = () => {
NovaMD
</Text>
<Group>
<WorkspaceSwitcher />
<Avatar src="https://via.placeholder.com/40" radius="xl" />
<WorkspaceMenu />
</Group>
<Settings />
</Group>

View File

@@ -1,77 +0,0 @@
import React from 'react';
import { Menu, ActionIcon, Text } from '@mantine/core';
import {
IconFolders,
IconFolderPlus,
IconSwitchHorizontal,
IconSettings,
IconTrash,
IconPencil,
IconFileExport,
IconFileImport,
} from '@tabler/icons-react';
import { useModalContext } from '../contexts/ModalContext';
import { useWorkspace } from '../contexts/WorkspaceContext';
const WorkspaceMenu = () => {
const { setSettingsModalVisible } = useModalContext();
const { currentWorkspace } = useWorkspace();
const openSettings = () => setSettingsModalVisible(true);
return (
<Menu shadow="md" width={200} position="bottom-end">
<Menu.Target>
<ActionIcon variant="subtle" size="lg">
<IconFolders size={24} />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<Menu.Label>Current Workspace</Menu.Label>
<Menu.Item disabled>
<Text size="sm" truncate>
{currentWorkspace?.name || 'No workspace selected'}
</Text>
</Menu.Item>
<Menu.Divider />
<Menu.Label>Workspace Actions</Menu.Label>
<Menu.Item leftSection={<IconFolderPlus size={14} />}>
Create Workspace
</Menu.Item>
<Menu.Item leftSection={<IconSwitchHorizontal size={14} />}>
Switch Workspace
</Menu.Item>
<Menu.Item leftSection={<IconPencil size={14} />}>
Rename Workspace
</Menu.Item>
<Menu.Item leftSection={<IconTrash size={14} />} color="red">
Delete Workspace
</Menu.Item>
<Menu.Divider />
<Menu.Label>Data Management</Menu.Label>
<Menu.Item leftSection={<IconFileExport size={14} />}>
Export Workspace
</Menu.Item>
<Menu.Item leftSection={<IconFileImport size={14} />}>
Import Workspace
</Menu.Item>
<Menu.Divider />
<Menu.Item
leftSection={<IconSettings size={14} />}
onClick={openSettings}
>
Workspace Settings
</Menu.Item>
</Menu.Dropdown>
</Menu>
);
};
export default WorkspaceMenu;

View File

@@ -0,0 +1,136 @@
import React, { useState } from 'react';
import {
Box,
Popover,
Stack,
Paper,
ScrollArea,
Group,
UnstyledButton,
Text,
Loader,
Center,
Button,
ActionIcon,
} from '@mantine/core';
import { IconFolders, IconSettings, IconFolderPlus } from '@tabler/icons-react';
import { useWorkspace } from '../contexts/WorkspaceContext';
import { useModalContext } from '../contexts/ModalContext';
import { listWorkspaces } from '../services/api';
const WorkspaceSwitcher = () => {
const { currentWorkspace, switchWorkspace } = useWorkspace();
const { setSettingsModalVisible } = useModalContext();
const [workspaces, setWorkspaces] = useState([]);
const [loading, setLoading] = useState(false);
const [popoverOpened, setPopoverOpened] = useState(false);
const loadWorkspaces = async () => {
setLoading(true);
try {
const list = await listWorkspaces();
setWorkspaces(list);
} catch (error) {
console.error('Failed to load workspaces:', error);
}
setLoading(false);
};
return (
<Popover
width={300}
position="bottom-start"
shadow="md"
opened={popoverOpened}
onChange={setPopoverOpened}
>
<Popover.Target>
<UnstyledButton
onClick={() => {
setPopoverOpened((o) => !o);
if (!popoverOpened) {
loadWorkspaces();
}
}}
>
<Group gap="xs">
<IconFolders size={20} />
<div>
<Text size="sm" fw={500}>
{currentWorkspace?.name || 'No workspace'}
</Text>
</div>
</Group>
</UnstyledButton>
</Popover.Target>
<Popover.Dropdown>
<Text size="sm" fw={600} mb="md">
Switch Workspace
</Text>
<ScrollArea.Autosize mah={400} mb="md" offsetScrollbars>
<Stack gap="xs">
{loading ? (
<Center p="md">
<Loader size="sm" />
</Center>
) : (
workspaces.map((workspace) => (
<UnstyledButton
key={workspace.id}
onClick={() => {
switchWorkspace(workspace.id);
setPopoverOpened(false);
}}
>
<Paper
p="xs"
withBorder={workspace.id === currentWorkspace?.id}
bg={
workspace.id === currentWorkspace?.id
? 'var(--mantine-color-blue-light)'
: undefined
}
>
<Group justify="space-between" wrap="nowrap">
<Box>
<Text size="sm" fw={500} truncate>
{workspace.name}
</Text>
<Text size="xs" c="dimmed">
{new Date(workspace.createdAt).toLocaleDateString()}
</Text>
</Box>
{workspace.id === currentWorkspace?.id && (
<ActionIcon
variant="subtle"
size="sm"
onClick={(e) => {
e.stopPropagation();
setSettingsModalVisible(true);
setPopoverOpened(false);
}}
>
<IconSettings size={14} />
</ActionIcon>
)}
</Group>
</Paper>
</UnstyledButton>
))
)}
</Stack>
</ScrollArea.Autosize>
<Button
variant="light"
leftSection={<IconFolderPlus size={14} />}
fullWidth
>
Create Workspace
</Button>
</Popover.Dropdown>
</Popover>
);
};
export default WorkspaceSwitcher;

View File

@@ -8,6 +8,8 @@ export const ModalProvider = ({ children }) => {
const [commitMessageModalVisible, setCommitMessageModalVisible] =
useState(false);
const [settingsModalVisible, setSettingsModalVisible] = useState(false);
const [switchWorkspaceModalVisible, setSwitchWorkspaceModalVisible] =
useState(false);
const value = {
newFileModalVisible,
@@ -18,6 +20,8 @@ export const ModalProvider = ({ children }) => {
setCommitMessageModalVisible,
settingsModalVisible,
setSettingsModalVisible,
switchWorkspaceModalVisible,
setSwitchWorkspaceModalVisible,
};
return (

View File

@@ -6,6 +6,7 @@ import React, {
useCallback,
} from 'react';
import { useMantineColorScheme } from '@mantine/core';
import { notifications } from '@mantine/notifications';
import {
fetchLastWorkspaceId,
fetchWorkspaceSettings,
@@ -22,18 +23,29 @@ export const WorkspaceProvider = ({ children }) => {
const [loading, setLoading] = useState(true);
const { colorScheme, setColorScheme } = useMantineColorScheme();
const loadWorkspaceData = useCallback(async (workspaceId) => {
try {
const workspace = await getWorkspace(workspaceId);
setCurrentWorkspace(workspace);
const workspaceSettings = await fetchWorkspaceSettings(workspaceId);
setSettings(workspaceSettings.settings);
setColorScheme(workspaceSettings.settings.theme);
} catch (error) {
console.error('Failed to load workspace data:', error);
notifications.show({
title: 'Error',
message: 'Failed to load workspace data',
color: 'red',
});
}
}, []);
useEffect(() => {
const loadWorkspace = async () => {
const initializeWorkspace = async () => {
try {
const { lastWorkspaceId } = await fetchLastWorkspaceId();
if (lastWorkspaceId) {
const workspace = await getWorkspace(lastWorkspaceId);
setCurrentWorkspace(workspace);
const workspaceSettings = await fetchWorkspaceSettings(
lastWorkspaceId
);
setSettings(workspaceSettings.settings);
setColorScheme(workspaceSettings.settings.theme);
await loadWorkspaceData(lastWorkspaceId);
} else {
console.warn('No last workspace found');
}
@@ -44,7 +56,29 @@ export const WorkspaceProvider = ({ children }) => {
}
};
loadWorkspace();
initializeWorkspace();
}, []);
const switchWorkspace = useCallback(async (workspaceId) => {
try {
setLoading(true);
await updateLastWorkspace(workspaceId);
await loadWorkspaceData(workspaceId);
notifications.show({
title: 'Success',
message: 'Workspace switched successfully',
color: 'green',
});
} catch (error) {
console.error('Failed to switch workspace:', error);
notifications.show({
title: 'Error',
message: 'Failed to switch workspace',
color: 'red',
});
} finally {
setLoading(false);
}
}, []);
const updateSettings = useCallback(
@@ -78,6 +112,7 @@ export const WorkspaceProvider = ({ children }) => {
loading,
colorScheme,
updateColorScheme,
switchWorkspace,
};
return (

View File

@@ -20,7 +20,7 @@ export const useFileList = () => {
console.error('Failed to load file list:', error);
setFiles([]);
}
}, [currentWorkspace, workspaceLoading]);
}, [currentWorkspace]);
return { files, loadFileList };
};

View File

@@ -38,7 +38,7 @@ export const useFileNavigation = () => {
});
}
},
[currentWorkspace, handleFileSelect]
[currentWorkspace]
);
return { handleLinkClick, selectedFile, isNewFile, handleFileSelect };

View File

@@ -21,7 +21,7 @@ export const useFileOperations = () => {
await handleCommitAndPush(commitMessage);
}
},
[settings, handleCommitAndPush]
[settings]
);
const handleSave = useCallback(