Merge branch 'improving-syntax-selection-ui' into 'dev'

Improving syntax selection UI

See merge request ligolang/ligo!445
This commit is contained in:
Jev Björsell 2020-02-24 19:29:11 +00:00
commit bbe7a29881
23 changed files with 407 additions and 165 deletions

View File

@ -1,5 +1,5 @@
(*_* (*_*
name: Cameligo Contract name: CameLIGO Contract
language: cameligo language: cameligo
compile: compile:
entrypoint: main entrypoint: main

View File

@ -1,5 +1,5 @@
(*_* (*_*
name: Pascaligo Contract name: PascaLIGO Contract
language: pascaligo language: pascaligo
compile: compile:
entrypoint: main entrypoint: main

View File

@ -1,5 +1,5 @@
(*_* (*_*
name: Reasonligo Contract name: ReasonLIGO Contract
language: reasonligo language: reasonligo
compile: compile:
entrypoint: main entrypoint: main

View File

@ -1,84 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
sodipodi:docname="ligo_run.svg"
id="svg4550"
version="1.1"
viewBox="0 0 193.35434 193.35434"
height="193.35434"
width="193.35434"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
style="fill:none">
<metadata
id="metadata4554">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2560"
inkscape:window-height="1400"
id="namedview4552"
showgrid="false"
inkscape:zoom="1.9624573"
inkscape:cx="24.54412"
inkscape:cy="104.17717"
inkscape:window-x="-12"
inkscape:window-y="-12"
inkscape:window-maximized="1"
inkscape:current-layer="svg4550"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0" />
<circle
cx="96.67717"
cy="96.67717"
r="74"
id="circle4541"
style="stroke:url(#paint0_linear);stroke-width:45.35433197;stroke-miterlimit:4;stroke-dasharray:none" />
<defs
id="defs4548">
<linearGradient
id="paint0_linear"
x1="100"
y1="54"
x2="100"
y2="254"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-3.322834,-57.322834)">
<stop
stop-color="#3AA0FF"
id="stop4543" />
<stop
offset="1"
stop-color="#0072DC"
id="stop4545" />
</linearGradient>
</defs>
<path
d="M 137.61977,80.627036 58.899178,68.296666 88.24752,141.00143 137.6208,80.624736 Z"
id="path4537"
inkscape:connector-curvature="0"
style="fill:#fc683a;stroke-width:2.53620434" />
</svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,121 @@
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import React, { useEffect, useRef, useState } from 'react';
import styled, { css } from 'styled-components';
import { Tooltip } from './tooltip';
const Container = styled.div`
display: flex;
align-items: center;
`;
const Input = styled.input<{ visible?: boolean }>`
position: absolute;
border-radius: var(--border_radius);
opacity: 0;
height: 2em;
width: 0;
border: none;
font-size: 1em;
outline: none;
z-index: 1;
${props =>
props.visible &&
css`
padding-left: 0.5em;
opacity: 1;
width: 15em;
`}
`;
const Label = styled.div<{ visible?: boolean }>`
color: var(--blue);
opacity: 0;
${props =>
props.visible &&
css`
opacity: 1;
`}
`;
const PencilIcon = ({ ...props }) => (
<FontAwesomeIcon {...props} icon={faPencilAlt} size="sm"></FontAwesomeIcon>
);
const Pencil = styled(PencilIcon)<{ visible: boolean }>`
margin-left: 10px;
cursor: pointer;
color: var(--label_foreground);
opacity: 0;
:hover {
opacity: 1;
}
${props =>
props.visible &&
css`
opacity: 0.5;
`}
`;
export const EditableTitleComponent = (props: {
id?: string;
title: string;
onChanged?: (value: string) => void;
className?: string;
}) => {
const [newTitle, setNewTitle] = useState(props.title);
const [showInput, setShowInput] = useState(false);
const inputEl = useRef<HTMLInputElement>(null);
const notifyChanged = () => {
if (props.onChanged && props.title !== newTitle) {
props.onChanged(newTitle);
}
setShowInput(false);
};
useEffect(() => {
setNewTitle(props.title);
}, [props.title]);
return (
<Container id={props.id} className={props.className}>
<Input
ref={inputEl}
visible={showInput}
value={newTitle}
onChange={event => setNewTitle(event.target.value)}
onBlur={_ => notifyChanged()}
onKeyDown={event => {
if (event.key === 'Enter') {
notifyChanged();
} else if (event.key === 'Escape') {
setNewTitle(props.title);
setShowInput(false);
}
}}
></Input>
<Label visible={!showInput}>{newTitle ? newTitle : 'Untitled'}</Label>
<div>
<Pencil
visible={!showInput}
onClick={() => {
if (inputEl.current) {
inputEl.current.select();
inputEl.current.setSelectionRange(0, 99999);
setShowInput(true);
}
}}
></Pencil>
<Tooltip>Rename</Tooltip>
</div>
</Container>
);
};

View File

@ -1,6 +1,10 @@
import React from 'react'; import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import styled from 'styled-components'; import styled from 'styled-components';
import { AppState } from '../redux/app';
import { ChangeTitleAction } from '../redux/editor';
import { EditableTitleComponent } from './editable-title';
import { MonacoComponent } from './monaco'; import { MonacoComponent } from './monaco';
import { ShareComponent } from './share'; import { ShareComponent } from './share';
import { SyntaxSelectComponent } from './syntax-select'; import { SyntaxSelectComponent } from './syntax-select';
@ -12,20 +16,44 @@ const Container = styled.div`
const Header = styled.div` const Header = styled.div`
flex: 1; flex: 1;
display: flex; display: flex;
justify-content: space-between; justify-content: flex-start;
align-items: center; align-items: center;
min-height: 2.5em; min-height: 2.5em;
border-bottom: 5px solid var(--blue_trans1); border-bottom: 5px solid var(--blue_trans1);
`; `;
const Subheader = styled.div`
flex: 1;
display: flex;
justify-content: space-between;
align-items: center;
background: var(--blue_trans1);
border: 5px solid rgba(0, 0, 0, 0);
border-top: none;
padding: 0 10px;
`;
export const EditorComponent = () => { export const EditorComponent = () => {
const dispatch = useDispatch();
const title = useSelector<AppState, string>(state => state.editor.title);
return ( return (
<Container> <Container>
<Header> <Header>
<SyntaxSelectComponent></SyntaxSelectComponent>
<ShareComponent></ShareComponent> <ShareComponent></ShareComponent>
</Header> </Header>
<Subheader>
<EditableTitleComponent
id="editor-title"
title={title}
onChanged={value => {
dispatch({ ...new ChangeTitleAction(value) });
}}
></EditableTitleComponent>
<SyntaxSelectComponent></SyntaxSelectComponent>
</Subheader>
<MonacoComponent></MonacoComponent> <MonacoComponent></MonacoComponent>
</Container> </Container>
); );

View File

@ -3,12 +3,13 @@ import { useDispatch, useSelector } from 'react-redux';
import styled from 'styled-components'; import styled from 'styled-components';
import { AppState } from '../redux/app'; import { AppState } from '../redux/app';
import { ChangeDirtyAction, EditorState } from '../redux/editor';
import { ChangeSelectedAction, ExamplesState } from '../redux/examples'; import { ChangeSelectedAction, ExamplesState } from '../redux/examples';
import { getExample } from '../services/api'; import { getExample } from '../services/api';
const bgColor = 'transparent'; const bgColor = 'transparent';
const borderSize = '5px'; const borderSize = '5px';
const verticalPadding = '0.8em'; const verticalPadding = '0.6em';
const Container = styled.div` const Container = styled.div`
flex: 0.5; flex: 0.5;
@ -16,7 +17,7 @@ const Container = styled.div`
flex-direction: column; flex-direction: column;
`; `;
const MenuItem = styled.div<{ selected: boolean }>` const MenuItem = styled.div<{ selected?: boolean }>`
padding: ${verticalPadding} 0 ${verticalPadding} 1em; padding: ${verticalPadding} 0 ${verticalPadding} 1em;
height: 1.5em; height: 1.5em;
display: flex; display: flex;
@ -27,25 +28,12 @@ const MenuItem = styled.div<{ selected: boolean }>`
border-left: ${`${borderSize} solid ${bgColor}`}; border-left: ${`${borderSize} solid ${bgColor}`};
border-left-color: ${props => (props.selected ? 'var(--blue)' : bgColor)}; border-left-color: ${props => (props.selected ? 'var(--blue)' : bgColor)};
:first-child {
margin-top: ${props => (props.selected ? '0' : `-${borderSize}`)};
}
:hover { :hover {
background-color: ${props => background-color: ${props =>
props.selected ? 'var(--blue_trans1)' : 'var(--blue_trans2)'}; props.selected ? 'var(--blue_trans1)' : 'var(--blue_trans2)'};
border-left: ${`${borderSize} solid ${bgColor}`}; border-left: ${`${borderSize} solid ${bgColor}`};
border-left-color: ${props => border-left-color: ${props =>
props.selected ? 'var(--blue)' : 'transparent'}; props.selected ? 'var(--blue)' : 'transparent'};
:first-child {
margin-top: ${props => (props.selected ? '0' : `-${borderSize}`)};
padding-top: ${props =>
props.selected
? `${verticalPadding}`
: `calc(${verticalPadding} - ${borderSize})`};
border-top: ${props =>
props.selected ? '' : `${borderSize} solid var(--blue_opaque1)`};
}
} }
`; `;
@ -57,44 +45,45 @@ const MenuContainer = styled.div`
box-sizing: border-box; box-sizing: border-box;
`; `;
const Header = styled.div<{ firstChildSelected: boolean }>` const Header = styled.div`
border-bottom: ${props =>
props.firstChildSelected ? '' : '5px solid var(--blue_trans1)'};
min-height: 2.5em; min-height: 2.5em;
padding: 0 10px; padding: 0 10px;
display: flex; display: flex;
align-items: center; align-items: center;
font-weight: 600;
`; `;
export const Examples = () => { export const Examples = () => {
const examples = useSelector<AppState, ExamplesState['list']>( const examples = useSelector<AppState, ExamplesState['list']>(
(state: AppState) => state.examples.list (state: AppState) => state.examples.list
); );
const selectedExample = useSelector<AppState, ExamplesState['selected']>( const editorDirty = useSelector<AppState, EditorState['dirty']>(
(state: AppState) => state.examples.selected (state: AppState) => state.editor.dirty
); );
const dispatch = useDispatch(); const dispatch = useDispatch();
return ( return (
<Container> <Container>
<Header <Header>Examples</Header>
firstChildSelected={
!!selectedExample && examples[0].id === selectedExample.id
}
>
<span>Examples</span>
</Header>
<MenuContainer> <MenuContainer>
{examples.map(example => { {examples.map(example => {
return ( return (
<MenuItem <MenuItem
id={example.id} id={example.id}
key={example.id} key={example.id}
selected={!!selectedExample && example.id === selectedExample.id}
onClick={async () => { onClick={async () => {
const response = await getExample(example.id); const response = await getExample(example.id);
dispatch({ ...new ChangeSelectedAction(response) }); if (
!editorDirty ||
window.confirm(
'Are you sure you want to navigate away? Data you have entered will be lost.\n\nPress OK to continue or Cancel to stay on the current page.\n\n'
)
) {
dispatch({ ...new ChangeSelectedAction(response) });
dispatch({ ...new ChangeDirtyAction(false) });
}
}} }}
> >
{example.name} {example.name}

View File

@ -6,9 +6,9 @@ const Container = styled.div`
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 0.5em 1em; padding: 0.2em 1em;
font-family: 'DM Sans', 'Open Sans', sans-serif; font-family: 'DM Sans', 'Open Sans', sans-serif;
box-shadow: 0px 2px 3px 0px rgba(0, 0, 0, 0.3); box-shadow: 0px 1px 1px 0px rgba(0, 0, 0, 0.3);
`; `;
const Group = styled.div` const Group = styled.div`

View File

@ -12,12 +12,12 @@ export const HGroup = styled.div`
export const Label = styled.label` export const Label = styled.label`
font-size: 1em; font-size: 1em;
color: rgba(153, 153, 153, 1); color: var(--label_foreground);
`; `;
export const Input = styled.input` export const Input = styled.input`
margin: 0.3em 0 0.7em 0; margin: 0.3em 0 0.7em 0;
background-color: #eff7ff; background-color: var(--input_background);
border-style: none; border-style: none;
border-bottom: 5px solid #e1f1ff; border-bottom: 5px solid #e1f1ff;
padding: 0.5em; padding: 0.5em;
@ -33,7 +33,7 @@ export const Input = styled.input`
export const Textarea = styled.textarea` export const Textarea = styled.textarea`
resize: vertical; resize: vertical;
margin: 0.3em 0 0.7em 0; margin: 0.3em 0 0.7em 0;
background-color: #eff7ff; background-color: var(--input_background);
border-style: none; border-style: none;
border-bottom: 5px solid #e1f1ff; border-bottom: 5px solid #e1f1ff;
padding: 0.5em; padding: 0.5em;

View File

@ -4,7 +4,8 @@ import { useDispatch, useStore } from 'react-redux';
import styled from 'styled-components'; import styled from 'styled-components';
import { AppState } from '../redux/app'; import { AppState } from '../redux/app';
import { ChangeCodeAction } from '../redux/editor'; import { ChangeCodeAction, ChangeDirtyAction } from '../redux/editor';
import { ClearSelectedAction } from '../redux/examples';
const Container = styled.div` const Container = styled.div`
height: var(--content_height); height: var(--content_height);
@ -53,8 +54,14 @@ export const MonacoComponent = () => {
} }
}); });
let shouldDispatchCodeChangedAction = true;
const { dispose } = editor.onDidChangeModelContent(() => { const { dispose } = editor.onDidChangeModelContent(() => {
dispatch({ ...new ChangeCodeAction(editor.getValue()) }); if (shouldDispatchCodeChangedAction) {
dispatch({ ...new ChangeCodeAction(editor.getValue()) });
dispatch({ ...new ChangeDirtyAction(true) });
dispatch({ ...new ClearSelectedAction() });
}
}); });
cleanupFunc.push(dispose); cleanupFunc.push(dispose);
@ -64,7 +71,9 @@ export const MonacoComponent = () => {
const { editor: editorState }: AppState = store.getState(); const { editor: editorState }: AppState = store.getState();
if (editorState.code !== editor.getValue()) { if (editorState.code !== editor.getValue()) {
shouldDispatchCodeChangedAction = false;
editor.setValue(editorState.code); editor.setValue(editorState.code);
shouldDispatchCodeChangedAction = true;
} }
if (editorState.language !== model.getModeId()) { if (editorState.language !== model.getModeId()) {

View File

@ -12,7 +12,7 @@ import { Tooltip } from './tooltip';
const Container = styled.div` const Container = styled.div`
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-start;
align-items: center; align-items: center;
`; `;
@ -81,9 +81,9 @@ const Input = styled.input<{ visible?: boolean }>`
opacity: 0; opacity: 0;
height: 2em; height: 2em;
width: 2em; width: 2em;
transform: translateX(-0.3em); transform: translateX(0.3em);
border: none; border: none;
padding: 0 1em; padding-left: 2em;
font-size: 1em; font-size: 1em;
color: white; color: white;

View File

@ -8,6 +8,7 @@ import styled, { css } from 'styled-components';
import { AppState } from '../redux/app'; import { AppState } from '../redux/app';
import { ChangeLanguageAction, EditorState } from '../redux/editor'; import { ChangeLanguageAction, EditorState } from '../redux/editor';
import { Language } from '../redux/types'; import { Language } from '../redux/types';
import { Tooltip } from './tooltip';
const Container = styled.div` const Container = styled.div`
display: flex; display: flex;
@ -22,14 +23,19 @@ const Header = styled.div`
flex: 1; flex: 1;
display: flex; display: flex;
align-items: center;
justify-content: space-between;
height: 2em; height: 2em;
padding: 0 0.5em; padding: 0 0.5em;
border: 1px solid var(--blue_trans1); 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 ArrowIcon = ({ rotate, ...props }: { rotate: boolean }) => (
<FontAwesomeIcon {...props} icon={faCaretDown} size="lg"></FontAwesomeIcon> <FontAwesomeIcon {...props} icon={faCaretDown} size="lg"></FontAwesomeIcon>
); );
@ -140,8 +146,11 @@ export const SyntaxSelectComponent = () => {
</List> </List>
</OutsideClickHandler> </OutsideClickHandler>
<Header id="syntax-select" onClick={() => open(true)}> <Header id="syntax-select" onClick={() => open(true)}>
<span>{OPTIONS[language]}</span> <Label>
<Arrow rotate={opened}></Arrow> {OPTIONS[language]}
<Arrow rotate={opened}></Arrow>
</Label>
<Tooltip>Select syntax</Tooltip>
</Header> </Header>
</Container> </Container>
); );

View File

@ -1,4 +1,4 @@
import React, { createElement, useEffect, useRef, useState } from 'react'; import React, { createElement, useCallback, useEffect, useRef, useState } from 'react';
import { render } from 'react-dom'; import { render } from 'react-dom';
import styled from 'styled-components'; import styled from 'styled-components';
@ -64,19 +64,22 @@ export const Tooltip = (props: { position?: Position; children: any }) => {
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const [isTooltipVisible, setTooltipVisible] = useState(false); const [isTooltipVisible, setTooltipVisible] = useState(false);
const renderTooltip = (visible: boolean, triggerRect: ClientRect) => { const renderTooltip = useCallback(
const tooltip = createElement( (visible: boolean, triggerRect: ClientRect) => {
StyledTooltip, const tooltip = createElement(
{ StyledTooltip,
visible, {
x: calcX(triggerRect, props.position), visible,
y: calcY(triggerRect, props.position) x: calcX(triggerRect, props.position),
}, y: calcY(triggerRect, props.position)
props.children },
); props.children
);
render(tooltip, document.getElementById(TOOLTIP_CONTAINER_ID)); render(tooltip, document.getElementById(TOOLTIP_CONTAINER_ID));
}; },
[props.position, props.children]
);
useEffect(() => { useEffect(() => {
if (ref.current) { if (ref.current) {
@ -98,7 +101,7 @@ export const Tooltip = (props: { position?: Position; children: any }) => {
}; };
} }
} }
}); }, [isTooltipVisible, renderTooltip]);
return <div ref={ref}></div>; return <div ref={ref}></div>;
}; };

View File

@ -10,6 +10,8 @@
--blue_opaque1: #dbeaff; --blue_opaque1: #dbeaff;
--blue_trans2: rgba(14, 116, 255, 0.08); /* #eff7ff; */ --blue_trans2: rgba(14, 116, 255, 0.08); /* #eff7ff; */
--input_background: #eff7ff;
--grey: #888; --grey: #888;
--box-shadow: 1px 3px 10px 0px rgba(153, 153, 153, 0.4); /* or #999999 */ --box-shadow: 1px 3px 10px 0px rgba(153, 153, 153, 0.4); /* or #999999 */
@ -46,7 +48,7 @@
--font_ghost_weight: 700; --font_ghost_weight: 700;
--font_ghost_color: rgba(153, 153, 153, 0.5); /* or #CFCFCF */ --font_ghost_color: rgba(153, 153, 153, 0.5); /* or #CFCFCF */
--content_height: 85vh; --content_height: 80vh;
--tooltip_foreground: white; --tooltip_foreground: white;
--tooltip_background: rgba(0, 0, 0, 0.75) /*#404040*/; --tooltip_background: rgba(0, 0, 0, 0.75) /*#404040*/;

View File

@ -3,12 +3,16 @@ import { Language } from './types';
export enum ActionType { export enum ActionType {
ChangeLanguage = 'editor-change-language', ChangeLanguage = 'editor-change-language',
ChangeCode = 'editor-change-code' ChangeCode = 'editor-change-code',
ChangeDirty = 'editor-change-dirty',
ChangeTitle = 'editor-change-title'
} }
export interface EditorState { export interface EditorState {
language: Language; language: Language;
code: string; code: string;
title: string;
dirty: boolean;
} }
export class ChangeLanguageAction { export class ChangeLanguageAction {
@ -21,14 +25,28 @@ export class ChangeCodeAction {
constructor(public payload: EditorState['code']) {} constructor(public payload: EditorState['code']) {}
} }
export class ChangeDirtyAction {
public readonly type = ActionType.ChangeDirty;
constructor(public payload: EditorState['dirty']) {}
}
export class ChangeTitleAction {
public readonly type = ActionType.ChangeTitle;
constructor(public payload: EditorState['title']) {}
}
type Action = type Action =
| ChangeCodeAction | ChangeCodeAction
| ChangeLanguageAction | ChangeLanguageAction
| ChangeDirtyAction
| ChangeTitleAction
| ChangeSelectedExampleAction; | ChangeSelectedExampleAction;
const DEFAULT_STATE: EditorState = { const DEFAULT_STATE: EditorState = {
language: Language.CameLigo, language: Language.CameLigo,
code: '' code: '',
title: '',
dirty: false
}; };
export default (state = DEFAULT_STATE, action: Action): EditorState => { export default (state = DEFAULT_STATE, action: Action): EditorState => {
@ -36,7 +54,9 @@ export default (state = DEFAULT_STATE, action: Action): EditorState => {
case ExamplesActionType.ChangeSelected: case ExamplesActionType.ChangeSelected:
return { return {
...state, ...state,
...(!action.payload ? DEFAULT_STATE : action.payload.editor) ...(!action.payload
? DEFAULT_STATE
: { ...action.payload.editor, title: action.payload.name })
}; };
case ActionType.ChangeLanguage: case ActionType.ChangeLanguage:
return { return {
@ -48,6 +68,17 @@ export default (state = DEFAULT_STATE, action: Action): EditorState => {
...state, ...state,
code: action.payload code: action.payload
}; };
case ActionType.ChangeDirty:
return {
...state,
dirty: action.payload
};
case ActionType.ChangeTitle:
return {
...state,
title: action.payload
};
default:
return state;
} }
return state;
}; };

View File

@ -1,7 +1,8 @@
import { ExampleState } from './example'; import { ExampleState } from './example';
export enum ActionType { export enum ActionType {
ChangeSelected = 'examples-change-selected' ChangeSelected = 'examples-change-selected',
ClearSelected = 'examples-clear-selected'
} }
export interface ExampleItem { export interface ExampleItem {
@ -19,13 +20,25 @@ export class ChangeSelectedAction {
constructor(public payload: ExamplesState['selected']) {} constructor(public payload: ExamplesState['selected']) {}
} }
type Action = ChangeSelectedAction; export class ClearSelectedAction {
public readonly type = ActionType.ClearSelected;
}
type Action = ChangeSelectedAction | ClearSelectedAction;
export const DEFAULT_STATE: ExamplesState = { export const DEFAULT_STATE: ExamplesState = {
selected: null, selected: null,
list: [] list: []
}; };
if (process.env.NODE_ENV === 'development') {
DEFAULT_STATE.list = [
{ id: 'MzkMQ1oiVHJqbcfUuVFKTw', name: 'CameLIGO Contract' },
{ id: 'FEb62HL7onjg1424eUsGSg', name: 'PascaLIGO Contract' },
{ id: 'JPhSOehj_2MFwRIlml0ymQ', name: 'ReasonLIGO Contract' }
];
}
export default (state = DEFAULT_STATE, action: Action): ExamplesState => { export default (state = DEFAULT_STATE, action: Action): ExamplesState => {
switch (action.type) { switch (action.type) {
case ActionType.ChangeSelected: case ActionType.ChangeSelected:
@ -33,6 +46,12 @@ export default (state = DEFAULT_STATE, action: Action): ExamplesState => {
...state, ...state,
selected: action.payload selected: action.payload
}; };
case ActionType.ClearSelected:
return {
...state,
selected: null
};
default:
return state;
} }
return state;
}; };

View File

@ -15,7 +15,7 @@ import {
ChangeParametersAction as ChangeDryRunParametersAction, ChangeParametersAction as ChangeDryRunParametersAction,
ChangeStorageAction as ChangeDryRunStorageAction, ChangeStorageAction as ChangeDryRunStorageAction,
} from './dry-run'; } from './dry-run';
import { ActionType as EditorActionType, ChangeCodeAction, ChangeLanguageAction } from './editor'; import { ActionType as EditorActionType, ChangeCodeAction, ChangeLanguageAction, ChangeTitleAction } from './editor';
import { import {
ActionType as EvaluateFunctionActionType, ActionType as EvaluateFunctionActionType,
ChangeEntrypointAction as ChangeEvaluateFunctionEntrypointAction, ChangeEntrypointAction as ChangeEvaluateFunctionEntrypointAction,
@ -25,6 +25,7 @@ import {
ActionType as EvaluateValueActionType, ActionType as EvaluateValueActionType,
ChangeEntrypointAction as ChangeEvaluateValueEntrypointAction, ChangeEntrypointAction as ChangeEvaluateValueEntrypointAction,
} from './evaluate-value'; } from './evaluate-value';
import { ActionType as ExamplesActionType, ChangeSelectedAction as ChangeSelectedExampleAction } from './examples';
export enum ActionType { export enum ActionType {
ChangeShareLink = 'share-change-link' ChangeShareLink = 'share-change-link'
@ -41,6 +42,7 @@ export class ChangeShareLinkAction {
type Action = type Action =
| ChangeShareLinkAction | ChangeShareLinkAction
| ChangeTitleAction
| ChangeCodeAction | ChangeCodeAction
| ChangeLanguageAction | ChangeLanguageAction
| ChangeCompileEntrypointAction | ChangeCompileEntrypointAction
@ -53,7 +55,8 @@ type Action =
| ChangeDryRunStorageAction | ChangeDryRunStorageAction
| ChangeEvaluateFunctionEntrypointAction | ChangeEvaluateFunctionEntrypointAction
| ChangeEvaluateFunctionParametersAction | ChangeEvaluateFunctionParametersAction
| ChangeEvaluateValueEntrypointAction; | ChangeEvaluateValueEntrypointAction
| ChangeSelectedExampleAction;
const DEFAULT_STATE: ShareState = { const DEFAULT_STATE: ShareState = {
link: '' link: ''
@ -61,6 +64,8 @@ const DEFAULT_STATE: ShareState = {
export default (state = DEFAULT_STATE, action: Action): ShareState => { export default (state = DEFAULT_STATE, action: Action): ShareState => {
switch (action.type) { switch (action.type) {
case EditorActionType.ChangeTitle:
case ExamplesActionType.ChangeSelected:
case EditorActionType.ChangeCode: case EditorActionType.ChangeCode:
case EditorActionType.ChangeLanguage: case EditorActionType.ChangeLanguage:
case CompileActionType.ChangeEntrypoint: case CompileActionType.ChangeEntrypoint:

View File

@ -1,6 +1,6 @@
const proxy = require('http-proxy-middleware'); const proxy = require('http-proxy-middleware');
module.exports = function(app) { module.exports = function (app) {
app.use( app.use(
'/api', '/api',
proxy({ proxy({
@ -8,4 +8,12 @@ module.exports = function(app) {
changeOrigin: true changeOrigin: true
}) })
); );
app.use(
'/static/examples',
proxy({
target: 'http://localhost:8080',
changeOrigin: true
})
);
}; };

View File

@ -24,7 +24,7 @@ describe('Share', () => {
await responseCallback; await responseCallback;
const actualShareLink = await page.evaluate(getInputValue, 'share-link'); const actualShareLink = await page.evaluate(getInputValue, 'share-link');
const expectedShareLink = `${API_HOST}/p/sIpy-2D9ExpCojwuBNw_-g`; const expectedShareLink = `${API_HOST}/p/WxKPBq9-mkZ_kq4cMHXfCQ`;
expect(actualShareLink).toEqual(expectedShareLink); expect(actualShareLink).toEqual(expectedShareLink);
done(); done();

View File

@ -10,6 +10,8 @@ interface ShareBody {
editor: { editor: {
language: string; language: string;
code: string; code: string;
dirty: boolean;
title: string;
}; };
compile: { compile: {
entrypoint: string; entrypoint: string;
@ -39,7 +41,9 @@ const validateRequest = (body: any): { value: ShareBody; error: any } => {
editor: joi editor: joi
.object({ .object({
language: joi.string().required(), language: joi.string().required(),
code: joi.string().required() code: joi.string().required(),
dirty: joi.boolean().optional(),
title: joi.string().allow('')
}) })
.required(), .required(),
compile: joi.object({ compile: joi.object({

View File

@ -24,7 +24,9 @@ export async function loadDefaultState(appBundleDirectory: string) {
deploy: {}, deploy: {},
evaluateValue: {}, evaluateValue: {},
evaluateFunction: {}, evaluateFunction: {},
editor: {}, editor: {
title: ''
},
examples: { examples: {
selected: null, selected: null,
list: examplesList list: examplesList
@ -59,9 +61,9 @@ export async function loadDefaultState(appBundleDirectory: string) {
}; };
defaultState.editor = { defaultState.editor = {
...defaultState.editor, ...defaultState.editor,
...defaultExample.editor ...defaultExample.editor,
title: defaultExample.name
}; };
defaultState.examples.selected = defaultExample;
} }
return defaultState; return defaultState;

View File

@ -1,3 +1,3 @@
import { SchemaMigrationV1 } from './share-v1'; import { SchemaMigrationV2 } from './share-v2';
export default new SchemaMigrationV1(); export default new SchemaMigrationV2();

View File

@ -0,0 +1,96 @@
import joi from '@hapi/joi';
import { Migration } from './migration';
import { SchemaMigrationV1, SchemaV1 } from './share-v1';
export type Version = 'v2';
export interface SchemaV2 {
version: Version;
state: {
editor: {
language: string;
code: string;
dirty: boolean;
title: string;
};
compile: {
entrypoint: string;
};
dryRun: {
entrypoint: string;
parameters: string;
storage: string;
};
deploy: {
entrypoint: string;
storage: string;
useTezBridge?: boolean;
};
evaluateValue: {
entrypoint: string;
};
evaluateFunction: {
entrypoint: string;
parameters: string;
};
};
}
export class SchemaMigrationV2 extends Migration {
readonly VERSION: Version = 'v2';
protected readonly schema = joi.object({
version: joi
.string()
.required()
.allow(this.VERSION),
state: joi.object({
editor: joi
.object({
language: joi.string().required(),
code: joi.string().required(),
dirty: joi.boolean().optional(),
title: joi.string().allow('')
})
.required(),
compile: joi.object({
entrypoint: joi.string().allow('')
}),
dryRun: joi.object({
entrypoint: joi.string().allow(''),
parameters: joi.any().allow(''),
storage: joi.any().allow('')
}),
deploy: joi.object({
entrypoint: joi.string().allow(''),
storage: joi.any().allow(''),
useTezBridge: joi.boolean().optional()
}),
evaluateValue: joi.object({
entrypoint: joi.string().allow('')
}),
evaluateFunction: joi.object({
entrypoint: joi.string().allow(''),
parameters: joi.any().allow('')
})
})
});
protected readonly previous = new SchemaMigrationV1();
protected migrate(data: SchemaV1): SchemaV2 {
return {
...data,
version: this.VERSION,
state: {
...data.state,
editor: {
...data.state.editor,
dirty: false,
title: ''
}
}
};
}
}