Merge branch 'master' into terminalwrapper

pull/4566/head
bunsenstraat 9 months ago committed by GitHub
commit 3d1bbb923f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 134
      apps/learneth/README.md
  2. 58
      apps/learneth/project.json
  3. 19
      apps/learneth/src/App.css
  4. 41
      apps/learneth/src/App.tsx
  5. 28
      apps/learneth/src/components/BackButton/index.scss
  6. 87
      apps/learneth/src/components/BackButton/index.tsx
  7. 17
      apps/learneth/src/components/LoadingScreen/index.css
  8. 16
      apps/learneth/src/components/LoadingScreen/index.tsx
  9. 4
      apps/learneth/src/components/RepoImporter/index.css
  10. 120
      apps/learneth/src/components/RepoImporter/index.tsx
  11. 21
      apps/learneth/src/components/SlideIn/index.css
  12. 18
      apps/learneth/src/components/SlideIn/index.tsx
  13. 13
      apps/learneth/src/index.css
  14. 18
      apps/learneth/src/index.html
  15. 13
      apps/learneth/src/main.tsx
  16. 24
      apps/learneth/src/pages/Home/index.css
  17. 96
      apps/learneth/src/pages/Home/index.tsx
  18. 20
      apps/learneth/src/pages/Logo/index.tsx
  19. 54
      apps/learneth/src/pages/StepDetail/index.scss
  20. 228
      apps/learneth/src/pages/StepDetail/index.tsx
  21. 144
      apps/learneth/src/pages/StepList/index.scss
  22. 41
      apps/learneth/src/pages/StepList/index.tsx
  23. 7
      apps/learneth/src/polyfills.ts
  24. 21
      apps/learneth/src/profile.json
  25. 5
      apps/learneth/src/redux/hooks.ts
  26. 14
      apps/learneth/src/redux/models/loading.ts
  27. 229
      apps/learneth/src/redux/models/remixide.ts
  28. 164
      apps/learneth/src/redux/models/workshop.ts
  29. 97
      apps/learneth/src/redux/store.ts
  30. 38
      apps/learneth/src/remix-client.ts
  31. 23
      apps/learneth/tsconfig.app.json
  32. 16
      apps/learneth/tsconfig.json
  33. 90
      apps/learneth/webpack.config.js
  34. 25
      apps/remix-ide-e2e/src/tests/gist.test.ts
  35. 7
      apps/remix-ide-e2e/src/tests/plugin_api.ts
  36. 2
      apps/remix-ide/project.json
  37. 15
      apps/remix-ide/src/app/panels/file-panel.js
  38. 6
      apps/remix-ide/src/app/tabs/locales/en/filePanel.json
  39. 2
      apps/remix-ide/src/remixAppManager.js
  40. 12
      libs/remix-core-plugin/src/lib/gist-handler.ts
  41. 2
      libs/remix-ui/drag-n-drop/src/lib/types/index.ts
  42. 2
      libs/remix-ui/file-decorators/src/lib/types/index.ts
  43. 29
      libs/remix-ui/workspace/src/lib/actions/index.ts
  44. 17
      libs/remix-ui/workspace/src/lib/actions/workspace.ts
  45. 8
      libs/remix-ui/workspace/src/lib/components/file-explorer-context-menu.tsx
  46. 26
      libs/remix-ui/workspace/src/lib/components/file-explorer-menu.tsx
  47. 6
      libs/remix-ui/workspace/src/lib/components/file-explorer.tsx
  48. 16
      libs/remix-ui/workspace/src/lib/components/workspace-hamburger.tsx
  49. 4
      libs/remix-ui/workspace/src/lib/contexts/index.ts
  50. 8
      libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx
  51. 8
      libs/remix-ui/workspace/src/lib/reducers/workspace.ts
  52. 44
      libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx
  53. 30
      libs/remix-ui/workspace/src/lib/types/index.ts
  54. 2
      libs/remix-ui/workspace/src/lib/utils/constants.ts
  55. 29
      libs/remix-ui/workspace/src/lib/utils/index.ts
  56. 8
      libs/remix-ui/workspace/src/lib/utils/types.ts
  57. 7
      package.json
  58. 213
      yarn.lock

