Merge branch 'master' into hhhh

pull/5370/head
David Disu 3 years ago committed by GitHub
commit a3731faba3
  1. 15
      apps/remix-ide-e2e/src/commands/currentSelectedFileIs.ts
  2. 211
      apps/remix-ide-e2e/src/tests/editor.test.ts
  3. 1
      apps/remix-ide-e2e/src/types/index.d.ts
  4. 2
      apps/remix-ide/src/app/editor/editor.js
  5. 2
      apps/remix-ide/src/app/files/fileManager.ts
  6. 22
      libs/remix-core-plugin/src/lib/editor-context-listener.ts
  7. 98
      libs/remix-ui/editor-context-view/src/lib/remix-ui-editor-context-view.tsx
  8. 3
      libs/remix-ui/editor/src/lib/remix-ui-editor.tsx
  9. 42
      libs/remix-ui/home-tab/src/lib/remix-ui-home-tab.tsx
  10. 4
      libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx
  11. 2
      libs/remix-ui/workspace/src/lib/actions/index.ts

@ -0,0 +1,15 @@
import { NightwatchBrowser } from 'nightwatch'
import EventEmitter from 'events'
class CurrentSelectedFileIs extends EventEmitter {
command (this: NightwatchBrowser, value: string): NightwatchBrowser {
this.api
.waitForElementContainsText('*[data-id="tabs-component"] *[data-id="tab-active"]', value)
.perform(() => {
this.emit('complete')
})
return this
}
}
module.exports = CurrentSelectedFileIs

