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.
-
-
-
-
-
- );
-};
-
-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(true);
+ }}
+ >
+ Restore Old Projects
+
+
+
+ {
+ 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 && ', '}
+
+ ))}
+
+
+
+ {
+ migrateProject();
+ }}
+ >
+ Restore
+
+
+ {migrationStatus === 'completed' && (
+ <>
+
+ Migration completed successfully. Now you can safely delete old
+ project from archive.
+
+ {}}
+ okText="Yes"
+ cancelText="No"
+ >
+
+ Delete Old Projects
+
+
+ >
+ )}
+
+ >
+ );
+};
+
+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:
-
-
- Create a new project with predefined template
-
- You will have 3 important files{' '}
- main.fc, stateInit.cell.js, contract.cell.js and test.spec.js
-
-
- 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.
-
-
- 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.
-
-
- contract.cell.js contains a cell which will be used for further
- internal message.
-
-
- test.spec.js used to write test cases. Test cases will run on
- TON sandbox.
-
- Write your code. And Go to compile from sidebar
-
- Build your contract, deploy it and then you can interact with the
- contract using getter and setter options.
-
-
- Project can be made public from setting option. It will enable any
- user view and clone project.
-
-
-
- );
-};
-
-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
-
-
-
-
-
-
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,
+ )}
+
+ );
+ })}
+
+ )}
+
= ({ projectId, contract, updateContract }) => {
if (activeProject?.language === 'tact') {
delete tempFormValues.contract;
- updateABIInputValues(
- {
- key: 'init',
- value: tempFormValues as TactInputFields,
- type: 'Init',
- },
- projectId as string,
- );
+ updateABIInputValues({
+ key: 'init',
+ value: tempFormValues as TactInputFields,
+ type: 'Init',
+ });
- let tsProjectFiles = {};
+ const tsProjectFiles: Record = {};
if (isIncludesTypeCellOrSlice(tempFormValues)) {
- tsProjectFiles = await getAllFilesWithContent(
- projectId,
- (file) =>
- !file.path?.startsWith('dist') &&
+ const fileCollection = await readdirTree(
+ `${activeProject.path}`,
+ {
+ basePath: null,
+ content: true,
+ },
+ (file: { path: string; name: string }) =>
+ !file.path.startsWith('dist') &&
file.name.endsWith('.ts') &&
!file.name.endsWith('.spec.ts'),
);
+ fileCollection.forEach((file) => {
+ tsProjectFiles[file.path!] = file.content ?? '';
+ });
}
initParams = await parseInputs(
@@ -279,8 +276,8 @@ const BuildProject: FC = ({ projectId, contract, updateContract }) => {
const deploy = async () => {
createLog(`Deploying contract ...`, 'info');
const contractBOCPath = selectedContract?.replace('.abi', '.code.boc');
- const contractBOC = await getFileByPath(contractBOCPath, projectId);
- if (!contractBOC?.content) {
+ const contractBOC = (await getFile(contractBOCPath!)) as string;
+ if (!contractBOC) {
throw new Error('Contract BOC is missing. Rebuild the contract.');
}
try {
@@ -303,10 +300,10 @@ const BuildProject: FC = ({ projectId, contract, updateContract }) => {
contract,
logs,
} = await deployContract(
- contractBOC.content,
+ contractBOC,
buildOutput?.dataCell as unknown as string,
environment.toLowerCase() as Network,
- activeProject!,
+ activeProject as Project,
);
Analytics.track('Deploy project', {
@@ -333,12 +330,9 @@ const BuildProject: FC = ({ projectId, contract, updateContract }) => {
updateContract(contract);
}
- updateProjectById(
- {
- contractAddress: _contractAddress,
- } as Project,
- projectId,
- );
+ updateProjectSetting({
+ contractAddress: _contractAddress,
+ } as ProjectSetting);
} catch (error) {
console.log(error, 'error');
const errorMessage = (error as Error).message.split('\n');
@@ -356,8 +350,13 @@ const BuildProject: FC = ({ projectId, contract, updateContract }) => {
}
const contractScriptPath = selectedContract.replace('.abi', '.ts');
if (!cellBuilderRef.current?.contentWindow) return;
- const contractScript = await getFileByPath(contractScriptPath, projectId);
- if (activeProject?.language === 'tact' && !contractScript?.content) {
+ let contractScript = '';
+ try {
+ contractScript = (await getFile(contractScriptPath)) as string;
+ } catch (error) {
+ /* empty */
+ }
+ if (activeProject?.language === 'tact' && !contractScript) {
throw new Error('Contract script is missing. Rebuild the contract.');
}
@@ -367,31 +366,32 @@ const BuildProject: FC = ({ projectId, contract, updateContract }) => {
if (activeProject?.language == 'tact') {
jsOutout = await buildTs(
{
- 'tact.ts': contractScript?.content ?? '',
+ 'tact.ts': contractScript,
},
'tact.ts',
);
} else {
- const stateInitContent = await getFileByPath(
- 'stateInit.cell.ts',
- projectId,
- );
+ let stateInitContent = '';
let cellCode = '';
- if (stateInitContent && !stateInitContent.content && !initParams) {
+ try {
+ stateInitContent = (await getFile(
+ `${activeProject?.path}/stateInit.cell.ts`,
+ )) as string;
+ } catch (error) {
+ console.log('stateInit.cell.ts is missing');
+ }
+ if (!stateInitContent && !initParams) {
throw new Error(
'State init data is missing in file stateInit.cell.ts',
);
}
if (initParams) {
cellCode = generateCellCode(initParams as unknown as CellValues[]);
- updateProjectById(
- {
- cellABI: { deploy: initParams as CellABI },
- } as Project,
- projectId,
- );
+ updateProjectSetting({
+ cellABI: { deploy: initParams as CellABI },
+ } as ProjectSetting);
} else {
- cellCode = stateInitContent?.content ?? '';
+ cellCode = stateInitContent;
}
jsOutout = await buildTs(
@@ -453,7 +453,7 @@ const BuildProject: FC = ({ projectId, contract, updateContract }) => {
const isContractInteraction = () => {
let isValid =
- activeProject?.id && selectedContract && activeProject.contractAddress
+ activeProject?.path && selectedContract && activeProject.contractAddress
? true
: false;
if (environment === 'SANDBOX') {
@@ -467,17 +467,17 @@ const BuildProject: FC = ({ projectId, contract, updateContract }) => {
setContractABI(blankABI);
return;
}
- const contractABIFile = await getFileByPath(selectedContract, projectId);
+ const contractABIFile = (await getFile(selectedContract)) as string;
- if (selectedContract && !contractABIFile?.content) {
+ if (selectedContract && !contractABIFile) {
updateSelectedContract('');
return;
}
- if (!contractABIFile?.content) {
+ if (!contractABIFile) {
createLog('Contract ABI is missing. Rebuild the contract.', 'error');
return;
}
- const contractABI = JSON.parse(contractABIFile.content || '{}');
+ const contractABI = JSON.parse(contractABIFile || '{}');
if (activeProject?.language === 'tact') {
const abi = new ABIParser(JSON.parse(JSON.stringify(contractABI)));
contractABI.getters = abi.getters;
@@ -512,27 +512,22 @@ const BuildProject: FC = ({ projectId, contract, updateContract }) => {
};
const updatNetworkEnvironment = (network: NetworkEnvironment) => {
- updateProjectById(
- {
- network,
- } as Project,
- projectId,
- );
+ updateProjectSetting({
+ network,
+ } as ProjectSetting);
setEnvironment(network);
};
const updateSelectedContract = (contract: string) => {
setSelectedContract(contract);
- updateProjectById(
- {
- selectedContract: contract,
- } as Project,
- projectId,
- );
+ updateProjectSetting({
+ selectedContract: contract,
+ } as ProjectSetting);
};
const extractContractName = (currentContractName: string) => {
return currentContractName
+ .replace(activeProject?.path + '/', '')
.replace('dist/', '')
.replace('.abi', '')
.replace('tact_', '')
@@ -552,8 +547,8 @@ const BuildProject: FC = ({ projectId, contract, updateContract }) => {
supressErrors = false,
) => {
const contractScriptPath = currentContractName.replace('.abi', '.ts');
- const contractScript = await getFileByPath(contractScriptPath, projectId);
- if (language === 'tact' && !contractScript?.content) {
+ const contractScript = (await getFile(contractScriptPath)) as string;
+ if (language === 'tact' && !contractScript) {
if (supressErrors) {
return;
}
@@ -562,7 +557,7 @@ const BuildProject: FC = ({ projectId, contract, updateContract }) => {
const jsOutout = await buildTs(
{
- 'tact.ts': contractScript?.content ?? '',
+ 'tact.ts': contractScript,
},
'tact.ts',
);
@@ -667,7 +662,7 @@ const BuildProject: FC = ({ projectId, contract, updateContract }) => {
};
if (activeProject?.language === 'tact') {
- const abiFields = getABIInputValues(projectId as string, 'init', 'Init');
+ const abiFields = getABIInputValues('init', 'Init');
if (abiFields) {
deployForm.setFieldsValue(abiFields);
}
@@ -717,7 +712,6 @@ const BuildProject: FC = ({ projectId, contract, updateContract }) => {
{
export default cell;`;
};
-const CellBuilder: FC = ({ info, projectId, type, form }) => {
- const { project } = useWorkspaceActions();
+const CellBuilder: FC = ({ info, type, form }) => {
+ const { activeProject } = useProject();
const [isCellBuilder, setIsCellBuilder] = useState(false);
- const activeProject = project(projectId);
-
const getPlaceHolder = (key: number) => {
return (
CellBuilderDataTypes[
@@ -92,9 +89,12 @@ const CellBuilder: FC = ({ info, projectId, type, form }) => {
- {info}
{' '}
or use {`"Cell Builder"`} {' '}
{type === 'deploy' && ' to send message'}
diff --git a/src/components/workspace/ContractInteraction/FuncContractInteraction.tsx b/src/components/workspace/ContractInteraction/FuncContractInteraction.tsx
index 3dcd7af..9a6b23e 100644
--- a/src/components/workspace/ContractInteraction/FuncContractInteraction.tsx
+++ b/src/components/workspace/ContractInteraction/FuncContractInteraction.tsx
@@ -1,7 +1,8 @@
+import { useFile } from '@/hooks';
import { useContractAction } from '@/hooks/contract.hooks';
import { useLogActivity } from '@/hooks/logActivity.hooks';
-import { useWorkspaceActions } from '@/hooks/workspace.hooks';
-import { CellABI, Project } from '@/interfaces/workspace.interface';
+import { useProject } from '@/hooks/projectV2.hooks';
+import { CellABI, ProjectSetting } from '@/interfaces/workspace.interface';
import { buildTs } from '@/utility/typescriptHelper';
import { Cell } from '@ton/core';
import { useTonConnectUI } from '@tonconnect/ui-react';
@@ -20,7 +21,6 @@ import s from './ContractInteraction.module.scss';
const FuncContractInteraction: FC = ({
contractAddress,
- projectId,
abi,
network,
contract = null,
@@ -28,7 +28,8 @@ const FuncContractInteraction: FC = ({
const [tonConnector] = useTonConnectUI();
const [isLoading, setIsLoading] = useState('');
const { sendMessage } = useContractAction();
- const { getFileByPath, updateProjectById } = useWorkspaceActions();
+ const { getFile } = useFile();
+ const { updateProjectSetting, activeProject } = useProject();
const { createLog } = useLogActivity();
const { sandboxWallet: wallet } = globalWorkspace;
const [messageForm] = useForm();
@@ -41,23 +42,19 @@ const FuncContractInteraction: FC = ({
if (!cellBuilderRef.current?.contentWindow) return;
let cellCode = '';
- const contractCellContent = await getFileByPath(
- 'message.cell.ts',
- projectId,
+ const contractCellContent = await getFile(
+ `${activeProject?.path}/message.cell.ts`,
);
- if (contractCellContent && !contractCellContent.content && !cell) {
+ if (!contractCellContent && !cell) {
throw new Error('Cell data is missing in file message.cell.ts');
}
if (cell) {
cellCode = generateCellCode(cell as unknown as CellValues[]);
- updateProjectById(
- {
- cellABI: { setter: cell as CellABI },
- } as Project,
- projectId,
- );
+ updateProjectSetting({
+ cellABI: { setter: cell as CellABI },
+ } as ProjectSetting);
} else {
- cellCode = contractCellContent?.content ?? '';
+ cellCode = contractCellContent as string;
}
try {
const jsOutout = await buildTs(
@@ -125,14 +122,7 @@ const FuncContractInteraction: FC = ({
};
const cellBuilder = (info: string) => {
- return (
-
- );
+ return ;
};
useEffect(() => {
diff --git a/src/components/workspace/Editor/Editor.tsx b/src/components/workspace/Editor/Editor.tsx
index cc0eb3c..fa77435 100644
--- a/src/components/workspace/Editor/Editor.tsx
+++ b/src/components/workspace/Editor/Editor.tsx
@@ -1,12 +1,14 @@
import { useSettingAction } from '@/hooks/setting.hooks';
-import { useWorkspaceActions } from '@/hooks/workspace.hooks';
import { ContractLanguage, Tree } from '@/interfaces/workspace.interface';
import EventEmitter from '@/utility/eventEmitter';
import { highlightCodeSnippets } from '@/utility/syntaxHighlighter';
-import { fileTypeFromFileName } from '@/utility/utils';
+import { delay, fileTypeFromFileName } from '@/utility/utils';
import EditorDefault, { loader } from '@monaco-editor/react';
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
import { FC, useEffect, useRef, useState } from 'react';
+// import { useLatest } from 'react-use';
+import { useFile, useFileTab } from '@/hooks';
+import { useProject } from '@/hooks/projectV2.hooks';
import { useLatest } from 'react-use';
import ReconnectingWebSocket from 'reconnecting-websocket';
import s from './Editor.module.scss';
@@ -14,14 +16,13 @@ import { editorOnMount } from './EditorOnMount';
type Monaco = typeof monaco;
interface Props {
- file: Tree;
- projectId: string;
className?: string;
}
-const Editor: FC = ({ file, projectId, className = '' }) => {
- const { updateFileContent, getFileContent, updateOpenFile } =
- useWorkspaceActions();
+const Editor: FC = ({ className = '' }) => {
+ const { activeProject } = useProject();
+ const { getFile, saveFile: storeFileContent } = useFile();
+ const { fileTab } = useFileTab();
const { isFormatOnSave, getSettingStateByKey } = useSettingAction();
@@ -34,7 +35,7 @@ const Editor: FC = ({ file, projectId, className = '' }) => {
// Using this extra state to trigger save file from js event
const [saveFileCounter, setSaveFileCounter] = useState(1);
- const latestFile = useLatest(file);
+ const latestFile = useLatest(fileTab.active);
const [initialFile, setInitialFile] = useState = ({ file, projectId, className = '' }) => {
const saveFile = async () => {
const fileContent = editorRef.current?.getValue() ?? '';
- if (!fileContent) return;
+ if (!fileContent || !fileTab.active) return;
try {
if (isFormatOnSave()) {
editorRef.current?.trigger(
@@ -61,9 +62,10 @@ const Editor: FC = ({ file, projectId, className = '' }) => {
'editor.action.formatDocument',
{},
);
+ await delay(200);
}
- await updateFileContent(file.id, fileContent, projectId);
- EventEmitter.emit('FILE_SAVED', { fileId: file.id });
+ await storeFileContent(fileTab.active, fileContent);
+ EventEmitter.emit('FILE_SAVED', { fileId: fileTab.active });
} catch (error) {
/* empty */
}
@@ -76,13 +78,16 @@ const Editor: FC = ({ file, projectId, className = '' }) => {
window.MonacoEnvironment.getWorkerUrl = (_: string, label: string) => {
if (label === 'typescript') {
return '/_next/static/ts.worker.js';
+ } else if (label === 'json') {
+ return '/_next/static/json.worker.js';
}
return '/_next/static/editor.worker.js';
};
loader.config({ monaco });
+ if (!fileTab.active) return;
await highlightCodeSnippets(
loader,
- fileTypeFromFileName(file.name) as ContractLanguage,
+ fileTypeFromFileName(fileTab.active) as ContractLanguage,
);
}
@@ -113,8 +118,11 @@ const Editor: FC = ({ file, projectId, className = '' }) => {
// If file is changed e.g. in case of build process then force update in editor
EventEmitter.on('FORCE_UPDATE_FILE', (filePath: string) => {
+ if (!activeProject?.path || latestFile.current?.includes('setting.json'))
+ return;
+
(async () => {
- if (filePath !== latestFile.current.path) return;
+ if (filePath !== latestFile.current) return;
await fetchFileContent(true);
})().catch((error) => {
console.error('Error handling FORCE_UPDATE_FILE event:', error);
@@ -127,8 +135,10 @@ const Editor: FC = ({ file, projectId, className = '' }) => {
}, [isLoaded]);
const fetchFileContent = async (force = false) => {
- if ((!file.id || file.id === initialFile?.id) && !force) return;
- let content = await getFileContent(file.id);
+ if (!fileTab.active) return;
+ if ((!fileTab.active || fileTab.active === initialFile?.id) && !force)
+ return;
+ let content = (await getFile(fileTab.active)) as string;
if (!editorRef.current) return;
let modelContent = editorRef.current.getValue();
@@ -141,25 +151,24 @@ const Editor: FC = ({ file, projectId, className = '' }) => {
} else {
editorRef.current.setValue(content);
}
- setInitialFile({ id: file.id, content });
+ setInitialFile({ id: fileTab.active, content });
editorRef.current.focus();
};
const markFileDirty = () => {
- if (!editorRef.current) return;
- const fileContent = editorRef.current.getValue();
- if (
- file.id !== initialFile?.id ||
- !initialFile.content ||
- initialFile.content === fileContent
- ) {
- return;
- }
- if (!fileContent) {
- return;
- }
-
- updateOpenFile(file.id, { isDirty: true }, projectId);
+ // if (!editorRef.current) return;
+ // const fileContent = editorRef.current.getValue();
+ // if (
+ // file.id !== initialFile?.id ||
+ // !initialFile.content ||
+ // initialFile.content === fileContent
+ // ) {
+ // return;
+ // }
+ // if (!fileContent) {
+ // return;
+ // }
+ // updateOpenFile(file.id, { isDirty: true }, projectId);
};
const initializeEditorMode = async () => {
@@ -192,7 +201,7 @@ const Editor: FC = ({ file, projectId, className = '' }) => {
(async () => {
await fetchFileContent();
})().catch(() => {});
- }, [file, isEditorInitialized]);
+ }, [fileTab.active, isEditorInitialized]);
useEffect(() => {
if (!monacoRef.current) {
@@ -229,12 +238,12 @@ const Editor: FC = ({ file, projectId, className = '' }) => {
| React.MouseEvent;
interface Props {
- file?: Tree | undefined;
projectId: Project['id'];
onCompile?: () => void;
onClick?: (e: ButtonClick, data: string) => void;
@@ -25,7 +25,6 @@ interface Props {
}
const ExecuteFile: FC = ({
- // file,
projectId,
onCompile,
onClick,
@@ -34,7 +33,8 @@ const ExecuteFile: FC = ({
description = '',
allowedFile = [],
}) => {
- const { compileTsFile, projectFiles } = useWorkspaceActions();
+ const { compileTsFile } = useWorkspaceActions();
+ const { projectFiles } = useProject();
const { compileFuncProgram, compileTactProgram } = useProjectActions();
const { createLog } = useLogActivity();
const [selectedFile, setSelectedFile] = useState();
@@ -44,7 +44,7 @@ const ExecuteFile: FC = ({
const isAutoBuildAndDeployEnabledRef = useRef(false);
- const fileList = projectFiles(projectId).filter((f: Tree | null) => {
+ const fileList = projectFiles.filter((f: Tree | null) => {
const _fileExtension = getFileExtension(f?.name ?? '');
if (f?.name === 'stdlib.fc') return false;
return allowedFile.includes(_fileExtension as string);
@@ -61,7 +61,7 @@ const ExecuteFile: FC = ({
try {
switch (_fileExtension) {
case 'ts':
- await compileTsFile(selectedFile, projectId);
+ await compileTsFile(selectedFile.path, projectId);
break;
case 'spec.ts':
if (!onClick || !selectedFile.path) return;
@@ -107,10 +107,14 @@ const ExecuteFile: FC = ({
};
const selectFile = (
- e: number | string | React.ChangeEvent,
+ e: number | string | undefined | React.ChangeEvent,
) => {
+ if (e === undefined) {
+ setSelectedFile(undefined);
+ return;
+ }
const selectedFile = fileList.find((f) => {
- if (typeof e === 'string') return f.id === e;
+ if (typeof e === 'string') return f.path === e;
return (
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
f.id === (e as React.ChangeEvent)?.target?.value
@@ -156,14 +160,14 @@ const ExecuteFile: FC = ({
showSearch
className="w-100"
defaultActiveFirstOption
- value={selectedFile?.id}
+ value={selectedFile?.path}
onChange={selectFile}
filterOption={(inputValue, option) => {
return option?.title.toLowerCase().includes(inputValue.toLowerCase());
}}
>
{fileList.map((f) => (
-
+
{f.name}
))}
diff --git a/src/components/workspace/OpenFile/OpenFile.tsx b/src/components/workspace/OpenFile/OpenFile.tsx
index 38b5b9f..99f9a9c 100644
--- a/src/components/workspace/OpenFile/OpenFile.tsx
+++ b/src/components/workspace/OpenFile/OpenFile.tsx
@@ -1,21 +1,20 @@
-import { useWorkspaceActions } from '@/hooks/workspace.hooks';
+import { useFileTab } from '@/hooks';
import { FC } from 'react';
import s from './OpenFile.module.scss';
interface Props {
path: string;
name: string;
- projectId: string;
}
-const OpenFile: FC = ({ path, name, projectId }) => {
- const { openFileByPath } = useWorkspaceActions();
+const OpenFile: FC = ({ path, name }) => {
+ const { open } = useFileTab();
return (
{
- openFileByPath(path, projectId);
+ open(name, path);
}}
>
{name}
diff --git a/src/components/workspace/ProjectSetting/ProjectSetting.module.scss b/src/components/workspace/ProjectSetting/ProjectSetting.module.scss
deleted file mode 100644
index 974adae..0000000
--- a/src/components/workspace/ProjectSetting/ProjectSetting.module.scss
+++ /dev/null
@@ -1,11 +0,0 @@
-.root {
- padding: 1rem;
- .copy {
- display: inline-flex;
- align-items: center;
- .icon {
- margin-right: 0.5rem;
- font-size: 0.8rem;
- }
- }
-}
diff --git a/src/components/workspace/ProjectSetting/ProjectSetting.tsx b/src/components/workspace/ProjectSetting/ProjectSetting.tsx
deleted file mode 100644
index a561241..0000000
--- a/src/components/workspace/ProjectSetting/ProjectSetting.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import AppIcon from '@/components/ui/icon';
-import { useWorkspaceActions } from '@/hooks/workspace.hooks';
-import { Project } from '@/interfaces/workspace.interface';
-import { Button, Form, message, Switch } from 'antd';
-import { FC, useState } from 'react';
-import s from './ProjectSetting.module.scss';
-
-interface Props {
- projectId: Project['id'];
-}
-
-const ProjectSetting: FC = ({ projectId }) => {
- const workspaceAction = useWorkspaceActions();
- const project = workspaceAction.project(projectId);
- const [isChecked, setIsChecked] = useState(project?.isPublic);
-
- const toggleProjectStatus = (status: boolean) => {
- setIsChecked(status);
- workspaceAction.updateProjectById(
- { isPublic: status } as Project,
- projectId,
- );
- };
-
- const copyURL = async () => {
- const { protocol, host, pathname } = window.location;
- const url = protocol + '//' + host + '/' + pathname;
- await navigator.clipboard.writeText(url);
- await message.info('Copied to clipboard');
- };
-
- return (
-
-
-
-
-
-
- You can make your project public if want to make it shareable anywhere
-
-
-
{
- copyURL().catch(() => {});
- }}
- className={s.copy}
- >
-
- Copy URL
-
-
- );
-};
-
-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() {
-