mirror of
https://github.com/lordmathis/lemma.git
synced 2025-11-05 23:44:22 +00:00
Add rename file functionality with modal support
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
|||||||
IconGitPullRequest,
|
IconGitPullRequest,
|
||||||
IconGitCommit,
|
IconGitCommit,
|
||||||
IconUpload,
|
IconUpload,
|
||||||
|
IconEdit,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { useModalContext } from '../../contexts/ModalContext';
|
import { useModalContext } from '../../contexts/ModalContext';
|
||||||
import { useWorkspace } from '../../hooks/useWorkspace';
|
import { useWorkspace } from '../../hooks/useWorkspace';
|
||||||
@@ -27,6 +28,7 @@ const FileActions: React.FC<FileActionsProps> = ({
|
|||||||
setNewFileModalVisible,
|
setNewFileModalVisible,
|
||||||
setDeleteFileModalVisible,
|
setDeleteFileModalVisible,
|
||||||
setCommitMessageModalVisible,
|
setCommitMessageModalVisible,
|
||||||
|
setRenameFileModalVisible,
|
||||||
} = useModalContext();
|
} = useModalContext();
|
||||||
|
|
||||||
const { handleUpload } = useFileOperations();
|
const { handleUpload } = useFileOperations();
|
||||||
@@ -36,6 +38,7 @@ const FileActions: React.FC<FileActionsProps> = ({
|
|||||||
|
|
||||||
const handleCreateFile = (): void => setNewFileModalVisible(true);
|
const handleCreateFile = (): void => setNewFileModalVisible(true);
|
||||||
const handleDeleteFile = (): void => setDeleteFileModalVisible(true);
|
const handleDeleteFile = (): void => setDeleteFileModalVisible(true);
|
||||||
|
const handleRenameFile = (): void => setRenameFileModalVisible(true);
|
||||||
const handleCommitAndPush = (): void => setCommitMessageModalVisible(true);
|
const handleCommitAndPush = (): void => setCommitMessageModalVisible(true);
|
||||||
|
|
||||||
const handleUploadClick = (): void => {
|
const handleUploadClick = (): void => {
|
||||||
@@ -91,6 +94,21 @@ const FileActions: React.FC<FileActionsProps> = ({
|
|||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip
|
||||||
|
label={selectedFile ? 'Rename current file' : 'No file selected'}
|
||||||
|
>
|
||||||
|
<ActionIcon
|
||||||
|
variant="default"
|
||||||
|
size="md"
|
||||||
|
onClick={handleRenameFile}
|
||||||
|
disabled={!selectedFile}
|
||||||
|
aria-label="Rename current file"
|
||||||
|
data-testid="rename-file-button"
|
||||||
|
>
|
||||||
|
<IconEdit size={16} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
label={selectedFile ? 'Delete current file' : 'No file selected'}
|
label={selectedFile ? 'Delete current file' : 'No file selected'}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ import { IconCode, IconEye, IconPointFilled } from '@tabler/icons-react';
|
|||||||
import ContentView from '../editor/ContentView';
|
import ContentView from '../editor/ContentView';
|
||||||
import CreateFileModal from '../modals/file/CreateFileModal';
|
import CreateFileModal from '../modals/file/CreateFileModal';
|
||||||
import DeleteFileModal from '../modals/file/DeleteFileModal';
|
import DeleteFileModal from '../modals/file/DeleteFileModal';
|
||||||
|
import RenameFileModal from '../modals/file/RenameFileModal';
|
||||||
import CommitMessageModal from '../modals/git/CommitMessageModal';
|
import CommitMessageModal from '../modals/git/CommitMessageModal';
|
||||||
|
|
||||||
import { useFileContent } from '../../hooks/useFileContent';
|
import { useFileContent } from '../../hooks/useFileContent';
|
||||||
import { useFileOperations } from '../../hooks/useFileOperations';
|
import { useFileOperations } from '../../hooks/useFileOperations';
|
||||||
import { useGitOperations } from '../../hooks/useGitOperations';
|
import { useGitOperations } from '../../hooks/useGitOperations';
|
||||||
|
import { useModalContext } from '../../contexts/ModalContext';
|
||||||
|
|
||||||
type ViewTab = 'source' | 'preview';
|
type ViewTab = 'source' | 'preview';
|
||||||
|
|
||||||
@@ -31,8 +33,10 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
setHasUnsavedChanges,
|
setHasUnsavedChanges,
|
||||||
handleContentChange,
|
handleContentChange,
|
||||||
} = useFileContent(selectedFile);
|
} = useFileContent(selectedFile);
|
||||||
const { handleSave, handleCreate, handleDelete } = useFileOperations();
|
const { handleSave, handleCreate, handleDelete, handleRename } =
|
||||||
|
useFileOperations();
|
||||||
const { handleCommitAndPush } = useGitOperations();
|
const { handleCommitAndPush } = useGitOperations();
|
||||||
|
const { setRenameFileModalVisible } = useModalContext();
|
||||||
|
|
||||||
const handleTabChange = useCallback((value: string | null): void => {
|
const handleTabChange = useCallback((value: string | null): void => {
|
||||||
if (value) {
|
if (value) {
|
||||||
@@ -73,14 +77,50 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
[handleDelete, handleFileSelect, loadFileList]
|
[handleDelete, handleFileSelect, loadFileList]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleRenameFile = useCallback(
|
||||||
|
async (oldPath: string, newPath: string): Promise<void> => {
|
||||||
|
const success = await handleRename(oldPath, newPath);
|
||||||
|
if (success) {
|
||||||
|
await loadFileList();
|
||||||
|
// If we renamed the currently selected file, update the selection
|
||||||
|
if (selectedFile === oldPath) {
|
||||||
|
await handleFileSelect(newPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[handleRename, handleFileSelect, loadFileList, selectedFile]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleBreadcrumbClick = useCallback(() => {
|
||||||
|
if (selectedFile) {
|
||||||
|
setRenameFileModalVisible(true);
|
||||||
|
}
|
||||||
|
}, [selectedFile, setRenameFileModalVisible]);
|
||||||
|
|
||||||
const renderBreadcrumbs = useMemo(() => {
|
const renderBreadcrumbs = useMemo(() => {
|
||||||
if (!selectedFile) return null;
|
if (!selectedFile) return null;
|
||||||
const pathParts = selectedFile.split('/');
|
const pathParts = selectedFile.split('/');
|
||||||
const items = pathParts.map((part, index) => (
|
const items = pathParts.map((part, index) => {
|
||||||
<Text key={index} size="sm">
|
// Make the filename (last part) clickable for rename
|
||||||
{part}
|
const isFileName = index === pathParts.length - 1;
|
||||||
</Text>
|
return (
|
||||||
));
|
<Text
|
||||||
|
key={index}
|
||||||
|
size="sm"
|
||||||
|
style={{
|
||||||
|
cursor: isFileName ? 'pointer' : 'default',
|
||||||
|
...(isFileName && {
|
||||||
|
textDecoration: 'underline',
|
||||||
|
textDecorationStyle: 'dotted',
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
onClick={isFileName ? handleBreadcrumbClick : undefined}
|
||||||
|
title={isFileName ? 'Click to rename file' : undefined}
|
||||||
|
>
|
||||||
|
{part}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group>
|
<Group>
|
||||||
@@ -93,7 +133,7 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
)}
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
}, [selectedFile, hasUnsavedChanges]);
|
}, [selectedFile, hasUnsavedChanges, handleBreadcrumbClick]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -128,6 +168,10 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
onDeleteFile={handleDeleteFile}
|
onDeleteFile={handleDeleteFile}
|
||||||
selectedFile={selectedFile}
|
selectedFile={selectedFile}
|
||||||
/>
|
/>
|
||||||
|
<RenameFileModal
|
||||||
|
onRenameFile={handleRenameFile}
|
||||||
|
selectedFile={selectedFile}
|
||||||
|
/>
|
||||||
<CommitMessageModal onCommitAndPush={handleCommitAndPush} />
|
<CommitMessageModal onCommitAndPush={handleCommitAndPush} />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
109
app/src/components/modals/file/RenameFileModal.tsx
Normal file
109
app/src/components/modals/file/RenameFileModal.tsx
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Modal, TextInput, Button, Group, Box } from '@mantine/core';
|
||||||
|
import { useModalContext } from '../../../contexts/ModalContext';
|
||||||
|
|
||||||
|
interface RenameFileModalProps {
|
||||||
|
onRenameFile: (oldPath: string, newPath: string) => Promise<void>;
|
||||||
|
selectedFile: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RenameFileModal: React.FC<RenameFileModalProps> = ({
|
||||||
|
onRenameFile,
|
||||||
|
selectedFile,
|
||||||
|
}) => {
|
||||||
|
const [newFileName, setNewFileName] = useState<string>('');
|
||||||
|
const { renameFileModalVisible, setRenameFileModalVisible } =
|
||||||
|
useModalContext();
|
||||||
|
|
||||||
|
// Extract just the filename from the full path for editing
|
||||||
|
const getCurrentFileName = (filePath: string | null): string => {
|
||||||
|
if (!filePath) return '';
|
||||||
|
const parts = filePath.split('/');
|
||||||
|
return parts[parts.length - 1] || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the directory path (everything except the filename)
|
||||||
|
const getDirectoryPath = (filePath: string | null): string => {
|
||||||
|
if (!filePath) return '';
|
||||||
|
const parts = filePath.split('/');
|
||||||
|
return parts.slice(0, -1).join('/');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set the current filename when modal opens or selectedFile changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (renameFileModalVisible && selectedFile) {
|
||||||
|
setNewFileName(getCurrentFileName(selectedFile));
|
||||||
|
}
|
||||||
|
}, [renameFileModalVisible, selectedFile]);
|
||||||
|
|
||||||
|
const handleSubmit = async (): Promise<void> => {
|
||||||
|
if (newFileName && selectedFile) {
|
||||||
|
const directoryPath = getDirectoryPath(selectedFile);
|
||||||
|
const newPath = directoryPath
|
||||||
|
? `${directoryPath}/${newFileName.trim()}`
|
||||||
|
: newFileName.trim();
|
||||||
|
|
||||||
|
await onRenameFile(selectedFile, newPath);
|
||||||
|
setNewFileName('');
|
||||||
|
setRenameFileModalVisible(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (event: React.KeyboardEvent): void => {
|
||||||
|
if (event.key === 'Enter' && !event.shiftKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
void handleSubmit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = (): void => {
|
||||||
|
setNewFileName('');
|
||||||
|
setRenameFileModalVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
opened={renameFileModalVisible}
|
||||||
|
onClose={handleClose}
|
||||||
|
title="Rename File"
|
||||||
|
centered
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<Box maw={400} mx="auto">
|
||||||
|
<TextInput
|
||||||
|
label="File Name"
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter new file name"
|
||||||
|
data-testid="rename-file-input"
|
||||||
|
value={newFileName}
|
||||||
|
onChange={(event) => setNewFileName(event.currentTarget.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
mb="md"
|
||||||
|
w="100%"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<Group justify="flex-end" mt="xl">
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
onClick={handleClose}
|
||||||
|
data-testid="cancel-rename-file-button"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => void handleSubmit()}
|
||||||
|
data-testid="confirm-rename-file-button"
|
||||||
|
disabled={
|
||||||
|
!newFileName.trim() ||
|
||||||
|
newFileName.trim() === getCurrentFileName(selectedFile)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Rename
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RenameFileModal;
|
||||||
@@ -10,6 +10,8 @@ interface ModalContextType {
|
|||||||
setNewFileModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
setNewFileModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
deleteFileModalVisible: boolean;
|
deleteFileModalVisible: boolean;
|
||||||
setDeleteFileModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
setDeleteFileModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
renameFileModalVisible: boolean;
|
||||||
|
setRenameFileModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
commitMessageModalVisible: boolean;
|
commitMessageModalVisible: boolean;
|
||||||
setCommitMessageModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
setCommitMessageModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
settingsModalVisible: boolean;
|
settingsModalVisible: boolean;
|
||||||
@@ -30,6 +32,7 @@ interface ModalProviderProps {
|
|||||||
export const ModalProvider: React.FC<ModalProviderProps> = ({ children }) => {
|
export const ModalProvider: React.FC<ModalProviderProps> = ({ children }) => {
|
||||||
const [newFileModalVisible, setNewFileModalVisible] = useState(false);
|
const [newFileModalVisible, setNewFileModalVisible] = useState(false);
|
||||||
const [deleteFileModalVisible, setDeleteFileModalVisible] = useState(false);
|
const [deleteFileModalVisible, setDeleteFileModalVisible] = useState(false);
|
||||||
|
const [renameFileModalVisible, setRenameFileModalVisible] = useState(false);
|
||||||
const [commitMessageModalVisible, setCommitMessageModalVisible] =
|
const [commitMessageModalVisible, setCommitMessageModalVisible] =
|
||||||
useState(false);
|
useState(false);
|
||||||
const [settingsModalVisible, setSettingsModalVisible] = useState(false);
|
const [settingsModalVisible, setSettingsModalVisible] = useState(false);
|
||||||
@@ -43,6 +46,8 @@ export const ModalProvider: React.FC<ModalProviderProps> = ({ children }) => {
|
|||||||
setNewFileModalVisible,
|
setNewFileModalVisible,
|
||||||
deleteFileModalVisible,
|
deleteFileModalVisible,
|
||||||
setDeleteFileModalVisible,
|
setDeleteFileModalVisible,
|
||||||
|
renameFileModalVisible,
|
||||||
|
setRenameFileModalVisible,
|
||||||
commitMessageModalVisible,
|
commitMessageModalVisible,
|
||||||
setCommitMessageModalVisible,
|
setCommitMessageModalVisible,
|
||||||
settingsModalVisible,
|
settingsModalVisible,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ interface UseFileOperationsResult {
|
|||||||
parentId: string | null,
|
parentId: string | null,
|
||||||
index: number
|
index: number
|
||||||
) => Promise<boolean>;
|
) => Promise<boolean>;
|
||||||
|
handleRename: (oldPath: string, newPath: string) => Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useFileOperations = (): UseFileOperationsResult => {
|
export const useFileOperations = (): UseFileOperationsResult => {
|
||||||
@@ -193,5 +194,40 @@ export const useFileOperations = (): UseFileOperationsResult => {
|
|||||||
[currentWorkspace, autoCommit]
|
[currentWorkspace, autoCommit]
|
||||||
);
|
);
|
||||||
|
|
||||||
return { handleSave, handleDelete, handleCreate, handleUpload, handleMove };
|
const handleRename = useCallback(
|
||||||
|
async (oldPath: string, newPath: string): Promise<boolean> => {
|
||||||
|
if (!currentWorkspace) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// TODO: Replace with your actual rename API call
|
||||||
|
// await renameFile(currentWorkspace.name, oldPath, newPath);
|
||||||
|
|
||||||
|
notifications.show({
|
||||||
|
title: 'Success',
|
||||||
|
message: 'File renamed successfully',
|
||||||
|
color: 'green',
|
||||||
|
});
|
||||||
|
await autoCommit(newPath, FileAction.Update);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error renaming file:', error);
|
||||||
|
notifications.show({
|
||||||
|
title: 'Error',
|
||||||
|
message: 'Failed to rename file',
|
||||||
|
color: 'red',
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[currentWorkspace, autoCommit]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleSave,
|
||||||
|
handleDelete,
|
||||||
|
handleCreate,
|
||||||
|
handleUpload,
|
||||||
|
handleMove,
|
||||||
|
handleRename,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user