Skip to content

Commit

Permalink
feat: implement file tabs management
Browse files Browse the repository at this point in the history
  • Loading branch information
rahulyadav-57 committed Aug 30, 2024
1 parent 278b2cc commit edbd3e9
Show file tree
Hide file tree
Showing 9 changed files with 203 additions and 65 deletions.
40 changes: 18 additions & 22 deletions src/components/workspace/Tabs/Tabs.tsx
Original file line number Diff line number Diff line change
@@ -1,52 +1,48 @@
import AppIcon from '@/components/ui/icon';
import { useWorkspaceActions } from '@/hooks/workspace.hooks';
import { Tree } from '@/interfaces/workspace.interface';
import { useFileTab } from '@/hooks';
import { useProject } from '@/hooks/projectV2.hooks';
import { fileTypeFromFileName } from '@/utility/utils';
import { FC } from 'react';
import { FC, useEffect } from 'react';
import s from './Tabs.module.scss';

interface Props {
projectId: string;
}
const Tabs: FC = () => {
const { fileTab, open, close, syncTabSettings } = useFileTab();
const { activeProject } = useProject();

const Tabs: FC<Props> = ({ projectId }) => {
const { openedFiles, openFile, closeFile } = useWorkspaceActions();
const openedFilesList = openedFiles(projectId);

const updateActiveTab = (node: Tree) => {
openFile(node.id, projectId);
};

const closeTab = (e: React.MouseEvent, id: string) => {
const closeTab = (e: React.MouseEvent, filePath: string) => {
e.preventDefault();
e.stopPropagation();
closeFile(id, projectId);
close(filePath);
};

if (openedFilesList.length === 0) {
useEffect(() => {
syncTabSettings();
}, [activeProject]);

if (fileTab.items.length === 0) {
return <></>;
}
return (
<div className={s.container}>
<div className={s.tabList}>
{openedFilesList.map((item) => (
{fileTab.items.map((item) => (
<div
onClick={() => {
updateActiveTab(item);
open(item.name, item.path);
}}
className={`${s.item}
file-icon
${item.name.split('.').pop()}-lang-file-icon
${fileTypeFromFileName(item.name)}-lang-file-icon
${item.isOpen ? s.isActive : ''}
${item.path === fileTab.active ? s.isActive : ''}
`}
key={item.id}
key={item.path}
>
{item.name}
<span
className={`${s.close} ${item.isDirty ? s.isDirty : ''}`}
onClick={(e) => {
closeTab(e, item.id);
closeTab(e, item.path);
}}
>
<span className={s.fileDirtyIcon}></span>
Expand Down
2 changes: 1 addition & 1 deletion src/components/workspace/WorkSpace/WorkSpace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ const WorkSpace: FC = () => {
>
<div>
<div className={s.tabsWrapper}>
<Tabs projectId={activeProject as string} />
<Tabs />
</div>

<div style={{ height: 'calc(100% - 43px)' }}>
Expand Down
14 changes: 9 additions & 5 deletions src/components/workspace/project/ManageProject/ManageProject.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,13 @@ import s from './ManageProject.module.scss';
const ManageProject: FC = () => {
const [isDeleteConfirmOpen, setIsDeleteConfirmOpen] = useState(false);

const { projects, setActiveProject, deleteProject, activeProject } =
useProject();
const {
projects,
setActiveProject,
deleteProject,
activeProject,
loadProjectFiles,
} = useProject();

const projectHeader = () => (
<>
Expand Down Expand Up @@ -85,14 +90,13 @@ const ManageProject: FC = () => {
}
};

const openProject = async (id: Project['id']) => {
if (!id) return;
const selectedProject = id as string;
const openProject = async (selectedProject: Project['id']) => {
if (!selectedProject) {
await message.error('Project not found');
return;
}
setActiveProject(selectedProject);
await loadProjectFiles(selectedProject);
};

useEffect(() => {
Expand Down
10 changes: 4 additions & 6 deletions src/components/workspace/tree/FileTree/TreeNode.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { useFileTab } from '@/hooks';
import { useLogActivity } from '@/hooks/logActivity.hooks';
import { useProject } from '@/hooks/projectV2.hooks';
import { useWorkspaceActions } from '@/hooks/workspace.hooks';
import { Project, Tree } from '@/interfaces/workspace.interface';
import { fileTypeFromFileName } from '@/utility/utils';
import { NodeModel } from '@minoru/react-dnd-treeview';
import cn from 'clsx';
import { useRouter } from 'next/router';
import { FC, useState } from 'react';
import s from './FileTree.module.scss';
import ItemAction, { actionsTypes } from './ItemActions';
Expand All @@ -30,11 +30,9 @@ const TreeNode: FC<Props> = ({ node, depth, isOpen, onToggle }) => {
const [isEditing, setIsEditing] = useState(false);
const [newItemAdd, setNewItemAdd] = useState<Tree['type'] | ''>('');

const router = useRouter();
const { id: projectId } = router.query;

const { openFile, isProjectEditable } = useWorkspaceActions();
const { isProjectEditable } = useWorkspaceActions();
const { deleteProjectFile, renameProjectFile, newFileFolder } = useProject();
const { open: openTab } = useFileTab();
const { createLog } = useLogActivity();

const disallowedFile = [
Expand All @@ -48,7 +46,7 @@ const TreeNode: FC<Props> = ({ node, depth, isOpen, onToggle }) => {
e.stopPropagation();
onToggle(node.id);
if (!node.droppable) {
openFile(node.id as string, projectId as string);
openTab(node.text, node.data?.path as string);
}
};

Expand Down
112 changes: 112 additions & 0 deletions src/hooks/fileTabs.hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import fileSystem from '@/lib/fs';
import { IDEContext, IFileTab } from '@/state/IDE.context';
import { useContext } from 'react';

const useFileTab = () => {
const { fileTab, setFileTab, activeProject } = useContext(IDEContext);

const syncTabSettings = async (updatedTab?: IFileTab) => {
if (!activeProject) return;

const defaultSetting = {
tab: {
items: [],
active: null,
},
};

try {
const settingPath = `/${activeProject}/.ide/setting.json`;
if (!(await fileSystem.exists(settingPath))) {
await fileSystem.writeFile(
settingPath,
JSON.stringify(defaultSetting, null, 2),
{
overwrite: true,
},
);
}
const setting = (await fileSystem.readFile(settingPath)) as string;

let parsedSetting = setting ? JSON.parse(setting) : defaultSetting;

if (updatedTab) {
parsedSetting.tab = updatedTab;
} else {
parsedSetting = {
...defaultSetting,
...parsedSetting,
};
setFileTab(parsedSetting.tab);
}

await fileSystem.writeFile(
settingPath,
JSON.stringify(parsedSetting, null, 2),
{
overwrite: true,
},
);
} catch (error) {
console.error('Error syncing tab settings:', error);
}
};

const open = (name: string, path: string) => {
if (fileTab.active === name) return;

const existingTab = fileTab.items.find((item) => item.path === path);

if (existingTab) {
const updatedTab = { ...fileTab, active: path };
setFileTab(updatedTab);
syncTabSettings(updatedTab);
} else {
const newTab = { name, path, isDirty: false };
const updatedTab = {
...fileTab,
items: [...fileTab.items, newTab],
active: path,
};
setFileTab(updatedTab);
syncTabSettings(updatedTab);
}
};

const close = (filePath: string, closeAll: boolean = false) => {
let updatedTab: IFileTab;

if (closeAll) {
updatedTab = { items: [], active: null };
} else {
const updatedItems = fileTab.items.filter(
(item) => item.path !== filePath,
);

let newActiveTab = fileTab.active;
if (fileTab.active === filePath) {
const closedTabIndex = fileTab.items.findIndex(
(item) => item.path === filePath,
);
if (updatedItems.length > 0) {
if (closedTabIndex > 0) {
newActiveTab = updatedItems[closedTabIndex - 1].path;
} else {
newActiveTab = updatedItems[0].path;
}
} else {
newActiveTab = null; // No more tabs open
}
}

updatedTab = { items: updatedItems, active: newActiveTab };
}

setFileTab(updatedTab);
syncTabSettings(updatedTab);
};

return { fileTab, open, close, syncTabSettings };
};

export default useFileTab;
1 change: 1 addition & 0 deletions src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as useFileTab } from './fileTabs.hooks';
22 changes: 11 additions & 11 deletions src/hooks/projectV2.hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,6 @@ export const useProject = () => {
} = useContext(IDEContext);
const baseProjectPath = '/';

useEffect(() => {
if (activeProject) {
loadProjectFiles(activeProject).catch(() => {});
}
}, [activeProject]);

useEffect(() => {
loadProjects().catch(() => {});
}, []);

const loadProjects = async () => {
const projectCollection = await fileSystem.readdir(baseProjectPath, {
onlyDir: true,
Expand Down Expand Up @@ -207,6 +197,15 @@ export const useProject = () => {
await loadProjectFiles(activeProject);
};

const updateActiveProject = (projectName: string | null) => {
if (activeProject === projectName) return;
setActiveProject(projectName);
};

useEffect(() => {
loadProjects();
}, []);

return {
projects,
projectFiles,
Expand All @@ -217,7 +216,8 @@ export const useProject = () => {
deleteProjectFile,
moveItem,
renameProjectFile,
setActiveProject,
setActiveProject: updateActiveProject,
loadProjectFiles,
};
};

Expand Down
12 changes: 10 additions & 2 deletions src/lib/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ class FileSystem {
}

async readFile(path: string) {
if (!(await this.exists(path))) {
throw new Error(`File not found: ${path}`);
}
return this.fs.readFile(path, 'utf8');
}

Expand All @@ -17,8 +20,13 @@ class FileSystem {
* @param data - The content to be written to the file. Can be a string or Uint8Array.
* @returns A promise that resolves once the file has been written.
*/
async writeFile(path: string, data: string | Uint8Array) {
const finalPath = await this.getUniquePath(path);
async writeFile(
path: string,
data: string | Uint8Array,
options?: { overwrite?: boolean },
) {
const { overwrite } = options ?? {};
const finalPath = overwrite ? path : await this.getUniquePath(path);
await this.ensureDirectoryExists(finalPath);
return this.fs.writeFile(finalPath, data);
}
Expand Down
Loading

0 comments on commit edbd3e9

Please sign in to comment.