hometab & clone

rdesktop^2
filip mertens 1 year ago
parent f542fd8d1b
commit 1d36d50ab6
  1. 22
      apps/remix-ide/src/app.js
  2. 6
      apps/remix-ide/src/app/files/dgitProvider.js
  3. 46
      apps/remix-ide/src/app/files/electronProvider.ts
  4. 2
      apps/remix-ide/src/app/files/fileManager.ts
  5. 1
      apps/remix-ide/src/app/files/remixDProvider.js
  6. 2
      apps/remix-ide/src/app/panels/file-panel.js
  7. 0
      apps/remix-ide/src/app/plugins/electron/electronConfigPlugin.ts
  8. 1
      apps/remix-ide/src/app/plugins/electron/fsPlugin.ts
  9. 0
      apps/remix-ide/src/app/plugins/electron/isoGitPlugin.ts
  10. 15
      apps/remix-ide/src/app/plugins/electron/templatesPlugin.ts
  11. 31
      apps/remix-ide/src/app/plugins/remix-templates.ts
  12. 2
      apps/remix-ide/src/remixEngine.js
  13. 1
      apps/remixdesktop/package.json
  14. 15
      apps/remixdesktop/src/engine.ts
  15. 9
      apps/remixdesktop/src/main.ts
  16. 10
      apps/remixdesktop/src/menus/commands.ts
  17. 17
      apps/remixdesktop/src/menus/darwin.ts
  18. 8
      apps/remixdesktop/src/menus/file.ts
  19. 20
      apps/remixdesktop/src/menus/git.ts
  20. 58
      apps/remixdesktop/src/plugins/fsPlugin.ts
  21. 2
      apps/remixdesktop/src/plugins/gitPlugin.ts
  22. 97
      apps/remixdesktop/src/plugins/isoGitPlugin.ts
  23. 75
      apps/remixdesktop/src/plugins/templates.ts
  24. 2
      apps/remixdesktop/src/plugins/xtermPlugin.ts
  25. 2
      apps/remixdesktop/src/preload.ts
  26. 2807
      apps/remixdesktop/yarn.lock
  27. 34
      libs/remix-ui/home-tab/src/lib/components/homeTabFileElectron.tsx
  28. 7
      libs/remix-ui/home-tab/src/lib/components/homeTabGetStarted.tsx
  29. 6
      libs/remix-ui/home-tab/src/lib/remix-ui-home-tab.tsx
  30. 43
      libs/remix-ui/workspace/src/lib/actions/index.ts
  31. 124
      libs/remix-ui/workspace/src/lib/actions/workspace.ts
  32. 12
      libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx

@ -45,9 +45,13 @@ import { FileDecorator } from './app/plugins/file-decorator'
import { CodeFormat } from './app/plugins/code-format'
import { SolidityUmlGen } from './app/plugins/solidity-umlgen'
import { ContractFlattener } from './app/plugins/contractFlattener'
import { fsPlugin } from './app/plugins/fsPlugin'
import { isoGitPlugin } from './app/plugins/isoGitPlugin'
import { electronConfig } from './app/plugins/electronConfigPlugin'
import { TemplatesPlugin } from './app/plugins/remix-templates'
import { fsPlugin } from './app/plugins/electron/fsPlugin'
import { isoGitPlugin } from './app/plugins/electron/isoGitPlugin'
import { electronConfig } from './app/plugins/electron/electronConfigPlugin'
import { electronTemplates } from './app/plugins/electron/templatesPlugin'
const isElectron = require('is-electron')
@ -116,7 +120,7 @@ class AppComponent {
name: 'fileproviders/workspace'
})
this._components.filesProviders.electron = new ElectronProvider()
this._components.filesProviders.electron = new ElectronProvider(this.appManager)
Registry.getInstance().put({
api: this._components.filesProviders.electron,
name: 'fileproviders/electron'
@ -197,7 +201,8 @@ class AppComponent {
//----- search
const search = new SearchPlugin()
//---- templates
const templates = new TemplatesPlugin()
//---------------- Solidity UML Generator -------------------------
const solidityumlgen = new SolidityUmlGen(appManager)
@ -322,6 +327,7 @@ class AppComponent {
solidityumlgen,
contractFlattener,
solidityScript,
templates
])
//---- fs plugin
@ -332,6 +338,8 @@ class AppComponent {
this.engine.register([isoGit])
const electronConfigPlugin = new electronConfig()
this.engine.register([electronConfigPlugin])
const templatesPlugin = new electronTemplates()
this.engine.register([templatesPlugin])
}
// LAYOUT & SYSTEM VIEWS
@ -447,10 +455,10 @@ class AppComponent {
await this.appManager.activatePlugin(['hiddenPanel', 'pluginManager', 'codeParser', 'codeFormatter', 'fileDecorator', 'terminal', 'blockchain', 'fetchAndCompile', 'contentImport', 'gistHandler'])
await this.appManager.activatePlugin(['settings'])
await this.appManager.activatePlugin(['walkthrough', 'storage', 'search', 'compileAndRun', 'recorder'])
await this.appManager.activatePlugin(['solidity-script'])
await this.appManager.activatePlugin(['solidity-script', 'remix-templates'])
if(isElectron()){
await this.appManager.activatePlugin(['fs', 'isogit', 'electronconfig'])
await this.appManager.activatePlugin(['fs', 'isogit', 'electronconfig', 'electronTemplates'])
}
this.appManager.on(

@ -366,7 +366,7 @@ class DGitProvider extends Plugin {
async clone(input, workspaceName, workspaceExists = false) {
if (isElectron()) {
const folder = await this.call('isogit', 'openFolder')
const folder = await this.call('fs', 'selectFolder')
if (!folder) return false
console.log('folder', folder)
const cmd = {
@ -378,8 +378,8 @@ class DGitProvider extends Plugin {
input
}
const result = await this.call('isogit', 'clone', cmd)
await this.call('fs', 'openWindow', folder)
this.call('fs', 'openWindow', folder)
return result
} else {
const permission = await this.askUserPermission('clone', 'Import multiple files into your workspaces.')
if (!permission) return false

@ -7,18 +7,45 @@ declare global {
}
export class ElectronProvider extends FileProvider {
constructor () {
constructor(appManager) {
super('')
this._appManager = appManager
}
async init() {
this._appManager.on('fs', 'change', (event, path) => {
console.log('change', event, path)
switch (event) {
case 'add':
this.event.emit('fileAdded', path)
break
case 'unlink':
this.event.emit('fileRemoved', path)
break
case 'change':
this.event.emit('fileChanged', path)
break
case 'rename':
this.event.emit('fileRenamed', path)
break
case 'addDir':
this.event.emit('folderAdded', path)
break
case 'unlinkDir':
this.event.emit('fileRemoved', path)
}
})
}
// isDirectory is already included
// this is a more efficient version of the default implementation
async resolveDirectory (path, cb) {
async resolveDirectory(path, cb) {
path = this.removePrefix(path)
if (path.indexOf('/') !== 0) path = '/' + path
try {
const files = await window.remixFileSystem.readdir(path)
console.log(files, 'files resolveDirectory ELECTRON')
console.log(files.length, 'files resolveDirectory ELECTRON')
const ret = {}
if (files) {
for (let element of files) {
@ -37,4 +64,17 @@ export class ElectronProvider extends FileProvider {
}
}
/**
* Removes the folder recursively
* @param {*} path is the folder to be removed
*/
async remove(path) {
console.log('remove', path)
try {
await window.remixFileSystem.rmdir(path)
} catch (error) {
console.log(error)
}
}
}

@ -24,7 +24,7 @@ const profile = {
permission: true,
version: packageJson.version,
methods: ['closeAllFiles', 'closeFile', 'file', 'exists', 'open', 'writeFile', 'readFile', 'copyFile', 'copyDir', 'rename', 'mkdir',
'readdir', 'dirList', 'fileList', 'remove', 'getCurrentFile', 'getFile', 'getFolder', 'setFile', 'switchFile', 'refresh',
'readdir', 'dirList', 'fileList', 'remove', 'getCurrentFile', 'getFile', 'selectFolder', 'setFile', 'switchFile', 'refresh',
'getProviderOf', 'getProviderByName', 'getPathFromUrl', 'getUrlFromPath', 'saveCurrentFile', 'setBatchFiles', 'isGitRepo'],
kind: 'file-system'
}

@ -26,6 +26,7 @@ module.exports = class RemixDProvider extends FileProvider {
})
this._appManager.on('remixd', 'fileAdded', (path) => {
console.log('fileAdded remixd', path)
this.event.emit('fileAdded', path)
})

@ -30,7 +30,7 @@ const { SlitherHandle } = require('../files/slither-handle.js')
const profile = {
name: 'filePanel',
displayName: 'File explorer',
methods: ['createNewFile', 'uploadFile', 'getCurrentWorkspace', 'getAvailableWorkspaceName', 'getWorkspaces', 'createWorkspace', 'setWorkspace', 'registerContextMenuItem', 'renameWorkspace', 'deleteWorkspace'],
methods: ['createNewFile', 'uploadFile', 'getCurrentWorkspace', 'getAvailableWorkspaceName', 'getWorkspaces', 'createWorkspace', 'setWorkspace', 'registerContextMenuItem', 'renameWorkspace', 'deleteWorkspace', 'loadTemplate', 'clone'],
events: ['setWorkspace', 'workspaceRenamed', 'workspaceDeleted', 'workspaceCreated'],
icon: 'assets/img/fileManager.webp',
description: 'Remix IDE file explorer',

@ -43,7 +43,6 @@ export class fsPlugin extends ElectronPlugin {
rmdir: async (path: string) => {
path = fixPath(path)
return await this.call('fs', 'rmdir', path)
},
readdir: async (path: string) => {
path = fixPath(path)

@ -0,0 +1,15 @@
import { ElectronPlugin } from '@remixproject/engine-electron';
export class electronTemplates extends ElectronPlugin {
constructor() {
super({
displayName: 'electronTemplates',
name: 'electronTemplates',
description: 'templates',
})
}
onActivation(): void {
}
}

@ -0,0 +1,31 @@
import { Plugin } from '@remixproject/engine'
import * as templateWithContent from '@remix-project/remix-ws-templates'
const profile = {
name: 'remix-templates',
displayName: 'remix-templates',
description: 'Remix Templates plugin',
methods: ['getTemplate', 'loadTemplateInNewWindow'],
}
export class TemplatesPlugin extends Plugin {
constructor() {
super(profile)
}
async getTemplate (template: string, opts?: any) {
const templateList = Object.keys(templateWithContent)
if (!templateList.includes(template)) return
// @ts-ignore
const files = await templateWithContent[template](opts)
console.log('files for template ', files)
return files
}
// electron only method
async loadTemplateInNewWindow (template: string, opts?: any) {
const files = await this.getTemplate(template, opts)
this.call('electronTemplates', 'loadTemplateInNewWindow', files)
}
}

@ -20,6 +20,8 @@ export class RemixEngine extends Engine {
if (name === 'fetchAndCompile') return { queueTimeout: 60000 * 4 }
if (name === 'walletconnect') return { queueTimeout: 60000 * 4 }
if (name === 'udapp') return { queueTimeout: 60000 * 4 }
if (name === 'fs') return { queueTimeout: 60000 * 4 }
if (name === 'isogit') return { queueTimeout: 60000 * 4 }
return { queueTimeout: 10000 }
}

@ -22,6 +22,7 @@
"typescript": "^5.1.3"
},
"dependencies": {
"@remix-project/remix-ws-templates": "^1.0.19",
"electron-is-packaged": "^1.0.2",
"glob": "^10.2.7",
"node-pty": "^0.10.1"

@ -7,6 +7,7 @@ import { XtermPlugin } from './plugins/xtermPlugin';
import git from 'isomorphic-git'
import { IsoGitPlugin } from './plugins/isoGitPlugin';
import { ConfigPlugin } from './plugins/configPlugin';
import { TemplatesPlugin } from './plugins/templates';
const engine = new Engine()
const appManager = new PluginManager()
@ -15,12 +16,14 @@ const gitPlugin = new GitPlugin()
const xtermPlugin = new XtermPlugin()
const isoGitPlugin = new IsoGitPlugin()
const configPlugin = new ConfigPlugin()
const templatesPlugin = new TemplatesPlugin()
engine.register(appManager)
engine.register(fsPlugin)
engine.register(gitPlugin)
engine.register(xtermPlugin)
engine.register(isoGitPlugin)
engine.register(configPlugin)
engine.register(templatesPlugin)
appManager.activatePlugin('electronconfig')
appManager.activatePlugin('fs')
@ -36,6 +39,18 @@ ipcMain.on('fs:openFolder', async (event) => {
fsPlugin.openFolder(event)
})
ipcMain.on('template:open', async (event) => {
console.log('template:open', event)
templatesPlugin.openTemplate(event)
})
ipcMain.on('git:startclone', async (event) => {
console.log('git:startclone', event)
isoGitPlugin.startClone(event)
})
ipcMain.handle('getWebContentsID', (event, message) => {
return event.sender.id
})

@ -46,10 +46,7 @@ export const createWindow = async (dir?: string): Promise<void> => {
})
windowSet.add(mainWindow)
// Open the DevTools.
mainWindow.webContents.openDevTools();
//mainWindow.webContents.openDevTools();
};
// This method will be called when Electron has finished
@ -97,6 +94,7 @@ import MainMenu from './menus/main';
import darwinMenu from './menus/darwin';
import WindowMenu from './menus/window';
import EditMenu from './menus/edit';
import GitMenu from './menus/git';
import { execCommand } from './menus/commands';
const commandKeys: Record<string, string> = {
@ -107,7 +105,8 @@ const commandKeys: Record<string, string> = {
const menu = [...(process.platform === 'darwin' ? [darwinMenu(commandKeys, execCommand, showAbout)] : []),
FileMenu(commandKeys, execCommand),
EditMenu(commandKeys, execCommand),
WindowMenu(commandKeys, execCommand, [])
WindowMenu(commandKeys, execCommand, []),
GitMenu(commandKeys, execCommand)
]
Menu.setApplicationMenu(Menu.buildFromTemplate(menu))

@ -11,6 +11,16 @@ const commands: Record<string, (focusedWindow?: BrowserWindow) => void> = {
if (focusedWindow) {
ipcMain.emit('fs:openFolder', focusedWindow.webContents.id);
}
},
'template:open': (focusedWindow) => {
if (focusedWindow) {
ipcMain.emit('template:open', focusedWindow.webContents.id);
}
},
'git:startclone': (focusedWindow) => {
if (focusedWindow) {
ipcMain.emit('git:startclone', focusedWindow.webContents.id);
}
}
};

@ -19,23 +19,6 @@ export default (
{
type: 'separator'
},
{
label: 'Preferences...',
accelerator: commandKeys['window:preferences'],
click() {
execCommand('window:preferences');
}
},
{
type: 'separator'
},
{
role: 'services',
submenu: []
},
{
type: 'separator'
},
{
role: 'hide'
},

@ -22,7 +22,13 @@ export default (
click(item, focusedWindow) {
execCommand('folder:open', focusedWindow);
}
}
},
{
label: 'Load Template in New Window',
click(item, focusedWindow) {
execCommand('template:open', focusedWindow);
}
},
]
};
};

@ -0,0 +1,20 @@
import {BrowserWindow, MenuItemConstructorOptions} from 'electron';
export default (
commandKeys: Record<string, string>,
execCommand: (command: string, focusedWindow?: BrowserWindow) => void
): MenuItemConstructorOptions => {
const isMac = process.platform === 'darwin';
return {
label: 'Git',
submenu: [
{
label: 'Clone Repository in New Window',
click(item, focusedWindow) {
execCommand('git:startclone', focusedWindow);
}
}
]
};
};

@ -72,7 +72,7 @@ const clientProfile: Profile = {
name: 'fs',
displayName: 'fs',
description: 'fs',
methods: ['readdir', 'readFile', 'writeFile', 'mkdir', 'rmdir', 'unlink', 'rename', 'stat', 'lstat', 'exists', 'currentPath', 'watch', 'closeWatch', 'setWorkingDir', 'openFolder', 'getRecentFolders', 'glob', 'openWindow']
methods: ['readdir', 'readFile', 'writeFile', 'mkdir', 'rmdir', 'unlink', 'rename', 'stat', 'lstat', 'exists', 'currentPath', 'watch', 'closeWatch', 'setWorkingDir', 'openFolder', 'getRecentFolders', 'glob', 'openWindow', 'selectFolder']
}
class FSPluginClient extends ElectronBasePluginClient {
@ -83,11 +83,12 @@ class FSPluginClient extends ElectronBasePluginClient {
super(webContentsId, profile)
this.onload(() => {
//console.log('fsPluginClient onload')
this.window.webContents.openDevTools()
this.window.on('close', async () => {
console.log('close', this.webContentsId)
await this.removeFromOpenedFolders(this.workingDir)
})
})
}
@ -96,9 +97,11 @@ class FSPluginClient extends ElectronBasePluginClient {
// call node fs.readdir
console.log('readdir', path)
if (!path) return []
const files = await fs.readdir(this.fixPath(path),{
const startTime = Date.now()
const files = await fs.readdir(this.fixPath(path), {
withFileTypes: true
})
const result: any[] = []
for (const file of files) {
const isDirectory = file.isDirectory()
@ -107,11 +110,12 @@ class FSPluginClient extends ElectronBasePluginClient {
isDirectory
})
}
console.log('readdir', path, Date.now() - startTime)
return result
}
async glob(path: string, pattern: string, options?: GlobOptions): Promise<string[] | Path[]> {
path = this.fixPath(path)
console.log('glob', path, pattern, options)
const files = await glob(path + pattern, {
@ -122,10 +126,10 @@ class FSPluginClient extends ElectronBasePluginClient {
for (const file of files) {
let pathWithoutWorkingDir = (file as Path).path.replace(this.workingDir, '')
if(!pathWithoutWorkingDir.endsWith('/')){
if (!pathWithoutWorkingDir.endsWith('/')) {
pathWithoutWorkingDir = pathWithoutWorkingDir + '/'
}
if(pathWithoutWorkingDir.startsWith('/')){
if (pathWithoutWorkingDir.startsWith('/')) {
pathWithoutWorkingDir = pathWithoutWorkingDir.slice(1)
}
result.push({
@ -133,7 +137,7 @@ class FSPluginClient extends ElectronBasePluginClient {
isDirectory: (file as Path).isDirectory(),
})
}
console.log('glob', result)
//console.log('glob', result)
return result
}
@ -159,7 +163,10 @@ class FSPluginClient extends ElectronBasePluginClient {
}
async rmdir(path: string): Promise<void> {
return fs.rmdir(this.fixPath(path))
console.log('rmdir', this.fixPath(path))
return fs.rm(this.fixPath(path), {
recursive: true
})
}
async unlink(path: string): Promise<void> {
@ -209,11 +216,25 @@ class FSPluginClient extends ElectronBasePluginClient {
return process.cwd()
}
async watch(path: string): Promise<void> {
async watch(): Promise<void> {
if (this.watcher) this.watcher.close()
console.log('watch', this.workingDir)
this.watcher =
chokidar.watch(this.fixPath(path)).on('change', (path, stats) => {
this.emit('change', path, stats)
chokidar.watch(this.workingDir, {
ignorePermissionErrors: true, ignoreInitial: true,
ignored: [
'**/node_modules/**',
'**/.git/**',
]
}).on('all', (eventName, path, stats) => {
console.log('change', eventName, path, stats)
// remove workingDir from path
path = path.replace(this.workingDir, '')
try {
this.emit('change', eventName, path)
} catch (e) {
console.log('error emitting change', e)
}
})
}
@ -253,6 +274,19 @@ class FSPluginClient extends ElectronBasePluginClient {
}
async selectFolder(path?: string): Promise<string> {
let dirs: string[] | undefined
if (!path) {
dirs = dialog.showOpenDialogSync(this.window, {
properties: ['openDirectory', 'createDirectory', "showHiddenFiles"]
})
}
path = dirs && dirs.length && dirs[0] ? dirs[0] : path
if (!path) return ''
return path
}
async openFolder(path?: string): Promise<void> {
let dirs: string[] | undefined
@ -268,6 +302,7 @@ class FSPluginClient extends ElectronBasePluginClient {
await this.updateOpenedFolders(path)
this.window.setTitle(this.workingDir)
console.log('setWorkingDir', path)
this.watch()
this.emit('workingDirChanged', path)
}
@ -277,6 +312,7 @@ class FSPluginClient extends ElectronBasePluginClient {
await this.updateRecentFolders(path)
await this.updateOpenedFolders(path)
this.window.setTitle(this.workingDir)
this.watch()
this.emit('workingDirChanged', path)
}

@ -9,7 +9,7 @@ const profile: Profile = {
}
export class GitPlugin extends ElectronBasePlugin {
client: PluginClient
clients: GitPluginClient[] = []
constructor() {
super(profile, clientProfile, GitPluginClient)
}

@ -13,13 +13,20 @@ const profile: Profile = {
}
export class IsoGitPlugin extends ElectronBasePlugin {
client: PluginClient
clients: IsoGitPluginClient[] = []
constructor() {
super(profile, clientProfile, IsoGitPluginClient)
}
startClone(webContentsId: any): void {
const client = this.clients.find(c => c.webContentsId === webContentsId)
if (client) {
client.startClone()
}
}
}
const parseInput = (input: any)=> {
const parseInput = (input: any) => {
return {
corsProxy: 'https://corsproxy.remixproject.org/',
http,
@ -51,30 +58,30 @@ class IsoGitPluginClient extends ElectronBasePluginClient {
console.log('workingDirChanged', path)
this.workingDir = path
await this.status({
})
})
})
}
async getGitConfig () {
return {
fs,
dir: this.workingDir,
}
async getGitConfig() {
return {
fs,
dir: this.workingDir,
}
}
async status (cmd: any) {
async status(cmd: any) {
console.log('status')
const status = await git.statusMatrix({
...await this.getGitConfig(),
...cmd
})
console.log('STATUS', status, await this.getGitConfig())
//console.log('STATUS', status, await this.getGitConfig())
return status
}
async log (cmd: any) {
async log(cmd: any) {
console.log('log')
const log = await git.log({
...await this.getGitConfig(),
@ -84,7 +91,7 @@ class IsoGitPluginClient extends ElectronBasePluginClient {
return log
}
async add (cmd: any) {
async add(cmd: any) {
console.log('add')
const add = await git.add({
...await this.getGitConfig(),
@ -104,7 +111,7 @@ class IsoGitPluginClient extends ElectronBasePluginClient {
return rm
}
async commit (cmd: any) {
async commit(cmd: any) {
console.log('commit')
const commit = await git.commit({
...await this.getGitConfig(),
@ -114,14 +121,14 @@ class IsoGitPluginClient extends ElectronBasePluginClient {
return commit
}
async init (input: any) {
async init(input: any) {
await git.init({
...await this.getGitConfig(),
defaultBranch: (input && input.branch) || 'main'
})
}
async branch (cmd: any) {
async branch(cmd: any) {
console.log('branch')
const branch = await git.branch({
...await this.getGitConfig(),
@ -131,7 +138,7 @@ class IsoGitPluginClient extends ElectronBasePluginClient {
return branch
}
async lsfiles (cmd: any) {
async lsfiles(cmd: any) {
console.log('lsfiles')
const lsfiles = await git.listFiles({
...await this.getGitConfig(),
@ -141,7 +148,7 @@ class IsoGitPluginClient extends ElectronBasePluginClient {
return lsfiles
}
async resolveref (cmd: any) {
async resolveref(cmd: any) {
console.log('resolveref')
const resolveref = await git.resolveRef({
...await this.getGitConfig(),
@ -152,7 +159,7 @@ class IsoGitPluginClient extends ElectronBasePluginClient {
}
async readblob (cmd: any) {
async readblob(cmd: any) {
console.log('readblob')
const readblob = await git.readBlob({
...await this.getGitConfig(),
@ -162,7 +169,7 @@ class IsoGitPluginClient extends ElectronBasePluginClient {
return readblob
}
async checkout (cmd: any) {
async checkout(cmd: any) {
console.log('checkout')
const checkout = await git.checkout({
...await this.getGitConfig(),
@ -172,7 +179,7 @@ class IsoGitPluginClient extends ElectronBasePluginClient {
return checkout
}
async push (cmd: any) {
async push(cmd: any) {
console.log('push')
const push = await git.push({
...await this.getGitConfig(),
@ -183,7 +190,7 @@ class IsoGitPluginClient extends ElectronBasePluginClient {
return push
}
async pull (cmd: any) {
async pull(cmd: any) {
console.log('pull', cmd)
const pull = await git.pull({
...await this.getGitConfig(),
@ -207,26 +214,19 @@ class IsoGitPluginClient extends ElectronBasePluginClient {
async clone(cmd: any) {
console.log('clone')
const clone = await git.clone({
...await this.getGitConfig(),
...cmd,
...parseInput(cmd.input),
dir: cmd.dir || this.workingDir
})
console.log('CLONE', clone)
return clone
}
async openFolder(path?: string): Promise<string> {
let dirs: string[] | undefined
if (!path) {
dirs = dialog.showOpenDialogSync(this.window, {
properties: ['openDirectory', 'createDirectory', "showHiddenFiles"]
try {
const clone = await git.clone({
...await this.getGitConfig(),
...cmd,
...parseInput(cmd.input),
dir: cmd.dir || this.workingDir
})
console.log('CLONE', clone)
return clone
} catch (e) {
console.log('CLONE ERROR', e)
throw e
}
path = dirs && dirs.length && dirs[0] ? dirs[0] : path
if (!path) return ''
return path
}
async addremote(cmd: any) {
@ -248,12 +248,12 @@ class IsoGitPluginClient extends ElectronBasePluginClient {
console.log('DELREMOTE', delremote)
return delremote
}
remotes = async () => {
let remotes = []
remotes = await git.listRemotes({...await this.getGitConfig() })
remotes = await git.listRemotes({ ...await this.getGitConfig() })
console.log('remotes', remotes)
return remotes
}
@ -272,7 +272,7 @@ class IsoGitPluginClient extends ElectronBasePluginClient {
async branches() {
try {
let cmd: any = {...await this.getGitConfig()}
let cmd: any = { ...await this.getGitConfig() }
const remotes = await this.remotes()
let branches = []
branches = (await git.listBranches(cmd)).map((branch) => { return { remote: undefined, name: branch } })
@ -281,10 +281,10 @@ class IsoGitPluginClient extends ElectronBasePluginClient {
...cmd,
remote: remote.remote
}
const remotebranches = (await git.listBranches(cmd)).map((branch) => { return { remote: remote.remote, name: branch } })
branches = [...branches, ...remotebranches]
}
console.log('branches', branches)
return branches
@ -293,11 +293,10 @@ class IsoGitPluginClient extends ElectronBasePluginClient {
}
}
async startClone() {
this.call('filePanel' as any, 'clone')
}
}

@ -0,0 +1,75 @@
import { PluginClient } from "@remixproject/plugin";
import { Profile } from "@remixproject/plugin-utils";
import { ElectronBasePlugin, ElectronBasePluginClient } from "@remixproject/plugin-electron"
import * as templateWithContent from '@remix-project/remix-ws-templates'
import fs from 'fs/promises'
import { createWindow } from "../main";
import path from 'path'
const profile: Profile = {
name: 'electronTemplates',
displayName: 'electronTemplates',
description: 'Templates plugin',
}
export class TemplatesPlugin extends ElectronBasePlugin {
clients: TemplatesPluginClient[] = []
constructor() {
super(profile, clientProfile, TemplatesPluginClient)
}
openTemplate(webContentsId: any): void {
const client = this.clients.find(c => c.webContentsId === webContentsId)
if (client) {
client.openTemplate()
}
}
}
const clientProfile: Profile = {
name: 'electronTemplates',
displayName: 'electronTemplates',
description: 'Templates plugin',
methods: ['loadTemplateInNewWindow', 'openTemplate'],
}
export type WorkspaceTemplate = 'gist-template' | 'code-template' | 'remixDefault' | 'blank' | 'ozerc20' | 'zeroxErc20' | 'ozerc721'
class TemplatesPluginClient extends ElectronBasePluginClient {
constructor(webContentsId: number, profile: Profile) {
super(webContentsId, profile)
this.onload(() => {
console.log('TemplatesPluginClient onload')
})
}
async loadTemplateInNewWindow (files: any) {
let folder = await this.call('fs' as any, 'selectFolder')
if (!folder || folder === '') return
// @ts-ignore
console.log('files for template ', files)
for (const file in files) {
try {
if(!folder.endsWith('/')) folder += '/'
console.log('writing file', folder + file)
await fs.mkdir(path.dirname(folder + file), { recursive: true})
await fs.writeFile(folder + file, files[file], {
encoding: 'utf8'
})
} catch (error) {
console.error(error)
}
}
createWindow(folder)
}
async openTemplate(){
this.call('filePanel' as any, 'loadTemplate')
}
}

@ -12,7 +12,7 @@ const profile: Profile = {
}
export class XtermPlugin extends ElectronBasePlugin {
client: PluginClient
clients: XtermPluginClient[] = []
constructor() {
super(profile, clientProfile, XtermPluginClient)
}

@ -6,7 +6,7 @@ console.log('preload.ts')
/* preload script needs statically defined API for each plugin */
const exposedPLugins = ['fs', 'git', 'xterm', 'isogit', 'electronconfig']
const exposedPLugins = ['fs', 'git', 'xterm', 'isogit', 'electronconfig', 'electronTemplates']
console.log('preload.ts', process)
let webContentsId: number | undefined

File diff suppressed because it is too large Load Diff

@ -0,0 +1,34 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import React, { useState, useRef, useReducer } from 'react'
import { FormattedMessage } from 'react-intl'
import { ModalDialog } from '@remix-ui/modal-dialog' // eslint-disable-line
import { Toaster } from '@remix-ui/toaster' // eslint-disable-line
const _paq = window._paq = window._paq || [] // eslint-disable-line
import { CustomTooltip } from '@remix-ui/helper';
interface HomeTabFileProps {
plugin: any
}
export const HomeTabFileElectron = ({ plugin }: HomeTabFileProps) => {
const loadTemplate = async () => {
plugin.call('filePanel', 'loadTemplate')
}
const clone = async () => {
plugin.call('filePanel', 'clone')
}
return (
<div className="justify-content-start mt-1 p-2 d-flex flex-column" id="hTFileSection">
<label style={{ fontSize: "1.2rem" }}><FormattedMessage id='home.files' /></label>
<label style={{ fontSize: "0.8rem" }} className="pt-2"><FormattedMessage id='home.loadFrom' /></label>
<div className="d-flex">
<button className="btn p-2 border mr-2" data-id="landingPageImportFromGistButton" onClick={async () => await loadTemplate()}>Project Template</button>
<button className="btn p-2 border mr-2" data-id="landingPageImportFromGistButton" onClick={async () => await clone()}>Clone a Git Repository</button>
</div>
</div>
)
}

@ -7,6 +7,7 @@ import Carousel from 'react-multi-carousel'
import WorkspaceTemplate from './workspaceTemplate'
import 'react-multi-carousel/lib/styles.css'
import CustomNavButtons from './customNavButtons'
import isElectron from 'is-electron'
declare global {
interface Window {
_paq: any
@ -58,6 +59,12 @@ function HomeTabGetStarted ({plugin}: HomeTabGetStartedProps) {
}
const createWorkspace = async (templateName) => {
if(isElectron()){
await plugin.call('remix-templates', 'loadTemplateInNewWindow', templateName)
return
}
await plugin.appManager.activatePlugin('filePanel')
const timeStamp = Date.now()
let templateDisplayName = TEMPLATE_NAMES[templateName]

@ -9,6 +9,8 @@ import HomeTabScamAlert from './components/homeTabScamAlert'
import HomeTabGetStarted from './components/homeTabGetStarted'
import HomeTabFeatured from './components/homeTabFeatured'
import HomeTabFeaturedPlugins from './components/homeTabFeaturedPlugins'
import isElectron from 'is-electron'
import { HomeTabFileElectron } from './components/homeTabFileElectron'
declare global {
interface Window {
@ -50,7 +52,9 @@ export const RemixUiHomeTab = (props: RemixUiHomeTabProps) => {
<div className='d-flex flex-row w-100'>
<div className="px-2 pl-3 justify-content-start d-flex border-right flex-column" id="remixUIHTLeft" style={{ width: 'inherit' }}>
<HomeTabTitle />
<HomeTabFile plugin={plugin} />
{!isElectron()?
<HomeTabFile plugin={plugin} />:
<HomeTabFileElectron plugin={plugin}></HomeTabFileElectron>}
<HomeTabLearn plugin={plugin} />
</div>
<div className="pl-2 pr-3 justify-content-start d-flex flex-column" style={{width: "65%"}} id="remixUIHTRight">

@ -8,7 +8,7 @@ import { createWorkspaceTemplate, getWorkspaces, loadWorkspacePreset, setPlugin,
import { QueryParams } from '@remix-project/remix-lib'
import { fetchContractFromEtherscan } from '@remix-project/core-plugin' // eslint-disable-line
import JSZip from 'jszip'
import isElectron from 'is-electron'
import isElectron from 'is-electron'
export * from './events'
export * from './workspace'
@ -55,7 +55,7 @@ export const initWorkspace = (filePanelPlugin) => async (reducerDispatch: React.
const electrOnProvider = filePanelPlugin.fileProviders.electron
const params = queryParams.get() as UrlParametersType
let workspaces = []
if (!isElectron()){
if (!isElectron()) {
workspaces = await getWorkspaces() || []
dispatch(setWorkspaces(workspaces))
}
@ -80,11 +80,11 @@ export const initWorkspace = (filePanelPlugin) => async (reducerDispatch: React.
let etherscanKey = await plugin.call('config', 'getAppParameter', 'etherscan-access-token')
if (!etherscanKey) etherscanKey = '2HKUX5ZVASZIKWJM8MIQVCRUVZ6JAWT531'
const networks = [
{id: 1, name: 'mainnet'},
{id: 3, name: 'ropsten'},
{id: 4, name: 'rinkeby'},
{id: 42, name: 'kovan'},
{id: 5, name: 'goerli'}
{ id: 1, name: 'mainnet' },
{ id: 3, name: 'ropsten' },
{ id: 4, name: 'rinkeby' },
{ id: 42, name: 'kovan' },
{ id: 5, name: 'goerli' }
]
let found = false
const workspaceName = 'etherscan-code-sample'
@ -113,7 +113,7 @@ export const initWorkspace = (filePanelPlugin) => async (reducerDispatch: React.
await workspaceProvider.set(filePath, data.compilationTargets[filePath]['content'])
}
plugin.on('editor', 'editorMounted', async () => await plugin.fileManager.openFile(filePath))
plugin.call('notification', 'toast', `Added ${count} verified contract${count === 1 ? '': 's'} from ${foundOnNetworks.join(',')} network${foundOnNetworks.length === 1 ? '': 's'} of Etherscan for contract address ${contractAddress} !!`)
plugin.call('notification', 'toast', `Added ${count} verified contract${count === 1 ? '' : 's'} from ${foundOnNetworks.join(',')} network${foundOnNetworks.length === 1 ? '' : 's'} of Etherscan for contract address ${contractAddress} !!`)
} catch (error) {
await basicWorkspaceInit(workspaces, workspaceProvider)
}
@ -121,21 +121,22 @@ export const initWorkspace = (filePanelPlugin) => async (reducerDispatch: React.
} else if (isElectron()) {
console.log('isElectron initWorkspace')
plugin.call('notification', 'toast', `connecting to electron...`)
if(params.opendir){
if (params.opendir) {
params.opendir = decodeURIComponent(params.opendir)
plugin.call('notification', 'toast', `opening ${params.opendir}...`)
plugin.call('fs', 'setWorkingDir', params.opendir)
await plugin.call('fs', 'setWorkingDir', params.opendir)
}
plugin.setWorkspace({ name: 'electron', isLocalhost: false })
dispatch(setCurrentWorkspace({ name: 'electron', isGitRepo: false }))
electrOnProvider.init()
listenOnProviderEvents(electrOnProvider)(dispatch)
listenOnPluginEvents(plugin)
dispatch(setMode('browser'))
dispatch(fsInitializationCompleted())
plugin.emit('workspaceInitializationCompleted')
return
} else if (localStorage.getItem("currentWorkspace")) {
const index = workspaces.findIndex(element => element.name == localStorage.getItem("currentWorkspace"))
if (index !== -1) {
@ -143,10 +144,10 @@ export const initWorkspace = (filePanelPlugin) => async (reducerDispatch: React.
workspaceProvider.setWorkspace(name)
plugin.setWorkspace({ name: name, isLocalhost: false })
dispatch(setCurrentWorkspace({ name: name, isGitRepo: false }))
}else{
} else {
_paq.push(['trackEvent', 'Storage', 'error', `Workspace in localstorage not found: ${localStorage.getItem("currentWorkspace")}`])
await basicWorkspaceInit(workspaces, workspaceProvider)
}
}
} else {
await basicWorkspaceInit(workspaces, workspaceProvider)
}
@ -155,9 +156,9 @@ export const initWorkspace = (filePanelPlugin) => async (reducerDispatch: React.
listenOnProviderEvents(workspaceProvider)(dispatch)
listenOnProviderEvents(localhostProvider)(dispatch)
listenOnProviderEvents(electrOnProvider)(dispatch)
if(isElectron()){
if (isElectron()) {
dispatch(setMode('browser'))
}else{
} else {
dispatch(setMode('browser'))
}
@ -207,11 +208,11 @@ export const publishToGist = async (path?: string, type?: string) => {
const accessToken = config.get('settings/gist-access-token')
if (!accessToken) {
dispatch(displayNotification('Authorize Token', 'Remix requires an access token (which includes gists creation permission). Please go to the settings tab to create one.', 'Close', null, () => {}))
dispatch(displayNotification('Authorize Token', 'Remix requires an access token (which includes gists creation permission). Please go to the settings tab to create one.', 'Close', null, () => { }))
} else {
const params = queryParams.get() as SolidityConfiguration
const description = 'Created using remix-ide: Realtime Ethereum Contract Compiler and Runtime. \n Load this file by pasting this gists URL or ID at https://remix.ethereum.org/#version=' +
params.version + '&optimize=' + params.optimize + '&runs=' + params.runs + '&gist='
params.version + '&optimize=' + params.optimize + '&runs=' + params.runs + '&gist='
const gists = new Gists({ token: accessToken })
if (id) {
@ -259,7 +260,7 @@ export const publishToGist = async (path?: string, type?: string) => {
}
} catch (error) {
console.log(error)
dispatch(displayNotification('Publish to gist Failed', 'Failed to create gist: ' + error.message, 'Close', null, async () => {}))
dispatch(displayNotification('Publish to gist Failed', 'Failed to create gist: ' + error.message, 'Close', null, async () => { }))
}
}
@ -292,7 +293,7 @@ export const createNewFolder = async (path: string, rootDir: string) => {
const exists = await fileManager.exists(dirName)
if (exists) {
return dispatch(displayNotification('Failed to create folder', `A folder ${extractNameFromKey(path)} already exists at this location. Please choose a different name.`, 'Close', null, () => {}))
return dispatch(displayNotification('Failed to create folder', `A folder ${extractNameFromKey(path)} already exists at this location. Please choose a different name.`, 'Close', null, () => { }))
}
await fileManager.mkdir(dirName)
path = path.indexOf(rootDir + '/') === 0 ? path.replace(rootDir + '/', '') : path
@ -318,7 +319,7 @@ export const renamePath = async (oldPath: string, newPath: string) => {
const exists = await fileManager.exists(newPath)
if (exists) {
dispatch(displayNotification('Rename File Failed', `A file or folder ${extractNameFromKey(newPath)} already exists at this location. Please choose a different name.`, 'Close', null, () => {}))
dispatch(displayNotification('Rename File Failed', `A file or folder ${extractNameFromKey(newPath)} already exists at this location. Please choose a different name.`, 'Close', null, () => { }))
} else {
await fileManager.rename(oldPath, newPath)
}
@ -476,7 +477,7 @@ const handleGistResponse = (error, data) => {
if (data.html_url) {
dispatch(displayNotification('Gist is ready', `The gist is at ${data.html_url}. Would you like to open it in a new window?`, 'OK', 'Cancel', () => {
window.open(data.html_url, '_blank')
}, () => {}))
}, () => { }))
} else {
const error = JSON.stringify(data.errors, null, '\t') || ''
const message = data.message === 'Not Found' ? data.message + '. Please make sure the API token has right to create a gist.' : data.message

@ -13,6 +13,7 @@ import { ROOT_PATH, slitherYml, solTestYml, tsSolTestYml } from '../utils/consta
import { IndexedDBStorage } from '../../../../../../apps/remix-ide/src/app/files/filesystems/indexedDB'
import { getUncommittedFiles } from '../utils/gitStatusFilter'
import { AppModal, ModalTypes } from '@remix-ui/app'
import isElectron from 'is-electron'
declare global {
interface Window { remixFileSystemCallback: IndexedDBStorage; }
@ -83,6 +84,15 @@ const removeSlash = (s: string) => {
}
export const createWorkspace = async (workspaceName: string, workspaceTemplateName: WorkspaceTemplate, opts = null, isEmpty = false, cb?: (err: Error, result?: string | number | boolean | Record<string, any>) => void, isGitRepo: boolean = false, createCommit: boolean = true) => {
if (isElectron()) {
if (workspaceTemplateName) {
await plugin.call('remix-templates', 'loadTemplateInNewWindow', workspaceTemplateName, opts)
}
return
}
await plugin.fileManager.closeAllFiles()
const promise = createWorkspaceTemplate(workspaceName, workspaceTemplateName)
dispatch(createWorkspaceRequest(promise))
@ -165,6 +175,7 @@ export type UrlParametersType = {
export const loadWorkspacePreset = async (template: WorkspaceTemplate = 'remixDefault', opts?) => {
const workspaceProvider = plugin.fileProviders.workspace
const electronProvider = plugin.fileProviders.electron
const params = queryParams.get() as UrlParametersType
switch (template) {
@ -243,6 +254,7 @@ export const loadWorkspacePreset = async (template: WorkspaceTemplate = 'remixDe
_paq.push(['trackEvent', 'workspace', 'template', template])
// @ts-ignore
const files = await templateWithContent[template](opts)
console.log('files for template ', files)
for (const file in files) {
try {
await workspaceProvider.set(file, files[file])
@ -276,7 +288,7 @@ export const fetchWorkspaceDirectory = async (path: string) => {
if (error) {
console.error(error)
return reject(error)
}
}
console.log('fetchWorkspaceDirectory', fileTree)
resolve(fileTree)
})
@ -346,12 +358,12 @@ export const switchToWorkspace = async (name: string) => {
// if there is no other workspace, create remix default workspace
plugin.call('notification', 'toast', `No workspace found! Creating default workspace ....`)
await createWorkspace('default_workspace', 'remixDefault')
} else if(name === ELECTRON) {
} else if (name === ELECTRON) {
await plugin.fileProviders.workspace.setWorkspace(name)
await plugin.setWorkspace({ name, isLocalhost: false })
dispatch(setMode('browser'))
dispatch(setCurrentWorkspace({ name, isGitRepo:false }))
dispatch(setCurrentWorkspace({ name, isGitRepo: false }))
} else {
const isActive = await plugin.call('manager', 'isActive', 'remixd')
@ -417,8 +429,8 @@ export const uploadFile = async (target, targetFolder: string, cb?: (err: Error,
okFn: () => {
loadFile(name, file, workspaceProvider, cb)
},
cancelFn: () => {},
hideFn: () => {}
cancelFn: () => { },
hideFn: () => { }
}
plugin.call('notification', 'modal', modalContent)
}
@ -426,7 +438,7 @@ export const uploadFile = async (target, targetFolder: string, cb?: (err: Error,
}
export const uploadFolder = async (target, targetFolder: string, cb?: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => {
for(const file of [...target.files]) {
for (const file of [...target.files]) {
const workspaceProvider = plugin.fileProviders.workspace
const name = targetFolder === '/' ? file.webkitRelativePath : `${targetFolder}/${file.webkitRelativePath}`
if (!await workspaceProvider.exists(name)) {
@ -442,8 +454,8 @@ export const uploadFolder = async (target, targetFolder: string, cb?: (err: Erro
okFn: () => {
loadFile(name, file, workspaceProvider, cb)
},
cancelFn: () => {},
hideFn: () => {}
cancelFn: () => { },
hideFn: () => { }
}
plugin.call('notification', 'modal', modalContent)
}
@ -495,46 +507,58 @@ export const cloneRepository = async (url: string) => {
const token = config.get('settings/gist-access-token')
const repoConfig = { url, token }
try {
const repoName = await getRepositoryTitle(url)
await createWorkspace(repoName, 'blank', null, true, null, true, false)
const promise = plugin.call('dGitProvider', 'clone', repoConfig, repoName, true)
dispatch(cloneRepositoryRequest())
promise.then(async () => {
const isActive = await plugin.call('manager', 'isActive', 'dgit')
if (!isActive) await plugin.call('manager', 'activatePlugin', 'dgit')
await fetchWorkspaceDirectory(ROOT_PATH)
const workspacesPath = plugin.fileProviders.workspace.workspacesPath
const branches = await getGitRepoBranches(workspacesPath + '/' + repoName)
dispatch(setCurrentWorkspaceBranches(branches))
const currentBranch = await getGitRepoCurrentBranch(workspacesPath + '/' + repoName)
dispatch(setCurrentWorkspaceCurrentBranch(currentBranch))
dispatch(cloneRepositorySuccess())
}).catch(() => {
const cloneModal = {
if (isElectron()) {
try {
await plugin.call('dGitProvider', 'clone', repoConfig)
} catch (e) {
console.log(e)
plugin.call('notification', 'alert', {
id: 'cloneGitRepository',
title: 'Clone Git Repository',
message: 'An error occurred: Please check that you have the correct URL for the repo. If the repo is private, you need to add your github credentials (with the valid token permissions) in Settings plugin',
modalType: 'modal',
okLabel: 'OK',
okFn: async () => {
await deleteWorkspace(repoName)
dispatch(cloneRepositoryFailed())
},
hideFn: async () => {
await deleteWorkspace(repoName)
dispatch(cloneRepositoryFailed())
message: e
})
}
} else {
try {
const repoName = await getRepositoryTitle(url)
await createWorkspace(repoName, 'blank', null, true, null, true, false)
const promise = plugin.call('dGitProvider', 'clone', repoConfig, repoName, true)
dispatch(cloneRepositoryRequest())
promise.then(async () => {
const isActive = await plugin.call('manager', 'isActive', 'dgit')
if (!isActive) await plugin.call('manager', 'activatePlugin', 'dgit')
await fetchWorkspaceDirectory(ROOT_PATH)
const workspacesPath = plugin.fileProviders.workspace.workspacesPath
const branches = await getGitRepoBranches(workspacesPath + '/' + repoName)
dispatch(setCurrentWorkspaceBranches(branches))
const currentBranch = await getGitRepoCurrentBranch(workspacesPath + '/' + repoName)
dispatch(setCurrentWorkspaceCurrentBranch(currentBranch))
dispatch(cloneRepositorySuccess())
}).catch(() => {
const cloneModal = {
id: 'cloneGitRepository',
title: 'Clone Git Repository',
message: 'An error occurred: Please check that you have the correct URL for the repo. If the repo is private, you need to add your github credentials (with the valid token permissions) in Settings plugin',
modalType: 'modal',
okLabel: 'OK',
okFn: async () => {
await deleteWorkspace(repoName)
dispatch(cloneRepositoryFailed())
},
hideFn: async () => {
await deleteWorkspace(repoName)
dispatch(cloneRepositoryFailed())
}
}
}
plugin.call('notification', 'modal', cloneModal)
})
} catch (e) {
dispatch(displayPopUp('An error occured: ' + e))
plugin.call('notification', 'modal', cloneModal)
})
} catch (e) {
dispatch(displayPopUp('An error occured: ' + e))
}
}
}
@ -671,21 +695,21 @@ export const createNewBranch = async (branch: string) => {
export const createSolidityGithubAction = async () => {
const path = '.github/workflows/run-solidity-unittesting.yml'
await plugin.call('fileManager', 'writeFile', path , solTestYml)
await plugin.call('fileManager', 'writeFile', path, solTestYml)
plugin.call('fileManager', 'open', path)
}
export const createTsSolGithubAction = async () => {
const path = '.github/workflows/run-js-test.yml'
await plugin.call('fileManager', 'writeFile', path , tsSolTestYml)
await plugin.call('fileManager', 'writeFile', path, tsSolTestYml)
plugin.call('fileManager', 'open', path)
}
export const createSlitherGithubAction = async () => {
const path = '.github/workflows/run-slither-action.yml'
await plugin.call('fileManager', 'writeFile', path , slitherYml)
await plugin.call('fileManager', 'writeFile', path, slitherYml)
plugin.call('fileManager', 'open', path)
}

@ -102,6 +102,16 @@ export function Workspace() {
}
setCurrentWorkspace(workspaceName)
resetFocus()
// expose some UI to the plugin, perhaps not the best way to do it
if (global.plugin) {
global.plugin.loadTemplate = async () => {
createWorkspace()
}
global.plugin.clone = async () => {
cloneGitRepository()
}
}
}, [])
useEffect(() => {
@ -686,7 +696,7 @@ export function Workspace() {
<FormattedMessage id='filePanel.workspace' />
</label>
</span> : null}
{currentWorkspace !== LOCALHOST && !isElectron() ? (<span className="remixui_menu remixui_topmenu d-flex justify-content-between align-items-end w-75">
{currentWorkspace !== LOCALHOST ? (<span className="remixui_menu remixui_topmenu d-flex justify-content-between align-items-end w-75">
<CustomTooltip
placement="top"
tooltipId="createWorkspaceTooltip"

Loading…
Cancel
Save