@ -147,6 +147,7 @@ module.exports = {
.waitForElementContainsText('.contextview .type', 'uint256') .waitForElementContainsText('.contextview .type', 'uint256')
.waitForElementContainsText('.contextview .name', 'number') .waitForElementContainsText('.contextview .name', 'number')
.click('.contextview [data-action="previous"]') // declaration .click('.contextview [data-action="previous"]') // declaration
.pause(1000)
.execute(() => { .execute(() => {
return (document.getElementById('editorView') as any).getCursorPosition() return (document.getElementById('editorView') as any).getCursorPosition()
}, [], (result) => { }, [], (result) => {
@ -154,6 +155,7 @@ module.exports = {
browser.assert.equal(result.value, '180') browser.assert.equal(result.value, '180')
}) })
.click('.contextview [data-action="next"]') // back to the initial state .click('.contextview [data-action="next"]') // back to the initial state
.pause(1000)
.execute(() => { .execute(() => {
return (document.getElementById('editorView') as any).getCursorPosition() return (document.getElementById('editorView') as any).getCursorPosition()
}, [], (result) => { }, [], (result) => {
@ -161,6 +163,7 @@ module.exports = {
browser.assert.equal(result.value, '323') browser.assert.equal(result.value, '323')
}) })
.click('.contextview [data-action="next"]') // next reference .click('.contextview [data-action="next"]') // next reference
.pause(1000)
.execute(() => { .execute(() => {
return (document.getElementById('editorView') as any).getCursorPosition() return (document.getElementById('editorView') as any).getCursorPosition()
}, [], (result) => { }, [], (result) => {
@ -168,12 +171,74 @@ module.exports = {
browser.assert.equal(result.value, '489') browser.assert.equal(result.value, '489')
}) })
.click('.contextview [data-action="gotoref"]') // back to the declaration .click('.contextview [data-action="gotoref"]') // back to the declaration
.pause(1000)
.execute(() => { .execute(() => {
return (document.getElementById('editorView') as any).getCursorPosition() return (document.getElementById('editorView') as any).getCursorPosition()
}, [], (result) => { }, [], (result) => {
console.log('result', result) console.log('result', result)
browser.assert.equal(result.value, '180') browser.assert.equal(result.value, '180')
}) })
},
'Should display the context view, loop over "Owner" by switching file #group2': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('solidity')
.click('[for="autoCompile"]') // disable auto compile
.openFile('contracts')
.openFile('contracts/3_Ballot.sol')
.waitForElementVisible('#editorView')
.setEditorValue(BallotWithARefToOwner)
.clickLaunchIcon('solidity')
.click('*[data-id="compilerContainerCompileBtn"]') // compile
.pause(2000)
.execute(() => {
(document.getElementById('editorView') as any).gotoLine(14, 6)
}, [], () => {})
.waitForElementVisible('.contextview')
.waitForElementContainsText('.contextview .type', 'ContractDefinition')
.waitForElementContainsText('.contextview .name', 'Owner')
.click('.contextview [data-action="next"]')
.pause(1000)
.execute(() => {
return (document.getElementById('editorView') as any).getCursorPosition()
}, [], (result) => {
console.log('result', result)
browser.assert.equal(result.value, '1061')
})
.click('.contextview [data-action="next"]')
.pause(1000)
.execute(() => {
return (document.getElementById('editorView') as any).getCursorPosition()
}, [], (result) => {
console.log('result', result)
browser.assert.equal(result.value, '122')
})
.currentSelectedFileIs('2_Owner.sol') // make sure the current file has been properly changed
.click('.contextview [data-action="next"]')
.pause(1000)
.execute(() => {
return (document.getElementById('editorView') as any).getCursorPosition()
}, [], (result) => {
console.log('result', result)
browser.assert.equal(result.value, '211')
})
.click('.contextview [data-action="next"]')
.currentSelectedFileIs('3_Ballot.sol')
.pause(1000)
.execute(() => {
return (document.getElementById('editorView') as any).getCursorPosition()
}, [], (result) => {
console.log('result', result)
browser.assert.equal(result.value, '1061')
})
.click('.contextview [data-action="gotoref"]') // go to the declaration
.pause(1000)
.execute(() => {
return (document.getElementById('editorView') as any).getCursorPosition()
}, [], (result) => {
console.log('result', result)
browser.assert.equal(result.value, '122')
})
.end() .end()
} }
} }
@ -281,3 +346,149 @@ contract Storage {
return number; return number;
} }
}` }`
const BallotWithARefToOwner = `
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
import "./2_Owner.sol";
/**
* @title Ballot
* @dev Implements voting process along with vote delegation
*/
contract Ballot {
Owner c;
struct Voter {
uint weight; // weight is accumulated by delegation
bool voted; // if true, that person already voted
address delegate; // person delegated to
uint vote; // index of the voted proposal
}
struct Proposal {
// If you can limit the length to a certain number of bytes,
// always use one of bytes1 to bytes32 because they are much cheaper
bytes32 name; // short name (up to 32 bytes)
uint voteCount; // number of accumulated votes
}
address public chairperson;
mapping(address => Voter) public voters;
Proposal[] public proposals;
/**
* @dev Create a new ballot to choose one of 'proposalNames'.
* @param proposalNames names of proposals
*/
constructor(bytes32[] memory proposalNames) {
c = new Owner();
chairperson = msg.sender;
voters[chairperson].weight = 1;
for (uint i = 0; i < proposalNames.length; i++) {
// 'Proposal({...})' creates a temporary
// Proposal object and 'proposals.push(...)'
// appends it to the end of 'proposals'.
proposals.push(Proposal({
name: proposalNames[i],
voteCount: 0
}));
}
}
/**
* @dev Give 'voter' the right to vote on this ballot. May only be called by 'chairperson'.
* @param voter address of voter
*/
function giveRightToVote(address voter) public {
require(
msg.sender == chairperson,
"Only chairperson can give right to vote."
);
require(
!voters[voter].voted,
"The voter already voted."
);
require(voters[voter].weight == 0);
voters[voter].weight = 1;
}
/**
* @dev Delegate your vote to the voter 'to'.
* @param to address to which vote is delegated
*/
function delegate(address to) public {
Voter storage sender = voters[msg.sender];
require(!sender.voted, "You already voted.");
require(to != msg.sender, "Self-delegation is disallowed.");
while (voters[to].delegate != address(0)) {
to = voters[to].delegate;
// We found a loop in the delegation, not allowed.
require(to != msg.sender, "Found loop in delegation.");
}
sender.voted = true;
sender.delegate = to;
Voter storage delegate_ = voters[to];
if (delegate_.voted) {
// If the delegate already voted,
// directly add to the number of votes
proposals[delegate_.vote].voteCount += sender.weight;
} else {
// If the delegate did not vote yet,
// add to her weight.
delegate_.weight += sender.weight;
}
}
/**
* @dev Give your vote (including votes delegated to you) to proposal 'proposals[proposal].name'.
* @param proposal index of proposal in the proposals array
*/
function vote(uint proposal) public {
Voter storage sender = voters[msg.sender];
require(sender.weight != 0, "Has no right to vote");
require(!sender.voted, "Already voted.");
sender.voted = true;
sender.vote = proposal;
// If 'proposal' is out of the range of the array,
// this will throw automatically and revert all
// changes.
proposals[proposal].voteCount += sender.weight;
}
/**
* @dev Computes the winning proposal taking all previous votes into account.
* @return winningProposal_ index of winning proposal in the proposals array
*/
function winningProposal() public view
returns (uint winningProposal_)
{
uint winningVoteCount = 0;
for (uint p = 0; p < proposals.length; p++) {
if (proposals[p].voteCount > winningVoteCount) {
winningVoteCount = proposals[p].voteCount;
winningProposal_ = p;
}
}
}
/**
* @dev Calls winningProposal() function to get the index of the winner contained in the proposals array and then
* @return winnerName_ the name of the winner
*/
function winnerName() public view
returns (bytes32 winnerName_)
{
winnerName_ = proposals[winningProposal()].name;
}
}
`

