Editor for custom types and ActionStatus (#794)

* Added code editor to developer settings

* Added code editor to developer settings

* Added ActionStatus on save

* Added translate to new strings

* tslint fixes

* Valid status message

* Added styling, an error class and a description to Editor component

* Updated yarn.lock

* Include Editor from ui-app

* Added invalid prop to Ediitor component

* use classes util instead of classnames module

* Move settings tab content to seperate components

* Finalized validation logic

* reset placeholder

* removed leftover code comment

* remove comsole.log

* code cleanup
This commit is contained in:
Stefanie Doll 2019-03-12 12:04:27 +01:00 committed by GitHub
parent 4ce1a3678d
commit ee968a59c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 1150 additions and 726 deletions

View File

@ -0,0 +1,185 @@
// Copyright 2017-2019 @polkadot/app-settings authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { AppProps, I18nProps } from '@polkadot/ui-app/types';
import React from 'react';
import store from 'store';
import { getTypeRegistry } from '@polkadot/types';
import { Button, Editor, InputFile } from '@polkadot/ui-app/index';
import { ActionStatus } from '@polkadot/ui-app/Status/types';
import { stringToU8a, u8aToString } from '@polkadot/util';
import translate from './translate';
type Props = AppProps & I18nProps & {
onStatusChange: (status: ActionStatus) => void
};
type State = {
code: string,
isJsonValid: boolean,
isTypesValid: boolean,
types?: { [index: string]: any } | null,
typesPlaceholder?: string
};
class Developer extends React.PureComponent<Props, State> {
private defaultCode: string = `{\n\n}`;
constructor (props: Props) {
super(props);
const types = store.get('types') || {};
const names = Object.keys(types);
this.state = {
code: Object.keys(types).length ? JSON.stringify(types, null, 2) : this.defaultCode,
isJsonValid: true,
isTypesValid: true,
types: names.length ? types : null,
typesPlaceholder: names.length
? names.join(', ')
: undefined
};
}
render () {
const { t } = this.props;
const { code, isJsonValid, isTypesValid, types, typesPlaceholder } = this.state;
return (
<div className='settings-Developer'>
<div className='ui--row'>
<div className='full'>
<InputFile
clearContent={!types && isTypesValid}
isError={!isTypesValid}
label={t('Upload your additional type definitions as a JSON file')}
onChange={this.onChangeTypes}
placeholder={typesPlaceholder}
/>
</div>
</div>
<div className='ui--row'>
<div className='full'>
<div className='ui--Labelled'>
<label>{t('Manually enter your custom type definitions as valid JSON')}</label>
<div className='ui--Labelled-content'>
<Editor
className='editor'
code={code}
isValid={isJsonValid}
onEdit={this.onEditTypes}
/>
</div>
</div>
</div>
</div>
<Button.Group>
<Button
isDisabled={!types}
isNegative
onClick={this.clearTypes}
label={t('Reset')}
/>
<Button.Or />
<Button
isDisabled={!isTypesValid || !isJsonValid}
isPrimary
onClick={this.saveDeveloper}
label={t('Save')}
/>
</Button.Group>
</div>
);
}
private clearTypes = (): void => {
store.remove('types');
this.setState({
code: this.defaultCode,
isJsonValid: true,
isTypesValid: true,
types: null,
typesPlaceholder: undefined
});
}
private onChangeTypes = (data: Uint8Array) => {
const dataToString = u8aToString(data);
try {
// @TODO: Include isJsonObject function from https://github.com/polkadot-js/common/pull/359
const types = JSON.parse(dataToString);
const typesPlaceholder = Object.keys(types).join(', ');
console.log('Registering types:', typesPlaceholder);
getTypeRegistry().register(types);
this.setState({
code: dataToString,
isJsonValid: true,
isTypesValid: true,
types,
typesPlaceholder
});
} catch (error) {
console.error('Error registering types:', error);
this.setState({
code: dataToString,
isJsonValid: false,
isTypesValid: false,
types: null,
typesPlaceholder: error.message
});
}
}
private onEditTypes = (code: string): void => {
try {
// @TODO: Include isJsonObject function from https://github.com/polkadot-js/common/pull/359
JSON.parse(code);
this.setState({
code,
isJsonValid: true
} as State);
this.onChangeTypes(stringToU8a(code));
} catch (e) {
this.setState({
code,
isJsonValid: false,
typesPlaceholder: e.message
} as State);
}
}
private saveDeveloper = (): void => {
const { t } = this.props;
const { isTypesValid, types } = this.state;
const status = {
status: 'success',
action: t('Your custom types have been added')
} as ActionStatus;
if (isTypesValid) {
store.set('types', types);
} else {
status.status = 'error';
status.action = t('Your custom types are invalid');
}
this.props.onStatusChange(status);
}
}
export default translate(Developer);

View File

@ -0,0 +1,201 @@
// Copyright 2017-2019 @polkadot/app-settings authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { AppProps, I18nProps } from '@polkadot/ui-app/types';
import { SettingsStruct } from '@polkadot/ui-settings/types';
import React from 'react';
import { Tab } from 'semantic-ui-react';
import { Button, Dropdown, Input } from '@polkadot/ui-app/index';
import { ActionStatus } from '@polkadot/ui-app/Status/types';
import uiSettings from '@polkadot/ui-settings';
import './index.css';
import translate from './translate';
type Props = AppProps & I18nProps & {
onStatusChange: (status: ActionStatus) => void
};
type State = {
isCustomNode: boolean,
isUrlValid: boolean,
settings: SettingsStruct
};
class General extends React.PureComponent<Props, State> {
constructor (props: Props) {
super(props);
const settings = uiSettings.get();
let isCustomNode = true;
// check to see if user has saved a custom node by seeing if their URL is equal to any preset
for (let i = 0; i < uiSettings.availableNodes.length; i++) {
if (uiSettings.availableNodes[i].value === settings.apiUrl) {
isCustomNode = false;
}
}
this.state = {
isCustomNode,
isUrlValid: this.isValidUrl(settings.apiUrl),
settings
};
}
render () {
const { t } = this.props;
const { isUrlValid, settings: { i18nLang, uiMode, uiTheme } } = this.state;
return (
<div className='settings-General'>
<div className='ui--row'>
{this.renderEndpoint()}
</div>
<div className='ui--row'>
<div className='medium'>
<Dropdown
defaultValue={uiTheme}
label={t('default interface theme')}
onChange={this.onChangeUiTheme}
options={uiSettings.availableUIThemes}
/>
</div>
<div className='medium'>
<Dropdown
defaultValue={uiMode}
label={t('interface operation mode')}
onChange={this.onChangeUiMode}
options={uiSettings.availableUIModes}
/>
</div>
</div>
<div className='ui--row'>
<div className='full'>
<Dropdown
defaultValue={i18nLang}
isDisabled
label={t('default interface language')}
onChange={this.onChangeLang}
options={uiSettings.availableLanguages}
/>
</div>
</div>
<Button.Group>
<Button
isDisabled={!isUrlValid}
isPrimary
onClick={this.saveAndReload}
label={t('Save & Reload')}
/>
</Button.Group>
</div>
);
}
private renderEndpoint = () => {
const { t } = this.props;
const { isUrlValid, settings: { apiUrl } } = this.state;
const preset = (
<Dropdown
defaultValue={apiUrl}
label={t('remote node/endpoint to connect to')}
onChange={this.onChangeApiUrl}
options={uiSettings.availableNodes}
/>
);
const custom = (
<Input
defaultValue={apiUrl}
isError={!isUrlValid}
label={t('remote node/endpoint to connect to')}
onChange={this.onChangeApiUrl}
/>
);
const panes = [
{ menuItem: t('pre-set'), render: () => <Tab.Pane attached={true}>{preset}</Tab.Pane> },
{ menuItem: t('custom'), render: () => <Tab.Pane attached={true}>{custom}</Tab.Pane> }
];
return (
<Tab menu={{ secondary: true }} onTabChange={this.toggleCustomNode} panes={panes} />
);
}
private onChangeApiUrl = (apiUrl: string): void => {
this.setState(({ settings }: State) => ({
isUrlValid: this.isValidUrl(apiUrl),
settings: {
...settings,
apiUrl
}
}));
}
private onChangeLang = (i18nLang: string): void => {
// ignore (for now), here to future-proof
}
private onChangeUiMode = (uiMode: string): void => {
this.setState(({ settings }: State) => ({
settings: {
...settings,
uiMode
}
}));
}
private onChangeUiTheme = (uiTheme: string): void => {
this.setState(({ settings }: State) => ({
settings: {
...settings,
uiTheme
}
}));
}
private toggleCustomNode = (): void => {
this.setState(({ isCustomNode, settings }: State) => {
// reset URL to a preset when toggled to preset
const apiUrl = isCustomNode
? uiSettings.availableNodes[0].value
: settings.apiUrl;
return {
isCustomNode: !isCustomNode,
isUrlValid: true,
settings: {
...settings,
apiUrl
}
};
});
}
private isValidUrl (apiUrl: string): boolean {
return (
// some random length... we probably want to parse via some lib
(apiUrl.length >= 7) &&
// check that it starts with a valid ws identifier
(apiUrl.startsWith('ws://') || apiUrl.startsWith('wss://'))
);
}
private saveAndReload = (): void => {
const { settings } = this.state;
uiSettings.set(settings);
// HACK This is terribe, but since the API needs to re-connect, but since
// the API does not yet handle re-connections properly, it is what it is
window.location.reload();
}
}
export default translate(General);

View File

@ -2,8 +2,32 @@
/* This software may be modified and distributed under the terms
/* of the Apache-2.0 license. See the LICENSE file for details. */
.sub-label {
cursor: pointer;
padding: 0rem .5833rem;
text-align: right;
.settings-General {
.ui.menu {
justify-content: flex-end;
margin-bottom: 0;
.active.item {
font-weight: bold;
}
}
.ui.attached.segment.tab {
padding: 0;
border: none;
}
.sub-label {
cursor: pointer;
padding: 0rem .5833rem;
text-align: right;
}
}
.settings-Developer {
.editor {
height: 300px;
position: relative;
}
}

View File

@ -4,30 +4,21 @@
import { AppProps, I18nProps } from '@polkadot/ui-app/types';
import { TabItem } from '@polkadot/ui-app/Tabs';
import { SettingsStruct } from '@polkadot/ui-settings/types';
import React from 'react';
import { Route, Switch } from 'react-router';
import store from 'store';
import { getTypeRegistry } from '@polkadot/types';
import { Button, Dropdown, Input, InputFile, Tabs } from '@polkadot/ui-app/index';
import uiSettings from '@polkadot/ui-settings';
import { u8aToString } from '@polkadot/util';
import { Tabs } from '@polkadot/ui-app/index';
import './index.css';
import translate from './translate';
import Developer from './Developer';
import General from './General';
type Props = AppProps & I18nProps;
type State = {
isCustomNode: boolean,
isTypesValid: boolean,
isUrlValid: boolean,
settings: SettingsStruct,
tabs: Array<TabItem>,
types?: { [index: string]: any } | null,
typesPlaceholder?: string
tabs: Array<TabItem>
};
class App extends React.PureComponent<Props, State> {
@ -35,22 +26,8 @@ class App extends React.PureComponent<Props, State> {
super(props);
const { t } = props;
const types = store.get('types') || {};
const names = Object.keys(types);
const settings = uiSettings.get();
let isCustomNode = true;
// check to see if user has saved a custom node by seeing if their URL is equal to any preset
for (let i = 0; i < uiSettings.availableNodes.length; i++) {
if (uiSettings.availableNodes[i].value === settings.apiUrl) {
isCustomNode = false;
}
}
this.state = {
isCustomNode,
isTypesValid: true,
isUrlValid: this.isValidUrl(settings.apiUrl),
tabs: [
{
name: 'general',
@ -60,11 +37,7 @@ class App extends React.PureComponent<Props, State> {
name: 'developer',
text: t('Developer')
}
],
typesPlaceholder: names.length
? names.join(', ')
: undefined,
settings
]
};
}
@ -81,231 +54,21 @@ class App extends React.PureComponent<Props, State> {
/>
</header>
<Switch>
<Route path={`${basePath}/developer`} render={this.renderDeveloper} />
<Route render={this.renderGeneral} />
<Route path={`${basePath}/developer`} render={this.DeveloperWithStatus} />
<Route component={General} />
</Switch>
</main>
);
}
private renderDeveloper = () => {
const { t } = this.props;
const { isTypesValid, types, typesPlaceholder } = this.state;
private DeveloperWithStatus = () => {
return (
<>
<div className='ui--row'>
<div className='full'>
<InputFile
clearContent={!types && isTypesValid}
isError={!isTypesValid}
label={t('additional type definitions (JSON)')}
onChange={this.onChangeTypes}
placeholder={typesPlaceholder}
/>
</div>
</div>
<Button.Group>
<Button
isDisabled={!types}
isNegative
onClick={this.clearTypes}
label={t('Reset')}
/>
<Button.Or />
<Button
isDisabled={!isTypesValid}
isPrimary
onClick={this.saveDeveloper}
label={t('Save')}
/>
</Button.Group>
</>
<Developer
onStatusChange={() => this.props.onStatusChange}
{...this.props}
/>
);
}
private renderGeneral = () => {
const { t } = this.props;
const { isCustomNode, isUrlValid, settings: { apiUrl, i18nLang, uiMode, uiTheme } } = this.state;
return (
<>
<div className='ui--row'>
<div className='full'>
<div className='sub-label'>
{
isCustomNode
? <><a onClick={this.toggleCustomNode}>{t('pre-set')}</a> | <b>{t('custom')}</b></>
: <><b>{t('pre-set')}</b> | <a onClick={this.toggleCustomNode}>{t('custom')}</a></>
}
</div>
{
isCustomNode
? <Input
defaultValue={apiUrl}
isError={!isUrlValid}
label={t('remote node/endpoint to connect to')}
onChange={this.onChangeApiUrl}
/>
: <Dropdown
defaultValue={apiUrl}
label={t('remote node/endpoint to connect to')}
onChange={this.onChangeApiUrl}
options={uiSettings.availableNodes}
/>
}
</div>
</div>
<div className='ui--row'>
<div className='medium'>
<Dropdown
defaultValue={uiTheme}
label={t('default interface theme')}
onChange={this.onChangeUiTheme}
options={uiSettings.availableUIThemes}
/>
</div>
<div className='medium'>
<Dropdown
defaultValue={uiMode}
label={t('interface operation mode')}
onChange={this.onChangeUiMode}
options={uiSettings.availableUIModes}
/>
</div>
</div>
<div className='ui--row'>
<div className='full'>
<Dropdown
defaultValue={i18nLang}
isDisabled
label={t('default interface language')}
onChange={this.onChangeLang}
options={uiSettings.availableLanguages}
/>
</div>
</div>
<Button.Group>
<Button
isDisabled={!isUrlValid}
isPrimary
onClick={this.saveAndReload}
label={t('Save & Reload')}
/>
</Button.Group>
</>
);
}
private clearTypes = (): void => {
this.setState({
isTypesValid: true,
types: null,
typesPlaceholder: ''
});
}
private onChangeApiUrl = (apiUrl: string): void => {
this.setState(({ settings }: State) => ({
isUrlValid: this.isValidUrl(apiUrl),
settings: {
...settings,
apiUrl
}
}));
}
private onChangeLang = (i18nLang: string): void => {
// ignore (for now), here to future-proof
}
private onChangeTypes = (data: Uint8Array) => {
try {
const types = JSON.parse(u8aToString(data));
const typesPlaceholder = Object.keys(types).join(', ');
console.log('Registering types:', typesPlaceholder);
getTypeRegistry().register(types);
this.setState({
isTypesValid: true,
types,
typesPlaceholder
});
} catch (error) {
console.error('Registering types:', error);
this.setState({
isTypesValid: false,
types: null,
typesPlaceholder: error.message
});
}
}
private onChangeUiMode = (uiMode: string): void => {
this.setState(({ settings }: State) => ({
settings: {
...settings,
uiMode
}
}));
}
private onChangeUiTheme = (uiTheme: string): void => {
this.setState(({ settings }: State) => ({
settings: {
...settings,
uiTheme
}
}));
}
private toggleCustomNode = (): void => {
this.setState(({ isCustomNode, settings }: State) => {
// reset URL to a preset when toggled to preset
const apiUrl = isCustomNode
? uiSettings.availableNodes[0].value
: settings.apiUrl;
return {
isCustomNode: !isCustomNode,
isUrlValid: true,
settings: {
...settings,
apiUrl
}
};
});
}
private isValidUrl (apiUrl: string): boolean {
return (
// some random length... we probably want to parse via some lib
(apiUrl.length >= 7) &&
// check that it starts with a valid ws identifier
(apiUrl.startsWith('ws://') || apiUrl.startsWith('wss://'))
);
}
private saveDeveloper = (): void => {
const { isTypesValid, types } = this.state;
if (isTypesValid) {
store.set('types', types);
}
}
private saveAndReload = (): void => {
const { settings } = this.state;
uiSettings.set(settings);
// HACK This is terribe, but since the API needs to re-connect, but since
// the API does not yet handle re-connections properly, it is what it is
window.location.reload();
}
}
export default translate(App);

View File

@ -18,6 +18,7 @@
"@polkadot/ui-settings": "^0.28.8",
"@polkadot/ui-util": "^0.28.8",
"@types/chart.js": "^2.7.45",
"@types/classnames": "^2.2.7",
"@types/i18next": "^12.1.0",
"@types/react-copy-to-clipboard": "^4.2.6",
"@types/react-dropzone": "^4.2.2",

View File

@ -5,13 +5,37 @@
import { BareProps } from '@polkadot/ui-app/types';
import React from 'react';
import { classes } from '@polkadot/ui-app/util';
import CodeFlask from 'codeflask';
import './style.css';
type Props = BareProps & {
code: string,
isValid?: boolean,
onEdit: (code: string) => void
};
/**
* @name Editor
* @summary A code editor based on the codeflask npm module
* @description It allows to live-edit code examples and JSON files.
*
* @example
* <BR>
*
* ```javascript
* import {Editor} from '@polkadot/ui-app';
*
* <Editor
* className={string} // optional
* code={string}
* isValid={boolean}, // optional
* onEdit={() => callbackFunction}
* />
* ```
*/
export default class Editor extends React.Component<Props> {
private id: string = `flask-${Date.now()}`;
private editor: any;
@ -42,8 +66,13 @@ export default class Editor extends React.Component<Props> {
}
render () {
const { className, isValid } = this.props;
return (
<div id={this.id} />
<div
id={this.id}
className={classes('ui-app-editor', className, !isValid ? 'invalid' : '')}
/>
);
}
}

View File

@ -0,0 +1,13 @@
.ui-app-editor {
border: 1px solid rgba(34,36,38,.15);
&.invalid {
background-color: #fff6f6;
border-color: #e0b4b4;
}
.codeflask {
background: transparent;
}
}

1152
yarn.lock

File diff suppressed because it is too large Load Diff