diff --git a/tools/webide/packages/client/package.json b/tools/webide/packages/client/package.json
index 53012c75e..d6ef4e53c 100644
--- a/tools/webide/packages/client/package.json
+++ b/tools/webide/packages/client/package.json
@@ -25,6 +25,7 @@
"redux": "^4.0.4",
"redux-devtools": "^3.5.0",
"redux-thunk": "^2.3.0",
+ "slugify": "^1.4.0",
"styled-components": "^4.4.0",
"typescript": "3.6.4"
},
diff --git a/tools/webide/packages/client/src/components/configure/configure-tab.tsx b/tools/webide/packages/client/src/components/configure/configure-tab.tsx
index 1292cd09c..0023f3ffc 100644
--- a/tools/webide/packages/client/src/components/configure/configure-tab.tsx
+++ b/tools/webide/packages/client/src/components/configure/configure-tab.tsx
@@ -7,15 +7,17 @@ import { DeployAction } from '../../redux/actions/deploy';
import { DryRunAction } from '../../redux/actions/dry-run';
import { EvaluateFunctionAction } from '../../redux/actions/evaluate-function';
import { EvaluateValueAction } from '../../redux/actions/evaluate-value';
+import { GenerateCommandAction } from '../../redux/actions/generate-command';
import { AppState } from '../../redux/app';
import { ChangeDispatchedAction, ChangeSelectedAction, CommandState } from '../../redux/command';
import { Command } from '../../redux/types';
-import { CommandSelectComponent } from './command-select';
+import { Option, Select } from '../form/select';
import { CompilePaneComponent } from './compile-pane';
import { DeployPaneComponent } from './deploy-pane';
import { DryRunPaneComponent } from './dry-run-pane';
import { EvaluateFunctionPaneComponent } from './evaluate-function-pane';
import { EvaluateValuePaneComponent } from './evaluate-value-pane';
+import { GenerateCommandPaneComponent } from './generate-command-pane';
const Container = styled.div<{ visible?: boolean }>`
position: absolute;
@@ -58,8 +60,8 @@ const RunButton = styled.div`
background-color: var(--orange);
`;
-const CommandPaneContainer = styled.div`
- padding-top: 1em;
+const SelectCommand = styled(Select)`
+ flex: 2;
`;
function createAction(command: Command) {
@@ -74,6 +76,8 @@ function createAction(command: Command) {
return new EvaluateValueAction();
case Command.EvaluateFunction:
return new EvaluateFunctionAction();
+ case Command.GenerateCommand:
+ return new GenerateCommandAction();
default:
throw new Error('Unsupported command');
}
@@ -97,12 +101,20 @@ export const ConfigureTabComponent = (props: {
return (
- {
dispatch({ ...new ChangeSelectedAction(command) });
}}
- >
+ >
+
+
+
+
+
+
+
{
@@ -120,23 +132,24 @@ export const ConfigureTabComponent = (props: {
Run
-
- {(command === Command.Compile && (
-
+ {(command === Command.Compile && (
+
+ )) ||
+ (command === Command.DryRun && (
+
)) ||
- (command === Command.DryRun && (
-
- )) ||
- (command === Command.Deploy && (
-
- )) ||
- (command === Command.EvaluateFunction && (
-
- )) ||
- (command === Command.EvaluateValue && (
-
- ))}
-
+ (command === Command.Deploy && (
+
+ )) ||
+ (command === Command.EvaluateFunction && (
+
+ )) ||
+ (command === Command.EvaluateValue && (
+
+ )) ||
+ (command === Command.GenerateCommand && (
+
+ ))}
);
};
diff --git a/tools/webide/packages/client/src/components/configure/generate-command-pane.tsx b/tools/webide/packages/client/src/components/configure/generate-command-pane.tsx
new file mode 100644
index 000000000..da56264af
--- /dev/null
+++ b/tools/webide/packages/client/src/components/configure/generate-command-pane.tsx
@@ -0,0 +1,85 @@
+import React from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import styled from 'styled-components';
+
+import { AppState } from '../../redux/app';
+import {
+ ChangeCommandAction,
+ ChangeEntrypointAction,
+ ChangeStorageAction,
+ ChangeToolAction,
+ GenerateCommandState,
+} from '../../redux/generate-command';
+import { Tool, ToolCommand } from '../../redux/types';
+import { AccessFunctionLabel, Group, Input, Label, Textarea } from '../form/inputs';
+import { Option, Select } from '../form/select';
+
+const Container = styled.div`
+ overflow: auto;
+`;
+
+export const GenerateCommandPaneComponent = () => {
+ const dispatch = useDispatch();
+
+ const tool = useSelector(
+ state => state.generateCommand.tool
+ );
+
+ const command = useSelector(
+ state => state.generateCommand.command
+ );
+
+ const entrypoint = useSelector(
+ state => state.generateCommand.entrypoint
+ );
+
+ const storage = useSelector(
+ state => state.generateCommand.storage
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ dispatch({ ...new ChangeEntrypointAction(ev.target.value) })
+ }
+ >
+
+
+
+
+
+
+ );
+};
diff --git a/tools/webide/packages/client/src/components/editor/editor.tsx b/tools/webide/packages/client/src/components/editor/editor.tsx
index f75009b07..6ed4571e0 100644
--- a/tools/webide/packages/client/src/components/editor/editor.tsx
+++ b/tools/webide/packages/client/src/components/editor/editor.tsx
@@ -3,11 +3,12 @@ import { useDispatch, useSelector } from 'react-redux';
import styled from 'styled-components';
import { AppState } from '../../redux/app';
-import { ChangeTitleAction } from '../../redux/editor';
+import { ChangeLanguageAction, ChangeTitleAction, EditorState } from '../../redux/editor';
+import { Language } from '../../redux/types';
+import { Option, Select } from '../form/select';
import { ShareComponent } from '../share';
import { EditableTitleComponent } from './editable-title';
import { MonacoComponent } from './monaco';
-import { SyntaxSelectComponent } from './syntax-select';
const Container = styled.div`
flex: 2;
@@ -35,6 +36,9 @@ const StyledEditableTitleComponent = styled(EditableTitleComponent)`
export const EditorComponent = () => {
const dispatch = useDispatch();
const title = useSelector(state => state.editor.title);
+ const language = useSelector(
+ state => state.editor.language
+ );
return (
@@ -49,7 +53,17 @@ export const EditorComponent = () => {
}}
>
-
+
diff --git a/tools/webide/packages/client/src/components/editor/syntax-select.tsx b/tools/webide/packages/client/src/components/editor/syntax-select.tsx
deleted file mode 100644
index dcb935e7e..000000000
--- a/tools/webide/packages/client/src/components/editor/syntax-select.tsx
+++ /dev/null
@@ -1,157 +0,0 @@
-import { faCaretDown } from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import React, { useState } from 'react';
-import OutsideClickHandler from 'react-outside-click-handler';
-import { useDispatch, useSelector } from 'react-redux';
-import styled, { css } from 'styled-components';
-
-import { AppState } from '../../redux/app';
-import { ChangeLanguageAction, EditorState } from '../../redux/editor';
-import { Language } from '../../redux/types';
-import { Tooltip } from '../tooltip';
-
-const Container = styled.div`
- display: flex;
- position: relative;
- z-index: 2;
- min-width: 10em;
-`;
-
-const Header = styled.div`
- cursor: pointer;
- user-select: none;
-
- flex: 1;
- display: flex;
- height: 2em;
- padding: 0 0.5em;
-
- border: 1px solid var(--blue_trans1);
-`;
-
-const Label = styled.div`
- flex: 1;
- display: flex;
- align-items: center;
- justify-content: space-between;
-`;
-
-const ArrowIcon = ({ rotate, ...props }: { rotate: boolean }) => (
-
-);
-
-const Arrow = styled(ArrowIcon)`
- margin-left: 0.5em;
- pointer-events: none;
- color: var(--blue_trans1);
- transition: transform 0.15s ease-in;
-
- ${(props: { rotate: boolean }) =>
- props.rotate &&
- css`
- transform: rotate(180deg);
- `};
-`;
-
-const List = styled.ul`
- position: absolute;
- list-style-type: none;
- background-color: white;
- width: 100%;
- margin: 0;
- padding: 0;
- box-shadow: 1px 3px 10px 0px rgba(153, 153, 153, 0.4);
- border-radius: 3px;
-
- visibility: hidden;
- opacity: 0;
- transition: opacity 0.15s ease-in;
-
- ${(props: { visible: boolean }) =>
- props.visible &&
- css`
- visibility: visible;
- opacity: 1;
- `}
-`;
-
-const Option = styled.li`
- cursor: pointer;
- user-select: none;
-
- display: flex;
- align-items: center;
- justify-content: space-between;
- height: 2em;
- padding: 0 0.5em;
-
- &:first-child {
- border-radius: 3px 3px 0 0;
- }
-
- &:last-child {
- border-radius: 0 0 3px 3px;
- }
-
- &:hover {
- background-color: var(--blue_trans2);
- font-weight: 600;
- }
-`;
-
-export const SyntaxSelectComponent = () => {
- const OPTIONS = {
- [Language.PascaLigo]: 'PascaLIGO',
- [Language.CameLigo]: 'CameLIGO',
- [Language.ReasonLIGO]: 'ReasonLIGO'
- };
-
- const moveOptionToTop = (option: Language) => {
- return Object.keys(OPTIONS).reduce((list, entry) => {
- if (entry === option) {
- list.unshift(entry);
- } else {
- list.push(entry as Language);
- }
- return list;
- }, [] as Language[]);
- };
-
- const language = useSelector(
- state => state.editor.language
- );
- const dispatch = useDispatch();
- const [opened, open] = useState(false);
-
- const selectOption = (option: Language) => {
- if (language !== option) {
- dispatch({ ...new ChangeLanguageAction(option) });
- }
- open(false);
- };
-
- return (
-
- open(false)}>
-
- {moveOptionToTop(language).map(option => (
-
- ))}
-
-
- open(true)}>
-
- Select syntax
-
-
- );
-};
diff --git a/tools/webide/packages/client/src/components/form/checkbox.tsx b/tools/webide/packages/client/src/components/form/checkbox.tsx
index 8f69ddaa0..9785291e2 100644
--- a/tools/webide/packages/client/src/components/form/checkbox.tsx
+++ b/tools/webide/packages/client/src/components/form/checkbox.tsx
@@ -3,7 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import React, { useState } from 'react';
import styled, { css } from 'styled-components';
-const Container = styled.div<{ checked: boolean }>`
+const Container = styled.div`
display: flex;
justify-content: center;
align-items: center;
@@ -44,7 +44,6 @@ export const CheckboxComponent = (props: {
return (
{
const newState = !isChecked;
diff --git a/tools/webide/packages/client/src/components/form/inputs.tsx b/tools/webide/packages/client/src/components/form/inputs.tsx
index 721a171bd..9ff057ea7 100644
--- a/tools/webide/packages/client/src/components/form/inputs.tsx
+++ b/tools/webide/packages/client/src/components/form/inputs.tsx
@@ -4,17 +4,20 @@ import styled from 'styled-components';
export const Group = styled.div`
display: flex;
flex-direction: column;
+ margin: 0.7em 0 0.7em 0;
`;
export const HGroup = styled.div`
display: flex;
align-items: center;
+ margin: 0.4em 0 0.4em 0;
`;
export const Label = styled.label`
font-size: 1em;
color: var(--label_foreground);
user-select: none;
+ margin: 0.3em 0 0.3em 0;
`;
export const Hint = styled.span`
@@ -25,7 +28,7 @@ export const Hint = styled.span`
export const AccessFunctionLabel = (props: any) => {
return (
@@ -33,7 +36,7 @@ export const AccessFunctionLabel = (props: any) => {
};
export const Input = styled.input`
- margin: 0.3em 0 0.7em 0;
+ /* margin: 0.3em 0 0.7em 0; */
background-color: var(--input_background);
border-style: none;
border-bottom: 5px solid #e1f1ff;
@@ -49,7 +52,7 @@ export const Input = styled.input`
export const Textarea = styled.textarea`
resize: vertical;
- margin: 0.3em 0 0.7em 0;
+ /* margin: 0.3em 0 0.7em 0; */
background-color: var(--input_background);
border-style: none;
border-bottom: 5px solid #e1f1ff;
diff --git a/tools/webide/packages/client/src/components/configure/command-select.tsx b/tools/webide/packages/client/src/components/form/select.tsx
similarity index 52%
rename from tools/webide/packages/client/src/components/configure/command-select.tsx
rename to tools/webide/packages/client/src/components/form/select.tsx
index 43711dfd3..b6b9749f5 100644
--- a/tools/webide/packages/client/src/components/configure/command-select.tsx
+++ b/tools/webide/packages/client/src/components/form/select.tsx
@@ -1,17 +1,13 @@
import { faCaretDown } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import React, { useState } from 'react';
+import React, { FunctionComponentElement, useState } from 'react';
import OutsideClickHandler from 'react-outside-click-handler';
import styled, { css } from 'styled-components';
-import { Command } from '../../redux/types';
-
const Container = styled.div`
- flex: 2;
display: flex;
position: relative;
min-width: 8em;
- z-index: 2;
`;
const Header = styled.div`
@@ -20,8 +16,8 @@ const Header = styled.div`
flex: 1;
display: flex;
- align-items: center;
justify-content: space-between;
+ align-items: center;
min-height: 2em;
padding: 0 0.5em;
@@ -33,6 +29,7 @@ const ArrowIcon = ({ rotate, ...props }: { rotate: boolean }) => (
);
const Arrow = styled(ArrowIcon)`
+ z-index: 1;
pointer-events: none;
color: var(--blue_trans1);
transition: transform 0.15s ease-in;
@@ -45,6 +42,7 @@ const Arrow = styled(ArrowIcon)`
`;
const List = styled.ul`
+ z-index: 1;
position: absolute;
list-style-type: none;
background-color: white;
@@ -66,7 +64,7 @@ const List = styled.ul`
`}
`;
-const Option = styled.li`
+const OptionContainer = styled.li`
cursor: pointer;
user-select: none;
@@ -90,56 +88,73 @@ const Option = styled.li`
}
`;
-export const CommandSelectComponent = (props: {
- selected: Command;
- onChange?: (value: Command) => void;
-}) => {
- const OPTIONS = {
- [Command.Compile]: 'Compile',
- [Command.Deploy]: 'Deploy',
- [Command.DryRun]: 'Dry Run',
- [Command.EvaluateFunction]: 'Evaluate Function',
- [Command.EvaluateValue]: 'Evaluate Value'
- };
+interface OptionProps {
+ value: string;
+ children: string;
+}
- const moveOptionToTop = (option: Command) => {
- return Object.keys(OPTIONS).reduce((list, entry) => {
- if (entry === option) {
+type OptionElement = FunctionComponentElement;
+
+export const Option = (props: OptionProps) => {
+ // This is an empty component. It's used as a way to get option information into its parent. It is not inserted into the DOM.
+ return <>>;
+};
+
+export const Select = (props: {
+ id: string;
+ value: any;
+ children: OptionElement[] | OptionElement;
+ onChange?: (value: any) => void;
+ className?: string;
+}) => {
+ const [isOpen, setOpen] = useState(false);
+
+ const options = Array.isArray(props.children)
+ ? props.children
+ : [props.children];
+
+ const labelLookup = new Map(
+ options.map(
+ child => [child.props.value, child.props.children] as [string, string]
+ )
+ );
+
+ const moveOptionToTop = (value: string) => {
+ return options.reduce((list, entry) => {
+ if (entry.props.value === value) {
list.unshift(entry);
} else {
- list.push(entry as Command);
+ list.push(entry);
}
return list;
- }, [] as Command[]);
+ }, [] as OptionElement[]);
};
- const [opened, open] = useState(false);
-
- const selectOption = (option: Command) => {
- if (props.selected !== option && props.onChange) {
- props.onChange(option);
+ const selectOption = (option: OptionElement) => {
+ if (props.value !== option.props.value && props.onChange) {
+ props.onChange(option.props.value);
}
- open(false);
+ setOpen(false);
};
return (
-
- open(false)}>
-
- {moveOptionToTop(props.selected).map(option => (
-
+ {option.props.children}
+
))}
- open(true)}>
- {OPTIONS[props.selected]}
-
+ setOpen(true)}>
+ {labelLookup.get(props.value)}
+
);
diff --git a/tools/webide/packages/client/src/components/form/toggle.tsx b/tools/webide/packages/client/src/components/form/toggle.tsx
deleted file mode 100644
index a1a0b3954..000000000
--- a/tools/webide/packages/client/src/components/form/toggle.tsx
+++ /dev/null
@@ -1,83 +0,0 @@
-import { faCheck } from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import React, { useState } from 'react';
-import styled, { css } from 'styled-components';
-
-const Container = styled.div<{ checked: boolean }>`
- position: relative;
- height: 2em;
- width: 3.5em;
- border-radius: 1em;
- background-color: var(--blue_trans1);
- border: 1px solid var(--blue);
- transition: background-color 0.2s ease-in;
-
- ${props =>
- props.checked &&
- css`
- background-color: var(--blue);
- `};
-`;
-
-const Button = styled.div<{ checked: boolean }>`
- display: flex;
- justify-content: center;
- align-items: center;
- position: absolute;
-
- height: 2em;
- width: 2em;
- background-color: white;
- border-radius: 50%;
- cursor: pointer;
- right: calc(1.5em);
- transition: right 0.2s ease-in;
-
- ${props =>
- props.checked &&
- css`
- right: 0;
- `};
-`;
-
-const CheckIcon = ({ visible, ...props }: { visible: boolean }) => (
-
-);
-
-const Check = styled(CheckIcon)`
- position: absolute;
- pointer-events: none;
- opacity: 1;
- transition: opacity 0.2s ease-in;
- color: var(--blue);
-
- ${props =>
- !props.visible &&
- css`
- opacity: 0;
- `}
-`;
-
-export const ToggleComponent = (props: {
- checked: boolean;
- onChanged: (value: boolean) => void;
- className?: string;
-}) => {
- const [isChecked, setChecked] = useState(props.checked);
-
- return (
-
-
-
- );
-};
diff --git a/tools/webide/packages/client/src/components/output/compile-output-pane.tsx b/tools/webide/packages/client/src/components/output/compile-output-pane.tsx
index 110f9f54e..057892d6d 100644
--- a/tools/webide/packages/client/src/components/output/compile-output-pane.tsx
+++ b/tools/webide/packages/client/src/components/output/compile-output-pane.tsx
@@ -5,6 +5,7 @@ import styled from 'styled-components';
import { AppState } from '../../redux/app';
import { ResultState } from '../../redux/result';
import { OutputToolbarComponent } from './output-toolbar';
+import { copyOutput, downloadOutput } from './utils';
const Container = styled.div<{ visible?: boolean }>`
display: flex;
@@ -23,35 +24,6 @@ const Pre = styled.pre`
margin: 0;
`;
-function copyOutput(el: HTMLElement | null) {
- if (el) {
- const range = document.createRange();
- range.selectNodeContents(el);
-
- const selection = window.getSelection();
-
- if (selection) {
- selection.removeAllRanges();
- selection.addRange(range);
- document.execCommand('copy');
- }
- }
-}
-
-function downloadOutput(output: string) {
- const anchor = document.createElement('a');
- anchor.setAttribute(
- 'href',
- `data:text/plain;charset=utf-8,${encodeURIComponent(output)}`
- );
- anchor.setAttribute('download', 'output.txt');
-
- anchor.style.display = 'none';
- document.body.appendChild(anchor);
- anchor.click();
- document.body.removeChild(anchor);
-}
-
export const CompileOutputPane = () => {
const output = useSelector(
state => state.result.output
@@ -62,6 +34,7 @@ export const CompileOutputPane = () => {
return (
copyOutput(preRef.current)}
onDownload={() => downloadOutput(output)}
>
diff --git a/tools/webide/packages/client/src/components/output/generate-command-output-pane.tsx b/tools/webide/packages/client/src/components/output/generate-command-output-pane.tsx
new file mode 100644
index 000000000..5dd714375
--- /dev/null
+++ b/tools/webide/packages/client/src/components/output/generate-command-output-pane.tsx
@@ -0,0 +1,45 @@
+import React, { useRef } from 'react';
+import { useSelector } from 'react-redux';
+import styled from 'styled-components';
+
+import { AppState } from '../../redux/app';
+import { ResultState } from '../../redux/result';
+import { OutputToolbarComponent } from './output-toolbar';
+import { copyOutput, downloadOutput } from './utils';
+
+const Container = styled.div<{ visible?: boolean }>`
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+`;
+
+const Output = styled.div`
+ flex: 1;
+ padding: 0.5em;
+ display: flex;
+ overflow: auto;
+`;
+
+const Pre = styled.pre`
+ margin: 0;
+`;
+
+export const GenerateCommandOutputPane = () => {
+ const output = useSelector(
+ state => state.result.output
+ );
+
+ const preRef = useRef(null);
+
+ return (
+
+ copyOutput(preRef.current)}
+ onDownload={() => downloadOutput(output)}
+ >
+
+
+ );
+};
diff --git a/tools/webide/packages/client/src/components/output/output-tab.tsx b/tools/webide/packages/client/src/components/output/output-tab.tsx
index 1f838aabf..5989b9f0e 100644
--- a/tools/webide/packages/client/src/components/output/output-tab.tsx
+++ b/tools/webide/packages/client/src/components/output/output-tab.tsx
@@ -8,6 +8,7 @@ import { ResultState } from '../../redux/result';
import { Command } from '../../redux/types';
import { CompileOutputPane } from './compile-output-pane';
import { DeployOutputPane } from './deploy-output-pane';
+import { GenerateCommandOutputPane } from './generate-command-output-pane';
import { Loading } from './loading';
import { OutputPane } from './output-pane';
@@ -41,14 +42,21 @@ export const OutputTab = (props: {
const loading = useSelector(
state => state.loading.loading
);
+ const output = useSelector(
+ state => state.result.output
+ );
const renderResult = () => {
if (loading) {
return ;
+ } else if (!output) {
+ return <>>;
} else if (command === Command.Compile) {
return ;
} else if (command === Command.Deploy) {
return ;
+ } else if (command === Command.GenerateCommand) {
+ return ;
}
return ;
diff --git a/tools/webide/packages/client/src/components/output/output-toolbar.tsx b/tools/webide/packages/client/src/components/output/output-toolbar.tsx
index 0606b8d7a..3c064399b 100644
--- a/tools/webide/packages/client/src/components/output/output-toolbar.tsx
+++ b/tools/webide/packages/client/src/components/output/output-toolbar.tsx
@@ -24,6 +24,7 @@ const Link = styled.a`
`;
export const OutputToolbarComponent = (props: {
+ showTryMichelson?: boolean;
onCopy?: () => void;
onDownload?: () => void;
}) => {
@@ -41,18 +42,20 @@ export const OutputToolbarComponent = (props: {
Download
-
- -
-
- View in Try-Michelson IDE
-
-
+ {props.showTryMichelson && }
+ {props.showTryMichelson && (
+ -
+
+ View in Try-Michelson IDE
+
+
+ )}
);
};
diff --git a/tools/webide/packages/client/src/components/output/utils.ts b/tools/webide/packages/client/src/components/output/utils.ts
new file mode 100644
index 000000000..79a5ede41
--- /dev/null
+++ b/tools/webide/packages/client/src/components/output/utils.ts
@@ -0,0 +1,28 @@
+export function copyOutput(el: HTMLElement | null) {
+ if (el) {
+ const range = document.createRange();
+ range.selectNodeContents(el);
+
+ const selection = window.getSelection();
+
+ if (selection) {
+ selection.removeAllRanges();
+ selection.addRange(range);
+ document.execCommand('copy');
+ }
+ }
+}
+
+export function downloadOutput(output: string) {
+ const anchor = document.createElement('a');
+ anchor.setAttribute(
+ 'href',
+ `data:text/plain;charset=utf-8,${encodeURIComponent(output)}`
+ );
+ anchor.setAttribute('download', 'output.txt');
+
+ anchor.style.display = 'none';
+ document.body.appendChild(anchor);
+ anchor.click();
+ document.body.removeChild(anchor);
+}
diff --git a/tools/webide/packages/client/src/redux/actions/generate-command.ts b/tools/webide/packages/client/src/redux/actions/generate-command.ts
new file mode 100644
index 000000000..f5e2847e4
--- /dev/null
+++ b/tools/webide/packages/client/src/redux/actions/generate-command.ts
@@ -0,0 +1,112 @@
+import { Tezos } from '@taquito/taquito';
+import { Dispatch } from 'redux';
+import slugify from 'slugify';
+
+import { compileContract, compileStorage, getErrorMessage } from '../../services/api';
+import { AppState } from '../app';
+import { MichelsonFormat } from '../compile';
+import { DoneLoadingAction, UpdateLoadingAction } from '../loading';
+import { ChangeOutputAction } from '../result';
+import { Command } from '../types';
+import { CancellableAction } from './cancellable';
+
+const URL = 'https://api.tez.ie/keys/carthagenet/';
+const AUTHORIZATION_HEADER = 'Bearer ligo-ide';
+
+export async function fetchRandomPrivateKey(): Promise {
+ const response = await fetch(URL, {
+ method: 'POST',
+ headers: { Authorization: AUTHORIZATION_HEADER }
+ });
+
+ return response.text();
+}
+
+export class GenerateCommandAction extends CancellableAction {
+ getAction() {
+ return async (dispatch: Dispatch, getState: () => AppState) => {
+ dispatch({ ...new UpdateLoadingAction('Compiling contract...') });
+
+ try {
+ const { editor, generateCommand } = getState();
+
+ const michelsonCodeJson = await compileContract(
+ editor.language,
+ editor.code,
+ generateCommand.entrypoint,
+ MichelsonFormat.Json
+ );
+
+ const michelsonCode = await compileContract(
+ editor.language,
+ editor.code,
+ generateCommand.entrypoint
+ );
+
+ if (this.isCancelled()) {
+ return;
+ }
+
+ dispatch({ ...new UpdateLoadingAction('Compiling storage...') });
+ const michelsonStorageJson = await compileStorage(
+ editor.language,
+ editor.code,
+ generateCommand.entrypoint,
+ generateCommand.storage,
+ MichelsonFormat.Json
+ );
+
+ const michelsonStorage = await compileStorage(
+ editor.language,
+ editor.code,
+ generateCommand.entrypoint,
+ generateCommand.storage
+ );
+
+ if (this.isCancelled()) {
+ return;
+ }
+
+ dispatch({ ...new UpdateLoadingAction('Estimating burn cap...') });
+
+ await Tezos.importKey(await fetchRandomPrivateKey());
+
+ const estimate = await Tezos.estimate.originate({
+ code: JSON.parse(michelsonCodeJson.result),
+ init: JSON.parse(michelsonStorageJson.result)
+ });
+
+ if (this.isCancelled()) {
+ return;
+ }
+
+ const title = slugify(editor.title).toLowerCase() || 'untitled';
+ const output = `tezos-client \\
+ ${generateCommand.command} \\
+ contract \\
+ ${title} \\
+ transferring 0 \\
+ from $YOUR_SOURCE_ACCOUNT \\
+ running '${michelsonCode.result.trim()}' \\
+ --init '${michelsonStorage.result.trim()}' \\
+ --burn-cap ${estimate.burnFeeMutez / 1000000}`;
+
+ dispatch({
+ ...new ChangeOutputAction(output, Command.GenerateCommand)
+ });
+ } catch (ex) {
+ if (this.isCancelled()) {
+ return;
+ }
+ dispatch({
+ ...new ChangeOutputAction(
+ `Error: ${getErrorMessage(ex)}`,
+ Command.GenerateCommand
+ )
+ });
+ }
+
+ dispatch({ ...new DoneLoadingAction() });
+ };
+ }
+}
diff --git a/tools/webide/packages/client/src/redux/app.ts b/tools/webide/packages/client/src/redux/app.ts
index e7829d8c9..304af9f9b 100644
--- a/tools/webide/packages/client/src/redux/app.ts
+++ b/tools/webide/packages/client/src/redux/app.ts
@@ -8,6 +8,7 @@ import editor, { EditorState } from './editor';
import evaluateFunction, { EvaluateFunctionState } from './evaluate-function';
import evaluateValue, { EvaluateValueState } from './evaluate-value';
import examples, { ExamplesState } from './examples';
+import generateCommand, { GenerateCommandState } from './generate-command';
import loading, { LoadingState } from './loading';
import result, { ResultState } from './result';
import share, { ShareState } from './share';
@@ -20,6 +21,7 @@ export interface AppState {
deploy: DeployState;
evaluateFunction: EvaluateFunctionState;
evaluateValue: EvaluateValueState;
+ generateCommand: GenerateCommandState;
result: ResultState;
command: CommandState;
examples: ExamplesState;
@@ -34,6 +36,7 @@ export default combineReducers({
deploy,
evaluateFunction,
evaluateValue,
+ generateCommand,
result,
command,
examples,
diff --git a/tools/webide/packages/client/src/redux/generate-command.ts b/tools/webide/packages/client/src/redux/generate-command.ts
new file mode 100644
index 000000000..dadd00163
--- /dev/null
+++ b/tools/webide/packages/client/src/redux/generate-command.ts
@@ -0,0 +1,81 @@
+import { Tool, ToolCommand } from './types';
+
+export enum ActionType {
+ ChangeTool = 'generate-command-change-tool',
+ ChangeCommand = 'generate-command-change-command',
+ ChangeEntrypoint = 'generate-command-change-entrypoint',
+ ChangeStorage = 'generate-command-change-storage'
+}
+
+export interface GenerateCommandState {
+ tool: Tool;
+ command: ToolCommand;
+ entrypoint: string;
+ originationAccount: string;
+ storage: string;
+ burnCap: number;
+}
+
+export class ChangeToolAction {
+ public readonly type = ActionType.ChangeTool;
+ constructor(public payload: GenerateCommandState['tool']) {}
+}
+
+export class ChangeCommandAction {
+ public readonly type = ActionType.ChangeCommand;
+ constructor(public payload: GenerateCommandState['command']) {}
+}
+
+export class ChangeEntrypointAction {
+ public readonly type = ActionType.ChangeEntrypoint;
+ constructor(public payload: GenerateCommandState['entrypoint']) {}
+}
+
+export class ChangeStorageAction {
+ public readonly type = ActionType.ChangeStorage;
+ constructor(public payload: GenerateCommandState['storage']) {}
+}
+
+type Action =
+ | ChangeToolAction
+ | ChangeCommandAction
+ | ChangeEntrypointAction
+ | ChangeStorageAction;
+
+const DEFAULT_STATE: GenerateCommandState = {
+ tool: Tool.TezosClient,
+ command: ToolCommand.Originate,
+ entrypoint: '',
+ storage: '',
+ originationAccount: '',
+ burnCap: 0
+};
+
+export default (
+ state = DEFAULT_STATE,
+ action: Action
+): GenerateCommandState => {
+ switch (action.type) {
+ case ActionType.ChangeTool:
+ return {
+ ...state,
+ tool: action.payload
+ };
+ case ActionType.ChangeCommand:
+ return {
+ ...state,
+ command: action.payload
+ };
+ case ActionType.ChangeEntrypoint:
+ return {
+ ...state,
+ entrypoint: action.payload
+ };
+ case ActionType.ChangeStorage:
+ return {
+ ...state,
+ storage: action.payload
+ };
+ }
+ return state;
+};
diff --git a/tools/webide/packages/client/src/redux/types.ts b/tools/webide/packages/client/src/redux/types.ts
index 29484403f..3aab55c08 100644
--- a/tools/webide/packages/client/src/redux/types.ts
+++ b/tools/webide/packages/client/src/redux/types.ts
@@ -9,5 +9,14 @@ export enum Command {
DryRun = 'dry-run',
EvaluateValue = 'evaluate-value',
EvaluateFunction = 'evaluate-function',
- Deploy = 'deploy'
+ Deploy = 'deploy',
+ GenerateCommand = 'generate-command'
+}
+
+export enum Tool {
+ TezosClient = 'tezos-client'
+}
+
+export enum ToolCommand {
+ Originate = 'originate'
}
diff --git a/tools/webide/packages/client/src/services/api.ts b/tools/webide/packages/client/src/services/api.ts
index 39defca87..128c3ff94 100644
--- a/tools/webide/packages/client/src/services/api.ts
+++ b/tools/webide/packages/client/src/services/api.ts
@@ -43,6 +43,9 @@ export async function compileStorage(
storage: string,
format?: string
) {
+ // For whatever reason, storage set by examples is not treated as a string. So we convert it here.
+ storage = `${storage}`;
+
const response = await axios.post('/api/compile-storage', {
syntax,
code,
diff --git a/tools/webide/packages/server/src/handlers/compile-storage.ts b/tools/webide/packages/server/src/handlers/compile-storage.ts
new file mode 100644
index 000000000..f0cc1a12b
--- /dev/null
+++ b/tools/webide/packages/server/src/handlers/compile-storage.ts
@@ -0,0 +1,52 @@
+import joi from '@hapi/joi';
+import { Request, Response } from 'express';
+
+import { CompilerError, LigoCompiler } from '../ligo-compiler';
+import { logger } from '../logger';
+
+interface CompileBody {
+ syntax: string;
+ code: string;
+ entrypoint: string;
+ storage: string;
+ format?: string;
+}
+
+const validateRequest = (body: any): { value: CompileBody; error: any } => {
+ return joi
+ .object({
+ syntax: joi.string().required(),
+ code: joi.string().required(),
+ entrypoint: joi.string().required(),
+ storage: joi.string().required(),
+ format: joi.string().optional()
+ })
+ .validate(body);
+};
+
+export async function compileStorageHandler(req: Request, res: Response) {
+ const { error, value: body } = validateRequest(req.body);
+
+ if (error) {
+ res.status(400).json({ error: error.message });
+ } else {
+ try {
+ const michelsonStorage = await new LigoCompiler().compileStorage(
+ body.syntax,
+ body.code,
+ body.entrypoint,
+ body.format || 'text',
+ body.storage
+ );
+
+ res.send({ result: michelsonStorage });
+ } catch (ex) {
+ if (ex instanceof CompilerError) {
+ res.status(400).json({ error: ex.message });
+ } else {
+ logger.error(ex);
+ res.sendStatus(500);
+ }
+ }
+ }
+}
diff --git a/tools/webide/packages/server/src/index.ts b/tools/webide/packages/server/src/index.ts
index 642ec46f0..41e4678ba 100644
--- a/tools/webide/packages/server/src/index.ts
+++ b/tools/webide/packages/server/src/index.ts
@@ -4,6 +4,7 @@ import { dirname, join } from 'path';
import { compileContractHandler } from './handlers/compile-contract';
import { compileExpressionHandler } from './handlers/compile-expression';
+import { compileStorageHandler } from './handlers/compile-storage';
import { deployHandler } from './handlers/deploy';
import { dryRunHandler } from './handlers/dry-run';
import { evaluateValueHandler } from './handlers/evaluate-value';
@@ -51,6 +52,7 @@ app.get(
);
app.post('/api/compile-contract', compileContractHandler);
app.post('/api/compile-expression', compileExpressionHandler);
+app.post('/api/compile-storage', compileStorageHandler);
app.post('/api/dry-run', dryRunHandler);
app.post('/api/share', shareHandler);
app.post('/api/evaluate-value', evaluateValueHandler);
diff --git a/tools/webide/yarn.lock b/tools/webide/yarn.lock
index 9bc7ff8c1..c3d792c78 100644
--- a/tools/webide/yarn.lock
+++ b/tools/webide/yarn.lock
@@ -10706,6 +10706,11 @@ slice-ansi@^2.1.0:
astral-regex "^1.0.0"
is-fullwidth-code-point "^2.0.0"
+slugify@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.4.0.tgz#c9557c653c54b0c7f7a8e786ef3431add676d2cb"
+ integrity sha512-FtLNsMGBSRB/0JOE2A0fxlqjI6fJsgHGS13iTuVT28kViI4JjUiNqp/vyis0ZXYcMnpR3fzGNkv+6vRlI2GwdQ==
+
snakeize@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/snakeize/-/snakeize-0.1.0.tgz#10c088d8b58eb076b3229bb5a04e232ce126422d"