= ({
src="/html/tonweb.html"
sandbox="allow-scripts allow-same-origin"
/>
+
+ You are using code that has been imported from an external source.
+ Exercise caution with the contract code before executing it.
+
Below options will be used to send internal message and call getter
method on contract after the contract is deployed.
diff --git a/src/components/workspace/ContractInteraction/TactContractInteraction.tsx b/src/components/workspace/ContractInteraction/TactContractInteraction.tsx
index d9fd82c..6dcb147 100644
--- a/src/components/workspace/ContractInteraction/TactContractInteraction.tsx
+++ b/src/components/workspace/ContractInteraction/TactContractInteraction.tsx
@@ -16,6 +16,10 @@ const TactContractInteraction: FC = ({
return (
+
+ You are using code that has been imported from an external source.
+ Exercise caution with the contract code before executing it.
+
Below options will be used to call receiver and call getter method on
contract after the contract is deployed.
diff --git a/src/components/workspace/Tabs/Tabs.tsx b/src/components/workspace/Tabs/Tabs.tsx
index 0888971..bbcfa2e 100644
--- a/src/components/workspace/Tabs/Tabs.tsx
+++ b/src/components/workspace/Tabs/Tabs.tsx
@@ -2,7 +2,7 @@ import AppIcon from '@/components/ui/icon';
import { useFileTab } from '@/hooks';
import { useProject } from '@/hooks/projectV2.hooks';
import EventEmitter from '@/utility/eventEmitter';
-import { fileTypeFromFileName } from '@/utility/utils';
+import { delay, fileTypeFromFileName } from '@/utility/utils';
import { FC, useEffect } from 'react';
import s from './Tabs.module.scss';
@@ -22,7 +22,10 @@ const Tabs: FC = () => {
};
useEffect(() => {
- syncTabSettings();
+ (async () => {
+ await delay(200);
+ syncTabSettings();
+ })();
}, [activeProject]);
useEffect(() => {
diff --git a/src/components/workspace/project/ManageProject/ManageProject.tsx b/src/components/workspace/project/ManageProject/ManageProject.tsx
index 1ffee0d..5fb4bc3 100644
--- a/src/components/workspace/project/ManageProject/ManageProject.tsx
+++ b/src/components/workspace/project/ManageProject/ManageProject.tsx
@@ -1,4 +1,5 @@
import {
+ CloneProject,
DownloadProject,
MigrateToUnifiedFS,
NewProject,
@@ -9,6 +10,7 @@ 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 Router from 'next/router';
import { FC, useEffect, useState } from 'react';
import s from './ManageProject.module.scss';
@@ -28,6 +30,7 @@ const ManageProject: FC = () => {
await deleteProject(id);
setActiveProject(null);
setIsDeleteConfirmOpen(false);
+ Router.push('/');
} catch (error) {
await message.error('Failed to delete project');
}
@@ -45,6 +48,7 @@ const ManageProject: FC = () => {
<>
Projects
+
void;
onNewDirectory?: () => void;
onDelete?: () => void;
+ onShare?: () => void;
}
const ItemAction: FC = ({
@@ -23,6 +24,7 @@ const ItemAction: FC = ({
onNewFile,
onNewDirectory,
onDelete,
+ onShare,
}) => {
const rootClassName = cn(s.actionRoot, className, 'actions');
const handleOnClick = (
@@ -51,6 +53,11 @@ const ItemAction: FC = ({
label: 'New Folder',
action: onNewDirectory,
},
+ {
+ title: 'Share',
+ label: 'Share',
+ action: onShare,
+ },
{
title: 'Close',
label: 'Delete',
diff --git a/src/components/workspace/tree/FileTree/TreeNode.tsx b/src/components/workspace/tree/FileTree/TreeNode.tsx
index 1969936..b73b1e3 100644
--- a/src/components/workspace/tree/FileTree/TreeNode.tsx
+++ b/src/components/workspace/tree/FileTree/TreeNode.tsx
@@ -1,9 +1,10 @@
-import { useFileTab } from '@/hooks';
+import { useFile, 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 { encodeBase64, fileTypeFromFileName } from '@/utility/utils';
import { NodeModel } from '@minoru/react-dnd-treeview';
+import { message } from 'antd';
import cn from 'clsx';
import { FC, useState } from 'react';
import s from './FileTree.module.scss';
@@ -32,6 +33,7 @@ const TreeNode: FC = ({ node, depth, isOpen, onToggle }) => {
const { deleteProjectFile, renameProjectFile, newFileFolder } = useProject();
const { open: openTab } = useFileTab();
const { createLog } = useLogActivity();
+ const { getFile } = useFile();
const disallowedFile = [
'message.cell.ts',
@@ -100,7 +102,12 @@ const TreeNode: FC = ({ node, depth, isOpen, onToggle }) => {
if (node.droppable) {
return ['Edit', 'NewFile', 'NewFolder', 'Close'];
}
- return ['Edit', 'Close'];
+ const options = ['Edit', 'Close'];
+ const allowedLanguages = ['tact', 'func'];
+ if (allowedLanguages.includes(fileTypeFromFileName(node.text))) {
+ options.push('Share');
+ }
+ return options;
};
const deleteItemFromNode = async () => {
@@ -113,6 +120,32 @@ const TreeNode: FC = ({ node, depth, isOpen, onToggle }) => {
await deleteProjectFile(nodePath);
};
+ const onShare = async () => {
+ try {
+ const fileContent =
+ ((await getFile(node.data?.path as string)) as string) || '';
+ const maxAllowedCharacters = 32779; // Maximum allowed characters in a Chrome. Firefox has more limit but we are using less for compatibility
+ if (!fileContent) {
+ message.error('File is empty');
+ return;
+ }
+ if (fileContent && fileContent.length > maxAllowedCharacters) {
+ message.error(
+ `File is too large to share. Maximum allowed characters is ${maxAllowedCharacters}`,
+ );
+ return;
+ }
+ const language = fileTypeFromFileName(node.text);
+ const shareableLink = `${window.location.origin}/?code=${encodeBase64(fileContent)}&lang=${language}`;
+
+ navigator.clipboard.writeText(shareableLink);
+
+ message.success("File's shareable link copied to clipboard");
+ } catch (error) {
+ message.error((error as Error).message);
+ }
+ };
+
const isAllowed = () => {
const isEditingItem = document.body.classList.contains(
'editing-file-folder',
@@ -168,6 +201,9 @@ const TreeNode: FC = ({ node, depth, isOpen, onToggle }) => {
onDelete={() => {
deleteItemFromNode().catch(() => {});
}}
+ onShare={() => {
+ onShare();
+ }}
/>
)}
diff --git a/src/enum/file.ts b/src/enum/file.ts
index 483017f..cdc04ea 100644
--- a/src/enum/file.ts
+++ b/src/enum/file.ts
@@ -31,6 +31,7 @@ export enum FileExtensionToFileType {
jsx = FileType.JavaScriptReact,
rs = FileType.Rust,
fc = FileType.FC,
+ func = FileType.FC,
tact = FileType.TACT,
json = FileType.JSON,
}
diff --git a/src/hooks/projectV2.hooks.ts b/src/hooks/projectV2.hooks.ts
index 20d2ca4..502e20d 100644
--- a/src/hooks/projectV2.hooks.ts
+++ b/src/hooks/projectV2.hooks.ts
@@ -5,8 +5,8 @@ import {
import {
ABIFormInputValues,
ContractLanguage,
+ CreateProjectParams,
ProjectSetting,
- ProjectTemplate,
Tree,
} from '@/interfaces/workspace.interface';
import fileSystem from '@/lib/fs';
@@ -60,21 +60,24 @@ export const useProject = () => {
}
};
- const createProject = async (
- name: string,
- language: ContractLanguage,
- template: ProjectTemplate,
- file: RcFile | null,
- defaultFiles?: Tree[],
+ const createProject = async ({
+ name,
+ language,
+ template,
+ file,
+ defaultFiles,
autoActivate = true,
- ) => {
- const projectDirectory = await fileSystem.mkdir(
- `${baseProjectPath}/${name}`,
- {
- overwrite: false,
- },
- );
- if (!projectDirectory) return;
+ isTemporary = false,
+ }: CreateProjectParams) => {
+ let projectDirectory = `${baseProjectPath}/${name}`;
+ try {
+ projectDirectory = (await fileSystem.mkdir(`${baseProjectPath}/${name}`, {
+ overwrite: isTemporary,
+ })) as string;
+ } catch (error) {
+ /* empty */
+ }
+ if (!name || !projectDirectory) return;
let files =
template === 'import' && defaultFiles?.length == 0
@@ -114,7 +117,7 @@ export const useProject = () => {
template,
};
- await writeFiles(projectDirectory, files);
+ await writeFiles(projectDirectory, files, { isTemporary });
const projectSettingPath = `${projectDirectory}/.ide/setting.json`;
if (!(await fileSystem.exists(projectSettingPath))) {
@@ -138,14 +141,18 @@ export const useProject = () => {
const writeFiles = async (
projectPath: string,
files: Pick
[],
- options?: { overwrite?: boolean },
+ options?: { overwrite?: boolean; isTemporary?: 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);
+ await fileSystem.writeFile(file.path, file.content ?? '', {
+ ...options,
+ virtual: options?.isTemporary ?? false,
+ overwrite: options?.isTemporary ? true : options?.overwrite,
+ });
EventEmitter.emit('FORCE_UPDATE_FILE', file.path);
return file.path;
}),
diff --git a/src/interfaces/workspace.interface.ts b/src/interfaces/workspace.interface.ts
index 2a5ccb1..d50ef48 100644
--- a/src/interfaces/workspace.interface.ts
+++ b/src/interfaces/workspace.interface.ts
@@ -1,6 +1,7 @@
import { IFileTab } from '@/state/IDE.context';
import { ABITypeRef } from '@ton/core';
import { Maybe } from '@ton/core/dist/utils/maybe';
+import { RcFile } from 'antd/es/upload';
export interface Tree {
id: string;
@@ -38,6 +39,16 @@ export interface ABIFormInputValues {
type: 'Init' | 'Getter' | 'Setter';
}
+export interface CreateProjectParams {
+ name: string;
+ language: ContractLanguage;
+ template: ProjectTemplate;
+ file: RcFile | null;
+ defaultFiles?: Tree[];
+ autoActivate?: boolean;
+ isTemporary?: boolean; // Used for temporary projects like code import from URL
+}
+
export interface Project {
id: string;
userId?: string;
diff --git a/src/lib/fs.ts b/src/lib/fs.ts
index 1e70fc3..ed1b2b9 100644
--- a/src/lib/fs.ts
+++ b/src/lib/fs.ts
@@ -2,8 +2,10 @@ import FS, { PromisifiedFS } from '@isomorphic-git/lightning-fs';
class FileSystem {
private fs: PromisifiedFS;
+ private virtualFiles: Map;
constructor(fs: PromisifiedFS) {
this.fs = fs;
+ this.virtualFiles = new Map();
}
get fsInstance() {
@@ -11,9 +13,14 @@ class FileSystem {
}
async readFile(path: string) {
+ if (this.virtualFiles.has(path)) {
+ return this.virtualFiles.get(path) as string;
+ }
+
if (!(await this.exists(path))) {
throw new Error(`File not found: ${path}`);
}
+
return this.fs.readFile(path, 'utf8');
}
@@ -27,9 +34,14 @@ class FileSystem {
async writeFile(
path: string,
data: string | Uint8Array,
- options?: { overwrite?: boolean },
+ options?: { overwrite?: boolean; virtual?: boolean },
) {
- const { overwrite } = options ?? {};
+ const { overwrite, virtual } = options ?? {};
+
+ if (!!virtual || this.virtualFiles.has(path)) {
+ this.virtualFiles.set(path, data);
+ return;
+ }
const finalPath = overwrite ? path : await this.getUniquePath(path);
await this.ensureDirectoryExists(finalPath);
return this.fs.writeFile(finalPath, data);
@@ -74,21 +86,34 @@ class FileSystem {
) {
if (!path) return [];
const { recursive, basePath, onlyDir } = options;
+ let results: string[] = [];
+
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ const files = (await this.fs.readdir(path)) ?? [];
+ results.push(...files);
+
+ const virtualFilesInDir = Array.from(this.virtualFiles.keys()).filter(
+ (key) => key.startsWith(path),
+ );
+ const virtualFilesNames = virtualFilesInDir.map((filePath) =>
+ filePath.replace(`${path}/`, ''),
+ );
+
+ results.push(...virtualFilesNames);
+
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 (!onlyDir) return results;
+ const dirs: string[] = [];
+ for (const file of results) {
+ const stat = await this.stat(`${path}/${file}`);
if (stat.isDirectory()) {
- results.push(file);
+ dirs.push(file);
}
}
- return results;
+ return dirs;
}
- let results: string[] = [];
- const files = await this.readdir(path);
- for (const file of files) {
+
+ for (const file of results) {
const filePath = `${path}/${file}`;
const stat = await this.stat(filePath);
if (stat.isDirectory()) {
@@ -99,13 +124,12 @@ class FileSystem {
});
results = results.concat(nestedFiles);
} else {
- // Remove the rootPath from the file path
- results.push(filePath.replace(basePath + '/', ''));
+ // Remove the basePath from the file path if provided
+ results.push(filePath.replace(basePath ? basePath + '/' : '', ''));
}
}
return results;
}
-
async mkdir(
path: string,
options: { overwrite?: boolean } = { overwrite: true },
@@ -156,10 +180,17 @@ class FileSystem {
}
async unlink(path: string) {
+ if (this.virtualFiles.has(path)) {
+ this.virtualFiles.delete(path);
+ return;
+ }
return this.fs.unlink(path);
}
async exists(path: string) {
+ if (this.virtualFiles.has(path)) {
+ return true;
+ }
try {
await this.fs.stat(path);
return true;
@@ -169,6 +200,12 @@ class FileSystem {
}
async stat(path: string) {
+ if (this.virtualFiles.has(path)) {
+ return {
+ isFile: () => true,
+ isDirectory: () => false,
+ };
+ }
return this.fs.stat(path);
}
@@ -228,6 +265,10 @@ class FileSystem {
await this.rmdir(path);
}
+ clearVirtualFiles() {
+ this.virtualFiles.clear();
+ }
+
async du(path = '/') {
return this.fs.du(path);
}
diff --git a/src/styles/global.scss b/src/styles/global.scss
index 4f462d1..b08d25a 100644
--- a/src/styles/global.scss
+++ b/src/styles/global.scss
@@ -122,3 +122,7 @@ hr {
background: rgb(61, 61, 61);
border: 0;
}
+
+.color-warn {
+ color: var(--color-warning);
+}