@ -0,0 +1,134 @@
# Remix LearnEth Plugin
## Available Scripts
In the project directory, you can run:
### `npm run serve:plugin --plugin=learneth`
Runs the app in the development mode.\
Open [http://localhost:2024](http://localhost:2024) to view it in the browser.
The page will reload if you make edits.\
You will also see any lint errors in the console.
### `npm run build:plugin --plugin=learneth`
Builds the app for production to the `dist/apps/learneth` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
## Loading the plugin in remix
When testing with localhost you should use the HTTP version of either REMIX or REMIX ALPHA. Click on the plugin manager icon and
add the plugin 'Connect to a local plugin'. Your plugin will be at http://localhost:2024/.
## Setting up the REMIX IDE for working with the plugin
The plugin only works when a compiler environment is loaded as well, for example on the home screen of the IDE you select 'Solidity' or 'Vyper'. Without this the plugin
cannot compile and test files in the workshops.
## Setting up your Github workshops repo
You can create your own workshops that can be imported in the plugin.
When importing a github repo the plugin will look for a directory structure describing the workshops.
For example: https://github.com/ethereum/remix-workshops
### Root directories
Root directories are individual workshops, the name used will be the name of the workshop unless you override this with the name property in the config.yml.
### README.md
The readme in each directry contains an explanation of what the workshop is about. If an additional summary property is provided in the config.yml that will be used in the overview section of the plugin.
### config.yml
This config file contains meta data describing some properties of your workshop, for example
```
---
id: someid
name: my workshop name
summary: something about this workshop
level: 4
tags:
- solidity
- beginner
```
Level: a level of difficulty indicator ( 1 - 5 )
Tags: an array of tags
id: this is used by the system to let REMIX call startTutorial(repo,branch,id). See below for more instructions.
### Steps
Each workshop contains what we call steps.
Each step is a directory containing:
- a readme describing the step, what to do.
- sol files:
- these can be sol files and test sol files. The test files should be name yoursolname_test.sol
- ANSWER files: these are named yoursolname_answer.sol and can be used to show the solution or the correct answer. The plugin will load the
file in the IDE when a user clicks on 'Show Answer'
- js files
- vyper files
## Functions to call the plugin from the IDE
### Add a repository:
```
addRepository(repoName, branch)
```
### Start a tutorial
```
startTutorial(repoName,branch,id)
```
You don't need to add a seperate addRepository before calling startTutorial, this call will also add the repo.
_Parameters_
id: this can be two things:
- type of number, it specifies the n-th tutorial in the list
- type of string, this refers to the ID parameter in the config.yml file in the tutorial
for example:
```
---
id: basics
name: 1 Basics of Solidity
summary: Some basic functions explained
level: 4
tags:
- solidity
```
### How to call these functions in the REMIX IDE
```
(function () {
try {
// You don't need to add a seperate addRepository before calling startTutorial, this is just an example
remix.call('LearnEth', 'addRepository', "ethereum/remix-workshops", "master")
remix.call('LearnEth', 'startTutorial', "ethereum/remix-workshops", "master", "basics")
remix.call('LearnEth', 'startTutorial', "ethereum/remix-workshops", "master", 2)
} catch (e) {
console.log(e.message)
}
})()
```
Then call this in the REMIX console
```
remix.exeCurrent()
```

@ -0,0 +1,58 @@
{
"name": "learneth",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/learneth/src",
"projectType": "application",
"implicitDependencies": [],
"targets": {
"build": {
"executor": "@nrwl/webpack:webpack",
"outputs": ["{options.outputPath}"],
"defaultConfiguration": "development",
"options": {
"compiler": "babel",
"outputPath": "dist/apps/learneth",
"index": "apps/learneth/src/index.html",
"baseHref": "./",
"main": "apps/learneth/src/main.tsx",
"polyfills": "apps/learneth/src/polyfills.ts",
"tsConfig": "apps/learneth/tsconfig.app.json",
"assets": ["apps/learneth/src/profile.json"],
"styles": ["apps/learneth/src/index.css"],
"scripts": [],
"webpackConfig": "apps/learneth/webpack.config.js"
},
"configurations": {
"development": {
},
"production": {
"fileReplacements": [
{
"replace": "apps/learneth/src/environments/environment.ts",
"with": "apps/learneth/src/environments/environment.prod.ts"
}
]
}
}
},
"serve": {
"executor": "@nrwl/webpack:dev-server",
"defaultConfiguration": "development",
"options": {
"buildTarget": "learneth:build",
"hmr": true,
"baseHref": "/"
},
"configurations": {
"development": {
"buildTarget": "learneth:build:development",
"port": 2024
},
"production": {
"buildTarget": "learneth:build:production"
}
}
}
},
"tags": []
}

@ -0,0 +1,19 @@
/* You can add global styles to this file, and also import other style files */
h1{
font-size: 1.2rem !important;
font-weight: 700;
}
h2{
font-size: 1rem !important;
font-weight: 700;
}
h3{
font-size: 1rem !important;
}
p {
font-size: 0.9rem;
}

@ -0,0 +1,41 @@
import React from 'react'
import {createHashRouter, RouterProvider} from 'react-router-dom'
import {ToastContainer} from 'react-toastify'
import LoadingScreen from './components/LoadingScreen'
import LogoPage from './pages/Logo'
import HomePage from './pages/Home'
import StepListPage from './pages/StepList'
import StepDetailPage from './pages/StepDetail'
import 'react-toastify/dist/ReactToastify.css'
import './App.css'
export const router = createHashRouter([
{
path: '/',
element: <LogoPage />,
},
{
path: '/home',
element: <HomePage />,
},
{
path: '/list',
element: <StepListPage />,
},
{
path: '/detail',
element: <StepDetailPage />,
},
])
function App(): JSX.Element {
return (
<>
<RouterProvider router={router} />
<LoadingScreen />
<ToastContainer position="bottom-right" newestOnTop closeOnClick rtl={false} pauseOnFocusLoss draggable pauseOnHover autoClose={false} theme="colored" />
</>
)
}
export default App

@ -0,0 +1,28 @@
a {
.arrow {
display: inline-block;
opacity: 0;
transform: scale(0.5);
transition: all 0.3s;
}
span {
display: inline-block;
padding-left: 5px;
transform: translateX(-0.875em); // size of icon
transition: transform 0.3s;
}
}
.workshoptitle{
text-decoration: none;
}
a:hover {
fa-icon {
opacity: 1;
transform: scale(1);
}
span {
transform: translateX(0);
}
}

@ -0,0 +1,87 @@
import React, {useState} from 'react'
import {Link, useLocation, useNavigate} from 'react-router-dom'
import {Button, Modal, Tooltip, OverlayTrigger} from 'react-bootstrap'
import './index.scss'
function BackButton({entity}: any) {
const navigate = useNavigate()
const location = useLocation()
const [show, setShow] = useState(false)
const isDetailPage = location.pathname === '/detail'
const queryParams = new URLSearchParams(location.search)
const stepId = Number(queryParams.get('stepId'))
return (
<nav className="navbar navbar-light bg-light justify-content-between pt-1 pb-1 pl-1">
<ul className="nav mr-auto">
<li className="nav-item">
<div
className="btn back"
onClick={() => {
setShow(true)
}}
role="button"
>
<OverlayTrigger placement="right" overlay={<Tooltip id="tooltip-right">Leave tutorial</Tooltip>}>
<i className="fas fa-home pl-1" />
</OverlayTrigger>
</div>
</li>
{isDetailPage && (
<li className="nav-item">
<Link className="btn" to={`/list?id=${entity.id}`} title="Tutorial menu">
<i className="fas fa-bars" />
</Link>
</li>
)}
</ul>
{isDetailPage && (
<form className="form-inline">
{stepId > 0 && (
<Link to={`/detail?id=${entity.id}&stepId=${stepId - 1}`}>
<i className="fas fa-chevron-left pr-1" />
</Link>
)}
{stepId + 1}/{entity && <div className="">{entity.steps.length}</div>}
{stepId < entity.steps.length - 1 && (
<Link to={`/detail?id=${entity.id}&stepId=${stepId + 1}`}>
<i className="fas fa-chevron-right pl-1" />
</Link>
)}
</form>
)}
<Modal
show={show}
onHide={() => {
setShow(false)
}}
>
<Modal.Header placeholder={''} closeButton>
<Modal.Title>Leave tutorial</Modal.Title>
</Modal.Header>
<Modal.Body>Are you sure you want to leave the tutorial?</Modal.Body>
<Modal.Footer>
<Button
variant="secondary"
onClick={() => {
setShow(false)
}}
>
No
</Button>
<Button
variant="success"
onClick={() => {
setShow(false)
navigate('/home')
}}
>
Yes
</Button>
</Modal.Footer>
</Modal>
</nav>
)
}
export default BackButton

@ -0,0 +1,17 @@
.spinnersOverlay {
background-color: rgba(51, 51, 51, 0.8);
z-index: 99;
opacity: 1;
height: 100%;
left: 0;
position: fixed;
top: 0;
width: 100%;
}
.spinnersLoading {
left: 50%;
margin: 0;
position: absolute;
top: 50%;
transform: translate(-50%,-50%);
}

@ -0,0 +1,16 @@
import React from 'react'
import BounceLoader from 'react-spinners/BounceLoader'
import './index.css'
import {useAppSelector} from '../../redux/hooks'
const LoadingScreen: React.FC = () => {
const loading = useAppSelector((state) => state.loading.screen)
return loading ? (
<div className="spinnersOverlay">
<BounceLoader color="#a7b0ae" size={100} className="spinnersLoading" />
</div>
) : null
}
export default LoadingScreen

@ -0,0 +1,4 @@
.arrow-icon{
width: 3px;
display: inline-block;
}

@ -0,0 +1,120 @@
import React, {useState, useEffect} from 'react'
import {Button, Dropdown, Form, Tooltip, OverlayTrigger} from 'react-bootstrap'
import {useAppDispatch} from '../../redux/hooks'
import './index.css'
function RepoImporter({list, selectedRepo}: any): JSX.Element {
const [open, setOpen] = useState(false)
const [name, setName] = useState('')
const [branch, setBranch] = useState('')
const dispatch = useAppDispatch()
useEffect(() => {
setName(selectedRepo.name)
setBranch(selectedRepo.branch)
}, [selectedRepo])
const panelChange = () => {
setOpen(!open)
}
const selectRepo = (repo: {name: string; branch: string}) => {
dispatch({type: 'workshop/loadRepo', payload: repo})
}
const importRepo = (event: {preventDefault: () => void}) => {
event.preventDefault()
dispatch({type: 'workshop/loadRepo', payload: {name, branch}})
}
const resetAll = () => {
dispatch({type: 'workshop/resetAll'})
setName('')
setBranch('')
}
return (
<>
{selectedRepo.name && (
<div className="container-fluid mb-3 small mt-3">
Tutorials from:
<h4 className="mb-1">{selectedRepo.name}</h4>
<span className="">Date modified: {new Date(selectedRepo.datemodified).toLocaleString()}</span>
</div>
)}
<div onClick={panelChange} style={{cursor: 'pointer'}} className="container-fluid d-flex mb-3 small">
<div className="d-flex pr-2 pl-2">
<i className={`arrow-icon pt-1 fas fa-xs ${open ? 'fa-chevron-down' : 'fa-chevron-right'}`}></i>
</div>
<div className="d-flex">Import another tutorial repo</div>
</div>
{open && (
<div className="container-fluid">
<Dropdown className="w-100">
<Dropdown.Toggle className="btn btn-secondary w-100" id="dropdownBasic1">
Select a repo
</Dropdown.Toggle>
<Dropdown.Menu className="w-100">
{list.map((item: any) => (
<Dropdown.Item
key={`${item.name}/${item.branch}`}
onClick={() => {
selectRepo(item)
}}
>
{item.name}-{item.branch}
</Dropdown.Item>
))}
</Dropdown.Menu>
</Dropdown>
<div onClick={resetAll} className="small mb-3" style={{cursor: 'pointer'}}>
reset list
</div>
</div>
)}
<div className="container-fluid mt-3">
{open && (
<Form onSubmit={importRepo}>
<Form.Group className="form-group">
<Form.Label className="mr-2" htmlFor="name">
REPO
</Form.Label>
<OverlayTrigger placement="right" overlay={<Tooltip id="tooltip-right">ie username/repository</Tooltip>}>
<i className="fas fa-question-circle" />
</OverlayTrigger>
<Form.Control
id="name"
required
onChange={(e) => {
setName(e.target.value)
}}
value={name}
/>
<Form.Label htmlFor="branch">BRANCH</Form.Label>
<Form.Control
id="branch"
required
onChange={(e) => {
setBranch(e.target.value)
}}
value={branch}
/>
</Form.Group>
<Button className="btn btn-success start w-100" type="submit" disabled={!name || !branch}>
Import {name}
</Button>
<a href="https://github.com/bunsenstraat/remix-learneth-plugin/blob/master/README.md" className="d-none" target="_blank" rel="noreferrer">
<i className="fas fa-info-circle" /> how to setup your repo
</a>
</Form>
)}
<hr />
</div>
</>
)
}
export default RepoImporter

@ -0,0 +1,21 @@
.slide-enter {
transform: translateY(100px);
opacity: 0;
}
.slide-enter-active {
transform: translateY(0);
opacity: 1;
transition: opacity 400ms, transform 400ms cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.slide-exit {
transform: translateY(0);
opacity: 1;
}
.slide-exit-active {
transform: translateY(100px);
opacity: 0;
transition: opacity 400ms, transform 400ms cubic-bezier(0.6, 0.04, 0.98, 0.335);
}

@ -0,0 +1,18 @@
import React, {type ReactNode, useEffect, useState} from 'react'
import {CSSTransition} from 'react-transition-group'
import './index.css'
const SlideIn: React.FC<{children: ReactNode}> = ({children}) => {
const [show, setShow] = useState(false)
useEffect(() => {
setShow(true)
}, [])
return (
<CSSTransition in={show} timeout={400} classNames="slide" unmountOnExit>
{children}
</CSSTransition>
)
}
export default SlideIn

@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Learn ETH</title>
<base href="./" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css" integrity="sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf" crossorigin="anonymous"/> -->
<!-- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous"> -->
<link rel="stylesheet" integrity="ha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf"
crossorigin="anonymous" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css">
</head>
<body>
<div id="root"></div>
<script src="https://kit.fontawesome.com/41dd021e94.js" crossorigin="anonymous"></script>
</body>
</html>

@ -0,0 +1,13 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import {Provider} from 'react-redux'
import './index.css'
import App from './App'
import {store} from './redux/store'
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
root.render(
<Provider store={store}>
<App />
</Provider>
)

@ -0,0 +1,24 @@
.description-collapsed{
height: 0px;
overflow: hidden;
word-wrap: break-word;
padding: 0px !important;
margin: 0px !important;
}
.tag{
display: inline;
}
.arrow-icon{
width: 12px;
display: inline-block;
cursor: pointer;
}
.workshop-link {
cursor: pointer;
}
.workshop-link:hover {
text-decoration: underline;
}

@ -0,0 +1,96 @@
import React, {useEffect} from 'react'
import {Link} from 'react-router-dom'
import Markdown from 'react-markdown'
import rehypeRaw from 'rehype-raw'
import remarkGfm from 'remark-gfm'
import {useAppDispatch, useAppSelector} from '../../redux/hooks'
import RepoImporter from '../../components/RepoImporter'
import './index.css'
function HomePage(): JSX.Element {
const [openKeys, setOpenKeys] = React.useState<string[]>([])
const isOpen = (key: string) => openKeys.includes(key)
const handleClick = (key: string) => {
setOpenKeys(isOpen(key) ? openKeys.filter((item) => item !== key) : [...openKeys, key])
}
const dispatch = useAppDispatch()
const {list, detail, selectedId} = useAppSelector((state) => state.workshop)
const selectedRepo = detail[selectedId]
const levelMap: any = {
1: 'Beginner',
2: 'Intermediate',
3: 'Advanced',
}
useEffect(() => {
dispatch({
type: 'workshop/init',
})
}, [])
return (
<div className="App">
<RepoImporter list={list} selectedRepo={selectedRepo || {}} />
{selectedRepo && (
<div className="container-fluid">
{Object.keys(selectedRepo.group).map((level) => (
<div key={level}>
<div className="mb-2 border-bottom small">{levelMap[level]}:</div>
{selectedRepo.group[level].map((item: any) => (
<div key={item.id}>
<div>
<span
className="arrow-icon"
onClick={() => {
handleClick(item.id)
}}
>
<i className={`fas fa-xs ${isOpen(item.id) ? 'fa-chevron-down' : 'fa-chevron-right'}`} />
</span>
<span
className="workshop-link"
onClick={() => {
handleClick(item.id)
}}
>
{selectedRepo.entities[item.id].name}
</span>
<Link to={`/list?id=${item.id}`} className="text-decoration-none float-right">
<i className="fas fa-play-circle fa-lg" />
</Link>
</div>
<div className={`container-fluid bg-light pt-3 mt-2 ${isOpen(item.id) ? '' : 'description-collapsed'}`}>
{levelMap[level] && <p className="tag pt-2 pr-1 font-weight-bold small text-uppercase">{levelMap[level]}</p>}
{selectedRepo.entities[item.id].metadata.data.tags?.map((tag: string) => (
<p key={tag} className="tag pr-1 font-weight-bold small text-uppercase">
{tag}
</p>
))}
{selectedRepo.entities[item.id].steps && <div className="d-none">{selectedRepo.entities[item.id].steps.length} step(s)</div>}
<div className="workshop-list_description pb-3 pt-3">
<Markdown rehypePlugins={[rehypeRaw]} remarkPlugins={[remarkGfm]}>
{selectedRepo.entities[item.id].description?.content}
</Markdown>
</div>
<div className="actions"></div>
</div>
<div className="mb-3"></div>
</div>
))}
</div>
))}
</div>
)}
</div>
)
}
export default HomePage

@ -0,0 +1,20 @@
import React, {useEffect} from 'react'
import {useAppDispatch} from '../../redux/hooks'
const LogoPage: React.FC = () => {
const dispatch = useAppDispatch()
useEffect(() => {
dispatch({type: 'remixide/connect'})
}, [])
return (
<div>
<div>
<img className="w-100" src="https://remix.ethereum.org/assets/img/remixLogo.webp" />
</div>
</div>
)
}
export default LogoPage

@ -0,0 +1,54 @@
step-view {
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
overflow: hidden;
}
header, footer {
padding: 10px 5px;
display: flex;
justify-content: space-between;
align-items: center;
}
.errorloadingspacer{
padding-top: 44px;
}
.title{
pointer-events: none;
}
h1 {
text-align: left;
font-size: 1.2rem !important;
word-break: break-word;
}
markdown {
display: block;
flex: 1;
overflow: auto;
padding: 0px;
h1 {
font-size: 1.2rem !important;
}
h2 {
font-size: 1rem;
}
h3 {
font-size: 1rem;
}
h4 {
font-size: 1rem;
}
}

@ -0,0 +1,228 @@
import React, {useEffect} from 'react'
import {useLocation, useNavigate} from 'react-router-dom'
import Markdown from 'react-markdown'
import rehypeRaw from 'rehype-raw'
import BackButton from '../../components/BackButton'
import {useAppSelector, useAppDispatch} from '../../redux/hooks'
import './index.scss'
function StepDetailPage() {
const navigate = useNavigate()
const location = useLocation()
const dispatch = useAppDispatch()
const queryParams = new URLSearchParams(location.search)
const id = queryParams.get('id') as string
const stepId = Number(queryParams.get('stepId'))
const {
workshop: {detail, selectedId},
remixide: {errorLoadingFile, errors, success},
} = useAppSelector((state: any) => state)
const entity = detail[selectedId].entities[id]
const steps = entity.steps
const step = steps[stepId]
console.log(step)
useEffect(() => {
dispatch({
type: 'remixide/displayFile',
payload: step,
})
dispatch({
type: 'remixide/save',
payload: {errors: [], success: false},
})
window.scrollTo(0, 0)
}, [step])
useEffect(() => {
if (errors.length > 0 || success) {
window.scrollTo(0, document.documentElement.scrollHeight)
}
}, [errors, success])
return (
<>
<div className="fixed-top">
<div className="bg-light">
<BackButton entity={entity} />
</div>
</div>
<div id="top"></div>
{errorLoadingFile ? (
<>
<div className="errorloadingspacer"></div>
<h1 className="pl-3 pr-3 pt-3 pb-1">{step.name}</h1>
<button
className="w-100nav-item rounded-0 nav-link btn btn-success test"
onClick={() => {
dispatch({
type: 'remixide/displayFile',
payload: step,
})
}}
>
Load the file
</button>
<div className="mb-4"></div>
</>
) : (
<>
<div className="menuspacer"></div>
<h1 className="pr-3 pl-3 pt-3 pb-1">{step.name}</h1>
</>
)}
<div className="container-fluid">
<Markdown rehypePlugins={[rehypeRaw]}>{step.markdown?.content}</Markdown>
</div>
{step.test?.content ? (
<>
<nav className="nav nav-pills nav-fill">
{errorLoadingFile ? (
<button
className="nav-item rounded-0 nav-link btn btn-warning test"
onClick={() => {
dispatch({
type: 'remixide/displayFile',
payload: step,
})
}}
>
Load the file
</button>
) : (
<>
{!errorLoadingFile ? (
<>
<button
className="nav-item rounded-0 nav-link btn btn-info test"
onClick={() => {
dispatch({
type: 'remixide/testStep',
payload: step,
})
}}
>
Check Answer
</button>
{step.answer?.content && (
<button
className="nav-item rounded-0 nav-link btn btn-warning test"
onClick={() => {
dispatch({
type: 'remixide/showAnswer',
payload: step,
})
}}
>
Show answer
</button>
)}
</>
) : (
<>
{!errorLoadingFile && (
<>
<button
className="nav-item rounded-0 nav-link btn btn-success test"
onClick={() => {
navigate(stepId === steps.length - 1 ? `/list?id=${id}` : `/detail?id=${id}&stepId=${stepId + 1}`)
}}
>
Next
</button>
{step.answer?.content && (
<button
className="nav-item rounded-0 nav-link btn btn-warning test"
onClick={() => {
dispatch({
type: 'remixide/showAnswer',
payload: step,
})
}}
>
Show answer
</button>
)}
</>
)}
</>
)}
</>
)}
</nav>
{success && (
<button
className="w-100 rounded-0 nav-item nav-link btn btn-success"
onClick={() => {
navigate(stepId === steps.length - 1 ? `/list?id=${id}` : `/detail?id=${id}&stepId=${stepId + 1}`)
}}
>
Next
</button>
)}
<div id="errors">
{success && (
<div className="alert rounded-0 alert-success mb-0 mt-0" role="alert">
Well done! No errors.
</div>
)}
{errors.length > 0 && (
<>
{!success && (
<div className="alert rounded-0 alert-danger mb-0 mt-0" role="alert">
Errors
</div>
)}
{errors.map((error: string, index: number) => (
<div key={index} className="alert rounded-0 alert-warning mb-0 mt-0">
{error}
</div>
))}
</>
)}
</div>
</>
) : (
<>
<nav className="nav nav-pills nav-fill">
{!errorLoadingFile && step.answer?.content && (
<button
className="nav-item rounded-0 nav-link btn btn-warning test"
onClick={() => {
dispatch({
type: 'remixide/showAnswer',
payload: step,
})
}}
>
Show answer
</button>
)}
</nav>
{stepId < steps.length - 1 && (
<button
className="w-100 btn btn-success"
onClick={() => {
navigate(`/detail?id=${id}&stepId=${stepId + 1}`)
}}
>
Next
</button>
)}
{stepId === steps.length - 1 && (
<button
className="w-100 btn btn-success"
onClick={() => {
navigate(`/list?id=${id}`)
}}
>
Finish tutorial
</button>
)}
</>
)}
</>
)
}
export default StepDetailPage

@ -0,0 +1,144 @@
:host {
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
}
header {
padding: 10px 5px;
}
.menuspacer{
margin-top: 52px;
}
.steplink {
text-decoration: none;
}
.title{
pointer-events: none;
}
h1 {
text-align: left;
font-size: 1.2rem !important;
word-break: break-word;
}
section {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
.start {
padding: 5px 25px;
animation: jittery 2s 0.5s infinite;
box-shadow: 0px 0px 1px 0px rgba(0, 0, 0, 0.5);
color: white;
cursor: pointer;
}
}
footer {
padding: 10px;
display: flex;
justify-content: space-between;
align-items: center;
}
@keyframes jittery {
5%,
50% {
transform: scale(1);
box-shadow: 0px 0px 1px 0px rgba(0, 0, 0, 0.5);
}
10% {
transform: scale(0.9);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23);
}
15% {
transform: scale(1.15);
box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
}
20% {
transform: scale(1.15) rotate(-5deg);
box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
}
25% {
transform: scale(1.15) rotate(5deg);
box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
}
30% {
transform: scale(1.15) rotate(-3deg);
box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
}
35% {
transform: scale(1.15) rotate(2deg);
box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
}
40% {
transform: scale(1.15) rotate(0);
box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
}
}
.slide-in {
animation: slideIn 0.5s forwards;
visibility: hidden;
}
@keyframes slideIn {
0% {
transform: translateY(-100%);
visibility: visible;
}
100% {
transform: translateY(0);
visibility: visible;
}
}
@-moz-keyframes slideIn {
0% {
transform: translateY(-100%);
visibility: visible;
}
100% {
transform: translateY(0);
visibility: visible;
}
}
@-webkit-keyframes slideIn {
0% {
transform: translateY(-100%);
visibility: visible;
}
100% {
transform: translateY(0);
visibility: visible;
}
}
@-o-keyframes slideIn {
0% {
transform: translateY(-100%);
visibility: visible;
}
100% {
transform: translateY(0);
visibility: visible;
}
}
@-ms-keyframes slideIn {
0% {
transform: translateY(-100%);
visibility: visible;
}
100% {
transform: translateY(0);
visibility: visible;
}
}

@ -0,0 +1,41 @@
import React from 'react'
import {Link, useLocation} from 'react-router-dom'
import Markdown from 'react-markdown'
import BackButton from '../../components/BackButton'
import SlideIn from '../../components/SlideIn'
import {useAppSelector} from '../../redux/hooks'
import './index.scss'
function StepListPage(): JSX.Element {
const location = useLocation()
const queryParams = new URLSearchParams(location.search)
const id = queryParams.get('id') as string
const {detail, selectedId} = useAppSelector((state) => state.workshop)
const entity = detail[selectedId].entities[id]
return (
<>
<div className="fixed-top">
<div className="bg-light">
<BackButton />
</div>
</div>
<div id="top"></div>
<h1 className="pl-3 pr-3 pt-2 pb-1 menuspacer">{entity.name}</h1>
<div className="container-fluid">
<Markdown>{entity.text}</Markdown>
</div>
<SlideIn>
<article className="list-group m-3">
{entity.steps.map((step: any, i: number) => (
<Link key={i} to={`/detail?id=${id}&stepId=${i}`} className="rounded-0 btn btn-light border-bottom text-left steplink">
{step.name} »
</Link>
))}
</article>
</SlideIn>
</>
)
}
export default StepListPage

@ -0,0 +1,7 @@
/**
* Polyfill stable language features. These imports will be optimized by `@babel/preset-env`.
*
* See: https://github.com/zloirock/core-js#babel
*/
import 'core-js/stable'
import 'regenerator-runtime/runtime'

@ -0,0 +1,21 @@
{
"name": "LearnEth",
"displayName": "LearnEth",
"description": "Learn Ethereum with Remix!",
"documentation": "https://remix-learneth-plugin.readthedocs.io/en/latest/index.html",
"version": "0.1.0",
"methods": [
"startTutorial",
"addRepository"
],
"kind": "none",
"icon": "assets/img/learnEthLogo.webp",
"location": "sidePanel",
"url": "plugins/learneth/index.html",
"repo": "https://github.com/ethereum/remix-project/tree/master/apps/learneth",
"maintainedBy": "Remix",
"authorContact": "",
"targets": [
"remix"
]
}

@ -0,0 +1,5 @@
import {useDispatch, type TypedUseSelectorHook, useSelector} from 'react-redux'
import {type AppDispatch, type RootState} from './store'
export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector

@ -0,0 +1,14 @@
import {type ModelType} from '../store'
const Model: ModelType = {
namespace: 'loading',
state: {screen: true},
reducers: {
save(state, {payload}) {
return {...state, ...payload}
},
},
effects: {},
}
export default Model

@ -0,0 +1,229 @@
import {toast} from 'react-toastify'
import {type ModelType} from '../store'
import remixClient from '../../remix-client'
import {router} from '../../App'
function getFilePath(file: string): string {
const name = file.split('/')
return name.length > 1 ? `${name[name.length - 1]}` : ''
}
const Model: ModelType = {
namespace: 'remixide',
state: {
errors: [],
success: false,
errorLoadingFile: false,
// theme: '',
},
reducers: {
save(state, {payload}) {
return {...state, ...payload}
},
},
effects: {
*connect(_, {put}) {
toast.info('connecting to the REMIX IDE')
yield put({
type: 'loading/save',
payload: {
screen: true,
},
})
yield remixClient.onload()
toast.dismiss()
yield put({
type: 'loading/save',
payload: {
screen: false,
},
})
yield router.navigate('/home')
},
*displayFile({payload: step}, {select, put}) {
let content = ''
let path = ''
if (step.solidity?.file) {
content = step.solidity.content
path = getFilePath(step.solidity.file)
}
if (step.js?.file) {
content = step.js.content
path = getFilePath(step.js.file)
}
if (step.vy?.file) {
content = step.vy.content
path = getFilePath(step.vy.file)
}
if (!content) {
return
}
toast.info(`loading ${path} into IDE`)
yield put({
type: 'loading/save',
payload: {
screen: true,
},
})
const {detail, selectedId} = yield select((state) => state.workshop)
const workshop = detail[selectedId]
console.log('loading ', step, workshop)
path = `.learneth/${workshop.name}/${step.name}/${path}`
try {
const isExist = yield remixClient.call('fileManager', 'exists' as any, path)
if (!isExist) {
yield remixClient.call('fileManager', 'setFile', path, content)
}
yield remixClient.call('fileManager', 'switchFile', `${path}`)
yield put({
type: 'remixide/save',
payload: {errorLoadingFile: false},
})
toast.dismiss()
} catch (error) {
toast.dismiss()
toast.error('File could not be loaded. Please try again.')
yield put({
type: 'remixide/save',
payload: {errorLoadingFile: true},
})
}
yield put({
type: 'loading/save',
payload: {
screen: false,
},
})
},
*testStep({payload: step}, {select, put}) {
yield put({
type: 'loading/save',
payload: {
screen: true,
},
})
try {
yield put({
type: 'remixide/save',
payload: {success: false},
})
const {detail, selectedId} = yield select((state) => state.workshop)
const workshop = detail[selectedId]
let path: string
if (step.solidity.file) {
path = getFilePath(step.solidity.file)
path = `.learneth/${workshop.name}/${step.name}/${path}`
yield remixClient.call('fileManager', 'switchFile', `${path}`)
}
console.log('testing ', step.test.content)
path = getFilePath(step.test.file)
path = `.learneth/${workshop.name}/${step.name}/${path}`
yield remixClient.call('fileManager', 'setFile', path, step.test.content)
const result = yield remixClient.call('solidityUnitTesting', 'testFromPath', path)
console.log('result ', result)
if (!result) {
yield put({
type: 'remixide/save',
payload: {errors: ['Compiler failed to test this file']},
})
} else {
const success = result.totalFailing === 0
if (success) {
yield put({
type: 'remixide/save',
payload: {errors: [], success: true},
})
} else {
yield put({
type: 'remixide/save',
payload: {
errors: result.errors.map((error: {message: any}) => error.message),
},
})
}
}
} catch (err) {
console.log('TESTING ERROR', err)
yield put({
type: 'remixide/save',
payload: {errors: [String(err)]},
})
}
yield put({
type: 'loading/save',
payload: {
screen: false,
},
})
},
*showAnswer({payload: step}, {select, put}) {
yield put({
type: 'loading/save',
payload: {
screen: true,
},
})
toast.info('loading answer into IDE')
try {
console.log('loading ', step)
const content = step.answer.content
let path = getFilePath(step.answer.file)
const {detail, selectedId} = yield select((state) => state.workshop)
const workshop = detail[selectedId]
path = `.learneth/${workshop.name}/${step.name}/${path}`
yield remixClient.call('fileManager', 'setFile', path, content)
yield remixClient.call('fileManager', 'switchFile', `${path}`)
} catch (err) {
yield put({
type: 'remixide/save',
payload: {errors: [String(err)]},
})
}
toast.dismiss()
yield put({
type: 'loading/save',
payload: {
screen: false,
},
})
},
*testSolidityCompiler(_, {put, select}) {
try {
yield remixClient.call('solidity', 'getCompilationResult')
} catch (err) {
const errors = yield select((state) => state.remixide.errors)
yield put({
type: 'remixide/save',
payload: {
errors: [...errors, "The `Solidity Compiler` is not yet activated.<br>Please activate it using the `SOLIDITY` button in the `Featured Plugins` section of the homepage.<img class='img-thumbnail mt-3' src='assets/activatesolidity.png'>"],
},
})
}
},
},
}
export default Model

@ -0,0 +1,164 @@
import axios from 'axios'
import {toast} from 'react-toastify'
import groupBy from 'lodash/groupBy'
import pick from 'lodash/pick'
import {type ModelType} from '../store'
import remixClient from '../../remix-client'
import {router} from '../../App'
// const apiUrl = 'http://localhost:3001';
const apiUrl = 'https://static.220.14.12.49.clients.your-server.de:3000'
const Model: ModelType = {
namespace: 'workshop',
state: {
list: [],
detail: {},
selectedId: '',
},
reducers: {
save(state, {payload}) {
return {...state, ...payload}
},
},
effects: {
*init(_, {put}) {
const cache = localStorage.getItem('workshop.state')
if (cache) {
const workshopState = JSON.parse(cache)
yield put({
type: 'workshop/save',
payload: workshopState,
})
} else {
yield put({
type: 'workshop/loadRepo',
payload: {
name: 'ethereum/remix-workshops',
branch: 'master',
},
})
}
},
*loadRepo({payload}, {put, select}) {
toast.info(`loading ${payload.name}/${payload.branch}`)
yield put({
type: 'loading/save',
payload: {
screen: true,
},
})
const {list, detail} = yield select((state) => state.workshop)
const url = `${apiUrl}/clone/${encodeURIComponent(payload.name)}/${payload.branch}?${Math.random()}`
console.log('loading ', url)
const {data} = yield axios.get(url)
console.log(data)
const repoId = `${payload.name}-${payload.branch}`
for (let i = 0; i < data.ids.length; i++) {
const {
steps,
metadata: {
data: {steps: metadataSteps},
},
} = data.entities[data.ids[i]]
let newSteps = []
if (metadataSteps) {
newSteps = metadataSteps.map((step: any) => {
return {
...steps.find((item: any) => item.name === step.path),
name: step.name,
}
})
} else {
newSteps = steps.map((step: any) => ({
...step,
name: step.name.replace('_', ' '),
}))
}
const stepKeysWithFile = ['markdown', 'solidity', 'test', 'answer', 'js', 'vy']
for (let j = 0; j < newSteps.length; j++) {
const step = newSteps[j]
for (let k = 0; k < stepKeysWithFile.length; k++) {
const key = stepKeysWithFile[k]
if (step[key]) {
try {
step[key].content = (yield remixClient.call('contentImport', 'resolve', step[key].file)).content
} catch (error) {
console.error(error)
}
}
}
}
data.entities[data.ids[i]].steps = newSteps
}
const workshopState = {
detail: {
...detail,
[repoId]: {
...data,
group: groupBy(
data.ids.map((id: string) => pick(data.entities[id], ['level', 'id'])),
(item: any) => item.level
),
...payload,
},
},
list: detail[repoId] ? list : [...list, payload],
selectedId: repoId,
}
yield put({
type: 'workshop/save',
payload: workshopState,
})
localStorage.setItem('workshop.state', JSON.stringify(workshopState))
toast.dismiss()
yield put({
type: 'loading/save',
payload: {
screen: false,
},
})
if (payload.id) {
const {detail, selectedId} = workshopState
const {ids, entities} = detail[selectedId]
for (let i = 0; i < ids.length; i++) {
const entity = entities[ids[i]]
if (entity.metadata.data.id === payload.id || i + 1 === payload.id) {
yield router.navigate(`/list?id=${ids[i]}`)
break
}
}
}
},
*resetAll(_, {put}) {
yield put({
type: 'workshop/save',
payload: {
list: [],
detail: {},
selectedId: '',
},
})
localStorage.removeItem('workshop.state')
yield put({
type: 'workshop/init',
})
},
},
}
export default Model

@ -0,0 +1,97 @@
import {configureStore, createSlice, type PayloadAction, type Reducer} from '@reduxjs/toolkit'
import createSagaMiddleware from 'redux-saga'
import {call, put, takeEvery, delay, select, all, fork, type ForkEffect} from 'redux-saga/effects'
// @ts-expect-error
const context = require.context('./models', false, /\.ts$/)
const models = context.keys().map((key: any) => context(key).default)
export type StateType = Record<string, any>
export interface ModelType {
namespace: string
state: StateType
reducers: Record<string, (state: StateType, action: PayloadAction<any>) => StateType>
effects: Record<
string,
(
action: PayloadAction<any>,
effects: {
call: typeof call
put: typeof put
delay: typeof delay
select: typeof select
}
) => Generator<any, void, any>
>
}
function createReducer(model: ModelType): Reducer {
const reducers = model.reducers
Object.keys(model.effects).forEach((key) => {
reducers[key] = (state: StateType, action: PayloadAction<any>) => state
})
const slice = createSlice({
name: model.namespace,
initialState: model.state,
reducers,
})
return slice.reducer
}
const rootReducer = models.reduce((prev: any, model: ModelType) => {
return {...prev, [model.namespace]: createReducer(model)}
}, {})
function watchEffects(model: ModelType): ForkEffect {
return fork(function* () {
for (const key in model.effects) {
const effect = model.effects[key]
yield takeEvery(`${model.namespace}/${key}`, function* (action: PayloadAction) {
yield put({
type: 'loading/save',
payload: {
[`${model.namespace}/${key}`]: true,
},
})
yield effect(action, {
call,
put,
delay,
select,
})
yield put({
type: 'loading/save',
payload: {
[`${model.namespace}/${key}`]: false,
},
})
})
}
})
}
function* rootSaga(): Generator {
yield all(models.map((model: ModelType) => watchEffects(model)))
}
const configureAppStore = (initialState = {}) => {
const reduxSagaMonitorOptions = {}
const sagaMiddleware = createSagaMiddleware(reduxSagaMonitorOptions)
const middleware = [sagaMiddleware]
const store = configureStore({
reducer: rootReducer,
middleware: (gDM) => gDM().concat([...middleware]),
preloadedState: initialState,
devTools: process.env.NODE_ENV !== 'production',
})
sagaMiddleware.run(rootSaga)
return store
}
export const store = configureAppStore()
export type AppDispatch = typeof store.dispatch
export type RootState = ReturnType<typeof store.getState>

@ -0,0 +1,38 @@
import {PluginClient} from '@remixproject/plugin'
import {createClient} from '@remixproject/plugin-webview'
import {store} from './redux/store'
import {router} from './App'
class RemixClient extends PluginClient {
constructor() {
super()
createClient(this)
}
startTutorial(name: any, branch: any, id: any): void {
console.log('start tutorial', name, branch, id)
void router.navigate('/home')
store.dispatch({
type: 'workshop/loadRepo',
payload: {
name,
branch,
id,
},
})
}
addRepository(name: any, branch: any) {
console.log('add repo', name, branch)
void router.navigate('/home')
store.dispatch({
type: 'workshop/loadRepo',
payload: {
name,
branch,
},
})
}
}
export default new RemixClient()

@ -0,0 +1,23 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": ["node"]
},
"files": [
"../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
"../../node_modules/@nrwl/react/typings/image.d.ts"
],
"exclude": [
"jest.config.ts",
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"**/*.spec.js",
"**/*.test.js",
"**/*.spec.jsx",
"**/*.test.jsx"
],
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
}

