Make setting URL component re-usable (#1556)
* Make setting URL component re-usable * Adjust toggle * Center icons * Cleanups * Fix linting * extract label * Simplify * More cleanups * Readability tweaks
This commit is contained in:
parent
10896e04bf
commit
c3f0151746
@ -3,259 +3,87 @@
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { AppProps, I18nProps } from '@polkadot/react-components/types';
|
||||
import { SettingsStruct } from '@polkadot/ui-settings/types';
|
||||
import { Option } from './types';
|
||||
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Button, ChainImg, Dropdown, Input, Toggle } from '@polkadot/react-components';
|
||||
import React, { useState } from 'react';
|
||||
import { Button, Dropdown } from '@polkadot/react-components';
|
||||
import { ActionStatus } from '@polkadot/react-components/Status/types';
|
||||
import uiSettings from '@polkadot/ui-settings';
|
||||
|
||||
import translate from './translate';
|
||||
|
||||
interface Option {
|
||||
text: React.ReactNode;
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
interface SetOption {
|
||||
info: string;
|
||||
text: string;
|
||||
value: string | number;
|
||||
}
|
||||
import { createOption, saveAndReload } from './util';
|
||||
import SelectUrl from './SelectUrl';
|
||||
|
||||
interface Props extends AppProps, I18nProps {
|
||||
onStatusChange: (status: ActionStatus) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
isCustomNode: boolean;
|
||||
isUrlValid: boolean;
|
||||
settings: SettingsStruct;
|
||||
}
|
||||
const prefixOptions = uiSettings.availablePrefixes.map((o): Option => createOption(o, ['default']));
|
||||
const themeOptions = uiSettings.availableUIThemes; // .map((o): Option => createOption(o));
|
||||
|
||||
const createOption = ({ info, text, value }: SetOption, overrides: string[] = [], override: string = 'empty'): Option => ({
|
||||
text: (
|
||||
<div className='ui--Dropdown-item'>
|
||||
<ChainImg
|
||||
className='ui--Dropdown-icon'
|
||||
logo={
|
||||
overrides.includes(info)
|
||||
? override
|
||||
: info
|
||||
}
|
||||
/>
|
||||
<div className='ui--Dropdown-name'>{text}</div>
|
||||
</div>
|
||||
),
|
||||
value
|
||||
});
|
||||
function General ({ className, t }: Props): React.ReactElement<Props> {
|
||||
const [settings, setSettings] = useState(uiSettings.get());
|
||||
const { i18nLang, prefix, uiMode, uiTheme } = settings;
|
||||
|
||||
const endpointOptions: Option[] = uiSettings.availableNodes.map((o): Option => createOption(o, ['local']));
|
||||
const prefixOptions: Option[] = uiSettings.availablePrefixes.map((o): Option => createOption(o, ['default']));
|
||||
const themeOptions: Option[] = uiSettings.availableUIThemes.map((o): Option => createOption(o));
|
||||
const _onChangeApiUrl = (apiUrl: string): void => setSettings({ ...settings, apiUrl });
|
||||
const _onChangePrefix = (prefix: number): void => setSettings({ ...settings, prefix });
|
||||
const _onChangeUiMode = (uiMode: string): void => setSettings({ ...settings, uiMode });
|
||||
const _onChangeUiTheme = (uiTheme: string): void => setSettings({ ...settings, uiTheme });
|
||||
const _saveAndReload = (): void => saveAndReload(settings);
|
||||
|
||||
class General extends React.PureComponent<Props, State> {
|
||||
public constructor (props: Props) {
|
||||
super(props);
|
||||
|
||||
const settings = uiSettings.get();
|
||||
const isCustomNode = uiSettings.availableNodes.reduce((isCustomNode, { value }): boolean => {
|
||||
return isCustomNode && value !== settings.apiUrl;
|
||||
}, true);
|
||||
|
||||
this.state = {
|
||||
isCustomNode,
|
||||
isUrlValid: this.isValidUrl(settings.apiUrl),
|
||||
settings
|
||||
};
|
||||
}
|
||||
|
||||
public render (): React.ReactNode {
|
||||
const { className, t } = this.props;
|
||||
const { isUrlValid, settings: { i18nLang, prefix, uiMode, uiTheme } } = this.state;
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{this.renderEndpoint()}
|
||||
<div className='ui--row'>
|
||||
<div className='ui--medium'>
|
||||
<Dropdown
|
||||
defaultValue={prefix}
|
||||
help={t('Override the default ss58 prefix for address generation')}
|
||||
label={t('address prefix')}
|
||||
onChange={this.onChangePrefix}
|
||||
options={prefixOptions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='ui--row'>
|
||||
<div className='medium'>
|
||||
<Dropdown
|
||||
defaultValue={uiTheme}
|
||||
help={t('The logo and colors for the app along with the identity icon theme.')}
|
||||
label={t('default interface theme')}
|
||||
onChange={this.onChangeUiTheme}
|
||||
options={themeOptions}
|
||||
/>
|
||||
</div>
|
||||
<div className='medium'>
|
||||
<Dropdown
|
||||
defaultValue={uiMode}
|
||||
help={t('Adjust the mode from basic (with a limited number of beginner-user-friendly apps) to full (with all basic & advanced apps available)')}
|
||||
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')}
|
||||
options={uiSettings.availableLanguages}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button.Group>
|
||||
<Button
|
||||
isDisabled={!isUrlValid}
|
||||
isPrimary
|
||||
onClick={this.saveAndReload}
|
||||
label={t('Save & Reload')}
|
||||
return (
|
||||
<div className={className}>
|
||||
<SelectUrl onChange={_onChangeApiUrl} />
|
||||
<div className='ui--row'>
|
||||
<div className='ui--medium'>
|
||||
<Dropdown
|
||||
defaultValue={prefix}
|
||||
help={t('Override the default ss58 prefix for address generation')}
|
||||
label={t('address prefix')}
|
||||
onChange={_onChangePrefix}
|
||||
options={prefixOptions}
|
||||
/>
|
||||
</Button.Group>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderEndpoint = (): React.ReactNode => {
|
||||
const { t } = this.props;
|
||||
const { isCustomNode, isUrlValid, settings: { apiUrl } } = this.state;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Toggle
|
||||
asSwitch
|
||||
className='settings--cutomToggle'
|
||||
defaultValue={isCustomNode}
|
||||
label={t('custom endpoint')}
|
||||
onChange={this.onChangeCustom}
|
||||
/>
|
||||
<div className='ui--row'>
|
||||
{
|
||||
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={endpointOptions}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private onChangeApiUrl = (apiUrl: string): void => {
|
||||
this.setState(({ settings }: State): Pick<State, never> => ({
|
||||
isUrlValid: this.isValidUrl(apiUrl),
|
||||
settings: {
|
||||
...settings,
|
||||
apiUrl
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private onChangePrefix = (prefix: number): void => {
|
||||
this.setState(({ settings }: State): Pick<State, never> => ({
|
||||
settings: {
|
||||
...settings,
|
||||
prefix
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private onChangeUiMode = (uiMode: string): void => {
|
||||
this.setState(({ settings }: State): Pick<State, never> => ({
|
||||
settings: {
|
||||
...settings,
|
||||
uiMode
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private onChangeUiTheme = (uiTheme: string): void => {
|
||||
this.setState(({ settings }: State): Pick<State, never> => ({
|
||||
settings: {
|
||||
...settings,
|
||||
uiTheme
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private onChangeCustom = (isCustomNode: boolean): void => {
|
||||
this.setState(({ settings }: State): Pick<State, never> => ({
|
||||
isCustomNode,
|
||||
isUrlValid: true,
|
||||
settings: {
|
||||
...settings,
|
||||
apiUrl: isCustomNode
|
||||
? settings.apiUrl
|
||||
: uiSettings.availableNodes[0].value
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
</div>
|
||||
<div className='ui--row'>
|
||||
<div className='medium'>
|
||||
<Dropdown
|
||||
defaultValue={uiTheme}
|
||||
help={t('The logo and colors for the app along with the identity icon theme.')}
|
||||
label={t('default interface theme')}
|
||||
onChange={_onChangeUiTheme}
|
||||
options={themeOptions}
|
||||
/>
|
||||
</div>
|
||||
<div className='medium'>
|
||||
<Dropdown
|
||||
defaultValue={uiMode}
|
||||
help={t('Adjust the mode from basic (with a limited number of beginner-user-friendly apps) to full (with all basic & advanced apps available)')}
|
||||
label={t('interface operation mode')}
|
||||
onChange={_onChangeUiMode}
|
||||
options={uiSettings.availableUIModes}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='ui--row'>
|
||||
<div className='full'>
|
||||
<Dropdown
|
||||
defaultValue={i18nLang}
|
||||
isDisabled
|
||||
label={t('default interface language')}
|
||||
options={uiSettings.availableLanguages}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button.Group>
|
||||
<Button
|
||||
isPrimary
|
||||
onClick={_saveAndReload}
|
||||
label={t('Save & Reload')}
|
||||
/>
|
||||
</Button.Group>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default translate(styled(General)`
|
||||
.settings--cutomToggle {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.ui.menu {
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 0;
|
||||
|
||||
.active.item {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.sub-label {
|
||||
cursor: pointer;
|
||||
padding: 0rem .5833rem;
|
||||
text-align: right;
|
||||
}
|
||||
`);
|
||||
export default translate(General);
|
||||
|
||||
123
packages/app-settings/src/SelectUrl.tsx
Normal file
123
packages/app-settings/src/SelectUrl.tsx
Normal file
@ -0,0 +1,123 @@
|
||||
// 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 { I18nProps } from '@polkadot/react-components/types';
|
||||
import { Option } from './types';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Dropdown, Input, Toggle } from '@polkadot/react-components';
|
||||
import uiSettings from '@polkadot/ui-settings';
|
||||
|
||||
import translate from './translate';
|
||||
import { createOption } from './util';
|
||||
|
||||
interface Props extends I18nProps {
|
||||
className?: string;
|
||||
onChange: (url: string) => void;
|
||||
}
|
||||
|
||||
interface StateUrl {
|
||||
isValid: boolean;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface State extends StateUrl {
|
||||
isCustom: boolean;
|
||||
}
|
||||
|
||||
const endpointOptions = uiSettings.availableNodes.map((o): Option => createOption(o, ['local']));
|
||||
|
||||
// check the validity of the url
|
||||
function isValidUrl (url: string): boolean {
|
||||
return (
|
||||
// some random length... we probably want to parse via some lib
|
||||
(url.length >= 7) &&
|
||||
// check that it starts with a valid ws identifier
|
||||
(url.startsWith('ws://') || url.startsWith('wss://'))
|
||||
);
|
||||
}
|
||||
|
||||
// sanitize a url and check it for validity
|
||||
function makeUrl (_url: string): StateUrl {
|
||||
const url = _url.trim();
|
||||
const isValid = isValidUrl(url);
|
||||
|
||||
return { isValid, url };
|
||||
}
|
||||
|
||||
// this allows us to retrieve the initial state by reading the settings and the applying
|
||||
// validation on-top of the values retrieved
|
||||
function getInitialState (): State {
|
||||
const url = uiSettings.get().apiUrl;
|
||||
const isCustom = uiSettings.availableNodes.reduce((isCustom: boolean, { value }): boolean => {
|
||||
return isCustom && value !== url;
|
||||
}, true);
|
||||
const isValid = isValidUrl(url);
|
||||
|
||||
return { isCustom, isValid, url };
|
||||
}
|
||||
|
||||
function SelectUrl ({ className, onChange, t }: Props): React.ReactElement<Props> {
|
||||
const [info, setInfo] = useState(getInitialState());
|
||||
const { isCustom, isValid, url } = info;
|
||||
const help = t('Select the remote endpoint, either from the dropdown on manual entered via the custom toggle');
|
||||
const label = t('remote node/endpoint to connect to');
|
||||
const _onChangeUrl = (url: string): void => setInfo({ ...info, ...makeUrl(url) });
|
||||
const _onChangeCustom = (isCustom: boolean): void =>
|
||||
setInfo({
|
||||
...makeUrl(
|
||||
isCustom
|
||||
? info.url
|
||||
: uiSettings.availableNodes[0].value as string
|
||||
),
|
||||
isCustom
|
||||
});
|
||||
|
||||
useEffect((): void => {
|
||||
if (onChange && info.isValid) {
|
||||
onChange(info.url);
|
||||
}
|
||||
}, [info]);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className='ui--row'>{
|
||||
isCustom
|
||||
? <Input
|
||||
defaultValue={url}
|
||||
help={help}
|
||||
isError={!isValid}
|
||||
label={label}
|
||||
onChange={_onChangeUrl}
|
||||
/>
|
||||
: <Dropdown
|
||||
defaultValue={url}
|
||||
help={help}
|
||||
label={label}
|
||||
onChange={_onChangeUrl}
|
||||
options={endpointOptions}
|
||||
/>
|
||||
}</div>
|
||||
<Toggle
|
||||
className='settings--customToggle'
|
||||
defaultValue={isCustom}
|
||||
label={t('custom endpoint')}
|
||||
onChange={_onChangeCustom}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default translate(
|
||||
styled(SelectUrl)`
|
||||
position: relative;
|
||||
|
||||
.settings--customToggle {
|
||||
position: absolute;
|
||||
top: .5rem;
|
||||
right: 3.5rem;
|
||||
}
|
||||
`
|
||||
);
|
||||
14
packages/app-settings/src/types.ts
Normal file
14
packages/app-settings/src/types.ts
Normal file
@ -0,0 +1,14 @@
|
||||
// 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.
|
||||
|
||||
export interface Option {
|
||||
text: React.ReactNode;
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
export interface SetOption {
|
||||
info: string;
|
||||
text: string;
|
||||
value: string | number;
|
||||
}
|
||||
41
packages/app-settings/src/util.tsx
Normal file
41
packages/app-settings/src/util.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
// 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 { SettingsStruct } from '@polkadot/ui-settings/types';
|
||||
import { Option, SetOption } from './types';
|
||||
|
||||
import React from 'react';
|
||||
import { ChainImg } from '@polkadot/react-components';
|
||||
import uiSettings from '@polkadot/ui-settings';
|
||||
|
||||
export function createOption ({ info, text, value }: SetOption, overrides: string[] = [], override: string = 'empty'): Option {
|
||||
return {
|
||||
text: (
|
||||
<div className='ui--Dropdown-item'>
|
||||
<ChainImg
|
||||
className='ui--Dropdown-icon'
|
||||
logo={
|
||||
overrides.includes(info)
|
||||
? override
|
||||
: info
|
||||
}
|
||||
/>
|
||||
<div className='ui--Dropdown-name'>{text}</div>
|
||||
</div>
|
||||
),
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
export function saveAndReload (settings: SettingsStruct | null): void {
|
||||
if (!settings) {
|
||||
return;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
@ -172,7 +172,7 @@ export default styled(Dropdown)`
|
||||
> .text > .ui--Dropdown-item {
|
||||
.ui--Dropdown-icon {
|
||||
left: -2.6rem;
|
||||
top: -1rem;
|
||||
top: -1.15rem;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
@ -23,6 +23,7 @@ class Toggle extends React.PureComponent<Props> {
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<label>{label}</label>
|
||||
<SUICheckbox
|
||||
checked={value}
|
||||
disabled={isDisabled}
|
||||
@ -30,7 +31,6 @@ class Toggle extends React.PureComponent<Props> {
|
||||
onChange={this.onChange}
|
||||
toggle={asSwitch}
|
||||
/>
|
||||
<label>{label}</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -45,7 +45,7 @@ class Toggle extends React.PureComponent<Props> {
|
||||
export default styled(Toggle)`
|
||||
> label {
|
||||
display: inline-block;
|
||||
margin-left: 0.5rem;
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
|
||||
> label,
|
||||
|
||||
@ -13,28 +13,12 @@
|
||||
.ui--row {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.ui--Param-overlay {
|
||||
margin-bottom: -100%;
|
||||
z-index: 1;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.ui--Param-Address {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.ui--Param-composite {
|
||||
position: relative;
|
||||
|
||||
.ui--Param-overlay {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 4.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.ui--Params-Content {
|
||||
box-sizing: border-box;
|
||||
padding: 0 0 0 1.75rem;
|
||||
|
||||
@ -8,6 +8,7 @@ import { ComponentMap, ParamDef, RawParam, RawParams, RawParamOnChange, RawParam
|
||||
import './Params.css';
|
||||
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Toggle } from '@polkadot/react-components';
|
||||
import { classes } from '@polkadot/react-components/util';
|
||||
import translate from '@polkadot/react-components/translate';
|
||||
@ -235,4 +236,16 @@ class Params extends React.PureComponent<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(Params);
|
||||
export default translate(
|
||||
styled(Params as React.ComponentClass<Props>)`
|
||||
.ui--Param-composite {
|
||||
position: relative;
|
||||
|
||||
.ui--Param-overlay {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 3.5rem;
|
||||
}
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user