Merge branch 'desktop-master' of https://github.com/ethereum/remix-project into desktopslither2

pull/4925/head
Your Name 8 months ago
commit 11e9666cba
  1. 4
      apps/remix-ide-e2e/src/tests/homeTab.test.ts
  2. 54
      apps/remix-ide-e2e/src/tests/importFromGithub.test.ts
  3. 2
      apps/remix-ide-e2e/src/tests/terminal.test.ts
  4. 13
      apps/remix-ide/src/app.js
  5. 3
      apps/remix-ide/src/app/panels/file-panel.js
  6. 2
      apps/remix-ide/src/app/plugins/permission-handler-plugin.tsx
  7. 17
      apps/remix-ide/src/app/plugins/remix-templates.ts
  8. 2
      apps/remix-ide/src/app/plugins/remixGuide.tsx
  9. 2
      apps/remix-ide/src/app/plugins/remixGuideData.json
  10. 2
      apps/remix-ide/src/app/tabs/locales/en/filePanel.json
  11. 5
      apps/remix-ide/src/app/tabs/locales/en/home.json
  12. 47
      apps/remix-ide/src/assets/js/loader.js
  13. 29
      apps/remixdesktop/src/main.ts
  14. 7
      apps/remixdesktop/src/preload.ts
  15. 25
      apps/remixdesktop/src/utils/matamo.ts
  16. 2
      apps/remixdesktop/test/tests/app/gist.test.ts
  17. 6
      libs/remix-ui/home-tab/src/lib/components/homeTabFeatured.tsx
  18. 236
      libs/remix-ui/home-tab/src/lib/components/homeTabFile.tsx
  19. 40
      libs/remix-ui/home-tab/src/lib/components/homeTabGetStarted.tsx
  20. 2
      libs/remix-ui/home-tab/src/lib/components/homeTabTitle.tsx
  21. 6
      libs/remix-ui/home-tab/src/lib/remix-ui-home-tab.css
  22. 6
      libs/remix-ui/home-tab/src/lib/remix-ui-home-tab.tsx
  23. 4
      libs/remix-ui/permission-handler/src/lib/permission-dialog.tsx
  24. 12
      libs/remix-ui/statusbar/src/css/statusbar.css
  25. 6
      libs/remix-ui/statusbar/src/lib/components/aiStatus.tsx
  26. 2
      libs/remix-ui/statusbar/src/lib/components/scamAlertStatus.tsx
  27. 2
      libs/remix-ui/workspace/src/lib/actions/index.ts
  28. 23
      libs/remix-ui/workspace/src/lib/components/file-explorer-menu.tsx
  29. 8
      libs/remix-ui/workspace/src/lib/components/file-explorer.tsx
  30. 172
      libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx
  31. 4
      libs/remix-ui/workspace/src/lib/types/index.ts