@ -0,0 +1,16 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"jsx": "react-jsx",
"allowJs": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.app.json"
}
]
}

@ -0,0 +1,90 @@
const {composePlugins, withNx} = require('@nrwl/webpack')
const webpack = require('webpack')
const TerserPlugin = require('terser-webpack-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
// Nx plugins for webpack.
module.exports = composePlugins(withNx(), (config) => {
// Update the webpack config as needed here.
// e.g. `config.plugins.push(new MyPlugin())`
// add fallback for node modules
config.resolve.fallback = {
...config.resolve.fallback,
crypto: require.resolve('crypto-browserify'),
stream: require.resolve('stream-browserify'),
path: require.resolve('path-browserify'),
http: require.resolve('stream-http'),
https: require.resolve('https-browserify'),
constants: require.resolve('constants-browserify'),
os: false, //require.resolve("os-browserify/browser"),
timers: false, // require.resolve("timers-browserify"),
zlib: require.resolve('browserify-zlib'),
fs: false,
module: false,
tls: false,
net: false,
readline: false,
child_process: false,
buffer: require.resolve('buffer/'),
vm: require.resolve('vm-browserify'),
}
// add externals
config.externals = {
...config.externals,
solc: 'solc',
}
// add public path
config.output.publicPath = './'
// add copy & provide plugin
config.plugins.push(
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
url: ['url', 'URL'],
process: 'process/browser',
})
)
// set the define plugin to load the WALLET_CONNECT_PROJECT_ID
config.plugins.push(
new webpack.DefinePlugin({
WALLET_CONNECT_PROJECT_ID: JSON.stringify(process.env.WALLET_CONNECT_PROJECT_ID),
})
)
// souce-map loader
config.module.rules.push({
test: /\.js$/,
use: ['source-map-loader'],
enforce: 'pre',
})
config.ignoreWarnings = [/Failed to parse source map/] // ignore source-map-loader warnings
// set minimizer
config.optimization.minimizer = [
new TerserPlugin({
parallel: true,
terserOptions: {
ecma: 2015,
compress: false,
mangle: false,
format: {
comments: false,
},
},
extractComments: false,
}),
new CssMinimizerPlugin(),
]
config.watchOptions = {
ignored: /node_modules/,
}
config.experiments.syncWebAssembly = true
return config
})