@ -61,6 +61,7 @@ declare module 'nightwatch' {
acceptAndRemember (this: NightwatchBrowser, remember: boolean, accept: boolean): NightwatchBrowser acceptAndRemember (this: NightwatchBrowser, remember: boolean, accept: boolean): NightwatchBrowser
clearConsole (this: NightwatchBrowser): NightwatchBrowser clearConsole (this: NightwatchBrowser): NightwatchBrowser
clearTransactions (this: NightwatchBrowser): NightwatchBrowser clearTransactions (this: NightwatchBrowser): NightwatchBrowser
currentSelectedFileIs (name: string): NightwatchBrowser
} }
export interface NightwatchBrowser { export interface NightwatchBrowser {

@ -438,7 +438,7 @@ class Editor extends Plugin {
if (!filePath) return if (!filePath) return
filePath = await this.call('fileManager', 'getPathFromUrl', filePath) filePath = await this.call('fileManager', 'getPathFromUrl', filePath)
filePath = filePath.file filePath = filePath.file
if (!this.sessions[filePath]) throw new Error('file not found' + filePath) if (!this.sessions[filePath]) return
const path = filePath || this.currentFile const path = filePath || this.currentFile
const { from } = this.currentRequest const { from } = this.currentRequest

@ -470,7 +470,7 @@ class FileManager extends Plugin {
} }
currentFile () { currentFile () {
return this._deps.config.get('currentFile') return this.editor.current()
} }
async closeAllFiles () { async closeAllFiles () {

@ -84,11 +84,6 @@ export class EditorContextListener extends Plugin {
async _highlightItems (cursorPosition, compilationResult, file) { async _highlightItems (cursorPosition, compilationResult, file) {
if (this.currentPosition === cursorPosition) return if (this.currentPosition === cursorPosition) return
if (this.currentFile !== file) {
this.currentFile = file
this.currentPosition = cursorPosition
return
}
this._stopHighlighting() this._stopHighlighting()
this.currentPosition = cursorPosition this.currentPosition = cursorPosition
this.currentFile = file this.currentFile = file
@ -122,9 +117,13 @@ export class EditorContextListener extends Plugin {
async _highlight (node, compilationResult) { async _highlight (node, compilationResult) {
if (!node) return if (!node) return
const position = sourceMappingDecoder.decode(node.src) const position = sourceMappingDecoder.decode(node.src)
const fileTarget = compilationResult.getSourceName(position.file)
const nodeFound = this._activeHighlights.find((el) => el.fileTarget === fileTarget && el.position.file === position.file && el.position.length === position.length && el.position.start === position.start)
if (nodeFound) return // if the content is already highlighted, do nothing.
await this._highlightInternal(position, node, compilationResult) await this._highlightInternal(position, node, compilationResult)
if (compilationResult && compilationResult.languageversion.indexOf('soljson') === 0) { if (compilationResult && compilationResult.languageversion.indexOf('soljson') === 0) {
this._activeHighlights.push({ position, fileTarget: compilationResult.getSourceName(position.file), nodeId: node.id }) this._activeHighlights.push({ position, fileTarget, nodeId: node.id })
} }
} }
@ -204,13 +203,16 @@ export class EditorContextListener extends Plugin {
} }
_loadContractInfos (node) { _loadContractInfos (node) {
const path = (this.nodes.length && this.nodes[0].absolutePath) || this.results.source.target
for (const i in this.nodes) { for (const i in this.nodes) {
if (this.nodes[i].id === node.scope) { if (this.nodes[i].id === node.scope) {
const contract = this.nodes[i] const contract = this.nodes[i]
this.contract = this.results.data.contracts[this.results.source.target][contract.name] this.contract = this.results.data.contracts[path][contract.name]
this.estimationObj = this.contract.evm.gasEstimates if (contract) {
this.creationCost = this.estimationObj === null ? '-' : this.estimationObj.creation.totalCost this.estimationObj = this.contract.evm.gasEstimates
this.codeDepositCost = this.estimationObj === null ? '-' : this.estimationObj.creation.codeDepositCost this.creationCost = this.estimationObj === null ? '-' : this.estimationObj.creation.totalCost
this.codeDepositCost = this.estimationObj === null ? '-' : this.estimationObj.creation.codeDepositCost
}
} }
} }
} }

@ -8,15 +8,26 @@ import './remix-ui-editor-context-view.css'
export type astNode = { export type astNode = {
name: string, name: string,
id: number, id: number,
children: Array<any>, children?: Array<any>,
typeDescriptions: any, typeDescriptions: any,
nodeType: String, nodeType: String,
src: any, src: string // e.g "142:1361:0"
nodeId: any, }
position: any
export type nodePositionLight = {
file: number,
length: number,
start: number
}
export type astNodeLight = {
fileTarget: String,
nodeId: number,
position: nodePositionLight
} }
export type onContextListenerChangedListener = (nodes: Array<astNode>) => void export type onContextListenerChangedListener = (nodes: Array<astNode>) => void
export type ononCurrentFileChangedListener = (name: string) => void
export type gasEstimationType = { export type gasEstimationType = {
executionCost: string, executionCost: string,
@ -30,8 +41,9 @@ export interface RemixUiEditorContextViewProps {
offsetToLineColumn: (position: any, file: any, sources: any, asts: any) => any, offsetToLineColumn: (position: any, file: any, sources: any, asts: any) => any,
getCurrentFileName: () => String getCurrentFileName: () => String
onContextListenerChanged: (listener: onContextListenerChangedListener) => void onContextListenerChanged: (listener: onContextListenerChangedListener) => void
onCurrentFileChanged: (listener: ononCurrentFileChangedListener) => void
referencesOf: (nodes: astNode) => Array<astNode> referencesOf: (nodes: astNode) => Array<astNode>
getActiveHighlights: () => Array<astNode> getActiveHighlights: () => Array<astNodeLight>
gasEstimation: (node: astNode) => gasEstimationType gasEstimation: (node: astNode) => gasEstimationType
declarationOf: (node: astNode) => astNode declarationOf: (node: astNode) => astNode
} }
@ -48,49 +60,56 @@ function isDefinition (node: any) {
type nullableAstNode = astNode | null type nullableAstNode = astNode | null
export function RemixUiEditorContextView (props: RemixUiEditorContextViewProps) { export function RemixUiEditorContextView (props: RemixUiEditorContextViewProps) {
/* const loopOverReferences = useRef(0)
gotoLineDisableRef is used to temporarily disable the update of the view. const currentNodeDeclaration = useRef<nullableAstNode>(null)
e.g when the user ask the component to "gotoLine" we don't want to rerender the component (but just to put the mouse on the desired line)
*/
const gotoLineDisableRef = useRef(false)
const [state, setState] = useState<{ const [state, setState] = useState<{
nodes: Array<astNode>, nodes: Array<astNode>,
references: Array<astNode>,
activeHighlights: Array<any> activeHighlights: Array<any>
currentNode: nullableAstNode,
gasEstimation: gasEstimationType gasEstimation: gasEstimationType
}>({ }>({
nodes: [], nodes: [],
references: [],
activeHighlights: [], activeHighlights: [],
currentNode: null,
gasEstimation: { executionCost: '', codeDepositCost: '' } gasEstimation: { executionCost: '', codeDepositCost: '' }
}) })
useEffect(() => { useEffect(() => {
props.onCurrentFileChanged(() => {
currentNodeDeclaration.current = null
setState(prevState => {
return { ...prevState, nodes: [], activeHighlights: [] }
})
})
props.onContextListenerChanged(async (nodes: Array<astNode>) => { props.onContextListenerChanged(async (nodes: Array<astNode>) => {
if (gotoLineDisableRef.current) { let nextNodeDeclaration
gotoLineDisableRef.current = false let nextNode
return
}
let currentNode
if (!props.hide && nodes && nodes.length) { if (!props.hide && nodes && nodes.length) {
currentNode = nodes[nodes.length - 1] nextNode = nodes[nodes.length - 1]
if (!isDefinition(currentNode)) { if (!isDefinition(nextNode)) {
currentNode = await props.declarationOf(currentNode) nextNodeDeclaration = await props.declarationOf(nextNode)
} else {
nextNodeDeclaration = nextNode
} }
} }
let references if (nextNodeDeclaration && currentNodeDeclaration.current && nextNodeDeclaration.id === currentNodeDeclaration.current.id) return
currentNodeDeclaration.current = nextNodeDeclaration
let gasEstimation let gasEstimation
if (currentNode) { if (currentNodeDeclaration.current) {
references = await props.referencesOf(currentNode) if (currentNodeDeclaration.current.nodeType === 'FunctionDefinition') {
if (currentNode.nodeType === 'FunctionDefinition') { gasEstimation = await props.gasEstimation(currentNodeDeclaration.current)
gasEstimation = await props.gasEstimation(currentNode)
} }
} }
const activeHighlights = await props.getActiveHighlights() const activeHighlights: Array<astNodeLight> = await props.getActiveHighlights()
if (nextNode && activeHighlights && activeHighlights.length) {
loopOverReferences.current = activeHighlights.findIndex((el: astNodeLight) => `${el.position.start}:${el.position.length}:${el.position.file}` === nextNode.src)
loopOverReferences.current = loopOverReferences.current === -1 ? 0 : loopOverReferences.current
} else {
loopOverReferences.current = 0
}
setState(prevState => { setState(prevState => {
return { ...prevState, nodes, references, activeHighlights, currentNode, gasEstimation } return { ...prevState, nodes, activeHighlights, gasEstimation }
}) })
}) })
}, []) }, [])
@ -123,8 +142,7 @@ export function RemixUiEditorContextView (props: RemixUiEditorContextViewProps)
if (fileName !== await props.getCurrentFileName()) { if (fileName !== await props.getCurrentFileName()) {
await props.openFile(fileName) await props.openFile(fileName)
} }
if (lineColumn.start && lineColumn.start.line && lineColumn.start.column) { if (lineColumn.start && lineColumn.start.line >= 0 && lineColumn.start.column >= 0) {
gotoLineDisableRef.current = true
props.gotoLine(lineColumn.start.line, lineColumn.end.column + 1) props.gotoLine(lineColumn.start.line, lineColumn.end.column + 1)
} }
} }
@ -141,14 +159,14 @@ export function RemixUiEditorContextView (props: RemixUiEditorContextViewProps)
} }
} }
const _render = (node: nullableAstNode) => { const _render = () => {
const node = currentNodeDeclaration.current
if (!node) return (<div></div>) if (!node) return (<div></div>)
const references = state.references const references = state.activeHighlights
const type = node.typeDescriptions && node.typeDescriptions.typeString ? node.typeDescriptions.typeString : node.nodeType const type = node.typeDescriptions && node.typeDescriptions.typeString ? node.typeDescriptions.typeString : node.nodeType
const referencesCount = `${references ? references.length : '0'} reference(s)` const referencesCount = `${references ? references.length : '0'} reference(s)`
let ref = 0 const nodes: Array<astNodeLight> = state.activeHighlights
const nodes: Array<astNode> = state.activeHighlights
const jumpTo = () => { const jumpTo = () => {
if (node && node.src) { if (node && node.src) {
@ -161,10 +179,10 @@ export function RemixUiEditorContextView (props: RemixUiEditorContextViewProps)
// JUMP BETWEEN REFERENCES // JUMP BETWEEN REFERENCES
const jump = (e: any) => { const jump = (e: any) => {
e.target.dataset.action === 'next' ? ref++ : ref-- e.target.dataset.action === 'next' ? loopOverReferences.current++ : loopOverReferences.current--
if (ref < 0) ref = nodes.length - 1 if (loopOverReferences.current < 0) loopOverReferences.current = nodes.length - 1
if (ref >= nodes.length) ref = 0 if (loopOverReferences.current >= nodes.length) loopOverReferences.current = 0
_jumpToInternal(nodes[ref].position) _jumpToInternal(nodes[loopOverReferences.current].position)
} }
return ( return (
@ -181,7 +199,7 @@ export function RemixUiEditorContextView (props: RemixUiEditorContextViewProps)
return ( return (
!props.hide && <div className="container-context-view contextviewcontainer bg-light text-dark border-0 py-1"> !props.hide && <div className="container-context-view contextviewcontainer bg-light text-dark border-0 py-1">
{_render(state.currentNode)} {_render()}
</div> </div>
) )
} }

@ -398,11 +398,12 @@ export const EditorUI = (props: EditorUIProps) => {
<RemixUiEditorContextView <RemixUiEditorContextView
hide={false} hide={false}
gotoLine={(line, column) => props.plugin.call('editor', 'gotoLine', line, column)} gotoLine={(line, column) => props.plugin.call('editor', 'gotoLine', line, column)}
openFile={(file) => props.plugin.call('editor', 'openFile', file)} openFile={(file) => props.plugin.call('fileManager', 'switchFile', file)}
getLastCompilationResult={() => { return props.plugin.call('compilerArtefacts', 'getLastCompilationResult') } } getLastCompilationResult={() => { return props.plugin.call('compilerArtefacts', 'getLastCompilationResult') } }
offsetToLineColumn={(position, file, sources, asts) => { return props.plugin.call('offsetToLineColumnConverter', 'offsetToLineColumn', position, file, sources, asts) } } offsetToLineColumn={(position, file, sources, asts) => { return props.plugin.call('offsetToLineColumnConverter', 'offsetToLineColumn', position, file, sources, asts) } }
getCurrentFileName={() => { return props.plugin.call('fileManager', 'file') } } getCurrentFileName={() => { return props.plugin.call('fileManager', 'file') } }
onContextListenerChanged={(listener) => { props.plugin.on('contextualListener', 'contextChanged', listener) }} onContextListenerChanged={(listener) => { props.plugin.on('contextualListener', 'contextChanged', listener) }}
onCurrentFileChanged={(listener) => { props.plugin.on('fileManager', 'currentFileChanged', listener) }}
referencesOf={(node: astNode) => { return props.plugin.call('contextualListener', 'referencesOf', node) }} referencesOf={(node: astNode) => { return props.plugin.call('contextualListener', 'referencesOf', node) }}
getActiveHighlights={() => { return props.plugin.call('contextualListener', 'getActiveHighlights') }} getActiveHighlights={() => { return props.plugin.call('contextualListener', 'getActiveHighlights') }}
gasEstimation={(node: astNode) => { return props.plugin.call('contextualListener', 'gasEstimation', node) }} gasEstimation={(node: astNode) => { return props.plugin.call('contextualListener', 'gasEstimation', node) }}

@ -1,6 +1,7 @@
import React, { useState, useRef, useEffect, useReducer } from 'react' // eslint-disable-line import React, { useState, useRef, useEffect, useReducer } from 'react' // eslint-disable-line
import './remix-ui-home-tab.css' import './remix-ui-home-tab.css'
import JSZip from 'jszip'
import { ModalDialog } from '@remix-ui/modal-dialog' // eslint-disable-line import { ModalDialog } from '@remix-ui/modal-dialog' // eslint-disable-line
import { Toaster } from '@remix-ui/toaster' // eslint-disable-line import { Toaster } from '@remix-ui/toaster' // eslint-disable-line
import PluginButton from './components/pluginButton' // eslint-disable-line import PluginButton from './components/pluginButton' // eslint-disable-line
@ -175,6 +176,43 @@ export const RemixUiHomeTab = (props: RemixUiHomeTabProps) => {
const startPluginManager = async () => { const startPluginManager = async () => {
plugin.verticalIcons.select('pluginManager') plugin.verticalIcons.select('pluginManager')
} }
const saveAs = (blob, name) => {
const node = document.createElement('a')
node.download = name
node.rel = 'noopener'
node.href = URL.createObjectURL(blob)
setTimeout(function () { URL.revokeObjectURL(node.href) }, 4E4) // 40s
setTimeout(function () {
try {
node.dispatchEvent(new MouseEvent('click'))
} catch (e) {
var evt = document.createEvent('MouseEvents')
evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80,
20, false, false, false, false, 0, null)
node.dispatchEvent(evt)
}
}, 0) // 40s
}
const downloadFiles = async () => {
try {
plugin.call('notification', 'toast', 'preparing files for download, please wait..')
const zip = new JSZip()
const browserProvider = fileManager.getProvider('browser')
await browserProvider.copyFolderToJson('/', ({ path, content }) => {
zip.file(path, content)
})
zip.generateAsync({ type: 'blob' }).then(function (blob) {
var today = new Date()
var date = today.getFullYear() + '-' + (today.getMonth() + 1) + '-' + today.getDate()
var time = today.getHours() + 'h' + today.getMinutes() + 'min'
saveAs(blob, `remix-backup-at-${time}-${date}.zip`)
}).catch((e) => {
plugin.call('notification', 'toast', e.message)
})
} catch (e) {
plugin.call('notification', 'toast', e.message)
}
}
const showFullMessage = (title: string, loadItem: string, examples: Array<string>) => { const showFullMessage = (title: string, loadItem: string, examples: Array<string>) => {
setState(prevState => { setState(prevState => {
@ -279,6 +317,10 @@ export const RemixUiHomeTab = (props: RemixUiHomeTabProps) => {
<i className="mr-1 far fa-hdd"></i> <i className="mr-1 far fa-hdd"></i>
<label className="ml-1 remixui_home_text" onClick={() => connectToLocalhost()}>Connect to Localhost</label> <label className="ml-1 remixui_home_text" onClick={() => connectToLocalhost()}>Connect to Localhost</label>
</p> </p>
<p className="mb-1">
<i className="mr-1 far fa-download"></i>
<label className="ml-1 remixui_home_text" onClick={() => downloadFiles()}>Download Backup</label>
</p>
<p className="mt-3 mb-0"><label>LOAD FROM:</label></p> <p className="mt-3 mb-0"><label>LOAD FROM:</label></p>
<div className="btn-group"> <div className="btn-group">
<button className="btn mr-1 btn-secondary" data-id="landingPageImportFromGistButton" onClick={() => importFromGist()}>Gist</button> <button className="btn mr-1 btn-secondary" data-id="landingPageImportFromGistButton" onClick={() => importFromGist()}>Gist</button>

@ -36,7 +36,7 @@ export const TabsUI = (props: TabsUIProps) => {
const classNameImg = 'my-1 mr-1 text-dark ' + tab.iconClass const classNameImg = 'my-1 mr-1 text-dark ' + tab.iconClass
const classNameTab = 'nav-item nav-link d-flex justify-content-center align-items-center px-2 py-1 tab' + (index === currentIndexRef.current ? ' active' : '') const classNameTab = 'nav-item nav-link d-flex justify-content-center align-items-center px-2 py-1 tab' + (index === currentIndexRef.current ? ' active' : '')
return ( return (
<div ref={el => { tabsRef.current[index] = el }} className={classNameTab} title={tab.tooltip}> <div ref={el => { tabsRef.current[index] = el }} className={classNameTab} data-id={index === currentIndexRef.current ? 'tab-active' : ''} title={tab.tooltip}>
{tab.icon ? (<img className="my-1 mr-1 iconImage" src={tab.icon} />) : (<i className={classNameImg}></i>)} {tab.icon ? (<img className="my-1 mr-1 iconImage" src={tab.icon} />) : (<i className={classNameImg}></i>)}
<span className="title-tabs">{tab.title}</span> <span className="title-tabs">{tab.title}</span>
<span className="close-tabs" onClick={(event) => { props.onClose(index); event.stopPropagation() }}> <span className="close-tabs" onClick={(event) => { props.onClose(index); event.stopPropagation() }}>
@ -74,7 +74,7 @@ export const TabsUI = (props: TabsUIProps) => {
}, []) }, [])
return ( return (
<div className="remix-ui-tabs d-flex justify-content-between border-0 header nav-tabs"> <div className="remix-ui-tabs d-flex justify-content-between border-0 header nav-tabs" data-id="tabs-component">
<div className="d-flex flex-row" style={ { maxWidth: 'fit-content', width: '97%' } }> <div className="d-flex flex-row" style={ { maxWidth: 'fit-content', width: '97%' } }>
<div className="d-flex flex-row justify-content-center align-items-center m-1 mt-2"> <div className="d-flex flex-row justify-content-center align-items-center m-1 mt-2">
<span data-id="tabProxyZoomOut" className="btn btn-sm px-2 fas fa-search-minus text-dark" title="Zoom out" onClick={() => props.onZoomOut()}></span> <span data-id="tabProxyZoomOut" className="btn btn-sm px-2 fas fa-search-minus text-dark" title="Zoom out" onClick={() => props.onZoomOut()}></span>

@ -179,7 +179,7 @@ export const createNewFolder = async (path: string, rootDir: string) => {
const exists = await fileManager.exists(dirName) const exists = await fileManager.exists(dirName)
if (exists) { if (exists) {
return dispatch(displayNotification('Rename File Failed', `A file or 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) await fileManager.mkdir(dirName)
path = path.indexOf(rootDir + '/') === 0 ? path.replace(rootDir + '/', '') : path path = path.indexOf(rootDir + '/') === 0 ? path.replace(rootDir + '/', '') : path

Loading…
Cancel
Save