@ -28,8 +28,8 @@ module.exports = {
'Should start with ERC20 workspace #group1': function (browser: NightwatchBrowser) {
browser
.click('*[data-path="home"')
.waitForElementVisible('*[data-id="homeTabGetStartedERC20"]')
.click('*[data-id="homeTabGetStartedERC20"')
.waitForElementVisible('*[data-id="homeTabGetStartedozerc20"]')
.click('*[data-id="homeTabGetStartedozerc20"')
.waitForElementVisible('*[data-id="treeViewLitreeViewItemcontracts/MyToken.sol"]')
.waitForElementVisible('*[data-id="treeViewDivtreeViewItemtests/MyToken_test.sol"]')
.click('*[data-id="treeViewDivtreeViewItemtests/MyToken_test.sol"]')

@ -3,9 +3,9 @@ import { NightwatchBrowser } from 'nightwatch'
import init from '../helpers/init'
const testData = {
validURL: 'https://github.com/OpenZeppelin/openzeppelin-solidity/blob/67bca857eedf99bf44a4b6a0fc5b5ed553135316/contracts/access/Roles.sol',
validURL: 'https://github.com/remix-project-org/git-hometab-test.git',
invalidURL: 'https://github.com/Oppelin/Roles.sol',
JSON: 'https://github.com/ethereum/remix-project/blob/master/package.json'
JSON: 'https://github.com/remix-project-org/git-hometab-test.git'
}
module.exports = {
@ -22,63 +22,53 @@ module.exports = {
.waitForElementVisible('button[data-id="landingPageImportFromGitHubButton"]')
.pause(1000)
.click('button[data-id="landingPageImportFromGitHubButton"]')
.waitForElementVisible('*[data-id="homeTabModalDialogModalTitle-react"]')
.assert.containsText('*[data-id="homeTabModalDialogModalTitle-react"]', 'Import from GitHub')
.waitForElementVisible('*[data-id="homeTabModalDialogModalBody-react"]')
.assert.containsText('*[data-id="homeTabModalDialogModalBody-react"]', 'Enter the github URL you would like to load.')
.waitForElementVisible('input[data-id="homeTabModalDialogCustomPromptText"]')
.waitForElementVisible('*[data-id="fileSystemModalDialogModalTitle-react"]')
.assert.containsText('*[data-id="fileSystemModalDialogModalTitle-react"]', 'Clone Git Repository')
.waitForElementVisible('*[data-id="fileSystemModalDialogModalBody-react"]')
.waitForElementVisible('input[data-id="modalDialogCustomPromptTextClone"]')
},
'Display Error Message For Invalid GitHub URL Modal #group1': function (browser: NightwatchBrowser) {
browser
.execute(() => {
(document.querySelector('input[data-id="homeTabModalDialogCustomPromptText"]') as any).focus()
(document.querySelector('input[data-id="modalDialogCustomPromptTextClone"]') as any).focus()
}, [], () => { })
.setValue('input[data-id="homeTabModalDialogCustomPromptText"]', testData.invalidURL)
.waitForElementVisible('*[data-id="homeTab-modal-footer-ok-react"]')
.click('[data-id="homeTab-modal-footer-ok-react"]') // submitted
.setValue('input[data-id="modalDialogCustomPromptTextClone"]', testData.invalidURL)
.waitForElementVisible('*[data-id="fileSystemModalDialogModalFooter-react"]')
.click('[data-id="fileSystem-modal-footer-ok-react"]') // submitted
//.waitForElementVisible('*[data-shared="tooltipPopup"]')
//.waitForElementContainsText('*[data-shared="tooltipPopup"] span', 'not found ' + testData.invalidURL)
},
'Import From GitHub For Valid URL #group2': function (browser: NightwatchBrowser) {
'Clone From GitHub with Valid URL #group2': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000)
.clickLaunchIcon('filePanel')
.click('div[data-id="verticalIconsHomeIcon"]')
.waitForElementVisible('button[data-id="landingPageImportFromGitHubButton"]').pause(1000)
.click('button[data-id="landingPageImportFromGitHubButton"]')
.waitForElementVisible('input[data-id="homeTabModalDialogCustomPromptText"]')
.waitForElementVisible('input[data-id="modalDialogCustomPromptTextClone"]')
.execute(() => {
(document.querySelector('input[data-id="homeTabModalDialogCustomPromptText"]') as any).focus()
(document.querySelector('input[data-id="modalDialogCustomPromptTextClone"]') as any).focus()
}, [], () => { })
.clearValue('input[data-id="homeTabModalDialogCustomPromptText"]').pause(1000)
.setValue('input[data-id="homeTabModalDialogCustomPromptText"]', testData.validURL)
.waitForElementVisible('*[data-id="homeTab-modal-footer-ok-react"]')
.click('[data-id="homeTab-modal-footer-ok-react"]')
.openFile('github/OpenZeppelin/openzeppelin-solidity/contracts/access/Roles.sol')
.clearValue('input[data-id="modalDialogCustomPromptTextClone"]').pause(1000)
.setValue('input[data-id="modalDialogCustomPromptTextClone"]', testData.validURL)
.waitForElementVisible('*[data-id="fileSystem-modal-footer-ok-react"]')
.click('[data-id="fileSystem-modal-footer-ok-react"]')
.openFile('Roles.sol')
.waitForElementVisible({
selector: `//*[@data-id='tab-active' and @data-path="default_workspace/github/OpenZeppelin/openzeppelin-solidity/contracts/access/Roles.sol"]`,
selector: `//*[@data-id='tab-active' and @data-path="git-hometab-test.git/Roles.sol"]`,
locateStrategy: 'xpath'
})
.getEditorValue((content) => {
browser.assert.ok(content.indexOf('library Roles {') !== -1, 'content does contain "library Roles {"')
})
},
'Import JSON From GitHub For Valid URL #group2': function (browser: NightwatchBrowser) {
'Confirm JSON After Cloning From GitHub For Valid URL #group2': function (browser: NightwatchBrowser) {
browser
.click('div[data-id="verticalIconsHomeIcon"]')
.click('button[data-id="landingPageImportFromGitHubButton"]')
.waitForElementVisible('input[data-id="homeTabModalDialogCustomPromptText"]').pause(1000)
.execute(() => {
(document.querySelector('input[data-id="homeTabModalDialogCustomPromptText"]') as any).focus()
}, [], () => { })
.clearValue('input[data-id="homeTabModalDialogCustomPromptText"]').pause(1000)
.setValue('input[data-id="homeTabModalDialogCustomPromptText"]', testData.JSON)
.waitForElementVisible('*[data-id="homeTab-modal-footer-ok-react"]')
.click('[data-id="homeTab-modal-footer-ok-react"]')
.openFile('github/ethereum/remix-project/package.json')
.waitForElementVisible("div[data-path='default_workspace/github/ethereum/remix-project/package.json'")
.openFile('package.json')
.waitForElementVisible("*[data-path='git-hometab-test.git/package.json'")
.getEditorValue((content) => {
browser.assert.ok(content.indexOf('"name": "remix-project",') !== -1, 'content does contain "name": "remix-project"')
})

@ -100,7 +100,7 @@ module.exports = {
.switchEnvironment('vm-london')
.click('*[data-id="terminalClearConsole"]') // clear the terminal
.clickLaunchIcon('filePanel')
.click('*[data-id="treeViewDivMenu"]') // make sure we create the file at the root folder
.click('*[data-id="treeViewUltreeViewMenu"]') // make sure we create the file at the root folder
.addFile('deployWithEthersJs.js', { content: deployWithEthersJs })
// .openFile('deployWithEthersJs.js')
.pause(1000)

@ -167,7 +167,14 @@ class AppComponent {
this.matomoConfAlreadySet = Registry.getInstance().get('config').api.exists('settings/matomo-analytics')
this.matomoCurrentSetting = Registry.getInstance().get('config').api.get('settings/matomo-analytics')
this.showMatamo = matomoDomains[window.location.hostname] && !this.matomoConfAlreadySet
let electronTracking = false
if (window.electronAPI) {
electronTracking = await window.electronAPI.canTrackMatomo()
}
this.showMatamo = (matomoDomains[window.location.hostname] || electronTracking) && !this.matomoConfAlreadySet
this.walkthroughService = new WalkthroughService(appManager)
@ -395,7 +402,7 @@ class AppComponent {
this.pinnedPanel = new PinnedPanel()
const pluginManagerComponent = new PluginManagerComponent(appManager, this.engine)
const filePanel = new FilePanel(appManager)
const filePanel = new FilePanel(appManager, contentImport)
this.statusBar = new StatusBar(filePanel, this.menuicons)
const landingPage = new LandingPage(appManager, this.menuicons, fileManager, filePanel, contentImport)
this.settings = new SettingsTab(Registry.getInstance().get('config').api, editor, appManager)
@ -521,7 +528,7 @@ class AppComponent {
)
await this.appManager.activatePlugin(['solidity-script'])
await this.appManager.activatePlugin(['solcoder'])
await this.appManager.activatePlugin(['filePanel'])
await this.appManager.activatePlugin(['filePanel'])
// Set workspace after initial activation
this.appManager.on('editor', 'editorMounted', () => {

@ -58,7 +58,7 @@ const profile = {
maintainedBy: 'Remix'
}
module.exports = class Filepanel extends ViewPlugin {
constructor(appManager) {
constructor(appManager, contentImport) {
super(profile)
this.registry = Registry.getInstance()
this.fileProviders = this.registry.get('fileproviders').api
@ -71,7 +71,6 @@ module.exports = class Filepanel extends ViewPlugin {
this.hardhatHandle = new HardhatHandle()
this.foundryHandle = new FoundryHandle()
this.truffleHandle = new TruffleHandle()
this.workspaces = []
this.appManager = appManager
this.currentWorkspaceMetadata = null

@ -98,7 +98,7 @@ export class PermissionHandlerPlugin extends Plugin {
<div className='d-flex flex-row'>
<span onClick={()=>{}}>To change the permission go to </span>
<span className='px-2' style={{ fontWeight: 'bolder' }}>Plugin Manager</span>
<img alt="" id="permissionModalImagesFrom" src="/assets/img/pluginManager.webp" style={{ height: '1rem', width: '1rem' }} />
<img alt="" id="permissionModalImagesFrom" src="assets/img/pluginManager.webp" style={{ height: '1rem', width: '1rem' }} />
<span className='pl-1' style={{ fontWeight: 'bolder' }}> / Permissions</span>
</div>
</div>

@ -1,5 +1,6 @@
import { Plugin } from '@remixproject/engine'
import * as templateWithContent from '@remix-project/remix-ws-templates'
import { TEMPLATE_METADATA } from '@remix-ui/workspace'
const profile = {
name: 'remix-templates',
@ -23,6 +24,22 @@ export class TemplatesPlugin extends Plugin {
}
// electron only method
async loadTemplateInNewWindow (template: string, opts?: any) {
const metadata = TEMPLATE_METADATA[template]
if (metadata) {
if (metadata.type === 'git') {
this.call('notification', 'alert', {
id: 'dgitAlert',
message: 'This template is not available in the desktop version',
})
return
} else if (metadata.type === 'plugin'){
this.call('notification', 'alert', {
id: 'dgitAlert',
message: 'This template is not available in the desktop version',
})
return
}
}
const files = await this.getTemplate(template, opts)
this.call('electronTemplates', 'loadTemplateInNewWindow', files)
}

@ -93,7 +93,7 @@ export class RemixGuidePlugin extends ViewPlugin {
<RemixUIGridView
plugin={this}
styleList={""}
logo='/assets/img/YouTubeLogo.webp'
logo='assets/img/YouTubeLogo.webp'
enableFilter={true}
showUntagged={true}
showPin={false}

@ -1,5 +1,5 @@
{
"logo": "/assets/img/YouTubeLogo.webp",
"logo": "assets/img/YouTubeLogo.webp",
"title": "Remix Guide",
"description": "Streamlined access to categorized video tutorials for mastering Remix IDE. From fundamentals to advanced techniques, level up your development skills with ease.",
"sections": [

@ -68,7 +68,7 @@
"filePanel.createNewFolder": "Create new folder",
"filePanel.publishToGist": "Publish to Gist",
"filePanel.workspace.publishToGist": "Publish workspace to GitHub gist",
"filePanel.uploadFile": "Upload files",
"filePanel.uploadFile": "Open a File from your File System",
"filePanel.uploadFolder": "Upload folder",
"filePanel.updateGist": "Update Gist",
"filePanel.workspace.updateGist": "Publish Gist update",

@ -66,6 +66,9 @@
"home.resources": "Resources",
"home.connectToLocalhost": "Connect to Localhost",
"home.seeAllTutorials": "See all tutorials",
"home.maintainedByRemix": "Maintained by Remix"
"home.maintainedByRemix": "Maintained by Remix",
"home.gitCloneTooltip": "Clone a Github repo to a new workspace",
"home.gistTooltip": "Open Gist repo",
"home.newFileTooltip": "Add a new file to a workspace"
}

@ -2,20 +2,17 @@ const domains = {
'remix-alpha.ethereum.org': 27,
'remix-beta.ethereum.org': 25,
'remix.ethereum.org': 23,
'6fd22d6fe5549ad4c4d8fd3ca0b7816b.mod': 35 // remix desktop
'localhost': 35 // remix desktop
}
const domainsSecondaryTracker = {
'remix-alpha.ethereum.org': 27,
'remix-beta.ethereum.org': 25,
'remix.ethereum.org': 23,
'6fd22d6fe5549ad4c4d8fd3ca0b7816b.mod': 35 // remix desktop
}
let domainToTrack = domains[window.location.hostname]
if (domains[window.location.hostname]) {
function trackDomain(domainToTrack) {
var _paq = window._paq = window._paq || []
console.log('Tracking domain', domainToTrack, _paq)
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq.push(["setExcludedQueryParams", ["code","gist"]]);
_paq.push(["setExcludedQueryParams", ["code", "gist"]]);
_paq.push(["setExcludedReferrers", ["etherscan.io"]]);
_paq.push(['enableJSErrorTracking']);
_paq.push(['trackPageView']);
@ -31,13 +28,37 @@ if (domains[window.location.hostname]) {
}
(function () {
var u = "https://ethereumfoundation.matomo.cloud/";
_paq.push(['setTrackerUrl', u + 'matomo.php']);
_paq.push(['setSiteId', domains[window.location.hostname]]);
_paq.push(['setTrackerUrl', u + 'matomo.php?debug=1']);
_paq.push(['setSiteId', domainToTrack]);
var d = document, g = d.createElement('script'), s = d.getElementsByTagName('script')[0];
g.async = true; g.src = '//cdn.matomo.cloud/ethereumfoundation.matomo.cloud/matomo.js'; s.parentNode.insertBefore(g,s);
})()
g.async = true; g.src = 'https://cdn.matomo.cloud/ethereumfoundation.matomo.cloud/matomo.js'; s.parentNode.insertBefore(g, s);
})();
}
if (window.electronAPI) {
window.electronAPI.canTrackMatomo().then((canTrack) => {
if (!canTrack) {
console.log('Matomo tracking is disabled on Dev mode')
return
}
window._paq = {
push: function (...data) {
if (!window.localStorage.getItem('config-v0.8:.remix.config') ||
(window.localStorage.getItem('config-v0.8:.remix.config') && !window.localStorage.getItem('config-v0.8:.remix.config').includes('settings/matomo-analytics'))) {
// require user tracking consent before processing data
} else {
if (JSON.parse(window.localStorage.getItem('config-v0.8:.remix.config'))['settings/matomo-analytics']) {
window.electronAPI.trackEvent(...data)
}
}
}
}
})
} else {
if (domainToTrack) {
trackDomain(domainToTrack)
}
}
function isElectron() {
// Renderer process
if (typeof window !== 'undefined' && typeof window.process === 'object' && window.process.type === 'renderer') {

@ -1,4 +1,4 @@
import { app, BrowserWindow, dialog, Menu, MenuItem, shell, utilityProcess, screen } from 'electron';
import { app, BrowserWindow, dialog, Menu, MenuItem, shell, utilityProcess, screen, ipcMain } from 'electron';
import path from 'path';
@ -36,11 +36,12 @@ const windowSet = new Set<BrowserWindow>([]);
export const createWindow = async (dir?: string): Promise<void> => {
// Create the browser window.
const mainWindow = new BrowserWindow({
height: (isE2E ? 1080 : screen.getPrimaryDisplay().size.height * 0.8),
width: (isE2E ? 1920 : screen.getPrimaryDisplay().size.width * 0.8),
height: (isE2E ? 1440 : screen.getPrimaryDisplay().size.height * 0.8),
width: (isE2E ? 2560 : screen.getPrimaryDisplay().size.width * 0.8),
frame: true,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
},
});
mainWindow.webContents.setWindowOpenHandler((details) => {
@ -76,7 +77,7 @@ export const createWindow = async (dir?: string): Promise<void> => {
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', async () => {
trackEvent('App', 'Launch', app.getVersion(), 1);
trackEvent('App', 'Launch', app.getVersion(), 1, 1);
trackEvent('App', 'OS', process.platform, 1);
require('./engine')
});
@ -145,4 +146,24 @@ if (!isE2E || isE2ELocal)
Menu.setApplicationMenu(Menu.buildFromTemplate(menu))
ipcMain.handle('config:isPackaged', async () => {
return isPackaged
})
ipcMain.handle('config:isE2E', async () => {
return isE2E
})
ipcMain.handle('config:canTrackMatomo', async (event, name: string) => {
console.log('config:canTrackMatomo', ((process.env.NODE_ENV === 'production' || isPackaged) && !isE2E))
return ((process.env.NODE_ENV === 'production' || isPackaged) && !isE2E)
})
ipcMain.handle('matomo:trackEvent', async (event, data) => {
if (data && data[0] && data[0] === 'trackEvent') {
trackEvent(data[1], data[2], data[3], data[4])
}
})

@ -15,12 +15,15 @@ ipcRenderer.invoke('getWebContentsID').then((id: number) => {
})
contextBridge.exposeInMainWorld('electronAPI', {
isPackaged: () => ipcRenderer.invoke('config:isPackaged'),
isE2E: () => ipcRenderer.invoke('config:isE2E'),
canTrackMatomo: () => ipcRenderer.invoke('config:canTrackMatomo'),
trackEvent: (args: any[]) => ipcRenderer.invoke('matomo:trackEvent', args),
activatePlugin: (name: string) => {
return ipcRenderer.invoke('manager:activatePlugin', name)
},
getWindowId: () => ipcRenderer.invoke('getWindowID'),
plugins: exposedPLugins.map(name => {
return {
name,

@ -1,21 +1,38 @@
import { isE2E } from "../main";
import { isPackaged } from "../main";
import { screen } from 'electron';
import { isPackaged, isE2E } from "../main";
var MatomoTracker = require('matomo-tracker');
// Function to send events to Matomo
export function trackEvent(category: string, action: string, name: string, value: string | number): void {
export function trackEvent(category: string, action: string, name: string, value: string | number, new_visit: number = 0): void {
var matomo = new MatomoTracker(35, 'http://ethereumfoundation.matomo.cloud/matomo.php');
matomo.on('error', function (err: any) {
console.log('error tracking request: ', err);
});
// Customize the user agent
const electronVersion = process.versions.electron;
const chromiumVersion = process.versions.chrome;
const os = process.platform; // 'darwin', 'win32', 'linux', etc.
const osVersion = process.getSystemVersion();
const ua = `Electron/${electronVersion} (Chromium/${chromiumVersion}) ${os} ${osVersion}`;
const res = `${screen.getPrimaryDisplay().size.width}x${screen.getPrimaryDisplay().size.height}`;
if ((process.env.NODE_ENV === 'production' || isPackaged) && !isE2E) {
console.log('trackEvent', category, action, name, value, ua, new_visit);
matomo.track({
e_c: category,
e_a: action,
e_n: name,
e_v: value,
ua,
new_visit,
res,
url: 'https://github.com/remix-project-org/remix-desktop'
// You can add other parameters if needed
}, (error: any) => {
@ -25,6 +42,8 @@ export function trackEvent(category: string, action: string, name: string, value
console.log('Event tracked successfully');
}
});
} else {
console.log('Matomo tracking is disabled');
}
}

@ -29,5 +29,5 @@ const tests = {
}
module.exports = {
...process.platform.startsWith('win')?{}:tests
...tests
}

@ -12,7 +12,7 @@ function HomeTabFeatured() {
const themeFilter = useContext(ThemeContext)
return (
<div className="pt-1 pl-2" id="hTFeaturedeSection">
<div className="pt-1 pl-2 h-100" id="hTFeaturedeSection">
<div className="mb-2 remix_ui-carousel-container">
<div className="w-100 d-flex flex-column rounded-3 remix_ui-carouselbox">
<ThemeContext.Provider value={themeFilter}>
@ -85,7 +85,7 @@ function HomeTabFeatured() {
</div>
<div className="mr-1 pr-1 d-flex align-items-center justify-content-center h-100">
<a href="https://www.youtube.com/@EthereumRemix/videos" target="__blank">
<img src={'/assets/img/YouTubeLogo.webp'} className="remixui_carouselImage" alt=""></img>
<img src={'assets/img/YouTubeLogo.webp'} className="remixui_carouselImage" alt=""></img>
</a>
<div className="h6 w-50 p-2 pl-4 align-self-center" style={{ flex: '1' }}>
<h5>
@ -109,7 +109,7 @@ function HomeTabFeatured() {
</div>
<div className="mr-1 pr-1 d-flex align-items-center justify-content-center h-100">
<a href="https://docs.google.com/forms/d/e/1FAIpQLSd0WsJnKbeJo-BGrnf7WijxAdmE4PnC_Z4M0IApbBfHLHZdsQ/viewform" target="__blank">
<img src={'/assets/img/remixRewardBetaTester_small.webp'} className="remixui_carouselImage" alt=""></img>
<img src={'assets/img/remixRewardBetaTester_small.webp'} className="remixui_carouselImage_remixbeta" alt=""></img>
</a>
<div className="h6 w-50 p-2 pl-4 align-self-center" style={{ flex: '1' }}>
<h5>

@ -1,31 +1,14 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import React, { useState, useRef, useReducer, useEffect } 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'
import { TEMPLATE_NAMES } from '@remix-ui/workspace'
interface HomeTabFileProps {
plugin: any
}
const loadingInitialState = {
tooltip: '',
showModalDialog: false,
importSource: '',
}
const loadingReducer = (state = loadingInitialState, action) => {
return {
...state,
tooltip: action.tooltip,
showModalDialog: false,
importSource: '',
}
}
function HomeTabFile({ plugin }: HomeTabFileProps) {
const [state, setState] = useState<{
searchInput: string
@ -48,10 +31,6 @@ function HomeTabFile({ plugin }: HomeTabFileProps) {
recentWorkspaces: [],
})
const [, dispatch] = useReducer(loadingReducer, loadingInitialState)
const inputValue = useRef(null)
useEffect(() => {
plugin.on('filePanel', 'setWorkspace', async () => {
let recents = JSON.parse(localStorage.getItem('recentWorkspaces'))
@ -59,8 +38,11 @@ function HomeTabFile({ plugin }: HomeTabFileProps) {
if (!recents) {
recents = []
} else {
const filtered = recents.filter((workspace) => {
return workspace !== null
})
setState((prevState) => {
return { ...prevState, recentWorkspaces: recents.slice(0, recents.length <= 3 ? recents.length : 3) }
return { ...prevState, recentWorkspaces: filtered.slice(0, filtered.length <= 3 ? filtered.length : 3) }
})
}
})
@ -91,41 +73,6 @@ function HomeTabFile({ plugin }: HomeTabFileProps) {
}
}, [plugin])
const processLoading = (type: string) => {
_paq.push(['trackEvent', 'hometab', 'filesSection', 'importFrom' + type])
const contentImport = plugin.contentImport
const workspace = plugin.fileManager.getProvider('workspace')
const startsWith = state.importSource.substring(0, 4)
if ((type === 'ipfs' || type === 'IPFS') && startsWith !== 'ipfs' && startsWith !== 'IPFS') {
setState((prevState) => {
return { ...prevState, importSource: startsWith + state.importSource }
})
}
contentImport.import(
state.modalInfo.prefix + state.importSource,
(loadingMsg) => dispatch({ tooltip: loadingMsg }),
async (error, content, cleanUrl, type, url) => {
if (error) {
toast(error.message || error)
} else {
try {
if (await workspace.exists(type + '/' + cleanUrl)) toast('File already exists in workspace')
else {
workspace.addExternal(type + '/' + cleanUrl, content, url)
plugin.call('menuicons', 'select', 'filePanel')
}
} catch (e) {
toast(e.message)
}
}
}
)
setState((prevState) => {
return { ...prevState, showModalDialog: false, importSource: '' }
})
}
const toast = (message: string) => {
setState((prevState) => {
return { ...prevState, toasterMsg: message }
@ -146,16 +93,16 @@ function HomeTabFile({ plugin }: HomeTabFileProps) {
await plugin.call('filePanel', 'switchToWorkspace', { name: wName, isLocalHost: false })
await plugin.call('filePanel', 'switchToWorkspace', { name: wName, isLocalHost: false }) // calling once is not working.
const content = `// SPDX-License-Identifier: MIT
pragma solidity >=0.6.12 <0.9.0;
contract HelloWorld {
/**
* @dev Prints Hello World string
*/
function print() public pure returns (string memory) {
return "Hello World!";
}
}
pragma solidity >=0.6.12 <0.9.0;
contract HelloWorld {
/**
* @dev Prints Hello World string
*/
function print() public pure returns (string memory) {
return "Hello World!";
}
}
`
if (createFile) {
const { newPath } = await plugin.call('fileManager', 'writeFileNoRewrite', '/contracts/HelloWorld.sol', content)
@ -180,77 +127,21 @@ contract HelloWorld {
plugin.verticalIcons.select('filePanel')
}
const showFullMessage = (title: string, loadItem: string, examples: Array<string>, prefix = '') => {
setState((prevState) => {
return {
...prevState,
showModalDialog: true,
modalInfo: {
title: title,
loadItem: loadItem,
examples: examples,
prefix,
},
}
})
}
const hideFullMessage = () => {
//eslint-disable-line
setState((prevState) => {
return { ...prevState, showModalDialog: false, importSource: '' }
})
}
const handleSwichToRecentWorkspace = async (e, workspaceName) => {
e.preventDefault()
_paq.push(['trackEvent', 'hometab', 'filesSection', 'loadRecentWorkspace'])
await plugin.call('filePanel', 'switchToWorkspace', { name: workspaceName, isLocalhost: false })
}
const examples = state.modalInfo.examples.map((urlEl, key) => (
<div key={key} className="p-1 user-select-auto">
<a>{urlEl}</a>
</div>
))
return (
<>
<ModalDialog id="homeTab" title={'Import from ' + state.modalInfo.title} okLabel="Import" hide={!state.showModalDialog} handleHide={() => hideFullMessage()} okFn={() => processLoading(state.modalInfo.title)}>
<div className="p-2 user-select-auto">
{state.modalInfo.loadItem !== '' && <span>Enter the {state.modalInfo.loadItem} you would like to load.</span>}
{state.modalInfo.examples.length !== 0 && (
<>
<div>e.g</div>
<div>{examples}</div>
</>
)}
<div className="d-flex flex-row">
{state.modalInfo.prefix && <span className="text-nowrap align-self-center mr-2">ipfs://</span>}
<input
ref={inputValue}
type="text"
name="prompt_text"
id="inputPrompt_text"
className="w-100 mt-1 form-control"
data-id="homeTabModalDialogCustomPromptText"
value={state.importSource}
onInput={(e) => {
setState((prevState) => {
return { ...prevState, importSource: inputValue.current.value }
})
}}
/>
</div>
</div>
</ModalDialog>
<Toaster message={state.toasterMsg} />
<div className="justify-content-start mt-1 p-2 d-flex flex-column" id="hTFileSection">
<div className="mb-3">
<div className="justify-content-start p-2 d-flex flex-column" id="hTFileSection">
<div className="mb-1">
{(state.recentWorkspaces[0] || state.recentWorkspaces[1] || state.recentWorkspaces[2]) && (
<div className="d-flex flex-column mb-5 remixui_recentworkspace">
<label style={{ fontSize: '0.8rem' }} className="mt-3">
Recent Workspaces
<label style={{ fontSize: '0.8rem' }} className="mt-1">
Recent Workspaces
</label>
{state.recentWorkspaces[0] && state.recentWorkspaces[0] !== '' && (
<a className="cursor-pointer mb-1 ml-2" href="#" onClick={(e) => handleSwichToRecentWorkspace(e, state.recentWorkspaces[0])}>
@ -270,58 +161,59 @@ contract HelloWorld {
</div>
)}
</div>
<div className="d-flex flex-column flex-nowrap pt-3">
<div className="d-flex flex-column flex-nowrap mt-4">
<label style={{ fontSize: '1.2rem' }}>
<FormattedMessage id="home.files" />
</label>
<div className="d-flex flex-column">
<div className="d-flex flex-row">
<CustomTooltip placement={'top'} tooltipId="overlay-tooltip" tooltipClasses="text-nowrap" tooltipText={<FormattedMessage id="home.startCodingPlayground" />} tooltipTextClasses="border bg-light text-dark p-1 pr-3">
<button className="btn btn-primary text-nowrap p-2 mr-2 border my-1" data-id="homeTabNewFile" style={{ width: 'fit-content' }} onClick={async () => await plugin.call('filePanel', 'createNewFile')}>
<FormattedMessage id="home.newFile" />
</button>
</CustomTooltip>
<CustomTooltip placement={'top'} tooltipId="overlay-tooltip" tooltipClasses="text-nowrap" tooltipText={<FormattedMessage id="home.openFileTooltip" />} tooltipTextClasses="border bg-light text-dark p-1 pr-3">
<span>
<label className="btn text-nowrap p-2 mr-2 border my-1" style={{ width: 'fit-content', cursor: 'pointer' }} htmlFor="openFileInput">
<FormattedMessage id="home.openFile" />
</label>
<input
title="open file"
type="file"
id="openFileInput"
onChange={(event) => {
event.stopPropagation()
plugin.verticalIcons.select('filePanel')
uploadFile(event.target)
}}
multiple
/>
</span>
</CustomTooltip>
<button className="btn text-nowrap p-2 mr-2 border my-1" onClick={() => showFullMessage('Ipfs', 'ipfs hash', ['ipfs://QmQQfBMkpDgmxKzYaoAtqfaybzfgGm9b2LWYyT56Chv6xH'], 'ipfs://')}>
IPFS
<div className="d-flex flex-row flex-wrap">
<CustomTooltip placement={'top'} tooltipId="overlay-tooltip" tooltipClasses="text-nowrap" tooltipText={<FormattedMessage id="home.newFileTooltip" />} tooltipTextClasses="border bg-light text-dark p-1 pr-3">
<button className="btn btn-primary text-nowrap p-2 mr-2 border my-1 mb-2" data-id="homeTabNewFile" style={{ width: 'fit-content' }} onClick={async () => {
_paq.push(['trackEvent', 'hometab', 'filesSection', 'newFile'])
await plugin.call('menuicons', 'select', 'filePanel')
await plugin.call('filePanel', 'createNewFile')
}}>
<i className="far fa-file pl-1 pr-2"></i>
<FormattedMessage id="home.newFile" />
</button>
<button className="btn text-nowrap p-2 mr-2 border my-1" data-id="landingPageImportFromGitHubButton" onClick={() => showFullMessage('GitHub', 'github URL', ['https://github.com/0xcert/ethereum-erc721/src/contracts/tokens/nf-token-metadata.sol', 'https://github.com/OpenZeppelin/openzeppelin-solidity/blob/67bca857eedf99bf44a4b6a0fc5b5ed553135316/contracts/access/Roles.sol'])}>
Git Clone
</button>
<button className="btn text-nowrap p-2 mr-2 border my-1" data-id="landingPageImportFromGistButton" onClick={() => importFromGist()}>
Gist
</CustomTooltip>
<CustomTooltip placement={'top'} tooltipId="overlay-tooltip" tooltipClasses="text-nowrap" tooltipText={<FormattedMessage id="home.openFileTooltip" />} tooltipTextClasses="border bg-light text-dark p-1 pr-3">
<span>
<label className="btn text-nowrap p-2 mr-2 border my-1 mb-2" style={{ width: 'fit-content', cursor: 'pointer' }} htmlFor="openFileInput">
<i className="far fa-upload pl-1 pr-2"></i>
<FormattedMessage id="home.openFile" />
</label>
<input
title="open file"
type="file"
id="openFileInput"
onChange={async (event) => {
event.stopPropagation()
await plugin.call('menuicons', 'select', 'filePanel')
uploadFile(event.target)
}}
multiple
/>
</span>
</CustomTooltip>
<CustomTooltip placement={'top'} tooltipId="overlay-tooltip" tooltipClasses="text-nowrap" tooltipText={<FormattedMessage id="home.gistTooltip" />} tooltipTextClasses="border bg-light text-dark p-1 pr-3"
>
<button className="btn text-nowrap p-2 mr-2 border my-1 mb-2" data-id="landingPageImportFromGistButton" onClick={() => importFromGist()}>
<i className="fab fa-github pl-1 pr-2"></i>
Gist
</button>
<button
className="btn text-nowrap p-2 mr-2 border my-1"
onClick={() =>
showFullMessage('Https', 'http/https raw content', ['https://raw.githubusercontent.com/OpenZeppelin/openzeppelin-contracts/master/contracts/token/ERC20/ERC20.sol'])
}
>
HTTPS
</CustomTooltip>
<CustomTooltip placement={'top'} tooltipId="overlay-tooltip" tooltipClasses="text-nowrap" tooltipText={<FormattedMessage id="home.gitCloneTooltip" />} tooltipTextClasses="border bg-light text-dark p-1 pr-3"
>
<button className="btn text-nowrap p-2 mr-2 border my-1 mb-2" data-id="landingPageImportFromGitHubButton" onClick={async () => {
_paq.push(['trackEvent', 'hometab', 'filesSection', 'Git Clone'])
await plugin.call('filePanel', 'clone')
}}>
<i className="fa-brands fa-github-alt pl-1 pr-2"></i>
Git Clone
</button>
</div>
</div>
<div className="d-flex mt-2 align-items-end w-100">
</CustomTooltip>
<CustomTooltip placement={'top'} tooltipId="overlay-tooltip" tooltipClasses="text-nowrap" tooltipText={<FormattedMessage id="home.connectToLocalhost" />} tooltipTextClasses="border bg-light text-dark p-1 pr-3">
<button className="btn btn-block text-nowrap p-2 border my-1" onClick={() => connectToLocalhost()}>
<button className="btn text-nowrap p-2 border my-1 mb-2" onClick={() => connectToLocalhost()}>
<i className="fa-regular fa-desktop pr-2"></i>
<FormattedMessage id="home.accessFileSystem" />
</button>

@ -30,24 +30,17 @@ const workspaceTemplates: WorkspaceTemplate[] = [
{
gsID: 'sUTLogo',
workspaceTitle: 'Start Coding',
description: 'Create a new project using this template.',
description: 'Start coding using the default template.',
projectLogo: 'assets/img/remixverticaltextLogo.png',
templateName: 'remixDefault',
},
{
gsID: 'sUTLogo',
workspaceTitle: 'Circom',
workspaceTitle: 'ZK Semaphore',
description: 'Create a new ZK Project with Circom using this template.',
projectLogo: 'assets/img/circom.webp',
templateName: 'semaphore',
},
{
gsID: 'sUTLogo',
workspaceTitle: 'Uniswap',
description: 'Create a new MultiSig wallet using this template.',
projectLogo: 'assets/img/gnosissafeLogo.png',
templateName: 'uniswapV4Template',
},
{
gsID: 'sUTLogo',
workspaceTitle: 'ERC20',
@ -55,6 +48,13 @@ const workspaceTemplates: WorkspaceTemplate[] = [
projectLogo: 'assets/img/oxprojectLogo.png',
templateName: 'ozerc20',
},
{
gsID: 'sUTLogo',
workspaceTitle: 'Uniswap V4 Hooks',
description: 'Create a new workspace based on this template.',
projectLogo: 'assets/img/gnosissafeLogo.png',
templateName: 'uniswapV4Template',
},
{
gsID: 'sUTLogo',
workspaceTitle: 'NFT / ERC721',
@ -149,12 +149,12 @@ function HomeTabGetStarted({ plugin }: HomeTabGetStartedProps) {
<div ref={carouselRefDiv} className="w-100 d-flex flex-column pt-1">
<ThemeContext.Provider value={themeFilter}>
<div className="pt-3">
<div className="d-flex flex-row align-items-center mb-3 flex-nowrap">
{workspaceTemplates.slice(0, 3).map((template, index) => (
<div className="d-flex flex-row align-items-center mb-3 flex-wrap">
{workspaceTemplates.map((template, index) => (
<CustomTooltip tooltipText={template.description} tooltipId={template.gsID} tooltipClasses="text-nowrap" tooltipTextClasses="border bg-light text-dark p-1 pr-3" placement="top-start" key={`${template.gsID}-${template.workspaceTitle}-${index}`}>
<button
key={index}
className={index === 0 ? 'btn btn-primary border p-2 text-nowrap mr-3' : index === workspaceTemplates.length - 1 ? 'btn border p-2 text-nowrap mr-2' : 'btn border p-2 text-nowrap mr-3'}
className={index === 0 ? 'btn btn-primary border p-2 text-nowrap mr-3 mb-3' : index === workspaceTemplates.length - 1 ? 'btn border p-2 text-nowrap mr-2 mb-3' : 'btn border p-2 text-nowrap mr-3 mb-3'}
onClick={(e) => {
createWorkspace(template.templateName)
}}
@ -165,22 +165,6 @@ function HomeTabGetStarted({ plugin }: HomeTabGetStartedProps) {
</CustomTooltip>
))}
</div>
<div className="d-flex flex-row align-items-center mb-2 flex-nowrap">
{workspaceTemplates.slice(3, workspaceTemplates.length).map((template, index) => (
<CustomTooltip tooltipText={template.description} tooltipId={template.gsID} tooltipClasses="text-nowrap" tooltipTextClasses="border bg-light text-dark p-1 pr-3" placement="bottom-start" key={`${template.gsID}-${template.workspaceTitle}-${index}`}>
<button
key={index}
className={'btn border p-2 text-nowrap mr-3'}
onClick={() => {
createWorkspace(template.templateName)
}}
data-id={`homeTabGetStarted${template.workspaceTitle}`}
>
{template.workspaceTitle}
</button>
</CustomTooltip>
))}
</div>
</div>
</ThemeContext.Provider>
</div>

@ -126,7 +126,7 @@ function HomeTabTitle() {
openLink(button.urlLink)
_paq.push(button.matomoTrackingEntry)
}}
className={`border-0 h-100 pl-1 pr-0 btn fab ${button.iconClass}`}
className={`border-0 h-100 px-1 btn fab ${button.iconClass}`}
></button>
</CustomTooltip>
))}

@ -125,6 +125,12 @@
width: 20rem;
}
.remixui_carouselImage_remixbeta {
flex: 1;
height: 20rem;
width: 18rem;
}
.remixui_carouselbox {
min-height: 25.12rem;
}

@ -73,10 +73,10 @@ export const RemixUiHomeTab = (props: RemixUiHomeTabProps) => {
// border-right
return (
<div className="d-flex flex-column w-100 h-100" data-id="remixUIHTAll">
<div className="d-flex flex-column w-100" data-id="remixUIHTAll">
<ThemeContext.Provider value={state.themeQuality}>
<div className="d-flex flex-row w-100 h-100 custom_home_bg">
<div className="px-2 pl-3 justify-content-start border-right d-flex flex-column" id="remixUIHTLeft" style={{ width: `${100 - carouselWidth}%` }}>
<div className="d-flex flex-row w-100 custom_home_bg">
<div className="px-2 pl-3 justify-content-start border-right d-flex flex-column" id="remixUIHTLeft" style={{ width: 'inherit' }}>
<HomeTabTitle />
<HomeTabGetStarted plugin={plugin}></HomeTabGetStarted>
{!(platform === appPlatformTypes.desktop) ?

@ -19,11 +19,11 @@ const PermissionHandlerDialog = (props: PermissionHandlerProps) => {
}
const imgFrom = () => {
if (!from.icon || from.icon === '') from.icon = "/assets/img/pluginManager.webp"
if (!from.icon || from.icon === '') from.icon = "assets/img/pluginManager.webp"
return <img className={`opacity(0.5);`} alt="" id="permissionModalImagesFrom" src={from.icon} />
}
const imgTo = () => {
if (!to.icon || to.icon === '') to.icon = "/assets/img/pluginManager.webp"
if (!to.icon || to.icon === '') to.icon = "assets/img/pluginManager.webp"
return <img className={`opacity(0.5);`} alt="" id="permissionModalImagesTo" src={to.icon} />
}
const pluginsImages = () => {

@ -1,10 +1,16 @@
remixui_statusbar:hover {
.remixui_statusbar_gitstatus
.remixui_statusbar_gitstatus:hover {
cursor: pointer;
}
.remixui_statusbar_gitstatus
.remixui_statusbar_gitstatus:hover {
.remixui_statusbar_scamAlert {}
.remixui_statusbar_scamAlert:hover {
cursor: pointer;
}
.remixui_statusbar_aistatus {}
.remixui_statusbar_aistatus:hover {
cursor: pointer;
}

@ -35,9 +35,9 @@ export default function AIStatus(props: AIStatusProps) {
<CustomTooltip
tooltipText={copilotActive ? "Remix Copilot activated" : "Remix Copilot disabled."}
>
<div className="d-flex flex-row pr-2 text-white justify-content-center align-items-center">
<span className={copilotActive === false ? "fa-regular fa-microchip-ai ml-1 text-danger" : "fa-regular fa-microchip-ai ml-1"}></span>
<span className={copilotActive === false ? "small mx-1 text-danger semi-bold" : "small mx-1 semi-bold" }>Remix Copilot</span>
<div className="d-flex flex-row pr-2 text-white justify-content-center align-items-center remixui_statusbar_aistatus">
<span className={copilotActive === false ? "fa-regular fa-microchip-ai ml-1 text-white" : "fa-regular fa-microchip-ai ml-1"}></span>
<span className={copilotActive === false ? "small mx-1 text-white semi-bold" : "small mx-1 semi-bold" }>Remix Copilot</span>
</div>
</CustomTooltip>
)

@ -15,7 +15,7 @@ export default function ScamAlertStatus ({ refs, getReferenceProps }: ScamAlertS
<CustomTooltip
tooltipText={"Scam Alerts"}
>
<div className="mr-2 d-flex align-items-center justify-content-center" id="hTScamAlertSection" ref={refs.setReference} {...getReferenceProps()}>
<div className="mr-2 d-flex align-items-center justify-content-center remixui_statusbar_scamAlert" id="hTScamAlertSection" ref={refs.setReference} {...getReferenceProps()}>
<span className="pr-2 far fa-exclamation-triangle text-white"></span>
<span className="text-white font-semibold small">
<FormattedMessage id="home.scamAlert" />

@ -10,6 +10,8 @@ import { fetchContractFromEtherscan, fetchContractFromBlockscout } from '@remix-
import JSZip from 'jszip'
import { Actions, FileTree } from '../types'
import IpfsHttpClient from 'ipfs-http-client'
import { AppModal } from '@remix-ui/app'
import { MessageWrapper } from '../components/file-explorer'
export * from './events'
export * from './workspace'

@ -1,5 +1,5 @@
import { CustomTooltip } from '@remix-ui/helper'
import React, {useState, useEffect, useContext} from 'react' //eslint-disable-line
import React, {useState, useEffect, useContext, useRef, useReducer} from 'react' //eslint-disable-line
import { FormattedMessage } from 'react-intl'
import { Placement } from 'react-bootstrap/esm/Overlay'
import { FileExplorerMenuProps } from '../types'
@ -39,6 +39,20 @@ export const FileExplorerMenu = (props: FileExplorerMenuProps) => {
icon: 'far fa-folder-upload',
placement: 'top',
platforms:[appPlatformTypes.web]
},
{
action: 'importFromIpfs',
title: 'Import files from ipfs',
icon: 'fa-regular fa-cube',
placement: 'top',
platforms: [appPlatformTypes.web, appPlatformTypes.desktop]
},
{
action: 'importFromHttps',
title: 'Import files with https',
icon: 'fa-solid fa-link',
placement: 'top',
platforms: [appPlatformTypes.web, appPlatformTypes.desktop]
}
].filter(
(item) =>
@ -49,6 +63,7 @@ export const FileExplorerMenu = (props: FileExplorerMenuProps) => {
),
actions: {}
})
const enableDirUpload = { directory: '', webkitdirectory: '' }
return (
@ -143,6 +158,12 @@ export const FileExplorerMenu = (props: FileExplorerMenuProps) => {
props.createNewFolder()
} else if (action === 'publishToGist' || action == 'updateGist') {
props.publishToGist()
} else if (action === 'importFromIpfs') {
_paq.push(['trackEvent', 'fileExplorer', 'fileAction', action])
props.importFromIpfs('Ipfs', 'ipfs hash', ['ipfs://QmQQfBMkpDgmxKzYaoAtqfaybzfgGm9b2LWYyT56Chv6xH'], 'ipfs://')
} else if (action === 'importFromHttps') {
_paq.push(['trackEvent', 'fileExplorer', 'fileAction', action])
props.importFromHttps('Https', 'http/https raw content', ['https://raw.githubusercontent.com/OpenZeppelin/openzeppelin-contracts/master/contracts/token/ERC20/ERC20.sol'])
} else {
state.actions[action]()
}

@ -383,6 +383,8 @@ export const FileExplorer = (props: FileExplorerProps) => {
publishToGist={publishToGist}
uploadFile={uploadFile}
uploadFolder={uploadFolder}
importFromIpfs={props.importFromIpfs}
importFromHttps={props.importFromHttps}
/>
</div>
</span>
@ -412,4 +414,10 @@ export const FileExplorer = (props: FileExplorerProps) => {
)
}
export const MessageWrapper = () => {
return (
<p>e.g ipfs://QmQQfBMkpDgmxKzYaoAtqfaybzfgGm9b2LWYyT56Chv6xH</p>
)
}
export default FileExplorer

@ -1,9 +1,10 @@
import React, {useState, useEffect, useRef, useContext, ChangeEvent} from 'react' // eslint-disable-line
import React, {useState, useEffect, useRef, useContext, ChangeEvent, useReducer} from 'react' // eslint-disable-line
import { FormattedMessage, useIntl } from 'react-intl'
import { Dropdown } from 'react-bootstrap'
import { CustomIconsToggle, CustomMenu, CustomToggle, CustomTooltip, extractNameFromKey, extractParentFromKey } from '@remix-ui/helper'
import { CopyToClipboard } from '@remix-ui/clipboard'
import {FileExplorer} from './components/file-explorer' // eslint-disable-line
import {ModalDialog, ValidationResult} from '@remix-ui/modal-dialog' // eslint-disable-line
import { FileSystemContext } from './contexts'
import './css/remix-ui-workspace.css'
import { ROOT_PATH, TEMPLATE_NAMES } from './utils/constants'
@ -105,6 +106,136 @@ export function Workspace() {
}
}, [canPaste])
const [modalState, setModalState] = useState<{
searchInput: string
showModalDialog: boolean
// modalValidation?: ValidationResult
modalInfo: {
title: string
loadItem: string
examples: Array<string>
prefix?: string
}
importSource: string
toasterMsg: string
}>({
searchInput: '',
showModalDialog: false,
// modalValidation: {} as ValidationResult,
modalInfo: { title: '', loadItem: '', examples: [], prefix: '' },
importSource: '',
toasterMsg: ''
})
const [validationResult, setValidationResult] = useState<ValidationResult>({ valid: true, message: '' })
const loadingInitialState = {
tooltip: '',
showModalDialog: false,
importSource: '',
}
const loadingReducer = (state = loadingInitialState, action) => {
return {
...state,
tooltip: action.tooltip,
showModalDialog: false,
importSource: '',
}
}
const inputValue = useRef(null)
const [, dispatch] = useReducer(loadingReducer, loadingInitialState)
const toast = (message: string) => {
setModalState((prevState) => {
return { ...prevState, toasterMsg: message }
})
}
const showFullMessage = async (title: string, loadItem: string, examples: Array<string>, prefix = '') => {
setModalState((prevState) => {
return {
...prevState,
showModalDialog: true,
modalInfo: {
title: title,
loadItem: loadItem,
examples: examples,
prefix,
},
}
})
}
const hideFullMessage = () => {
//eslint-disable-line
setModalState((prevState) => {
return { ...prevState, showModalDialog: false, importSource: '' }
})
}
const examples = modalState.modalInfo.examples.map((urlEl, key) => (
<div key={key} className="p-1 user-select-auto">
<a>{urlEl}</a>
</div>
))
const processLoading = (type: string) => {
_paq.push(['trackEvent', 'hometab', 'filesSection', 'importFrom' + type])
const contentImport = global.plugin.contentImport
const workspace = global.plugin.fileManager.getProvider('workspace')
const startsWith = modalState.importSource.substring(0, 4)
if ((type === 'ipfs' || type === 'IPFS') && startsWith !== 'ipfs' && startsWith !== 'IPFS') {
setModalState((prevState) => {
return { ...prevState, importSource: startsWith + modalState.importSource }
})
}
contentImport.import(
modalState.modalInfo.prefix + modalState.importSource,
(loadingMsg) => dispatch({ tooltip: loadingMsg }),
async (error, content, cleanUrl, type, url) => {
if (error) {
toast(error.message || error)
} else {
try {
if (await workspace.exists(type + '/' + cleanUrl)) toast('File already exists in workspace')
else {
workspace.addExternal(type + '/' + cleanUrl, content, url)
global.plugin.call('menuicons', 'select', 'filePanel')
}
} catch (e) {
toast(e.message)
}
}
}
)
setModalState((prevState) => {
return { ...prevState, showModalDialog: false, importSource: '' }
})
}
/**
* show modal for either ipfs or https icons in file explorer menu
* @returns void
*/
const importFromUrl = (title: string, loadItem: string, examples: Array<string>, prefix = '') => {
showFullMessage(title, loadItem, examples, prefix)
}
/**
* Validate the url fed into the modal for ipfs and https imports
* @returns {ValidationResult}
*/
const validateUrlForImport = (input: any) => {
if ((input.trim().startsWith('ipfs://') && input.length > 7) || input.trim().startsWith('https://') || input.trim() !== '') {
return { valid: true, message: '' }
} else {
global.plugin.call('notification', 'alert', { id: 'homeTabAlert', message: 'The provided value is invalid!' })
return { valid: false, message: 'The provided value is invalid!' }
}
}
useEffect(() => {
let workspaceName = localStorage.getItem('currentWorkspace')
if (!workspaceName && global.fs.browser.workspaces.length) {
@ -1086,7 +1217,8 @@ export function Workspace() {
<FileExplorer
fileState={global.fs.browser.fileState}
name={currentWorkspace}
menuItems={['createNewFile', 'createNewFolder', selectedWorkspace && selectedWorkspace.isGist ? 'updateGist' : 'publishToGist', canUpload ? 'uploadFile' : '', canUpload ? 'uploadFolder' : '']}
menuItems={['createNewFile', 'createNewFolder', selectedWorkspace && selectedWorkspace.isGist ? 'updateGist' : 'publishToGist', canUpload ? 'uploadFile' : '', canUpload ? 'uploadFolder' : '', 'importFromIpfs',
'importFromHttps']}
contextMenuItems={global.fs.browser.contextMenu.registeredMenuItems}
removedContextMenuItems={global.fs.browser.contextMenu.removedMenuItems}
files={global.fs.browser.files}
@ -1138,6 +1270,8 @@ export function Workspace() {
createNewFolder={handleNewFolderInput}
deletePath={deletePath}
renamePath={editModeOn}
importFromIpfs={importFromUrl}
importFromHttps={importFromUrl}
/>
)}
@ -1202,6 +1336,8 @@ export function Workspace() {
deletePath={deletePath}
renamePath={editModeOn}
dragStatus={dragStatus}
importFromIpfs={importFromUrl}
importFromHttps={importFromUrl}
/>
)}
</div>
@ -1334,6 +1470,38 @@ export function Workspace() {
downloadPath={downloadPath}
/>
)}
<ModalDialog id="homeTab" title={'Import from ' + modalState.modalInfo.title}
okLabel="Import" hide={!modalState.showModalDialog} handleHide={() => hideFullMessage()}
okFn={() => processLoading(modalState.modalInfo.title)} validationFn={validateUrlForImport}
>
<div className="p-2 user-select-auto">
{modalState.modalInfo.loadItem !== '' && <span>Enter the {modalState.modalInfo.loadItem} you would like to load.</span>}
{modalState.modalInfo.examples.length !== 0 && (
<>
<div>e.g</div>
<div>{examples}</div>
</>
)}
<div className="d-flex flex-row">
{modalState.modalInfo.prefix && <span className="text-nowrap align-self-center mr-2">ipfs://</span>}
<input
ref={inputValue}
type="text"
name="prompt_text"
id="inputPrompt_text"
className="w-100 mt-1 form-control"
data-id="homeTabModalDialogCustomPromptText"
value={modalState.importSource}
onInput={(e) => {
setModalState((prevState) => {
return { ...prevState, importSource: inputValue.current.value }
})
}}
/>
</div>
</div>
</ModalDialog>
</div>
)
}

@ -153,6 +153,8 @@ export interface FileExplorerProps {
createNewFolder:(parentFolder?: string) => Promise<void>
renamePath:(path: string, type: string, isNew?: boolean) => void
dragStatus: (status: boolean) => void
importFromIpfs: any
importFromHttps: any
}
export interface FileExplorerMenuProps {
@ -163,6 +165,8 @@ export interface FileExplorerMenuProps {
publishToGist: (path?: string) => void
uploadFile: (target: EventTarget & HTMLInputElement) => void
uploadFolder: (target: EventTarget & HTMLInputElement) => void
importFromIpfs: any
importFromHttps: any
tooltipPlacement?: Placement
}
export interface FileExplorerContextMenuProps {

Loading…
Cancel
Save