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:
parent
4ce1a3678d
commit
ee968a59c3
185
packages/app-settings/src/Developer.tsx
Normal file
185
packages/app-settings/src/Developer.tsx
Normal 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);
|
||||
201
packages/app-settings/src/General.tsx
Normal file
201
packages/app-settings/src/General.tsx
Normal 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);
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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' : '')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
13
packages/ui-app/src/Editor/style.css
Normal file
13
packages/ui-app/src/Editor/style.css
Normal 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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user