From bb73b38bf97b80fc1f30f7c380edf140e4a7d960 Mon Sep 17 00:00:00 2001 From: Rahul Yadav Date: Wed, 18 Sep 2024 00:57:25 +0530 Subject: [PATCH] feat: revamp file system for efficient data management (#97) --- .eslintrc.cjs | 4 +- next.config.js | 2 +- package-lock.json | 128 ++-- package.json | 2 +- src/components/auth/TonAuth/TonAuth.tsx | 3 - .../dashboard/Dashboard/Dashboard.module.scss | 32 - .../dashboard/Dashboard/Dashboard.tsx | 25 - src/components/dashboard/Dashboard/index.ts | 1 - .../DashboardSidebar.module.scss | 66 -- .../DashboardSidebar/DashboardSidebar.tsx | 48 -- .../dashboard/DashboardSidebar/index.ts | 1 - src/components/dashboard/index.ts | 2 - .../MigrateToUnifiedFS/IndexedDBHelper.ts | 67 +++ .../MigrateToUnifiedFS.module.scss | 39 ++ .../MigrateToUnifiedFS/MigrateToUnifiedFS.tsx | 241 ++++++++ .../project/MigrateToUnifiedFS/index.ts | 1 + .../project/NewProject/NewProject.tsx | 31 +- .../ProjectListing/ProjectListing.module.scss | 70 --- .../project/ProjectListing/ProjectListing.tsx | 71 --- .../project/ProjectListing/index.ts | 1 - src/components/project/index.ts | 2 +- .../shared/HowToUse/HowToUse.module.scss | 10 - src/components/shared/HowToUse/HowToUse.tsx | 48 -- src/components/shared/HowToUse/index.ts | 1 - src/components/shared/Layout/Layout.tsx | 11 +- src/components/shared/LogView/LogView.tsx | 8 +- .../UserOnboarding/UserOnboarding.module.scss | 105 ---- .../shared/UserOnboarding/UserOnboarding.tsx | 34 -- src/components/shared/UserOnboarding/index.ts | 1 - .../UserOnboardingWizard.module.scss | 0 .../UserOnboardingWizard.tsx | 99 --- .../shared/UserOnboardingWizard/index.ts | 1 - src/components/shared/index.ts | 3 - src/components/workspace/ABIUi/TactABIUi.tsx | 50 +- .../workspace/BottomPanel/BottomPanel.tsx | 2 + .../workspace/BuildProject/BuildProject.tsx | 194 +++--- .../workspace/BuildProject/CellBuilder.tsx | 16 +- .../FuncContractInteraction.tsx | 36 +- src/components/workspace/Editor/Editor.tsx | 77 +-- .../workspace/ExecuteFile/ExecuteFile.tsx | 22 +- .../workspace/OpenFile/OpenFile.tsx | 9 +- .../ProjectSetting/ProjectSetting.module.scss | 11 - .../ProjectSetting/ProjectSetting.tsx | 55 -- .../workspace/ProjectSetting/index.ts | 1 - src/components/workspace/Tabs/Tabs.tsx | 40 +- .../workspace/TestCases/TestCases.tsx | 23 +- .../workspace/WorkSpace/WorkSpace.module.scss | 2 +- .../workspace/WorkSpace/WorkSpace.tsx | 101 ++-- .../WorkspaceSidebar/WorkspaceSidebar.tsx | 16 +- .../project/ManageProject/ManageProject.tsx | 105 ++-- .../workspace/tree/FileTree/FileTree.tsx | 32 +- .../workspace/tree/FileTree/TreeNode.tsx | 114 ++-- .../tree/FileTree/TreePlaceholderInput.tsx | 30 +- src/constant/UserOnboardingSteps.tsx | 110 ---- src/enum/file.ts | 1 + src/hooks/auth.hooks.ts | 29 - src/hooks/file.hooks.ts | 20 + src/hooks/fileTabs.hooks.ts | 112 ++++ src/hooks/index.ts | 2 + src/hooks/project.hooks.ts | 286 ++------- src/hooks/projectV2.hooks.ts | 381 ++++++++++++ src/hooks/setting.hooks.ts | 45 +- src/hooks/userOnboarding.hooks.ts | 47 -- src/hooks/workspace.hooks.ts | 569 +----------------- src/interfaces/workspace.interface.ts | 29 +- src/lib/fs.ts | 260 ++++++++ src/lib/zip.ts | 115 ++++ src/pages/404.tsx | 19 + src/pages/_app.tsx | 41 +- src/pages/_document.tsx | 2 - src/pages/project/[id].tsx | 14 - src/pages/project/index.tsx | 12 - src/state/IDE.context.tsx | 104 ++++ src/state/auth.state.ts | 15 - src/state/setting.state.ts | 17 - src/state/userOnboarding.state.ts | 20 - src/state/workspace.state.ts | 17 - src/utility/eventEmitter.ts | 2 + 78 files changed, 2019 insertions(+), 2344 deletions(-) delete mode 100644 src/components/dashboard/Dashboard/Dashboard.module.scss delete mode 100644 src/components/dashboard/Dashboard/Dashboard.tsx delete mode 100644 src/components/dashboard/Dashboard/index.ts delete mode 100644 src/components/dashboard/DashboardSidebar/DashboardSidebar.module.scss delete mode 100644 src/components/dashboard/DashboardSidebar/DashboardSidebar.tsx delete mode 100644 src/components/dashboard/DashboardSidebar/index.ts delete mode 100644 src/components/dashboard/index.ts create mode 100644 src/components/project/MigrateToUnifiedFS/IndexedDBHelper.ts create mode 100644 src/components/project/MigrateToUnifiedFS/MigrateToUnifiedFS.module.scss create mode 100644 src/components/project/MigrateToUnifiedFS/MigrateToUnifiedFS.tsx create mode 100644 src/components/project/MigrateToUnifiedFS/index.ts delete mode 100644 src/components/project/ProjectListing/ProjectListing.module.scss delete mode 100644 src/components/project/ProjectListing/ProjectListing.tsx delete mode 100644 src/components/project/ProjectListing/index.ts delete mode 100644 src/components/shared/HowToUse/HowToUse.module.scss delete mode 100644 src/components/shared/HowToUse/HowToUse.tsx delete mode 100644 src/components/shared/HowToUse/index.ts delete mode 100644 src/components/shared/UserOnboarding/UserOnboarding.module.scss delete mode 100644 src/components/shared/UserOnboarding/UserOnboarding.tsx delete mode 100644 src/components/shared/UserOnboarding/index.ts delete mode 100644 src/components/shared/UserOnboardingWizard/UserOnboardingWizard.module.scss delete mode 100644 src/components/shared/UserOnboardingWizard/UserOnboardingWizard.tsx delete mode 100644 src/components/shared/UserOnboardingWizard/index.ts delete mode 100644 src/components/workspace/ProjectSetting/ProjectSetting.module.scss delete mode 100644 src/components/workspace/ProjectSetting/ProjectSetting.tsx delete mode 100644 src/components/workspace/ProjectSetting/index.ts delete mode 100644 src/constant/UserOnboardingSteps.tsx delete mode 100644 src/hooks/auth.hooks.ts create mode 100644 src/hooks/file.hooks.ts create mode 100644 src/hooks/fileTabs.hooks.ts create mode 100644 src/hooks/index.ts create mode 100644 src/hooks/projectV2.hooks.ts delete mode 100644 src/hooks/userOnboarding.hooks.ts create mode 100644 src/lib/fs.ts create mode 100644 src/lib/zip.ts create mode 100644 src/pages/404.tsx delete mode 100644 src/pages/project/[id].tsx delete mode 100644 src/pages/project/index.tsx create mode 100644 src/state/IDE.context.tsx delete mode 100644 src/state/auth.state.ts delete mode 100644 src/state/setting.state.ts delete mode 100644 src/state/userOnboarding.state.ts delete mode 100644 src/state/workspace.state.ts diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 68fa9e8..ef1bb44 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -61,7 +61,7 @@ module.exports = { "@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/no-extra-non-null-assertion": "error", "@typescript-eslint/no-extraneous-class": "error", - "@typescript-eslint/no-floating-promises": "error", + "@typescript-eslint/no-floating-promises": "off", "@typescript-eslint/no-for-in-array": "error", "no-implied-eval": "off", "@typescript-eslint/no-implied-eval": "error", @@ -71,7 +71,7 @@ module.exports = { "@typescript-eslint/no-loss-of-precision": "error", "@typescript-eslint/no-meaningless-void-operator": "error", "@typescript-eslint/no-misused-new": "error", - "@typescript-eslint/no-misused-promises": "error", + "@typescript-eslint/no-misused-promises": "off", "@typescript-eslint/no-mixed-enums": "error", "@typescript-eslint/no-namespace": "error", "@typescript-eslint/no-non-null-asserted-nullish-coalescing": "error", diff --git a/next.config.js b/next.config.js index 59fd021..52f42a0 100644 --- a/next.config.js +++ b/next.config.js @@ -29,7 +29,7 @@ const nextConfig = withTM({ if (!options.isServer) { config.plugins.push( new MonacoWebpackPlugin({ - languages: ["typescript"], + languages: ["typescript", "json"], filename: "static/[name].worker.js", }), ); diff --git a/package-lock.json b/package-lock.json index 7277687..867a2d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@codingame/monaco-jsonrpc": "^0.3.1", "@codingame/monaco-languageclient": "^0.17.0", + "@isomorphic-git/lightning-fs": "^4.6.0", "@minoru/react-dnd-treeview": "^3.4.1", "@monaco-editor/react": "^4.5.1", "@orbs-network/ton-access": "^2.3.0", @@ -48,7 +49,6 @@ "react-dnd": "^16.0.1", "react-dom": "18.2.0", "react-icons": "^4.8.0", - "react-joyride": "^2.5.4", "react-markdown": "^9.0.1", "react-split": "^2.0.14", "react-syntax-highlighter": "^15.5.0", @@ -255,10 +255,6 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/@gilbarbara/deep-equal": { - "version": "0.1.2", - "license": "MIT" - }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "license": "Apache-2.0", @@ -293,6 +289,25 @@ "multiformats": "^9.5.4" } }, + "node_modules/@isomorphic-git/idb-keyval": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@isomorphic-git/idb-keyval/-/idb-keyval-3.3.2.tgz", + "integrity": "sha512-r8/AdpiS0/WJCNR/t/gsgL+M8NMVj/ek7s60uz3LmpCaTF2mEVlZJlB01ZzalgYzRLXwSPC92o+pdzjM7PN/pA==" + }, + "node_modules/@isomorphic-git/lightning-fs": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@isomorphic-git/lightning-fs/-/lightning-fs-4.6.0.tgz", + "integrity": "sha512-tfon8f1h6LawjFI/d8lZPWRPTxmdvyTMbkT/j5yo6dB0hALhKw5D9JsdCcUu/D1pAcMMiU7GZFDsDGqylerr7g==", + "dependencies": { + "@isomorphic-git/idb-keyval": "3.3.2", + "isomorphic-textencoder": "1.0.1", + "just-debounce-it": "1.1.0", + "just-once": "1.1.0" + }, + "bin": { + "superblocktxt": "src/superblocktxt.js" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.3", "dev": true, @@ -3474,10 +3489,6 @@ "node": ">=12.0.0" } }, - "node_modules/exenv": { - "version": "1.2.2", - "license": "BSD-3-Clause" - }, "node_modules/extend": { "version": "3.0.2", "license": "MIT" @@ -3529,6 +3540,11 @@ "node_modules/fast-shallow-equal": { "version": "1.0.0" }, + "node_modules/fast-text-encoding": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz", + "integrity": "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==" + }, "node_modules/fastest-stable-stringify": { "version": "2.0.2", "license": "MIT" @@ -4447,10 +4463,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-lite": { - "version": "0.9.2", - "license": "MIT" - }, "node_modules/is-map": { "version": "2.0.3", "license": "MIT", @@ -4631,6 +4643,14 @@ "whatwg-fetch": "^3.4.1" } }, + "node_modules/isomorphic-textencoder": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/isomorphic-textencoder/-/isomorphic-textencoder-1.0.1.tgz", + "integrity": "sha512-676hESgHullDdHDsj469hr+7t3i/neBKU9J7q1T4RHaWwLAsaQnywC0D1dIUId0YZ+JtVrShzuBk1soo0+GVcQ==", + "dependencies": { + "fast-text-encoding": "^1.0.0" + } + }, "node_modules/it-all": { "version": "1.0.6", "license": "ISC" @@ -4789,6 +4809,16 @@ "node": ">=4.0" } }, + "node_modules/just-debounce-it": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/just-debounce-it/-/just-debounce-it-1.1.0.tgz", + "integrity": "sha512-87Nnc0qZKgBZuhFZjYVjSraic0x7zwjhaTMrCKlj0QYKH6lh0KbFzVnfu6LHan03NO7J8ygjeBeD0epejn5Zcg==" + }, + "node_modules/just-once": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/just-once/-/just-once-1.1.0.tgz", + "integrity": "sha512-+rZVpl+6VyTilK7vB/svlMPil4pxqIJZkbnN7DKZTOzyXfun6ZiFeq2Pk4EtCEHZ0VU4EkdFzG8ZK5F3PErcDw==" + }, "node_modules/jwa": { "version": "1.4.1", "license": "MIT", @@ -6085,14 +6115,6 @@ "tslib": "^2.1.0" } }, - "node_modules/popper.js": { - "version": "1.16.1", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, "node_modules/possible-typed-array-names": { "version": "1.0.0", "license": "MIT", @@ -6922,27 +6944,6 @@ "react": "^18.2.0" } }, - "node_modules/react-floater": { - "version": "0.7.6", - "license": "MIT", - "dependencies": { - "deepmerge": "^4.2.2", - "exenv": "^1.2.2", - "is-lite": "^0.8.2", - "popper.js": "^1.16.0", - "prop-types": "^15.8.1", - "react-proptype-conditional-require": "^1.0.4", - "tree-changes": "^0.9.1" - }, - "peerDependencies": { - "react": "15 - 18", - "react-dom": "15 - 18" - } - }, - "node_modules/react-floater/node_modules/is-lite": { - "version": "0.8.2", - "license": "MIT" - }, "node_modules/react-icons": { "version": "4.8.0", "license": "MIT", @@ -6954,25 +6955,6 @@ "version": "16.13.1", "license": "MIT" }, - "node_modules/react-joyride": { - "version": "2.5.4", - "license": "MIT", - "dependencies": { - "deepmerge": "^4.3.1", - "exenv": "^1.2.2", - "is-lite": "^0.9.2", - "prop-types": "^15.8.1", - "react-floater": "^0.7.6", - "react-is": "^16.13.1", - "scroll": "^3.0.1", - "scrollparent": "^2.0.1", - "tree-changes": "^0.9.2" - }, - "peerDependencies": { - "react": "15 - 18", - "react-dom": "15 - 18" - } - }, "node_modules/react-markdown": { "version": "9.0.1", "license": "MIT", @@ -6997,10 +6979,6 @@ "react": ">=18" } }, - "node_modules/react-proptype-conditional-require": { - "version": "1.0.4", - "license": "MIT" - }, "node_modules/react-split": { "version": "2.0.14", "license": "MIT", @@ -7499,10 +7477,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/scroll": { - "version": "3.0.1", - "license": "MIT" - }, "node_modules/scroll-into-view-if-needed": { "version": "3.0.6", "license": "MIT", @@ -7510,10 +7484,6 @@ "compute-scroll-into-view": "^3.0.0" } }, - "node_modules/scrollparent": { - "version": "2.1.0", - "license": "ISC" - }, "node_modules/semver": { "version": "7.6.2", "license": "ISC", @@ -8003,18 +7973,6 @@ "version": "0.0.3", "license": "MIT" }, - "node_modules/tree-changes": { - "version": "0.9.3", - "license": "MIT", - "dependencies": { - "@gilbarbara/deep-equal": "^0.1.1", - "is-lite": "^0.8.2" - } - }, - "node_modules/tree-changes/node_modules/is-lite": { - "version": "0.8.2", - "license": "MIT" - }, "node_modules/trim-lines": { "version": "3.0.1", "license": "MIT", diff --git a/package.json b/package.json index 6a106bf..db2d127 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "dependencies": { "@codingame/monaco-jsonrpc": "^0.3.1", "@codingame/monaco-languageclient": "^0.17.0", + "@isomorphic-git/lightning-fs": "^4.6.0", "@minoru/react-dnd-treeview": "^3.4.1", "@monaco-editor/react": "^4.5.1", "@orbs-network/ton-access": "^2.3.0", @@ -51,7 +52,6 @@ "react-dnd": "^16.0.1", "react-dom": "18.2.0", "react-icons": "^4.8.0", - "react-joyride": "^2.5.4", "react-markdown": "^9.0.1", "react-split": "^2.0.14", "react-syntax-highlighter": "^15.5.0", diff --git a/src/components/auth/TonAuth/TonAuth.tsx b/src/components/auth/TonAuth/TonAuth.tsx index cfcb781..72adb68 100644 --- a/src/components/auth/TonAuth/TonAuth.tsx +++ b/src/components/auth/TonAuth/TonAuth.tsx @@ -1,4 +1,3 @@ -import { useAuthAction } from '@/hooks/auth.hooks'; import { ConnectedWallet, useTonConnectUI } from '@tonconnect/ui-react'; import { Button } from 'antd'; import Image from 'next/image'; @@ -8,7 +7,6 @@ import s from './TonAuth.module.scss'; const TonAuth: FC = () => { const [tonConnector] = useTonConnectUI(); const [isConnected, setIsConnected] = useState(false); - const { updateAuth } = useAuthAction(); const handleConnectWallet = async () => { try { @@ -32,7 +30,6 @@ const TonAuth: FC = () => { tonConnector.onStatusChange((wallet: ConnectedWallet | null) => { if (!wallet || !tonConnector.connected) return; setIsConnected(Boolean(wallet) || tonConnector.connected); - updateAuth({ walletAddress: wallet.account.address }); }); }, []); diff --git a/src/components/dashboard/Dashboard/Dashboard.module.scss b/src/components/dashboard/Dashboard/Dashboard.module.scss deleted file mode 100644 index 5040e0d..0000000 --- a/src/components/dashboard/Dashboard/Dashboard.module.scss +++ /dev/null @@ -1,32 +0,0 @@ -.root { - display: grid; - grid-template-columns: 200px 1fr; - gap: 50px 30px; - .column { - height: 100vh; - padding-top: 1rem; - overflow-x: hidden; - } - .onlyDesktop { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: #000; - z-index: 1000; - text-align: center; - align-items: center; - justify-content: center; - flex-direction: column; - display: none; - padding: 1rem; - @media screen and (max-width: 767px) { - display: flex; - } - .label { - display: inline-block; - margin-top: 1rem; - } - } -} diff --git a/src/components/dashboard/Dashboard/Dashboard.tsx b/src/components/dashboard/Dashboard/Dashboard.tsx deleted file mode 100644 index 8554f17..0000000 --- a/src/components/dashboard/Dashboard/Dashboard.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { DashboardSidebar } from '@/components/dashboard'; -import { ProjectListing } from '@/components/project'; -import { AppLogo } from '@/components/ui'; -import { FC } from 'react'; -import s from './Dashboard.module.scss'; - -const Dashboard: FC = () => { - return ( -
-
- - - Only desktop screen is supported at the moment. - -
- -
-

Recent Projects:

- -
-
- ); -}; - -export default Dashboard; diff --git a/src/components/dashboard/Dashboard/index.ts b/src/components/dashboard/Dashboard/index.ts deleted file mode 100644 index 449ae56..0000000 --- a/src/components/dashboard/Dashboard/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './Dashboard'; diff --git a/src/components/dashboard/DashboardSidebar/DashboardSidebar.module.scss b/src/components/dashboard/DashboardSidebar/DashboardSidebar.module.scss deleted file mode 100644 index 248d9aa..0000000 --- a/src/components/dashboard/DashboardSidebar/DashboardSidebar.module.scss +++ /dev/null @@ -1,66 +0,0 @@ -.root { - padding: 0 1rem; - height: 100%; - background-color: var(--grey-500); - .brandLogo { - width: 8rem; - display: block; - position: relative; - &::before { - content: ''; - position: absolute; - bottom: -1rem; - left: -1rem; - right: -4rem; - background-color: #494949; - height: 1px; - } - img { - max-width: 100%; - } - } - .menuItems { - display: flex; - margin-top: 2rem; - justify-content: space-between; - flex-direction: column; - height: 83%; - .item { - color: #fff; - display: flex; - align-items: center; - font-size: 1rem; - padding: 0.5rem 0; - cursor: pointer; - gap: 4px; - > div { - width: 100%; - } - .label { - display: inline-flex; - margin-left: 5px; - } - .icon { - width: 0.9rem; - margin-right: 3px; - } - } - } - .logoutContainer { - cursor: default !important; - .name { - display: block; - margin-bottom: 1rem; - font-size: 0.9rem; - } - } - .logout { - cursor: pointer; - display: flex; - align-items: center; - padding: 0.5rem 0; - background: #5f5e5e; - border-radius: 5px; - justify-content: center; - } -} diff --git a/src/components/dashboard/DashboardSidebar/DashboardSidebar.tsx b/src/components/dashboard/DashboardSidebar/DashboardSidebar.tsx deleted file mode 100644 index 3d36a11..0000000 --- a/src/components/dashboard/DashboardSidebar/DashboardSidebar.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { AppLogo } from '@/components/ui'; -import AppIcon from '@/components/ui/icon'; -import Link from 'next/link'; -import { FC } from 'react'; -import s from './DashboardSidebar.module.scss'; - -interface Props { - className?: string; -} - -const DashboardSidebar: FC = ({ className }) => { - return ( -
- - -
-
- Welcome 👋 - {/* startOnboarding(0)} - > - - Start help wizard - */} - - - Documentation - - - - Share Feedback - -
-
-
- ); -}; - -export default DashboardSidebar; diff --git a/src/components/dashboard/DashboardSidebar/index.ts b/src/components/dashboard/DashboardSidebar/index.ts deleted file mode 100644 index 6fb5fcc..0000000 --- a/src/components/dashboard/DashboardSidebar/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './DashboardSidebar'; diff --git a/src/components/dashboard/index.ts b/src/components/dashboard/index.ts deleted file mode 100644 index 43610e4..0000000 --- a/src/components/dashboard/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default as Dashboard } from './Dashboard'; -export { default as DashboardSidebar } from './DashboardSidebar'; diff --git a/src/components/project/MigrateToUnifiedFS/IndexedDBHelper.ts b/src/components/project/MigrateToUnifiedFS/IndexedDBHelper.ts new file mode 100644 index 0000000..4b319a5 --- /dev/null +++ b/src/components/project/MigrateToUnifiedFS/IndexedDBHelper.ts @@ -0,0 +1,67 @@ +export class IndexedDBHelper { + private dbName: string; + private storeName: string; + private dbVersion: number; + + constructor(dbName: string, storeName: string, dbVersion: number) { + this.dbName = dbName; + this.storeName = storeName; + this.dbVersion = dbVersion; + } + + private async getDB(): Promise { + return new Promise((resolve, reject) => { + const request = indexedDB.open(this.dbName, this.dbVersion); + + request.onupgradeneeded = (event) => { + const db = (event.target as IDBOpenDBRequest).result; + if (!db.objectStoreNames.contains(this.storeName)) { + db.createObjectStore(this.storeName, { keyPath: 'id' }); + } + }; + + request.onsuccess = () => { + resolve(request.result); + }; + + request.onerror = () => { + reject(request.error); + }; + }); + } + + async getAllFiles(): Promise { + const db = await this.getDB(); + return new Promise((resolve, reject) => { + const transaction = db.transaction([this.storeName], 'readonly'); + const store = transaction.objectStore(this.storeName); + const request = store.getAll(); + + request.onsuccess = () => { + resolve(request.result); + }; + + request.onerror = () => { + reject(request.error); + }; + }); + } + + async deleteDatabase(): Promise { + return new Promise((resolve, reject) => { + const deleteRequest = indexedDB.deleteDatabase(this.dbName); + + deleteRequest.onsuccess = () => { + resolve(); + }; + + deleteRequest.onerror = () => { + reject(deleteRequest.error); + }; + + deleteRequest.onblocked = () => { + console.warn(`Delete request for database ${this.dbName} is blocked.`); + }; + }); + } +} diff --git a/src/components/project/MigrateToUnifiedFS/MigrateToUnifiedFS.module.scss b/src/components/project/MigrateToUnifiedFS/MigrateToUnifiedFS.module.scss new file mode 100644 index 0000000..d2d810f --- /dev/null +++ b/src/components/project/MigrateToUnifiedFS/MigrateToUnifiedFS.module.scss @@ -0,0 +1,39 @@ +.root { + min-width: 12rem; + .btnRestore { + .icon { + min-width: 1rem; + } + } + + .description { + font-size: 0.8rem; + color: #fff; + margin-bottom: 0.5rem; + margin-top: 1rem; + span { + font-weight: 700; + font-size: 1rem; + } + } +} + +.modal { + .title { + text-align: center; + display: block; + font-size: 1.1rem; + font-weight: 600; + } + + .description { + margin-top: 2rem; + font-size: 0.9rem; + } + + .successMessage { + margin-top: 1rem; + color: #00ff00; + font-size: 0.9rem; + } +} diff --git a/src/components/project/MigrateToUnifiedFS/MigrateToUnifiedFS.tsx b/src/components/project/MigrateToUnifiedFS/MigrateToUnifiedFS.tsx new file mode 100644 index 0000000..62b8656 --- /dev/null +++ b/src/components/project/MigrateToUnifiedFS/MigrateToUnifiedFS.tsx @@ -0,0 +1,241 @@ +import AppIcon from '@/components/ui/icon'; +import { useLogActivity } from '@/hooks/logActivity.hooks'; +import { useProject } from '@/hooks/projectV2.hooks'; +import { + ContractLanguage, + Project, + Tree, +} from '@/interfaces/workspace.interface'; +import { Button, ConfigProvider, message, Modal, Popconfirm } from 'antd'; +import { FC, useEffect, useState } from 'react'; +import { IndexedDBHelper } from './IndexedDBHelper'; +import s from './MigrateToUnifiedFS.module.scss'; + +interface Props { + hasDescription?: boolean; +} + +interface DBFile { + id: string; + content: string; +} + +interface IProject { + projectDetails: Project; + files: Tree[]; +} + +const MigrateToUnifiedFS: FC = ({ hasDescription = false }) => { + const [isMigrationView, setIsMigrationView] = useState(false); + const fileSystem = new IndexedDBHelper('NujanFiles', 'files', 10); + const [projects, setProjects] = useState(null); + const [migrationStatus, setMigrationStatus] = useState('pending'); + const { createProject } = useProject(); + const { createLog } = useLogActivity(); + const note = `We've recently upgraded the IDE, and some of your projects may not be visible.`; + + const checkMigration = async () => { + const filesInDB = (await fileSystem.getAllFiles()) as DBFile[]; + const localStorageItems = localStorage.getItem('recoil-persist'); + if (localStorageItems) { + const parsedItems = JSON.parse(localStorageItems); + const existingProjects = parsedItems?.['workspaceState']?.[ + 'projects' + ] as Partial; + const projectFiles = parsedItems?.['workspaceState']?.['projectFiles']; + if (!existingProjects) return; + + const project = existingProjects.map((project) => { + if (!project) return; + const { id, ...rest } = project; + const files = projectFiles?.[id as keyof typeof projectFiles] as Tree[]; + files.forEach((element) => { + const file = filesInDB.find((file) => file.id === element.id); + if (file) { + element.content = file.content; + } + }); + + return { + projectDetails: { ...rest }, + files, + }; + }); + + if (Array.isArray(existingProjects) && existingProjects.length > 0) { + // setHasFileForMigration(true); + setProjects(project as IProject[]); + } + } + }; + + const migrateProject = async () => { + if (!projects || projects.length === 0) { + message.error('No project found to migrate'); + return; + } + try { + setMigrationStatus('migrating'); + message.warning('Migrating project...'); + const migratedProjects = []; + + for (let i = 0; i < projects.length; i++) { + const project = projects[i]; + const isLastProject = i === projects.length - 1; + + await createProject( + project.projectDetails.name as string, + project.projectDetails.language as ContractLanguage, + 'import', + null, + project.files as Tree[], + isLastProject, + ); + + migratedProjects.push(project.projectDetails.name); + } + + message.success('Project migrated successfully'); + localStorage.setItem('migrationStatus', 'completed'); + createLog( + `Total: ${migratedProjects.length} migrated. \nProjects: ${migratedProjects.toString()}`, + 'success', + ); + setMigrationStatus('completed'); + } catch (error) { + createLog('Failed to migrate project', 'error'); + setMigrationStatus('failed'); + } + }; + + const deleteOldProjects = async () => { + try { + localStorage.removeItem('recoil-persist'); + await fileSystem.deleteDatabase(); + localStorage.removeItem('migrationStatus'); + message.success('Old projects deleted successfully'); + setIsMigrationView(false); + setMigrationStatus('done'); + } catch (error) { + if (error instanceof Error) { + message.error(error.message); + return; + } + message.error('Failed to delete old projects'); + } + }; + + useEffect(() => { + const migrationStatus = localStorage.getItem('migrationStatus'); + if (migrationStatus === 'completed') { + setMigrationStatus('completed'); + } + try { + checkMigration(); + } catch (error) { + /* empty */ + } + }, []); + + if (projects === null || migrationStatus === 'done') return null; + + return ( + <> +
+ {hasDescription && ( +
+ Note: + {note} To restore them, simply click the{' '} + `Restore Old Projects` button. +
+ )} + + + +
+ { + setIsMigrationView(false); + }} + footer={null} + > +
+

+ Note: + {note} +

+

+ Migrating your project to the new file system ensures better + compatibility and performance with upcoming updates. All your + project files preserved during the migration. +

+ +

+ Projects({projects.length}): + {projects.map((project, index) => ( + + {project.projectDetails.name} + {index !== projects.length - 1 && ', '} + + ))} +

+
+ + + + {migrationStatus === 'completed' && ( + <> +

+ Migration completed successfully. Now you can safely delete old + project from archive. +

+ {}} + okText="Yes" + cancelText="No" + > + + + + )} +
+ + ); +}; + +export default MigrateToUnifiedFS; diff --git a/src/components/project/MigrateToUnifiedFS/index.ts b/src/components/project/MigrateToUnifiedFS/index.ts new file mode 100644 index 0000000..625230f --- /dev/null +++ b/src/components/project/MigrateToUnifiedFS/index.ts @@ -0,0 +1 @@ +export { default } from './MigrateToUnifiedFS'; diff --git a/src/components/project/NewProject/NewProject.tsx b/src/components/project/NewProject/NewProject.tsx index 6031559..b15d560 100644 --- a/src/components/project/NewProject/NewProject.tsx +++ b/src/components/project/NewProject/NewProject.tsx @@ -1,7 +1,6 @@ import { Tooltip } from '@/components/ui'; import AppIcon, { AppIconType } from '@/components/ui/icon'; -import { useProjectActions } from '@/hooks/project.hooks'; -import { useWorkspaceActions } from '@/hooks/workspace.hooks'; +import { useProject } from '@/hooks/projectV2.hooks'; import { ContractLanguage, ProjectTemplate, @@ -9,7 +8,6 @@ import { } from '@/interfaces/workspace.interface'; import { Analytics } from '@/utility/analytics'; import EventEmitter from '@/utility/eventEmitter'; -import { downloadRepo } from '@/utility/gitRepoDownloader'; import { Button, Form, Input, Modal, Radio, Upload, message } from 'antd'; import { useForm } from 'antd/lib/form/Form'; import type { RcFile } from 'antd/lib/upload'; @@ -43,8 +41,7 @@ const NewProject: FC = ({ name, }) => { const [isActive, setIsActive] = useState(active); - const { projects } = useWorkspaceActions(); - const { createProject } = useProjectActions(); + const { createProject } = useProject(); const [isLoading, setIsLoading] = useState(false); const router = useRouter(); @@ -72,20 +69,21 @@ const NewProject: FC = ({ const onFormFinish = async (values: FormValues) => { const { githubUrl, language } = values; - let { name: projectName } = values; - let files: Tree[] = defaultFiles; + const { name: projectName } = values; + const files: Tree[] = defaultFiles; try { setIsLoading(true); - if (projects().findIndex((p) => p.name == projectName) >= 0) { - projectName += '-' + projects().length + 1; - } if (projectType === 'git') { - files = await downloadRepo(githubUrl as string); + throw new Error( + `Git import has been disabled for now. Repo: ${githubUrl}`, + ); + // TODO: Implement downloadRepo function + // files = await downloadRepo(githubUrl as string); } - const projectId = await createProject( + await createProject( projectName, language, values.template ?? 'import', @@ -101,8 +99,8 @@ const NewProject: FC = ({ sourceType: projectType, template: values.template, }); - await message.success(`Project '${projectName}' created`); - await router.push(`/project/${projectId}`); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + message.success(`Project '${projectName}' created`); } catch (error) { let errorMessage = 'Error in creating project'; if (typeof error === 'string') { @@ -110,7 +108,8 @@ const NewProject: FC = ({ } else { errorMessage = (error as Error).message || errorMessage; } - await message.error(errorMessage); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + message.error(errorMessage); } finally { setIsLoading(false); } @@ -244,7 +243,7 @@ const NewProject: FC = ({ >
- Choose a file or drag it here + Choose a .zip file or drag it here
diff --git a/src/components/project/ProjectListing/ProjectListing.module.scss b/src/components/project/ProjectListing/ProjectListing.module.scss deleted file mode 100644 index f3873bf..0000000 --- a/src/components/project/ProjectListing/ProjectListing.module.scss +++ /dev/null @@ -1,70 +0,0 @@ -.root { - padding-right: 1rem; - .content { - display: grid; - grid-template-columns: repeat(auto-fill, 25%); - min-height: 40vh; - > div, - > a { - margin-right: 1rem; - margin-bottom: 1rem; - } - } - - .item { - background-color: var(--grey-500); - border-radius: var(--border-radius); - position: relative; - padding: 2rem 1rem; - text-align: center; - cursor: pointer; - transition: all 0.2s ease-in-out 0s; - color: #fff; - height: 10rem; - - &:hover { - transform: scale(1.05); - } - - .platformIcon { - display: block; - text-align: center; - margin: 0 auto; - opacity: 0.6; - } - .name { - display: block; - font-size: 1.2rem; - font-weight: 500; - margin-top: 1rem; - } - .language { - position: absolute; - bottom: 0.5rem; - right: 0.5rem; - opacity: 0.5; - } - } - .deleting { - animation: fade 1s ease-in-out infinite; - pointer-events: none; - } - .deleteProject { - position: absolute; - right: 0; - top: 0; - padding: 0.7rem; - color: var(--color-danger); - cursor: pointer; - } -} - -@keyframes fade { - 0%, - 100% { - opacity: 0.5; - } - 50% { - opacity: 1; - } -} diff --git a/src/components/project/ProjectListing/ProjectListing.tsx b/src/components/project/ProjectListing/ProjectListing.tsx deleted file mode 100644 index 4d6ce32..0000000 --- a/src/components/project/ProjectListing/ProjectListing.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import AppIcon from '@/components/ui/icon'; -import { useWorkspaceActions } from '@/hooks/workspace.hooks'; -import { Project } from '@/interfaces/workspace.interface'; -import Image from 'next/image'; -import Link from 'next/link'; -import { FC, useState } from 'react'; -import NewProject from '../NewProject'; -import s from './ProjectListing.module.scss'; - -const ProjectListing: FC = () => { - const { projects, deleteProject } = useWorkspaceActions(); - const [projectToDelete, setProjectToDelete] = useState( - null, - ); - - const deleteSelectedProject = async ( - e: React.MouseEvent, - id: Project['id'], - ) => { - e.preventDefault(); - e.stopPropagation(); - setProjectToDelete(id); - - try { - await deleteProject(id); - } catch (error) { - /* empty */ - } finally { - setProjectToDelete(null); - } - }; - - return ( -
-
- - {[...projects()].reverse().map((item) => ( - - {''} - -
{ - deleteSelectedProject(e, item.id).catch(() => {}); - }} - > - -
- - {item.name} - {item.language ?? 'func'} - - ))} -
-
- ); -}; - -export default ProjectListing; diff --git a/src/components/project/ProjectListing/index.ts b/src/components/project/ProjectListing/index.ts deleted file mode 100644 index abef4bd..0000000 --- a/src/components/project/ProjectListing/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './ProjectListing'; diff --git a/src/components/project/index.ts b/src/components/project/index.ts index e1e276a..2c0b2af 100644 --- a/src/components/project/index.ts +++ b/src/components/project/index.ts @@ -1,2 +1,2 @@ +export { default as MigrateToUnifiedFS } from './MigrateToUnifiedFS'; export { default as NewProject } from './NewProject'; -export { default as ProjectListing } from './ProjectListing'; diff --git a/src/components/shared/HowToUse/HowToUse.module.scss b/src/components/shared/HowToUse/HowToUse.module.scss deleted file mode 100644 index 88339fa..0000000 --- a/src/components/shared/HowToUse/HowToUse.module.scss +++ /dev/null @@ -1,10 +0,0 @@ -.root { - margin-top: 2rem; - ol { - list-style-type: auto; - padding-left: 1.2rem; - li { - list-style: auto; - } - } -} diff --git a/src/components/shared/HowToUse/HowToUse.tsx b/src/components/shared/HowToUse/HowToUse.tsx deleted file mode 100644 index daabd91..0000000 --- a/src/components/shared/HowToUse/HowToUse.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { FC } from 'react'; -import s from './HowToUse.module.scss'; - -const HowToUse: FC = () => { - return ( -
-

- How to use: -

-
    -
  1. Create a new project with predefined template
  2. -
  3. - You will have 3 important files{' '} - main.fc, stateInit.cell.js, contract.cell.js and test.spec.js -
  4. -
  5. - main.fc is the root file which will be compiled. You do not - have to import stdlib.fc. It is already included at built - dyanamically. -
  6. -
  7. - stateInit.cell.js contains a cell which will be used to deploy - the contract. This will be initial state of the contract. To create a - cell we are using tonweb. -
  8. -
  9. - contract.cell.js contains a cell which will be used for further - internal message. -
  10. -
  11. - test.spec.js used to write test cases. Test cases will run on - TON sandbox. -
  12. -
  13. Write your code. And Go to compile from sidebar
  14. -
  15. - Build your contract, deploy it and then you can interact with the - contract using getter and setter options. -
  16. -
  17. - Project can be made public from setting option. It will enable any - user view and clone project. -
  18. -
-
- ); -}; - -export default HowToUse; diff --git a/src/components/shared/HowToUse/index.ts b/src/components/shared/HowToUse/index.ts deleted file mode 100644 index f68895d..0000000 --- a/src/components/shared/HowToUse/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './HowToUse'; diff --git a/src/components/shared/Layout/Layout.tsx b/src/components/shared/Layout/Layout.tsx index 910439b..8361dd4 100644 --- a/src/components/shared/Layout/Layout.tsx +++ b/src/components/shared/Layout/Layout.tsx @@ -1,4 +1,3 @@ -import { useUserOnboardingAction } from '@/hooks/userOnboarding.hooks'; import { FC, useEffect, useState } from 'react'; import s from './Layout.module.scss'; @@ -8,7 +7,6 @@ interface Props { } export const Layout: FC = ({ children }) => { const [isLoaded, setIsLoaded] = useState(false); - const { onboarding } = useUserOnboardingAction(); useEffect(() => { setIsLoaded(true); }, []); @@ -18,14 +16,7 @@ export const Layout: FC = ({ children }) => { } return ( <> - {/* */} -
- {children} -
+
{children}
); }; diff --git a/src/components/shared/LogView/LogView.tsx b/src/components/shared/LogView/LogView.tsx index f55b09c..e60cea6 100644 --- a/src/components/shared/LogView/LogView.tsx +++ b/src/components/shared/LogView/LogView.tsx @@ -174,17 +174,15 @@ const LogView: FC = ({ filter }) => { } }; - EventEmitter.on('ON_SPLIT_DRAG_END', () => { - onReSize(); - }); + EventEmitter.on('ON_SPLIT_DRAG_END', onReSize); return () => { isTerminalLoaded.current = false; EventEmitter.off('LOG', onGenericLog); EventEmitter.off('TEST_CASE_LOG', onTestCaseLog); EventEmitter.off('LOG_CLEAR'); - EventEmitter.off('ON_SPLIT_DRAG_END'); - terminal.current?.dispose(); + EventEmitter.off('ON_SPLIT_DRAG_END', onReSize); + // terminal.current?.dispose(); }; }); diff --git a/src/components/shared/UserOnboarding/UserOnboarding.module.scss b/src/components/shared/UserOnboarding/UserOnboarding.module.scss deleted file mode 100644 index 7adf7a4..0000000 --- a/src/components/shared/UserOnboarding/UserOnboarding.module.scss +++ /dev/null @@ -1,105 +0,0 @@ -$appPrimary: #b0ed01; -.root { - display: grid; - grid-template-columns: 2fr 1fr; - height: 100vh; - @media screen and (max-width: 767px) { - grid-template-columns: 1fr; - height: unset; - display: flex; - flex-direction: column-reverse; - } - * { - font-family: 'Josefin Sans', sans-serif; - } - > div { - height: 100%; - } - .columnLeft { - background-image: url('/images/layout/user-onboarding.jpg'); - background-size: cover; - display: flex; - align-items: center; - justify-content: center; - @media screen and (max-width: 767px) { - justify-content: flex-start; - } - .content { - background: rgba(140, 139, 139, 0.4); - backdrop-filter: blur(2.5px); - display: inline-block; - padding: 3rem; - @media screen and (max-width: 767px) { - padding: 1rem; - } - .heading { - font-size: 3.4rem; - margin-bottom: 2rem; - @media screen and (max-width: 767px) { - font-size: 2rem; - } - span { - color: $appPrimary; - } - &, - * { - font-family: 'monomaniac'; - line-height: 1; - text-transform: uppercase; - } - } - } - } - .columnRight { - display: flex; - align-items: center; - justify-content: center; - .content { - padding: 2rem; - .heading { - margin-top: 3rem; - } - .form { - min-width: 300px; - margin-top: 1.6rem; - input { - height: 2.5rem; - &:hover, - &:focus { - border-color: $appPrimary !important; - } - } - .btnAction { - // width: 100%; - background-color: #fff !important; - color: #000; - font-weight: 600; - // text-transform: uppercase; - height: 2.5rem; - margin-top: 1rem; - display: flex; - align-items: center; - justify-content: center; - width: 100%; - height: 3rem; - &:hover { - background-color: ($appPrimary) !important; - } - .icon { - margin-right: 0.5rem; - font-size: 1.3rem; - } - .label { - margin-top: 7px; - } - } - } - .formItem { - margin-bottom: 1rem; - } - div[class*='ant-form-item-label'] { - padding-bottom: 0; - } - } - } -} diff --git a/src/components/shared/UserOnboarding/UserOnboarding.tsx b/src/components/shared/UserOnboarding/UserOnboarding.tsx deleted file mode 100644 index 6383a65..0000000 --- a/src/components/shared/UserOnboarding/UserOnboarding.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import TonAuth from '@/components/auth/TonAuth'; -import Image from 'next/image'; -import { FC } from 'react'; - -import s from './UserOnboarding.module.scss'; - -const UserOnboarding: FC = () => { - return ( -
-
-
-

- Write Your
- Smart Contract
- Effortlessly -

-

No Setup | No Configuration | No Downloads

-
-
-
-
- Nujan -

Hey, Hackers

-

Connect your wallet to get started

-
- -
-
-
-
- ); -}; - -export default UserOnboarding; diff --git a/src/components/shared/UserOnboarding/index.ts b/src/components/shared/UserOnboarding/index.ts deleted file mode 100644 index e937d42..0000000 --- a/src/components/shared/UserOnboarding/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './UserOnboarding'; diff --git a/src/components/shared/UserOnboardingWizard/UserOnboardingWizard.module.scss b/src/components/shared/UserOnboardingWizard/UserOnboardingWizard.module.scss deleted file mode 100644 index e69de29..0000000 diff --git a/src/components/shared/UserOnboardingWizard/UserOnboardingWizard.tsx b/src/components/shared/UserOnboardingWizard/UserOnboardingWizard.tsx deleted file mode 100644 index 74ab12e..0000000 --- a/src/components/shared/UserOnboardingWizard/UserOnboardingWizard.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { userOnboardingSteps } from '@/constant/UserOnboardingSteps'; -import { useUserOnboardingAction } from '@/hooks/userOnboarding.hooks'; -import EventEmitter, { EventEmitterPayloads } from '@/utility/eventEmitter'; -import { delay } from '@/utility/utils'; -import { useRouter } from 'next/router'; -import { FC, useEffect } from 'react'; -import ReactJoyride, { ACTIONS, CallBackProps, EVENTS } from 'react-joyride'; - -const UserOnboardingWizard: FC = () => { - const { - onboarding, - stepIndex, - updateStepIndex, - startOnboarding, - stopOnboarding, - } = useUserOnboardingAction(); - - const router = useRouter(); - - const callBack = async (data: CallBackProps) => { - if ( - data.action === ACTIONS.SKIP || - (data.type === EVENTS.TOUR_END && onboarding().tourActive) - ) { - stopOnboarding(); - return; - } - if ( - data.action === ACTIONS.NEXT && - userOnboardingSteps.steps[data.index].afterEvent - ) { - const afterEvent = - userOnboardingSteps.steps[data.index].afterEvent ?? null; - - if (afterEvent && afterEvent in EventEmitter) { - EventEmitter.emit(afterEvent as keyof EventEmitterPayloads); - } - // Adding delay so that onboarding wizard should render after popup is opened - await delay(500); - } - if ( - data.action === ACTIONS.NEXT && - userOnboardingSteps.steps[data.index].name === 'codeEditor' - ) { - await router.replace( - `/project/${router.query.id as string}?tab=build`, - undefined, - { - shallow: true, - }, - ); - } - if (data.action === ACTIONS.NEXT && data.type === EVENTS.STEP_AFTER) { - updateStepIndex(data.index + 1); - } - }; - - useEffect(() => { - if ( - router.query.tab !== 'build' && - router.pathname === '/project/[id]' && - onboarding().tourActive - ) { - startOnboarding(2); - } - }, [router, onboarding, startOnboarding]); - - useEffect(() => { - // updateStepIndex(0); - }, []); - - return ( - { - callBack(data).catch(() => {}); - }} - /> - ); -}; - -export default UserOnboardingWizard; diff --git a/src/components/shared/UserOnboardingWizard/index.ts b/src/components/shared/UserOnboardingWizard/index.ts deleted file mode 100644 index 1492a36..0000000 --- a/src/components/shared/UserOnboardingWizard/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './UserOnboardingWizard'; diff --git a/src/components/shared/index.ts b/src/components/shared/index.ts index 0ef1047..94d5a8a 100644 --- a/src/components/shared/index.ts +++ b/src/components/shared/index.ts @@ -1,5 +1,2 @@ -export { default as HowToUse } from './HowToUse'; export { default as Layout } from './Layout'; export { default as LogView } from './LogView'; -export { default as UserOnboarding } from './UserOnboarding'; -export { default as UserOnboardingWizard } from './UserOnboardingWizard'; diff --git a/src/components/workspace/ABIUi/TactABIUi.tsx b/src/components/workspace/ABIUi/TactABIUi.tsx index d9de623..8c33838 100644 --- a/src/components/workspace/ABIUi/TactABIUi.tsx +++ b/src/components/workspace/ABIUi/TactABIUi.tsx @@ -1,7 +1,7 @@ import AppIcon from '@/components/ui/icon'; import { UserContract, useContractAction } from '@/hooks/contract.hooks'; import { useLogActivity } from '@/hooks/logActivity.hooks'; -import { useWorkspaceActions } from '@/hooks/workspace.hooks'; +import { useProject } from '@/hooks/projectV2.hooks'; import { LogType } from '@/interfaces/log.interface'; import { TactABIField, @@ -17,7 +17,6 @@ import { SandboxContract } from '@ton/sandbox'; import { Button, Form, Input, Popover, Select, Switch } from 'antd'; import { Rule, RuleObject } from 'antd/es/form'; import { useForm } from 'antd/lib/form/Form'; -import { useRouter } from 'next/router'; import { FC, Fragment, useEffect, useState } from 'react'; import { ABIUiProps } from './ABIUi'; import s from './ABIUi.module.scss'; @@ -78,8 +77,8 @@ function FieldItem( .filter( (f) => f.name.includes('.ts') && - !f.path?.startsWith('dist') && - !f.path?.endsWith('.spec.ts'), + !f.path.startsWith('dist') && + !f.path.endsWith('.spec.ts'), ) .map((file) => ( = ({ const [form] = useForm(); const { projectFiles, - getAllFilesWithContent, - updateABIInputValues, + readdirTree, + activeProject, getABIInputValues, - project, - } = useWorkspaceActions(); - const router = useRouter(); - const { id: projectId } = router.query; - const activeProject = project(projectId as string); + updateABIInputValues, + } = useProject(); const getItemHeading = (item: TactType) => { if (item.type?.kind === 'simple') { @@ -348,15 +344,22 @@ const TactABIUi: FC = ({ const onSubmit = async (formValues: TactInputFields, fieldName: string) => { try { - let tsProjectFiles = {}; + const tsProjectFiles: Record = {}; if (isIncludesTypeCellOrSlice(formValues)) { - tsProjectFiles = await getAllFilesWithContent( - projectId as string, + const fileCollection = await readdirTree( + `/${activeProject?.path}`, + { + basePath: null, + content: true, + }, (file) => - !file.path?.startsWith('dist') && + !file.path.startsWith('dist') && file.name.endsWith('.ts') && !file.name.endsWith('.spec.ts'), ); + fileCollection.forEach((file) => { + tsProjectFiles[file.path!] = file.content ?? ''; + }); } const parsedInputsValues = Object.values( await parseInputs(formValues, tsProjectFiles), @@ -386,10 +389,11 @@ const TactABIUi: FC = ({ } else { createLog(JSON.stringify(response, null, 2)); } - updateABIInputValues( - { key: abiType.name, value: formValues, type: type }, - projectId as string, - ); + updateABIInputValues({ + key: abiType.name, + value: formValues, + type: type, + }); } catch (error) { if ((error as Error).message.includes('no healthy nodes for')) { createLog( @@ -409,11 +413,7 @@ const TactABIUi: FC = ({ useEffect(() => { if (!activeProject) return; - const abiFields = getABIInputValues( - projectId as string, - abiType.name, - type, - ); + const abiFields = getABIInputValues(abiType.name, type); if (!abiFields) return; form.setFieldsValue(abiFields); }, []); @@ -434,7 +434,7 @@ const TactABIUi: FC = ({ {renderField( field as TactABIField, - projectFiles(projectId as string), + projectFiles, [], type === 'Setter' ? -1 : 0, )} diff --git a/src/components/workspace/BottomPanel/BottomPanel.tsx b/src/components/workspace/BottomPanel/BottomPanel.tsx index 8ec35d6..c0dc3ca 100644 --- a/src/components/workspace/BottomPanel/BottomPanel.tsx +++ b/src/components/workspace/BottomPanel/BottomPanel.tsx @@ -1,3 +1,4 @@ +import { MigrateToUnifiedFS } from '@/components/project'; import { LogView } from '@/components/shared'; import { Tooltip } from '@/components/ui'; import AppIcon from '@/components/ui/icon'; @@ -51,6 +52,7 @@ const BottomPanel: FC = () => {
LOG
+ { diff --git a/src/components/workspace/BuildProject/BuildProject.tsx b/src/components/workspace/BuildProject/BuildProject.tsx index 0936201..e6057b2 100644 --- a/src/components/workspace/BuildProject/BuildProject.tsx +++ b/src/components/workspace/BuildProject/BuildProject.tsx @@ -1,12 +1,12 @@ import TonAuth from '@/components/auth/TonAuth/TonAuth'; import { UserContract, useContractAction } from '@/hooks/contract.hooks'; import { useLogActivity } from '@/hooks/logActivity.hooks'; -import { useWorkspaceActions } from '@/hooks/workspace.hooks'; import { ABIField, CellABI, NetworkEnvironment, Project, + ProjectSetting, TactABIField, TactInputFields, } from '@/interfaces/workspace.interface'; @@ -31,6 +31,8 @@ import ExecuteFile from '../ExecuteFile/ExecuteFile'; import s from './BuildProject.module.scss'; import AppIcon from '@/components/ui/icon'; +import { useFile } from '@/hooks'; +import { useProject } from '@/hooks/projectV2.hooks'; import { useSettingAction } from '@/hooks/setting.hooks'; import { ABIParser, parseInputs } from '@/utility/abi'; import { Maybe } from '@ton/core/dist/utils/maybe'; @@ -73,6 +75,15 @@ const BuildProject: FC = ({ projectId, contract, updateContract }) => { ); const { isAutoBuildAndDeployEnabled } = useSettingAction(); + const { + projectFiles, + readdirTree, + activeProject, + updateProjectSetting, + updateABIInputValues, + getABIInputValues, + } = useProject(); + const { getFile } = useFile(); const [tonConnector] = useTonConnectUI(); const chain = tonConnector.wallet?.account.chain; @@ -86,29 +97,14 @@ const BuildProject: FC = ({ projectId, contract, updateContract }) => { const [deployForm] = useForm(); - const { - projectFiles, - getFileByPath, - updateProjectById, - project, - activeFile, - getAllFilesWithContent, - updateABIInputValues, - getABIInputValues, - } = useWorkspaceActions(); - - const currentActiveFile = activeFile(projectId as string); - const { deployContract } = useContractAction(); - const activeProject = project(projectId); - const contractsToDeploy = () => { - return projectFiles(projectId) + return projectFiles .filter((f) => { const _fileExtension = getFileExtension(f.name || ''); return ( - f.path?.startsWith('dist') && + f.path.startsWith(`${activeProject?.path}/dist`) && ['abi'].includes(_fileExtension as string) ); }) @@ -127,14 +123,7 @@ const BuildProject: FC = ({ projectId, contract, updateContract }) => { const cellBuilder = (info: string) => { if (!activeProject?.language || activeProject.language !== 'func') return <>; - return ( - - ); + return ; }; const deployView = () => { @@ -171,26 +160,30 @@ const BuildProject: FC = ({ projectId, contract, updateContract }) => { allowClear > {_contractsToDeploy.map((f) => ( - + {f.name} ))} {cellBuilder('Update initial contract state in ')} -
- {selectedContract && - contractABI.initParams?.map((item) => { - return ( - - {renderField( - item as unknown as TactABIField, - projectFiles(projectId), - )} - - ); - })} -
+ {selectedContract && + contractABI.initParams && + contractABI.initParams.length > 0 && ( +
+ {contractABI.initParams.map((item) => { + return ( + + {renderField( + item as unknown as TactABIField, + projectFiles, + )} + + ); + })} +
+ )} + -
- ); -}; - -export default ProjectSetting; diff --git a/src/components/workspace/ProjectSetting/index.ts b/src/components/workspace/ProjectSetting/index.ts deleted file mode 100644 index cabca00..0000000 --- a/src/components/workspace/ProjectSetting/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './ProjectSetting'; diff --git a/src/components/workspace/Tabs/Tabs.tsx b/src/components/workspace/Tabs/Tabs.tsx index 8f14e07..f6d09a3 100644 --- a/src/components/workspace/Tabs/Tabs.tsx +++ b/src/components/workspace/Tabs/Tabs.tsx @@ -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 = ({ 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 (
- {openedFilesList.map((item) => ( + {fileTab.items.map((item) => (
{ - 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} { - closeTab(e, item.id); + closeTab(e, item.path); }} > diff --git a/src/components/workspace/TestCases/TestCases.tsx b/src/components/workspace/TestCases/TestCases.tsx index 6b27bb6..c79a01c 100644 --- a/src/components/workspace/TestCases/TestCases.tsx +++ b/src/components/workspace/TestCases/TestCases.tsx @@ -1,10 +1,10 @@ /* eslint-disable no-useless-escape */ +import { useFile } from '@/hooks'; import { useLogActivity } from '@/hooks/logActivity.hooks'; import { useProjectActions } from '@/hooks/project.hooks'; import { useWorkspaceActions } from '@/hooks/workspace.hooks'; import { Analytics } from '@/utility/analytics'; import EventEmitter from '@/utility/eventEmitter'; -import { getFileNameFromPath } from '@/utility/utils'; import { FC } from 'react'; import ExecuteFile from '../ExecuteFile'; import s from './TestCases.module.scss'; @@ -16,16 +16,15 @@ interface Props { const TestCases: FC = ({ projectId }) => { const { createLog } = useLogActivity(); - const { getFileByPath, compileTsFile } = useWorkspaceActions(); + const { compileTsFile } = useWorkspaceActions(); + const { getFile } = useFile(); const { compileFuncProgram } = useProjectActions(); const executeTestCases = async (filePath: string) => { - const file = await getFileByPath(filePath, projectId); - if (!file) return; let testCaseCode = ''; try { - testCaseCode = (await compileTsFile(file, projectId))[0].code; + testCaseCode = (await compileTsFile(filePath, projectId))[0].code; } catch (error) { if ((error as Error).message) { createLog((error as Error).message, 'error'); @@ -55,7 +54,12 @@ const TestCases: FC = ({ projectId }) => { // createLog('Please specify contract path', 'error'); // return; // } - const contractFile = await getFileByPath(contractPath, projectId); + let contractFile = undefined; + try { + contractFile = await getFile(contractPath!); + } catch (error) { + /* empty */ + } if (contractPath && !contractFile) { createLog( `Contract file not found - ${contractPath}. Define correct absolute path. Ex. contracts/main.fc`, @@ -123,17 +127,18 @@ const TestCases: FC = ({ projectId }) => { const runIt = async (filePath: string, codeBase: string) => { const _webcontainerInstance = window.webcontainerInstance; - filePath = getFileNameFromPath(filePath).replace('.spec.ts', '.spec.js'); + filePath = filePath.replace('.spec.ts', '.spec.js'); if (!_webcontainerInstance?.path) { return; } createLog('Running test cases...', 'info', true); - await _webcontainerInstance.fs.writeFile(filePath, codeBase); + const fileName = filePath.split('/').pop(); + await _webcontainerInstance.fs.writeFile(fileName!, codeBase); const response = await _webcontainerInstance.spawn('npx', [ 'jest', - filePath, + fileName!, ]); await response.output.pipeTo( diff --git a/src/components/workspace/WorkSpace/WorkSpace.module.scss b/src/components/workspace/WorkSpace/WorkSpace.module.scss index bdec4f1..630cc4f 100644 --- a/src/components/workspace/WorkSpace/WorkSpace.module.scss +++ b/src/components/workspace/WorkSpace/WorkSpace.module.scss @@ -35,7 +35,7 @@ } } .tree { - padding-top: 0.5rem; + // padding-top: 0.5rem; // flex: 0 0 250px; // width: 250px; background-color: #141416; diff --git a/src/components/workspace/WorkSpace/WorkSpace.tsx b/src/components/workspace/WorkSpace/WorkSpace.tsx index e0c1ed1..3ad670f 100644 --- a/src/components/workspace/WorkSpace/WorkSpace.tsx +++ b/src/components/workspace/WorkSpace/WorkSpace.tsx @@ -1,7 +1,11 @@ +'use client'; + import { ProjectTemplate } from '@/components/template'; import { AppConfig } from '@/config/AppConfig'; +import { useFileTab } from '@/hooks'; import { useLogActivity } from '@/hooks/logActivity.hooks'; -import { useWorkspaceActions } from '@/hooks/workspace.hooks'; +import { useProject } from '@/hooks/projectV2.hooks'; +import { useSettingAction } from '@/hooks/setting.hooks'; import { Project, Tree } from '@/interfaces/workspace.interface'; import { Analytics } from '@/utility/analytics'; import EventEmitter from '@/utility/eventEmitter'; @@ -16,7 +20,6 @@ import { useEffectOnce } from 'react-use'; import BottomPanel from '../BottomPanel/BottomPanel'; import BuildProject from '../BuildProject'; import Editor from '../Editor'; -import ProjectSetting from '../ProjectSetting'; import Tabs from '../Tabs'; import TestCases from '../TestCases'; import WorkspaceSidebar from '../WorkspaceSidebar'; @@ -28,8 +31,7 @@ import ItemAction from '../tree/FileTree/ItemActions'; import s from './WorkSpace.module.scss'; const WorkSpace: FC = () => { - const workspaceAction = useWorkspaceActions(); - const { createLog, clearLog } = useLogActivity(); + const { clearLog, createLog } = useLogActivity(); const router = useRouter(); const [activeMenu, setActiveMenu] = useState('code'); @@ -37,18 +39,26 @@ const WorkSpace: FC = () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const [contract, setContract] = useState(''); - const { id: projectId, tab } = router.query; - - const activeFile = workspaceAction.activeFile(projectId as string); - - const activeProject = useMemo(() => { - return workspaceAction.project(projectId as string); - }, [projectId]); - - const commitItemCreation = (type: string, name: string) => { - workspaceAction - .createNewItem('', name, type, projectId as string) - .catch(() => {}); + const { tab } = router.query; + const { + activeProject, + setActiveProject, + projectFiles, + loadProjectFiles, + newFileFolder, + } = useProject(); + + const { fileTab, open: openTab } = useFileTab(); + + const { init: initGlobalSetting } = useSettingAction(); + + const commitItemCreation = async (type: Tree['type'], name: string) => { + if (!name) return; + try { + await newFileFolder(name, type); + } catch (error) { + createLog((error as Error).message, 'error'); + } }; const createSandbox = async (force: boolean = false) => { @@ -61,6 +71,19 @@ const WorkSpace: FC = () => { globalWorkspace.sandboxWallet = wallet; }; + const openProject = async (selectedProjectPath: Project['id']) => { + if (!selectedProjectPath) { + createLog(`${selectedProjectPath} - project not found`, 'error'); + return; + } + await setActiveProject(selectedProjectPath); + await loadProjectFiles(selectedProjectPath); + }; + + const cachedProjectPath = useMemo(() => { + return activeProject?.path as string; + }, [activeProject]); + const onKeydown = (e: KeyboardEvent) => { if ((e.ctrlKey || e.metaKey) && e.key === 's') { e.preventDefault(); @@ -68,6 +91,16 @@ const WorkSpace: FC = () => { } }; + const reloadProjectFiles = async (projectPath: string) => { + if (!projectPath) return; + await loadProjectFiles(projectPath); + }; + + useEffect(() => { + if (!cachedProjectPath) return; + openProject(cachedProjectPath).catch(() => {}); + }, [cachedProjectPath]); + useEffect(() => { if (!activeProject) { return; @@ -75,17 +108,19 @@ const WorkSpace: FC = () => { createLog(`Project '${activeProject.name}' is opened`); createSandbox(true).catch(() => {}); - if (activeFile) return; - const projectFiles = workspaceAction.projectFiles(activeProject.id); + if (fileTab.active) return; + // Open main file on project switch const mainFile = projectFiles.find((file) => ['main.tact', 'main.fc'].includes(file.name), ); if (!mainFile) return; - workspaceAction.openFile(mainFile.id, activeProject.id); - }, [activeProject]); + openTab(mainFile.name, mainFile.path); + }, [cachedProjectPath]); useEffect(() => { document.addEventListener('keydown', onKeydown); + EventEmitter.on('RELOAD_PROJECT_FILES', reloadProjectFiles); + EventEmitter.on('OPEN_PROJECT', openProject); Analytics.track('Project Opened', { platform: 'IDE', @@ -95,6 +130,8 @@ const WorkSpace: FC = () => { return () => { try { document.removeEventListener('keydown', onKeydown); + EventEmitter.off('RELOAD_PROJECT_FILES', reloadProjectFiles); + EventEmitter.off('OPEN_PROJECT', openProject); clearLog(); } catch (error) { /* empty */ @@ -110,6 +147,7 @@ const WorkSpace: FC = () => { useEffectOnce(() => { setIsLoaded(true); + initGlobalSetting(); window.TonCore = TonCore; window.TonCrypto = TonCrypto; window.Buffer = Buffer; @@ -120,7 +158,7 @@ const WorkSpace: FC = () => {
{ setActiveMenu(name); router @@ -141,14 +179,11 @@ const WorkSpace: FC = () => { }} >
- {activeMenu === 'setting' && ( - - )} {isLoaded && activeMenu === 'code' && (
Explorer - {activeProject && ( + {activeProject?.path && (
{AppConfig.name} IDE {
)} - +
)} {activeMenu === 'build' && globalWorkspace.sandboxBlockchain && ( {}} contract={contract} updateContract={(contractInstance) => { @@ -179,7 +214,7 @@ const WorkSpace: FC = () => { )} {activeMenu === 'test-cases' && (
- +
)}
@@ -198,17 +233,11 @@ const WorkSpace: FC = () => { >
- +
- {!projectId && !activeFile && } - {activeFile && ( - - )} + {fileTab.active ? : }
diff --git a/src/components/workspace/WorkspaceSidebar/WorkspaceSidebar.tsx b/src/components/workspace/WorkspaceSidebar/WorkspaceSidebar.tsx index ced9463..af01b9d 100644 --- a/src/components/workspace/WorkspaceSidebar/WorkspaceSidebar.tsx +++ b/src/components/workspace/WorkspaceSidebar/WorkspaceSidebar.tsx @@ -2,8 +2,6 @@ import { AppLogo, Tooltip } from '@/components/ui'; import AppIcon, { AppIconType } from '@/components/ui/icon'; import { AppData } from '@/constant/AppData'; import { useSettingAction } from '@/hooks/setting.hooks'; -import { useWorkspaceActions } from '@/hooks/workspace.hooks'; -import { Project } from '@/interfaces/workspace.interface'; import { Form, Input, Popover, Select, Switch } from 'antd'; import Link from 'next/link'; import { FC } from 'react'; @@ -20,15 +18,14 @@ interface MenuItem { interface Props { activeMenu: WorkSpaceMenu; onMenuClicked: (name: WorkSpaceMenu) => void; - projectId: Project['id']; + projectName?: string | null; } const WorkspaceSidebar: FC = ({ activeMenu, onMenuClicked, - projectId, + projectName, }) => { - const { isProjectEditable } = useWorkspaceActions(); const { isContractDebugEnabled, toggleContractDebug, @@ -42,8 +39,7 @@ const WorkspaceSidebar: FC = ({ updateEditorMode, } = useSettingAction(); - const hasEditAccess = isProjectEditable(); - const editorMode = getSettingStateByKey('editorMode') ?? 'default'; + const editorMode = getSettingStateByKey('editorMode'); const menuItems: MenuItem[] = [ { @@ -172,7 +168,7 @@ const WorkspaceSidebar: FC = ({
{menuItems.map((menu, i) => { - if (menu.private && !hasEditAccess) { + if (menu.private) { return; } return ( @@ -180,9 +176,9 @@ const WorkspaceSidebar: FC = ({
{ - if (!projectId) return; + if (!projectName) return; onMenuClicked(menu.value); }} > diff --git a/src/components/workspace/project/ManageProject/ManageProject.tsx b/src/components/workspace/project/ManageProject/ManageProject.tsx index c17ea82..2800933 100644 --- a/src/components/workspace/project/ManageProject/ManageProject.tsx +++ b/src/components/workspace/project/ManageProject/ManageProject.tsx @@ -1,34 +1,48 @@ -import { NewProject } from '@/components/project'; +import { MigrateToUnifiedFS, NewProject } from '@/components/project'; import { Tooltip } from '@/components/ui'; import AppIcon from '@/components/ui/icon'; -import { useWorkspaceActions } from '@/hooks/workspace.hooks'; +import { baseProjectPath, useProject } from '@/hooks/projectV2.hooks'; import { Project } from '@/interfaces/workspace.interface'; +import EventEmitter from '@/utility/eventEmitter'; import { Button, Modal, Select, message } from 'antd'; -import { useRouter } from 'next/router'; import { FC, useEffect, useState } from 'react'; import s from './ManageProject.module.scss'; const ManageProject: FC = () => { - const { project, projects, deleteProject } = useWorkspaceActions(); - const [currentProject, setCurrentProject] = useState(null); const [isDeleteConfirmOpen, setIsDeleteConfirmOpen] = useState(false); - const router = useRouter(); - const { id: projectId, importURL } = router.query; + const { + projects, + setActiveProject, + deleteProject, + activeProject, + loadProjects, + } = useProject(); + + const deleteSelectedProject = async (id: Project['id']) => { + try { + await deleteProject(id); + setActiveProject(null); + setIsDeleteConfirmOpen(false); + } catch (error) { + await message.error('Failed to delete project'); + } + }; + + const openProject = async (selectedProject: Project['id']) => { + if (!selectedProject) { + await message.error('Project not found'); + return; + } + EventEmitter.emit('OPEN_PROJECT', `${baseProjectPath}/${selectedProject}`); + }; const projectHeader = () => ( <> Projects
- + {
{ - if (!currentProject) return; + if (!activeProject) return; setIsDeleteConfirmOpen(true); }} > @@ -60,7 +72,7 @@ const ManageProject: FC = () => { placeholder="Select a project" showSearch className="w-100 select-search-input-dark" - value={currentProject?.id} + value={activeProject?.name} onChange={(_project) => { openProject(_project).catch(() => {}); }} @@ -69,13 +81,9 @@ const ManageProject: FC = () => { return option?.title.toLowerCase().includes(inputValue.toLowerCase()); }} > - {[...projects()].reverse().map((project) => ( - - {project.name} - {project.language ?? 'func'} + {[...projects].reverse().map((project) => ( + + {project} ))} @@ -86,47 +94,20 @@ const ManageProject: FC = () => {
Begin by initiating a new project +
); - const hasProjects = () => { - return projects().length > 0; - }; - - const deleteSelectedProject = async (id: Project['id']) => { - try { - await deleteProject(id); - setCurrentProject(null); - setIsDeleteConfirmOpen(false); - await router.push('/'); - } catch (error) { - await message.error('Failed to delete project'); - } - }; - - const openProject = async (id: Project['id']) => { - if (!id) return; - const selectedProject = project(id as string); - if (!selectedProject) { - await message.error('Project not found'); - return; - } - setCurrentProject(selectedProject); - await router.push(`/project/${selectedProject.id}`); - }; - useEffect(() => { - if (!projectId || currentProject?.id == projectId) return; - openProject(projectId as string).catch(() => {}); - }, [projectId]); + loadProjects(); + }, []); return (
- {hasProjects() && projectHeader()} - {!hasProjects() && noProjectExistsUI()} + {projects.length > 0 ? projectHeader() : noProjectExistsUI()}
- {hasProjects() && projectOptions()} + {projects.length > 0 && projectOptions()} { footer={null} > - Delete my `{currentProject?.name}`{' '} + Delete my `{activeProject?.name}`{' '} Project?
@@ -163,8 +144,8 @@ const ManageProject: FC = () => { type="primary" danger onClick={() => { - if (currentProject) { - deleteSelectedProject(currentProject.id).catch(() => {}); + if (activeProject?.path) { + deleteSelectedProject(activeProject.path).catch(() => {}); } }} > diff --git a/src/components/workspace/tree/FileTree/FileTree.tsx b/src/components/workspace/tree/FileTree/FileTree.tsx index 2371fc8..d5dd9db 100644 --- a/src/components/workspace/tree/FileTree/FileTree.tsx +++ b/src/components/workspace/tree/FileTree/FileTree.tsx @@ -1,4 +1,4 @@ -import { useWorkspaceActions } from '@/hooks/workspace.hooks'; +import { useProject } from '@/hooks/projectV2.hooks'; import { DropOptions, getBackendOptions, @@ -9,44 +9,50 @@ import { import { FC } from 'react'; import { DndProvider } from 'react-dnd'; import s from './FileTree.module.scss'; -import TreeNode from './TreeNode'; +import TreeNode, { TreeNodeData } from './TreeNode'; interface Props { projectId: string; } const FileTree: FC = ({ projectId }) => { - const workspaceAction = useWorkspaceActions(); + const { activeProject, projectFiles, moveItem } = useProject(); - const projectFiles = (): NodeModel[] => { - return workspaceAction.projectFiles(projectId).map((item) => { + const getProjectFiles = (): NodeModel[] => { + if (!activeProject?.path) return []; + return projectFiles.map((item) => { return { - id: item.id, - parent: item.parent ?? 0, + id: item.path, + parent: item.parent ? item.parent : (activeProject.path as string), droppable: item.type === 'directory', text: item.name, + data: { + path: item.path, + }, }; }); }; - const handleDrop = (_: unknown, options: DropOptions) => { - workspaceAction.moveFile( + + const handleDrop = async (_: unknown, options: DropOptions) => { + await moveItem( options.dragSourceId as string, options.dropTargetId as string, - projectId, ); }; + if (!activeProject?.path) return null; + return (
( } depth={depth} isOpen={isOpen} onToggle={onToggle} diff --git a/src/components/workspace/tree/FileTree/TreeNode.tsx b/src/components/workspace/tree/FileTree/TreeNode.tsx index 629ae96..1969936 100644 --- a/src/components/workspace/tree/FileTree/TreeNode.tsx +++ b/src/components/workspace/tree/FileTree/TreeNode.tsx @@ -1,22 +1,27 @@ -import { useWorkspaceActions } from '@/hooks/workspace.hooks'; +import { useFileTab } from '@/hooks'; +import { useLogActivity } from '@/hooks/logActivity.hooks'; +import { useProject } from '@/hooks/projectV2.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'; import TreePlaceholderInput from './TreePlaceholderInput'; interface Props { - node: NodeModel; + node: NodeModel; depth: number; isOpen: boolean; onToggle: (id: NodeModel['id']) => void; projectId: Project['id']; } +export interface TreeNodeData { + path: string; +} + const TreeNode: FC = ({ node, depth, isOpen, onToggle }) => { const { droppable } = node; const indent = (depth + 1) * 15; @@ -24,23 +29,22 @@ const TreeNode: FC = ({ node, depth, isOpen, onToggle }) => { const [isEditing, setIsEditing] = useState(false); const [newItemAdd, setNewItemAdd] = useState(''); - const router = useRouter(); - const { id: projectId } = router.query; - - const { openFile, renameItem, deleteItem, createNewItem, isProjectEditable } = - useWorkspaceActions(); + const { deleteProjectFile, renameProjectFile, newFileFolder } = useProject(); + const { open: openTab } = useFileTab(); + const { createLog } = useLogActivity(); const disallowedFile = [ 'message.cell.ts', 'stateInit.cell.ts', 'test.spec.js', + 'setting.json', ]; const handleClick = (e: React.MouseEvent) => { e.stopPropagation(); onToggle(node.id); if (!node.droppable) { - openFile(node.id as string, projectId as string); + openTab(node.text, node.data?.path as string); } }; @@ -51,19 +55,32 @@ const TreeNode: FC = ({ node, depth, isOpen, onToggle }) => { setIsEditing(true); }; - const commitEditing = (name: string) => { - renameItem(node.id as string, name, projectId as string); - reset(); + const commitEditing = async (name: string) => { + try { + await renameProjectFile(node.data?.path as string, name); + reset(); + } catch (error) { + createLog((error as Error).message, 'error'); + } + }; + + const commitItemCreation = async (name: string) => { + if (!newItemAdd) return; + const path = `${node.data?.path}/${name}`; + try { + await newFileFolder(path, newItemAdd); + reset(); + } catch (error) { + createLog((error as Error).message, 'error'); + } }; - const commitItemCreation = (name: string) => { - createNewItem( - node.id as string, - name, - newItemAdd, - projectId as string, - ).catch(() => {}); - reset(); + const updateItemTypeCreation = (type: Tree['type']) => { + if (!isAllowed()) return; + if (node.droppable && !isOpen) { + onToggle(node.id); + } + setNewItemAdd(type); }; const reset = () => { @@ -86,8 +103,14 @@ const TreeNode: FC = ({ node, depth, isOpen, onToggle }) => { return ['Edit', 'Close']; }; - const deleteItemFromNode = () => { - deleteItem(node.id as string, projectId as string); + const deleteItemFromNode = async () => { + const nodePath = node.data?.path; + if (!nodePath) { + createLog(`'${nodePath}' not found`, 'error'); + return; + } + + await deleteProjectFile(nodePath); }; const isAllowed = () => { @@ -111,6 +134,11 @@ const TreeNode: FC = ({ node, depth, isOpen, onToggle }) => { [`${fileTypeFromFileName(node.text)}-lang-file-icon`]: !droppable, }); + // Hide ./ide/settings.json file + if (node.data?.path.includes('.ide')) { + return null; + } + return ( <>
= ({ node, depth, isOpen, onToggle }) => { }`} > {node.text} - {isProjectEditable() && ( - { - handleItemAction(); - }} - allowedActions={getAllowedActions() as actionsTypes[]} - onNewFile={() => { - if (!isAllowed()) { - return; - } - setNewItemAdd('file'); - }} - onNewDirectory={() => { - if (!isAllowed()) { - return; - } - setNewItemAdd('directory'); - }} - onDelete={() => { - deleteItemFromNode(); - }} - /> - )} + { + handleItemAction(); + }} + allowedActions={getAllowedActions() as actionsTypes[]} + onNewFile={() => { + updateItemTypeCreation('file'); + }} + onNewDirectory={() => { + updateItemTypeCreation('directory'); + }} + onDelete={() => { + deleteItemFromNode().catch(() => {}); + }} + />
)} @@ -163,7 +183,7 @@ const TreeNode: FC = ({ node, depth, isOpen, onToggle }) => {
{newItemAdd && ( = ({ return (
{type === 'directory' ? ( - + ) : ( = ({ inputRef, defaultValue, style }) => { +const FolderEdit: FC = ({ inputRef, defaultValue }) => { return ( - + ); }; @@ -109,19 +99,9 @@ interface FileEditProps { style?: React.CSSProperties; } -const FileEdit: FC = ({ - inputRef, - updateExt, - defaultValue, - style, -}) => { +const FileEdit: FC = ({ inputRef, updateExt, defaultValue }) => { return ( - + ); }; diff --git a/src/constant/UserOnboardingSteps.tsx b/src/constant/UserOnboardingSteps.tsx deleted file mode 100644 index 6fda950..0000000 --- a/src/constant/UserOnboardingSteps.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { Placement } from 'react-joyride'; - -export const userOnboardingSteps = { - styleConfiguration: { - options: { - arrowColor: '#b6c4b0', - backgroundColor: '#b6c4b0', - overlayColor: 'rgba(0, 0, 0, 0.6)', - primaryColor: '#000', - textColor: '#000', - zIndex: 1000000, - width: 450, - }, - }, - steps: [ - { - target: '.onboarding-new-project', - content: 'Create a new project', - title: 'Welcome to Nujan', - offset: -10, - disableBeacon: true, - afterEvent: 'ONBOARDING_NEW_PROJECT', - }, - { - target: '.onboarding-new-project-form', - disableBeacon: true, - content: ( -
-

1. What would you like to name your project?

-

2. Choose a template or start from scratch

-
- ), - }, - { - target: '.onboarding-workspace-sidebar', - disableBeacon: true, - placement: 'right-start' as Placement, - content: ( -
-

- Home: Return to the project listing screen. -

-

- Code: You can start writing your smart contract from here. -

-

- Build & Deploy: Build and deploy contract to Sandbox, - Testnet, Mainnet -

-
- ), - }, - { - target: '.onboarding-file-explorer', - disableBeacon: true, - placement: 'right-start' as Placement, - title: 'File Explorer', - content: ( -
-

You can manage your files and folder here.

-

- message.cell.ts: Contains a cell which will be used for - sending internal message to deployed contract. -

-

- main.fc: It is a main contract file which will be compiled. -

-

- stateInit.cell.ts: Contains a cell which will be used to - deploy the contract. This will be initial state of the contract. -

-

- stdlib.fc: This file is part of TON FunC Standard Library. -

-
- ), - }, - { - target: '.onboarding-code-editor', - title: 'Code Editor', - content: 'Write your code here', - disableBeacon: true, - name: 'codeEditor', - }, - { - target: '.onboarding-build-deploy', - title: 'Build & Deploy', - content: ( -
-

- Sandbox: It is a local TON network. Allows you to emulate TON - smart contracts, send messages to them and run get methods on them - as if they were deployed on a real network. -

-

- Testnet: It is network to test your contract before deploying - to main network. To deploy on it you can use test TON coin. -

-

- Mainnet: It allows you to deploy you contract on mainnet. You - need to have real TON coin to deploy on it. -

-
- ), - disableBeacon: true, - name: 'buildDeploy', - placement: 'right-start' as Placement, - }, - ], -}; diff --git a/src/enum/file.ts b/src/enum/file.ts index ae2af90..483017f 100644 --- a/src/enum/file.ts +++ b/src/enum/file.ts @@ -32,5 +32,6 @@ export enum FileExtensionToFileType { rs = FileType.Rust, fc = FileType.FC, tact = FileType.TACT, + json = FileType.JSON, } /* eslint-enable @typescript-eslint/prefer-literal-enum-member */ diff --git a/src/hooks/auth.hooks.ts b/src/hooks/auth.hooks.ts deleted file mode 100644 index 26becca..0000000 --- a/src/hooks/auth.hooks.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { AuthInterface } from '@/interfaces/auth.interface'; -import { authState } from '@/state/auth.state'; -import { useRecoilState } from 'recoil'; - -export function useAuthAction() { - const [authDetails, setAuthDetails] = useRecoilState(authState); - - return { - updateAuth, - user: user(), - logout, - }; - - function updateAuth(userInfo: AuthInterface) { - setAuthDetails(userInfo); - } - - function user() { - return authDetails; - } - - function logout() { - setAuthDetails({ - id: '', - walletAddress: '', - token: '', - }); - } -} diff --git a/src/hooks/file.hooks.ts b/src/hooks/file.hooks.ts new file mode 100644 index 0000000..47bd75a --- /dev/null +++ b/src/hooks/file.hooks.ts @@ -0,0 +1,20 @@ +import fileSystem from '@/lib/fs'; + +const useFile = () => { + const getFile = async (filePath: string) => { + return fileSystem.readFile(filePath); + }; + + const saveFile = async (filePath: string, content: string) => { + return fileSystem.writeFile(filePath, content, { + overwrite: true, + }); + }; + + return { + getFile, + saveFile, + }; +}; + +export default useFile; diff --git a/src/hooks/fileTabs.hooks.ts b/src/hooks/fileTabs.hooks.ts new file mode 100644 index 0000000..a53e960 --- /dev/null +++ b/src/hooks/fileTabs.hooks.ts @@ -0,0 +1,112 @@ +import fileSystem from '@/lib/fs'; +import { IDEContext, IFileTab } from '@/state/IDE.context'; +import EventEmitter from '@/utility/eventEmitter'; +import { useContext } from 'react'; + +const useFileTab = () => { + const { fileTab, setFileTab, activeProject } = useContext(IDEContext); + + const syncTabSettings = async (updatedTab?: IFileTab) => { + if (!activeProject || Object.keys(activeProject).length === 0) return; + + const defaultSetting = { + tab: { + items: [], + active: null, + }, + }; + + try { + const settingPath = `${activeProject.path}/.ide/setting.json`; + if (!(await fileSystem.exists(settingPath))) { + await fileSystem.writeFile( + settingPath, + JSON.stringify(defaultSetting, null, 4), + { + 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, + }, + ); + EventEmitter.emit('FORCE_UPDATE_FILE', settingPath); + } catch (error) { + console.error('Error syncing tab settings:', error); + } + }; + + const open = (name: string, path: string) => { + if (fileTab.active === path) return; + + const existingTab = fileTab.items.find((item) => item.path === path); + + if (existingTab) { + const updatedTab = { ...fileTab, active: path }; + syncTabSettings(updatedTab); + } else { + const newTab = { name, path, isDirty: false }; + const updatedTab = { + ...fileTab, + items: [...fileTab.items, newTab], + active: path, + }; + 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; diff --git a/src/hooks/index.ts b/src/hooks/index.ts new file mode 100644 index 0000000..50353d1 --- /dev/null +++ b/src/hooks/index.ts @@ -0,0 +1,2 @@ +export { default as useFile } from './file.hooks'; +export { default as useFileTab } from './fileTabs.hooks'; diff --git a/src/hooks/project.hooks.ts b/src/hooks/project.hooks.ts index a0b4c1f..4ead1e3 100644 --- a/src/hooks/project.hooks.ts +++ b/src/hooks/project.hooks.ts @@ -1,15 +1,5 @@ -import { - ProjectTemplate as ProjectTemplateData, - commonProjectFiles, -} from '@/constant/ProjectTemplate'; -import { - ContractLanguage, - Project, - ProjectTemplate, - Tree, -} from '@/interfaces/workspace.interface'; +import { Project, Tree } from '@/interfaces/workspace.interface'; import { OverwritableVirtualFileSystem } from '@/utility/OverwritableVirtualFileSystem'; -import { FileInterface } from '@/utility/fileSystem'; import { extractCompilerDiretive, parseGetters } from '@/utility/getterParser'; import { LogLevel, @@ -19,6 +9,7 @@ import { import stdLibFiles from '@tact-lang/compiler/dist/imports/stdlib'; import { precompile } from '@tact-lang/compiler/dist/pipeline/precompile'; +import fileSystem from '@/lib/fs'; import { getContractInitParams } from '@/utility/abi'; import TactLogger from '@/utility/tactLogger'; import { CompilerContext } from '@tact-lang/compiler/dist/context'; @@ -27,100 +18,44 @@ import { SuccessResult, compileFunc, } from '@ton-community/func-js'; -import { BlobReader, TextWriter, ZipReader } from '@zip.js/zip.js'; -import { RcFile } from 'antd/es/upload'; -import cloneDeep from 'lodash.clonedeep'; -import { v4 as uuidv4 } from 'uuid'; +import useFile from './file.hooks'; +import { useProject } from './projectV2.hooks'; import { useSettingAction } from './setting.hooks'; -import { useWorkspaceActions } from './workspace.hooks'; export function useProjectActions() { - const { - createNewProject, - getFileByPath, - addFilesToDatabase, - createFiles, - projectFiles, - } = useWorkspaceActions(); const { isContractDebugEnabled } = useSettingAction(); + const { writeFiles, projectFiles } = useProject(); + const { getFile } = useFile(); return { - createProject, compileFuncProgram, compileTactProgram, }; - async function createProject( - name: string, - language: ContractLanguage, - template: ProjectTemplate, - file: RcFile | null, - defaultFiles?: Tree[], - ) { - let { files, filesWithId } = - template === 'import' && defaultFiles?.length == 0 - ? await importUserFile(file as RcFile, language) - : createTemplateBasedProject(template, language, defaultFiles); - - const convertedFileObject: Record = files.reduce( - (acc, current) => { - acc[current.name] = current; - return acc; - }, - {} as Record, - ); - - if ( - (!convertedFileObject['stateInit.cell.ts'] || - !convertedFileObject['message.cell.ts']) && - language !== 'tact' - ) { - const commonFiles = createTemplateBasedProject( - 'import', - language, - commonProjectFiles, - ); - files = [...files, ...commonFiles.files]; - filesWithId = [...filesWithId, ...commonFiles.filesWithId]; - } - - addFilesToDatabase(filesWithId); - const projectId = uuidv4(); - const project = { - id: projectId, - name, - language, - template, - }; - - createNewProject({ ...project }, files); - return projectId; - } - async function compileFuncProgram( file: Pick, projectId: Project['id'], ) { - const fileList: Record = {}; + const fileList: Record = {}; const filesToProcess = [file.path]; while (filesToProcess.length !== 0) { - const fileToProcess = filesToProcess.pop(); - const file = await getFileByPath(fileToProcess, projectId); - if (file?.content) { - fileList[file.id] = file; - } - if (!file?.content) { + const singleFileToProcess = filesToProcess.pop(); + const fileContent = await getFile(singleFileToProcess!); + if (!fileContent) { continue; } - let compileDirectives = await extractCompilerDiretive(file.content); + fileList[singleFileToProcess!] = fileContent as string; + let compileDirectives = await extractCompilerDiretive( + fileContent as string, + ); compileDirectives = compileDirectives.map((d: string) => { - const pathParts = file.path?.split('/'); - if (!pathParts) { - return d; - } + const pathParts = file.path.split('/'); + // if (!pathParts) { + // return d; + // } // Convert relative path to absolute path by prepending the current file directory if (pathParts.length > 1) { @@ -137,15 +72,10 @@ export function useProjectActions() { } filesToProcess.push(...compileDirectives); } - const filesCollection: Tree[] = Object.values(fileList); const buildResult: CompileResult = await compileFunc({ targets: [file.path!], sources: (path) => { - const file = filesCollection.find((f: Tree) => f.path === path); - if (file?.content) { - fileList[file.id] = file; - } - return file?.content ?? ''; + return fileList[path] ?? ''; }, }); @@ -155,25 +85,26 @@ export function useProjectActions() { const abi = await generateABI(fileList); - const contractName = file.path?.replace('.fc', ''); - await createFiles( - [ - { - path: `dist/func_${contractName}.abi`, - content: JSON.stringify({ - name: contractName, - getters: abi, - setters: [], - }), - }, - { - path: `dist/func_${contractName}.code.boc`, - content: (buildResult as SuccessResult).codeBoc, - }, - ], - 'dist', - projectId, - ); + const contractName = file.path + .replace(`${projectId}/`, '') + .replace('.fc', ''); + const buildFiles = [ + { + path: `${projectId}/dist/func_${contractName}.abi`, + content: JSON.stringify({ + name: contractName, + getters: abi, + setters: [], + }), + type: 'file' as const, + }, + { + path: `${projectId}/dist/func_${contractName}.code.boc`, + content: (buildResult as SuccessResult).codeBoc, + type: 'file' as const, + }, + ]; + await writeFiles(projectId, buildFiles, { overwrite: true }); return { contractBOC: (buildResult as SuccessResult).codeBoc }; } @@ -183,33 +114,34 @@ export function useProjectActions() { ) { const filesToProcess = [file.path]; - projectFiles(projectId).forEach((f) => { + projectFiles.forEach((f) => { if ( /\.(tact|fc|func)$/.test(f.name) && !filesToProcess.includes(f.path) && - !f.path?.startsWith('dist/') + !f.path.startsWith('dist/') ) { filesToProcess.push(f.path); } }); - const fs = new OverwritableVirtualFileSystem(); + const fs = new OverwritableVirtualFileSystem(`/`); while (filesToProcess.length !== 0) { const fileToProcess = filesToProcess.pop(); - const file = await getFileByPath(fileToProcess, projectId); - if (file?.path) { - fs.writeContractFile(file.path!, file.content ?? ''); + const fileContent = await fileSystem.readFile(fileToProcess!); + if (fileContent) { + fs.writeContractFile(fileToProcess!, fileContent as string); } } let ctx = new CompilerContext({ shared: {} }); const stdlib = createVirtualFileSystem('@stdlib', stdLibFiles); - ctx = precompile(ctx, fs, stdlib, file.path!); + const entryFile = file.path; + ctx = precompile(ctx, fs, stdlib, entryFile); const response = await buildTact({ config: { - path: file.path!, + path: entryFile, output: 'dist', name: 'tact', options: { @@ -246,9 +178,9 @@ export function useProjectActions() { } }); - const buildFiles: Pick[] = []; + const buildFiles: Pick[] = []; fs.overwrites.forEach((value, key) => { - const filePath = key.slice(1); + const filePath = `${projectId}/${key.slice(1)}`; let fileContent = value.toString(); if (key.includes('.abi')) { @@ -269,131 +201,21 @@ export function useProjectActions() { buildFiles.push({ path: filePath, content: fileContent, + type: 'file', }); // TODO: Do this after the build files are updated. // EventEmitter.emit('FORCE_UPDATE_FILE', filePath); }); - await createFiles(buildFiles, 'dist', projectId); - + await writeFiles(projectId, buildFiles, { overwrite: true }); return fs.overwrites; } - async function generateABI(fileList: Record) { + async function generateABI(fileList: Record) { const unresolvedPromises = Object.values(fileList).map(async (file) => { - if (!file.content) { - return; - } - return await parseGetters(file.content); + return await parseGetters(file); }); const results = await Promise.all(unresolvedPromises); return results[0]; } } - -const createTemplateBasedProject = ( - template: 'tonBlank' | 'tonCounter' | 'import', - language: ContractLanguage = 'tact', - files: Tree[] = [], -) => { - let _files: Tree[] = cloneDeep(files); - if (files.length === 0 && template !== 'import') { - _files = ProjectTemplateData[template][language]; - } - const filesWithId: FileInterface[] = []; - - _files = _files.map((file) => { - if (file.type !== 'file') { - return file; - } - const fileId = uuidv4(); - filesWithId.push({ id: fileId, content: file.content ?? '' }); - return { - ...file, - id: fileId, - content: '', - }; - }); - return { files: _files, filesWithId }; -}; - -const importUserFile = async ( - file: RcFile, - language: ContractLanguage = 'tact', -) => { - const sysrootArchiveReader = new ZipReader(new BlobReader(file)); - const sysrootArchiveEntries = await sysrootArchiveReader.getEntries(); - const filesToSkip = [ - '._', - '._.DS_Store', - '.DS_Store', - 'node_modules', - 'build', - '.git', - '.zip', - ]; - const files: Tree[] = []; - - const fileDirectoryMap: Record = {}; - - // for storing file in indexed DB - const filesWithId: FileInterface[] = []; - for (const entry of sysrootArchiveEntries) { - if (filesToSkip.some((file) => entry.filename.includes(file))) { - continue; - } - const filePath = entry.filename; - const pathParts = filePath.split('/'); - const fileName = pathParts[pathParts.length - 1]; - const fileDirectory = pathParts.slice(0, pathParts.length - 1).join('/'); - const currentDirectory = fileDirectory.split('/').slice(-1)[0]; - let parentDirectory = ''; - let fileContent = ''; - - if (entry.directory) { - parentDirectory = fileDirectory.split('/').slice(0, -1).join('/'); - } - - const fileId = uuidv4(); - - const currentFile: Tree = { - id: fileId, - name: entry.directory ? currentDirectory : fileName, - type: entry.directory ? 'directory' : 'file', - parent: null, - path: filePath.replace(/^\/|\/$/g, ''), // remove last slash - }; - - currentFile.parent = - fileDirectoryMap[fileDirectory] || fileDirectoryMap[parentDirectory]; - - if (entry.directory && fileDirectory) { - fileDirectoryMap[fileDirectory] = fileId; - } - - if (!entry.directory) { - fileContent = await entry.getData!(new TextWriter()); - } - - filesWithId.push({ id: fileId, content: fileContent }); - files.push(currentFile); - } - - let commonFiles: { files: Tree[]; filesWithId: FileInterface[] } = { - files: [], - filesWithId: [], - }; - - if (language !== 'tact') { - commonFiles = createTemplateBasedProject( - 'import', - language, - commonProjectFiles, - ); - } - - return { - files: [...files, ...commonFiles.files], - filesWithId: [...filesWithId, ...commonFiles.filesWithId], - }; -}; diff --git a/src/hooks/projectV2.hooks.ts b/src/hooks/projectV2.hooks.ts new file mode 100644 index 0000000..20d2ca4 --- /dev/null +++ b/src/hooks/projectV2.hooks.ts @@ -0,0 +1,381 @@ +import { + ProjectTemplate as ProjectTemplateData, + commonProjectFiles, +} from '@/constant/ProjectTemplate'; +import { + ABIFormInputValues, + ContractLanguage, + ProjectSetting, + ProjectTemplate, + Tree, +} from '@/interfaces/workspace.interface'; +import fileSystem from '@/lib/fs'; +import ZIP from '@/lib/zip'; +import EventEmitter from '@/utility/eventEmitter'; +import { RcFile } from 'antd/es/upload'; +import cloneDeep from 'lodash.clonedeep'; +import { useContext } from 'react'; +import { IDEContext } from '../state/IDE.context'; + +export interface FileNode { + name: string; + path: string; + type: 'file' | 'directory'; + parent?: string; + content?: string; +} + +export const baseProjectPath = '/projects'; + +export const useProject = () => { + const { + projects, + setProjects, + activeProject, + setActiveProject, + projectFiles, + setProjectFiles, + setFileTab, + } = useContext(IDEContext); + + const loadProjects = async () => { + let projectCollection: string[] = []; + try { + projectCollection = await fileSystem.readdir(baseProjectPath, { + onlyDir: true, + }); + + // remove base path from project path + projectCollection = projectCollection.map((project) => { + return project.replace(baseProjectPath, ''); + }); + } catch (error) { + try { + await fileSystem.create(baseProjectPath, 'directory'); + } catch (error) { + /* empty */ + } + } finally { + setProjects([...projectCollection]); + } + }; + + const createProject = async ( + name: string, + language: ContractLanguage, + template: ProjectTemplate, + file: RcFile | null, + defaultFiles?: Tree[], + autoActivate = true, + ) => { + const projectDirectory = await fileSystem.mkdir( + `${baseProjectPath}/${name}`, + { + overwrite: false, + }, + ); + if (!projectDirectory) return; + + let files = + template === 'import' && defaultFiles?.length == 0 + ? await new ZIP(fileSystem).importZip(file as RcFile, projectDirectory) + : createTemplateBasedProject( + template, + language, + defaultFiles, + projectDirectory, + ); + + const fileMapping: Record | undefined> = files.reduce( + (acc, current) => { + acc[current.path] = current; + return acc; + }, + {} as Record>, + ); + + if ( + (!fileMapping[`${projectDirectory}/stateInit.cell.ts`] || + !fileMapping[`${projectDirectory}/message.cell.ts`]) && + language === 'func' + ) { + const commonFiles = createTemplateBasedProject( + 'import', + language, + commonProjectFiles, + projectDirectory, + ); + files = [...files, ...commonFiles]; + } + + const project = { + name: projectDirectory.replace(baseProjectPath + '/', ''), + language, + template, + }; + + await writeFiles(projectDirectory, files); + + const projectSettingPath = `${projectDirectory}/.ide/setting.json`; + if (!(await fileSystem.exists(projectSettingPath))) { + await fileSystem.writeFile( + projectSettingPath, + JSON.stringify({ ...project }), + ); + } + await loadProjects(); + + if (autoActivate) { + setActiveProject({ + path: projectDirectory, + ...project, + }); + } + + return projectDirectory; + }; + + const writeFiles = async ( + projectPath: string, + files: Pick[], + options?: { overwrite?: boolean }, + ) => { + await Promise.all( + files.map(async (file) => { + if (file.type === 'directory') { + return fileSystem.mkdir(file.path); + } + await fileSystem.writeFile(file.path, file.content ?? '', options); + EventEmitter.emit('FORCE_UPDATE_FILE', file.path); + return file.path; + }), + ); + EventEmitter.emit('RELOAD_PROJECT_FILES', projectPath); + }; + + const loadProjectFiles = async (projectPath: string) => { + let projectFiles: FileNode[] = []; + try { + projectFiles = await readdirTree(projectPath); + } catch (error) { + console.log('Error loading project files', error); + /* empty */ + } finally { + setProjectFiles(projectFiles as Tree[]); + } + }; + + /** + * Read the contents of a directory in a tree structure + * @param path + * @returns FileNode[] + */ + const readdirTree = async ( + path: string, + options: { basePath: null | string; content: boolean } = { + basePath: null, + content: false, + }, + filter?: (fileNode: FileNode) => boolean, + ): Promise => { + const results: FileNode[] = []; + const basePath = options.basePath ?? path; + + const files = await fileSystem.readdir(path); + + for (const file of files) { + const filePath = `${path}/${file}`; + const stat = await fileSystem.stat(filePath); + const fileNode: FileNode = { + name: file, + path: filePath, + type: stat.isDirectory() ? 'directory' : 'file', + parent: path === basePath ? undefined : path, + content: options.content + ? ((await fileSystem.readFile(filePath)) as string) + : '', + }; + + if (!filter || filter(fileNode)) { + results.push(fileNode); + } + + if (stat.isDirectory()) { + const nestedFiles = await readdirTree( + filePath, + { + basePath, + content: options.content, + }, + filter, + ); + results.push(...nestedFiles); + } + } + + return results; + }; + + const deleteProject = async (projectName: string) => { + await fileSystem.rmdir(projectName, { recursive: true }); + await loadProjects(); + setProjectFiles([]); + setFileTab({ items: [], active: null }); + + return projectName; + }; + + const deleteAllProjects = async () => { + await fileSystem.rmdir(baseProjectPath, { recursive: true }); + setProjectFiles([]); + setFileTab({ items: [], active: null }); + setActiveProject(null); + await loadProjects(); + }; + + const newFileFolder = async (path: string, type: 'file' | 'directory') => { + if (!activeProject?.path) return; + const newPath = `${activeProject.path}/${path}`; + await fileSystem.create(newPath, type); + await loadProjectFiles(activeProject.path); + }; + + const deleteProjectFile = async (path: string) => { + if (!activeProject?.path) return; + await fileSystem.remove(path, { + recursive: true, + }); + await loadProjectFiles(activeProject.path); + }; + + const moveItem = async (oldPath: string, targetPath: string) => { + if (!activeProject?.path) return; + if (oldPath === targetPath) return; + + const newPath = targetPath + '/' + oldPath.split('/').pop(); + + await fileSystem.rename(oldPath, newPath); + await loadProjectFiles(activeProject.path); + }; + + const renameProjectFile = async (oldPath: string, newName: string) => { + if (!activeProject?.path) return; + const newPath = oldPath.includes('/') + ? oldPath.split('/').slice(0, -1).join('/') + '/' + newName + : newName; + + const success = await fileSystem.rename(oldPath, newPath); + if (!success) return; + await loadProjectFiles(activeProject.path); + }; + + const updateActiveProject = async ( + projectPath: string | null, + force = false, + ) => { + if (activeProject?.path === projectPath && !force) return; + const projectSettingPath = `${projectPath}/.ide/setting.json`; + if (projectPath && (await fileSystem.exists(projectSettingPath))) { + const setting = (await fileSystem.readFile(projectSettingPath)) as string; + const parsedSetting = setting ? JSON.parse(setting) : {}; + setActiveProject({ + ...parsedSetting, + path: projectPath, + }); + } else { + setActiveProject(null); + } + }; + + const updateProjectSetting = async (itemToUpdate: ProjectSetting) => { + if (!activeProject?.path) return; + const projectSettingPath = `${activeProject.path}/.ide/setting.json`; + if (!(await fileSystem.exists(projectSettingPath))) { + await fileSystem.writeFile(projectSettingPath, JSON.stringify({})); + } else { + const setting = (await fileSystem.readFile(projectSettingPath)) as string; + const parsedSetting = setting ? JSON.parse(setting) : {}; + await fileSystem.writeFile( + projectSettingPath, + JSON.stringify({ ...parsedSetting, ...itemToUpdate }), + { + overwrite: true, + }, + ); + await updateActiveProject(activeProject.path, true); + } + await loadProjectFiles(activeProject.path); + }; + + function updateABIInputValues(inputValues: ABIFormInputValues) { + if (!activeProject) { + return; + } + const formInputValues = cloneDeep(inputValues); + const abiFormInputValues = + cloneDeep(activeProject.abiFormInputValues) ?? []; + const index = abiFormInputValues.findIndex( + (item) => + item.key === formInputValues.key && item.type === formInputValues.type, + ); + if (index < 0) { + abiFormInputValues.push(formInputValues); + } else { + abiFormInputValues[index] = formInputValues; + } + updateProjectSetting({ + abiFormInputValues, + }); + } + + function getABIInputValues(key: string, type: string) { + if (!activeProject) { + return []; + } + return activeProject.abiFormInputValues?.find( + (item) => item.type === type && item.key === key, + )?.value; + } + + return { + projects, + projectFiles, + activeProject, + createProject, + writeFiles, + deleteProject, + readdirTree, + newFileFolder, + deleteProjectFile, + deleteAllProjects, + moveItem, + renameProjectFile, + setActiveProject: updateActiveProject, + loadProjectFiles, + loadProjects, + updateProjectSetting, + getABIInputValues, + updateABIInputValues, + }; +}; + +const createTemplateBasedProject = ( + template: 'tonBlank' | 'tonCounter' | 'import', + language: ContractLanguage = 'tact', + files: Tree[] = [], + basePath?: string, +) => { + let _files: Pick[] = cloneDeep(files); + if (files.length === 0 && template !== 'import') { + _files = ProjectTemplateData[template][language]; + } + + _files = _files.map((file) => { + return { + type: file.type, + path: `${basePath}/${file.path}`, + content: file.content, + }; + }); + return _files; +}; diff --git a/src/hooks/setting.hooks.ts b/src/hooks/setting.hooks.ts index 126625e..2c0efe6 100644 --- a/src/hooks/setting.hooks.ts +++ b/src/hooks/setting.hooks.ts @@ -1,11 +1,17 @@ import { SettingInterface } from '@/interfaces/setting.interface'; -import { settingState } from '@/state/setting.state'; -import { useRecoilState } from 'recoil'; +import fileSystem from '@/lib/fs'; +import { IDEContext } from '@/state/IDE.context'; +import EventEmitter from '@/utility/eventEmitter'; +import { useContext } from 'react'; +import { baseProjectPath } from './projectV2.hooks'; export function useSettingAction() { - const [setting, updateSetting] = useRecoilState(settingState); + const { setting, setSetting } = useContext(IDEContext); + const settingPath = `${baseProjectPath}/setting.json`; return { + setting, + init, getSettingStateByKey, isContractDebugEnabled, toggleContractDebug, @@ -18,13 +24,32 @@ export function useSettingAction() { updateEditorMode, }; - function updateStateByKey(dataByKey: Partial) { - updateSetting((oldState) => { - return { - ...oldState, - ...dataByKey, - }; - }); + async function init() { + const isSettingExists = await fileSystem.exists(settingPath); + if (!isSettingExists) { + await fileSystem.writeFile(settingPath, JSON.stringify(setting)); + } + const settingData = await fileSystem.readFile(settingPath); + setSetting(JSON.parse(settingData as string)); + } + + async function updateStateByKey(dataByKey: Partial) { + const newState = { + ...setting, + ...dataByKey, + }; + try { + await fileSystem.writeFile(settingPath, JSON.stringify(newState), { + overwrite: true, + }); + setSetting(newState); + } catch (error) { + EventEmitter.emit('LOG', { + text: `Setting update error: ${(error as Error).message}`, + type: 'error', + timestamp: Date.now().toLocaleString(), + }); + } } function getSettingStateByKey(key: keyof SettingInterface) { diff --git a/src/hooks/userOnboarding.hooks.ts b/src/hooks/userOnboarding.hooks.ts deleted file mode 100644 index adff7f1..0000000 --- a/src/hooks/userOnboarding.hooks.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { userOnboardingState } from '@/state/userOnboarding.state'; -import { useRecoilState } from 'recoil'; - -export function useUserOnboardingAction() { - const [userOnboarding, setUserOnboarding] = - useRecoilState(userOnboardingState); - - return { - onboarding, - stepIndex, - updateStepIndex, - startOnboarding, - stopOnboarding, - }; - - function onboarding() { - return userOnboarding; - } - - function stepIndex() { - return userOnboarding.stepIndex; - } - - function updateStepIndex(stepIndex: number) { - setUserOnboarding({ - ...userOnboarding, - stepIndex, - }); - } - - function startOnboarding(stepIndex: number = -1) { - setUserOnboarding({ - ...userOnboarding, - stepIndex: stepIndex >= 0 ? stepIndex : onboarding().stepIndex, - run: true, - tourActive: true, - }); - } - - function stopOnboarding() { - setUserOnboarding({ - ...userOnboarding, - tourActive: false, - run: false, - }); - } -} diff --git a/src/hooks/workspace.hooks.ts b/src/hooks/workspace.hooks.ts index 3cd7663..db83d7d 100644 --- a/src/hooks/workspace.hooks.ts +++ b/src/hooks/workspace.hooks.ts @@ -1,574 +1,43 @@ -import { - ABIFormInputValues, - Project, - Tree, - WorkspaceState, -} from '@/interfaces/workspace.interface'; -import { workspaceState } from '@/state/workspace.state'; -import { FileInterface, fileSystem } from '@/utility/fileSystem'; +import { Project, Tree } from '@/interfaces/workspace.interface'; import { buildTs } from '@/utility/typescriptHelper'; -import { notification } from 'antd'; -import cloneDeep from 'lodash.clonedeep'; -import { useRecoilState } from 'recoil'; import { OutputChunk } from 'rollup'; -import { v4 } from 'uuid'; +import { useProject } from './projectV2.hooks'; export { useWorkspaceActions }; function useWorkspaceActions() { - const [workspace, updateWorkspace] = useRecoilState(workspaceState); + const { readdirTree } = useProject(); return { - createNewProject, - deleteProject, - setProjects, - projects, - project, - projectFiles, - updateProjectFiles, - addFilesToDatabase, - openFile, - openFileByPath, - updateOpenFile, - renameItem, - deleteItem, - moveFile, - createNewItem, - createFiles, - openedFiles, - activeFile, - getFileById, - getFileContent, - getFileByPath, - closeFile, - updateFileContent, - updateProjectById, - closeAllFile, - getAllFilesWithContent, compileTsFile, isProjectEditable, - updateABIInputValues, - getABIInputValues, - clearWorkSpace, }; - function updateStateByKey(dataByKey: Partial) { - updateWorkspace((oldState) => { - return { - ...oldState, - ...dataByKey, - }; - }); - } - - function createNewProject(project: Project, template: Tree[]) { - if (projects().findIndex((p) => p.name == project.name) >= 0) { - throw new Error('Project with the same name already exists'); - } - updateStateByKey({ - projects: [...workspace.projects, project], - projectFiles: { ...workspace.projectFiles, [project.id]: template }, - }); - } - - async function deleteProject(projectId: string) { - const projectIndex = projects().findIndex((item) => item.id === projectId); - if (projectIndex < 0) { - return; - } - let _projectFiles = cloneDeep(workspace.projectFiles); - - if (_projectFiles?.[projectId]) { - const fileIds = _projectFiles[projectId].map((item) => item.id); - await fileSystem.files.bulkDelete(fileIds); - - // delete project files - const { [projectId]: _, ...rest } = _projectFiles; - _projectFiles = rest; - } - const projectList = [...workspace.projects]; - projectList.splice(projectIndex, 1); - updateStateByKey({ - projects: projectList, - projectFiles: _projectFiles, - }); - } - - function setProjects(projects: Project[]) { - updateStateByKey({ - projects, - }); - } - - function updateProjectList(projectId: string, projectListItem: Project) { - const projectIndex = projects().findIndex((item) => item.id === projectId); - if (projectIndex < 0) { - return; - } - const projectList = [...workspace.projects]; - projectList[projectIndex] = { - ...projectList[projectIndex], - ...projectListItem, - }; - updateStateByKey({ - projects: projectList, - }); - } - - function projects() { - return workspace.projects; - } - - function project(projectId: string) { - return projects().find((p) => p.id === projectId); - } - - function projectFiles(projectId: string): Tree[] { - return workspace.projectFiles?.[projectId] ?? []; - } - - function updateProjectFiles(project: Tree[], projectId: string) { - updateStateByKey({ - projectFiles: { ...workspace.projectFiles, [projectId]: project }, - }); - } - - function addFilesToDatabase(files: FileInterface[]) { - fileSystem.files.bulkAdd(files).catch(() => {}); - } - - function getFile(id: Tree['id'], projectId: string) { - return projectFiles(projectId).find((item) => item.id == id); - } - - function openFile(id: Tree['id'], projectId: string) { - const openFiles = openedFiles(projectId).map((item) => { - return { - ...item, - isOpen: false, - }; - }); - - const currentFile = getFile(id, projectId); - if (!currentFile) { - return; - } - - const isAlreadyOpend = openFiles.find((item) => item.id === id); - if (isAlreadyOpend) { - isAlreadyOpend.isOpen = true; - } else { - const fileData = { - id: currentFile.id, - name: currentFile.name, - path: currentFile.path, - }; - openFiles.push({ ...(fileData as Tree), isOpen: true }); - } - - updateStateByKey({ - openFiles: { ...workspace.openFiles, [projectId]: openFiles }, - }); - } - - function openFileByPath(path: string, projectId: string) { - const file = projectFiles(projectId).find((item) => item.path === path); - if (!file) { - return; - } - openFile(file.id, projectId); - } - - function updateOpenFile( - id: Tree['id'], - data: Partial, - projectId: Project['id'], - ) { - const openFiles = openedFiles(projectId).map((item) => { - if (item.id === id) { - return { - ...item, - ...data, - }; - } - return item; - }); - updateStateByKey({ - openFiles: { ...workspace.openFiles, [projectId]: openFiles }, - }); - } - - function onFileRename( - fileId: Tree['id'], - name: string, - projectId: Project['id'], - ) { - const files = cloneDeep(openedFiles(projectId)); - const fileToChange = files.find((item) => item.id === fileId); - if (!fileToChange) return; - fileToChange.name = name; - updateStateByKey({ - openFiles: { ...workspace.openFiles, [projectId]: files }, - }); - } - - function openedFiles(projectId: Project['id']) { - return workspace.openFiles[projectId] ?? []; - } - - function activeFile(projectId: string) { - const file = openedFiles(projectId).find((item) => item.isOpen); - if (!file) { - return undefined; - } - return file; - } - - async function getFileById( - id: Tree['id'], - projectId: string, - ): Promise { - const file = projectFiles(projectId).find((file) => file.id === id); - const fileContent = await getFileContent(id); - return { ...file, content: fileContent } as Tree | undefined; - } - - async function getFileContent(id: Tree['id']) { - if (!id) return ''; - const fileContent = await fileSystem.files.get(id); - return fileContent?.content ?? ''; - } - - async function getFileByPath( - path: Tree['path'], - projectId: string, - ): Promise { - const file = projectFiles(projectId).find((file) => file.path === path); - if (!file) { - return undefined; - } - const fileContent = await fileSystem.files.get(file.id); - return { ...file, content: fileContent?.content }; - } - - async function updateFileContent( - id: Tree['id'], - content: string, - projectId: Project['id'], - ) { - await fileSystem.files.update(id, { content }); - updateOpenFile(id, { isDirty: false }, projectId); - } - - function updateProjectById(updateObject: Project, projectId: string) { - updateProjectList(projectId, { - ...project(projectId), - ...updateObject, - }); - } - - function closeFile(id: string, projectId: Project['id']) { - let openFiles = openedFiles(projectId).filter((item) => item.id !== id); - openFiles = openFiles.map((item) => { - return { - ...item, - isOpen: false, - }; - }); - if (openFiles.length > 0) { - openFiles[openFiles.length - 1].isOpen = true; - } - updateStateByKey({ - openFiles: { ...workspace.openFiles, [projectId]: openFiles }, - }); - } - - function closeAllFile() { - // updateStateByKey({ openFiles: [] }); - } - - function renameItem(id: string, name: string, projectId: string) { - const item = searchNode(id, projectId); - if (!item.node) { - return; - } - - if (isFileExists(name, projectId, item.node.parent ?? '')) { - return; - } - item.node.name = name; - let newPath = name; - const pathArray = item.node.path?.split('/') ?? []; - if (pathArray.length > 1) { - const currentPath = pathArray.pop() ?? []; - newPath = currentPath.toString() + '/' + name; - } - item.node.path = newPath; - updateProjectFiles(item.project, projectId); - onFileRename(id, name, projectId); - } - - function deleteItem(id: Tree['id'], projectId: string) { - const item = searchNode(id, projectId); - if (!item.node) { - return; - } - - item.project = item.project.filter( - (file: Tree) => file.id !== id && file.parent !== id, - ); - - closeFile(id, projectId); - updateProjectFiles(item.project, projectId); - } - - function moveFile( - sourceId: Tree['id'], - destinationId: Tree['id'], + async function compileTsFile( + filePath: Tree['path'], projectId: Project['id'], ) { - let parent = destinationId ? destinationId : null; - - const sourceItem = searchNode(sourceId, projectId); - let sourcePath = sourceItem.node?.name; - const destinationItem = searchNode(destinationId, projectId); - if (!sourceItem.node) { - return; - } - if (!destinationId) { - parent = null; - } else { - sourcePath = destinationItem.node?.path + '/' + sourceItem.node.name; - } - - if (isFileExists(sourceItem.node.name, projectId, destinationId)) { - return; - } - - sourceItem.node.parent = parent; - sourceItem.node.path = sourcePath; - updateProjectFiles(sourceItem.project, projectId); - } - - async function createNewItem( - id: Tree['parent'] | null, - name: string, - type: string, - projectId: string, - content: string = '', - ) { - let parentId = id; - let itemName = name; - let newDirectory = ''; - const item = searchNode(id as string, projectId, 'parent'); - const currentItem = searchNode(id as string, projectId); - let filePath = currentItem.node?.path; - if (isFileExists(name, projectId, item.node?.parent ?? '')) { - return; - } - - // check if file name contains directory. Then create a directory first and then create a file - if (name.includes('/')) { - const pathArray = name.split('/'); - const fileName = [...pathArray].pop(); - itemName = fileName ?? name; - newDirectory = pathArray[0] || ''; - filePath = newDirectory; - } - if (!id && name.includes('/')) { - const newItem = _createItem('directory', newDirectory || '', '', ''); - item.project.push(newItem); - parentId = newItem.id; - } - - const newItem = _createItem( - type, - itemName, - parentId as string, - filePath ?? '', - ); - if (type === 'file') { - await fileSystem.files.add({ id: newItem.id, content: content }); - } - - item.project.push(newItem); - updateProjectFiles(item.project, projectId); - return newItem; - } - - async function createFiles( - files: Pick[], - directoryPath: string, - projectId: string, - ) { - const _projectFiles = cloneDeep(projectFiles(projectId)); - // check if file name contains directory. Then create a directory first and then create a file - let directoryItem = await getFileByPath(directoryPath, projectId); - if (!directoryItem) { - directoryItem = _createItem('directory', directoryPath || '', '', ''); - _projectFiles.push(directoryItem); + if (!filePath.endsWith('.ts')) { + throw new Error('Not a typescript file'); } + const tsProjectFiles: Record = {}; - await Promise.all( - files.map(async (file) => { - const fileName = file.path!.split('/').pop(); - let currentFile = _projectFiles.find((item) => item.name === fileName); - let isNewFile = false; - if (!currentFile) { - currentFile = _createItem( - 'file', - fileName!, - directoryItem?.id ?? '', - directoryPath || '', - ); - isNewFile = true; - } - if (isNewFile) { - await fileSystem.files.add({ - id: currentFile.id, - content: file.content ?? '', - }); - } else { - await fileSystem.files.update(currentFile.id, { - content: file.content ?? '', - }); - } - if (isNewFile) { - _projectFiles.push(currentFile); - } - }), + const filesWithContent = await readdirTree( + `/${projectId}`, + { + basePath: null, + content: true, + }, + (file: { path: string; name: string }) => file.name.endsWith('.ts'), ); - updateProjectFiles(_projectFiles, projectId); - } - - function isFileExists( - name: string, - projectId: string, - parentId: string = '', - ): boolean { - let exists = false; - if (!parentId) { - exists = !!( - projectFiles(projectId).findIndex( - (file) => file.parent == null && file.name === name, - ) >= 0 - ); - } else { - exists = - projectFiles(projectId).findIndex( - (file) => file.parent === parentId && file.name === name, - ) >= 0; - } - if (exists) { - notification.warning({ - message: name + ': Already exists', - key: name + 'exists', - }); - } - - return exists; - } - - function searchNode( - id: string, - projectId: string, - key: 'id' | 'parent' = 'id', - ): { node: Tree | null; project: Tree[] } { - const projectTemp = cloneDeep(projectFiles(projectId)); - const node = projectTemp.find((file) => file[key] === id); - - return { node: node ?? null, project: projectTemp }; - } - - function _createItem( - type: string, - name: string, - parent: string, - parentPath: string, - ) { - return { - id: v4(), - name, - parent: parent || null, - type: type as Tree['type'], - content: '', - path: `${parentPath ? parentPath + '/' : ''}${name}`, - }; - } - - async function getAllFilesWithContent( - projectId: Project['id'], - filterFunction?: (file: Tree) => boolean, - ) { - let files = projectFiles(projectId); - - if (filterFunction) { - files = files.filter(filterFunction); - } - - const filesWithContent: Record = {}; - // eslint-disable-next-line @typescript-eslint/prefer-for-of - for (let index = 0; index < files.length; index++) { - const currentFile = files[index]; - if (!currentFile.path) continue; - filesWithContent[currentFile.path] = - (await getFileById(currentFile.id, projectId))?.content ?? ''; - } - return filesWithContent; - } + filesWithContent.forEach((file) => { + tsProjectFiles[file.path!] = file.content ?? ''; + }); - async function compileTsFile(rootFile: Tree, projectId: Project['id']) { - if (!rootFile.name.endsWith('.ts')) { - throw new Error('Not a typescript file'); - } - const filesWithContent = await getAllFilesWithContent(projectId, (file) => - file.name.endsWith('.ts'), - ); - return buildTs(filesWithContent, rootFile.path) as Promise; + return buildTs(tsProjectFiles, filePath) as Promise; } function isProjectEditable() { return true; } - - function updateABIInputValues( - inputValues: ABIFormInputValues, - projectId: string, - ) { - const projectItem = project(projectId); - if (!projectItem) { - return; - } - const formInputValues = cloneDeep(inputValues); - const abiFormInputValues = cloneDeep(projectItem.abiFormInputValues) ?? []; - const index = abiFormInputValues.findIndex( - (item) => - item.key === formInputValues.key && item.type === formInputValues.type, - ); - if (index < 0) { - abiFormInputValues.push(formInputValues); - } else { - abiFormInputValues[index] = formInputValues; - } - updateProjectById( - { - abiFormInputValues, - } as Project, - projectId, - ); - } - - function getABIInputValues(projectId: string, key: string, type: string) { - const projectItem = project(projectId); - if (!projectItem) { - return []; - } - return projectItem.abiFormInputValues?.find( - (item) => item.type === type && item.key === key, - )?.value; - } - - function clearWorkSpace() { - updateStateByKey({ openFiles: {}, projectFiles: null, projects: [] }); - } } diff --git a/src/interfaces/workspace.interface.ts b/src/interfaces/workspace.interface.ts index dcd0045..2a5ccb1 100644 --- a/src/interfaces/workspace.interface.ts +++ b/src/interfaces/workspace.interface.ts @@ -1,3 +1,4 @@ +import { IFileTab } from '@/state/IDE.context'; import { ABITypeRef } from '@ton/core'; import { Maybe } from '@ton/core/dist/utils/maybe'; @@ -7,7 +8,7 @@ export interface Tree { parent: string | null; type: 'directory' | 'file'; isOpen?: boolean; - path?: string; + path: string; content?: string; isDirty?: boolean; createdAt?: Date; @@ -20,8 +21,6 @@ export type NetworkEnvironment = 'TESTNET' | 'MAINNET' | 'SANDBOX'; export type ContractLanguage = 'func' | 'tact'; -type ProjectFiles = Record; - export interface InitParams { name: string; type: string; @@ -42,9 +41,9 @@ export interface ABIFormInputValues { export interface Project { id: string; userId?: string; - name: string; + name?: string; language?: ContractLanguage; - template: string; + template?: string; contractAddress?: string; contractBOC?: string; abi?: ABI; @@ -58,14 +57,22 @@ export interface Project { updatedAt?: Date; cellABI?: CellABI; abiFormInputValues?: ABIFormInputValues[]; + path?: string; } -export type WorkspaceState = { - openFiles: ProjectFiles; - projectFiles: ProjectFiles | null; - projects: Project[]; - activeProjectId: string; -}; +export interface ProjectSetting { + name?: string; + path?: string; + template?: ProjectTemplate; + language?: ContractLanguage; + contractName?: string; + network?: NetworkEnvironment; + selectedContract?: string; + contractAddress?: string; + tab?: IFileTab; + cellABI?: CellABI; + abiFormInputValues?: ABIFormInputValues[]; +} export interface ABIParameter { type: string; diff --git a/src/lib/fs.ts b/src/lib/fs.ts new file mode 100644 index 0000000..9455a60 --- /dev/null +++ b/src/lib/fs.ts @@ -0,0 +1,260 @@ +import FS, { PromisifiedFS } from '@isomorphic-git/lightning-fs'; + +class FileSystem { + private fs: PromisifiedFS; + constructor(fs: PromisifiedFS) { + this.fs = fs; + } + + async readFile(path: string) { + if (!(await this.exists(path))) { + throw new Error(`File not found: ${path}`); + } + return this.fs.readFile(path, 'utf8'); + } + + /** + * Writes a file to the filesystem, ensuring the directory structure exists. + * + * @param path - The path where the file should be written, including the directory structure. + * @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, + options?: { overwrite?: boolean }, + ) { + const { overwrite } = options ?? {}; + const finalPath = overwrite ? path : await this.getUniquePath(path); + await this.ensureDirectoryExists(finalPath); + return this.fs.writeFile(finalPath, data); + } + + /** + * Ensures that the directory structure for a given file path exists. + * Creates any missing directories in the path. + * + * @param filePath - The full file path, including the directory structure. + * @returns A promise that resolves once the directory structure is ensured. + */ + async ensureDirectoryExists(filePath: string) { + const dirname = filePath.substring(0, filePath.lastIndexOf('/')); + if (!dirname) return; + + try { + await this.fs.mkdir(dirname); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (error: any) { + if (error.code === 'EEXIST') { + // Directory already exists, no need to do anything + } else if (error.code === 'ENOENT') { + // Parent directory does not exist, create it recursively + await this.ensureDirectoryExists(dirname); + await this.fs.mkdir(dirname); + } else { + throw error; + } + } + } + + /** + * Read the contents of a directory + * @param path + * @param options + * @returns string[] + */ + async readdir( + path: string, + options: { recursive?: boolean; basePath?: string; onlyDir?: boolean } = {}, + ) { + if (!path) return []; + const { recursive, basePath, onlyDir } = options; + if (!recursive) { + const files = await this.fs.readdir(path); + if (!onlyDir) return files; + const results: string[] = []; + for (const file of files) { + const stat = await this.fs.stat(`${path}/${file}`); + if (stat.isDirectory()) { + results.push(file); + } + } + return results; + } + let results: string[] = []; + const files = await this.readdir(path); + for (const file of files) { + const filePath = `${path}/${file}`; + const stat = await this.stat(filePath); + if (stat.isDirectory()) { + const nestedFiles = await this.readdir(filePath, { + recursive, + basePath, + onlyDir, + }); + results = results.concat(nestedFiles); + } else { + // Remove the rootPath from the file path + results.push(filePath.replace(basePath + '/', '')); + } + } + return results; + } + + async mkdir( + path: string, + options: { overwrite?: boolean } = { overwrite: true }, + ) { + if (!path) return; + const newPath = options.overwrite + ? path + : await this.getUniquePath(path, true); + await this.fs.mkdir(newPath); + return newPath; + } + + async create(path: string, type: 'file' | 'directory') { + if (await this.exists(path)) { + const name = path.substring(path.lastIndexOf('/') + 1); + throw new Error( + `File or folder already exists with the same name: ${name}`, + ); + } + if (type === 'file') { + return this.writeFile(path, ''); + } + return this.mkdir(path); + } + + async rmdir(path: string, options: { recursive?: boolean } = {}) { + if (!options.recursive) { + return this.fs.rmdir(path); + } + + const entries = await this.fs.readdir(path); + + for (const entry of entries) { + const fullPath = `${path}/${entry}`; + const stat = await this.fs.stat(fullPath); + + if (stat.isDirectory()) { + // If the entry is a directory, recursively delete its contents + await this.rmdir(fullPath, { recursive: true }); + } else { + // If the entry is a file, delete it + await this.fs.unlink(fullPath); + } + } + + // Once all the contents are deleted, remove the directory itself + return this.fs.rmdir(path); + } + + async unlink(path: string) { + return this.fs.unlink(path); + } + + async exists(path: string) { + try { + await this.fs.stat(path); + return true; + } catch (e) { + return false; + } + } + + async stat(path: string) { + return this.fs.stat(path); + } + + async rename(oldPath: string, newPath: string) { + if (oldPath === newPath) return false; + if (await this.exists(newPath)) { + throw new Error(`File or folder already exists with the same name`); + } + await this.fs.rename(oldPath, newPath); + return true; + } + + async copy(oldPath: string, newPath: string) { + const data = await this.readFile(oldPath); + await this.writeFile(newPath, data); + } + + async copyDir(oldPath: string, newPath: string) { + await this.mkdir(newPath); + const files = await this.readdir(oldPath); + for (const file of files) { + const oldFilePath = `${oldPath}/${file}`; + const newFilePath = `${newPath}/${file}`; + const stat = await this.stat(oldFilePath); + if (stat.isDirectory()) { + await this.copyDir(oldFilePath, newFilePath); + } else { + await this.copy(oldFilePath, newFilePath); + } + } + } + + async remove(path: string, options: { recursive?: boolean } = {}) { + const stat = await this.stat(path); + if (stat.isDirectory()) { + if (options.recursive) { + await this.removeDir(path); + return; + } + await this.rmdir(path); + } else { + await this.unlink(path); + } + } + + private async removeDir(path: string) { + const files = await this.readdir(path); + for (const file of files) { + const filePath = `${path}/${file}`; + const stat = await this.stat(filePath); + if (stat.isDirectory()) { + await this.removeDir(filePath); + } else { + await this.unlink(filePath); + } + } + await this.rmdir(path); + } + + async du(path = '/') { + return this.fs.du(path); + } + + // Generate a unique path if the file/directory already exists + private async getUniquePath( + path: string, + isDirectory = false, + ): Promise { + let newPath = path; + let counter = 1; + while (await this.exists(newPath)) { + const extension = isDirectory ? '' : this.getExtension(path); + const baseName = this.getBaseName(path, extension); + newPath = `${baseName}(${counter})${extension}`; + counter++; + } + return newPath; + } + + private getExtension(path: string): string { + const dotIndex = path.lastIndexOf('.'); + return dotIndex !== -1 ? path.substring(dotIndex) : ''; + } + + private getBaseName(path: string, extension: string): string { + return extension ? path.substring(0, path.length - extension.length) : path; + } +} + +const fileSystem = new FileSystem(new FS('IDE_FS').promises); +Object.freeze(fileSystem); +export default fileSystem; +export type { FileSystem }; diff --git a/src/lib/zip.ts b/src/lib/zip.ts new file mode 100644 index 0000000..2e78733 --- /dev/null +++ b/src/lib/zip.ts @@ -0,0 +1,115 @@ +import { BlobReader, BlobWriter, ZipReader, ZipWriter } from '@zip.js/zip.js'; +import { RcFile } from 'antd/es/upload'; +import { FileSystem } from './fs'; + +class ZIP { + private fs: FileSystem; + constructor(fs: FileSystem) { + this.fs = fs; + } + + async importZip(file: RcFile, outputDir: string) { + const reader = new ZipReader(new BlobReader(file)); + const entries = await reader.getEntries(); + const filesToSkip = [ + '._', + '._.DS_Store', + '.DS_Store', + 'node_modules', + 'build', + '.git', + '.zip', + ]; + + for (const entry of entries) { + const outputPath = `${outputDir}/${entry.filename}`; + + // Skip files or folders that match any pattern in filesToSkip + if (filesToSkip.some((skip) => entry.filename.includes(skip))) { + continue; + } + if (entry.directory) { + await this.fs.mkdir(outputDir); + } else if (entry.getData) { + // Ensure getData is defined before calling it + const writer = new BlobWriter(); + await entry.getData(writer); + const fileBlob = await writer.getData(); + const arrayBuffer = await fileBlob.arrayBuffer(); + await this.fs.writeFile(outputPath, new Uint8Array(arrayBuffer)); + } + } + + await reader.close(); + return []; + } + + // zip files and directories and trigger download + async bundleFilesAndDownload( + pathsToZip: string[], + zipFilename: string = 'archive.zip', + ) { + // Create a BlobWriter for the ZipWriter to write the zip file data to a Blob + const blobWriter = new BlobWriter(); + const writer: ZipWriter = new ZipWriter(blobWriter); + + for (const path of pathsToZip) { + const stat = await this.fs.stat(path); + if (stat.isDirectory()) { + await this.addDirectoryToZip(writer, path, path); + } else { + await this.addFileToZip(writer, path, path); + } + } + + await writer.close(); + // Get the Blob containing the zip file data + const blob = await blobWriter.getData(); + // Trigger download + this.downloadBlob(blob, zipFilename); + } + + // Add files to the ZIP + private async addFileToZip( + writer: ZipWriter, + filePath: string, + rootPath: string, + ) { + const data = await this.fs.readFile(filePath); + const blob = new Blob([data]); // Create a Blob from the file data + const reader = new BlobReader(blob); + await writer.add(filePath.replace(rootPath + '/', ''), reader); // Add the file to the zip + } + + // Add directories to the ZIP + private async addDirectoryToZip( + writer: ZipWriter, + dirPath: string, + rootPath: string, + ) { + const files = await this.fs.readdir(dirPath); + for (const file of files) { + const fullPath = `${dirPath}/${file}`; + const stat = await this.fs.stat(fullPath); + if (stat.isDirectory()) { + await this.addDirectoryToZip(writer, fullPath, rootPath); + } else { + await this.addFileToZip(writer, fullPath, rootPath); + } + } + } + + // Helper method to trigger the download + private downloadBlob(blob: Blob, filename: string) { + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } +} + +export default ZIP; diff --git a/src/pages/404.tsx b/src/pages/404.tsx new file mode 100644 index 0000000..8248a04 --- /dev/null +++ b/src/pages/404.tsx @@ -0,0 +1,19 @@ +import { Result } from 'antd'; +import Link from 'next/link'; + +const pageNotFound = () => { + return ( + + Back Home + + } + /> + ); +}; + +export default pageNotFound; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index c30ffa6..0271819 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,5 +1,6 @@ import { Layout } from '@/components/shared'; import { AppConfig } from '@/config/AppConfig'; +import { IDEProvider } from '@/state/IDE.context'; import '@/styles/theme.scss'; import { THEME } from '@tonconnect/ui'; import { TonConnectUIProvider } from '@tonconnect/ui-react'; @@ -83,26 +84,28 @@ export default function App({ - - + - - - - - + + + + + + + ); diff --git a/src/pages/_document.tsx b/src/pages/_document.tsx index ef3b629..a7daabf 100644 --- a/src/pages/_document.tsx +++ b/src/pages/_document.tsx @@ -1,5 +1,4 @@ import { Head, Html, Main, NextScript } from 'next/document'; -import Script from 'next/script'; export default function Document() { return ( @@ -15,7 +14,6 @@ export default function Document() {
-