From c3f0151746cc9b31c632e2c0b81f1de0f0a4dd9b Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Thu, 29 Aug 2019 11:47:11 +0200 Subject: [PATCH] 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 --- packages/app-settings/src/General.tsx | 306 +++++---------------- packages/app-settings/src/SelectUrl.tsx | 123 +++++++++ packages/app-settings/src/types.ts | 14 + packages/app-settings/src/util.tsx | 41 +++ packages/react-components/src/Dropdown.tsx | 2 +- packages/react-components/src/Toggle.tsx | 4 +- packages/react-params/src/Params.css | 16 -- packages/react-params/src/index.tsx | 15 +- 8 files changed, 262 insertions(+), 259 deletions(-) create mode 100644 packages/app-settings/src/SelectUrl.tsx create mode 100644 packages/app-settings/src/types.ts create mode 100644 packages/app-settings/src/util.tsx diff --git a/packages/app-settings/src/General.tsx b/packages/app-settings/src/General.tsx index a90a8053ee..cc1e4b93ce 100644 --- a/packages/app-settings/src/General.tsx +++ b/packages/app-settings/src/General.tsx @@ -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: ( -
- -
{text}
-
- ), - value -}); +function General ({ className, t }: Props): React.ReactElement { + 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 { - 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 ( -
- {this.renderEndpoint()} -
-
- -
-
-
-
- -
-
- -
-
-
-
- -
-
- -
+ ); } -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); diff --git a/packages/app-settings/src/SelectUrl.tsx b/packages/app-settings/src/SelectUrl.tsx new file mode 100644 index 0000000000..1dc92970ad --- /dev/null +++ b/packages/app-settings/src/SelectUrl.tsx @@ -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 { + 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 ( +
+
{ + isCustom + ? + : + }
+ +
+ ); +} + +export default translate( + styled(SelectUrl)` + position: relative; + + .settings--customToggle { + position: absolute; + top: .5rem; + right: 3.5rem; + } + ` +); diff --git a/packages/app-settings/src/types.ts b/packages/app-settings/src/types.ts new file mode 100644 index 0000000000..897cf11ffb --- /dev/null +++ b/packages/app-settings/src/types.ts @@ -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; +} diff --git a/packages/app-settings/src/util.tsx b/packages/app-settings/src/util.tsx new file mode 100644 index 0000000000..2ac7ee0789 --- /dev/null +++ b/packages/app-settings/src/util.tsx @@ -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: ( +
+ +
{text}
+
+ ), + 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(); +} diff --git a/packages/react-components/src/Dropdown.tsx b/packages/react-components/src/Dropdown.tsx index 87b61fac63..7a47b20cfb 100644 --- a/packages/react-components/src/Dropdown.tsx +++ b/packages/react-components/src/Dropdown.tsx @@ -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; } diff --git a/packages/react-components/src/Toggle.tsx b/packages/react-components/src/Toggle.tsx index 813a4a32f4..6022cd4d09 100644 --- a/packages/react-components/src/Toggle.tsx +++ b/packages/react-components/src/Toggle.tsx @@ -23,6 +23,7 @@ class Toggle extends React.PureComponent { return (
+ { onChange={this.onChange} toggle={asSwitch} /> -
); } @@ -45,7 +45,7 @@ class Toggle extends React.PureComponent { export default styled(Toggle)` > label { display: inline-block; - margin-left: 0.5rem; + margin: 0 0.5rem; } > label, diff --git a/packages/react-params/src/Params.css b/packages/react-params/src/Params.css index de461f5ce5..87450fcfc6 100644 --- a/packages/react-params/src/Params.css +++ b/packages/react-params/src/Params.css @@ -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; diff --git a/packages/react-params/src/index.tsx b/packages/react-params/src/index.tsx index 9fd02a9204..fdc400837e 100644 --- a/packages/react-params/src/index.tsx +++ b/packages/react-params/src/index.tsx @@ -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 { } } -export default translate(Params); +export default translate( + styled(Params as React.ComponentClass)` + .ui--Param-composite { + position: relative; + + .ui--Param-overlay { + position: absolute; + top: 0.5rem; + right: 3.5rem; + } + } + ` +);