Add git api calls on frontend

This commit is contained in:
2024-09-28 19:39:45 +02:00
parent 4290119b93
commit 5af07a9e35
6 changed files with 317 additions and 87 deletions

View File

@@ -9,6 +9,22 @@ import './App.scss';
function App() {
const [themeType, setThemeType] = useState('light');
const [userId, setUserId] = useState(1);
const [settings, setSettings] = useState({ gitEnabled: false });
useEffect(() => {
const loadUserSettings = async () => {
try {
const fetchedSettings = await fetchUserSettings(userId);
setSettings(fetchedSettings.settings);
setThemeType(fetchedSettings.settings.theme);
} catch (error) {
console.error('Failed to load user settings:', error);
}
};
loadUserSettings();
}, [userId]);
const {
content,
files,
@@ -19,20 +35,8 @@ function App() {
handleFileSelect,
handleContentChange,
handleSave,
} = useFileManagement();
useEffect(() => {
const loadUserSettings = async () => {
try {
const settings = await fetchUserSettings(userId);
setThemeType(settings.settings.theme);
} catch (error) {
console.error('Failed to load user settings:', error);
}
};
loadUserSettings();
}, [userId]);
pullLatestChanges,
} = useFileManagement(settings.gitEnabled);
const setTheme = (newTheme) => {
setThemeType(newTheme);
@@ -43,7 +47,7 @@ function App() {
<CssBaseline />
<Page>
<Header currentTheme={themeType} onThemeChange={setTheme} />
<Page.Content>
<Page.Content className='page-content'>
<MainContent
content={content}
files={files}
@@ -54,6 +58,8 @@ function App() {
onFileSelect={handleFileSelect}
onContentChange={handleContentChange}
onSave={handleSave}
settings={settings}
pullLatestChanges={pullLatestChanges}
/>
</Page.Content>
</Page>

View File

@@ -6,13 +6,25 @@ $navbar-height: 64px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 $padding;
height: $navbar-height;
}
.sidebar {
overflow-y: auto;
overflow: auto;
padding: $padding;
.file-tree-container {
width: 100%;
&>div {
padding: 0;
margin: 0;
}
}
}
.page-content {
padding: 0 $padding;
}
.main-content {
@@ -25,7 +37,7 @@ $navbar-height: 64px;
display: flex;
justify-content: space-between;
align-items: center;
padding: $padding;
padding: 0 $padding;
.breadcrumbs-container {
flex-grow: 1;
@@ -50,6 +62,7 @@ $navbar-height: 64px;
.setting-group {
margin-bottom: 1rem;
}
.setting-item {
display: flex;
justify-content: space-between;
@@ -64,7 +77,8 @@ $navbar-height: 64px;
flex-direction: column;
}
.editor-container, .markdown-preview {
.editor-container,
.markdown-preview {
flex-grow: 1;
overflow-y: auto;
padding: $padding;
@@ -72,7 +86,7 @@ $navbar-height: 64px;
.editor-container {
height: 100%;
.cm-editor {
height: 100%;
}
@@ -82,6 +96,10 @@ $navbar-height: 64px;
}
}
.tree {
padding-top: $padding;
}
// Geist UI Tree component customization
:global {
.file-tree {

View File

@@ -1,22 +1,28 @@
import React from 'react';
import { Tree } from '@geist-ui/core';
import { File, Folder } from '@geist-ui/icons';
import { Tree, Button, Tooltip, Spacer, ButtonGroup } from '@geist-ui/core';
import { File, Folder, GitPullRequest, GitCommit, Plus, Trash } from '@geist-ui/icons';
const FileTree = ({
files = [],
onFileSelect = () => {},
selectedFile = null
selectedFile = null,
gitEnabled = false,
gitAutoCommit = false,
onPull = () => {},
onCommitAndPush = () => {},
onCreateFile = () => {},
onDeleteFile = () => {}
}) => {
if (files.length === 0) {
return <div>No files to display</div>;
}
const handleSelect = (filePath) => {
onFileSelect(filePath);
onFileSelect(filePath);
};
const renderLabel = (node) => {
const path = getFilePath(node);
const path = node.extra;
return (
<span style={{ color: path === selectedFile ? '#0070f3' : 'inherit' }}>
{node.name}
@@ -27,12 +33,63 @@ const FileTree = ({
const renderIcon = ({ type }) => type === 'directory' ? <Folder /> : <File />;
return (
<Tree
value={files}
onClick={handleSelect}
renderIcon={renderIcon}
renderLabel={renderLabel}
/>
<div>
<ButtonGroup className='file-tree-buttons'>
<Tooltip text="Create new file" type="dark">
<Button
icon={<Plus />}
auto
scale={2/3}
onClick={onCreateFile}
px={0.6}
/>
</Tooltip>
<Spacer w={0.5} />
<Tooltip text={selectedFile ? "Delete current file" : "No file selected"} type="dark">
<Button
icon={<Trash />}
auto
scale={2/3}
onClick={onDeleteFile}
disabled={!selectedFile}
type="error"
px={0.6}
/>
</Tooltip>
<Spacer w={0.5} />
<Tooltip text={gitEnabled ? "Pull changes from remote" : "Git is not enabled"} type="dark">
<Button
icon={<GitPullRequest />}
auto
scale={2/3}
onClick={onPull}
disabled={!gitEnabled}
px={0.6}
/>
</Tooltip>
<Spacer w={0.5} />
<Tooltip text={
!gitEnabled ? "Git is not enabled" :
gitAutoCommit ? "Auto-commit is enabled" :
"Commit and push changes"
} type="dark">
<Button
icon={<GitCommit />}
auto
scale={2/3}
onClick={onCommitAndPush}
disabled={!gitEnabled || gitAutoCommit}
px={0.6}
/>
</Tooltip>
</ButtonGroup>
<Tree
value={files}
onClick={handleSelect}
renderIcon={renderIcon}
renderLabel={renderLabel}
/>
</div>
);
};

View File

@@ -1,9 +1,10 @@
import React, { useState } from 'react';
import { Grid, Breadcrumbs, Tabs, Dot, useTheme } from '@geist-ui/core';
import { Grid, Breadcrumbs, Tabs, Dot, useTheme, useToasts, Modal, Input, Button } from '@geist-ui/core';
import { Code, Eye } from '@geist-ui/icons';
import Editor from './Editor';
import FileTree from './FileTree';
import MarkdownPreview from './MarkdownPreview';
import { commitAndPush, saveFileContent, deleteFile } from '../services/api';
const MainContent = ({
content,
@@ -14,9 +15,71 @@ const MainContent = ({
onFileSelect,
onContentChange,
onSave,
settings,
pullLatestChanges,
}) => {
const [activeTab, setActiveTab] = useState('source');
const { type: themeType } = useTheme();
const { setToast } = useToasts();
const [newFileModalVisible, setNewFileModalVisible] = useState(false);
const [newFileName, setNewFileName] = useState('');
const handlePull = async () => {
try {
await pullLatestChanges();
setToast({ text: 'Successfully pulled latest changes', type: 'success' });
} catch (error) {
setToast({ text: 'Failed to pull changes: ' + error.message, type: 'error' });
}
};
const handleCommitAndPush = async () => {
try {
const message = prompt('Enter commit message:');
if (message) {
await commitAndPush(message);
setToast({ text: 'Changes committed and pushed successfully', type: 'success' });
await pullLatestChanges(); // Pull changes after successful push
}
} catch (error) {
setToast({ text: 'Failed to commit and push changes: ' + error.message, type: 'error' });
}
};
const handleCreateFile = () => {
setNewFileModalVisible(true);
};
const handleNewFileSubmit = async () => {
if (newFileName) {
try {
await saveFileContent(newFileName, '');
setToast({ text: 'New file created successfully', type: 'success' });
await pullLatestChanges(); // Refresh file list
onFileSelect(newFileName); // Select the new file
} catch (error) {
setToast({ text: 'Failed to create new file: ' + error.message, type: 'error' });
}
}
setNewFileModalVisible(false);
setNewFileName('');
};
const handleDeleteFile = async () => {
if (selectedFile) {
const confirmDelete = window.confirm(`Are you sure you want to delete "${selectedFile}"?`);
if (confirmDelete) {
try {
await deleteFile(selectedFile);
setToast({ text: 'File deleted successfully', type: 'success' });
await pullLatestChanges(); // Refresh file list
onFileSelect(null); // Deselect the file
} catch (error) {
setToast({ text: 'Failed to delete file: ' + error.message, type: 'error' });
}
}
}
};
const renderBreadcrumbs = () => {
if (!selectedFile) return null;
@@ -34,41 +97,60 @@ const MainContent = ({
};
return (
<Grid.Container gap={1} height="calc(100vh - 64px)">
<Grid xs={24} sm={6} md={5} lg={4} height="100%" className="sidebar">
{error ? (
<div className="error">{error}</div>
) : (
<FileTree
files={files}
onFileSelect={onFileSelect}
selectedFile={selectedFile}
/>
)}
</Grid>
<Grid xs={24} sm={18} md={19} lg={20} height="100%" className="main-content">
<div className="content-header">
{renderBreadcrumbs()}
<Tabs value={activeTab} onChange={setActiveTab}>
<Tabs.Item label={<Code />} value="source" />
<Tabs.Item label={<Eye />} value="preview" />
</Tabs>
</div>
<div className="content-body">
{activeTab === 'source' ? (
<Editor
content={content}
onChange={onContentChange}
onSave={onSave}
filePath={selectedFile}
themeType={themeType}
<>
<Grid.Container gap={1} height="calc(100vh - 64px)">
<Grid xs={24} sm={6} md={5} lg={4} height="100%" className="sidebar">
<div className="file-tree-container">
<FileTree
files={files}
onFileSelect={onFileSelect}
selectedFile={selectedFile}
gitEnabled={settings.gitEnabled}
gitAutoCommit={settings.gitAutoCommit}
onPull={handlePull}
onCommitAndPush={handleCommitAndPush}
onCreateFile={handleCreateFile}
onDeleteFile={handleDeleteFile}
/>
) : (
<MarkdownPreview content={content} />
)}
</div>
</Grid>
</Grid.Container>
</div>
</Grid>
<Grid xs={24} sm={18} md={19} lg={20} height="100%" className="main-content">
<div className="content-header">
{renderBreadcrumbs()}
<Tabs value={activeTab} onChange={setActiveTab}>
<Tabs.Item label={<Code />} value="source" />
<Tabs.Item label={<Eye />} value="preview" />
</Tabs>
</div>
<div className="content-body">
{activeTab === 'source' ? (
<Editor
content={content}
onChange={onContentChange}
onSave={onSave}
filePath={selectedFile}
themeType={themeType}
/>
) : (
<MarkdownPreview content={content} />
)}
</div>
</Grid>
</Grid.Container>
<Modal visible={newFileModalVisible} onClose={() => setNewFileModalVisible(false)}>
<Modal.Title>Create New File</Modal.Title>
<Modal.Content>
<Input
width="100%"
placeholder="Enter file name"
value={newFileName}
onChange={(e) => setNewFileName(e.target.value)}
/>
</Modal.Content>
<Modal.Action passive onClick={() => setNewFileModalVisible(false)}>Cancel</Modal.Action>
<Modal.Action onClick={handleNewFileSubmit}>Create</Modal.Action>
</Modal>
</>
);
};

View File

@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback } from 'react';
import { useToasts } from '@geist-ui/core';
import { fetchFileList, fetchFileContent, saveFileContent } from '../services/api';
import { fetchFileList, fetchFileContent, saveFileContent, pullChanges } from '../services/api';
const DEFAULT_FILE = {
name: 'New File.md',
@@ -8,7 +8,7 @@ const DEFAULT_FILE = {
content: '# Welcome to NovaMD\n\nStart editing here!'
};
const useFileManagement = () => {
const useFileManagement = (gitEnabled = false) => {
const [content, setContent] = useState(DEFAULT_FILE.content);
const [files, setFiles] = useState([]);
const [selectedFile, setSelectedFile] = useState(DEFAULT_FILE.path);
@@ -17,23 +17,41 @@ const useFileManagement = () => {
const [error, setError] = useState(null);
const { setToast } = useToasts();
useEffect(() => {
const loadFileList = async () => {
try {
const fileList = await fetchFileList();
if (Array.isArray(fileList)) {
setFiles(fileList);
} else {
throw new Error('File list is not an array');
}
} catch (error) {
console.error('Failed to load file list:', error);
setError('Failed to load file list. Please try again later.');
const loadFileList = useCallback(async () => {
try {
const fileList = await fetchFileList();
if (Array.isArray(fileList)) {
setFiles(fileList);
} else {
throw new Error('File list is not an array');
}
} catch (error) {
console.error('Failed to load file list:', error);
setError('Failed to load file list. Please try again later.');
}
}, []);
const pullLatestChanges = useCallback(async () => {
if (gitEnabled) {
try {
await pullChanges();
setToast({ text: 'Latest changes pulled successfully', type: 'success' });
await loadFileList(); // Reload file list after pulling changes
} catch (error) {
console.error('Failed to pull latest changes:', error);
setToast({ text: 'Failed to pull latest changes: ' + error.message, type: 'error' });
}
}
}, [gitEnabled, loadFileList, setToast]);
useEffect(() => {
const initializeFileSystem = async () => {
await pullLatestChanges();
await loadFileList();
};
loadFileList();
}, []);
initializeFileSystem();
}, [pullLatestChanges, loadFileList]);
const handleFileSelect = async (filePath) => {
if (hasUnsavedChanges) {
@@ -66,14 +84,13 @@ const useFileManagement = () => {
setIsNewFile(false);
setHasUnsavedChanges(false);
if (isNewFile) {
const updatedFileList = await fetchFileList();
setFiles(updatedFileList);
await loadFileList();
}
} catch (error) {
console.error('Error saving file:', error);
setToast({ text: 'Failed to save file. Please try again.', type: 'error' });
}
}, [setToast, isNewFile]);
}, [setToast, isNewFile, loadFileList]);
return {
content,
@@ -85,6 +102,7 @@ const useFileManagement = () => {
handleFileSelect,
handleContentChange,
handleSave,
pullLatestChanges,
};
};

View File

@@ -42,6 +42,21 @@ export const saveFileContent = async (filePath, content) => {
return await response.text();
};
export const deleteFile = async (filePath) => {
try {
const response = await fetch(`${API_BASE_URL}/files/${filePath}`, {
method: 'DELETE',
});
if (!response.ok) {
throw new Error('Failed to delete file');
}
return await response.text();
} catch (error) {
console.error('Error deleting file:', error);
throw error;
}
};
export const fetchUserSettings = async (userId) => {
try {
const response = await fetch(`${API_BASE_URL}/settings?userId=${userId}`);
@@ -75,4 +90,38 @@ export const saveUserSettings = async (settings) => {
console.error('Error saving user settings:', error);
throw error;
}
};
};
export const pullChanges = async () => {
try {
const response = await fetch(`${API_BASE_URL}/git/pull`, {
method: 'POST',
});
if (!response.ok) {
throw new Error('Failed to pull changes');
}
return await response.json();
} catch (error) {
console.error('Error pulling changes:', error);
throw error;
}
};
export const commitAndPush = async (message) => {
try {
const response = await fetch(`${API_BASE_URL}/git/commit`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ message }),
});
if (!response.ok) {
throw new Error('Failed to commit and push changes');
}
return await response.json();
} catch (error) {
console.error('Error committing and pushing changes:', error);
throw error;
}
};