@ -37,9 +37,9 @@ module.exports = {
.addFile('File.sol', { content: '' })
.executeScriptInTerminal(`remix.loadgist('${gistid}')`)
// .perform((done) => { if (runtimeBrowser === 'chrome') { browser.openFile('gists') } done() })
.waitForElementVisible(`[data-id="treeViewLitreeViewItemgist-${gistid}"]`)
.waitForElementVisible(`[data-id="treeViewLitreeViewItemREADME.txt"]`)
.openFile(`gist-${gistid}/README.txt`)
.openFile(`README.txt`)
// Remix publish to gist
/* .click('*[data-id="fileExplorerNewFilepublishToGist"]')
.pause(2000)
@ -110,8 +110,8 @@ module.exports = {
.waitForElementVisible('[data-id="settingsTabRemoveGistToken"]')
.click('[data-id="settingsTabRemoveGistToken"]')
.clickLaunchIcon('filePanel')
.waitForElementVisible('*[data-id="fileExplorerNewFilepublishToGist"]')
.click('*[data-id="fileExplorerNewFilepublishToGist"]')
.click('*[data-id="workspacesMenuDropdown"]')
.click('*[data-id="workspacepublishToGist"]')
.waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.execute(function () { (document.querySelector('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') as HTMLElement).click() })
.pause(10000)
@ -142,9 +142,9 @@ module.exports = {
.setValue('*[data-id="gisthandlerModalDialogModalBody-react"] input[data-id="modalDialogCustomPromp"]', testData.validGistId)
.modalFooterOKClick('gisthandler')
.pause(10000)
.openFile(`gist-${testData.validGistId}/README.txt`)
.waitForElementVisible(`div[data-path='default_workspace/gist-${testData.validGistId}/README.txt']`)
.assert.containsText(`div[data-path='default_workspace/gist-${testData.validGistId}/README.txt'] > span`, 'README.txt')
.openFile(`README.txt`)
.waitForElementVisible(`div[data-path='gist ${testData.validGistId}/README.txt']`)
.assert.containsText(`div[data-path='gist ${testData.validGistId}/README.txt'] > span`, 'README.txt')
},
'Load Gist from URL and verify truncated files are loaded #group3': function (browser: NightwatchBrowser) {
@ -152,14 +152,15 @@ module.exports = {
browser
.url('http://127.0.0.1:8080/#gist=' + gistId) // loading the gist
.refreshPage()
.currentWorkspaceIs('gist ' + gistId)
.waitForElementVisible('*[data-id="remixIdeIconPanel"]', 15000)
.waitForElementVisible(`#fileExplorerView li[data-path='gist-${gistId}/README.txt']`, 30000)
.openFile(`gist-${gistId}/scripts/deploy_with_ethers.ts`)
.waitForElementVisible(`#fileExplorerView li[data-path='contracts']`, 30000)
.openFile(`contracts/2_Owner.sol`)
.getEditorValue((content) => {
browser.assert.ok(content !== '')
browser.assert.ok(content.indexOf('contract Owner {') !== -1)
})
.rightClickCustom(`li[data-path='gist-${gistId}'] div`) // saving the gist
.click('[data-id="contextMenuItempublishFolderToGist"]')
.click('*[data-id="workspacesMenuDropdown"]')
.click('*[data-id="workspacepublishToGist"]')
.modalFooterOKClick('fileSystem')
.waitForElementVisible('*[data-shared="tooltipPopup"]', 5000)
.assert.containsText('*[data-shared="tooltipPopup"]', 'Saving gist (' + gistId + ') ...')

@ -128,6 +128,7 @@ const checkForAcceptAndRemember = async function (browser: NightwatchBrowser) {
*/
const clickAndCheckLog = async (browser: NightwatchBrowser, buttonText: string, methodResult: any, eventResult: any, payload: any, waitResult: boolean = true) => { // eslint-disable-line
console.log('clickAndCheckLog', buttonText, methodResult, eventResult, payload, waitResult)
if (payload) {
await setPayload(browser, payload)
} else {
@ -295,7 +296,7 @@ module.exports = {
}, null, '/')
},
'Should get all workspaces #group2': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'filePanel:getWorkspaces', [{name:"default_workspace",isGitRepo:false,hasGitSubmodules:false}, {name:"emptyworkspace",isGitRepo:false,hasGitSubmodules:false}, {name:"testspace",isGitRepo:false,hasGitSubmodules:false}], null, null)
await clickAndCheckLog(browser, 'filePanel:getWorkspaces', [{name:"default_workspace",isGitRepo:false,hasGitSubmodules:false,isGist:null}, {name:"emptyworkspace",isGitRepo:false,hasGitSubmodules:false,isGist:null}, {name:"testspace",isGitRepo:false,hasGitSubmodules:false,isGist:null}], null, null)
},
'Should have set workspace event #group2': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'filePanel:createWorkspace', null, { event: 'setWorkspace', args: [{ name: 'newspace', isLocalhost: false }] }, 'newspace')
@ -309,11 +310,11 @@ module.exports = {
'Should rename workspace #group2': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'filePanel:renameWorkspace', null, null, ['default_workspace', 'renamed'])
await clickAndCheckLog(browser, 'filePanel:getWorkspaces', [{name:"emptyworkspace",isGitRepo:false,hasGitSubmodules:false},{name:"testspace",isGitRepo:false,hasGitSubmodules:false},{name:"newspace",isGitRepo:false,hasGitSubmodules:false},{name:"renamed",isGitRepo:false,hasGitSubmodules:false}], null, null)
await clickAndCheckLog(browser, 'filePanel:getWorkspaces', [{name:"emptyworkspace",isGitRepo:false,hasGitSubmodules:false,isGist:null},{name:"testspace",isGitRepo:false,hasGitSubmodules:false,isGist:null},{name:"newspace",isGitRepo:false,hasGitSubmodules:false,isGist:null},{name:"renamed",isGitRepo:false,hasGitSubmodules:false,isGist:null}], null, null)
},
'Should delete workspace #group2': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'filePanel:deleteWorkspace', null, null, ['testspace'])
await clickAndCheckLog(browser, 'filePanel:getWorkspaces', [{name:"emptyworkspace",isGitRepo:false,hasGitSubmodules:false},{name:"newspace",isGitRepo:false,hasGitSubmodules:false},{name:"renamed",isGitRepo:false,hasGitSubmodules:false}], null, null)
await clickAndCheckLog(browser, 'filePanel:getWorkspaces', [{name:"emptyworkspace",isGitRepo:false,hasGitSubmodules:false,isGist:null},{name:"newspace",isGitRepo:false,hasGitSubmodules:false,isGist:null},{name:"renamed",isGitRepo:false,hasGitSubmodules:false,isGist:null}], null, null)
},
// DGIT
'Should have changes on new workspace #group3': async function (browser: NightwatchBrowser) {

@ -3,7 +3,7 @@
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/remix-ide/src",
"projectType": "application",
"implicitDependencies": ["doc-gen", "doc-viewer", "etherscan", "vyper", "solhint", "walletconnect", "circuit-compiler"],
"implicitDependencies": ["doc-gen", "doc-viewer", "etherscan", "vyper", "solhint", "walletconnect", "circuit-compiler", "learneth"],
"targets": {
"build": {
"executor": "@nrwl/webpack:webpack",

@ -46,6 +46,7 @@ const profile = {
'loadTemplate',
'clone',
'isExpanded',
'isGist'
],
events: ['setWorkspace', 'workspaceRenamed', 'workspaceDeleted', 'workspaceCreated'],
icon: 'assets/img/fileManager.webp',
@ -131,6 +132,20 @@ module.exports = class Filepanel extends ViewPlugin {
})
}
/**
* return the gist id if the current workspace is a gist workspace, otherwise returns null
* @argument {String} workspaceName - the name of the workspace to check against. default to the current workspace.
* @returns {string} gist id or null
*/
isGist (workspaceName) {
workspaceName = workspaceName || this.currentWorkspaceMetadata && this.currentWorkspaceMetadata.name
const isGist = workspaceName.startsWith('gist')
if (isGist) {
return workspaceName.split(' ')[1]
}
return null
}
getCurrentWorkspace() {
return this.currentWorkspaceMetadata
}

@ -62,10 +62,12 @@
"filePanel.compileForNahmii": "Compile for Nahmii",
"filePanel.createNewFile": "Create new file",
"filePanel.createNewFolder": "Create new folder",
"filePanel.publishToGist": "Publish all files to GitHub gist",
"filePanel.publishToGist": "Publish to Gist",
"filePanel.workspace.publishToGist": "Publish workspace to GitHub gist",
"filePanel.uploadFile": "Upload files",
"filePanel.uploadFolder": "Upload folder",
"filePanel.updateGist": "Update the current [gist] explorer",
"filePanel.updateGist": "Update Gist",
"filePanel.workspace.updateGist": "Publish Gist update",
"filePanel.viewAllBranches": "View all branches",
"filePanel.createBranch": "Create branch",
"filePanel.switchBranches": "Switch branches",

@ -89,7 +89,7 @@ let requiredModules = [ // services + layout views + system views
// dependentModules shouldn't be manually activated (e.g hardhat is activated by remixd)
const dependentModules = ['foundry', 'hardhat', 'truffle', 'slither']
const loadLocalPlugins = ['doc-gen', 'doc-viewer', 'etherscan', 'vyper', 'solhint', 'walletconnect', 'circuit-compiler']
const loadLocalPlugins = ['doc-gen', 'doc-viewer', 'etherscan', 'vyper', 'solhint', 'walletconnect', 'circuit-compiler', 'learneth']
const sensitiveCalls = {
fileManager: ['writeFile', 'copyFile', 'rename', 'copyDir'],

@ -114,10 +114,20 @@ export class GistHandler extends Plugin {
return
}
const gistIdWorkspace = 'gist ' + gistId
const workspaces = await this.call('filePanel', 'getWorkspaces')
const found = workspaces.find((workspace) => workspace.name === gistIdWorkspace)
if (found) {
await this.call('notification', 'alert', `workspace "${gistIdWorkspace}" already exist`)
return
}
await this.call('filePanel', 'createWorkspace', 'gist ' + gistId, '', true)
await this.call('filePanel', 'switchToWorkspace', { name: 'gist ' + gistId, isLocalHost: false })
const obj: StringByString = {}
Object.keys(data.files).forEach((element) => {
const path = element.replace(/\.\.\./g, '/')
obj['/gist-' + gistId + '/' + path] = data.files[element]
obj['/' + path] = data.files[element]
})
this.call('fileManager', 'setBatchFiles', obj, isElectron()? 'electron':'workspace', true, async (errorSavingFiles: any) => {
if (errorSavingFiles) {

@ -4,7 +4,7 @@ export interface FileType {
path: string,
name: string,
isDirectory: boolean,
type: 'folder' | 'file' | 'gist',
type: 'folder' | 'file',
child?: File[]
}

@ -24,6 +24,6 @@ export interface FileType {
path: string,
name?: string,
isDirectory?: boolean,
type?: 'folder' | 'file' | 'gist',
type?: 'folder' | 'file',
child?: File[]
}

@ -72,9 +72,10 @@ export const initWorkspace = (filePanelPlugin) => async (reducerDispatch: React.
dispatch(setWorkspaces(workspaces))
}
if (params.gist) {
await createWorkspaceTemplate('code-sample', 'gist-template')
plugin.setWorkspace({ name: 'code-sample', isLocalhost: false })
dispatch(setCurrentWorkspace({ name: 'code-sample', isGitRepo: false }))
const name = 'gist ' + params.gist
await createWorkspaceTemplate(name, 'gist-template')
plugin.setWorkspace({ name, isLocalhost: false })
dispatch(setCurrentWorkspace({ name, isGitRepo: false }))
await loadWorkspacePreset('gist-template')
} else if (params.code || params.url || params.shareCode) {
await createWorkspaceTemplate('code-sample', 'code-template')
@ -224,14 +225,19 @@ export type SolidityConfiguration = {
runs: string
}
export const publishToGist = async (path?: string, type?: string) => {
export const publishToGist = async (path?: string) => {
// If 'id' is not defined, it is not a gist update but a creation so we have to take the files from the browser explorer.
const folder = path || '/'
try {
const name = extractNameFromKey(path)
const id = name && name.startsWith('gist-') ? name.split('-')[1] : null
let id
if (path) {
// check if the current folder is a gist folder
id = await plugin.call('filePanel', 'isGist', extractNameFromKey(path))
} else {
// check if the current workspace is a gist workspace
id = await plugin.call('filePanel', 'isGist')
}
const packaged = await packageGistFiles(folder)
// check for token
const config = plugin.registry.get('config').api
@ -314,7 +320,7 @@ export const createNewFile = async (path: string, rootDir: string) => {
}
}
export const setFocusElement = async (elements: { key: string, type: 'file' | 'folder' | 'gist' }[]) => {
export const setFocusElement = async (elements: { key: string, type: 'file' | 'folder' }[]) => {
dispatch(focusElement(elements))
}
@ -429,7 +435,7 @@ export const emitContextMenuEvent = async (cmd: customAction) => {
await plugin.call(cmd.id, cmd.name, cmd)
}
export const handleClickFile = async (path: string, type: 'file' | 'folder' | 'gist') => {
export const handleClickFile = async (path: string, type: 'file' | 'folder' ) => {
if (type === 'file' && path.endsWith('.md')) {
// just opening the preview
await plugin.call('doc-viewer' as any, 'viewDocs', [path])
@ -520,11 +526,6 @@ const packageGistFiles = async (directory) => {
if (/^\s+$/.test(content) || !content.length) {
content = '// this line is added to create a gist. Empty file is not allowed.'
}
if (path.indexOf('gist-') === 0) {
path = path.split('/')
path.shift()
path = path.join('/')
}
path = path.replace(/\//g, '...')
ret[path] = { content }
})

@ -2,7 +2,7 @@ import React from 'react'
import { bufferToHex } from '@ethereumjs/util'
import { hash } from '@remix-project/remix-lib'
import { TEMPLATE_METADATA, TEMPLATE_NAMES } from '../utils/constants'
import { TemplateType } from '../utils/types'
import { TemplateType } from '../types'
import IpfsHttpClient from 'ipfs-http-client'
import axios, { AxiosResponse } from 'axios'
import {
@ -345,9 +345,9 @@ export const loadWorkspacePreset = async (template: WorkspaceTemplate = 'remixDe
}
if (data.files[element].type === 'application/json') {
obj['/' + 'gist-' + gistId + '/' + path] = { content: JSON.stringify(value.content, null, '\t') }
obj['/' + path] = { content: JSON.stringify(value.content, null, '\t') }
} else
obj['/' + 'gist-' + gistId + '/' + path] = value
obj['/' + path] = value
}
plugin.fileManager.setBatchFiles(obj, 'workspace', true, (errorLoadingFile) => {
if (errorLoadingFile) {
@ -609,6 +609,7 @@ export const getWorkspaces = async (): Promise<{ name: string; isGitRepo: boolea
Object.keys(items)
.filter((item) => items[item].isDirectory)
.map(async (folder) => {
const name = folder.replace(workspacesPath + '/', '')
const isGitRepo: boolean = await plugin.fileProviders.browser.exists('/' + folder + '/.git')
const hasGitSubmodules: boolean = await plugin.fileProviders.browser.exists('/' + folder + '/.gitmodules')
if (isGitRepo) {
@ -618,17 +619,19 @@ export const getWorkspaces = async (): Promise<{ name: string; isGitRepo: boolea
branches = await getGitRepoBranches(folder)
currentBranch = await getGitRepoCurrentBranch(folder)
return {
name: folder.replace(workspacesPath + '/', ''),
name,
isGitRepo,
branches,
currentBranch,
hasGitSubmodules
hasGitSubmodules,
isGist: null
}
} else {
return {
name: folder.replace(workspacesPath + '/', ''),
name,
isGitRepo,
hasGitSubmodules
hasGitSubmodules,
isGist: plugin.isGist(name) // plugin is filePanel
}
}
})

@ -187,15 +187,15 @@ export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) =>
break
case 'Push changes to gist':
_paq.push(['trackEvent', 'fileExplorer', 'contextMenu', 'pushToChangesoGist'])
pushChangesToGist(path, type)
pushChangesToGist(path)
break
case 'Publish folder to gist':
_paq.push(['trackEvent', 'fileExplorer', 'contextMenu', 'publishFolderToGist'])
publishFolderToGist(path, type)
publishFolderToGist(path)
break
case 'Publish file to gist':
_paq.push(['trackEvent', 'fileExplorer', 'contextMenu', 'publishFileToGist'])
publishFileToGist(path, type)
publishFileToGist(path)
break
case 'Run':
_paq.push(['trackEvent', 'fileExplorer', 'contextMenu', 'runScript'])
@ -227,7 +227,7 @@ export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) =>
break
case 'Publish Workspace to Gist':
_paq.push(['trackEvent', 'fileExplorer', 'contextMenu', 'publishWorkspace'])
publishFolderToGist(path, type)
publishFolderToGist(path)
break
default:
_paq.push(['trackEvent', 'fileExplorer', 'contextMenu', `${item.id}/${item.name}`])

@ -26,13 +26,6 @@ export const FileExplorerMenu = (props: FileExplorerMenuProps) => {
placement: 'top',
platforms:[appPlatformTypes.web, appPlatformTypes.desktop]
},
{
action: 'publishToGist',
title: 'Publish current workspace to GitHub gist',
icon: 'fab fa-github',
placement: 'top',
platforms:[appPlatformTypes.web]
},
{
action: 'uploadFile',
title: 'Upload files into current workspace',
@ -46,13 +39,6 @@ export const FileExplorerMenu = (props: FileExplorerMenuProps) => {
icon: 'far fa-folder-upload',
placement: 'top',
platforms:[appPlatformTypes.web]
},
{
action: 'updateGist',
title: 'Update the current [gist] explorer',
icon: 'fab fa-github',
placement: 'bottom-start',
platforms:[appPlatformTypes.web]
}
].filter(
(item) =>
@ -65,16 +51,6 @@ export const FileExplorerMenu = (props: FileExplorerMenuProps) => {
})
const enableDirUpload = {directory: '', webkitdirectory: ''}
useEffect(() => {
const actions = {
updateGist: () => {}
}
setState((prevState) => {
return {...prevState, actions}
})
}, [])
return (
(!global.fs.browser.isSuccessfulWorkspace ? null :
<>
@ -165,7 +141,7 @@ export const FileExplorerMenu = (props: FileExplorerMenuProps) => {
props.createNewFile()
} else if (action === 'createNewFolder') {
props.createNewFolder()
} else if (action === 'publishToGist') {
} else if (action === 'publishToGist' || action == 'updateGist') {
props.publishToGist()
} else {
state.actions[action]()

@ -143,12 +143,12 @@ export const FileExplorer = (props: FileExplorerProps) => {
}
}
const publishToGist = (path?: string, type?: string) => {
const publishToGist = (path?: string) => {
props.modal(
intl.formatMessage({ id: 'filePanel.createPublicGist' }),
intl.formatMessage({ id: 'filePanel.createPublicGistMsg4' }, { name }),
intl.formatMessage({ id: 'filePanel.ok' }),
() => toGist(path, type),
() => toGist(path),
intl.formatMessage({ id: 'filePanel.cancel' }),
() => { }
)
@ -173,7 +173,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
}
}
const handleClickFolder = async (path: string, type: 'folder' | 'file' | 'gist') => {
const handleClickFolder = async (path: string, type: 'folder' | 'file' ) => {
if (state.ctrlKey) {
if (props.focusElement.findIndex((item) => item.key === path) !== -1) {
const focusElement = props.focusElement.filter((item) => item.key !== path)

@ -2,13 +2,16 @@ import { appPlatformTypes } from '@remix-ui/app'
import React from 'react'
import {Dropdown} from 'react-bootstrap'
import {HamburgerMenuItem, HamburgerSubMenuItem} from './workspace-hamburger-item'
import { WorkspaceMetadata } from '../types'
export interface HamburgerMenuProps {
selectedWorkspace: WorkspaceMetadata
createWorkspace: () => void
renameCurrentWorkspace: () => void
downloadCurrentWorkspace: () => void
deleteCurrentWorkspace: () => void
deleteAllWorkspaces: () => void
pushChangesToGist: () => void
cloneGitRepository: () => void
downloadWorkspaces: () => void
restoreBackup: () => void
@ -24,7 +27,7 @@ export interface HamburgerMenuProps {
}
export function HamburgerMenu(props: HamburgerMenuProps) {
const {showIconsMenu, hideWorkspaceOptions, hideLocalhostOptions, hideFileOperations} = props
const {showIconsMenu, hideWorkspaceOptions, hideLocalhostOptions, hideFileOperations, selectedWorkspace} = props
return (
<>
<HamburgerMenuItem
@ -88,6 +91,17 @@ export function HamburgerMenu(props: HamburgerMenuProps) {
platforms={[appPlatformTypes.web]}
></HamburgerMenuItem>
<Dropdown.Divider className="border mb-0 mt-0 remixui_menuhr" style={{pointerEvents: 'none'}} />
<HamburgerMenuItem
kind={selectedWorkspace.isGist ? "updateGist" : "publishToGist"}
fa="fab fa-github"
hideOption={hideWorkspaceOptions || hideLocalhostOptions}
actionOnClick={() => {
props.pushChangesToGist()
props.hideIconsMenu(!showIconsMenu)
}}
platforms={[appPlatformTypes.web]}
></HamburgerMenuItem>
<Dropdown.Divider className="border mb-0 mt-0 remixui_menuhr" style={{pointerEvents: 'none'}} />
<HamburgerMenuItem
kind="deleteAll"
fa="far fa-trash-alt"

@ -21,7 +21,7 @@ export const FileSystemContext = createContext<{
dispatchUploadFile: (target?: SyntheticEvent, targetFolder?: string) => Promise<void>,
dispatchUploadFolder: (target?: SyntheticEvent, targetFolder?: string) => Promise<void>,
dispatchCreateNewFile: (path: string, rootDir: string) => Promise<void>,
dispatchSetFocusElement: (elements: { key: string, type: 'file' | 'folder' | 'gist' }[]) => Promise<void>,
dispatchSetFocusElement: (elements: { key: string, type: 'file' | 'folder' }[]) => Promise<void>,
dispatchCreateNewFolder: (path: string, rootDir: string) => Promise<void>,
dispatchDeletePath: (path: string[]) => Promise<void>,
dispatchRenamePath: (oldPath: string, newPath: string) => Promise<void>,
@ -31,7 +31,7 @@ export const FileSystemContext = createContext<{
dispatchCopyFolder: (src: string, dest: string) => Promise<void>,
dispatchRunScript: (path: string) => Promise<void>,
dispatchEmitContextMenuEvent: (cmd: customAction) => Promise<void>,
dispatchHandleClickFile: (path: string, type: 'file' | 'folder' | 'gist') => Promise<void>
dispatchHandleClickFile: (path: string, type: 'file' | 'folder' ) => Promise<void>
dispatchHandleExpandPath: (paths: string[]) => Promise<void>,
dispatchHandleDownloadFiles: () => Promise<void>,
dispatchHandleDownloadWorkspace: () => Promise<void>,

@ -117,8 +117,8 @@ export const FileSystemProvider = (props: WorkspaceProps) => {
await deleteAllWorkspaces()
}
const dispatchPublishToGist = async (path?: string, type?: string) => {
await publishToGist(path, type)
const dispatchPublishToGist = async (path?: string) => {
await publishToGist(path)
}
const dispatchUploadFile = async (target?: SyntheticEvent, targetFolder?: string) => {
@ -133,7 +133,7 @@ export const FileSystemProvider = (props: WorkspaceProps) => {
await createNewFile(path, rootDir)
}
const dispatchSetFocusElement = async (elements: {key: string; type: 'file' | 'folder' | 'gist'}[]) => {
const dispatchSetFocusElement = async (elements: {key: string; type: 'file' | 'folder' }[]) => {
await setFocusElement(elements)
}
@ -173,7 +173,7 @@ export const FileSystemProvider = (props: WorkspaceProps) => {
await emitContextMenuEvent(cmd)
}
const dispatchHandleClickFile = async (path: string, type: 'file' | 'folder' | 'gist') => {
const dispatchHandleClickFile = async (path: string, type: 'file' | 'folder' ) => {
await handleClickFile(path, type)
}

@ -17,6 +17,7 @@ export interface BrowserState {
name: string
}[]
currentBranch?: string
isGist: string
}[]
files: {[x: string]: Record<string, FileType>}
flatTree: FileType[]
@ -150,7 +151,6 @@ export const browserReducer = (state = browserInitialState, action: Actions) =>
case 'SET_WORKSPACES': {
const payload = action.payload
return {
...state,
browser: {
@ -986,8 +986,7 @@ const removeInputField = (
isDirectory: true,
path,
name: extractNameFromKey(path),
type:
extractNameFromKey(path).indexOf('gist-') === 0 ? 'gist' : 'folder',
type: 'folder',
child: prevFiles ? prevFiles.child : {}
},
Object
@ -1117,8 +1116,7 @@ const normalize = (
path,
name: extractNameFromKey(path),
isDirectory: filesList[key].isDirectory,
type:
extractNameFromKey(path).indexOf('gist-') === 0 ? 'gist' : 'folder'
type: 'folder'
}
} else {
files[extractNameFromKey(key)] = {

@ -2,13 +2,14 @@ import React, {useState, useEffect, useRef, useContext, ChangeEvent} from 'react
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 {FileSystemContext} from './contexts'
import './css/remix-ui-workspace.css'
import {ROOT_PATH, TEMPLATE_NAMES} from './utils/constants'
import {HamburgerMenu} from './components/workspace-hamburger'
import {MenuItems, WorkSpaceState} from './types'
import {MenuItems, WorkSpaceState, WorkspaceMetadata} from './types'
import {contextMenuActions} from './utils'
import FileExplorerContextMenu from './components/file-explorer-context-menu'
import { customAction } from '@remixproject/plugin-api'
@ -26,13 +27,7 @@ export function Workspace() {
const LOCALHOST = ' - connect to localhost - '
const NO_WORKSPACE = ' - none - '
const [currentWorkspace, setCurrentWorkspace] = useState<string>(NO_WORKSPACE)
const [selectedWorkspace, setSelectedWorkspace] = useState<{
name: string
isGitRepo: boolean
hasGitSubmodules?: boolean
branches?: {remote: any; name: string}[]
currentBranch?: string
}>(null)
const [selectedWorkspace, setSelectedWorkspace] = useState<WorkspaceMetadata>(null)
const [showDropdown, setShowDropdown] = useState<boolean>(false)
const [showIconsMenu, hideIconsMenu] = useState<boolean>(false)
const [showBranches, setShowBranches] = useState<boolean>(false)
@ -73,7 +68,7 @@ export function Workspace() {
},
mouseOverElement: null,
showContextMenu: false,
reservedKeywords: [ROOT_PATH, 'gist-'],
reservedKeywords: [ROOT_PATH],
copyElement: [],
dragStatus: false
})
@ -437,7 +432,7 @@ export function Workspace() {
}
}
const handleCopyClick = (path: string, type: 'folder' | 'gist' | 'file' | 'workspace') => {
const handleCopyClick = (path: string, type: 'folder' | 'file' | 'workspace') => {
setState((prevState) => {
return {...prevState, copyElement: [{key: path, type}]}
})
@ -506,7 +501,6 @@ export function Workspace() {
const focusElement = global.fs.focusElement
if (focusElement[0]) {
if (focusElement[0].type === 'folder' && focusElement[0].key) return focusElement[0].key
else if (focusElement[0].type === 'gist' && focusElement[0].key) return focusElement[0].key
else if (focusElement[0].type === 'file' && focusElement[0].key) return extractParentFromKey(focusElement[0].key) ? extractParentFromKey(focusElement[0].key) : ROOT_PATH
else return ROOT_PATH
}
@ -570,34 +564,34 @@ export function Workspace() {
}
}
const pushChangesToGist = (path?: string, type?: string) => {
const pushChangesToGist = (path?: string) => {
global.modal(
intl.formatMessage({id: 'filePanel.createPublicGist'}),
intl.formatMessage({id: 'filePanel.createPublicGistMsg1'}),
intl.formatMessage({id: 'filePanel.ok'}),
() => toGist(path, type),
() => toGist(path),
intl.formatMessage({id: 'filePanel.cancel'}),
() => {}
)
}
const publishFolderToGist = (path?: string, type?: string) => {
const publishFolderToGist = (path?: string) => {
global.modal(
intl.formatMessage({id: 'filePanel.createPublicGist'}),
intl.formatMessage({id: 'filePanel.createPublicGistMsg2'}, {path}),
intl.formatMessage({id: 'filePanel.ok'}),
() => toGist(path, type),
() => toGist(path),
intl.formatMessage({id: 'filePanel.cancel'}),
() => {}
)
}
const publishFileToGist = (path?: string, type?: string) => {
const publishFileToGist = (path?: string) => {
global.modal(
intl.formatMessage({id: 'filePanel.createPublicGist'}),
intl.formatMessage({id: 'filePanel.createPublicGistMsg3'}, {path}),
intl.formatMessage({id: 'filePanel.ok'}),
() => toGist(path, type),
() => toGist(path),
intl.formatMessage({id: 'filePanel.cancel'}),
() => {}
)
@ -632,8 +626,8 @@ export function Workspace() {
)
}
const toGist = (path?: string, type?: string) => {
global.dispatchPublishToGist(path, type)
const toGist = (path?: string) => {
global.dispatchPublishToGist(path)
}
const editModeOn = (path: string, type: string, isNew = false) => {
@ -922,7 +916,6 @@ export function Workspace() {
</>
)
}
return (
<div className="d-flex flex-column justify-content-between h-100">
<div
@ -952,11 +945,13 @@ export function Workspace() {
></Dropdown.Toggle>
<Dropdown.Menu as={CustomMenu} data-id="wsdropdownMenu" className="custom-dropdown-items remixui_menuwidth" rootCloseEvent="click">
<HamburgerMenu
selectedWorkspace={selectedWorkspace}
createWorkspace={createWorkspace}
renameCurrentWorkspace={renameCurrentWorkspace}
downloadCurrentWorkspace={downloadCurrentWorkspace}
deleteCurrentWorkspace={deleteCurrentWorkspace}
deleteAllWorkspaces={deleteAllWorkspaces}
pushChangesToGist={pushChangesToGist}
cloneGitRepository={cloneGitRepository}
downloadWorkspaces={downloadWorkspaces}
restoreBackup={restoreBackup}
@ -988,6 +983,12 @@ export function Workspace() {
>
<i onClick={() => saveSampleCodeWorkspace()} className="far fa-exclamation-triangle text-warning ml-2 align-self-center" aria-hidden="true"></i>
</CustomTooltip>}
{selectedWorkspace && selectedWorkspace.isGist && <CopyToClipboard tip={'Copy Gist ID to clipboard'} getContent={() => selectedWorkspace.isGist} direction="bottom" icon="far fa-copy">
<i className="remixui_copyIcon ml-2 fab fa-github text-info" aria-hidden="true" style={{fontSize: '1.1rem', cursor: 'pointer'}} ></i>
</CopyToClipboard>
}
</span>
</div>
<div className='mx-2'>
@ -1077,11 +1078,10 @@ export function Workspace() {
</div>
)}
{!(global.fs.browser.isRequestingWorkspace || global.fs.browser.isRequestingCloning) && global.fs.mode === 'browser' && currentWorkspace !== NO_WORKSPACE && (
<FileExplorer
fileState={global.fs.browser.fileState}
name={currentWorkspace}
menuItems={['createNewFile', 'createNewFolder', 'publishToGist', canUpload ? 'uploadFile' : '', canUpload ? 'uploadFolder' : '']}
menuItems={['createNewFile', 'createNewFolder', selectedWorkspace && selectedWorkspace.isGist ? 'updateGist' : 'publishToGist', canUpload ? 'uploadFile' : '', canUpload ? 'uploadFolder' : '']}
contextMenuItems={global.fs.browser.contextMenu.registeredMenuItems}
removedContextMenuItems={global.fs.browser.contextMenu.removedMenuItems}
files={global.fs.browser.files}

@ -43,10 +43,28 @@ export interface FileType {
path: string
name: string
isDirectory: boolean
type: 'folder' | 'file' | 'gist'
type: 'folder' | 'file'
child?: File[]
}
export type WorkspaceMetadata = {
name: string
isGitRepo: boolean
hasGitSubmodules?: boolean
branches?: {remote: any; name: string}[]
currentBranch?: string
isGist: string
}
export type TemplateType = {
type: 'git' | 'plugin'
url?: string
branch?: string
name?: string
endpoint?: string
params?: any[]
}
export interface FilePanelType extends ViewPlugin {
setWorkspace: ({ name, isLocalhost }, setEvent: boolean) => void
createWorkspace: (name: string, workspaceTemplateName: string) => void
@ -155,10 +173,10 @@ export interface FileExplorerContextMenuProps {
renamePath: (path: string, type: string) => void
downloadPath: (path: string) => void
hideContextMenu: () => void
publishToGist?: (path?: string, type?: string) => void
pushChangesToGist?: (path?: string, type?: string) => void
publishFolderToGist?: (path?: string, type?: string) => void
publishFileToGist?: (path?: string, type?: string) => void
publishToGist?: (path?: string) => void
pushChangesToGist?: (path?: string) => void
publishFolderToGist?: (path?: string) => void
publishFileToGist?: (path?: string) => void
runScript?: (path: string) => void
emit?: (cmd: customAction) => void
pageX: number
@ -319,4 +337,4 @@ export interface Action<T extends keyof ActionPayloadTypes> {
export type Actions = {[A in keyof ActionPayloadTypes]: Action<A>}[keyof ActionPayloadTypes]
export type WorkspaceElement = 'folder' | 'gist' | 'file' | 'workspace'
export type WorkspaceElement = 'folder' | 'file' | 'workspace'

@ -1,4 +1,4 @@
import { TemplateType } from './types'
import { TemplateType } from '../types'
export const ROOT_PATH = '/'
export const solTestYml = `
name: Running Solidity Unit Tests

@ -1,18 +1,18 @@
import { appPlatformTypes } from '@remix-ui/app'
import { FileType } from '@remix-ui/file-decorators'
import { WorkspaceProps, MenuItems } from '../types'
import { MenuItems } from '../types'
export const contextMenuActions: MenuItems = [{
id: 'newFile',
name: 'New File',
type: ['folder', 'gist', 'workspace'],
type: ['folder', 'workspace'],
multiselect: false,
label: '',
group: 0
}, {
id: 'newFolder',
name: 'New Folder',
type: ['folder', 'gist', 'workspace'],
type: ['folder', 'workspace'],
multiselect: false,
label: '',
group: 0
@ -26,7 +26,7 @@ export const contextMenuActions: MenuItems = [{
}, {
id: 'delete',
name: 'Delete',
type: ['file', 'folder', 'gist'],
type: ['file', 'folder'],
multiselect: false,
label: '',
group: 0
@ -80,14 +80,6 @@ export const contextMenuActions: MenuItems = [{
multiselect: false,
label: '',
group: 3
}, {
id: 'pushChangesToGist',
name: 'Push changes to gist',
type: ['gist'],
multiselect: false,
label: '',
group: 4,
platform: appPlatformTypes.web
}, {
id: 'publishFolderToGist',
name: 'Publish folder to gist',
@ -107,21 +99,12 @@ export const contextMenuActions: MenuItems = [{
}, {
id: 'uploadFile',
name: 'Load a Local File',
type: ['folder', 'gist', 'workspace'],
type: ['folder', 'workspace'],
multiselect: false,
label: 'Load a Local File',
group: 4,
platform: appPlatformTypes.web
}, {
id: 'publishToGist',
name: 'Push changes to gist',
type: ['folder', 'gist'],
multiselect: false,
label: 'Publish all to Gist',
group: 4,
platform: appPlatformTypes.web
},
{
},{
id: 'publishWorkspace',
name: 'Publish Workspace to Gist',
type: ['workspace'],

@ -1,8 +0,0 @@
export type TemplateType = {
type: 'git' | 'plugin'
url?: string
branch?: string
name?: string
endpoint?: string
params?: any[]
}

@ -30,6 +30,8 @@
"start": "nx start",
"serve": "nx serve remix-ide --configuration=development",
"serve:hot": "nx serve remix-ide --configuration=hot",
"serve:plugin": "nx serve ${npm_config_plugin} --configuration=development",
"build:plugin": "NODE_ENV=production nx build ${npm_config_plugin} --configuration=production --skip-nx-cache",
"build": "nx build",
"test": "nx test",
"lint": "nx lint",
@ -141,6 +143,7 @@
"@openzeppelin/contracts": "^5.0.0",
"@openzeppelin/upgrades-core": "^1.30.0",
"@openzeppelin/wizard": "0.4.0",
"@reduxjs/toolkit": "^2.0.1",
"@remixproject/engine": "0.3.42",
"@remixproject/engine-electron": "0.3.42",
"@remixproject/engine-web": "0.3.42",
@ -214,12 +217,16 @@
"react-markdown": "^8.0.5",
"react-multi-carousel": "^2.8.2",
"react-router-dom": "^6.16.0",
"react-spinners": "^0.13.8",
"react-tabs": "^6.0.2",
"react-toastify": "^10.0.3",
"react-virtualized": "^9.22.5",
"react-virtuoso": "^4.6.2",
"react-window": "^1.8.10",
"react-zoom-pan-pinch": "^3.1.0",
"redux-saga": "^1.3.0",
"regenerator-runtime": "0.13.7",
"rehype-raw": "^6.0.0",
"remark-gfm": "^3.0.1",
"rlp": "^3.0.0",
"rss-parser": "^3.12.0",

@ -5342,6 +5342,59 @@
unbzip2-stream "1.4.3"
yargs "17.7.1"
"@redux-saga/core@^1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@redux-saga/core/-/core-1.3.0.tgz#2ce08b73d407fc6ea9e7f7d83d2e97d981a3a8b8"
integrity sha512-L+i+qIGuyWn7CIg7k1MteHGfttKPmxwZR5E7OsGikCL2LzYA0RERlaUY00Y3P3ZV2EYgrsYlBrGs6cJP5OKKqA==
dependencies:
"@babel/runtime" "^7.6.3"
"@redux-saga/deferred" "^1.2.1"
"@redux-saga/delay-p" "^1.2.1"
"@redux-saga/is" "^1.1.3"
"@redux-saga/symbols" "^1.1.3"
"@redux-saga/types" "^1.2.1"
typescript-tuple "^2.2.1"
"@redux-saga/deferred@^1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@redux-saga/deferred/-/deferred-1.2.1.tgz#aca373a08ccafd6f3481037f2f7ee97f2c87c3ec"
integrity sha512-cmin3IuuzMdfQjA0lG4B+jX+9HdTgHZZ+6u3jRAOwGUxy77GSlTi4Qp2d6PM1PUoTmQUR5aijlA39scWWPF31g==
"@redux-saga/delay-p@^1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@redux-saga/delay-p/-/delay-p-1.2.1.tgz#e72ac4731c5080a21f75b61bedc31cb639d9e446"
integrity sha512-MdiDxZdvb1m+Y0s4/hgdcAXntpUytr9g0hpcOO1XFVyyzkrDu3SKPgBFOtHn7lhu7n24ZKIAT1qtKyQjHqRd+w==
dependencies:
"@redux-saga/symbols" "^1.1.3"
"@redux-saga/is@^1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@redux-saga/is/-/is-1.1.3.tgz#b333f31967e87e32b4e6b02c75b78d609dd4ad73"
integrity sha512-naXrkETG1jLRfVfhOx/ZdLj0EyAzHYbgJWkXbB3qFliPcHKiWbv/ULQryOAEKyjrhiclmr6AMdgsXFyx7/yE6Q==
dependencies:
"@redux-saga/symbols" "^1.1.3"
"@redux-saga/types" "^1.2.1"
"@redux-saga/symbols@^1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@redux-saga/symbols/-/symbols-1.1.3.tgz#b731d56201719e96dc887dc3ae9016e761654367"
integrity sha512-hCx6ZvU4QAEUojETnX8EVg4ubNLBFl1Lps4j2tX7o45x/2qg37m3c6v+kSp8xjDJY+2tJw4QB3j8o8dsl1FDXg==
"@redux-saga/types@^1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@redux-saga/types/-/types-1.2.1.tgz#9403f51c17cae37edf870c6bc0c81c1ece5ccef8"
integrity sha512-1dgmkh+3so0+LlBWRhGA33ua4MYr7tUOj+a9Si28vUi0IUFNbff1T3sgpeDJI/LaC75bBYnQ0A3wXjn0OrRNBA==
"@reduxjs/toolkit@^2.0.1":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-2.2.0.tgz#69b7d7933ea3e7f0cbbe182caa4850e09ea643ae"
integrity sha512-ZvPYKfu4kDnAqPhJ1bsis8QFbiQRz3Q2HxW3tw9tVGusPzYKRG7ju1FA+34PGcwCoemjGGv+f/7fEygcRZIwmA==
dependencies:
immer "^10.0.3"
redux "^5.0.1"
redux-thunk "^3.1.0"
reselect "^5.0.1"
"@remix-run/router@1.14.0":
version "1.14.0"
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.14.0.tgz#9bc39a5a3a71b81bdb310eba6def5bc3966695b7"
@ -6411,6 +6464,11 @@
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
"@types/parse5@^6.0.0":
version "6.0.3"
resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-6.0.3.tgz#705bb349e789efa06f43f128cef51240753424cb"
integrity sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==
"@types/pbkdf2@^3.0.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@types/pbkdf2/-/pbkdf2-3.1.0.tgz#039a0e9b67da0cdc4ee5dab865caa6b267bb66b1"
@ -10790,6 +10848,11 @@ clsx@^2.0.0:
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.0.0.tgz#12658f3fd98fafe62075595a5c30e43d18f3d00b"
integrity sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==
clsx@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.0.tgz#e851283bcb5c80ee7608db18487433f7b23f77cb"
integrity sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==
cluster-key-slot@^1.1.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac"
@ -16120,11 +16183,71 @@ hasha@^3.0.0:
dependencies:
is-stream "^1.0.1"
hast-util-from-parse5@^7.0.0:
version "7.1.2"
resolved "https://registry.yarnpkg.com/hast-util-from-parse5/-/hast-util-from-parse5-7.1.2.tgz#aecfef73e3ceafdfa4550716443e4eb7b02e22b0"
integrity sha512-Nz7FfPBuljzsN3tCQ4kCBKqdNhQE2l0Tn+X1ubgKBPRoiDIu1mL08Cfw4k7q71+Duyaw7DXDN+VTAp4Vh3oCOw==
dependencies:
"@types/hast" "^2.0.0"
"@types/unist" "^2.0.0"
hastscript "^7.0.0"
property-information "^6.0.0"
vfile "^5.0.0"
vfile-location "^4.0.0"
web-namespaces "^2.0.0"
hast-util-parse-selector@^3.0.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-3.1.1.tgz#25ab00ae9e75cbc62cf7a901f68a247eade659e2"
integrity sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA==
dependencies:
"@types/hast" "^2.0.0"
hast-util-raw@^7.2.0:
version "7.2.3"
resolved "https://registry.yarnpkg.com/hast-util-raw/-/hast-util-raw-7.2.3.tgz#dcb5b22a22073436dbdc4aa09660a644f4991d99"
integrity sha512-RujVQfVsOrxzPOPSzZFiwofMArbQke6DJjnFfceiEbFh7S05CbPt0cYN+A5YeD3pso0JQk6O1aHBnx9+Pm2uqg==
dependencies:
"@types/hast" "^2.0.0"
"@types/parse5" "^6.0.0"
hast-util-from-parse5 "^7.0.0"
hast-util-to-parse5 "^7.0.0"
html-void-elements "^2.0.0"
parse5 "^6.0.0"
unist-util-position "^4.0.0"
unist-util-visit "^4.0.0"
vfile "^5.0.0"
web-namespaces "^2.0.0"
zwitch "^2.0.0"
hast-util-to-parse5@^7.0.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/hast-util-to-parse5/-/hast-util-to-parse5-7.1.0.tgz#c49391bf8f151973e0c9adcd116b561e8daf29f3"
integrity sha512-YNRgAJkH2Jky5ySkIqFXTQiaqcAtJyVE+D5lkN6CdtOqrnkLfGYYrEcKuHOJZlp+MwjSwuD3fZuawI+sic/RBw==
dependencies:
"@types/hast" "^2.0.0"
comma-separated-tokens "^2.0.0"
property-information "^6.0.0"
space-separated-tokens "^2.0.0"
web-namespaces "^2.0.0"
zwitch "^2.0.0"
hast-util-whitespace@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz#0ec64e257e6fc216c7d14c8a1b74d27d650b4557"
integrity sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==
hastscript@^7.0.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-7.2.0.tgz#0eafb7afb153d047077fa2a833dc9b7ec604d10b"
integrity sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw==
dependencies:
"@types/hast" "^2.0.0"
comma-separated-tokens "^2.0.0"
hast-util-parse-selector "^3.0.0"
property-information "^6.0.0"
space-separated-tokens "^2.0.0"
hawk@~3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4"
@ -16248,6 +16371,11 @@ html-react-parser@^3.0.4:
react-property "2.0.0"
style-to-js "1.1.1"
html-void-elements@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-2.0.1.tgz#29459b8b05c200b6c5ee98743c41b979d577549f"
integrity sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==
html2canvas@^1.0.0-rc.5:
version "1.4.1"
resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543"
@ -16571,6 +16699,11 @@ immediate@~3.0.5:
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=
immer@^10.0.3:
version "10.0.3"
resolved "https://registry.yarnpkg.com/immer/-/immer-10.0.3.tgz#a8de42065e964aa3edf6afc282dfc7f7f34ae3c9"
integrity sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A==
immutable@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.1.0.tgz#f795787f0db780183307b9eb2091fcac1f6fafef"
@ -23072,6 +23205,11 @@ parse5@4.0.0:
resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608"
integrity sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==
parse5@^6.0.0:
version "6.0.1"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
parse5@^7.0.0:
version "7.1.2"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32"
@ -24624,6 +24762,11 @@ react-router@6.21.0:
dependencies:
"@remix-run/router" "1.14.0"
react-spinners@^0.13.8:
version "0.13.8"
resolved "https://registry.yarnpkg.com/react-spinners/-/react-spinners-0.13.8.tgz#5262571be0f745d86bbd49a1e6b49f9f9cb19acc"
integrity sha512-3e+k56lUkPj0vb5NDXPVFAOkPC//XyhKPJjvcGjyMNPWsBKpplfeyialP74G7H7+It7KzhtET+MvGqbKgAqpZA==
react-tabs@^6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/react-tabs/-/react-tabs-6.0.2.tgz#bc1065c3828561fee285a8fd045f22e0fcdde1eb"
@ -24641,6 +24784,13 @@ react-textarea-autosize@~8.3.2:
use-composed-ref "^1.3.0"
use-latest "^1.2.1"
react-toastify@^10.0.3:
version "10.0.4"
resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-10.0.4.tgz#6ecdbbf923a07fc45850e69b0566efc7bf733283"
integrity sha512-etR3RgueY8pe88SA67wLm8rJmL1h+CLqUGHuAoNsseW35oTGJEri6eBTyaXnFKNQ80v/eO10hBYLgz036XRGgA==
dependencies:
clsx "^2.1.0"
react-transition-group@^4.4.1:
version "4.4.5"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1"
@ -25027,6 +25177,18 @@ redis-parser@^3.0.0:
dependencies:
redis-errors "^1.0.0"
redux-saga@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/redux-saga/-/redux-saga-1.3.0.tgz#a59ada7c28010189355356b99738c9fcb7ade30e"
integrity sha512-J9RvCeAZXSTAibFY0kGw6Iy4EdyDNW7k6Q+liwX+bsck7QVsU78zz8vpBRweEfANxnnlG/xGGeOvf6r8UXzNJQ==
dependencies:
"@redux-saga/core" "^1.3.0"
redux-thunk@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-3.1.0.tgz#94aa6e04977c30e14e892eae84978c1af6058ff3"
integrity sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==
redux@^4.0.0, redux@^4.0.4:
version "4.1.2"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.2.tgz#140f35426d99bb4729af760afcf79eaaac407104"
@ -25034,6 +25196,11 @@ redux@^4.0.0, redux@^4.0.4:
dependencies:
"@babel/runtime" "^7.9.2"
redux@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/redux/-/redux-5.0.1.tgz#97fa26881ce5746500125585d5642c77b6e9447b"
integrity sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==
regenerate-unicode-properties@^10.1.0:
version "10.1.0"
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz#7c3192cab6dd24e21cb4461e5ddd7dd24fa8374c"
@ -25232,6 +25399,15 @@ regjsparser@^0.9.1:
dependencies:
jsesc "~0.5.0"
rehype-raw@^6.0.0:
version "6.1.1"
resolved "https://registry.yarnpkg.com/rehype-raw/-/rehype-raw-6.1.1.tgz#81bbef3793bd7abacc6bf8335879d1b6c868c9d4"
integrity sha512-d6AKtisSRtDRX4aSPsJGTfnzrX2ZkHQLE5kiUuGOeEoLpbEulFF4hj0mLPbsa+7vmguDKOVVEQdHKDSwoaIDsQ==
dependencies:
"@types/hast" "^2.0.0"
hast-util-raw "^7.2.0"
unified "^10.0.0"
release-zalgo@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/release-zalgo/-/release-zalgo-1.0.0.tgz#09700b7e5074329739330e535c5a90fb67851730"
@ -25405,6 +25581,11 @@ reselect@^4.0.0:
resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.1.7.tgz#56480d9ff3d3188970ee2b76527bd94a95567a42"
integrity sha512-Zu1xbUt3/OPwsXL46hvOOoQrap2azE7ZQbokq61BQfiXvhewsKDwhMeZjTX9sX0nvw1t/U5Audyn1I9P/m9z0A==
reselect@^5.0.1:
version "5.1.0"
resolved "https://registry.yarnpkg.com/reselect/-/reselect-5.1.0.tgz#c479139ab9dd91be4d9c764a7f3868210ef8cd21"
integrity sha512-aw7jcGLDpSgNDyWBQLv2cedml85qd95/iszJjN988zX1t7AVRJi19d9kto5+W7oCfQ94gyo40dVbT6g2k4/kXg==
reset@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/reset/-/reset-0.1.0.tgz#9fc7314171995ae6cb0b7e58b06ce7522af4bafb"
@ -28407,6 +28588,25 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
typescript-compare@^0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/typescript-compare/-/typescript-compare-0.0.2.tgz#7ee40a400a406c2ea0a7e551efd3309021d5f425"
integrity sha512-8ja4j7pMHkfLJQO2/8tut7ub+J3Lw2S3061eJLFQcvs3tsmJKp8KG5NtpLn7KcY2w08edF74BSVN7qJS0U6oHA==
dependencies:
typescript-logic "^0.0.0"
typescript-logic@^0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/typescript-logic/-/typescript-logic-0.0.0.tgz#66ebd82a2548f2b444a43667bec120b496890196"
integrity sha512-zXFars5LUkI3zP492ls0VskH3TtdeHCqu0i7/duGt60i5IGPIpAHE/DWo5FqJ6EjQ15YKXrt+AETjv60Dat34Q==
typescript-tuple@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/typescript-tuple/-/typescript-tuple-2.2.1.tgz#7d9813fb4b355f69ac55032e0363e8bb0f04dad2"
integrity sha512-Zcr0lbt8z5ZdEzERHAMAniTiIKerFCMgd7yjq1fPnDJ43et/k9twIFQMUYff9k5oXcsQ0WpvFcgzK2ZKASoW6Q==
dependencies:
typescript-compare "^0.0.2"
typescript@^4.8.4:
version "4.8.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6"
@ -29157,6 +29357,14 @@ verror@1.3.6:
dependencies:
extsprintf "1.0.2"
vfile-location@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-4.1.0.tgz#69df82fb9ef0a38d0d02b90dd84620e120050dd0"
integrity sha512-YF23YMyASIIJXpktBa4vIGLJ5Gs88UB/XePgqPmTa7cDA+JeO3yclbpheQYCHjVHBn/yePzrXuygIL+xbvRYHw==
dependencies:
"@types/unist" "^2.0.0"
vfile "^5.0.0"
vfile-message@^3.0.0:
version "3.1.4"
resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-3.1.4.tgz#15a50816ae7d7c2d1fa87090a7f9f96612b59dea"
@ -29340,6 +29548,11 @@ web-encoding@^1.0.2, web-encoding@^1.0.6:
optionalDependencies:
"@zxing/text-encoding" "0.9.0"
web-namespaces@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-2.0.1.tgz#1010ff7c650eccb2592cebeeaf9a1b253fd40692"
integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==
web-streams-polyfill@^3.1.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.0.tgz#a6b74026b38e4885869fb5c589e90b95ccfc7965"

Loading…
Cancel
Save