diff --git a/README.md b/README.md index 4302c7607..85d50e24d 100644 --- a/README.md +++ b/README.md @@ -49,14 +49,15 @@ API binding can be customized to the value of your choice. Simply create a setti } ``` - **defaultNetwork** - Key of the default network that will be connected automatically -- **networks.key** - Specifies a unique key for a network -- **networks.name** - A human readable network name that will be shown in the interface -- **networks.networkID** - Unique identificator that is imprinted in all transactions. Refer to the configuration of your go-apla instance -- **networks.fullNodes** - Prebuilt list of URLs that will be used for synchronization -- **networks.socketUrl** - Optional parameter that overrides Centrifugo connection endpoint. Default: provided by go-apla configuration -- **networks.activationEmail** - Optional parameter that will be shown to a user when there are no active endpoints to log in. Used for KYC -- **networks.enableDemoMode** - When set to true, will enable authorization using the guest key -- **networks.disableSync** - Optional parameter that disables synchronization of the full nodes. Unsafe, use with caution +- **networks** - Specifies an array of predefined network endpoints that are used to connect your application to the blockchain +- **networks[x].key** - Specifies a unique key for a network +- **networks[x].name** - A human readable network name that will be shown in the interface +- **networks[x].networkID** - Unique identificator that is imprinted in all transactions. Refer to the configuration of your go-apla instance +- **networks[x].fullNodes** - Prebuilt list of URLs that will be used for synchronization +- **networks[x].socketUrl** - Optional parameter that overrides Centrifugo connection endpoint. Default: provided by go-apla configuration +- **networks[x].activationEmail** - Optional parameter that will be shown to a user when there are no active endpoints to log in. Used for KYC +- **networks[x].enableDemoMode** - When set to true, will enable authorization using the guest key +- **networks[x].disableSync** - Optional parameter that disables synchronization of the full nodes. Unsafe, use with caution Development server emits warnings and will report errors in readable format. You can hack around it, but it is suited only for development/testing. To use it in production environment you will need to build the project. diff --git a/now.json b/now.json new file mode 100644 index 000000000..75e34a8bf --- /dev/null +++ b/now.json @@ -0,0 +1,17 @@ +{ + "version": 2, + "routes": [ + { + "src": "/(fonts|img|locales|styles|static)/(.*)", + "dest": "/$1/$2" + }, + { + "src": "/(settings\\.json|asset-manifest\\.json|favicon\\.ico|manifest\\.json|service-worker\\.json)", + "dest": "/$1" + }, + { + "src": "/(.*)", + "dest": "/index.html" + } + ] +} \ No newline at end of file diff --git a/package.json b/package.json index 9f41467c6..30781ad10 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "apla-front", - "version": "2.0.0", + "version": "2.1.0", "author": { "name": "apla-front" }, @@ -53,7 +53,6 @@ "react-monaco-editor": "^0.25.1", "react-onclickoutside": "^6.6.0", "react-redux": "^5.0.6", - "react-redux-loading-bar": "^2.9.3", "react-router-dom": "^4.1.2", "react-router-transition": "^1.1.0", "react-sortable-tree": "^2.1.0", @@ -64,6 +63,7 @@ "redux-localstorage-debounce": "^0.1.0", "redux-localstorage-filter": "^0.1.1", "redux-observable": "^0.16.0", + "route-parser": "^0.0.5", "rxjs": "^5.4.3", "simple-line-icons": "^2.4.1", "sockjs-client": "^1.1.4", @@ -88,7 +88,8 @@ "start-js-desktop": "react-scripts-ts-electron start", "build-desktop": "npm run build-css && cross-env PUBLIC_URL=./ REACT_APP_VERSION=$npm_package_version react-scripts-ts-electron build", "package": "electron-builder --config electron-builder.json --dir", - "release": "electron-builder --config electron-builder.json" + "release": "electron-builder --config electron-builder.json", + "serve": "now ./build -A ../now.json -n apla" }, "devDependencies": { "@types/bluebird": "^3.5.18", @@ -128,6 +129,7 @@ "@types/redux-localstorage": "^1.0.8", "@types/redux-localstorage-debounce": "^0.1.4", "@types/redux-localstorage-filter": "^0.1.4", + "@types/route-parser": "^0.1.3", "@types/rx": "^4.1.1", "@types/sockjs-client": "^1.0.32", "@types/url-join": "^0.8.2", @@ -151,4 +153,4 @@ "resolutions": { "**/event-stream": "^4.0.1" } -} +} \ No newline at end of file diff --git a/public/locales/en-US.json b/public/locales/en-US.json index 63024d30a..4ebc6661c 100644 --- a/public/locales/en-US.json +++ b/public/locales/en-US.json @@ -79,11 +79,15 @@ "confirm": "Confirm", "contract": "Smart contract", "contract.exec": "Execute contract", + "editor": "Editor", + "editor.app": "Application", "editor.block.create": "Create block", "editor.close.confirm": "Do you really want to close '{name}' without saving changes?", "editor.close.all": "Close all tabs", + "editor.close.all.confirm": "Do you really want to close all tabs without saving?", "editor.close.saved": "Close saved tabs", "editor.conditions.change": "Change conditions", + "editor.contract.create": "Create smart contract", "editor.create": "Create", "editor.execute": "Execute", "editor.menu": "Menu", @@ -92,7 +96,7 @@ "editor.page.name": "Name", "editor.param.add": "Add parameter", "editor.revert": "Revert", - "editor.revert.confirm": "Do you really want to discard all changes?", + "editor.revert.confirm": "Do you really want to discard all changes in '{name}'?", "editor.save": "Save", "editor.tool.developer": "Developer", "editor.tool.designer": "Designer", @@ -114,7 +118,7 @@ "general.download.asfile": "Download as file", "general.ecosystems": "Ecosystems", "general.error": "Error", - "general.error.notfound": "The page you are looking for does not exists", + "general.error.notfound": "The page you are looking for does not exist", "general.error.page": "The page you are looking for could not be processed", "general.error.timeout": "The page you are looking for is too heavy to be processed. Consider reducing number of database queries", "general.error.socket": "Notifications are unavailable", @@ -122,6 +126,7 @@ "general.home": "Home", "general.key.private": "Private key", "general.key.public": "Public key", + "general.main_menu": "Main menu", "general.network.id.short": "ID", "general.network.id.auto": "Automatic discovery", "general.network.add": "Add network", @@ -143,7 +148,7 @@ "general.network.error.E_SERVER_MISCONFIGURATION": "Server configuration error", "general.networks": "Networks", "general.notfound.page": "We couldn't find this page", - "general.notfound.page.notexists": "The page you are looking for does not exists", + "general.notfound.page.notexists": "The page you are looking for does not exist", "general.password": "Password", "general.password.repeat": "Repeat password", "general.password.old": "Old password", @@ -180,18 +185,17 @@ "map.editor": "Map editor", "map.meter.short": "m", "modal.authorization.title": "Authorization", - "modal.authorization.password": "Enter your password to execute transaction {contract}", + "modal.authorization.password": "Please enter your password to perform this action", "modal.confirm.title": "Confirmation", "modal.imageeditor.cancel": "Cancel", "modal.imageeditor.confirm": "Confirm", "modal.imageeditor.desc": "Prepare your image for uploading by selecting which part of the image you want to use", "modal.imageeditor.title": "Image editor", - "modal.locale.title": "Switch language", "modal.network.remove.confirmation": "Do you really want to delete network \"{name}\"?", - "navigation.back": "Back", - "navigation.forward": "Forward", - "navigation.home": "Home", + "navigation.default_page": "Main page", + "navigation.loaded_page": "Loaded page", "navigation.refresh": "Refresh", + "navigation.return": "Return", "notifications": "Notifications", "notification.ecosystem_invited": "Joined ecosystem {ecosystem}", "notification.tx_batch": "Batch processing completed", @@ -224,7 +228,7 @@ "tx.error.E_LIMITFORSIGN": "Execution failed", "tx.error.E_OFFLINE": "Service offline", "tx.error.E_SERVER": "Runtime error", - "tx.error.E_CONTRACT.desc": "Contract '{0}' does not exists", + "tx.error.E_CONTRACT.desc": "Contract '{0}' does not exist", "tx.error.E_DELETEDKEY.desc": "Your account has been removed", "tx.error.E_GUEST_VIOLATION.desc": "Transaction can't be executed using demo mode", "tx.error.E_INVALID_PASSWORD.desc": "Invalid password", diff --git a/public/locales/ru-RU.json b/public/locales/ru-RU.json index 5b97dce9f..c6143a87d 100644 --- a/public/locales/ru-RU.json +++ b/public/locales/ru-RU.json @@ -79,11 +79,15 @@ "confirm": "Подтвердить", "contract": "Смарт-контракт", "contract.exec": "Вызвать контракт", + "editor": "Редактор", + "editor.app": "Приложение", "editor.block.create": "Создать блок", "editor.close.confirm": "Вы действительно хотите закрыть '{name}' без сохранения изменений?", "editor.close.all": "Закрыть все вкладки", + "editor.close.all.confirm": "Вы действительно хотите закрыть все вкладки без сохранения изменений?", "editor.close.saved": "Закрыть сохранённые", "editor.conditions.change": "Права на изменение", + "editor.contract.create": "Создать смарт-контракт", "editor.create": "Создать", "editor.execute": "Вызвать", "editor.menu": "Меню", @@ -92,7 +96,7 @@ "editor.page.name": "Название", "editor.param.add": "Добавить параметр", "editor.revert": "Сброс", - "editor.revert.confirm": "Вы действительно хотите сбросить все изменения?", + "editor.revert.confirm": "Вы действительно хотите сбросить все изменения в '{name}'?", "editor.save": "Сохранить", "editor.tool.developer": "Разработка", "editor.tool.designer": "Дизайн", @@ -122,6 +126,7 @@ "general.home": "Домашняя страница", "general.key.private": "Закрытый ключ", "general.key.public": "Открытый ключ", + "general.main_menu": "Главное меню", "general.network.id.short": "ID", "general.network.id.auto": "Автоматическое обнаружение", "general.network.add": "Добавить сеть", @@ -180,18 +185,17 @@ "map.editor": "Редактирование карты", "map.meter.short": "м", "modal.authorization.title": "Авторизация", - "modal.authorization.password": "Введите пароль для вызова транзакции {contract}", + "modal.authorization.password": "Пожалуйста, введите пароль для выполнения данного действия", "modal.confirm.title": "Подтверждение", "modal.imageeditor.cancel": "Отмена", "modal.imageeditor.confirm": "Подтверждение", "modal.imageeditor.desc": "Произведите обработку вашего изображения для загрузки, выбрав необходимую область", "modal.imageeditor.title": "Редактирование изображения", - "modal.locale.title": "Выбор языка", "modal.network.remove.confirmation": "Вы действительно хотите удалить сеть \"{name}\"?", - "navigation.back": "Назад", - "navigation.forward": "Вперёд", - "navigation.home": "Домашняя страница", + "navigation.default_page": "Главная страница", + "navigation.loaded_page": "Загруженная страница", "navigation.refresh": "Обновить", + "navigation.return": "Назад", "notifications": "Уведомления", "notification.ecosystem_invited": "Добавлена экосистема {ecosystem}", "notification.tx_batch": "Пакетная обработка завершена", diff --git a/src/app/components/Animation/Dropdown.tsx b/src/app/components/Animation/Dropdown.tsx index 25031efe0..105d803e3 100644 --- a/src/app/components/Animation/Dropdown.tsx +++ b/src/app/components/Animation/Dropdown.tsx @@ -3,12 +3,13 @@ * See LICENSE in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as React from 'react'; +import React from 'react'; import Transition from 'react-transition-group/Transition'; -const animationDuration = 300; +const animationDuration = 200; const containerAnimationDef = { defaultStyle: { + opacity: 1, position: 'absolute', overflow: 'hidden', zIndex: 600 @@ -42,22 +43,26 @@ const containerAnimationDef = { const animationDef = { defaultStyle: { - transition: `transform ${animationDuration}ms cubic-bezier(0,0.5,0,1)`, - transform: 'translateY(-100%)', - marginTop: '0' + transition: `transform ${animationDuration}ms cubic-bezier(0,0.5,0.5,1), opacity ${animationDuration}ms`, + transform: 'translateY(-25%)', + marginTop: '0', + opacity: 0 }, entering: { - transform: 'translateY(0)' + transform: 'translateY(0)', + opacity: 1 }, entered: { - transform: 'translateY(0)' + transform: 'translateY(0)', + opacity: 1 }, // We use negative margin to make children unclickable and not to break // animation that will be used later exited: { - transform: 'translateY(-100%)', - marginTop: '-100000px' + transform: 'translateY(-25%)', + marginTop: '-100000px', + opacity: 0 } }; diff --git a/src/app/components/App.tsx b/src/app/components/App.tsx new file mode 100644 index 000000000..13dfa236d --- /dev/null +++ b/src/app/components/App.tsx @@ -0,0 +1,110 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import React from 'react'; +import { INetworkEndpoint } from 'apla/auth'; +import { Route } from 'react-router-dom'; +import { FormattedMessage, IntlProvider } from 'react-intl'; +import { mainRoute } from 'lib/routing'; +import platform from 'lib/platform'; +import classnames from 'classnames'; +import baseTheme from 'components/Theme/baseTheme'; + +import { AnimatedSwitch } from 'components/Animation'; +import themed from 'components/Theme/themed'; +import Auth from 'containers/Auth'; +import Error from 'containers/Auth/Error'; +import Splash from 'components/Splash'; +import ModalProvider from 'containers/Modal/ModalProvider'; +import NotificationsProvider from 'containers/Notifications/NotificationsProvider'; +import SecurityWarning from 'containers/SecurityWarning'; +import ThemeProvider from 'components/Theme/ThemeProvider'; +import Titlebar from 'components/Titlebar'; +import Main from './Main'; + +interface AppProps { + network: INetworkEndpoint; + locale: string; + isSessionAcquired: boolean; + isAuthenticated: boolean; + isLoaded: boolean; + isFatal: boolean; + securityWarningClosed: boolean; + localeMessages: { [key: string]: string }; + initialize?: () => void; +} + +const ThemedApp = themed.div` + &.platform-windows { + border: solid 1px ${props => props.theme.windowBorder}; + } +`; + +const StyledTitlebar = themed.div` + background: ${props => props.theme.headerBackground}; + height: ${props => props.theme.headerHeight}px; + line-height: ${props => props.theme.headerHeight}px; + font-size: 15px; + color: #fff; + text-align: center; +`; + +class App extends React.Component { + componentDidMount() { + this.props.initialize(); + } + + render() { + const appTitle = `Apla ${this.props.network ? '(' + this.props.network.apiHost + ')' : ''}`; + const classes = classnames({ + 'wrapper': true, + 'layout-fixed': true, + 'platform-desktop': platform.select({ desktop: true }), + 'platform-web': platform.select({ web: true }), + 'platform-windows': platform.select({ win32: true }) + }); + + return ( + + + + + {appTitle} + + + + + + {platform.select({ + web: !this.props.securityWarningClosed && ( + + + + ) + })} + + + {this.props.isFatal && ( + + )} + {!this.props.isLoaded && ( + + )} + {!this.props.isAuthenticated && ( + + )} + {!this.props.isSessionAcquired && ( + + )} +
} /> + + + + + ); + } +} + +export default App; \ No newline at end of file diff --git a/src/app/components/App/index.tsx b/src/app/components/App/index.tsx deleted file mode 100644 index 6371cb656..000000000 --- a/src/app/components/App/index.tsx +++ /dev/null @@ -1,86 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import React from 'react'; -import { Route } from 'react-router-dom'; -import { FormattedMessage } from 'react-intl'; -import platform from 'lib/platform'; -import classnames from 'classnames'; -import themed from 'components/Theme/themed'; - -import { AnimatedSwitch } from 'components/Animation'; -import Main from 'containers/Main'; -import Auth from 'containers/Auth'; -import Error from 'containers/Auth/Error'; -import Splash from 'components/Splash'; -import InitHook from 'containers/App/InitHook'; -import ModalProvider from 'containers/Modal/ModalProvider'; -import NotificationsProvider from 'containers/Notifications/NotificationsProvider'; -import SecurityWarning from 'containers/SecurityWarning'; - -interface IAppProps { - isAuthenticated: boolean; - isLoaded: boolean; - isCollapsed: boolean; - isFatal: boolean; - securityWarningClosed: boolean; - switchWindow: (wnd: string) => void; -} - -const ThemedApp = themed.div` - &.platform-windows { - border: solid 1px ${props => props.theme.windowBorder}; - } -`; - -class App extends React.Component { - componentWillReceiveProps(props: IAppProps) { - if (this.props.isAuthenticated !== props.isAuthenticated) { - props.switchWindow(props.isAuthenticated ? 'main' : 'general'); - } - } - - render() { - const classes = classnames({ - 'wrapper': true, - 'layout-fixed': true, - 'aside-collapsed': this.props.isCollapsed, - 'aside-toggled': !this.props.isCollapsed, - 'platform-desktop': platform.select({ desktop: true }), - 'platform-web': platform.select({ web: true }), - 'platform-windows': platform.select({ win32: true }) - }); - - return ( - - - - - {platform.select({ - web: !this.props.securityWarningClosed && ( - - - - ) - })} - - - {this.props.isFatal && ( - - )} - {!this.props.isLoaded && ( - - )} - {!this.props.isAuthenticated && ( - - )} - - - - ); - } -} - -export default App; \ No newline at end of file diff --git a/src/app/components/Auth/Heading/BackButton.tsx b/src/app/components/Auth/Heading/BackButton.tsx index 8ab95fb15..25173ebd1 100644 --- a/src/app/components/Auth/Heading/BackButton.tsx +++ b/src/app/components/Auth/Heading/BackButton.tsx @@ -7,7 +7,6 @@ import * as React from 'react'; import styled from 'styled-components'; import { Link } from 'react-router-dom'; import { FormattedMessage } from 'react-intl'; -import platform from 'lib/platform'; export interface IBackButtonProps { className?: string; @@ -32,12 +31,12 @@ const BackButton: React.SFC = props => props.returnUrl ? ); -export default styled(BackButton) ` +export default styled(BackButton)` text-decoration: none !important; border: 0; background: 0; padding: 0; - color: ${platform.select({ desktop: '#4085dc', web: '#fff' })}; + color: #fff; font-size: 14px; &:hover { diff --git a/src/app/components/Auth/Heading/NetworkIndicator.tsx b/src/app/components/Auth/Heading/NetworkIndicator.tsx index 4739883e9..c835bf5f8 100644 --- a/src/app/components/Auth/Heading/NetworkIndicator.tsx +++ b/src/app/components/Auth/Heading/NetworkIndicator.tsx @@ -5,7 +5,6 @@ import React from 'react'; import styled from 'styled-components'; -import platform from 'lib/platform'; import { Link } from 'react-router-dom'; import { FormattedMessage } from 'react-intl'; @@ -31,7 +30,7 @@ export default styled(NetworkIndicator)` border: 0; background: 0; padding: 0; - color: ${platform.select({ desktop: '#4085dc', web: '#fff' })}; + color: #fff; font-size: 14px; &:hover { diff --git a/src/app/components/Auth/Heading/index.tsx b/src/app/components/Auth/Heading/index.tsx index 030f753ce..0e10d3941 100644 --- a/src/app/components/Auth/Heading/index.tsx +++ b/src/app/components/Auth/Heading/index.tsx @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import React from 'react'; -import platform from 'lib/platform'; import themed from 'components/Theme/themed'; import BackButton from './BackButton'; @@ -43,7 +42,7 @@ const Heading: React.SFC = props => ( ); export default themed(Heading)` - background: ${props => platform.select({ web: props.theme.headerBackground, desktop: 'transparent' })}; + background: ${props => props.theme.headerBackground}; margin: -15px -15px 15px; padding: 0 15px; height: 45px; @@ -67,7 +66,7 @@ export default themed(Heading)` } .heading-title { - color: ${props => platform.select({ web: props.theme.headerForeground, desktop: '#000' })}; + color: ${props => props.theme.headerForeground}; font-size: 18px; } } diff --git a/src/app/components/Auth/Wallet/Create.tsx b/src/app/components/Auth/Wallet/Create.tsx index 33e221f21..7ecc8641f 100644 --- a/src/app/components/Auth/Wallet/Create.tsx +++ b/src/app/components/Auth/Wallet/Create.tsx @@ -5,6 +5,8 @@ import React from 'react'; import { injectIntl, FormattedMessage, InjectedIntlProps } from 'react-intl'; +import { readTextFile } from 'lib/fs'; +import keyring from 'lib/keyring'; import LocalizedDocumentTitle from 'components/DocumentTitle/LocalizedDocumentTitle'; import Generator from './Generator'; @@ -12,18 +14,13 @@ import Validation from 'components/Validation'; import HeadingNetwork from 'containers/Auth/HeadingNetwork'; export interface ICreateProps { - seed: string; - seedConfirm: string; onCreate: (params: { seed: string, password: string }) => void; - onGenerateSeed: () => void; - onChangeSeed: (seed: string) => void; - onChangeSeedConfirmation: (value: string) => void; - onImportSeed: (file: File) => void; - onImportSeedConfirmation: (file: File) => void; onDownloadSeed: (seed: string) => void; } interface ICreateState { + seed: string; + seedConfirmation: string; isConfirming: boolean; password: string; passwordConfirm: string; @@ -36,15 +33,13 @@ class Create extends React.Component { this.setState({ isConfirming: false @@ -54,12 +49,11 @@ class Create extends React.Component { if (this.state.isConfirming) { this.props.onCreate({ - seed: this.props.seedConfirm, + seed: this.state.seedConfirmation, password: this.state.passwordConfirm }); } else { - this.props.onChangeSeedConfirmation(''); this.setState({ isConfirming: true, password: values.password @@ -68,11 +62,21 @@ class Create extends React.Component { - this.props.onGenerateSeed(); + this.setState({ + seed: keyring.generateSeed() + }); + } + + onSeedChange = (seed: string) => { + this.setState({ + seed + }); } - onSeedConfirmationChange = (seedConfirm: string) => { - this.props.onChangeSeedConfirmation(seedConfirm); + onSeedConfirmationChange = (seedConfirmation: string) => { + this.setState({ + seedConfirmation + }); } onPasswordChange = (password: string) => { @@ -88,19 +92,27 @@ class Create extends React.Component { - this.props.onDownloadSeed(this.props.seed); + this.props.onDownloadSeed(this.state.seed); } onLoad = () => { this._inputFile.click(); } - onLoadSuccess = (e: React.ChangeEvent) => { - if (this.state.isConfirming) { - this.props.onImportSeedConfirmation(e.target.files[0]); + onLoadSuccess = async (e: React.ChangeEvent) => { + try { + const content = await readTextFile(e.target.files[0]); + this._inputFile.setAttribute('value', ''); + + if (this.state.isConfirming) { + this.onSeedConfirmationChange(content); + } + else { + this.onSeedChange(content); + } } - else { - this.props.onImportSeed(e.target.files[0]); + catch (e) { + // Fall back silently } } @@ -116,11 +128,11 @@ class Create extends React.Component {!this.state.isConfirming && ( void; onConfirm: (params: { backup: string, password: string }) => void; } @@ -34,17 +32,7 @@ class Import extends React.Component { + onSubmit = () => { this.props.onConfirm({ backup: this.state.backup, password: this.state.password @@ -67,8 +55,18 @@ class Import extends React.Component) => { - this.props.onImportBackup(e.target.files[0]); + onLoadSuccess = async (e: React.ChangeEvent) => { + try { + const backup = await readTextFile(e.target.files[0]); + this._inputFile.setAttribute('value', ''); + + this.setState({ + backup + }); + } + catch (e) { + // Fall back silently + } } render() { diff --git a/src/app/components/Auth/index.tsx b/src/app/components/Auth/index.tsx index 5f72b0615..5eb925d02 100644 --- a/src/app/components/Auth/index.tsx +++ b/src/app/components/Auth/index.tsx @@ -7,14 +7,13 @@ import React from 'react'; import { Route, Switch, Redirect } from 'react-router-dom'; import { FormattedMessage, InjectedIntlProps, injectIntl } from 'react-intl'; import imgLogo from 'images/logo.svg'; -import platform from 'lib/platform'; import themed from 'components/Theme/themed'; -import Titlebar from 'components/Main/Titlebar'; import Wallet from 'components/Auth/Wallet'; import Login from 'containers/Auth/Login'; import NetworkList from 'containers/Auth/Login/NetworkList'; import AddNetwork from 'containers/Auth/Login/NetworkList/AddNetwork'; +import { Button } from 'react-bootstrap'; export interface IAuthProps { className?: string; @@ -27,13 +26,6 @@ const Auth: React.SFC = props => (
- {platform.select({ - desktop: ( -
- -
- ) - })}
@@ -44,58 +36,54 @@ const Auth: React.SFC = props => (
- {platform.select({ - web: ( -
-
-
- - - - -
-
-
- - - -
+
+
+
+ + + +
- ) - })} +
+
+ +
+
); export default themed(injectIntl(Auth))` - display: ${platform.select({ web: 'table', desktop: 'block' })}; + display: table; width: 100%; height: 100%; max-height: 100%; overflow: hidden; .auth-window-container { - display: ${platform.select({ web: 'table-cell', desktop: 'block' })}; + display: table-cell; vertical-align: middle; overflow: hidden; height: 100%; .auth-window { text-align: center; - max-width: ${platform.select({ web: '600px', desktop: 'none' })}; - height: ${platform.select({ web: 'auto', desktop: '100%' })}; - padding: ${platform.select({ web: '10px', desktop: '0' })}; - padding-top: ${props => platform.select({ web: '0', desktop: props.theme.headerHeight + 'px' })}; + max-width: 600px; + height: auto; + padding: 10px; + padding-top: 0; margin: 0 auto; .panel { - height: ${platform.select({ web: 'auto', desktop: '100%' })}; + height: auto; overflow: hidden; background: #fff; border: 0; diff --git a/src/app/components/Avatar/index.tsx b/src/app/components/Avatar/index.tsx index 69159930a..374eee793 100644 --- a/src/app/components/Avatar/index.tsx +++ b/src/app/components/Avatar/index.tsx @@ -51,7 +51,7 @@ export default styled(Avatar) ` height: ${props => props.size ? props.size + 'px' : 'auto'}; .avatar__image { - max-width: ${props => props.size ? props.size + 'px' : 'none'}; - max-height: ${props => props.size ? props.size + 'px' : 'none'}; + max-width: ${props => props.size ? props.size + 'px' : '100%'}; + max-height: ${props => props.size ? props.size + 'px' : '100%'}; } `; \ No newline at end of file diff --git a/src/app/components/Button/DropdownButton.tsx b/src/app/components/Button/DropdownButton.tsx new file mode 100644 index 000000000..1050fefd4 --- /dev/null +++ b/src/app/components/Button/DropdownButton.tsx @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import React from 'react'; +import propTypes from 'prop-types'; +import onClickOutside, { InjectedOnClickOutProps } from 'react-onclickoutside'; + +import Dropdown from 'components/Dropdown'; +import Button from './'; + +type ButtonComponent = + React.ComponentType<{ onClick: (e: React.MouseEvent) => void, disabled?: boolean, className?: string }>; + +interface Props { + buttonComponent?: ButtonComponent; + disabled?: boolean; + className?: string; + active?: boolean; + content: React.ReactNode; + align?: 'left' | 'right'; + menuWidth?: number; +} + +interface State { + active: boolean; +} + +class DropdownButton extends React.Component { + state: State = { + active: false + }; + + static childContextTypes = { + closeDropdown: propTypes.func.isRequired + }; + + getChildContext = () => ({ + closeDropdown: () => { + this.setState({ + active: false + }); + } + }) + + handleClick = () => { + this.setState({ + active: !this.state.active + }); + } + + handleClickOutside = (_event: React.MouseEvent) => { + this.setState({ + active: false + }); + } + + render() { + const Component = this.props.buttonComponent || Button; + return ( +
+ + {this.props.children} + + + {this.props.content} + +
+ ); + } +} + +export default onClickOutside(DropdownButton); \ No newline at end of file diff --git a/src/app/components/Main/Toolbar/SectionToolButton.tsx b/src/app/components/Button/SegmentButton.tsx similarity index 89% rename from src/app/components/Main/Toolbar/SectionToolButton.tsx rename to src/app/components/Button/SegmentButton.tsx index 1af104f13..0b2b8eeae 100644 --- a/src/app/components/Main/Toolbar/SectionToolButton.tsx +++ b/src/app/components/Button/SegmentButton.tsx @@ -8,7 +8,7 @@ import classNames from 'classnames'; import themed from 'components/Theme/themed'; -export interface ISectionToolButtonProps { +interface Props { className?: string; disabled?: boolean; activeIndex: number; @@ -16,7 +16,7 @@ export interface ISectionToolButtonProps { onChange?: (index: number) => void; } -const SectionToolButton: React.SFC = props => ( +const SegmentButton: React.SFC = props => (
    {props.items.map((l, i) => (
  • @@ -28,7 +28,7 @@ const SectionToolButton: React.SFC = props => (
); -const StyledSectionToolButton = themed(SectionToolButton)` +const StyledSegmentButton = themed(SegmentButton)` border: solid 1px ${props => props.theme.sectionButtonOutline}; border-radius: 2px; list-style-type: none; @@ -36,6 +36,8 @@ const StyledSectionToolButton = themed(SectionToolButton)` margin: 0; font-size: 0; display: inline-block; + line-height: 20px; + height: 22px; li { display: inline-block; @@ -66,4 +68,4 @@ const StyledSectionToolButton = themed(SectionToolButton)` } `; -export default StyledSectionToolButton; \ No newline at end of file +export default StyledSegmentButton; \ No newline at end of file diff --git a/src/app/components/Checkbox/index.tsx b/src/app/components/Checkbox/index.tsx deleted file mode 100644 index 32bc22387..000000000 --- a/src/app/components/Checkbox/index.tsx +++ /dev/null @@ -1,29 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as React from 'react'; - -interface ICheckboxProps { - id?: string; - title?: string; - disabled?: boolean; - className?: string; - defaultChecked?: boolean; - onChange?: React.ChangeEventHandler; - readOnly?: boolean; - checked?: boolean; -} - -const Checkbox: React.SFC = (props) => ( -
- -
-); - -export default Checkbox; \ No newline at end of file diff --git a/src/app/components/DocumentTitle/LocalizedDocumentTitle.tsx b/src/app/components/DocumentTitle/LocalizedDocumentTitle.tsx index 6659bd12f..d6b97cb3f 100644 --- a/src/app/components/DocumentTitle/LocalizedDocumentTitle.tsx +++ b/src/app/components/DocumentTitle/LocalizedDocumentTitle.tsx @@ -14,15 +14,9 @@ export interface ILocalizedDocumentTitleProps extends IDocumentTitleProps { const LocalizedDocumentTitle: React.SFC = props => ( {props.children} diff --git a/src/app/components/DocumentTitle/index.tsx b/src/app/components/DocumentTitle/index.tsx index 01990a3b3..4a337caa3 100644 --- a/src/app/components/DocumentTitle/index.tsx +++ b/src/app/components/DocumentTitle/index.tsx @@ -3,7 +3,7 @@ * See LICENSE in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as React from 'react'; +import React from 'react'; import { injectIntl, InjectedIntlProps } from 'react-intl'; import NativeDocumentTitle from 'react-document-title'; @@ -12,13 +12,19 @@ export interface IDocumentTitleProps { } const DocumentTitle: React.SFC = props => { - const staticTitle = props.intl.formatMessage({ - id: 'general.title', - defaultMessage: 'Apla' - }); + const title = props.title ? + props.intl.formatMessage({ + id: 'general.title.format', + defaultMessage: '{title} | Apla' + }, { title: props.title }) : + + props.intl.formatMessage({ + id: 'general.title', + defaultMessage: 'Apla' + }); return ( - + {props.children} ); diff --git a/src/app/components/Dropdown/Heading.tsx b/src/app/components/Dropdown/Heading.tsx new file mode 100644 index 000000000..e2725510a --- /dev/null +++ b/src/app/components/Dropdown/Heading.tsx @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import themed from 'components/Theme/themed'; + +export default themed.div` + border-top: solid 1px ${props => props.theme.dropdownMenuSeparator}; + height: 30px; + line-height: 30px; + padding: 0 12px; + font-size: 11px; + text-transform: uppercase; + color: ${props => props.theme.dropdownMenuSecondary}; + + &:first-child { + border-top: none; + } +`; \ No newline at end of file diff --git a/src/app/components/Dropdown/Info.tsx b/src/app/components/Dropdown/Info.tsx new file mode 100644 index 000000000..9e10096e8 --- /dev/null +++ b/src/app/components/Dropdown/Info.tsx @@ -0,0 +1,12 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import themed from 'components/Theme/themed'; + +export default themed.p` + font-size: 14px; + padding: 0 15px; + color: ${props => props.theme.dropdownMenuPrimary}; +`; \ No newline at end of file diff --git a/src/app/components/Dropdown/Item.tsx b/src/app/components/Dropdown/Item.tsx new file mode 100644 index 000000000..aa48a9edd --- /dev/null +++ b/src/app/components/Dropdown/Item.tsx @@ -0,0 +1,96 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import React from 'react'; +import themed from 'components/Theme/themed'; +import classNames from 'classnames'; +import propTypes from 'prop-types'; + +const StyledItem = themed.button` + border-radius: 0; + outline: 0; + border: 0; + background: 0; + transition: background .15s; + width: 100%; + padding: 0 12px; + margin: 0; + height: 40px; + line-height: 40px; + font-size: 13px; + font-weight: 500; + text-decoration: none; + color: ${props => props.theme.dropdownMenuForeground}; + cursor: pointer; + display: block; + text-align: left; + border: dashed 1px transparent; + position: relative; + white-space: nowrap; + + &.item_hasicon { + padding-left: 40px; + } + + > .item__icon { + position: absolute; + left: 12px; + top: 0; + bottom: 0; + + > em { + font-weight: 500; + font-size: 15px; + line-height: 40px; + margin-right: 12px; + } + } + + &[disabled] { + color: ${props => props.theme.dropdownMenuDisabled}; + } + + &:hover { + background: ${props => props.theme.dropdownMenuActive}; + } + + &:focus { + border-color: #84baff; + } +`; + +interface Props { + icon?: string; + disabled?: boolean; + onClick?: React.MouseEventHandler; +} + +const Item: React.SFC = (props, context) => { + const handleClick: React.MouseEventHandler = e => { + context.closeDropdown(); + if (props.onClick) { + props.onClick(e); + } + }; + + return ( + + {props.icon && ( +
+ +
+ )} +
+ {props.children} +
+
+ ); +}; + +Item.contextTypes = { + closeDropdown: propTypes.func.isRequired +}; + +export default Item; \ No newline at end of file diff --git a/src/app/components/Dropdown/index.tsx b/src/app/components/Dropdown/index.tsx new file mode 100644 index 000000000..bcfe4eea1 --- /dev/null +++ b/src/app/components/Dropdown/index.tsx @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import React from 'react'; + +import themed from 'components/Theme/themed'; +import DropdownAnimation from 'components/Animation/Dropdown'; + +const StyledDropdown = themed.div` + display: inline-block; + position: relative; + line-height: normal; + background: ${props => props.theme.dropdownMenuBackground}; + box-shadow: 0 0 15px rgba(0,0,0,.15); + border-radius: 4px; + border-top: none; + text-align: left; + overflow: hidden; +`; + +interface Props { + className?: string; + active?: boolean; + align?: 'left' | 'right'; + width?: number; +} + +const Dropdown: React.SFC = props => ( + + + {props.children} + + +); + +export default Dropdown; \ No newline at end of file diff --git a/src/app/components/DropdownButton/index.tsx b/src/app/components/DropdownButton/index.tsx deleted file mode 100644 index 31ba1ce7b..000000000 --- a/src/app/components/DropdownButton/index.tsx +++ /dev/null @@ -1,227 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import React from 'react'; -import classNames from 'classnames'; -import propTypes from 'prop-types'; - -import themed from 'components/Theme/themed'; -import Dropdown from 'components/Animation/Dropdown'; -import onClickOutside, { InjectedOnClickOutProps } from 'react-onclickoutside'; - -const StyledDropdown = themed.div` - display: inline-block; - position: relative; - - &.dropdown-active button.dropdown-toggle { - background: rgba(0,0,0,0.15); - } - - button.dropdown-toggle { - min-width: 40px; - height: 40px; - line-height: 40px; - padding: 0 10px; - text-align: center; - position: relative; - z-index: 1000; - - .dropdown-badge { - position: absolute; - bottom: 0; - right: 0; - background: #d46565; - display: block; - width: 16px; - height: 16px; - padding: 0; - line-height: 16px; - font-size: 12px; - font-weight: bold; - } - } - - .dropdown-content { - line-height: normal; - background: ${props => props.theme.dropdownMenuBackground}; - box-shadow: 0 0 25px rgba(0,0,0,.15); - border: solid 1px ${props => props.theme.dropdownMenuOutline}; - border-top: none; - text-align: left; - - &.dropdown-leftmost { - border-left: none; - } - - &.dropdown-rightmost { - border-right: none; - } - - &.icon-left .dropdown-group > li button > .icon { - float: left; - margin-right: 12px; - } - - .dropdown-heading { - border-top: solid 1px ${props => props.theme.dropdownMenuSeparator}; - height: 30px; - line-height: 30px; - padding: 0 15px; - font-size: 11px; - text-transform: uppercase; - color: ${props => props.theme.dropdownMenuSecondary}; - - &:first-child { - border-top: none; - } - } - - .dropdown-info { - font-size: 14px; - padding: 0 15px; - color: ${props => props.theme.dropdownMenuPrimary}; - } - - .dropdown-group { - list-style: none; - padding: 0; - margin: 0; - - > li { - a { - text-decoration: none; - } - - button { - border-radius: 0; - outline: 0; - border: 0; - background: 0; - transition: background .15s; - width: 100%; - padding: 0 15px !important; - margin: 0; - height: 40px; - line-height: 40px; - font-size: 13px; - font-weight: 500; - text-decoration: none; - color: ${props => props.theme.dropdownMenuForeground}; - cursor: pointer; - display: block; - text-align: left; - - > .icon { - float: right; - font-weight: 500; - font-size: 14px; - line-height: 40px; - } - - &[disabled] { - color: ${props => props.theme.dropdownMenuDisabled}; - } - - &:hover { - background: ${props => props.theme.dropdownMenuActive}; - } - - &:focus { - outline: dashed 1px #84baff !important; - } - } - } - } - } -`; - -export interface IDropdownButtonProps { - content: React.ReactNode; - className?: string; - leftMost?: boolean; - rightMost?: boolean; - align?: 'left' | 'right'; - width?: number; - badge?: number; - disabled?: boolean; - onClick?: React.EventHandler>; -} - -interface IDropdownButtonState { - active: boolean; -} - -class DropdownButton extends React.Component { - static childContextTypes = { - closeDropdown: propTypes.func.isRequired - }; - - constructor(props: IDropdownButtonProps & InjectedOnClickOutProps) { - super(props); - this.state = { - active: false - }; - } - - getChildContext() { - return { - closeDropdown: this.onCollapseToggle.bind(this, false) - }; - } - - handleClickOutside(event: React.MouseEvent) { - this.onCollapseToggle(false); - } - - onCollapseToggle(active?: boolean) { - this.setState({ - active: undefined === active ? !this.state.active : active - }); - } - - onClick(e: React.MouseEvent) { - this.onCollapseToggle(); - if (this.props.onClick) { - this.props.onClick(e); - } - } - - render() { - return ( - - - -
- {this.props.content} -
-
-
- ); - } -} - -export const CloseDropdownButton: React.SFC, HTMLButtonElement>> = (props, context: { closeDropdown: () => void }) => { - const onClick: React.MouseEventHandler = e => { - context.closeDropdown(); - if (props.onClick) { - props.onClick(e); - } - }; - - return ( - - - - )} - > - - - - - - ); - } - - renderSecond() { - return ( - -
- -
- - )} - > - - - - - - - - - - - - - - - - - - - -
- - - - {this.state.privateKey} - - - - -
- - {this.state.publicKey}
- - {this.props.account && this.props.account.address}
- - -
- -
- -
-
-
-
- ); - } - - render() { - return ( - - ) - }} - description={ - - } - > - {this.props.account && ( - - {this.state.privateKey ? this.renderSecond() : this.renderFirst()} - - )} - - ); - } -} - -export default Backup; \ No newline at end of file diff --git a/src/app/components/Main/Editor/Designer/index.tsx b/src/app/components/Main/Editor/Designer/index.tsx index 9f93a4f36..69461890a 100644 --- a/src/app/components/Main/Editor/Designer/index.tsx +++ b/src/app/components/Main/Editor/Designer/index.tsx @@ -20,6 +20,7 @@ import TreeTheme from './Tree/Theme'; import imgGrid from 'images/constructor/grid.png'; interface IConstructorProps { + section: string; pageTree: any; treeData: any; page?: any; @@ -42,7 +43,6 @@ interface IConstructorProps { canUndo: boolean; canRedo: boolean; - navigatePage?: (params: { name: string, params?: any }) => void; menus?: { id: string, name: string, conditions: string, value: string }[]; onSave?: (block: string, error?: { type: string, error: string }) => void; canSave?: boolean; @@ -348,6 +348,7 @@ class Constructor extends React.Component ) => void; } -const EditorTab: React.SFC = props => { +const EditorTab: React.SFC = props => { const onClose = (e: React.MouseEvent) => { e.stopPropagation(); if (props.onClose) { @@ -58,7 +58,7 @@ const EditorTab: React.SFC = props => { ); }; -const StyledEditorTab = themed(EditorTab) ` +const StyledEditorTab = themed(EditorTab)` position: relative; background: #ddd; color: #7a899c; diff --git a/src/app/components/Main/Editor/EditorTabs.tsx b/src/app/components/Main/Editor/EditorTabs.tsx index a0a458bd9..c75242b37 100644 --- a/src/app/components/Main/Editor/EditorTabs.tsx +++ b/src/app/components/Main/Editor/EditorTabs.tsx @@ -6,14 +6,14 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; import { TEditorTab } from 'apla/editor'; -import imgSim from './sim.svg'; -import imgTpl from './tpl.svg'; +import imgSim from 'components/Editor/simvolio/icon.svg'; +import imgTpl from 'components/Editor/protypo/icon.svg'; import themed from 'components/Theme/themed'; import EditorTab from './EditorTab'; -import SystemButton from 'components/Main/SystemButton'; -import { CloseDropdownButton } from 'components/DropdownButton'; +import HeaderButton from '../Header/HeaderButton'; import ScrollView from 'components/ScrollView'; +import Item from 'components/Dropdown/Item'; export const TYPE_ICONS: { [type: string]: string } = { contract: imgSim, @@ -23,32 +23,20 @@ export const TYPE_ICONS: { [type: string]: string } = { default: null }; -const StyledTabsMenu = themed.div` - background: ${props => props.theme.editorBackground}; - position: absolute; - top: 0px; - right: 0px; - width: 40px; - - & button.dropdown-toggle { - height: 36px; - } -`; - -export interface IEditorTabsProps { +interface Props { className?: string; tabIndex: number; tabs: TEditorTab[]; - onChange: (index: number) => void; - onClose: (index: number) => void; + onChange: (uuid: string) => void; + onClose: (uuid: string) => void; onCloseAll: () => void; onCloseSaved: () => void; } -const EditorTabs: React.SFC = (props) => ( -
-
- +const EditorTabs: React.SFC = (props) => ( +
+
+
    {props.tabs.map((tab, index) => ( = (props) => ( key={index} active={props.tabIndex === index} icon={TYPE_ICONS[tab.type] || TYPE_ICONS.default} - onClick={props.onChange.bind(null, index)} - onClose={props.onClose.bind(null, index)} + onClick={props.onChange.bind(null, tab.uuid)} + onClose={props.onClose.bind(null, tab.uuid)} /> ))}
- - + -
    -
  • - - - - - - -
  • -
  • - - - - - - -
  • -
+ + + + + +
} > - - + +
); const StyledEditorTabs = themed(EditorTabs)` background: ${props => props.theme.editorBackground}; height: 36px; - margin-right: 40px; - - .editor-scroll-area { + position: relative; + + .editortabs__scrollarea { ul { height: 36px; list-style-type: none; @@ -112,6 +87,25 @@ const StyledEditorTabs = themed(EditorTabs)` white-space: nowrap; } } + + .editortabs__selector { + height: 100%; + margin-right: 40px; + } + + .editortabs__menu { + background: ${props => props.theme.editorBackground}; + position: absolute; + top: 0; + right: 0; + + } + + .editortabs__menubutton { + width: 36px; + height: 36px; + line-height: 36px; + } `; export default StyledEditorTabs; \ No newline at end of file diff --git a/src/app/components/Main/Editor/EditorTool.tsx b/src/app/components/Main/Editor/EditorTool.tsx new file mode 100644 index 000000000..7e2ac8a28 --- /dev/null +++ b/src/app/components/Main/Editor/EditorTool.tsx @@ -0,0 +1,52 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import React from 'react'; +import { TEditorTab } from 'apla/editor'; + +import ConstructorTabbed from 'containers/Main/Editor/ConstructorTabbed'; +import Page from 'components/Main/Navigator/Page'; + +interface Props { + mainSection: string; + value: TEditorTab; +} + +const EditorTool: React.SFC = props => { + switch (props.value.tool) { + case 'constructor': + return ( + + ); + + case 'preview': + return ( +
+ +
+ ); + + default: + return null; + } +}; + +export default EditorTool; \ No newline at end of file diff --git a/src/app/components/Main/Toolbar/EditorToolbar.tsx b/src/app/components/Main/Editor/EditorToolbar.tsx similarity index 55% rename from src/app/components/Main/Toolbar/EditorToolbar.tsx rename to src/app/components/Main/Editor/EditorToolbar.tsx index 6d236e8a4..609e68e67 100644 --- a/src/app/components/Main/Toolbar/EditorToolbar.tsx +++ b/src/app/components/Main/Editor/EditorToolbar.tsx @@ -3,14 +3,18 @@ * See LICENSE in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as React from 'react'; +import React from 'react'; import { FormattedMessage } from 'react-intl'; import { TEditorTab } from 'apla/editor'; -import ToolButton from './ToolButton'; -import SectionToolButton from './SectionToolButton'; +import Toolbar, { Filler } from '../Toolbar'; +import ToolButton from 'components/Main/Toolbar/ToolButton'; +import SegmentButton from 'components/Button/SegmentButton'; +import DropdownToolButton from '../Toolbar/DropdownToolButton'; +import Heading from 'components/Dropdown/Heading'; +import Item from 'components/Dropdown/Item'; -export interface IEditorToolbarProps { +interface Props { currentTab: TEditorTab; canSave: boolean; canRevert: boolean; @@ -18,6 +22,7 @@ export interface IEditorToolbarProps { onToolChange: (tool: string) => void; onExec: () => void; onSave: () => void; + onCreateTab: (type: string) => void; } const editorTools = [ @@ -45,7 +50,7 @@ const resolveToolIndex = (tool: string) => { return editorTools.findIndex(l => l.type === tool); }; -const EditorToolbar: React.SFC = props => { +const EditorToolbar: React.SFC = props => { const onToolChange = (toolIndex: number) => { const toolDef = editorTools[toolIndex]; if (toolDef) { @@ -54,31 +59,53 @@ const EditorToolbar: React.SFC = props => { }; return ( -
+ + + props.onCreateTab('contract')}> + + + + + + props.onCreateTab('page')}> + + + props.onCreateTab('menu')}> + + + props.onCreateTab('block')}> + + +
+ } + > + + - {/* - - */} + {props.currentTab && 'contract' === props.currentTab.type && ( + + + + )} + {props.currentTab && 'contract' !== props.currentTab.type && ( -
  • - + l.content)} /> -
  • - )} - {props.currentTab && 'contract' === props.currentTab.type && ( - - - + )} - + ); }; diff --git a/src/app/components/Main/Editor/index.tsx b/src/app/components/Main/Editor/index.tsx index abbd322fa..9e49a3f8c 100644 --- a/src/app/components/Main/Editor/index.tsx +++ b/src/app/components/Main/Editor/index.tsx @@ -6,71 +6,51 @@ import React from 'react'; import { TEditorTab } from 'apla/editor'; -import CodeEditor from 'components/Editor'; +import CodeEditor from 'components/Editor/CodeEditor'; import EditorTabs from './EditorTabs'; -import Page from 'components/Main/Page'; -import ConstructorTabbed from 'containers/Main/Editor/ConstructorTabbed'; +import EditorTool from './EditorTool'; +import EditorToolbar from 'containers/Toolbar/EditorToolbar'; +import LocalizedDocumentTitle from 'components/DocumentTitle/LocalizedDocumentTitle'; -export interface IEditorProps { +interface Props { + mainSection: string; tabIndex: number; tabs: TEditorTab[]; - onTabChange: (index: number) => void; - onTabUpdate: (value: string) => void; - onTabClose: (index: number) => void; - onTabCloseAll: () => void; - onTabCloseSaved: () => void; + onTabChange?: (uuid: string) => void; + onTabUpdate?: (value: string) => void; + onTabClose?: (uuid: string) => void; + onTabCloseAll?: () => void; + onTabCloseSaved?: () => void; } -class Editor extends React.Component { - renderTool(tab: TEditorTab) { - switch (tab.tool) { - case 'constructor': - return ( - - ); - - case 'preview': - return ( -
    - = props => ( + +
    + + + {props.tabs.map((tab, index) => ( +
    +
    +
    - ); - - default: - return null; - } - } - - render() { - return ( -
    - - {this.props.tabs.map((tab, index) => ( -
    -
    - -
    - {index === this.props.tabIndex && 'editor' !== tab.tool ? this.renderTool(tab) : null} -
    - ))} -
    - ); - } -} + {index === props.tabIndex && 'editor' !== tab.tool ? ( + + ) : null} +
    + ))} +
    +
    +); export default Editor; \ No newline at end of file diff --git a/src/app/components/Main/Header/HeaderButton.tsx b/src/app/components/Main/Header/HeaderButton.tsx new file mode 100644 index 000000000..5ce320472 --- /dev/null +++ b/src/app/components/Main/Header/HeaderButton.tsx @@ -0,0 +1,80 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import React from 'react'; +import classNames from 'classnames'; + +import themed from 'components/Theme/themed'; +import DropdownButton from 'components/Button/DropdownButton'; + +interface Props { + className?: string; + disabled?: boolean; + warning?: boolean; + badge?: number; + align?: 'left' | 'right'; + menuWidth?: number; + content: React.ReactNode; +} + +const StyledHeaderButton = themed(DropdownButton)` + background: 0; + padding: 0; + border: 0; + outline: 0; + transition: background ease-in-out .17s; + min-width: ${props => props.theme.menubarSize}px; + height: ${props => props.theme.menubarSize}px; + line-height: ${props => props.theme.menubarSize}px; + + &:hover { + background: ${props => props.theme.menubarBackgroundFocused}; + } + + &._warning { + background: ${props => props.theme.menubarBackgroundSecondary}; + color: ${props => props.theme.menubarForegroundActive}; + } + + &._active { + color: ${props => props.theme.menubarForegroundActive}; + } + + .dropdown__badge { + position: absolute; + bottom: 0; + right: 0; + background: #d46565; + display: block; + width: 16px; + height: 16px; + padding: 0; + line-height: 16px; + font-size: 12px; + font-weight: bold; + } +`; + +const HeaderButton: React.SFC = props => ( + + {props.children} + {props.badge ? ( + + {Math.min(props.badge, 99)} + + ) : null} + +); + +export default HeaderButton; \ No newline at end of file diff --git a/src/app/components/Main/Toolbar/ToolIndicator.tsx b/src/app/components/Main/Header/HeaderIndicator.tsx similarity index 80% rename from src/app/components/Main/Toolbar/ToolIndicator.tsx rename to src/app/components/Main/Header/HeaderIndicator.tsx index 674955944..5f8585cd2 100644 --- a/src/app/components/Main/Toolbar/ToolIndicator.tsx +++ b/src/app/components/Main/Header/HeaderIndicator.tsx @@ -8,7 +8,7 @@ import themed from 'components/Theme/themed'; import Tooltip from 'components/Tooltip'; -export interface IToolIndicatorProps { +interface Props { icon: string; className?: string; right?: boolean; @@ -16,7 +16,7 @@ export interface IToolIndicatorProps { titleDesc: JSX.Element | string; } -const ToolIndicator: React.SFC = props => ( +const HeaderIndicator: React.SFC = props => (
  • @@ -27,7 +27,7 @@ const ToolIndicator: React.SFC = props => (
  • ); -const StyledToolButton = themed(ToolIndicator)` +const StyledHeaderIndicator = themed(HeaderIndicator)` display: inline-block; vertical-align: top; text-align: center; @@ -42,7 +42,7 @@ const StyledToolButton = themed(ToolIndicator)` font-weight: 300; em.tool-icon { - color: ${props => props.theme.toolbarIconColor}; + color: ${props => props.theme.menubarForeground}; transition: color .15s; vertical-align: middle; height: 18px; @@ -51,15 +51,15 @@ const StyledToolButton = themed(ToolIndicator)` > span.button-label { margin-left: 8px; - color: ${props => props.theme.toolbarForeground}; + color: ${props => props.theme.menubarForeground}; } &:hover { em.tool-icon { - color: ${props => props.theme.toolbarIconHighlight}; + color: ${props => props.theme.menubarForegroundActive}; } } } `; -export default StyledToolButton; \ No newline at end of file +export default StyledHeaderIndicator; \ No newline at end of file diff --git a/src/app/components/Main/Header/HeaderLink.tsx b/src/app/components/Main/Header/HeaderLink.tsx new file mode 100644 index 000000000..2e10281a2 --- /dev/null +++ b/src/app/components/Main/Header/HeaderLink.tsx @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import React from 'react'; +import { Link } from 'react-router-dom'; +import classNames from 'classnames'; + +import themed from 'components/Theme/themed'; + +const StyledHeaderLink = themed(Link)` + position: relative; + border-radius: 0; + padding: 9px 0 2px 0; + margin: 0 0 8px 0; + outline: 0; + border: 0; + background: 0; + color: ${props => props.theme.menubarForeground}; + font-size: 15px; + font-weight: 600; + transition: background .15s; + display: inline-block; + border-bottom: solid 2px transparent; + line-height: normal; + text-align: center; + transition: color ease-in-out .12s; + + && { + text-decoration: none; + } + + &:hover { + color: ${props => props.theme.menubarForegroundActive}; + } + + &.active { + border-bottom-color: ${props => props.theme.menubarBackgroundActive}; + color: ${props => props.theme.menubarForegroundActive}; + } +`; + +interface Props { + to: string; + active?: boolean; +} + +const HeaderLink: React.SFC = props => ( + + {props.children} + +); + +export default HeaderLink; \ No newline at end of file diff --git a/src/app/components/Main/Header/HeaderSpacer.tsx b/src/app/components/Main/Header/HeaderSpacer.tsx new file mode 100644 index 000000000..f87f4d0c1 --- /dev/null +++ b/src/app/components/Main/Header/HeaderSpacer.tsx @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) EGAAS S.A. All rights reserved. +* See LICENSE in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import themed from 'components/Theme/themed'; + +export default themed.em` + &:after { + content: '|'; + display: inline-block; + margin: -1px 15px 0; + color: ${props => props.theme.menubarForeground}; + line-height: ${props => props.theme.menubarSize}px; + } +`; \ No newline at end of file diff --git a/src/app/components/Main/Header/LangMenu.tsx b/src/app/components/Main/Header/LangMenu.tsx new file mode 100644 index 000000000..fddf798e8 --- /dev/null +++ b/src/app/components/Main/Header/LangMenu.tsx @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import React from 'react'; +import { ILocale } from 'apla'; + +import HeaderButton from './HeaderButton'; +import Heading from 'components/Dropdown/Heading'; +import Item from 'components/Dropdown/Item'; + +interface Props { + locale: string; + locales: ILocale[]; + onChange?: (locale: string) => void; +} + +const LangMenu: React.SFC = props => ( + + Language +
    + {props.locales.map(locale => ( + props.onChange(locale.key)}>{locale.name} + ))} +
    +
    + )} + > + + +); + +export default LangMenu; \ No newline at end of file diff --git a/src/app/components/Main/NotificationsMenu.tsx b/src/app/components/Main/Header/NotificationsMenu.tsx similarity index 75% rename from src/app/components/Main/NotificationsMenu.tsx rename to src/app/components/Main/Header/NotificationsMenu.tsx index cff65eff7..99c4e0ca1 100644 --- a/src/app/components/Main/NotificationsMenu.tsx +++ b/src/app/components/Main/Header/NotificationsMenu.tsx @@ -4,27 +4,30 @@ *--------------------------------------------------------------------------------------------*/ import React from 'react'; -import Protypo from 'containers/Widgets/Protypo'; import { FormattedMessage } from 'react-intl'; import { TProtypoElement } from 'apla/protypo'; -import SystemButton from './SystemButton'; +import HeaderButton from './HeaderButton'; +import Protypo from 'containers/Widgets/Protypo'; +import Heading from 'components/Dropdown/Heading'; +import Info from 'components/Dropdown/Info'; -export interface INotificationsMenuProps { +interface Props { offline: boolean; count: number; + mainSection: string; notificationsBody: TProtypoElement[]; } -const NotificationsMenu: React.SFC = props => ( - = props => ( + -
    + {props.offline ? ( @@ -32,15 +35,15 @@ const NotificationsMenu: React.SFC = props => ( ) } -
    +
    {props.offline ? ( -

    + -

    + ) : ( - + ) }
    @@ -54,7 +57,7 @@ const NotificationsMenu: React.SFC = props => ( ) } -
    + ); export default NotificationsMenu; \ No newline at end of file diff --git a/src/app/components/Main/TransactionsMenu.tsx b/src/app/components/Main/Header/TransactionsMenu.tsx similarity index 77% rename from src/app/components/Main/TransactionsMenu.tsx rename to src/app/components/Main/Header/TransactionsMenu.tsx index 40598ed10..208ecd51c 100644 --- a/src/app/components/Main/TransactionsMenu.tsx +++ b/src/app/components/Main/Header/TransactionsMenu.tsx @@ -3,18 +3,18 @@ * See LICENSE in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as React from 'react'; +import React from 'react'; -import DropdownButton from 'components/DropdownButton'; +import DropdownButton from 'components/Button/DropdownButton'; -export interface ITransactionsMenuProps { +interface Props { } -const TransactionsMenu: React.SFC = props => ( +const TransactionsMenu: React.SFC = props => (
    Content goes here
    diff --git a/src/app/components/Main/Header/UserMenu.tsx b/src/app/components/Main/Header/UserMenu.tsx new file mode 100644 index 000000000..094c702c4 --- /dev/null +++ b/src/app/components/Main/Header/UserMenu.tsx @@ -0,0 +1,139 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import React from 'react'; +import { FormattedMessage } from 'react-intl'; +import { IAccountContext } from 'apla/auth'; +import { IEcosystemInfo } from 'apla/api'; + +import HeaderButton from './HeaderButton'; +import Avatar from 'containers/Avatar'; +import themed from 'components/Theme/themed'; +import Item from 'components/Dropdown/Item'; +import Heading from 'components/Dropdown/Heading'; + +const StyledUserMenu = themed.div` + -webkit-app-region: no-drag; + line-height: 0; + display: inline-block; + vertical-align: top; + height: 40px; + padding: 0 0 0 10px !important; + line-height: 18px; + color: #fff; + + > .user-info { + text-align: right; + float: left; + margin-right: 5px; + white-space: nowrap; + max-width: 170px; + + > .user-title { + margin-top: 4px; + font-size: 14px; + font-weight: 600; + overflow: hidden; + text-overflow: ellipsis; + } + + > .user-subtitle { + text-transform: uppercase; + font-size: 13px; + font-weight: 300; + overflow: hidden; + text-overflow: ellipsis; + } + } + + > .user-avatar { + float: right; + margin: 4px; + } + + .user-dropdown { + background: #fff; + box-shadow: 0 0 25px rgba(0,0,0,.15); + border-left: solid 1px #add1ff; + border-bottom: solid 1px #add1ff; + } +`; + +interface Props { + isDefaultWallet: boolean; + wallet?: IAccountContext; + walletEcosystems: IEcosystemInfo[]; + onSwitchEcosystem: (ecosystem: string, defaultRole?: boolean) => void; + onLogout: () => void; + onChangePassword: () => void; + onBackup: () => void; +} + +const UserMenu: React.SFC = props => props.wallet && props.wallet.wallet && ( + + {!props.isDefaultWallet && ( + <> + + + + + + + + )} + + + + + + + {props.walletEcosystems.map(value => ( + props.onSwitchEcosystem(value.ecosystem, !value.roles.length)}> + {value.name ? + ( + value.name + ) : + ( + + ) + } + + ))} + + } + > + +
    +
    + {props.isDefaultWallet ? + ( + + ) : + ( + {props.wallet.wallet.address} + ) + } +
    +
    + {props.wallet.access.name || ( + + )} +
    +
    + +
    +
    +); + +export default UserMenu; \ No newline at end of file diff --git a/src/app/components/Main/Header/index.tsx b/src/app/components/Main/Header/index.tsx new file mode 100644 index 000000000..cd346af48 --- /dev/null +++ b/src/app/components/Main/Header/index.tsx @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) EGAAS S.A. All rights reserved. +* See LICENSE in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import React from 'react'; +import { FormattedMessage } from 'react-intl'; + +import themed from 'components/Theme/themed'; +import HeaderIndicator from './HeaderIndicator'; +import NotificationsMenu from 'containers/Main/Header/NotificationsMenu'; +import UserMenu from 'containers/Main/Header/UserMenu'; +import imgLogo from 'images/logoHeader.svg'; +import Selector from 'containers/Main/Navigator/Sections/Selector'; +import HeaderSpacer from './HeaderSpacer'; +import HeaderLink from './HeaderLink'; +import LangMenu from 'containers/Main/Header/LangMenu'; + +interface Props { + app?: string; + page?: string; + isAuthorized: boolean; +} + +const StyledHeader = themed.header` + background: url(${imgLogo}) 20px center ${props => props.theme.menubarBackground} no-repeat; + height: ${props => props.theme.menubarSize}px; + color: ${props => props.theme.menubarForeground}; + transition: width .32s ease-in-out; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 8000; + display: flex; + flex-direction: row; + padding-left: 80px; + + .header__filler { + flex: 1; + } +`; + +const Header: React.SFC = props => ( + + + + + + + +
    + + {props.isAuthorized && ( + } + titleDesc={} + /> + )} + + + {/**/} + + +); + +export default Header; \ No newline at end of file diff --git a/src/app/components/Main/LoadingBar.tsx b/src/app/components/Main/LoadingBar.tsx deleted file mode 100644 index 44463cf48..000000000 --- a/src/app/components/Main/LoadingBar.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import React from 'react'; -import LoadingBarNative from 'react-redux-loading-bar'; -import { withTheme } from 'styled-components'; -import { IThemeDefinition } from 'apla/theme'; -import themed from 'components/Theme/themed'; - -export interface ILoadingBarProps { - theme: IThemeDefinition; -} - -const StyledLoadingBar = themed(LoadingBarNative)` - position: absolute; - bottom: 0; - right: 0; - left: 0; -`; - -const LoadingBar: React.SFC = props => ( - -); - -export default withTheme(LoadingBar); \ No newline at end of file diff --git a/src/app/components/Main/Navigation/ResizeHandle.tsx b/src/app/components/Main/Navigation/ResizeHandle.tsx deleted file mode 100644 index 4c5e7ed80..000000000 --- a/src/app/components/Main/Navigation/ResizeHandle.tsx +++ /dev/null @@ -1,120 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import React from 'react'; -import classNames from 'classnames'; - -import themed from 'components/Theme/themed'; - -interface IResizeHandleProps { - width: number; - resizing: boolean; - disabled: boolean; - setResizing: (resizing: boolean) => void; - navigationResize: (width: number) => void; - navigationToggle: () => void; -} - -export const styles = { - hoverWidth: 10, - initialWidth: 2, - extendWidth: 2 -}; - -const StyledResizeHandle = themed.button` - position: absolute; - top: 0; - bottom: 0; - right: ${styles.initialWidth / 2}px; - text-align: center; - width: ${styles.hoverWidth}px; - outline: 0; - border: 0; - background: none; - padding: 0; - margin: 0; - cursor: col-resize; - margin-right: -${styles.hoverWidth / 2}px; - - &.disabled { - cursor: default; - } - - > div { - position: absolute; - top: 0; - bottom: 0; - right: ${(styles.hoverWidth / 2) - (styles.initialWidth / 2)}px; - margin: 0; - width: ${styles.initialWidth}px; - background: ${props => props.theme.menuOutline}; - transition: all .16s; - } - - &:hover > div, &.active > div { - width: ${styles.extendWidth * 2 + styles.initialWidth}px; - margin: 0 -${styles.extendWidth}px 0 0; - } -`; - -class ResizeHandle extends React.Component { - private _mouseUpListenerBind: (e: MouseEvent) => void; - private _mouseMoveListenerBind: (e: MouseEvent) => void; - private _clickThresholdValue: number; - private _clickThreshold = 1; - - componentDidMount() { - this._mouseMoveListenerBind = this.onMouseMove.bind(this); - this._mouseUpListenerBind = this.onMouseUp.bind(this); - document.body.addEventListener('mousemove', this._mouseMoveListenerBind); - document.body.addEventListener('mouseup', this._mouseUpListenerBind); - } - - componentWillUnmount() { - document.body.removeEventListener('mousemove', this._mouseMoveListenerBind); - } - - onMouseMove(e: MouseEvent) { - if (!this.props.disabled && this.props.resizing && e.clientX !== this.props.width) { - this.props.navigationResize(e.clientX); - this._clickThresholdValue++; - } - } - - onMouseDown(e: React.MouseEvent) { - if (!this.props.disabled && !this.props.resizing && 0 === e.button) { - this.props.setResizing(true); - this._clickThresholdValue = 0; - } - } - - onMouseUp(e: MouseEvent) { - if (!this.props.disabled && this.props.resizing && 0 === e.button) { - this.props.setResizing(false); - } - } - - onClick() { - // TODO: Temporarily disabled - if (!this.props.disabled && this._clickThresholdValue < this._clickThreshold) { - // this.props.navigationToggle(); - } - } - - render() { - const classes = classNames({ - disabled: this.props.disabled, - active: this.props.resizing - }); - - return ( - this.onMouseDown(e)} className={classes}> -
    - - ); - } -} - -export default ResizeHandle; \ No newline at end of file diff --git a/src/app/components/Main/Navigation/index.tsx b/src/app/components/Main/Navigation/index.tsx deleted file mode 100644 index f3670081e..000000000 --- a/src/app/components/Main/Navigation/index.tsx +++ /dev/null @@ -1,152 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import React from 'react'; -import { injectIntl, InjectedIntlProps } from 'react-intl'; -import { TMenu } from 'apla/content'; -import platform from 'lib/platform'; - -import StackGroup from 'components/Animation/StackGroup'; -import themed from 'components/Theme/themed'; -import Protypo from 'containers/Widgets/Protypo'; -import ResizeHandle from 'containers/Main/Navigation/ResizeHandle'; -import ScrollView from 'components/ScrollView'; - -const StyledNavigation = themed.aside` - &.navigation-collapsed { - overflow: hidden; - width: 0; - } - - .scrollarea { - background: ${props => props.theme.menuBackground}; - height: 100%; - - .scrollbar-container { - opacity: 0; - } - } - - position: absolute; - top: ${platform.select({ win32: '1px' }) || 0}; - left: ${platform.select({ win32: '1px' }) || 0}; - bottom: ${platform.select({ win32: '1px' }) || 0}; - z-index: 150; -`; - -const StyledBackButton = themed.button` - position: relative; - display: block; - width: 100%; - height: 58px; - padding: 10px 25px; - color: ${props => props.theme.menuPrimaryForeground}; - font-weight: 300; - text-decoration: none; - outline: none; - border: none; - text-align: left; - background: transparent; - - &.disabled { - &:hover { - color: ${props => props.theme.menuPrimaryForeground}; - } - } - - &:hover { - color: ${props => props.theme.menuPrimaryActive}; - } - - .icon { - vertical-align: top; - display: inline-block; - width: 30px; - } - - em { - font-size: 15px; - } - - span { - font-size: 21px; - font-weight: 300; - } -`; - -const StyledMenu = themed.div` - overflow: hidden; - position: absolute; - bottom: 0px; - left: 0; - right: 0; - top: ${props => props.theme.headerHeight + props.theme.menuHeight + props.theme.toolbarHeight}px; -`; - -const StyledMenuContent = themed.div` - position: absolute; - top: 0; - right: 0; - left: 0; - bottom: 0; - background: ${props => props.theme.menuBackground}; - - > div { - background: ${props => props.theme.menuBackground}; - } - - .title-wrap { - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - } -`; - -export interface INavigationProps { - preloading: boolean; - preloadingError: string; - visible: boolean; - width: number; - menus: TMenu[]; - menuPop: () => void; -} - -class Navigation extends React.Component { - render() { - return ( - - - - - ); - } -} - -export default injectIntl(Navigation); \ No newline at end of file diff --git a/src/app/components/Main/Navigator/Menu/ResizeHandle.tsx b/src/app/components/Main/Navigator/Menu/ResizeHandle.tsx new file mode 100644 index 000000000..8c81596cc --- /dev/null +++ b/src/app/components/Main/Navigator/Menu/ResizeHandle.tsx @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import React from 'react'; + +import themed from 'components/Theme/themed'; + +interface Props { + onFoldToggle: () => void; +} + +const styles = { + // Active hover zone at each size + hoverWidth: 11, + + // Visible border width + borderSize: 2 +}; + +const StyledResizeHandle = themed.button` + position: absolute; + top: 0; + bottom: 0; + left: 0; + text-align: center; + width: ${styles.hoverWidth * 2}px; + outline: 0; + border: 0; + background: none; + padding: 0; + margin: 0; + margin-left: ${-styles.hoverWidth}px; + z-index: 105; + + &.disabled { + cursor: default; + } + + > div { + position: absolute; + top: 0; + bottom: 0; + left: ${styles.hoverWidth - styles.borderSize / 2}px; + margin: 0; + width: ${styles.borderSize}px; + transition: background .16s; + } + + &:hover > div { + background: ${props => props.theme.menuBorder}; + } +`; + +const ResizeHandle: React.SFC = props => ( + +
    + +); + +export default ResizeHandle; \ No newline at end of file diff --git a/src/app/components/Main/Navigator/Menu/index.tsx b/src/app/components/Main/Navigator/Menu/index.tsx new file mode 100644 index 000000000..8999d6213 --- /dev/null +++ b/src/app/components/Main/Navigator/Menu/index.tsx @@ -0,0 +1,146 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import React from 'react'; +import { IMenu } from 'apla/content'; + +import StackGroup from 'components/Animation/StackGroup'; +import themed from 'components/Theme/themed'; +import Protypo from 'containers/Widgets/Protypo'; +import ScrollView from 'components/ScrollView'; +import classNames from 'classnames'; +import { FormattedMessage } from 'react-intl'; + +const StyledNavigation = themed.aside` + background: ${props => props.theme.menuBackground}; + width: ${props => props.theme.menuSize}px; + transition: width ease-in-out .12s; + + .scrollarea { + background: ${props => props.theme.menuBackground}; + height: 100%; + + .scrollbar-container { + opacity: 0; + } + } + + position: absolute; + top: 0; + left: 0; + bottom: 0; +`; + +const StyledBackButton = themed.button` + position: relative; + display: block; + width: 100%; + height: 58px; + padding: 10px 20px; + color: ${props => props.theme.menuPrimaryForeground}; + font-weight: 300; + text-decoration: none; + outline: none; + border: none; + text-align: left; + background: transparent; + + &.disabled { + &:hover { + color: ${props => props.theme.menuPrimaryForeground}; + } + } + + &:hover { + color: ${props => props.theme.menuPrimaryActive}; + } + + .icon { + vertical-align: top; + display: inline-block; + width: 35px; + } + + em { + font-size: 15px; + } + + span { + font-size: 21px; + font-weight: 300; + } +`; + +const StyledMenu = themed.div` + overflow: hidden; + position: absolute; + bottom: 0; + left: 0; + right: 0; + top: 0; +`; + +const StyledMenuContent = themed.div` + position: absolute; + top: 0; + right: 0; + left: 0; + bottom: 0; + background: ${props => props.theme.menuBackground}; + z-index: 90; + + .title-wrap { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } +`; + +interface Props { + menus: IMenu[]; + section: string; + active: boolean; + folded: boolean; + menuPop: () => void; + onMouseOver?: () => void; + onMouseLeave?: () => void; +} + +const Menu: React.SFC = props => ( + + + +); + +export default Menu; \ No newline at end of file diff --git a/src/app/components/Main/Page/Error.tsx b/src/app/components/Main/Navigator/Page/Error.tsx similarity index 100% rename from src/app/components/Main/Page/Error.tsx rename to src/app/components/Main/Navigator/Page/Error.tsx diff --git a/src/app/components/Main/Page/NotFound.tsx b/src/app/components/Main/Navigator/Page/NotFound.tsx similarity index 99% rename from src/app/components/Main/Page/NotFound.tsx rename to src/app/components/Main/Navigator/Page/NotFound.tsx index 081335240..bf588c599 100644 --- a/src/app/components/Main/Page/NotFound.tsx +++ b/src/app/components/Main/Navigator/Page/NotFound.tsx @@ -19,7 +19,7 @@ const NotFound: React.SFC = (props) => (

    - +

    diff --git a/src/app/components/Main/Page/Timeout.tsx b/src/app/components/Main/Navigator/Page/Timeout.tsx similarity index 100% rename from src/app/components/Main/Page/Timeout.tsx rename to src/app/components/Main/Navigator/Page/Timeout.tsx diff --git a/src/app/components/Main/Navigator/Page/index.tsx b/src/app/components/Main/Navigator/Page/index.tsx new file mode 100644 index 000000000..e8655892a --- /dev/null +++ b/src/app/components/Main/Navigator/Page/index.tsx @@ -0,0 +1,67 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import React from 'react'; +import Protypo from 'containers/Widgets/Protypo'; +import { IPage } from 'apla/content'; +import { STATIC_PAGES } from 'lib/staticPages'; + +import themed from 'components/Theme/themed'; +import Error from './Error'; +import Timeout from './Timeout'; +import NotFound from './NotFound'; +import DocumentTitle from 'components/DocumentTitle'; + +export interface IPageProps { + section: string; + value: IPage; +} + +const StyledPage = themed.article` + display: flex; + flex: 1; + flex-direction: column; + min-height: 0; + background-color: ${props => props.theme.contentBackground}; + overflow-x: hidden; + overflow-y: auto; +`; + +const Page: React.SFC = props => { + if (props.value.error) { + switch (props.value.error) { + case 'E_HEAVYPAGE': return (); + case 'E_NOTFOUND': return (); + default: return (); + } + } + else { + const staticPage = STATIC_PAGES[props.value.name]; + const title = props.value.location && props.value.location.state && props.value.location.state.from && props.value.location.state.from.title; + + return ( + + + {props.value.static && ( + staticPage.render(props.section, { + ...props.value.params, + children: props.value && props.value.content + }) + )} + {!props.value.static && ( + + )} + + + ); + } +}; + +export default Page; \ No newline at end of file diff --git a/src/app/components/Main/Navigator/Sections/Breadcrumb.tsx b/src/app/components/Main/Navigator/Sections/Breadcrumb.tsx new file mode 100644 index 000000000..0fdeeb8be --- /dev/null +++ b/src/app/components/Main/Navigator/Sections/Breadcrumb.tsx @@ -0,0 +1,80 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import React from 'react'; +import PageLink from 'containers/Routing/PageLink'; +import themed from 'components/Theme/themed'; +import { FormattedMessage } from 'react-intl'; + +interface Props { + home?: boolean; + active?: boolean; + section: string; + page: string; + params: { + [key: string]: string; + }; +} + +const StyledBreadcrumb = themed.div` + vertical-align: top; + display: inline-block; + + &.breadcrumb_active { + .breadcrumb__label, .breadcrumb__icon { + color: ${props => props.theme.toolbarForegroundPrimary}; + } + } + + .breadcrumb__icon { + line-height: inherit; + font-size: 16px; + color: ${props => props.theme.toolbarForeground}; + } + + .breadcrumb__label { + line-height: inherit; + font-size: 14px; + color: ${props => props.theme.toolbarForeground}; + } +`; + +const placeholder = ( + +); + +const Breadcrumb: React.SFC = props => { + const titleText = props.children || placeholder; + const title = props.home ? + ( + + ) + : + ( + {titleText} + ); + + if (!props.active) { + return ( + + {title} + + ); + } + + return ( + + + {title} + + + ); +}; + +export default Breadcrumb; \ No newline at end of file diff --git a/src/app/components/Main/Navigator/Sections/Breadcrumbs.tsx b/src/app/components/Main/Navigator/Sections/Breadcrumbs.tsx new file mode 100644 index 000000000..37e220fa5 --- /dev/null +++ b/src/app/components/Main/Navigator/Sections/Breadcrumbs.tsx @@ -0,0 +1,86 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import React from 'react'; +import { IBreadcrumb } from 'apla/content'; +import { FormattedMessage } from 'react-intl'; + +import themed from 'components/Theme/themed'; +import Toolbar from 'components/Main/Toolbar'; +import Breadcrumb from './Breadcrumb'; +import ToolButton from 'components/Main/Toolbar/ToolButton'; + +interface Props { + values: IBreadcrumb[]; + onRefresh: () => void; +} + +const StyledBreadcrumbs = themed.ul` + height: 100%; + line-height: inherit; + margin: 0; + padding: 0 10px; + font-size: 0; + flex: 1; + + > li { + display: inline-block; + font-size: 13px; + font-weight: 400; + height: 100%; + line-height: inherit; + color: ${props => props.theme.toolbarForegroundActive}; + margin-right: 10px; + vertical-align: top; + + &:first-child:before { + content: none; + } + + &:before { + content: '/'; + font-size: 13px; + color: ${props => props.theme.toolbarSpacerForeground}; + display: inline-block; + margin-right: 8px; + } + } +`; + +const Breadcrumbs: React.SFC = (props) => ( + + + {props.values.map((breadcrumb, i) => ( +
  • + + {breadcrumb.title} + +
  • + ))} + {1 === props.values.length && ( +
  • + + + +
  • + )} +
    + + + +
    +); + +export default Breadcrumbs; \ No newline at end of file diff --git a/src/app/components/Main/Navigator/Sections/Section.tsx b/src/app/components/Main/Navigator/Sections/Section.tsx new file mode 100644 index 000000000..c155e6b3e --- /dev/null +++ b/src/app/components/Main/Navigator/Sections/Section.tsx @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import React from 'react'; +import classNames from 'classnames'; +import { IPage, IBreadcrumb } from 'apla/content'; + +import Page from '../Page'; +import themed from 'components/Theme/themed'; +import ResizeHandle from 'containers/Main/Navigator/Menu/ResizeHandle'; + +interface Props { + name: string; + page?: IPage; + breadcrumbs: IBreadcrumb[]; + folded: boolean; + menuActive: boolean; +} + +const StyledSection = themed.div` + background: ${props => props.theme.contentBackground}; + box-shadow: rgba(0,0,0,0.06) -5px 0 10px; + position: relative; + z-index: 100; + margin-left: ${props => props.theme.menuSize}px; + transition: margin-left ease-in-out .12s, transform ease-in-out .12s; + + &.section_folded { + margin-left: ${props => props.theme.menuSizeFolded}px; + + &.section_unfolded { + margin-left: 0; + transform: translateX(${props => props.theme.menuSize}px); + } + } +`; + +const Section: React.SFC = props => ( + + + {props.page && ( + + )} + +); + +export default Section; \ No newline at end of file diff --git a/src/app/components/Main/Navigator/Sections/SectionButton.tsx b/src/app/components/Main/Navigator/Sections/SectionButton.tsx new file mode 100644 index 000000000..e0a8eb4d0 --- /dev/null +++ b/src/app/components/Main/Navigator/Sections/SectionButton.tsx @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import React from 'react'; +import { generateRoute } from 'services/router'; + +import HeaderLink from 'components/Main/Header/HeaderLink'; + +interface Props { + section: string; + page: string; + params: { [name: string]: string }; + active?: boolean; +} + +const SectionButton: React.SFC = props => ( + + {props.children} + +); + +export default SectionButton; \ No newline at end of file diff --git a/src/app/components/Main/Navigator/Sections/Selector.tsx b/src/app/components/Main/Navigator/Sections/Selector.tsx new file mode 100644 index 000000000..50d1d7e60 --- /dev/null +++ b/src/app/components/Main/Navigator/Sections/Selector.tsx @@ -0,0 +1,63 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import React from 'react'; +import themed from 'components/Theme/themed'; + +import SectionButton from './SectionButton'; + +export interface ISelectorProps { + section: string; + values: { + index: number; + title: string; + name: string; + page: string; + params: { + [name: string]: string; + } + }[]; +} + +const StyledSelector = themed.ul` + margin: 0; + padding: 0; + height: 100%; + + > li { + list-style-type: none; + height: 100%; + display: inline-block; + padding 0 10px; + + &:first-child { + padding-left: 0; + } + + &:last-child { + padding-right: 0; + } + } +`; + +const Selector: React.SFC = (props) => ( + + {props.values.map(section => ( +
  • + + {section.title} + +
  • + + ))} +
    +); + +export default Selector; \ No newline at end of file diff --git a/src/app/components/Main/Navigator/Sections/index.tsx b/src/app/components/Main/Navigator/Sections/index.tsx new file mode 100644 index 000000000..9fb4a6abd --- /dev/null +++ b/src/app/components/Main/Navigator/Sections/index.tsx @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import React from 'react'; +import { ISection } from 'apla/content'; + +import Section from './Section'; +import Menu from 'containers/Main/Navigator/Menu'; + +interface Props { + section: string; + page: string; + folded: boolean; + menuActive: boolean; + values: { + [key: string]: ISection; + }; +} + +const Sections: React.SFC = (props) => ( +
    + +
    +
    +); + +export default Sections; \ No newline at end of file diff --git a/src/app/components/Main/Navigator/index.tsx b/src/app/components/Main/Navigator/index.tsx new file mode 100644 index 000000000..38a3fe830 --- /dev/null +++ b/src/app/components/Main/Navigator/index.tsx @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import React from 'react'; +import { ISection } from 'apla/content'; + +import themed from 'components/Theme/themed'; +import Sections from 'containers/Main/Navigator/Sections'; +import Breadcrumbs from './Sections/Breadcrumbs'; +import NotFound from './Page/NotFound'; + +const StyledWrapper = themed.div` + background-color: #f6f8fa; + position: relative; +`; + +interface Props { + section: string; + sections: { [key: string]: ISection }; + page: string; + stylesheet: string; + onRefresh?: () => void; +} + +const StyledContent = themed.section` + margin-left: 0 !important; + && { background: ${props => props.theme.contentBackground}; } + color: ${props => props.theme.contentForeground}; + transition: none !important; + overflow: hidden; +`; + +const Navigator: React.SFC = props => { + const section = props.sections[props.section]; + + return ( + + + + {section ? + ( + <> + + + + ) + : + ( + + ) + } + + + ); +}; + +export default Navigator; \ No newline at end of file diff --git a/src/app/components/Main/Page/index.tsx b/src/app/components/Main/Page/index.tsx deleted file mode 100644 index fdcde6b3e..000000000 --- a/src/app/components/Main/Page/index.tsx +++ /dev/null @@ -1,36 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as React from 'react'; -import Protypo from 'containers/Widgets/Protypo'; -import { TPage } from 'apla/content'; - -import DocumentTitle from 'components/DocumentTitle'; -import Error from './Error'; -import Timeout from './Timeout'; -import NotFound from './NotFound'; - -export interface IPageProps extends TPage { - -} - -const Page: React.SFC = (props) => { - if (props.error) { - switch (props.error) { - case 'E_HEAVYPAGE': return (); - case 'E_NOTFOUND': return (); - default: return (); - } - } - else { - return ( - - - - ); - } -}; - -export default Page; \ No newline at end of file diff --git a/src/app/components/Main/SectionButton.tsx b/src/app/components/Main/SectionButton.tsx deleted file mode 100644 index 457349fec..000000000 --- a/src/app/components/Main/SectionButton.tsx +++ /dev/null @@ -1,77 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as React from 'react'; -import * as classNames from 'classnames'; -import imgClose from 'images/close.svg'; - -import themed from 'components/Theme/themed'; - -const StyledSectionButton = themed.button` - position: relative; - border-radius: 0; - padding: 0 20px; - margin: 0; - outline: 0; - border: 0; - background: 0; - color: ${props => props.theme.headerForeground}; - font-size: 16px; - font-weight: 300; - transition: background .15s; - - &:hover { - background: rgba(0,0,0,0.1); - } - - &.active { - background: ${props => props.theme.headerBackgroundActive}; - color: ${props => props.theme.headerForegroundActive}; - } - - &.closeable { - padding-right: 0; - } - - .section-close { - padding: 0 10px 0 8px; - width: 0; - font-size: 15px; - opacity: 0.5; - transition: opacity ease-in-out .17s; - font-weight: bold; - - &:hover { - opacity: 1; - } - } -`; - -export interface ISectionButtonProps { - active?: boolean; - closeable?: boolean; - onClick?: () => void; - onClose?: () => void; -} - -const SectionButton: React.SFC = props => { - const onClose = props.closeable ? (e: React.MouseEvent) => { - e.stopPropagation(); - props.onClose(); - } : null; - - return ( - - {props.children} - {props.closeable && ( - - - - )} - - ); -}; - -export default SectionButton; \ No newline at end of file diff --git a/src/app/components/Main/SystemButton.tsx b/src/app/components/Main/SystemButton.tsx deleted file mode 100644 index 360b50004..000000000 --- a/src/app/components/Main/SystemButton.tsx +++ /dev/null @@ -1,50 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import React from 'react'; -import classNames from 'classnames'; - -import themed from 'components/Theme/themed'; -import DropdownButton, { IDropdownButtonProps } from 'components/DropdownButton'; - -export interface ISystemButtonProps extends IDropdownButtonProps { - warning?: boolean; -} - -const SystemButton: React.SFC = props => ( - - {props.children} - -); - -const StyledSystemButton = themed(SystemButton) ` - background: 0; - padding: 0; - border: 0; - outline: 0; - transition: background ease-in-out .17s; - - &:hover { - background: ${props => props.theme.systemButtonActive}; - } - - &._warning { - background: ${props => props.theme.systemButtonSecondary}; - } -`; - -export default StyledSystemButton; \ No newline at end of file diff --git a/src/app/components/Main/Titlebar/SystemMenu.tsx b/src/app/components/Main/Titlebar/SystemMenu.tsx deleted file mode 100644 index e26584790..000000000 --- a/src/app/components/Main/Titlebar/SystemMenu.tsx +++ /dev/null @@ -1,109 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import React from 'react'; -import { remote } from 'electron'; -import { FormattedMessage } from 'react-intl'; - -import themed from 'components/Theme/themed'; -import DropdownButton, { CloseDropdownButton } from 'components/DropdownButton'; - -const SystemDropdown = themed(DropdownButton)` - background: 0; - border: 0; - outline: 0; - color: #bad8ff; - width: ${props => props.theme.headerHeight}px !important; - height: ${props => props.theme.headerHeight}px !important; - padding: 0 !important; - margin: 0 !important; - outline: 0 !important; - min-width: 0 !important; - line-height: ${props => props.theme.headerHeight + 2}px !important; - transition: color ease-in-out .16s; - cursor: pointer !important; - - &:hover { - color: #fff; - } -`; - -const SystemButton = themed.button` - background: 0; - border: 0; - outline: 0; - color: #bad8ff; - width: ${props => props.theme.headerHeight}px !important; - height: ${props => props.theme.headerHeight}px !important; - padding: 0 !important; - margin: 0 !important; - outline: 0 !important; - min-width: 0 !important; - line-height: ${props => props.theme.headerHeight + 2}px !important; - transition: color ease-in-out .16s; - cursor: pointer !important; - - &:hover { - color: #fff; - } -`; - -export interface ISystemMenuProps { - align: 'left' | 'right'; - onAbout: () => void; - onChangeLocale: () => void; -} - -const SystemMenu: React.SFC = props => { - const elements = [ - ( - -
    Apla
    -
      -
    • - - - - - - -
    • -
    • - remote.getCurrentWindow().webContents.openDevTools({ mode: 'detach' })}> - - - - - -
    • -
    -
    - } - > - - - ), - ( - - - - ) - ]; - return ( -
    - {(props.align === 'left' ? elements : elements.reverse()).map((e, i) => ( - e - ))} -
    - ); -}; - -export default SystemMenu; \ No newline at end of file diff --git a/src/app/components/Main/Toolbar/DropdownToolButton.tsx b/src/app/components/Main/Toolbar/DropdownToolButton.tsx index ba7aca5a5..396536a4f 100644 --- a/src/app/components/Main/Toolbar/DropdownToolButton.tsx +++ b/src/app/components/Main/Toolbar/DropdownToolButton.tsx @@ -3,95 +3,32 @@ * See LICENSE in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as React from 'react'; -import styled from 'styled-components'; -import classNames from 'classnames'; +import React from 'react'; -import DropdownButton from 'components/DropdownButton'; +import DropdownButton from 'components/Button/DropdownButton'; +import ToolButton from './ToolButton'; +import themed from 'components/Theme/themed'; -export interface IDropdownToolButtonProps { - icon: string; - className?: string; +interface Props { disabled?: boolean; - right?: boolean; - leftMost?: boolean; + icon?: string; content: React.ReactNode; } -const DropdownToolButton: React.SFC = props => ( -
  • - - - {props.children && ({props.children})} - - -
  • -); - -const StyledDropdownToolButton = styled(DropdownToolButton) ` - display: inline-block; - vertical-align: top; - - &.disabled { - button { - > em.icon, em.icon-chevron { - color: #bcc8d6; - } - - > span.button-label { - color: #93a7bf; - } - } - } - - .dropdown-active .dropdown-toggle { - background: #e4e4e4 !important; - ${props => !props.leftMost && 'border-left: solid 1px #add1ff;padding-left: 9px;'} - } - - .dropdown-toggle { - text-align: center; - border-radius: 0; - min-width: 40px; - height: 40px; - outline: 0; - border: 0; - background: 0; - padding: 0 12px; - font-size: 14px; - font-weight: 300; - line-height: 40px; - transition: background .15s; - - > em.icon, .icon-chevron { - color: #5b97e4; - vertical-align: middle; - height: 18px; - display: inline-block; - } - - > em.icon-chevron { - font-size: 12px; - margin-left: 8px; - margin-top: 5px; - } - - > span.button-label { - margin-left: 8px; - color: #194a8a; - } - - &:hover { - background: rgba(0,0,0,0.03); - } - } +const DropdownChevron = themed.em` + font-size: 14px; + margin-left: 8px; `; -export default StyledDropdownToolButton; \ No newline at end of file +const DropdownToolButton: React.SFC = props => ( + } + disabled={props.disabled} + content={props.content} + > + {props.children} + + +); + +export default DropdownToolButton; \ No newline at end of file diff --git a/src/app/components/Main/Toolbar/ToolButton.tsx b/src/app/components/Main/Toolbar/ToolButton.tsx index 5df2f4611..8f52976c6 100644 --- a/src/app/components/Main/Toolbar/ToolButton.tsx +++ b/src/app/components/Main/Toolbar/ToolButton.tsx @@ -8,65 +8,67 @@ import classNames from 'classnames'; import themed from 'components/Theme/themed'; -export interface IToolButtonProps { - icon: string; - className?: string; +interface Props { disabled?: boolean; - right?: boolean; - onClick?: (e: React.MouseEvent) => void; + icon?: string; + onClick?: React.MouseEventHandler; } -const ToolButton: React.SFC = props => ( -
  • - -
  • -); - -const StyledToolButton = themed(ToolButton) ` - display: inline-block; - vertical-align: top; +const StyledToolButton = themed.button` + background: 0; + outline: 0; + border: 0; + padding: 0 10px; + height: ${props => props.theme.toolbarHeight}px; + line-height: inherit; - &.disabled { - button { - > em.icon, > span.button-label { - color: ${props => props.theme.toolbarDisabled}; - } - } + &:hover { + background: ${props => props.theme.toolbarBackgroundFocused}; } - > button { - text-align: center; - border-radius: 0; - min-width: 40px; - height: 40px; - outline: 0; - border: 0; - background: 0; - padding: 0 12px; - font-size: 14px; - font-weight: 300; - line-height: 40px; - transition: background .15s; + &:active { + background: ${props => props.theme.toolbarBackgroundActive}; + } - > em.icon { - color: ${props => props.theme.toolbarIconColor}; - vertical-align: middle; - height: 18px; - display: inline-block; + &:disabled { + &:hover, &:active { + background: 0; } - > span.button-label { - margin-left: 8px; - color: ${props => props.theme.toolbarForeground}; + .toolbutton__icon, .toolbutton__label { + color: ${props => props.theme.toolbarForegroundDisabled}; } + } - &:hover { - background: rgba(0,0,0,0.03); + .toolbutton__icon { + line-height: inherit; + margin-right: 7px; + font-size: 16px; + color: ${props => props.theme.toolbarForegroundPrimary}; + } + + .toolbutton__label { + vertical-align: top; + line-height: inherit; + font-size: 14px; + font-weight: 600; + color: ${props => props.theme.toolbarForeground}; + + > * { + vertical-align: top; } } `; -export default StyledToolButton; \ No newline at end of file +const ToolButton: React.SFC = props => ( + + {props.icon && ( + + )} + + {props.children} + + +); + +export default ToolButton; \ No newline at end of file diff --git a/src/app/components/Main/Toolbar/index.tsx b/src/app/components/Main/Toolbar/index.tsx index 0dbc0c1d9..f54a18b57 100644 --- a/src/app/components/Main/Toolbar/index.tsx +++ b/src/app/components/Main/Toolbar/index.tsx @@ -4,32 +4,32 @@ *--------------------------------------------------------------------------------------------*/ import React from 'react'; - import themed from 'components/Theme/themed'; -export interface IToolbarButton { - icon: string; - onClick?: (e: React.MouseEvent) => void; -} - -export interface IToolbarProps { - className?: string; -} - -const Toolbar: React.SFC = props => ( -
      +export const Filler: React.SFC = props => ( +
      {props.children} -
    + ); -const StyledToolbar = themed(Toolbar) ` +export default themed.div` + box-shadow: rgba(0,0,0,0.07) 0 2px 5px; background: ${props => props.theme.toolbarBackground}; - border-bottom: solid 2px ${props => props.theme.toolbarOutline}; - height: 40px; - list-style-type: none; + border-bottom: solid 1px ${props => props.theme.uiBorderLight}; + min-height: ${props => props.theme.toolbarHeight}px; + height: ${props => props.theme.toolbarHeight}px; + line-height: ${props => props.theme.toolbarHeight}px; + color: ${props => props.theme.toolbarForeground}; + padding: 0 10px; margin: 0; - padding: 0; - font-size: 0; -`; + position: relative; + z-index: 110; + white-space: nowrap; + display: flex; + flex-direction: row; + align-items: flex-start; -export default StyledToolbar; \ No newline at end of file + .toolbar__filler { + flex: 1; + } +`; \ No newline at end of file diff --git a/src/app/components/Main/UserMenu.tsx b/src/app/components/Main/UserMenu.tsx deleted file mode 100644 index 6c1c29dfc..000000000 --- a/src/app/components/Main/UserMenu.tsx +++ /dev/null @@ -1,168 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import React from 'react'; -import styled from 'styled-components'; -import { FormattedMessage } from 'react-intl'; -import { IAccountContext } from 'apla/auth'; -import { IEcosystemInfo } from 'apla/api'; - -import { CloseDropdownButton } from 'components/DropdownButton'; -import PageLink from 'containers/Routing/PageLink'; -import SystemButton from './SystemButton'; -import Avatar from 'containers/Avatar'; - -const StyledUserMenu = styled.div` - -webkit-app-region: no-drag; - line-height: 0; - display: inline-block; - vertical-align: top; - height: 40px; - padding: 0 0 0 10px !important; - line-height: 18px; - color: #fff; - - > .user-info { - text-align: right; - float: left; - margin-right: 5px; - white-space: nowrap; - max-width: 170px; - - > .user-title { - margin-top: 4px; - font-size: 14px; - font-weight: 600; - overflow: hidden; - text-overflow: ellipsis; - } - - > .user-subtitle { - text-transform: uppercase; - font-size: 13px; - font-weight: 300; - overflow: hidden; - text-overflow: ellipsis; - } - } - - > .user-avatar { - float: right; - margin: 4px; - } - - .user-dropdown { - background: #fff; - box-shadow: 0 0 25px rgba(0,0,0,.15); - border-left: solid 1px #add1ff; - border-bottom: solid 1px #add1ff; - } -`; - -export interface IUserMenuProps { - isDefaultWallet: boolean; - wallet: IAccountContext; - walletEcosystems: IEcosystemInfo[]; - onSwitchEcosystem: (ecosystem: string, defaultRole?: boolean) => void; - onLogout: () => void; - onChangePassword: () => void; -} - -class UserMenu extends React.Component { - render() { - return this.props.wallet && this.props.wallet.wallet ? ( - -
      - {!this.props.isDefaultWallet && ( - <> -
    • - - - - - - -
    • -
    • - - - - - - - - -
    • - - )} -
    • - - - - - - -
    • -
    -
    - -
    -
      - {this.props.walletEcosystems.map(value => ( -
    • - {/*wallet.ecosystem !== this.props.wallet.ecosystem && this.props.switchWallet.bind(this, wallet)*/} - this.props.onSwitchEcosystem(value.ecosystem, !value.roles.length)}> - {value.name ? - ( - value.name - ) : - ( - - ) - } - -
    • - ))} -
    - - } - > - -
    -
    - {this.props.isDefaultWallet ? - ( - - ) : - ( - {this.props.wallet.wallet.address} - ) - } -
    -
    - {this.props.wallet.access.name || ( - - )} -
    -
    - -
    -
    - ) : null; - } -} - -export default UserMenu; \ No newline at end of file diff --git a/src/app/components/Main/index.tsx b/src/app/components/Main/index.tsx index 290ca9ae8..83e11fe2e 100644 --- a/src/app/components/Main/index.tsx +++ b/src/app/components/Main/index.tsx @@ -4,184 +4,49 @@ *--------------------------------------------------------------------------------------------*/ import React from 'react'; -import _ from 'lodash'; -import { FormattedMessage } from 'react-intl'; -import { INetworkEndpoint } from 'apla/auth'; -import { TSection } from 'apla/content'; -import { history } from 'store'; -import platform from 'lib/platform'; +import { Redirect } from 'react-router'; +import { routes } from 'lib/routing'; import themed from 'components/Theme/themed'; -import Titlebar from './Titlebar'; -import UserMenu from 'containers/Widgets/UserMenu'; -import Navigation from 'containers/Main/Navigation'; -import NotificationsMenu from 'containers/Widgets/NotificationsMenu'; -import Toolbar from './Toolbar'; -import SectionButton from 'components/Main/SectionButton'; -import ToolButton from 'components/Main/Toolbar/ToolButton'; -import EditorToolbar from 'containers/Main/Toolbar/EditorToolbar'; -import ToolIndicator from 'components/Main/Toolbar/ToolIndicator'; -import LoadingBar from './LoadingBar'; -const StyledWrapper = themed.div` - background-color: #f6f8fa; -`; - -export interface IMainProps { - network: INetworkEndpoint; - isAuthorized: boolean; - pending: boolean; - section: string; - sections: { [name: string]: TSection }; - stylesheet: string; - navigationSize: number; - navigationVisible: boolean; - transactionsCount: number; - onRefresh: () => void; - onNavigateHome: () => void; - onNavigationToggle: () => void; - onSwitchSection: (section: string) => void; - onCloseSection: (section: string) => void; +interface Props { + app?: string; + page?: string; + action?: string; } -const StyledControls = themed.div` - position: fixed; - top: ${platform.select({ win32: '1px' }) || 0}; - left: ${platform.select({ win32: '1px' }) || 0}; - right: ${platform.select({ win32: '1px' }) || 0}; - z-index: 10000; -`; - -const StyledTitlebar = themed.div` - background: ${props => props.theme.headerBackground}; - height: ${props => props.theme.headerHeight}px; - line-height: ${props => props.theme.headerHeight}px; - font-size: 15px; - color: #fff; - text-align: center; -`; - -const StyledMenu = themed.ul` - background: ${props => props.theme.headerBackground}; - list-style-type: none; - padding: 0; - margin: 0; - height: ${props => props.theme.menuHeight}px; +const StyledLayout = themed.main` + background: ${props => props.theme.contentBackground}; position: relative; - - > li { - margin-top: 6px; - height: 34px; - line-height: 34px; - display: inline-block; - font-size: 16px; - color: #fff; - -webkit-app-region: no-drag; - user-select: none; - - &.user-menu { - margin-top: 0; - height: ${props => props.theme.menuHeight}px; - line-height: normal; - position: absolute; - top: 0; - right: 0; - bottom: 0; - } - } -`; - -const StyledContent = themed.section` - && { background: ${props => props.theme.contentBackground}; } - color: ${props => props.theme.contentForeground}; - margin-top: ${props => props.theme.headerHeight + props.theme.menuHeight + props.theme.toolbarHeight}px !important; - transition: none !important; + padding-top: ${props => props.theme.menubarSize}px; + display: flex; + flex: 1; + flex-direction: column; + overflow: hidden; `; -class Main extends React.Component { - onBack() { - history.goBack(); - } - - onForward() { - history.goForward(); - } - - render() { - const appTitle = `Apla ${this.props.network ? '(' + this.props.network.apiHost + ')' : ''}`; - - return ( - - - - - {appTitle} - - -
  • - - - -
  • - {_.map(this.props.sections, l => l.visible ? ( -
  • - - {l.title} - -
  • - ) : null)} -
  • - - {/**/} - -
  • -
    - - {this.props.isAuthorized && ( - } - titleDesc={} - /> - )} - {'editor' === this.props.section ? - ( - - ) : ( -
    - - - - - - - - - - - - -
    - ) - } -
    - -
    - - - {this.props.children} - -
    - ); - } -} +const Main: React.SFC = props => { + const Route = routes[props.app]; + const headerProps = (Route && Route.mapHeaderParams) ? Route.mapHeaderParams(props) : props; + const contentProps = (Route && Route.mapContentParams) ? Route.mapContentParams(props) : props; + + return ( + + {Route ? + ( + + <> + + + + ) + : + ( + + ) + } + + ); +}; export default Main; \ No newline at end of file diff --git a/src/app/components/Modal/AboutModal.tsx b/src/app/components/Modal/AboutModal.tsx index 5d7885be1..a76e5126f 100644 --- a/src/app/components/Modal/AboutModal.tsx +++ b/src/app/components/Modal/AboutModal.tsx @@ -11,7 +11,7 @@ import { FormattedMessage } from 'react-intl'; import Modal from './'; class AboutModal extends Modal { - openWebsite() { + openWebsite = () => { const electron = require('electron'); electron.shell.openExternal(this.props.intl.formatMessage({ id: 'legal.homepage', diff --git a/src/app/components/Modal/BackupModal.tsx b/src/app/components/Modal/BackupModal.tsx new file mode 100644 index 000000000..1c4ace2f2 --- /dev/null +++ b/src/app/components/Modal/BackupModal.tsx @@ -0,0 +1,93 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import React from 'react'; +import { Button } from 'react-bootstrap'; +import { FormattedMessage } from 'react-intl'; +import { sendAttachment } from 'lib/fs'; +import CopyToClipboard from 'react-copy-to-clipboard'; +import QRCode from 'qrcode.react'; + +import Modal from './'; + +interface Props { + privateKey: string; + publicKey: string; + address: string; + onCopy: () => void; +} + +class BackupModal extends Modal { + onKeyDownload = () => { + sendAttachment(`${this.props.params.address || 'account'}.txt`, this.props.params.privateKey); + } + + render() { + return ( +
    + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + {this.props.params.privateKey} + + + + +
    + + {this.props.params.publicKey}
    + + {this.props.params.address}
    + + +
    + +
    + +
    +
    +
    +
    + +
    +
    + + + +
    + ); + } +} +export default BackupModal; \ No newline at end of file diff --git a/src/app/components/Modal/ChangeLocale.tsx b/src/app/components/Modal/ChangeLocaleModal.tsx similarity index 94% rename from src/app/components/Modal/ChangeLocale.tsx rename to src/app/components/Modal/ChangeLocaleModal.tsx index ca27fbbf3..fd0948a43 100644 --- a/src/app/components/Modal/ChangeLocale.tsx +++ b/src/app/components/Modal/ChangeLocaleModal.tsx @@ -26,7 +26,7 @@ class ChangeLocaleModal extends Modal { return (
    - + Switch language {this.props.params.locales.map(l => ( diff --git a/src/app/components/Modal/Editor/CreateContractModal.tsx b/src/app/components/Modal/Editor/CreateContractModal.tsx new file mode 100644 index 000000000..5691f27b0 --- /dev/null +++ b/src/app/components/Modal/Editor/CreateContractModal.tsx @@ -0,0 +1,65 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as React from 'react'; +import { Button } from 'react-bootstrap'; + +import Modal from '../'; +import { FormattedMessage } from 'react-intl'; +import Validation from 'components/Validation'; + +interface Props { + apps: { + id: string; + name: string; + }[]; +} + +class CreateContractModal extends Modal { + onSubmit = (values: { [key: string]: any }) => { + this.props.onResult({ + app: values.app, + conditions: values.conditions + }); + } + + render() { + return ( + + + + + + + + + {this.props.params.apps.map(app => ( + + ))} + + + + + + + + + + + + + + + ); + } +} + +export default CreateContractModal; \ No newline at end of file diff --git a/src/app/components/Modal/Editor/CreateInterfaceModal.tsx b/src/app/components/Modal/Editor/CreateInterfaceModal.tsx index 1c35cf3fd..bd07f49cb 100644 --- a/src/app/components/Modal/Editor/CreateInterfaceModal.tsx +++ b/src/app/components/Modal/Editor/CreateInterfaceModal.tsx @@ -11,13 +11,18 @@ import { FormattedMessage } from 'react-intl'; import Validation from 'components/Validation'; export interface ICreatePageModalProps { + apps: { + id: string; + name: string; + }[]; type: 'block' | 'menu'; } -class CreateInterfaceModal extends Modal { +class CreateInterfaceModal extends Modal { onSubmit = (values: { [key: string]: any }) => { this.props.onResult({ name: values.name, + app: values.app, conditions: values.conditions }); } @@ -40,13 +45,23 @@ class CreateInterfaceModal extends Modal + + + + {this.props.params.apps.map(app => ( + + ))} + + - + + + +
    + ); + } +} +export default EditorCloseAllModal; \ No newline at end of file diff --git a/src/app/components/Modal/EditorCloseModal.tsx b/src/app/components/Modal/EditorCloseModal.tsx new file mode 100644 index 000000000..4d4e046be --- /dev/null +++ b/src/app/components/Modal/EditorCloseModal.tsx @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import React from 'react'; +import { Button } from 'react-bootstrap'; +import { FormattedMessage } from 'react-intl'; + +import Modal from './'; + +interface Props { + name: string; +} + +class EditorCloseModal extends Modal { + render() { + return ( +
    + + + + + + + + + + +
    + ); + } +} +export default EditorCloseModal; \ No newline at end of file diff --git a/src/app/components/Modal/EditorRevertModal.tsx b/src/app/components/Modal/EditorRevertModal.tsx new file mode 100644 index 000000000..696a42364 --- /dev/null +++ b/src/app/components/Modal/EditorRevertModal.tsx @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import React from 'react'; +import { Button } from 'react-bootstrap'; +import { FormattedMessage } from 'react-intl'; + +import Modal from './'; + +interface Props { + name: string; +} + +class EditorRevertModal extends Modal { + render() { + return ( +
    + + + + + + + + + + +
    + ); + } +} +export default EditorRevertModal; \ No newline at end of file diff --git a/src/app/components/Modal/Header.tsx b/src/app/components/Modal/Header.tsx new file mode 100644 index 000000000..795165e33 --- /dev/null +++ b/src/app/components/Modal/Header.tsx @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import React from 'react'; + +import themed from 'components/Theme/themed'; + +interface Props { + onClose: () => void; +} + +const StyledHeader = themed.div` + background: ${props => props.theme.modalHeaderBackground}; + color: ${props => props.theme.modalHeaderForeground}; + margin: -1px -1px 0 -1px; + height: 40px; + line-height: 40px; + padding: 0 15px; + + .header__close { + border: 0; + outline: 0; + background: 0; + padding: 0; + position: absolute; + top: 0; + right: 0; + width: 40px; + height: 40px; + color: ${props => props.theme.modalHeaderForeground}; + opacity: 0.5; + font-size: 26px; + + &:hover { + opacity: 1; + } + } +`; + +const Header: React.SFC = props => ( + +
    + {props.children} +
    + +
    +); + +export default Header; \ No newline at end of file diff --git a/src/app/components/Modal/MapEditorModal.tsx b/src/app/components/Modal/MapEditorModal.tsx index 8d53272a8..d971d054d 100644 --- a/src/app/components/Modal/MapEditorModal.tsx +++ b/src/app/components/Modal/MapEditorModal.tsx @@ -16,7 +16,7 @@ import Modal, { IModalProps } from './'; import Validation from 'components/Validation'; import MapView from 'components/Map/MapView'; import Tooltip from 'components/Tooltip'; -import SectionToolButton from 'components/Main/Toolbar/SectionToolButton'; +import SegmentButton from 'components/Button/SegmentButton'; export interface IMapEditorModalProps { mapType?: 'streets' | 'satellite' | 'hybrid' | 'topo' | 'gray' | 'dark-gray' | 'oceans' | 'national-geographic' | 'terrain' | 'osm'; @@ -338,7 +338,7 @@ class MapEditorModal extends Modal
    - { @@ -28,7 +33,17 @@ class PageModal extends Modal { {this.props.params.title} - +
    ); diff --git a/src/app/components/Modal/Tx/AuthorizeModal.tsx b/src/app/components/Modal/Tx/AuthorizeModal.tsx index 7bd2f1cbe..74ec91364 100644 --- a/src/app/components/Modal/Tx/AuthorizeModal.tsx +++ b/src/app/components/Modal/Tx/AuthorizeModal.tsx @@ -3,18 +3,14 @@ * See LICENSE in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as React from 'react'; +import React from 'react'; import { Button } from 'react-bootstrap'; import { FormattedMessage } from 'react-intl'; import Modal from '../'; import Validation from 'components/Validation'; -export interface IAuthorizeModalProps { - contract: string; -} - -class AuthorizeModal extends Modal { +class AuthorizeModal extends Modal { onSuccess = (values: { [key: string]: any }) => { this.props.onResult(values.password); } @@ -27,7 +23,7 @@ class AuthorizeModal extends Modal {
    - +
    props.theme.headerHeight - 1}px; - padding-top: ${props => 50 + props.theme.toolbarHeight}px; + margin-top: ${props => props.theme.headerHeight + (platform.select({ win32: 1 }) || 0)}px; &::before { content: ' '; diff --git a/src/app/components/Modal/index.tsx b/src/app/components/Modal/index.tsx index 7bdc382b4..1b041e206 100644 --- a/src/app/components/Modal/index.tsx +++ b/src/app/components/Modal/index.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { InjectedIntl } from 'react-intl'; import themed from 'components/Theme/themed'; +import Header from 'containers/Modal/Header'; export interface IModalProps { intl: InjectedIntl; @@ -21,15 +22,6 @@ export type TModalComponentClass = React.ComponentType> | React.SFC>; -const StyledHeader = themed.div` - background: ${props => props.theme.modalHeaderBackground}; - color: ${props => props.theme.modalHeaderForeground}; - margin: -1px -1px 0 -1px; - height: 40px; - line-height: 40px; - padding: 0 15px; -`; - const StyledBody = themed.div` padding: 15px; min-width: 300px; @@ -42,13 +34,13 @@ const StyledFooter = themed.div` `; export abstract class ModalContainer extends React.Component { - public static Header = StyledHeader; + public static Header = Header; public static Body = StyledBody; public static Footer = StyledFooter; } export default abstract class Modal extends React.Component, S> { - public static Header = StyledHeader; + public static Header = Header; public static Body = StyledBody; public static Footer = StyledFooter; } \ No newline at end of file diff --git a/src/app/components/Money/index.tsx b/src/app/components/Money/index.tsx deleted file mode 100644 index bf5757889..000000000 --- a/src/app/components/Money/index.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as React from 'react'; -import { FormattedNumber } from 'react-intl'; - -export interface IMoneyProps { - value: string | number; - children?: never; -} - -const Money: React.SFC = (props) => ( - -); - -export default Money; \ No newline at end of file diff --git a/src/app/components/NotFound/index.tsx b/src/app/components/NotFound/index.tsx deleted file mode 100644 index 59d4007ec..000000000 --- a/src/app/components/NotFound/index.tsx +++ /dev/null @@ -1,56 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as React from 'react'; -import { Link } from 'react-router-dom'; -import { FormattedMessage } from 'react-intl'; - -import LocalizedDocumentTitle from 'components/DocumentTitle/LocalizedDocumentTitle'; -import Center from 'components/Center'; - -export interface INotFoundProps { - main?: boolean; -} - -const NotFound: React.SFC = (props) => ( - -
    -
    -
    -
    404
    -

    - -

    -

    - -

    -
    - {!props.main && ( -
      -
    • - - - -
    • -
    • |
    • -
    • - - - -
    • -
    • |
    • -
    • - - - -
    • -
    - )} -
    -
    -
    -); - -export default NotFound; \ No newline at end of file diff --git a/src/app/components/Protypo/Protypo.tsx b/src/app/components/Protypo/Protypo.tsx index 508049191..b5640062f 100644 --- a/src/app/components/Protypo/Protypo.tsx +++ b/src/app/components/Protypo/Protypo.tsx @@ -9,19 +9,20 @@ import propTypes from 'prop-types'; import contextDefinitions from './contexts'; import { TProtypoElement, ISource } from 'apla/protypo'; import { IValidationResult } from 'components/Validation/ValidatedForm'; -import Heading from 'components/Heading'; +import Heading from './components/Heading'; import ToolButton, { IToolButtonProps } from 'containers/ToolButton/ToolButton'; import { IConstructorElementProps } from 'apla/editor'; +import { TBreadcrumbType } from 'apla/content'; export interface IProtypoProps extends IConstructorElementProps { apiHost: string; wrapper?: JSX.Element; context: string; - page: string; + page?: string; + menu?: string; + section: string; content: TProtypoElement[]; menuPush: (params: { name: string, content: TProtypoElement[] }) => void; - navigatePage: (params: { name: string, params: any, force?: boolean }) => void; - navigate: (url: string) => void; displayData: (link: string) => void; } @@ -37,65 +38,76 @@ export interface IParamSpec { class Protypo extends React.Component { private _lastID: number; - private _menuPushBind: Function; - private _navigatePageBind: Function; - private _navigateBind: Function; - private _resolveSourceBind: Function; - private _renderElementsBind: Function; private _title: string; private _toolButtons: IToolButtonProps[]; private _sources: { [key: string]: ISource }; private _errors: { name: string, description: string }[]; - constructor(props: IProtypoProps) { - super(props); - this._menuPushBind = props.menuPush.bind(this); - this._navigatePageBind = props.navigatePage.bind(this); - this._navigateBind = props.navigate.bind(this); - this._resolveSourceBind = this.resolveSource.bind(this); - this._renderElementsBind = this.renderElements.bind(this); - } - getChildContext() { return { protypo: this, - menuPush: this._menuPushBind, - navigatePage: this._navigatePageBind, - navigate: this._navigateBind, - resolveSource: this._resolveSourceBind, - renderElements: this._renderElementsBind + section: this.props.section, + menuPush: this.props.menuPush, + resolveSource: this.resolveSource, + resolveText: this.resolveText, + getFromContext: this.getFromContext, + renderElements: this.renderElements }; } - getCurrentPage() { + getFromContext: (computeTitle?: React.ReactNode) => { type: TBreadcrumbType, section: string, name: string } | undefined = computeTitle => { + const title = computeTitle ? this.resolveText(computeTitle) : ''; + + if (this.props.page) { + return { + type: 'PAGE', + section: this.props.section, + title: title || this.props.page, + name: this.props.page + }; + } + else if (this.props.menu) { + return { + type: 'MENU', + section: this.props.section, + title: title || this.props.menu, + name: this.props.menu + }; + } + else { + return undefined; + } + } + + getCurrentPage = () => { return this.props.page; } - setTitle(title: string) { + setTitle = (title: string) => { this._title = title; } - addToolButton(props: IToolButtonProps) { + addToolButton = (props: IToolButtonProps) => { this._toolButtons.push(props); } - displayData(link: string) { + displayData = (link: string) => { this.props.displayData(link); } - registerSource(name: string, payload: ISource) { + registerSource = (name: string, payload: ISource) => { this._sources[name] = payload; } - resolveSource(name: string) { + resolveSource = (name: string) => { return this._sources[name]; } - resolveData(name: string) { + resolveData = (name: string) => { return `${this.props.apiHost}${name}`; } - resolveParams(values: IParamsSpec, formValues?: { [key: string]: IValidationResult }) { + resolveParams = (values: IParamsSpec, formValues?: { [key: string]: IValidationResult }) => { const result: { [key: string]: string } = {}; for (let itr in values) { if (values.hasOwnProperty(itr)) { @@ -114,7 +126,35 @@ class Protypo extends React.Component { return result; } - renderElement(element: TProtypoElement, optionalKey?: string): React.ReactNode { + resolveText = (value: React.ReactNode) => { + let result = ''; + if (value) { + if ('string' === typeof value || 'number' === typeof value) { + result += value; + } + else if ('object' === typeof value) { + let children = null; + if (Array.isArray(value)) { + children = value; + } + else if ('props' in value) { + children = value.props.children; + } + if (children) { + if (Array.isArray(children)) { + children.forEach(o => { + result += this.resolveText(o); + }); + } else { + result += this.resolveText(children); + } + } + } + } + return result; + } + + renderElement = (element: TProtypoElement, optionalKey?: string): React.ReactNode => { switch (element.tag) { case 'text': return element.text; @@ -160,7 +200,7 @@ class Protypo extends React.Component { } } - renderElements(elements: TProtypoElement[], keyPrefix?: string): React.ReactNode[] { + renderElements = (elements: TProtypoElement[], keyPrefix?: string): React.ReactNode[] => { if (!elements) { return null; } @@ -170,7 +210,7 @@ class Protypo extends React.Component { )); } - renderHeading() { + renderHeading = () => { return (this.props.context === 'page' && this._title) ? ( {this._title} @@ -222,12 +262,13 @@ class Protypo extends React.Component { } (Protypo as any).childContextTypes = { + section: propTypes.string.isRequired, protypo: propTypes.object.isRequired, - navigatePage: propTypes.func.isRequired, - navigate: propTypes.func.isRequired, menuPush: propTypes.func.isRequired, resolveSource: propTypes.func.isRequired, - renderElements: propTypes.func.isRequired + resolveText: propTypes.func.isRequired, + renderElements: propTypes.func.isRequired, + getFromContext: propTypes.func.isRequired }; export default Protypo; diff --git a/src/app/components/Heading/index.tsx b/src/app/components/Protypo/components/Heading.tsx similarity index 85% rename from src/app/components/Heading/index.tsx rename to src/app/components/Protypo/components/Heading.tsx index d2989972d..b6eebae20 100644 --- a/src/app/components/Heading/index.tsx +++ b/src/app/components/Protypo/components/Heading.tsx @@ -3,16 +3,17 @@ * See LICENSE in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as React from 'react'; -import styled from 'styled-components'; +import React from 'react'; +import themed from 'components/Theme/themed'; -const StyledHeading = styled.div` +const StyledHeading = themed.div` z-index: 1000; font-size: 20px; line-height: 45px; height: 46px; color: #000; font-weight: normal; + margin-top: 10px; padding: 0 20px; border: 0; `; diff --git a/src/app/components/Protypo/components/ToolButton.tsx b/src/app/components/Protypo/components/ToolButton.tsx index eccbebc1f..978e2d032 100644 --- a/src/app/components/Protypo/components/ToolButton.tsx +++ b/src/app/components/Protypo/components/ToolButton.tsx @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import React from 'react'; -import propTypes from 'prop-types'; +import classNames from 'classnames'; -import Protypo from '../Protypo'; +import themed from 'components/Theme/themed'; export interface IToolButtonProps { title?: string; @@ -18,23 +18,44 @@ export interface IToolButtonProps { onClick: (e: any) => void; } -interface IToolButtonContext { - protypo: Protypo; -} +const StyledToolButton = themed.button` + border: 0; + background: 0; + outline: 0; + border: 0; + height: 35px; + line-height: 35px; + white-space: nowrap; + padding: 0 10px; + transition: background ease-in-out .12s; + + &:hover { + background: rgba(0,0,0,0.05); + } + + .toolbutton__icon { + font-size: 16px; + color: #4688ff; + margin-right: 8px; + } + + .toolbutton__label { + vertical-align: top; + white-space: nowrap; + font-size: 15px; + color: #333; + } +`; -const ToolButton: React.SFC = (props, context: IToolButtonContext) => { +const ToolButton: React.SFC = props => { return ( - - - - {props.title} + + + + {props.title} - + ); }; -ToolButton.contextTypes = { - protypo: propTypes.object.isRequired -}; - export default ToolButton; \ No newline at end of file diff --git a/src/app/components/Protypo/functions/addToolButton.ts b/src/app/components/Protypo/functions/addToolButton.ts index 5f07fe22a..4e130b5dd 100644 --- a/src/app/components/Protypo/functions/addToolButton.ts +++ b/src/app/components/Protypo/functions/addToolButton.ts @@ -3,7 +3,6 @@ * See LICENSE in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import Protypo from '../'; import { IParamsSpec } from 'components/Protypo/Protypo'; export interface IAddToolButtonProps { @@ -13,9 +12,10 @@ export interface IAddToolButtonProps { pageparams?: IParamsSpec; } -const addToolButton = (context: Protypo, props: IAddToolButtonProps) => { +const addToolButton = (context: any, props: IAddToolButtonProps) => { context.addToolButton({ ...props, + section: context.section, pageparams: context.resolveParams(props.pageparams) }); }; diff --git a/src/app/components/Protypo/handlers/Button.tsx b/src/app/components/Protypo/handlers/Button.tsx index 6ecd40667..2b9641fbe 100644 --- a/src/app/components/Protypo/handlers/Button.tsx +++ b/src/app/components/Protypo/handlers/Button.tsx @@ -16,6 +16,10 @@ import { IErrorRedirect } from 'apla/protypo'; export interface IButtonProps { 'class'?: string; 'className'?: string; + 'action'?: { + name: string; + params?: { [key: string]: string }; + }[]; 'alert'?: { icon: string; text: string; @@ -45,6 +49,7 @@ export interface IButtonProps { interface IButtonContext { form: ValidatedForm; + section: string; protypo: Protypo; } @@ -131,6 +136,7 @@ const Button: React.SFC = (props, context: IBu return ( = (props, context: IBu name: tx.name, params: tx.data }))} + from={context.protypo.getFromContext(props.children)} contract={props.contract} contractParams={getParams} page={props.page} + section={context.section} pageParams={getPageParams} popup={popup} errorRedirects={getErrorRedirectParams} @@ -156,6 +164,7 @@ const Button: React.SFC = (props, context: IBu Button.contextTypes = { form: propTypes.object, + section: propTypes.string, protypo: propTypes.object.isRequired }; diff --git a/src/app/components/Protypo/handlers/Hint.tsx b/src/app/components/Protypo/handlers/Hint.tsx index 9558a41dd..2aeb72a70 100644 --- a/src/app/components/Protypo/handlers/Hint.tsx +++ b/src/app/components/Protypo/handlers/Hint.tsx @@ -26,7 +26,7 @@ export const HintWrapper = themed.div` font-weight: 300; em.tool-icon { - color: ${props => props.theme.toolbarIconColor}; + color: #5b97e4, transition: color .15s; vertical-align: middle; height: 18px; @@ -35,12 +35,12 @@ export const HintWrapper = themed.div` > span.button-label { margin-left: 8px; - color: ${props => props.theme.toolbarForeground}; + color: #fff; } &:hover { em.tool-icon { - color: ${props => props.theme.toolbarIconHighlight}; + color: #a9ccf9; } } } diff --git a/src/app/components/Protypo/handlers/LinkPage.tsx b/src/app/components/Protypo/handlers/LinkPage.tsx index 821b5d288..e70925ac8 100644 --- a/src/app/components/Protypo/handlers/LinkPage.tsx +++ b/src/app/components/Protypo/handlers/LinkPage.tsx @@ -3,11 +3,12 @@ * See LICENSE in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as React from 'react'; -import * as propTypes from 'prop-types'; +import React from 'react'; +import propTypes from 'prop-types'; -import Protypo, { IParamsSpec } from '../Protypo'; +import { IParamsSpec } from '../Protypo'; import StyledComponent from './StyledComponent'; +import PageLink from 'containers/Routing/PageLink'; export interface ILinkPageProps { 'class'?: string; @@ -17,31 +18,26 @@ export interface ILinkPageProps { } interface ILinkPageContext { - protypo: Protypo; - navigatePage: (params: { name: string, params: any, force?: boolean }) => void; + section: string; + getFromContext: () => any; + protypo: any; } -const LinkPage: React.SFC = (props, context: ILinkPageContext) => { - const onNavigate = (e: React.MouseEvent) => { - e.preventDefault(); - context.navigatePage({ - name: props.page, - params: context.protypo.resolveParams(props.pageparams), - force: true - }); - return false; - }; - - return ( - - {props.children} - - ); -}; +const LinkPage: React.SFC = (props, context: ILinkPageContext) => ( + + {props.children} + +); LinkPage.contextTypes = { protypo: propTypes.object.isRequired, - navigatePage: propTypes.func.isRequired + section: propTypes.string.isRequired }; export default StyledComponent(LinkPage); \ No newline at end of file diff --git a/src/app/components/Protypo/handlers/MenuItem.tsx b/src/app/components/Protypo/handlers/MenuItem.tsx index cdcfa4448..8b3a7d67e 100644 --- a/src/app/components/Protypo/handlers/MenuItem.tsx +++ b/src/app/components/Protypo/handlers/MenuItem.tsx @@ -19,6 +19,8 @@ export interface IMenuItemProps { } export const StyledMenuItem = themed.div` + border-bottom: solid 1px ${props => props.theme.menuOutline}; + > a, > a:hover { text-decoration: none !important; } @@ -47,10 +49,10 @@ export const StyledMenuItem = themed.div` display: block; height: 50px; line-height: 50px; - padding: 0 25px; + padding: 0 18px; color: ${props => props.theme.menuForeground}; font-size: 14px; - font-weight: 200; + font-weight: 400; text-align: left; text-decoration: none; overflow: hidden; @@ -58,16 +60,16 @@ export const StyledMenuItem = themed.div` white-space: nowrap; .icon { - margin-right: 14px; + margin-right: 16px; color: ${props => props.theme.menuIconColor}; - font-size: 17px; - position: relative; - top: 3px; + font-size: 21px; + vertical-align: middle; } } `; interface IMenuItemContext { + section: string; protypo: Protypo; } @@ -79,7 +81,16 @@ const MenuItem: React.SFC = (props, context: IMenuItemContext) = return ( - + {props.icon && ()} @@ -91,8 +102,8 @@ const MenuItem: React.SFC = (props, context: IMenuItemContext) = }; MenuItem.contextTypes = { - protypo: propTypes.object.isRequired, - navigatePage: propTypes.func.isRequired + section: propTypes.string.isRequired, + protypo: propTypes.object.isRequired }; export default MenuItem; \ No newline at end of file diff --git a/src/app/components/Routing/PageLink.tsx b/src/app/components/Routing/PageLink.tsx index 630ef2aaf..abbc683ee 100644 --- a/src/app/components/Routing/PageLink.tsx +++ b/src/app/components/Routing/PageLink.tsx @@ -4,39 +4,36 @@ *--------------------------------------------------------------------------------------------*/ import React from 'react'; -import { LEGACY_PAGES } from 'lib/legacyPages'; +import { Link } from 'react-router-dom'; +import { generateRoute } from 'services/router'; +import { TBreadcrumbType } from 'apla/content'; export interface IPageLinkProps { - section?: string; - mainSection: string; - currentSection: string; + className?: string; + section: string; page: string; params?: { [key: string]: string }; - className?: string; - navigatePage: (params: { name: string, section: string, params: { [key: string]: string } }) => void; -} - -const PageLink: React.SFC = props => { - const sectionName = ((LEGACY_PAGES[props.page] && (LEGACY_PAGES[props.page].section || props.mainSection))) || props.section || props.currentSection; - - const navigateUrl = `/${sectionName}/${props.page}`; - const navigatePage = (e: React.MouseEvent) => { - e.preventDefault(); - props.navigatePage({ - name: props.page, - section: sectionName, - params: props.params - }); - return false; + from?: { + name: string; + title?: string; + type: TBreadcrumbType; }; +} - return ( - - {props.children} - - ); -}; +const PageLink: React.SFC = props => ( + + {props.children} + +); export default PageLink; \ No newline at end of file diff --git a/src/app/components/StaticPages/TxInfo.tsx b/src/app/components/StaticPages/TxInfo.tsx index 322d65992..f93216b18 100644 --- a/src/app/components/StaticPages/TxInfo.tsx +++ b/src/app/components/StaticPages/TxInfo.tsx @@ -13,6 +13,7 @@ import PrintZone from 'components/PrintZone'; import { FormattedMessage } from 'react-intl'; export interface ITxInfoProps { + section: string; txStack: { hash: string; tx: ITransaction; @@ -93,6 +94,7 @@ const TxInfo: React.SFC = props => ( ))}
    diff --git a/src/app/components/Tabs/index.tsx b/src/app/components/Tabs/index.tsx deleted file mode 100644 index bf3d5237c..000000000 --- a/src/app/components/Tabs/index.tsx +++ /dev/null @@ -1,185 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as React from 'react'; -import styled from 'styled-components'; - -const TabItems = styled.div` - background-color: #707c91; - background-color: rgba(0, 0, 0, 0.1); -`; - -const TabsContainer = styled.div` - display: flex; - flex: 1 1; - -ms-flex-direction: column; - flex-direction: column; - - .tab-pane { - display: none; - flex: 1 1; - -ms-flex-direction: column; - flex-direction: column; - } - - .tab-pane.active { - display: flex; - } -`; - -const TabContent = styled.div` - display: flex; - flex: 1 1; - flex-direction: column; -`; - -const TabItem = styled.div` - display: inline-block; - color: #FFF; - line-height: 26px; - - background-color: #465669; - opacity: 0.5; - cursor: pointer; - - margin-right: 1px; - - span { - padding-left: 10px; - padding-right: 10px; - } - - a { - color: #FFF; - padding-left: 5px; - padding-right: 10px; - text-decoration: none; - font-size: 16px; - } - - a:hover { - color: #FF5555; - } - - &:hover { - opacity: 0.8; - } - - &.active { - opacity: 1; - } - - &.interfacePage { - background-color: #883498; - } - - &.interfaceBlock { - background-color: #349837; - } - - &.interfaceMenu { - background-color: #985f34; - } - - &.contract { - background-color: #346198; - } - - &.parameter { - background-color: #333333; - } -`; - -export interface IConstructorTabsProps { - tabList: { id: string, type: string, name?: string, visible?: boolean }[]; - children: JSX.Element[]; - openedTab: { id: string, type: string }; - onTabClose?: any; - className?: string; -} - -interface IConstructorTabsState { - tabIndex: number; - loaded: boolean; -} - -export default class ConstructorTabs extends React.Component { - constructor(props: IConstructorTabsProps) { - super(props); - this.state = { - tabIndex: 0, - loaded: false - }; - } - - onTabSwitch(tabIndex: number) { - this.setState({ - tabIndex, - loaded: true - }); - } - - componentWillReceiveProps(props: IConstructorTabsProps) { - // open selected tab - if (props.tabList && !this.state.loaded) { - let tabIndex = props.tabList.findIndex((item: any) => item.id === props.openedTab.id && item.type === props.openedTab.type); - if (tabIndex >= 0 && this.state.tabIndex < tabIndex) { - this.setState({ - tabIndex, - loaded: false - }); - } - } - } - - onTabClose(id: string, type: string) { - // if closed tab was active, set first tab active - - if (this.props.tabList) { - let closedTabItemIndex = this.props.tabList.findIndex((tabItem: any) => tabItem.id === id && tabItem.type === type); - if (closedTabItemIndex === this.state.tabIndex) { - let switchToTabIndex = this.props.tabList.findIndex((tabItem: any, index: number) => - tabItem.visible !== false && index !== closedTabItemIndex - ); - if (switchToTabIndex !== -1) { - this.setState({ - tabIndex: switchToTabIndex, - loaded: true - }); - } - } - } - - let openedTabs = this.props.tabList.filter((tabItem: any, index: number) => - tabItem.visible !== false - ).length; - - if (this.props.onTabClose && openedTabs > 1) { - this.props.onTabClose(id, type); - } - } - - render() { - return ( - - - {this.props.tabList && this.props.tabList.map((tab, index) => ( - - {tab.name} - × - - ))} - - - {this.props.children.map((element, index) => ( -
    - {element} -
    - ))} -
    -
    - ); - } -} \ No newline at end of file diff --git a/src/app/components/Theme/baseTheme.ts b/src/app/components/Theme/baseTheme.ts index e7c0c060e..bd3ef78de 100644 --- a/src/app/components/Theme/baseTheme.ts +++ b/src/app/components/Theme/baseTheme.ts @@ -9,33 +9,41 @@ import platform from 'lib/platform'; const baseTheme: IThemeDefinition = { windowBorder: '#4c7dbd', - headerBackground: '#586180', + headerBackground: '#354a69', headerForeground: '#fff', - headerBackgroundActive: '#f3f3f3', - headerForegroundActive: '#194a8a', headerHeight: platform.select({ desktop: 28, web: 0 }), - toolbarBackground: '#f3f3f3', - toolbarForeground: '#194a8a', - toolbarOutline: '#e5e5e5', - toolbarIconColor: '#5b97e4', - toolbarIconHighlight: '#a9ccf9', - toolbarDisabled: '#93a7bf', - toolbarHeight: 40, + menubarSize: 40, + menubarBackground: '#3873A6', + menubarBackgroundActive: '#9CB9D2', + menubarBackgroundFocused: 'rgba(255,255,255,0.09)', + menubarBackgroundSecondary: '#ffa500', + menubarForeground: '#9CB9D3', + menubarForegroundActive: '#fff', - progressBarForeground: '#b2c5dc', + toolbarBackground: '#f1f1f1', + toolbarBackgroundActive: 'rgba(0,0,0,0.1)', + toolbarBackgroundFocused: 'rgba(0,0,0,0.05)', + toolbarForeground: '#333', + toolbarForegroundActive: '#5d5d5d', + toolbarForegroundPrimary: '#4688ff', + toolbarForegroundDisabled: '#ccc', + toolbarHeight: 40, + toolbarSpacerForeground: '#C6C6C6', - menuHeight: 40, menuBackground: '#fff', menuForeground: '#0a1d33', - menuBackgroundActive: '#ececec', - menuOutline: '#e5e4e5', + menuBackgroundActive: '#f6f7f9', + menuBorder: '#35abff', + menuOutline: '#eff2f5', menuIconColor: '#3577cc', menuPrimaryForeground: '#2886ff', menuPrimaryActive: '#7bb0f5', + menuSize: 230, + menuSizeFolded: 55, contentForeground: '#515253', - contentBackground: '#f6f7fa', + contentBackground: '#fff', editorBackground: '#c3c7ce', @@ -55,19 +63,17 @@ const baseTheme: IThemeDefinition = { sectionButtonPrimary: '#fff', dropdownMenuBackground: '#fff', - dropdownMenuForeground: '#666', + dropdownMenuForeground: '#464646', dropdownMenuDisabled: '#ccc', - dropdownMenuOutline: '#c5cbe2', - dropdownMenuActive: 'rgba(0,0,0,0.1)', - dropdownMenuSeparator: '#ddd', + dropdownMenuActive: 'rgba(0,0,0,0.06)', + dropdownMenuSeparator: '#efefef', dropdownMenuPrimary: '#4b7dbd', dropdownMenuSecondary: '#999', - systemButtonSecondary: '#ffa500', - systemButtonActive: 'rgba(0,0,0,0.1)', - securityWarningBackground: '#ff5555', - securityWarningForeground: '#ffffff' + securityWarningForeground: '#ffffff', + + uiBorderLight: '#e8eaf1' }; export default baseTheme; \ No newline at end of file diff --git a/src/app/components/Main/Titlebar/DarwinTitlebar.tsx b/src/app/components/Titlebar/DarwinTitlebar.tsx similarity index 98% rename from src/app/components/Main/Titlebar/DarwinTitlebar.tsx rename to src/app/components/Titlebar/DarwinTitlebar.tsx index 3d695a1b0..e0cd748d7 100644 --- a/src/app/components/Main/Titlebar/DarwinTitlebar.tsx +++ b/src/app/components/Titlebar/DarwinTitlebar.tsx @@ -10,7 +10,7 @@ import { remote } from 'electron'; import imgControls from './wndControls.svg'; import { ITitlebarProps } from './'; -import SystemMenu from 'containers/Main/Titlebar/SystemMenu'; +import SystemMenu from 'containers/Titlebar/SystemMenu'; const StyledControls = styled.div` -webkit-app-region: no-drag; @@ -25,7 +25,7 @@ const StyledControls = styled.div` .window-controls { position: absolute; left: 6px; - top: 5px; + top: 2px; button { background: url(${imgControls}) 0 0 no-repeat; diff --git a/src/app/components/Main/Titlebar/LinuxTitlebar.tsx b/src/app/components/Titlebar/LinuxTitlebar.tsx similarity index 97% rename from src/app/components/Main/Titlebar/LinuxTitlebar.tsx rename to src/app/components/Titlebar/LinuxTitlebar.tsx index 1da112d45..f185f328a 100644 --- a/src/app/components/Main/Titlebar/LinuxTitlebar.tsx +++ b/src/app/components/Titlebar/LinuxTitlebar.tsx @@ -3,13 +3,13 @@ * See LICENSE in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import imgControls from './wndControls.svg'; import { remote } from 'electron'; import { ITitlebarProps } from './'; -import SystemMenu from 'containers/Main/Titlebar/SystemMenu'; +import SystemMenu from 'containers/Titlebar/SystemMenu'; const StyledControls = styled.div` position: absolute; diff --git a/src/app/components/Titlebar/SystemMenu.tsx b/src/app/components/Titlebar/SystemMenu.tsx new file mode 100644 index 000000000..0411633fe --- /dev/null +++ b/src/app/components/Titlebar/SystemMenu.tsx @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import React from 'react'; +import { remote } from 'electron'; +import { FormattedMessage } from 'react-intl'; + +import themed from 'components/Theme/themed'; +import DropdownButton from 'components/Button/DropdownButton'; +import Heading from 'components/Dropdown/Heading'; +import Item from 'components/Dropdown/Item'; + +const SystemDropdown = themed(DropdownButton)` + background: 0; + border: 0; + outline: 0; + color: #bad8ff; + width: ${props => props.theme.headerHeight}px !important; + height: ${props => props.theme.headerHeight}px !important; + padding: 0 !important; + margin: 0 !important; + outline: 0 !important; + min-width: 0 !important; + line-height: ${props => props.theme.headerHeight + 2}px !important; + transition: color ease-in-out .16s; + cursor: pointer !important; + + &:hover { + color: #fff; + } +`; + +export interface ISystemMenuProps { + align: 'left' | 'right'; + onAbout: () => void; +} + +const SystemMenu: React.SFC = props => { + const elements = [ + ( + + + + + + + + remote.getCurrentWindow().webContents.openDevTools({ mode: 'detach' })} icon="icon-calculator text-danger"> + + + + } + > + + + ) + ]; + return ( +
    + {(props.align === 'left' ? elements : elements.reverse()).map((e, i) => ( + e + ))} +
    + ); +}; + +export default SystemMenu; \ No newline at end of file diff --git a/src/app/components/Main/Titlebar/WinTitlebar.tsx b/src/app/components/Titlebar/WinTitlebar.tsx similarity index 97% rename from src/app/components/Main/Titlebar/WinTitlebar.tsx rename to src/app/components/Titlebar/WinTitlebar.tsx index 52c9dbe8c..8b7dc0759 100644 --- a/src/app/components/Main/Titlebar/WinTitlebar.tsx +++ b/src/app/components/Titlebar/WinTitlebar.tsx @@ -3,13 +3,13 @@ * See LICENSE in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import imgControls from './wndControls.svg'; import { remote } from 'electron'; import { ITitlebarProps } from './'; -import SystemMenu from 'containers/Main/Titlebar/SystemMenu'; +import SystemMenu from 'containers/Titlebar/SystemMenu'; const StyledControls = styled.div` position: absolute; diff --git a/src/app/components/Main/Titlebar/index.tsx b/src/app/components/Titlebar/index.tsx similarity index 96% rename from src/app/components/Main/Titlebar/index.tsx rename to src/app/components/Titlebar/index.tsx index 4b0e12970..126182e7f 100644 --- a/src/app/components/Main/Titlebar/index.tsx +++ b/src/app/components/Titlebar/index.tsx @@ -3,7 +3,7 @@ * See LICENSE in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as React from 'react'; +import React from 'react'; import styled from 'styled-components'; import platform from 'lib/platform'; @@ -13,6 +13,7 @@ export interface ITitlebarProps { const StyledControls = styled.div` position: relative; + z-index: 20000; .window-title { position: absolute; diff --git a/src/app/components/Main/Titlebar/wndControls.svg b/src/app/components/Titlebar/wndControls.svg similarity index 100% rename from src/app/components/Main/Titlebar/wndControls.svg rename to src/app/components/Titlebar/wndControls.svg diff --git a/src/app/components/Tooltip/index.tsx b/src/app/components/Tooltip/index.tsx index 2f8fc7e91..e763859ba 100644 --- a/src/app/components/Tooltip/index.tsx +++ b/src/app/components/Tooltip/index.tsx @@ -22,6 +22,7 @@ interface ITooltipState { const StyledTooltip = themed.div` position: absolute; padding: 10px; + z-index: 700; > div { line-height: 16px; diff --git a/src/app/components/Wrapper/index.tsx b/src/app/components/Wrapper/index.tsx deleted file mode 100644 index b6c899513..000000000 --- a/src/app/components/Wrapper/index.tsx +++ /dev/null @@ -1,105 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as React from 'react'; -import { Link } from 'react-router-dom'; -import styled from 'styled-components'; - -import LocalizedDocumentTitle from 'components/DocumentTitle/LocalizedDocumentTitle'; -import Heading from 'components/Heading'; - -type TMixedContent = - JSX.Element | string; - -export interface IWrapperProps { - type: 'default' | 'fullscreen' | 'noscroll'; - title: { title: string, defaultTitle: string }; - heading?: { - content: TMixedContent; - toolButtons?: { - url: string; - icon: string; - title: TMixedContent; - }[]; - }; - description?: React.ReactNode; - breadcrumbs?: IBreadcrumbProps[]; -} - -export interface IBreadcrumbProps { - title: TMixedContent; - url?: string; -} - -const StyledDescription = styled.div` - padding: 10px 20px; - color: #84909e; - border-bottom: 1px solid #cfdbe2; -`; - -const StyledBreadcrumbs = styled.ul` - padding: 10px 20px; - color: #84909e; - border-bottom: 1px solid #cfdbe2; - list-style: none; -`; - -const Breadcrumb: React.SFC = props => ( -
  • - {props.url ? - ( - {props.title} - ) : ( - {props.title} - ) - } -
  • -); - -const bodyClasses = { - default: 'content-wrapper', - fullscreen: 'fullscreen', - noscroll: 'fullscreen-wrapper' -}; - -const Wrapper: React.SFC = props => ( - -
    - {props.heading && ( - -
    - {props.heading.toolButtons && props.heading.toolButtons.map((button, index) => ( - - - {button.title} - - ))} -
    -
    {props.heading.content}
    -
    - )} - - {props.description && ( - - {props.description} - - )} - - {props.breadcrumbs && ( - - {props.breadcrumbs.map((breadcrumb, index) => ( - - ))} - - )} - -
    - {props.children} -
    -
    -
    -); - -export default Wrapper; \ No newline at end of file diff --git a/src/app/containers/App.ts b/src/app/containers/App.ts new file mode 100644 index 000000000..d7f772c31 --- /dev/null +++ b/src/app/containers/App.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { connect } from 'react-redux'; +import { IRootState } from 'modules'; +import { DragDropContext } from 'react-dnd'; +import { initialize } from 'modules/engine/actions'; +import HTML5Backend from 'react-dnd-html5-backend'; + +import App from 'components/App'; + +const mapStateToProps = (state: IRootState) => ({ + locale: state.engine.locale || 'en-US', + localeMessages: state.engine.localeMessages, + isSessionAcquired: state.auth.isAcquired, + isAuthenticated: state.auth.isAuthenticated, + isLoaded: state.engine.isLoaded, + isFatal: !!state.engine.fatalError, + securityWarningClosed: state.storage.securityWarningClosed, + network: state.engine.guestSession && state.engine.guestSession.network +}); + +const mapDispatchToProps = { + initialize: initialize.started +}; + +export default DragDropContext(HTML5Backend)( + connect(mapStateToProps, mapDispatchToProps, null, { pure: false })(App) +); diff --git a/src/app/containers/App/index.tsx b/src/app/containers/App/index.tsx deleted file mode 100644 index 0d984cd95..000000000 --- a/src/app/containers/App/index.tsx +++ /dev/null @@ -1,66 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as React from 'react'; -import { connect } from 'react-redux'; -import { IRootState } from 'modules'; -import { IntlProvider } from 'react-intl'; -import { DragDropContext } from 'react-dnd'; -import { switchWindow } from 'modules/gui/actions'; -import HTML5Backend from 'react-dnd-html5-backend'; - -import App from 'components/App'; -import ThemeProvider from 'components/Theme/ThemeProvider'; -import baseTheme from 'components/Theme/baseTheme'; - -export interface IAppContainerProps { -} - -interface IAppContainerState { - locale: string; - isAuthenticated: boolean; - isLoaded: boolean; - isCollapsed: boolean; - isFatal: boolean; - localeMessages: { [key: string]: string }; - securityWarningClosed: boolean; -} - -interface IAppContainerDispatch { - switchWindow: typeof switchWindow.started; -} - -const AppContainer: React.SFC = props => ( - - - - - -); - -const mapStateToProps = (state: IRootState) => ({ - locale: state.engine.locale || 'en-US', - localeMessages: state.engine.localeMessages, - isAuthenticated: state.auth.isAuthenticated, - isCollapsed: state.engine.isCollapsed, - isLoaded: state.engine.isLoaded, - isFatal: !!state.engine.fatalError, - securityWarningClosed: state.storage.securityWarningClosed -}); - -const mapDispatchToProps = { - switchWindow: switchWindow.started -}; - -export default DragDropContext(HTML5Backend)( - connect(mapStateToProps, mapDispatchToProps, null, { pure: false })(AppContainer) -); diff --git a/src/app/containers/Auth/Wallet/Create.tsx b/src/app/containers/Auth/Wallet/Create.tsx index ed668cd8c..9ad3e21e4 100644 --- a/src/app/containers/Auth/Wallet/Create.tsx +++ b/src/app/containers/Auth/Wallet/Create.tsx @@ -3,54 +3,23 @@ * See LICENSE in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import React from 'react'; import { connect } from 'react-redux'; import { IRootState } from 'modules'; -import { createWallet, importSeed, generateSeed, changeSeed, changeSeedConfirmation, importSeedConfirmation } from 'modules/auth/actions'; +import { createWallet } from 'modules/auth/actions'; import { ICreateWalletCall } from 'apla/auth'; import { sendAttachment } from 'modules/io/actions'; import Create from 'components/Auth/Wallet/Create'; -export interface ICreateContainerProps { - -} - -interface ICreateContainerState { - seed: string; - seedConfirm: string; -} - -interface ICreateContainerDispatch { - onImportSeed: () => void; - onImportSeedConfirmation: () => void; - onCreate: typeof createWallet.started; - onGenerateSeed: () => void; - onChangeSeed: (value: string) => void; - onChangeSeedConfirmation: (value: string) => void; - onDownloadSeed: (seed: string) => void; -} - -const mapStateToProps = (state: IRootState) => ({ - seed: state.auth.seed, - seedConfirm: state.auth.seedConfirm +const mapStateToProps = (_state: IRootState) => ({ }); const mapDispatchToProps = { onCreate: (params: ICreateWalletCall) => createWallet.started(params), - onImportSeed: (file: File) => importSeed.started(file), - onImportSeedConfirmation: (file: File) => importSeedConfirmation.started(file), - onGenerateSeed: () => generateSeed.started(null), - onChangeSeed: (value: string) => changeSeed(value), - onChangeSeedConfirmation: (value: string) => changeSeedConfirmation(value), onDownloadSeed: (seed: string) => sendAttachment({ name: 'seed.txt', data: seed }) }; -const CreateContainer: React.SFC = props => ( - -); - -export default connect(mapStateToProps, mapDispatchToProps)(CreateContainer); \ No newline at end of file +export default connect(mapStateToProps, mapDispatchToProps)(Create); \ No newline at end of file diff --git a/src/app/containers/Auth/Wallet/Import.tsx b/src/app/containers/Auth/Wallet/Import.tsx index d9dd1069a..80534a428 100644 --- a/src/app/containers/Auth/Wallet/Import.tsx +++ b/src/app/containers/Auth/Wallet/Import.tsx @@ -3,39 +3,17 @@ * See LICENSE in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import React from 'react'; import { connect } from 'react-redux'; import { IRootState } from 'modules'; -import { importSeed, importWallet } from 'modules/auth/actions'; +import { importWallet } from 'modules/auth/actions'; import Import from 'components/Auth/Wallet/Import'; -export interface IImportContainerProps { - -} - -interface IImportContainerState { - backup: string; - pending: boolean; -} - -interface IImportContainerDispatch { - onImportBackup: () => void; - onConfirm: (params: { backup: string, password: string }) => void; -} - -const mapStateToProps = (state: IRootState) => ({ - backup: state.auth.seed, - pending: false +const mapStateToProps = (_state: IRootState) => ({ }); const mapDispatchToProps = { - onImportBackup: (file: File) => importSeed.started(file), onConfirm: importWallet.started }; -const ImportContainer: React.SFC = props => ( - -); - -export default connect(mapStateToProps, mapDispatchToProps)(ImportContainer); \ No newline at end of file +export default connect(mapStateToProps, mapDispatchToProps)(Import); \ No newline at end of file diff --git a/src/app/containers/Auth/index.ts b/src/app/containers/Auth/index.ts new file mode 100644 index 000000000..300b5cacd --- /dev/null +++ b/src/app/containers/Auth/index.ts @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { connect } from 'react-redux'; +import { IRootState } from 'modules'; +import { modalShow } from 'modules/modal/actions'; + +import Auth from 'components/Auth'; + +const mapStateToProps = (state: IRootState) => ({ +}); + +const mapDispatchToProps = { + changeLocale: () => modalShow({ + id: 'CHANGE_LOCALE', + type: 'CHANGE_LOCALE', + params: {} + }) +}; + +export default connect(mapStateToProps, mapDispatchToProps)(Auth); \ No newline at end of file diff --git a/src/app/containers/Auth/index.tsx b/src/app/containers/Auth/index.tsx deleted file mode 100644 index 3fbdf0dd8..000000000 --- a/src/app/containers/Auth/index.tsx +++ /dev/null @@ -1,48 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import React from 'react'; -import { connect } from 'react-redux'; -import { IRootState } from 'modules'; -import { modalShow } from 'modules/modal/actions'; - -import Auth from 'components/Auth'; - -export interface IAuthContainerProps { - -} - -interface IAuthContainerState { - locale: string; -} - -interface IAuthContainerDispatch { - changeLocale: (locale: string) => void; -} - -const mapStateToProps = (state: IRootState) => ({ - locale: state.storage.locale -}); - -const mapDispatchToProps = { - changeLocale: (locale: string) => modalShow({ - id: 'CHANGE_LOCALE', - type: 'CHANGE_LOCALE', - params: { - value: locale - } - }) -}; - -const AuthContainer: React.SFC = props => { - const changeLocale = () => - props.changeLocale(props.locale); - - return ( - - ); -}; - -export default connect(mapStateToProps, mapDispatchToProps)(AuthContainer); \ No newline at end of file diff --git a/src/app/containers/Button/TxButton.tsx b/src/app/containers/Button/TxButton.tsx index 58f78be96..814fc9fe5 100644 --- a/src/app/containers/Button/TxButton.tsx +++ b/src/app/containers/Button/TxButton.tsx @@ -10,15 +10,23 @@ import { OrderedMap } from 'immutable'; import { IRootState } from 'modules'; import { connect } from 'react-redux'; import { buttonInteraction } from 'modules/content/actions'; +import { TBreadcrumbType } from 'apla/content'; +import { IErrorRedirect, IAction } from 'apla/protypo'; import Button from 'components/Button'; -import { IErrorRedirect } from 'apla/protypo'; export interface ITxButtonProps { disabled?: boolean; silent?: boolean; className?: string; + actions: IAction[]; + from?: { + type: TBreadcrumbType; + section: string; + name: string; + }; + // Called first confirm?: { icon: string; @@ -42,6 +50,7 @@ export interface ITxButtonProps { // Redirect if all previous actions succeeded page?: string; + section: string; pageParams?: { [key: string]: any } | (() => { [key: string]: any }); // Page must be rendered within a modal dialog @@ -100,11 +109,14 @@ class TxButton extends React.Component ({ - account: state.auth.wallet && state.auth.wallet.wallet, - privateKey: state.auth.privateKey -}); - -export default connect(mapStateToProps, { - onError: () => modalShow({ - id: 'E_INVALID_PASSWORD', - type: 'AUTH_ERROR', - params: { - error: 'E_INVALID_PASSWORD' - } - }), - onCopy: () => modalShow({ - id: 'I_COPIED', - type: 'INFO', - params: { - value: () - } - }) -})(Backup); \ No newline at end of file diff --git a/src/app/containers/Main/DefaultPage/index.tsx b/src/app/containers/Main/DefaultPage/index.tsx deleted file mode 100644 index 55fbe6f4c..000000000 --- a/src/app/containers/Main/DefaultPage/index.tsx +++ /dev/null @@ -1,40 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import React from 'react'; -import { connect } from 'react-redux'; -import { IRootState } from 'modules'; -import { ecosystemInit } from 'modules/content/actions'; - -export interface IDefaultPageContainerProps { -} - -interface IDefaultPageContainerState { - -} - -interface IDefaultPageContainerDispatch { - ecosystemInit: typeof ecosystemInit.started; -} - -class DefaultPageContainer extends React.Component { - componentDidMount() { - this.props.ecosystemInit({}); - } - - render() { - return null as JSX.Element; - } -} - -const mapStateToProps = (state: IRootState) => ({ - -}); - -const mapDispatchToProps = { - ecosystemInit: ecosystemInit.started -}; - -export default connect(mapStateToProps, mapDispatchToProps)(DefaultPageContainer); \ No newline at end of file diff --git a/src/app/containers/Main/Editor/ConstructorTabbed.tsx b/src/app/containers/Main/Editor/ConstructorTabbed.tsx index fd8bc783b..d39289b5e 100644 --- a/src/app/containers/Main/Editor/ConstructorTabbed.tsx +++ b/src/app/containers/Main/Editor/ConstructorTabbed.tsx @@ -13,6 +13,7 @@ import { generatePageTemplate } from 'modules/editor/actions'; import { IChangePageCall, TConstructorData, IAddTagCall, IOperateTagCall, IMoveTreeTag, ISetTagCanDropPositionCall } from 'apla/editor'; export interface IConstructorTabbedContainerProps { + section: string; pageID: string; pageName: string; menus?: { id: string, name: string, conditions: string, value: string }[]; @@ -153,6 +154,7 @@ class ConstructorTabbedContainer extends React.Component { - private _pendingClose: number; - - constructor(props: IEditorContainerProps & InjectedIntlProps & IEditorContainerState & IEditorContainerDispatch) { - super(props); - - if (props.open && props.name) { - props.onTabLoad({ - type: props.open, - name: props.name - }); - } - else if (props.appId && props.create) { - props.onTabCreate({ - type: props.create, - appId: props.appId - }); - } - } - - componentWillReceiveProps(props: IEditorContainerProps & IEditorContainerState & IEditorContainerDispatch) { - if ('number' === typeof this._pendingClose && props.modalResult) { - if ('RESULT' === props.modalResult.reason) { - this.props.onTabClose(this._pendingClose); - } - - this._pendingClose = null; - } - - if (props.open && props.name && (this.props.open !== props.open || this.props.name !== props.name)) { - props.onTabLoad({ - type: props.open, - name: props.name - }); - } - - if (props.appId && props.create && (this.props.appId !== props.appId && this.props.create !== props.create)) { - props.onTabCreate({ - type: props.create, - appId: props.appId - }); - } - } - - onTabClose = (index: number) => { - const tab = this.props.tabs[index]; - - if (tab.dirty) { - this._pendingClose = index; - this.props.modalShow({ - id: 'EDITOR_CLOSE', - type: 'CONFIRM', - params: { - description: this.props.intl.formatMessage({ - id: 'editor.close.confirm', - defaultMessage: 'Do you really want to close \'{name}\' without saving changes?' - }, { name: tab.name }) - } - }); - } - else { - this.props.onTabClose(index); - } - } - - render() { - return ( - - ); - } -} - const mapStateToProps = (state: IRootState) => ({ + mainSection: state.sections.mainSection, tabIndex: state.editor.tabIndex, tabs: state.editor.tabs, - modalResult: state.modal.result }); const mapDispatchToProps = { - modalShow: modalShow, - onTabCreate: createEditorTab.started, onTabLoad: loadEditorTab.started, - onTabChange: changeEditorTab, - onTabClose: closeEditorTab, - onTabCloseAll: closeAllEditorTab, - onTabCloseSaved: closeSavedEditorTab, + onTabChange: (uuid: string) => changeEditorTab(uuid), + onTabClose: (uuid: string) => closeEditorTab(uuid), + onTabCloseAll: () => modalShow({ + id: 'EDITOR_CLOSE_ALL', + type: 'EDITOR_CLOSE_ALL', + params: {} + }), + onTabCloseSaved: () => closeSavedEditorTab(), onTabUpdate: updateEditorTab, }; -export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(EditorContainer)); \ No newline at end of file +export default connect(mapStateToProps, mapDispatchToProps)(Editor); \ No newline at end of file diff --git a/src/app/containers/Main/Header/LangMenu.ts b/src/app/containers/Main/Header/LangMenu.ts new file mode 100644 index 000000000..266cbdd79 --- /dev/null +++ b/src/app/containers/Main/Header/LangMenu.ts @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IRootState } from 'modules'; +import { connect } from 'react-redux'; +import { setLocale } from 'modules/engine/actions'; + +import LangMenu from 'components/Main/Header/LangMenu'; + +const mapStateToProps = (state: IRootState) => ({ + locale: state.engine.locale, + locales: state.engine.locales +}); + +export default connect(mapStateToProps, { + onChange: setLocale.started + +})(LangMenu); \ No newline at end of file diff --git a/src/app/containers/Main/Header/NotificationsMenu.ts b/src/app/containers/Main/Header/NotificationsMenu.ts new file mode 100644 index 000000000..a4912821e --- /dev/null +++ b/src/app/containers/Main/Header/NotificationsMenu.ts @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { connect } from 'react-redux'; +import { IRootState } from 'modules'; + +import NotificationsMenu from 'components/Main/Header/NotificationsMenu'; +import findNotificationsCount from 'modules/socket/util/findNotificationsCount'; + +const mapStateToProps = (state: IRootState) => ({ + count: findNotificationsCount(state.socket, state.auth.wallet), + offline: !state.socket.connected, + mainSection: state.sections.mainSection, + notificationsBody: state.content.notifications +}); + +export default connect(mapStateToProps)(NotificationsMenu); \ No newline at end of file diff --git a/src/app/containers/Widgets/UserMenu/index.tsx b/src/app/containers/Main/Header/UserMenu.ts similarity index 87% rename from src/app/containers/Widgets/UserMenu/index.tsx rename to src/app/containers/Main/Header/UserMenu.ts index f7a934f6b..faa0a6633 100644 --- a/src/app/containers/Widgets/UserMenu/index.tsx +++ b/src/app/containers/Main/Header/UserMenu.ts @@ -5,10 +5,10 @@ import { IRootState } from 'modules'; import { connect } from 'react-redux'; -import { logout, changePassword, switchWallet } from 'modules/auth/actions'; +import { logout, changePassword, switchWallet, backupAccount } from 'modules/auth/actions'; import { modalShow } from 'modules/modal/actions'; -import UserMenu from 'components/Main/UserMenu'; +import UserMenu from 'components/Main/Header/UserMenu'; const mapStateToProps = (state: IRootState) => ({ isDefaultWallet: state.auth.isDefaultWallet, @@ -30,6 +30,7 @@ export default connect(mapStateToProps, { ecosystem } }), + onBackup: () => backupAccount(), onChangePassword: () => changePassword.started(null) })(UserMenu); \ No newline at end of file diff --git a/src/app/modules/engine/reducers/setCollapsedHandler.ts b/src/app/containers/Main/Header/index.ts similarity index 52% rename from src/app/modules/engine/reducers/setCollapsedHandler.ts rename to src/app/containers/Main/Header/index.ts index aae00714c..695b73f15 100644 --- a/src/app/modules/engine/reducers/setCollapsedHandler.ts +++ b/src/app/containers/Main/Header/index.ts @@ -3,13 +3,13 @@ * See LICENSE in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { State } from '../reducer'; -import { setCollapsed } from '../actions'; -import { Reducer } from 'modules'; +import { connect } from 'react-redux'; +import { IRootState } from 'modules'; -const setCollapsedHandler: Reducer = (state, payload) => ({ - ...state, - isCollapsed: payload +import Header from 'components/Main/Header'; + +const mapStateToProps = (state: IRootState) => ({ + isAuthorized: !!state.auth.privateKey }); -export default setCollapsedHandler; \ No newline at end of file +export default connect(mapStateToProps)(Header); \ No newline at end of file diff --git a/src/app/containers/Main/Navigation/ResizeHandle.tsx b/src/app/containers/Main/Navigation/ResizeHandle.tsx deleted file mode 100644 index 2e35c2bc0..000000000 --- a/src/app/containers/Main/Navigation/ResizeHandle.tsx +++ /dev/null @@ -1,47 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as React from 'react'; -import { connect } from 'react-redux'; -import { IRootState } from 'modules'; -import { setResizing } from 'modules/content/actions'; -import { navigationToggle } from 'modules/sections/actions'; -import { saveNavigationSize } from 'modules/storage/actions'; - -import ResizeHandle from 'components/Main/Navigation/ResizeHandle'; - -interface IResizeHandleContainerProps { - -} - -interface IResizeHandleContainerState { - width: number; - resizing: boolean; - disabled: boolean; -} - -interface IResizeHandleContainerDispatch { - setResizing: typeof setResizing; - navigationResize: typeof saveNavigationSize; - navigationToggle: typeof navigationToggle; -} - -const ResizeHandleContainer: React.SFC = (props) => ( - -); - -const mapStateToProps = (state: IRootState) => ({ - width: state.storage.navigationSize, - resizing: state.content.navigationResizing, - disabled: !state.engine.isCollapsed -}); - -const mapDispatchToProps = { - setResizing, - navigationResize: saveNavigationSize, - navigationToggle -}; - -export default connect(mapStateToProps, mapDispatchToProps)(ResizeHandleContainer); \ No newline at end of file diff --git a/src/app/containers/Main/Navigation/index.tsx b/src/app/containers/Main/Navigation/index.tsx deleted file mode 100644 index a644b64eb..000000000 --- a/src/app/containers/Main/Navigation/index.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as React from 'react'; -import { connect } from 'react-redux'; -import { IRootState } from 'modules'; -import { ecosystemInit } from 'modules/content/actions'; -import { menuPop, menuPush } from 'modules/sections/actions'; -import { TMenu } from 'apla/content'; - -import Navigation from 'components/Main/Navigation'; - -interface INavigationContainerProps { - -} - -interface INavigationContainerState { - preloading: boolean; - preloadingError: string; - visible: boolean; - width: number; - menus: TMenu[]; -} - -interface INavigationContainerDispatch { - menuPop: typeof menuPop; - menuPush: typeof menuPush; - ecosystemInit: typeof ecosystemInit.started; -} - -const NavigationContainer: React.SFC = (props) => ( - -); - -const mapStateToProps = (state: IRootState) => { - const section = state.sections.sections[state.sections.section] || state.sections.sections.home; - return { - preloading: state.content.preloading, - preloadingError: state.content.preloadingError, - visible: state.sections.sections[state.sections.section] && ( - state.sections.sections[state.sections.section].menuDisabled ? - false : - state.sections.sections[state.sections.section].menuVisible - ), - width: state.storage.navigationSize, - menus: section ? section.menus : [] - }; -}; - -const mapDispatchToProps = { - menuPop, - menuPush, - ecosystemInit: ecosystemInit.started -}; - -export default connect(mapStateToProps, mapDispatchToProps)(NavigationContainer); \ No newline at end of file diff --git a/src/app/containers/Main/Navigator/Menu/ResizeHandle.ts b/src/app/containers/Main/Navigator/Menu/ResizeHandle.ts new file mode 100644 index 000000000..d8e3ad2ae --- /dev/null +++ b/src/app/containers/Main/Navigator/Menu/ResizeHandle.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { connect } from 'react-redux'; +import { IRootState } from 'modules'; +import { setMenuFolded } from 'modules/storage/actions'; + +import ResizeHandle from 'components/Main/Navigator/Menu/ResizeHandle'; + +const mapStateToProps = (state: IRootState) => ({ + folded: state.storage.menuFolded +}); + +const mapDispatchToProps = { + setMenuFolded +}; + +export default connect(mapStateToProps, mapDispatchToProps, (state, dispatch: any) => ({ + onFoldToggle: () => dispatch.setMenuFolded(!state.folded) +}))(ResizeHandle); \ No newline at end of file diff --git a/src/app/containers/Main/Navigator/Menu/index.ts b/src/app/containers/Main/Navigator/Menu/index.ts new file mode 100644 index 000000000..42a9709c0 --- /dev/null +++ b/src/app/containers/Main/Navigator/Menu/index.ts @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { connect } from 'react-redux'; +import { IRootState } from 'modules'; +import { menuPop } from 'modules/sections/actions'; +import { setMenuActive } from 'modules/content/actions'; + +import Menu from 'components/Main/Navigator/Menu'; + +export interface INavigationProps { + section: string; +} + +const mapStateToProps = (state: IRootState, props: INavigationProps) => { + const section = state.sections.sections[props.section]; + return { + menus: section ? section.menus : [], + folded: state.storage.menuFolded, + active: state.content.menuActive + }; +}; + +const mapDispatchToProps = { + menuPop, + setMenuActive +}; + +export default connect(mapStateToProps, mapDispatchToProps, (state, dispatch: any, props) => ({ + ...state, + ...props, + menuPop: () => dispatch.menuPop(props.section), + onMouseOver: () => { + if (state.folded && !state.active) { + dispatch.setMenuActive(true); + } + }, + onMouseLeave: () => { + if (state.folded && state.active) { + dispatch.setMenuActive(false); + } + } +}))(Menu); \ No newline at end of file diff --git a/src/app/containers/Main/Navigator/Sections/Selector.ts b/src/app/containers/Main/Navigator/Sections/Selector.ts new file mode 100644 index 000000000..4d242d13d --- /dev/null +++ b/src/app/containers/Main/Navigator/Sections/Selector.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import _ from 'lodash'; +import { connect } from 'react-redux'; +import { IRootState } from 'modules'; +import { ISection, TPageParams } from 'apla/content'; + +import Selector from 'components/Main/Navigator/Sections/Selector'; + +const mapSectionParam = (section: ISection) => { + const page = section.page ? section.page.name : section.defaultPage; + const params: TPageParams = section.page ? section.page.params : {}; + + return { + index: section.index, + title: section.title, + name: section.name, + page, + params + }; +}; + +const mapStateToProps = (state: IRootState, props: { section?: string }) => ({ + section: null === props.section ? null : (props.section || state.sections.mainSection), + values: _.map(state.sections.sections, mapSectionParam).sort((a, b) => a.index - b.index) +}); + +export default connect(mapStateToProps)(Selector); \ No newline at end of file diff --git a/src/app/containers/Main/Navigator/Sections/index.ts b/src/app/containers/Main/Navigator/Sections/index.ts new file mode 100644 index 000000000..7ba9eab2d --- /dev/null +++ b/src/app/containers/Main/Navigator/Sections/index.ts @@ -0,0 +1,21 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { connect } from 'react-redux'; +import { IRootState } from 'modules'; +import { menuPop } from 'modules/sections/actions'; + +import Sections from 'components/Main/Navigator/Sections'; + +const mapStateToProps = (state: IRootState) => ({ + folded: state.storage.menuFolded, + menuActive: state.content.menuActive +}); + +const mapDispatchToProps = { + menuPop: menuPop +}; + +export default connect(mapStateToProps, mapDispatchToProps)(Sections); \ No newline at end of file diff --git a/src/app/containers/Main/Navigator/index.ts b/src/app/containers/Main/Navigator/index.ts new file mode 100644 index 000000000..18e65d1e4 --- /dev/null +++ b/src/app/containers/Main/Navigator/index.ts @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { connect } from 'react-redux'; +import { IRootState } from 'modules'; +import { reloadPage } from 'modules/sections/actions'; + +import Navigator from 'components/Main/Navigator'; + +interface Props { + section?: string; + page?: string; +} + +const mapStateToProps = (state: IRootState, props: Props) => { + const sectionName = props.section || state.sections.mainSection; + const section = state.sections.sections[sectionName]; + const defaultPage = section ? section.defaultPage : ''; + const page = props.page || defaultPage; + + return { + stylesheet: state.content.stylesheet, + section: sectionName, + sections: state.sections.sections, + page + }; +}; + +const mapDispatchToProps = { + reloadPage +}; + +export default connect(mapStateToProps, mapDispatchToProps, (state, dispatch: any, props) => ({ + ...state, + onRefresh: () => dispatch.reloadPage({ section: props.section }) +}))(Navigator); \ No newline at end of file diff --git a/src/app/containers/Main/Page/index.tsx b/src/app/containers/Main/Page/index.tsx deleted file mode 100644 index d74ac0386..000000000 --- a/src/app/containers/Main/Page/index.tsx +++ /dev/null @@ -1,138 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as React from 'react'; -import * as _ from 'lodash'; -import * as queryString from 'query-string'; -import { connect } from 'react-redux'; -import { IRootState } from 'modules'; -import { ecosystemInit } from 'modules/content/actions'; -import { renderPage, renderLegacyPage, navigatePage, switchSection } from 'modules/sections/actions'; -import { TSection } from 'apla/content'; -import { LEGACY_PAGES } from 'lib/legacyPages'; - -import Page from 'components/Main/Page'; -import NotFound from 'components/Main/Page/NotFound'; - -export interface IPageContainerProps { - match?: { params: { [key: string]: string } }; - location?: { - key: string; - search: string; - }; -} - -interface IPageContainerState { - initialized: boolean; - section: string; - sections: { - [name: string]: TSection; - }; -} - -interface IPageContainerDispatch { - navigatePage: typeof navigatePage.started; - renderPage: typeof renderPage.started; - renderLegacyPage: typeof renderLegacyPage.started; - switchSection: typeof switchSection; - ecosystemInit: typeof ecosystemInit.started; -} - -class PageContainer extends React.Component { - componentDidMount() { - if (!this.props.initialized) { - this.props.ecosystemInit({ - section: this.props.match.params.section - }); - } - } - - componentWillReceiveProps(props: IPageContainerProps & IPageContainerState & IPageContainerDispatch) { - if (!props.initialized) { - return; - } - - if (props.section !== props.match.params.section && this.props.match.params.section !== props.match.params.section) { - this.props.switchSection(props.match.params.section); - } - else { - this.renderPage(props); - } - } - - renderPage(props: IPageContainerProps & IPageContainerState & IPageContainerDispatch) { - const section = props.sections[props.match.params.section]; - const isPending = section ? section.pending : false; - const requestPage = (props.match.params.pageName || (section && section.defaultPage)); - const params = queryString.parse(props.location.search); - - if (!isPending && section) { - if (!section.page || section.page.name !== requestPage || section.force || this.props.location.search !== props.location.search) { - const legacyPage = LEGACY_PAGES[requestPage]; - - if (legacyPage && (!legacyPage.section || section.name === legacyPage.section)) { - props.renderLegacyPage({ - section: legacyPage.section || section.name, - name: requestPage, - menu: legacyPage.menu, - params - }); - } - else { - props.renderPage({ - key: props.location.key, - section: section.name, - name: requestPage, - params - }); - } - } - } - } - - render() { - return ( -
    - {this.props.initialized && !this.props.sections[this.props.match.params.section] && ( - - )} - {_.map(this.props.sections, section => { - const isLegacy = section.page && section.page.legacy; - const legacyPage = isLegacy ? LEGACY_PAGES[section.page.name] : null; - - return ( -
    - {isLegacy ? legacyPage.render({ ...section.page.params, children: section.page && section.page.content }) : ( - - )} -
    - ); - })} -
    - ); - } -} - -const mapStateToProps = (state: IRootState) => ({ - initialized: state.sections.inited, - section: state.sections.section, - sections: state.sections.sections -}); - -const mapDispatchToProps = { - navigatePage: navigatePage.started, - renderPage: renderPage.started, - renderLegacyPage: renderLegacyPage.started, - ecosystemInit: ecosystemInit.started, - switchSection -}; - -export default connect(mapStateToProps, mapDispatchToProps)(PageContainer); \ No newline at end of file diff --git a/src/app/containers/Main/Titlebar/SystemMenu.tsx b/src/app/containers/Main/Titlebar/SystemMenu.tsx deleted file mode 100644 index 1f949a1eb..000000000 --- a/src/app/containers/Main/Titlebar/SystemMenu.tsx +++ /dev/null @@ -1,57 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import React from 'react'; -import { IRootState } from 'modules'; -import { connect } from 'react-redux'; -import { modalShow } from 'modules/modal/actions'; - -import SystemMenu from 'components/Main/Titlebar/SystemMenu'; - -export interface ISystemMenuContainerProps { - align: 'left' | 'right'; -} - -interface ISystemMenuContainerState { - locale: string; -} - -interface ISystemMenuContainerDispatch { - modalShow: typeof modalShow; -} - -const SystemMenuContainer: React.SFC = (props) => { - const onAbout = () => { - props.modalShow({ - id: 'ABOUT', - type: 'ABOUT', - params: {} - }); - }; - - const onChangeLocale = () => { - props.modalShow({ - id: 'CHANGE_LOCALE', - type: 'CHANGE_LOCALE', - params: { - value: props.locale - } - }); - }; - - return ( - - ); -}; - -const mapStateToProps = (state: IRootState) => ({ - locale: state.storage.locale -}); - -const mapDispatchToProps = { - modalShow: modalShow -}; - -export default connect(mapStateToProps, mapDispatchToProps)(SystemMenuContainer); \ No newline at end of file diff --git a/src/app/containers/Main/Toolbar/EditorToolbar.tsx b/src/app/containers/Main/Toolbar/EditorToolbar.tsx deleted file mode 100644 index 3db0fd892..000000000 --- a/src/app/containers/Main/Toolbar/EditorToolbar.tsx +++ /dev/null @@ -1,110 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as React from 'react'; -import { IRootState } from 'modules'; -import { connect } from 'react-redux'; -import { modalShow } from 'modules/modal/actions'; -import { editorSave, revertEditorTab, changeEditorTool, debugContract } from 'modules/editor/actions'; -import { IModalResult } from 'apla/modal'; -import { TEditorTab } from 'apla/editor'; -import { injectIntl, InjectedIntlProps } from 'react-intl'; - -import EditorToolbar from 'components/Main/Toolbar/EditorToolbar'; - -export interface IEditorToolbarProps { - -} - -interface IEditorToolbarState { - modalResult: IModalResult; - currentTab: TEditorTab; - currentTabIndex: number; - canSave: boolean; - canRevert: boolean; -} - -interface IEditorToolbarDispatch { - modalShow: typeof modalShow; - changeEditorTool: typeof changeEditorTool.started; - revertEditorTab: typeof revertEditorTab; - editorSave: typeof editorSave; - onExec: typeof debugContract; -} - -class EditorToolbarContainer extends React.Component { - private _pendingRevert: number; - - onRevert = () => { - this._pendingRevert = this.props.currentTabIndex; - this.props.modalShow({ - id: 'EDITOR_REVERT', - type: 'CONFIRM', - params: { - description: this.props.intl.formatMessage({ - id: 'editor.revert.confirm', - defaultMessage: 'Do you really want to discard all changes?' - }) - } - }); - } - - onSave = () => { - if (this.props.currentTab) { - this.props.editorSave(this.props.currentTab); - } - } - - onExec = () => { - this.props.onExec(this.props.currentTab.name); - } - - componentWillReceiveProps(props: IEditorToolbarProps & IEditorToolbarState & IEditorToolbarDispatch) { - if ('number' === typeof this._pendingRevert && props.modalResult) { - if ('RESULT' === props.modalResult.reason) { - this.props.revertEditorTab(this._pendingRevert); - } - this._pendingRevert = null; - } - } - - render() { - return ( - - ); - } -} - -const mapStateToProps = (state: IRootState) => { - const currentTab = state.editor.tabs[state.editor.tabIndex]; - - return { - currentTab, - currentTabIndex: state.editor.tabIndex, - modalResult: state.modal.result, - canSave: !state.editor.pending && - currentTab && currentTab.dirty, - canRevert: !state.editor.pending && - currentTab && (currentTab.dirty && null !== currentTab.initialValue) - }; -}; - -const mapDispatchToProps = { - modalShow: modalShow, - onExec: debugContract, - revertEditorTab: revertEditorTab, - changeEditorTool: changeEditorTool.started, - editorSave -}; - -export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(EditorToolbarContainer)); \ No newline at end of file diff --git a/src/app/containers/Main/index.tsx b/src/app/containers/Main/index.tsx deleted file mode 100644 index d887f104b..000000000 --- a/src/app/containers/Main/index.tsx +++ /dev/null @@ -1,61 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import React from 'react'; -import { connect, Dispatch } from 'react-redux'; -import { Route } from 'react-router-dom'; -import { IRootState } from 'modules'; -import { reloadPage, navigationToggle, renderSection, navigatePage, closeSection } from '../../modules/sections/actions'; - -import { AnimatedSwitch } from 'components/Animation'; -import Main, { IMainProps } from 'components/Main'; -import DefaultPage from 'containers/Main/DefaultPage'; -import Page from 'containers/Main/Page'; -import NotFound from 'components/NotFound'; - -const MainContainer: React.SFC = props => ( -
    - - - - } /> - -
    -); - -const mapStateToProps = (state: IRootState) => ({ - network: state.auth.session && state.auth.session.network, - isAuthorized: !!state.auth.privateKey, - stylesheet: state.content.stylesheet, - section: state.sections.section, - sections: state.sections.sections, - navigationSize: state.storage.navigationSize, - navigationVisible: state.sections.sections[state.sections.section] && - (state.sections.sections[state.sections.section].menuDisabled ? - false : - state.sections.sections[state.sections.section].menuVisible - ), - transactionsCount: state.tx.transactions.count() -}); - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - onNavigationToggle: () => { - dispatch(navigationToggle()); - }, - onNavigateHome: () => { - dispatch(navigatePage.started({ params: {} })); - }, - onRefresh: (section: string) => { - dispatch(reloadPage.started({})); - }, - onSwitchSection: (section: string) => { - dispatch(renderSection(section)); - }, - onCloseSection: (section: string) => { - dispatch(closeSection(section)); - } -}); - -export default connect(mapStateToProps, mapDispatchToProps)(MainContainer); \ No newline at end of file diff --git a/src/app/containers/Modal/BackupModal.tsx b/src/app/containers/Modal/BackupModal.tsx new file mode 100644 index 000000000..944eb6263 --- /dev/null +++ b/src/app/containers/Modal/BackupModal.tsx @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import React from 'react'; +import { connect } from 'react-redux'; +import { IRootState } from 'modules'; +import { IModalProps } from 'components/Modal'; +import { FormattedMessage } from 'react-intl'; +import { modalShow } from 'modules/modal/actions'; + +import BackupModal from 'components/Modal/BackupModal'; + +const mapStateToProps = (state: IRootState) => ({ + privateKey: state.auth.privateKey, + publicKey: state.auth.wallet.wallet.publicKey, + address: state.auth.wallet.wallet.address +}); + +export default connect(mapStateToProps, { + modalShow + +}, (state, dispatch: any, props: IModalProps) => ({ + ...props, + params: { + ...state, + onCopy: () => dispatch.modalShow({ + id: 'I_COPIED', + type: 'INFO', + params: { + value: () + } + }) + }, +}))(BackupModal); \ No newline at end of file diff --git a/src/app/containers/Modal/ChangeLocaleModal.tsx b/src/app/containers/Modal/ChangeLocaleModal.ts similarity index 66% rename from src/app/containers/Modal/ChangeLocaleModal.tsx rename to src/app/containers/Modal/ChangeLocaleModal.ts index 24f5d6e76..25d30a39f 100644 --- a/src/app/containers/Modal/ChangeLocaleModal.tsx +++ b/src/app/containers/Modal/ChangeLocaleModal.ts @@ -8,20 +8,23 @@ import { IRootState } from 'modules'; import { IModalProps } from 'components/Modal'; import { setLocale } from 'modules/engine/actions'; -import ChangeLocaleModal, { IChangeLocaleModalProps } from 'components/Modal/ChangeLocale'; +import ChangeLocaleModal from 'components/Modal/ChangeLocaleModal'; -const mapStateToProps = (state: IRootState, props: IModalProps) => ({ +const mapStateToProps = (state: IRootState) => ({ + value: state.engine.locale, locales: state.engine.locales }); export default connect(mapStateToProps, { setLocale: setLocale.started -}, (state, dispatch: any, props) => ({ +}, (state, dispatch: any, props: IModalProps) => ({ ...props, params: { + ...state, ...props.params, - locales: state.locales, - onChangeLocale: dispatch.setLocale - } + onChangeLocale: (locale: string) => { + dispatch.setLocale(locale); + } + }, }))(ChangeLocaleModal); \ No newline at end of file diff --git a/src/app/containers/Modal/EditorCloseAllModal.ts b/src/app/containers/Modal/EditorCloseAllModal.ts new file mode 100644 index 000000000..3bc514a77 --- /dev/null +++ b/src/app/containers/Modal/EditorCloseAllModal.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { connect } from 'react-redux'; +import { IRootState } from 'modules'; +import { modalClose } from 'modules/modal/actions'; +import { closeAllEditorTabs } from 'modules/editor/actions'; +import { IModalProps } from 'components/Modal'; + +import EditorCloseAllModal from 'components/Modal/EditorCloseAllModal'; + +const mapStateToProps = (state: IRootState) => ({ +}); + +export default connect(mapStateToProps, { + modalClose, + closeAllEditorTabs + +}, (_state, dispatch: any, props: IModalProps) => ({ + ...props, + onResult: (_data: void) => { + dispatch.modalClose({ + reason: 'RESULT', + data: null + }); + + dispatch.closeAllEditorTabs(); + } +}))(EditorCloseAllModal); \ No newline at end of file diff --git a/src/app/containers/Modal/EditorCloseModal.ts b/src/app/containers/Modal/EditorCloseModal.ts new file mode 100644 index 000000000..059bd6d4a --- /dev/null +++ b/src/app/containers/Modal/EditorCloseModal.ts @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { connect } from 'react-redux'; +import { IRootState } from 'modules'; +import { modalClose } from 'modules/modal/actions'; +import { destroyEditorTab } from 'modules/editor/actions'; +import { IModalProps } from 'components/Modal'; + +import EditorCloseModal from 'components/Modal/EditorCloseModal'; + +interface Props { + uuid: string; +} + +const mapStateToProps = (state: IRootState, props: IModalProps) => { + const tab = state.editor.tabs.find(t => t.uuid === props.params.uuid); + return { + name: tab ? tab.name : '' + }; +}; + +export default connect(mapStateToProps, { + modalClose, + destroyEditorTab + +}, (state, dispatch: any, props: IModalProps) => ({ + ...props, + params: { + ...state + }, + onResult: (_data: void) => { + dispatch.modalClose({ + reason: 'RESULT', + data: null + }); + + dispatch.destroyEditorTab(props.params.uuid); + } +}))(EditorCloseModal); \ No newline at end of file diff --git a/src/app/containers/Modal/EditorRevertModal.ts b/src/app/containers/Modal/EditorRevertModal.ts new file mode 100644 index 000000000..f1eaf0d51 --- /dev/null +++ b/src/app/containers/Modal/EditorRevertModal.ts @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { connect } from 'react-redux'; +import { IRootState } from 'modules'; +import { modalClose } from 'modules/modal/actions'; +import { resetEditorTab } from 'modules/editor/actions'; +import { IModalProps } from 'components/Modal'; + +import EditorRevertModal from 'components/Modal/EditorRevertModal'; + +interface Props { + uuid: string; +} + +const mapStateToProps = (state: IRootState, props: IModalProps) => { + const tab = state.editor.tabs.find(t => t.uuid === props.params.uuid); + return { + name: tab ? tab.name : '' + }; +}; + +export default connect(mapStateToProps, { + modalClose, + resetEditorTab + +}, (state, dispatch: any, props: IModalProps) => ({ + ...props, + params: { + ...state + }, + onResult: (_data: void) => { + dispatch.modalClose({ + reason: 'RESULT', + data: null + }); + + dispatch.resetEditorTab(props.params.uuid); + } +}))(EditorRevertModal); \ No newline at end of file diff --git a/src/app/containers/Modal/Header.ts b/src/app/containers/Modal/Header.ts new file mode 100644 index 000000000..6a70eeb60 --- /dev/null +++ b/src/app/containers/Modal/Header.ts @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { connect } from 'react-redux'; +import { IRootState } from 'modules'; +import { modalClose } from 'modules/modal/actions'; + +import Header from 'components/Modal/Header'; + +const mapStateToProps = (state: IRootState) => ({ + +}); + +const mapDispatchToProps = { + onClose: () => modalClose({ + reason: 'CANCEL', + data: null + }) +}; + +export default connect<{}, { onClose: () => void }, {}>(mapStateToProps, mapDispatchToProps)(Header); \ No newline at end of file diff --git a/src/app/containers/Modal/RolePickerModal.tsx b/src/app/containers/Modal/RolePickerModal.tsx index 2ae183b60..ec21f2d53 100644 --- a/src/app/containers/Modal/RolePickerModal.tsx +++ b/src/app/containers/Modal/RolePickerModal.tsx @@ -7,9 +7,9 @@ import { connect } from 'react-redux'; import { IRootState } from 'modules'; import { IModalProps } from 'components/Modal'; import { switchWallet } from 'modules/auth/actions'; +import { modalClose } from 'modules/modal/actions'; import RolePickerModal from 'components/Modal/RolePickerModal'; -import { modalClose } from 'modules/modal/actions'; export interface IRolePickerModalProps { account: string; diff --git a/src/app/containers/Routing/PageLink.ts b/src/app/containers/Routing/PageLink.ts new file mode 100644 index 000000000..4c2aa0e64 --- /dev/null +++ b/src/app/containers/Routing/PageLink.ts @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { connect } from 'react-redux'; +import { IRootState } from 'modules'; + +import PageLink from 'components/Routing/PageLink'; + +interface Props { + section?: string; +} + +const mapStateToProps = (state: IRootState, props: Props) => ({ + section: props.section || state.sections.mainSection +}); + +export default connect(mapStateToProps, {})(PageLink); \ No newline at end of file diff --git a/src/app/containers/Routing/PageLink.tsx b/src/app/containers/Routing/PageLink.tsx deleted file mode 100644 index 691e51107..000000000 --- a/src/app/containers/Routing/PageLink.tsx +++ /dev/null @@ -1,44 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as React from 'react'; -import { connect } from 'react-redux'; -import { IRootState } from 'modules'; -import { navigatePage } from '../../modules/sections/actions'; - -import PageLink from 'components/Routing/PageLink'; - -export interface IPageLinkContainerProps { - page: string; - section?: string; - className?: string; - params?: { - [key: string]: string; - }; -} - -interface IPageLinkContainerState { - mainSection: string; - currentSection: string; -} - -interface IPageLinkContainerDispatch { - navigatePage: typeof navigatePage.started; -} - -const PageLinkContainer: React.SFC = (props) => ( - -); - -const mapStateToProps = (state: IRootState) => ({ - mainSection: state.sections.mainSection, - currentSection: state.sections.section -}); - -const mapDispatchToProps = { - navigatePage: navigatePage.started -}; - -export default connect(mapStateToProps, mapDispatchToProps)(PageLinkContainer); \ No newline at end of file diff --git a/src/app/containers/Titlebar/SystemMenu.ts b/src/app/containers/Titlebar/SystemMenu.ts new file mode 100644 index 000000000..d75a30fab --- /dev/null +++ b/src/app/containers/Titlebar/SystemMenu.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IRootState } from 'modules'; +import { connect } from 'react-redux'; +import { modalShow } from 'modules/modal/actions'; + +import SystemMenu from 'components/Titlebar/SystemMenu'; + +const mapStateToProps = (state: IRootState) => ({ +}); + +const mapDispatchToProps = { + modalShow: modalShow +}; + +export default connect(mapStateToProps, mapDispatchToProps, (state, dispatch: any, props) => ({ + ...props, + onAbout: () => dispatch.modalShow({ + id: 'ABOUT', + type: 'ABOUT', + params: {} + }) +}))(SystemMenu); \ No newline at end of file diff --git a/src/app/containers/ToolButton/ToolButton.tsx b/src/app/containers/ToolButton/ToolButton.tsx index bb4cc038f..94a9ad218 100644 --- a/src/app/containers/ToolButton/ToolButton.tsx +++ b/src/app/containers/ToolButton/ToolButton.tsx @@ -5,6 +5,7 @@ import React from 'react'; import uuid from 'uuid'; +import propTypes from 'prop-types'; import { IRootState } from 'modules'; import { connect } from 'react-redux'; import { buttonInteraction } from 'modules/content/actions'; @@ -37,6 +38,11 @@ interface IToolButtonDispatch { class ToolButtonContainer extends React.Component { private _uuid: string = null; + static contextTypes = { + protypo: propTypes.object.isRequired, + section: propTypes.string.isRequired + }; + onClick = (e: React.MouseEvent) => { e.preventDefault(); this._uuid = uuid.v4(); @@ -59,9 +65,12 @@ class ToolButtonContainer extends React.Component { + const currentTab = state.editor.tabs[state.editor.tabIndex]; + + return { + currentTab, + canSave: !state.editor.pending && + currentTab && currentTab.dirty, + canRevert: !state.editor.pending && + currentTab && (currentTab.dirty && null !== currentTab.initialValue) + }; +}; + +const mapDispatchToProps = { + debugContract, + revertEditorTab, + editorSave, + createEditorTab: createEditorTab.started, + changeEditorTool: changeEditorTool.started +}; + +export default connect(mapStateToProps, mapDispatchToProps, (state, dispatch: any) => ({ + ...state, + onExec: () => { dispatch.debugContract(state.currentTab.name); }, + onRevert: () => { dispatch.revertEditorTab(state.currentTab.uuid); }, + onToolChange: (tool: string) => { dispatch.changeEditorTool(tool); }, + onSave: () => { dispatch.editorSave(state.currentTab); }, + onCreateTab: (type: string) => { dispatch.createEditorTab(type); } + +}))(EditorToolbar); \ No newline at end of file diff --git a/src/app/containers/Widgets/NotificationsMenu/index.tsx b/src/app/containers/Widgets/NotificationsMenu/index.tsx deleted file mode 100644 index af36aceee..000000000 --- a/src/app/containers/Widgets/NotificationsMenu/index.tsx +++ /dev/null @@ -1,61 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import React from 'react'; -import { connect } from 'react-redux'; -import { IRootState } from 'modules'; -import { fetchNotifications } from 'modules/content/actions'; -import { TProtypoElement } from 'apla/protypo'; - -import NotificationsMenu from 'components/Main/NotificationsMenu'; - -export interface INotificationsMenuContainerProps { - -} - -interface INotificationsMenuContainerState { - offline: boolean; - count: number; - notificationsBody: TProtypoElement[]; -} - -interface INotificationsMenuContainerDispatch { - fetchNotifications: typeof fetchNotifications.started; -} - -class NotificationsContainer extends React.Component { - componentWillReceiveProps(props: INotificationsMenuContainerProps & INotificationsMenuContainerState & INotificationsMenuContainerDispatch) { - if (this.props.count !== props.count) { - props.fetchNotifications(null); - } - } - - render() { - return ( - - ); - } -} - -const mapStateToProps = (state: IRootState) => { - const notifications = state.auth.wallet && state.auth.wallet.wallet ? state.socket.notifications.filter(l => - ((state.auth.wallet.role && state.auth.wallet.role.id === l.role) || l.role === '0') && - l.id === state.auth.wallet.wallet.id && - l.ecosystem === state.auth.wallet.access.ecosystem - ).map(l => l.count) : []; - const count = notifications.length ? notifications.reduce((a, b) => a + b) : 0; - - return { - count, - offline: !state.socket.connected, - notificationsBody: state.content.notifications - }; -}; - -const mapDispatchToProps = { - fetchNotifications: fetchNotifications.started -}; - -export default connect(mapStateToProps, mapDispatchToProps)(NotificationsContainer); \ No newline at end of file diff --git a/src/app/containers/Widgets/Protypo/index.tsx b/src/app/containers/Widgets/Protypo/index.tsx index 7bacd5cf4..d8bf5fafd 100644 --- a/src/app/containers/Widgets/Protypo/index.tsx +++ b/src/app/containers/Widgets/Protypo/index.tsx @@ -3,63 +3,31 @@ * See LICENSE in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as React from 'react'; import { connect } from 'react-redux'; import { IRootState } from 'modules'; -import { navigate } from 'modules/engine/actions'; import { displayData } from 'modules/content/actions'; -import { navigatePage, menuPush } from 'modules/sections/actions'; +import { menuPush } from 'modules/sections/actions'; import { TProtypoElement } from 'apla/protypo'; -import Protypo from 'components/Protypo'; - -export interface IProtypoContainerProps { - editable?: boolean; +export interface IProtypoProps { wrapper?: JSX.Element; context: string; + page?: string; + menu?: string; + section: string; content: TProtypoElement[]; - changePage?: any; - setTagCanDropPosition?: any; - addTag?: any; - moveTag?: any; - copyTag?: any; - removeTag?: any; - selectTag?: any; - selectedTag?: any; - logic?: boolean; -} - -interface IProtypoContainerState { - apiHost: string; - page: string; } -interface IProtypoContainerDispatch { - navigatePage: typeof navigatePage.started; - navigate: typeof navigate; - menuPush: typeof menuPush; - displayData: typeof displayData.started; -} - -const ProtypoContainer: React.SFC = (props) => ( - -); - -const mapStateToProps = (state: IRootState) => { - const section = state.sections.sections[state.sections.section]; +import Protypo from 'components/Protypo'; - return { - apiHost: state.auth.session && (state.auth.session.network.apiHost + '/api/v2'), - section: state.sections.section, - page: section && section.page && section.page.name - }; -}; +const mapStateToProps = (state: IRootState, props: IProtypoProps) => ({ + apiHost: state.auth.session && (state.auth.session.network.apiHost + '/api/v2'), + page: props.page, + ...props +}); -const mapDispatchToProps = { - navigatePage: navigatePage.started, - navigate, +export default connect(mapStateToProps, { menuPush, displayData: displayData.started -}; -export default connect(mapStateToProps, mapDispatchToProps)(ProtypoContainer); \ No newline at end of file +})(Protypo as any); \ No newline at end of file diff --git a/src/app/containers/Widgets/ProtypoConstructor/index.tsx b/src/app/containers/Widgets/ProtypoConstructor/index.tsx index c4a1926bd..0b86d8b0c 100644 --- a/src/app/containers/Widgets/ProtypoConstructor/index.tsx +++ b/src/app/containers/Widgets/ProtypoConstructor/index.tsx @@ -11,6 +11,7 @@ import { TProtypoElement } from 'apla/protypo'; import ProtypoConstructor from 'components/ProtypoConstructor'; export interface IProtypoConstructorContainerProps { + section: string; editable?: boolean; wrapper?: JSX.Element; context: string; @@ -38,12 +39,11 @@ const ProtypoConstructorContainer: React.SFC ); -const mapStateToProps = (state: IRootState) => { - const section = state.sections.sections[state.sections.section]; +const mapStateToProps = (state: IRootState, props: IProtypoConstructorContainerProps) => { + const section = state.sections.sections[props.section]; return { apiHost: state.auth.session && (state.auth.session.network.apiHost + '/api/v2'), - section: state.sections.section, page: section.page && section.page.name }; }; diff --git a/src/app/images/logoHeader.svg b/src/app/images/logoHeader.svg new file mode 100644 index 000000000..3133f976a --- /dev/null +++ b/src/app/images/logoHeader.svg @@ -0,0 +1,27 @@ + diff --git a/src/app/lib/aplaAPI/index.ts b/src/app/lib/aplaAPI/index.ts index c7624e84f..585e43078 100644 --- a/src/app/lib/aplaAPI/index.ts +++ b/src/app/lib/aplaAPI/index.ts @@ -210,7 +210,11 @@ class AplaAPI { }) }); public keyinfo = this.setEndpoint<{ id: string }, IKeyInfo>('get', 'keyinfo/{id}', { - requestTransformer: request => null + requestTransformer: request => null, + responseTransformer: response => ({ + ...response, + ecosystems: response.ecosystems || [] + }) }); // Data getters diff --git a/src/app/lib/routing.ts b/src/app/lib/routing.ts new file mode 100644 index 000000000..dd3d5405f --- /dev/null +++ b/src/app/lib/routing.ts @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) EGAAS S.A. All rights reserved. +* See LICENSE in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import React from 'react'; +import Header from 'containers/Main/Header'; +import Navigator from 'containers/Main/Navigator'; +import Editor from 'containers/Main/Editor'; + +interface RouteDict { + [name: string]: { + Header: React.ComponentType; + Content: React.ComponentType; + mapHeaderParams?: (params: any) => any; + mapContentParams?: (params: any) => any; + }; +} +export const mainRoute = '/:app?/:page?/:action?'; + +export const routes: RouteDict = { + browse: { + Header, + Content: Navigator, + mapContentParams: params => ({ + app: params.app, + section: params.page, + page: params.action + }) + }, + editor: { + Header, + Content: Editor + } +}; \ No newline at end of file diff --git a/src/app/lib/legacyPages.tsx b/src/app/lib/staticPages.tsx similarity index 50% rename from src/app/lib/legacyPages.tsx rename to src/app/lib/staticPages.tsx index 4171552d3..e255aabd5 100644 --- a/src/app/lib/legacyPages.tsx +++ b/src/app/lib/staticPages.tsx @@ -4,41 +4,28 @@ *--------------------------------------------------------------------------------------------*/ import React from 'react'; -import Backup from 'containers/Main/Backup'; -import Editor from 'containers/Main/Editor'; import TxInfo from 'containers/StaticPages/TxInfo'; -export interface ILegacyPage { - menu: string; - section: string; +export interface IStaticPage { renderSubstitute?: (props?: T) => { name: string; params: TSubParams; }; - render: (props?: T) => React.ReactNode; + render: (section: string, props?: T) => React.ReactNode; } -const LEGACY_PAGES: { [page: string]: ILegacyPage } = { - 'backup': { section: null, menu: null, render: () => }, - 'editor': { - section: 'editor', - menu: null, - render: (props: { open?: string, create?: string, name?: string, vde?: string }) => - - }, +const STATIC_PAGES: { [page: string]: IStaticPage } = { 'txinfo': { - section: null, - menu: null, renderSubstitute: props => ({ name: props.page, params: { txhashes: props.txhashes } }), - render: props => + render: (section, props) => } }; export { - LEGACY_PAGES + STATIC_PAGES }; \ No newline at end of file diff --git a/src/app/modules/auth/actions.ts b/src/app/modules/auth/actions.ts index fe0f8fdca..c3ed66e3e 100644 --- a/src/app/modules/auth/actions.ts +++ b/src/app/modules/auth/actions.ts @@ -9,15 +9,11 @@ import { ICreateWalletCall, IImportWalletCall } from 'apla/auth'; import { IAccount } from 'apla/api'; const actionCreator = actionCreatorFactory('auth'); +export const acquireSession = actionCreator.async('ACQUIRE_SESSION'); export const login = actionCreator.async('LOGIN'); export const loginGuest = actionCreator.async('LOGIN_GUEST'); export const logout = actionCreator.async('LOGOUT'); export const inviteEcosystem = actionCreator<{ ecosystem: string, redirectPage?: string }>('INVITE_ECOSYSTEM'); -export const generateSeed = actionCreator.async('GENERATE_SEED'); -export const importSeed = actionCreator.async('IMPORT_SEED'); -export const importSeedConfirmation = actionCreator.async('IMPORT_SEED_CONFIRMATION'); -export const changeSeed = actionCreator('CHANGE_SEED'); -export const changeSeedConfirmation = actionCreator('CHANGE_SEED_CONFIRMATION'); export const createWallet = actionCreator.async('CREATE_WALLET'); export const importWallet = actionCreator.async('IMPORT_WALLET'); export const removeWallet = actionCreator('REMOVE_WALLET'); @@ -27,4 +23,5 @@ export const authorize = actionCreator('AUTHORIZE'); export const deauthorize = actionCreator('DEAUTHORIZE'); export const changePassword = actionCreator.async('CHANGE_PASSWORD'); export const loadWallets = actionCreator.async('LOAD_WALLETS'); -export const loadWallet = actionCreator('LOAD_WALLET'); \ No newline at end of file +export const loadWallet = actionCreator('LOAD_WALLET'); +export const backupAccount = actionCreator('BACKUP_ACCOUNT'); \ No newline at end of file diff --git a/src/app/modules/auth/epic.ts b/src/app/modules/auth/epic.ts index 6866206e5..ecf404eb0 100644 --- a/src/app/modules/auth/epic.ts +++ b/src/app/modules/auth/epic.ts @@ -9,27 +9,24 @@ import logoutEpic from './epics/logoutEpic'; import authorizeEpic from './epics/authorizeEpic'; import createWalletEpic from './epics/createWalletEpic'; import importWalletEpic from './epics/importWalletEpic'; -import importSeedEpic from './epics/importSeedEpic'; -import generateSeedEpic from './epics/generateSeedEpic'; import authErrorEpic from './epics/authErrorEpic'; import removeWalletEpic from './epics/removeWalletEpic'; import logoutEmptySessionEpic from './epics/logoutEmptySessionEpic'; import changePasswordEpic from './epics/changePasswordEpic'; import changePasswordDoneEpic from './epics/changePasswordDoneEpic'; -import importSeedConfirmationEpic from './epics/importSeedConfirmationEpic'; import loadWalletsEpic from './epics/loadWalletsEpic'; import reloadWalletsEpic from './epics/reloadWalletsEpic'; import loadSavedWalletEpic from './epics/loadSavedWalletEpic'; import switchWalletEpic from './epics/switchWalletEpic'; import loginGuestEpic from './epics/loginGuestEpic'; +import acquireSessionEpic from './epics/acquireSessionEpic'; +import backupAccountEpic from './epics/backupAccountEpic'; export default combineEpics( + acquireSessionEpic, authorizeEpic, createWalletEpic, - generateSeedEpic, importWalletEpic, - importSeedEpic, - importSeedConfirmationEpic, loginEpic, authErrorEpic, logoutEmptySessionEpic, @@ -41,5 +38,6 @@ export default combineEpics( changePasswordEpic, changePasswordDoneEpic, switchWalletEpic, - loginGuestEpic + loginGuestEpic, + backupAccountEpic ); \ No newline at end of file diff --git a/src/app/modules/auth/epics/acquireSessionEpic.ts b/src/app/modules/auth/epics/acquireSessionEpic.ts new file mode 100644 index 000000000..81582ded4 --- /dev/null +++ b/src/app/modules/auth/epics/acquireSessionEpic.ts @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Action } from 'redux'; +import { Epic } from 'modules'; +import { acquireSession } from '../actions'; +import { ISection } from 'apla/content'; +import { sectionsInit } from 'modules/sections/actions'; +import { fetchNotifications, ecosystemInit } from 'modules/content/actions'; +import { Observable } from 'rxjs'; + +enum RemoteSectionStatus { + Removed = '0', + Default = '1', + Main = '2' +} + +const acquireSessionEpic: Epic = (action$, store, { api }) => action$.ofAction(acquireSession.started) + .flatMap(action => { + const state = store.getState(); + const client = api({ + apiHost: action.payload.network.apiHost, + sessionToken: action.payload.sessionToken + }); + + return Observable.forkJoin( + Observable.from(client.sections({ locale: state.storage.locale })).map(s => s.list), + Observable.from(client.getParam({ name: 'stylesheet' })).map(p => p.value).catch(e => Observable.of('')), + Observable.from(client.getParam({ name: 'print_stylesheet' })).map(p => p.value).catch(e => Observable.of('')) + + ).flatMap(([sections, stylesheet, printStylesheet]) => { + const sectionsResult: { [name: string]: ISection } = {}; + const mainSection = sections.find(l => RemoteSectionStatus.Main === l.status); + + sections.forEach((section, index) => { + sectionsResult[section.urlname] = { + index, + name: section.urlname, + title: section.title, + defaultPage: section.page, + breadcrumbs: [{ + caller: '', + type: 'PAGE', + title: section.title, + section: section.urlname, + page: section.page, + params: {} + }], + menus: [], + page: undefined + }; + }); + + return Observable.of( + sectionsInit({ + mainSection: mainSection ? mainSection.urlname : sections[0].urlname, + sections: sectionsResult + }), + ecosystemInit({ + stylesheet, + printStylesheet + }), + fetchNotifications.started(undefined), + acquireSession.done({ + params: action.payload, + result: true + }) + ); + }).catch(e => Observable.of(acquireSession.done({ + params: action.payload, + result: false + }))); + }); + +export default acquireSessionEpic; \ No newline at end of file diff --git a/src/app/modules/auth/epics/backupAccountEpic.ts b/src/app/modules/auth/epics/backupAccountEpic.ts new file mode 100644 index 000000000..3e6f071b8 --- /dev/null +++ b/src/app/modules/auth/epics/backupAccountEpic.ts @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Epic } from 'modules'; +import { backupAccount } from '../actions'; +import { modalShow } from 'modules/modal/actions'; +import { Observable } from 'rxjs'; +import { txAuthorize } from 'modules/tx/actions'; +import { isType } from 'typescript-fsa'; + +const backupAccountEpic: Epic = (action$, store) => action$.ofAction(backupAccount) + .flatMap(action => + Observable.if( + () => !!store.getState().auth.privateKey, + Observable.defer(() => Observable.of(modalShow({ + id: 'BACKUP', + type: 'BACKUP', + params: {} + }))), + Observable.merge( + Observable.of(txAuthorize.started({})), + action$.filter(l => txAuthorize.done.match(l) || txAuthorize.failed.match(l)) + .take(1) + .flatMap(result => Observable.if( + () => isType(result, txAuthorize.done), + Observable.defer(() => Observable.of(modalShow({ + id: 'BACKUP', + type: 'BACKUP', + params: {} + }))), + Observable.empty() + )) + ) + ) + ); + +export default backupAccountEpic; \ No newline at end of file diff --git a/src/app/modules/auth/epics/generateSeedEpic.ts b/src/app/modules/auth/epics/generateSeedEpic.ts deleted file mode 100644 index 60eeea7c7..000000000 --- a/src/app/modules/auth/epics/generateSeedEpic.ts +++ /dev/null @@ -1,18 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Epic } from 'modules'; -import { generateSeed } from '../actions'; -import keyring from 'lib/keyring'; - -const authorizeEpic: Epic = (action$, store) => action$.ofAction(generateSeed.started) - .map(action => - generateSeed.done({ - params: action.payload, - result: keyring.generateSeed() - }) - ); - -export default authorizeEpic; \ No newline at end of file diff --git a/src/app/modules/auth/epics/importSeedConfirmationEpic.ts b/src/app/modules/auth/epics/importSeedConfirmationEpic.ts deleted file mode 100644 index c9d5200e6..000000000 --- a/src/app/modules/auth/epics/importSeedConfirmationEpic.ts +++ /dev/null @@ -1,28 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Epic } from 'modules'; -import { importSeedConfirmation } from '../actions'; -import { Observable } from 'rxjs/Observable'; -import { readTextFile } from 'lib/fs'; - -const importSeedConfirmationEpic: Epic = (action$, store) => action$.ofAction(importSeedConfirmation.started) - .switchMap(action => - Observable.from(readTextFile(action.payload)) - .map(payload => - importSeedConfirmation.done({ - params: null, - result: payload - }) - - ).catch(e => - Observable.of(importSeedConfirmation.failed({ - params: null, - error: null - })) - ) - ); - -export default importSeedConfirmationEpic; \ No newline at end of file diff --git a/src/app/modules/auth/epics/importSeedEpic.ts b/src/app/modules/auth/epics/importSeedEpic.ts deleted file mode 100644 index df00c0c3d..000000000 --- a/src/app/modules/auth/epics/importSeedEpic.ts +++ /dev/null @@ -1,28 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Epic } from 'modules'; -import { importSeed } from '../actions'; -import { Observable } from 'rxjs/Observable'; -import { readTextFile } from 'lib/fs'; - -const importSeedEpic: Epic = (action$, store) => action$.ofAction(importSeed.started) - .switchMap(action => - Observable.from(readTextFile(action.payload)) - .map(payload => - importSeed.done({ - params: null, - result: payload - }) - - ).catch(e => - Observable.of(importSeed.failed({ - params: null, - error: null - })) - ) - ); - -export default importSeedEpic; \ No newline at end of file diff --git a/src/app/modules/auth/epics/loginEpic.ts b/src/app/modules/auth/epics/loginEpic.ts index d4a6a1597..582f003f5 100644 --- a/src/app/modules/auth/epics/loginEpic.ts +++ b/src/app/modules/auth/epics/loginEpic.ts @@ -5,7 +5,7 @@ import { Action } from 'redux'; import { Epic } from 'modules'; -import { login } from '../actions'; +import { login, acquireSession } from '../actions'; import { Observable } from 'rxjs/Observable'; import keyring from 'lib/keyring'; import { push } from 'connected-react-router'; @@ -39,20 +39,23 @@ const loginEpic: Epic = (action$, store, { api }) => action$.ofAction(login.star }) // Successful authentication. Yield the result - .flatMap(session => { + .flatMap(response => { + const sessionResult = { + sessionToken: response.token, + network: networkEndpoint + }; + return Observable.of( push('/'), login.done({ params: action.payload, result: { - session: { - sessionToken: session.token, - network: networkEndpoint - }, + session: sessionResult, privateKey, publicKey } - }) + }), + acquireSession.started(sessionResult) ); }) diff --git a/src/app/modules/auth/epics/logoutEpic.ts b/src/app/modules/auth/epics/logoutEpic.ts index 06ef7090c..a7b3bc4ef 100644 --- a/src/app/modules/auth/epics/logoutEpic.ts +++ b/src/app/modules/auth/epics/logoutEpic.ts @@ -7,13 +7,18 @@ import { Action } from 'redux'; import { Epic } from 'modules'; import { logout, deauthorize } from '../actions'; import { Observable } from 'rxjs/Observable'; +import { closeAllEditorTabs } from 'modules/editor/actions'; +import { isType } from 'typescript-fsa'; +import { discoverNetwork } from 'modules/engine/actions'; -const logoutEpic: Epic = (action$, store) => action$.ofAction(logout.started) +const logoutEpic: Epic = (action$, store) => action$ + .filter(action => isType(action, logout.started) || isType(action, discoverNetwork.done)) .flatMap(action => Observable.of( deauthorize(null), + closeAllEditorTabs(), logout.done({ - params: action.payload, + params: null, result: null }) ) diff --git a/src/app/modules/auth/reducer.ts b/src/app/modules/auth/reducer.ts index fba2761a8..2e3d60cee 100644 --- a/src/app/modules/auth/reducer.ts +++ b/src/app/modules/auth/reducer.ts @@ -17,23 +17,19 @@ import createWalletFailedHandler from './reducers/createWalletFailedHandler'; import importWalletHandler from './reducers/importWalletHandler'; import importWalletDoneHandler from './reducers/importWalletDoneHandler'; import importWalletFailedHandler from './reducers/importWalletFailedHandler'; -import importSeedDoneHandler from './reducers/importSeedDoneHandler'; import selectWalletHandler from './reducers/selectWalletHandler'; import authorizeHandler from './reducers/authorizeHandler'; import deauthorizeHandler from './reducers/deauthorizeHandler'; -import generateSeedDoneHandler from './reducers/generateSeedDoneHandler'; -import changeSeedHandler from './reducers/changeSeedHandler'; -import changeSeedConfirmationHandler from './reducers/changeSeedConfirmation'; -import importSeedConfirmationDoneHandler from './reducers/importSeedConfirmationDoneHandler'; import loadWalletsDoneHandler from './reducers/loadWalletsDoneHandler'; import loadWalletHandler from './reducers/loadWalletHandler'; import loginGuestHandler from './reducers/loginGuestHandler'; import loginGuestDoneHandler from './reducers/loginGuestDoneHandler'; import loginGuestFailedHandler from './reducers/loginGuestFailedHandler'; +import acquireSessionHandler from './reducers/acquireSessionHandler'; +import acquireSessionDoneHandler from './reducers/acquireSessionDoneHandler'; export type State = { - readonly seed: string; - readonly seedConfirm: string; + readonly isAcquired: boolean; readonly isAuthenticated: boolean; readonly isLoggingIn: boolean; readonly isCreatingWallet: boolean; @@ -50,8 +46,7 @@ export type State = { }; export const initialState: State = { - seed: '', - seedConfirm: '', + isAcquired: false, isAuthenticated: false, isLoggingIn: false, isCreatingWallet: false, @@ -81,13 +76,10 @@ export default reducerWithInitialState(initialState) .case(actions.importWallet.started, importWalletHandler) .case(actions.importWallet.done, importWalletDoneHandler) .case(actions.importWallet.failed, importWalletFailedHandler) - .case(actions.importSeed.done, importSeedDoneHandler) - .case(actions.importSeedConfirmation.done, importSeedConfirmationDoneHandler) .case(actions.selectWallet, selectWalletHandler) .case(actions.authorize, authorizeHandler) .case(actions.deauthorize, deauthorizeHandler) - .case(actions.generateSeed.done, generateSeedDoneHandler) - .case(actions.changeSeed, changeSeedHandler) - .case(actions.changeSeedConfirmation, changeSeedConfirmationHandler) .case(actions.loadWallets.done, loadWalletsDoneHandler) - .case(actions.loadWallet, loadWalletHandler); \ No newline at end of file + .case(actions.loadWallet, loadWalletHandler) + .case(actions.acquireSession.started, acquireSessionHandler) + .case(actions.acquireSession.done, acquireSessionDoneHandler); \ No newline at end of file diff --git a/src/app/modules/auth/reducers/importSeedConfirmationDoneHandler.ts b/src/app/modules/auth/reducers/acquireSessionDoneHandler.ts similarity index 60% rename from src/app/modules/auth/reducers/importSeedConfirmationDoneHandler.ts rename to src/app/modules/auth/reducers/acquireSessionDoneHandler.ts index 1a20431d8..d64abad55 100644 --- a/src/app/modules/auth/reducers/importSeedConfirmationDoneHandler.ts +++ b/src/app/modules/auth/reducers/acquireSessionDoneHandler.ts @@ -4,12 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { State } from '../reducer'; -import { importSeed } from '../actions'; +import { acquireSession } from '../actions'; import { Reducer } from 'modules'; -const importSeedConfirmationDoneHandler: Reducer = (state, payload) => ({ +const acquireSessionDoneHandler: Reducer = (state, payload): State => ({ ...state, - seedConfirm: payload.result + isAuthenticated: payload.result, + isAcquired: payload.result }); -export default importSeedConfirmationDoneHandler; \ No newline at end of file +export default acquireSessionDoneHandler; \ No newline at end of file diff --git a/src/app/modules/auth/reducers/importSeedDoneHandler.ts b/src/app/modules/auth/reducers/acquireSessionHandler.ts similarity index 66% rename from src/app/modules/auth/reducers/importSeedDoneHandler.ts rename to src/app/modules/auth/reducers/acquireSessionHandler.ts index ff7438712..0c7530fad 100644 --- a/src/app/modules/auth/reducers/importSeedDoneHandler.ts +++ b/src/app/modules/auth/reducers/acquireSessionHandler.ts @@ -4,12 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import { State } from '../reducer'; -import { importSeed } from '../actions'; +import { acquireSession } from '../actions'; import { Reducer } from 'modules'; -const importSeedDoneHandler: Reducer = (state, payload) => ({ +const acquireSessionHandler: Reducer = (state): State => ({ ...state, - seed: payload.result + isAcquired: false }); -export default importSeedDoneHandler; \ No newline at end of file +export default acquireSessionHandler; \ No newline at end of file diff --git a/src/app/modules/auth/reducers/generateSeedDoneHandler.ts b/src/app/modules/auth/reducers/generateSeedDoneHandler.ts deleted file mode 100644 index ce4e8a784..000000000 --- a/src/app/modules/auth/reducers/generateSeedDoneHandler.ts +++ /dev/null @@ -1,15 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { State } from '../reducer'; -import { generateSeed } from '../actions'; -import { Reducer } from 'modules'; - -const generateSeedDoneHandler: Reducer = (state, payload) => ({ - ...state, - seed: payload.result -}); - -export default generateSeedDoneHandler; \ No newline at end of file diff --git a/src/app/modules/content/actions.ts b/src/app/modules/content/actions.ts index 447eb965a..a263483e6 100644 --- a/src/app/modules/content/actions.ts +++ b/src/app/modules/content/actions.ts @@ -9,8 +9,8 @@ import { TProtypoElement, IButtonInteraction } from 'apla/protypo'; const actionCreator = actionCreatorFactory('content'); // Navigation -export const setResizing = actionCreator('SET_RESIZING'); -export const ecosystemInit = actionCreator.async<{ section?: string }, { stylesheet: string, printStylesheet: string }, string>('ECOSYSTEM_INIT'); +export const ecosystemInit = actionCreator<{ stylesheet: string, printStylesheet: string }>('ECOSYSTEM_INIT'); +export const setMenuActive = actionCreator('SET_MENU_ACTIVE'); // Interaction export const buttonInteraction = actionCreator('BUTTON_INTERACTION'); diff --git a/src/app/modules/content/epic.ts b/src/app/modules/content/epic.ts index f29e7c015..32707ba52 100644 --- a/src/app/modules/content/epic.ts +++ b/src/app/modules/content/epic.ts @@ -4,13 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { combineEpics } from 'redux-observable'; -import ecosystemInitEpic from './epics/ecosystemInitEpic'; import displayDataEpic from './epics/displayDataEpic'; import fetchNotificationsEpic from './epics/fetchNotificationsEpic'; import buttonInteractionEpic from './epics/buttonInteractionEpic'; export default combineEpics( - ecosystemInitEpic, displayDataEpic, fetchNotificationsEpic, buttonInteractionEpic diff --git a/src/app/modules/content/epics/buttonInteractionEpic.ts b/src/app/modules/content/epics/buttonInteractionEpic.ts index 96bbe1bc9..b73e07af9 100644 --- a/src/app/modules/content/epics/buttonInteractionEpic.ts +++ b/src/app/modules/content/epics/buttonInteractionEpic.ts @@ -3,15 +3,18 @@ * See LICENSE in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Action } from 'redux'; import { Epic } from 'modules'; import { Observable } from 'rxjs/Observable'; import { buttonInteraction } from 'modules/content/actions'; import { isType } from 'typescript-fsa'; import { txCall, txExec } from 'modules/tx/actions'; -import { modalShow, modalClose, modalPage } from 'modules/modal/actions'; -import { navigatePage } from 'modules/sections/actions'; +import { modalShow, modalClose } from 'modules/modal/actions'; +import { push } from 'connected-react-router'; +import { renderPage } from 'modules/sections/actions'; +import { createEditorTab, loadEditorTab } from 'modules/editor/actions'; -const buttonInteractionEpic: Epic = (action$, store, { api }) => action$.ofAction(buttonInteraction) +const buttonInteractionEpic: Epic = (action$, store, { routerService }) => action$.ofAction(buttonInteraction) // Show confirmation window if there is any .flatMap(rootAction => { return Observable.if( @@ -46,6 +49,7 @@ const buttonInteractionEpic: Epic = (action$, store, { api }) => action$.ofActio Observable.of(txCall({ uuid: action.payload.uuid, silent: action.payload.silent, + section: action.payload.from.section, contracts: action.payload.contracts, errorRedirects: action.payload.errorRedirects })), @@ -76,36 +80,45 @@ const buttonInteractionEpic: Epic = (action$, store, { api }) => action$.ofActio } }).flatMap(action => { - if (isType(action, buttonInteraction)) { - if (action.payload.page) { - const params = action.payload.page.params; - if ('txinfo' === action.payload.page.name) { - params.txhashes = (action.meta.txHashes || []).join(','); - } + if (isType(action, buttonInteraction) && action.payload.page) { + const params = action.payload.page.params; + if ('txinfo' === action.payload.page.name) { + params.txhashes = ((action.meta || {}).txHashes || []).join(','); + } - if (action.payload.popup) { - return Observable.of(modalPage({ - name: action.payload.page.name, - params, - title: action.payload.popup.title, - width: action.payload.popup.width - })); - } - else { - return Observable.of(navigatePage.started({ - name: action.payload.page.name, - params, - force: true - })); - } + if (action.payload.popup) { + return Observable.of(renderPage.started({ + location: null, + section: action.payload.page.section, + name: action.payload.page.name, + params: action.payload.page.params, + popup: action.payload.popup + })); } else { - return Observable.empty(); + const redirectUrl = routerService.generateRoute(`/browse/${action.payload.page.section}/${action.payload.page.name}`, action.payload.page.params); + return Observable.of( + push(redirectUrl, { from: action.payload.from }) + ); } } else { return Observable.of(action); } + + }).flatMap(action => { + if (isType(action, buttonInteraction)) { + return Observable.from(action.payload.actions).flatMap(buttonAction => { + switch (buttonAction.name) { + case 'CREATE': return Observable.of(createEditorTab.started(buttonAction.params.Type)); + case 'EDIT': return Observable.of(loadEditorTab.started({ type: buttonAction.params.Type, name: buttonAction.params.Name })); + default: return Observable.empty(); + } + }); + } + else { + return Observable.of(action); + } }); }); diff --git a/src/app/modules/content/epics/ecosystemInitEpic.ts b/src/app/modules/content/epics/ecosystemInitEpic.ts deleted file mode 100644 index 126e1315a..000000000 --- a/src/app/modules/content/epics/ecosystemInitEpic.ts +++ /dev/null @@ -1,62 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Action } from 'redux'; -import { Epic } from 'modules'; -import { Observable } from 'rxjs/Observable'; -import { ecosystemInit, fetchNotifications } from 'modules/content/actions'; -import { sectionsInit } from 'modules/sections/actions'; -import { logout, selectWallet } from 'modules/auth/actions'; - -const ecosystemInitEpic: Epic = (action$, store, { api }) => action$.ofAction(ecosystemInit.started) - .flatMap(action => { - const state = store.getState(); - const client = api({ - apiHost: state.auth.session.network.apiHost, - sessionToken: state.auth.session.sessionToken - }); - - return Observable.zip( - Observable.from(client.getParam({ name: 'stylesheet' })) - .map(l => l.value) - .catch(e => Observable.of('')), - Observable.from(client.getParam({ name: 'print_stylesheet' })) - .map(l => l.value) - .catch(e => Observable.of('')) - ).flatMap(([stylesheet, printStylesheet]) => - Observable.of( - fetchNotifications.started(null), - ecosystemInit.done({ - params: action.payload, - result: { - stylesheet, - printStylesheet - } - }), - sectionsInit.started(action.payload.section) - ) - ).catch(e => { - if ('E_OFFLINE' === e.error || 'E_SERVER' === e.error || 'E_TOKENEXPIRED' === e.error) { - const wallet = store.getState().auth.wallet; - - return Observable.of( - logout.started(null), - selectWallet(wallet), - ecosystemInit.failed({ - params: action.payload, - error: e.error - }) - ); - } - return Observable.of( - ecosystemInit.failed({ - params: action.payload, - error: e.error - }) - ); - }); - }); - -export default ecosystemInitEpic; \ No newline at end of file diff --git a/src/app/modules/content/reducer.ts b/src/app/modules/content/reducer.ts index 0ae8daa5b..cce7e0608 100644 --- a/src/app/modules/content/reducer.ts +++ b/src/app/modules/content/reducer.ts @@ -6,35 +6,27 @@ import * as actions from './actions'; import { reducerWithInitialState } from 'typescript-fsa-reducers'; import { TProtypoElement } from 'apla/protypo'; -import ecosystemInitDoneHandler from './reducers/ecosystemInitDoneHandler'; -import ecosystemInitFailedHandler from './reducers/ecosystemInitFailedHandler'; import fetchNotificationsDoneHandler from './reducers/fetchNotificationsDoneHandler'; -import setResizingHandler from './reducers/setResizingHandler'; import ecosystemInitHandler from './reducers/ecosystemInitHandler'; import reloadStylesheetHandler from './reducers/reloadStylesheetHandler'; +import setMenuActiveHandler from './reducers/setMenuActiveHandler'; export type State = { - readonly preloading: boolean; - readonly preloadingError: string; readonly stylesheet: string; readonly printStylesheet: string; - readonly navigationResizing: boolean; readonly notifications: TProtypoElement[]; + readonly menuActive: boolean; }; export const initialState: State = { - preloading: false, - preloadingError: null, stylesheet: null, printStylesheet: null, - navigationResizing: false, - notifications: null + notifications: null, + menuActive: false }; export default reducerWithInitialState(initialState) - .case(actions.ecosystemInit.done, ecosystemInitDoneHandler) - .case(actions.ecosystemInit.failed, ecosystemInitFailedHandler) - .case(actions.ecosystemInit.started, ecosystemInitHandler) + .case(actions.ecosystemInit, ecosystemInitHandler) .case(actions.fetchNotifications.done, fetchNotificationsDoneHandler) - .case(actions.setResizing, setResizingHandler) - .case(actions.reloadStylesheet, reloadStylesheetHandler); \ No newline at end of file + .case(actions.reloadStylesheet, reloadStylesheetHandler) + .case(actions.setMenuActive, setMenuActiveHandler); \ No newline at end of file diff --git a/src/app/modules/content/reducers/ecosystemInitDoneHandler.ts b/src/app/modules/content/reducers/ecosystemInitDoneHandler.ts deleted file mode 100644 index bbf20ef86..000000000 --- a/src/app/modules/content/reducers/ecosystemInitDoneHandler.ts +++ /dev/null @@ -1,16 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { State } from '../reducer'; -import { ecosystemInit } from '../actions'; -import { Reducer } from 'modules'; - -const ecosystemInitDoneHandler: Reducer = (state, payload) => ({ - ...state, - stylesheet: payload.result.stylesheet, - printStylesheet: payload.result.printStylesheet -}); - -export default ecosystemInitDoneHandler; \ No newline at end of file diff --git a/src/app/modules/content/reducers/ecosystemInitFailedHandler.ts b/src/app/modules/content/reducers/ecosystemInitFailedHandler.ts deleted file mode 100644 index 3a53558a4..000000000 --- a/src/app/modules/content/reducers/ecosystemInitFailedHandler.ts +++ /dev/null @@ -1,16 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { State } from '../reducer'; -import { ecosystemInit } from '../actions'; -import { Reducer } from 'modules'; - -const ecosystemInitFailedHandler: Reducer = (state, payload) => ({ - ...state, - preloading: false, - preloadingError: payload.error -}); - -export default ecosystemInitFailedHandler; \ No newline at end of file diff --git a/src/app/modules/content/reducers/ecosystemInitHandler.ts b/src/app/modules/content/reducers/ecosystemInitHandler.ts index e0fe2f68c..4fdff459b 100644 --- a/src/app/modules/content/reducers/ecosystemInitHandler.ts +++ b/src/app/modules/content/reducers/ecosystemInitHandler.ts @@ -7,12 +7,10 @@ import { State } from '../reducer'; import { ecosystemInit } from '../actions'; import { Reducer } from 'modules'; -const ecosystemInitHandler: Reducer = (state, payload) => ({ +const ecosystemInitDoneHandler: Reducer = (state, payload) => ({ ...state, - preloading: true, - preloadingError: null, - stylesheet: '', + stylesheet: payload.stylesheet, printStylesheet: '' }); -export default ecosystemInitHandler; \ No newline at end of file +export default ecosystemInitDoneHandler; \ No newline at end of file diff --git a/src/app/modules/auth/reducers/changeSeedHandler.ts b/src/app/modules/content/reducers/setMenuActiveHandler.ts similarity index 67% rename from src/app/modules/auth/reducers/changeSeedHandler.ts rename to src/app/modules/content/reducers/setMenuActiveHandler.ts index d4aad59a2..24e3136dc 100644 --- a/src/app/modules/auth/reducers/changeSeedHandler.ts +++ b/src/app/modules/content/reducers/setMenuActiveHandler.ts @@ -4,12 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import { State } from '../reducer'; -import { changeSeed } from '../actions'; +import { setMenuActive } from '../actions'; import { Reducer } from 'modules'; -const changeSeedHandler: Reducer = (state, payload) => ({ +const setMenuActiveHandler: Reducer = (state, payload) => ({ ...state, - seed: payload + menuActive: payload }); -export default changeSeedHandler; \ No newline at end of file +export default setMenuActiveHandler; \ No newline at end of file diff --git a/src/app/modules/dependencies.ts b/src/app/modules/dependencies.ts index 68c283104..c1e9cc4b6 100644 --- a/src/app/modules/dependencies.ts +++ b/src/app/modules/dependencies.ts @@ -8,6 +8,7 @@ import CodeGenerator, { setIds, convertToTreeData, findTagById, copyObject, idGe import Properties from 'lib/constructor/properties'; import getConstructorTemplate from 'lib/constructor/templates'; import resolveTagHandler from 'lib/constructor/tags'; +import * as routerService from 'services/router'; import 'whatwg-fetch'; export interface IStoreDependencies { @@ -15,6 +16,7 @@ export interface IStoreDependencies { defaultKey: string; defaultPassword: string; constructorModule: IConstructorDependenies; + routerService: typeof routerService; } export interface IAPIDependency { @@ -54,7 +56,8 @@ const storeDependencies: IStoreDependencies = { resolveTagHandler, CodeGenerator, Properties - } + }, + routerService }; export default storeDependencies; \ No newline at end of file diff --git a/src/app/modules/editor/actions.ts b/src/app/modules/editor/actions.ts index 855f81b41..32d263965 100644 --- a/src/app/modules/editor/actions.ts +++ b/src/app/modules/editor/actions.ts @@ -7,7 +7,7 @@ import actionCreatorFactory from 'typescript-fsa'; import { TProtypoElement } from 'apla/protypo'; import { - IEditorTabCreateCall, ILoadEditorTabCall, ICreateEditorTabCall, IReloadEditorTabCall, TEditorTab, IChangePageCall, IChangePageResult, ISaveConstructorHistoryResult, + IEditorTabCreateCall, ILoadEditorTabCall, IReloadEditorTabCall, TEditorTab, IChangePageCall, IChangePageResult, ISaveConstructorHistoryResult, IConstructorUndoRedoResult, ISetTagCanDropPositionCall, ISetTagCanDropPositionResult, IAddTagCall, IOperateTagCall, IOperateTagResult, IMoveTreeTag, ISelectTagResult, IGetPageTreeResult } from 'apla/editor'; @@ -15,14 +15,16 @@ import { const actionCreator = actionCreatorFactory('editor'); export const editorSave = actionCreator('EDITOR_SAVE'); -export const createEditorTab = actionCreator.async('CREATE_EDITOR_TAB'); +export const createEditorTab = actionCreator.async('CREATE_EDITOR_TAB'); export const loadEditorTab = actionCreator.async('LOAD_EDITOR_TAB'); -export const changeEditorTab = actionCreator('CHANGE_EDITOR_TAB'); -export const closeEditorTab = actionCreator('CLOSE_EDITOR_TAB'); -export const closeAllEditorTab = actionCreator('CLOSE_ALL_EDITOR_TAB'); +export const changeEditorTab = actionCreator('CHANGE_EDITOR_TAB'); +export const closeEditorTab = actionCreator('CLOSE_EDITOR_TAB'); +export const closeAllEditorTabs = actionCreator('CLOSE_ALL_EDITOR_TABS'); export const closeSavedEditorTab = actionCreator('CLOSE_SAVED_EDITOR_TAB'); +export const destroyEditorTab = actionCreator('DESTROY_EDITOR_TAB'); export const updateEditorTab = actionCreator('UPDATE_EDITOR_TAB'); -export const revertEditorTab = actionCreator('REVERT_EDITOR_TAB'); +export const revertEditorTab = actionCreator('REVERT_EDITOR_TAB'); +export const resetEditorTab = actionCreator('RESET_EDITOR_TAB'); export const reloadEditorTab = actionCreator('RELOAD_EDITOR_TAB'); export const changeEditorTool = actionCreator.async('CHANGE_EDITOR_TOOL'); export const setPageTemplate = actionCreator('SET_PAGE_TEMPLATE'); @@ -39,4 +41,4 @@ export const saveConstructorHistory = actionCreator.async('CONSTRUCTOR_UNDO'); export const constructorRedo = actionCreator.async('CONSTRUCTOR_REDO'); export const generatePageTemplate = actionCreator('GENERATE_PAGE_TEMPLATE'); -export const debugContract = actionCreator('DEBUG_CONTRACT'); +export const debugContract = actionCreator('DEBUG_CONTRACT'); \ No newline at end of file diff --git a/src/app/modules/editor/epic.ts b/src/app/modules/editor/epic.ts index 68515bee6..ca8ae84e2 100644 --- a/src/app/modules/editor/epic.ts +++ b/src/app/modules/editor/epic.ts @@ -29,6 +29,8 @@ import constructorUndoEpic from './epics/constructorUndoEpic'; import constructorRedoEpic from './epics/constructorRedoEpic'; import setTagCanDropPositionEpic from './epics/setTagCanDropPositionEpic'; import debugContractEpic from './epics/debugContractEpic'; +import revertEditorTabEpic from './epics/revertEditorTabEpic'; +import openEditorEpic from './epics/openEditorEpic'; export default combineEpics( changeEditorToolEpic, @@ -55,5 +57,7 @@ export default combineEpics( constructorUndoEpic, constructorRedoEpic, setTagCanDropPositionEpic, - debugContractEpic + debugContractEpic, + revertEditorTabEpic, + openEditorEpic ); \ No newline at end of file diff --git a/src/app/modules/editor/epics/closeEditorTabEpic.ts b/src/app/modules/editor/epics/closeEditorTabEpic.ts index 1ebd27754..f0bc396c4 100644 --- a/src/app/modules/editor/epics/closeEditorTabEpic.ts +++ b/src/app/modules/editor/epics/closeEditorTabEpic.ts @@ -6,19 +6,30 @@ import { Action } from 'redux'; import { Epic } from 'redux-observable'; import { IRootState } from 'modules'; -import { closeEditorTab } from '../actions'; -import { updateSection } from 'modules/sections/actions'; +import { closeEditorTab, destroyEditorTab } from '../actions'; +import { Observable } from 'rxjs'; +import { modalShow } from 'modules/modal/actions'; -const closeEditorTabEpic: Epic = - (action$, store) => action$.ofAction(closeEditorTab) - .map(action => { - const state = store.getState(); - const section = state.sections.sections.editor; +const closeEditorTabEpic: Epic = (action$, store) => action$.ofAction(closeEditorTab) + .flatMap(action => { + const state = store.getState(); + const tab = state.editor.tabs.find(t => t.uuid === action.payload); - return updateSection({ - ...section, - visible: 0 < state.editor.tabs.length - }); - }); + if (!tab) { + return Observable.empty(); + } + + if (tab.dirty) { + return Observable.of(modalShow({ + id: 'EDITOR_CLOSE', + type: 'EDITOR_CLOSE_UNSAVED', + params: { + uuid: tab.uuid + } + })); + } + + return Observable.of(destroyEditorTab(tab.uuid)); + }); export default closeEditorTabEpic; \ No newline at end of file diff --git a/src/app/modules/editor/epics/createEditorTabEpic.ts b/src/app/modules/editor/epics/createEditorTabEpic.ts index bbc18770a..51150368f 100644 --- a/src/app/modules/editor/epics/createEditorTabEpic.ts +++ b/src/app/modules/editor/epics/createEditorTabEpic.ts @@ -3,71 +3,51 @@ * See LICENSE in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Action } from 'redux'; -import { Epic } from 'redux-observable'; -import { IRootState } from 'modules'; +import uuid from 'uuid'; +import { Epic } from 'modules'; import { createEditorTab } from '../actions'; -import { updateSection } from 'modules/sections/actions'; -import { Observable } from 'rxjs/Observable'; -import { replace } from 'connected-react-router'; -const createEditorTabEpic: Epic = - (action$, store) => action$.ofAction(createEditorTab.started) - .delay(0) - .map(action => { - const state = store.getState(); +const createEditorTabEpic: Epic = (action$, store) => action$.ofAction(createEditorTab.started) + .map(action => { + const state = store.getState(); - const ids = state.editor.tabs - .filter(l => l.new) - .map(l => l.id) - .sort(); + const ids = state.editor.tabs + .filter(l => l.new) + .map(l => l.id) + .sort(); - const id = (ids.length ? parseInt(ids[ids.length - 1], 10) + 1 : 1).toString(); + const id = (ids.length ? parseInt(ids[ids.length - 1], 10) + 1 : 1).toString(); - switch (action.payload.type) { - case 'contract': - return createEditorTab.done({ - params: action.payload, - result: { - id, - name: null, - value: 'contract ... {\n data {\n\n }\n conditions {\n\n }\n action {\n\n }\n}' - } - }); - - case 'page': - case 'block': - case 'menu': - return createEditorTab.done({ - params: action.payload, - result: { - id, - name, - value: '' - } - }); - - default: return createEditorTab.failed({ + switch (action.payload) { + case 'contract': + return createEditorTab.done({ params: action.payload, - error: null + result: { + uuid: uuid.v4(), + id, + name: null, + value: 'contract ... {\n data {\n\n }\n conditions {\n\n }\n action {\n\n }\n}' + } }); - } - }) - .flatMap(action => { - const editor = store.getState().sections.sections.editor; - return Observable.of( - replace('/editor'), - updateSection({ - ...editor, - visible: true, - page: { - ...editor.page, - params: {} + case 'page': + case 'block': + case 'menu': + return createEditorTab.done({ + params: action.payload, + result: { + uuid: uuid.v4(), + id, + name: null, + value: '' } - }), - action - ); - }); + }); + + default: return createEditorTab.failed({ + params: action.payload, + error: null + }); + } + }); export default createEditorTabEpic; \ No newline at end of file diff --git a/src/app/modules/editor/epics/loadEditorTabEpic.ts b/src/app/modules/editor/epics/loadEditorTabEpic.ts index 65818c9a9..327d49114 100644 --- a/src/app/modules/editor/epics/loadEditorTabEpic.ts +++ b/src/app/modules/editor/epics/loadEditorTabEpic.ts @@ -3,12 +3,10 @@ * See LICENSE in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Action } from 'redux'; +import uuid from 'uuid'; import { Epic } from 'modules'; import { loadEditorTab } from '../actions'; import { Observable } from 'rxjs/Observable'; -import { updateSection } from 'modules/sections/actions'; -import { replace } from 'connected-react-router'; const loadEditorTabEpic: Epic = (action$, store, { api }) => action$.ofAction(loadEditorTab.started) .flatMap(action => { @@ -19,122 +17,111 @@ const loadEditorTabEpic: Epic = (action$, store, { api }) => action$.ofAction(lo }); const nameParser = /^(@[0-9]+)?(.*)$/i; - switch (action.payload.type) { - case 'contract': - return Observable.fromPromise(client.getContract({ - name: action.payload.name - - }).then(contract => - client.getRow({ - table: 'contracts', - id: contract.tableid.toString() - - }).then(row => ({ - id: contract.tableid.toString(), - name: nameParser.exec(contract.name)[2], - contract: row.value - })) - - )).map(data => - loadEditorTab.done({ - params: action.payload, - result: { - type: 'contract', - id: data.id, - new: false, - name: data.contract.name, - tool: 'editor', - value: data.contract.value, - initialValue: data.contract.value, - dirty: false - } - }) - ); - - case 'page': - return Observable.from(client.getPage({ - name: action.payload.name - - })).map(data => - loadEditorTab.done({ - params: action.payload, - result: { - type: 'page', - id: data.id.toString(), - new: false, - name: data.name, - tool: 'editor', - value: data.value, - initialValue: data.value, - dirty: false - } - }) - ); - - case 'menu': - return Observable.from(client.getMenu({ - name: action.payload.name - - })).map(data => - loadEditorTab.done({ - params: action.payload, - result: { - type: 'menu', - id: data.id.toString(), - new: false, - name: data.name, - tool: 'editor', - value: data.value, - initialValue: data.value, - dirty: false - } - }) - ); - - case 'block': - return Observable.from(client.getBlock({ - name: action.payload.name - - })).map(data => - loadEditorTab.done({ - params: action.payload, - result: { - type: 'block', - id: data.id.toString(), - new: false, - name: data.name, - tool: 'editor', - value: data.value, - initialValue: data.value, - dirty: false - } - }) - ); - - default: - throw { error: 'E_FAILED' }; - } - - }).flatMap(result => { - const editor = store.getState().sections.sections.editor; - return Observable.of( - replace('/editor'), - result, - updateSection({ - ...editor, - visible: true, - page: { - ...editor.page, - params: {} - } - }) - ); - - }).catch(error => - Observable.of(loadEditorTab.failed({ - params: null, + return Observable.of(action.payload.type).flatMap(type => { + switch (type) { + case 'contract': + return Observable.fromPromise(client.getContract({ + name: action.payload.name + + }).then(contract => + client.getRow({ + table: 'contracts', + id: contract.tableid.toString() + + }).then(row => ({ + id: contract.tableid.toString(), + name: nameParser.exec(contract.name)[2], + contract: row.value + })) + + )).map(data => + loadEditorTab.done({ + params: action.payload, + result: { + uuid: uuid.v4(), + type: 'contract', + id: data.id, + new: false, + name: data.contract.name, + tool: 'editor', + value: data.contract.value, + initialValue: data.contract.value, + dirty: false + } + }) + ); + + case 'page': + return Observable.from(client.getPage({ + name: action.payload.name + + })).map(data => + loadEditorTab.done({ + params: action.payload, + result: { + uuid: uuid.v4(), + type: 'page', + id: data.id.toString(), + new: false, + name: data.name, + tool: 'editor', + value: data.value, + initialValue: data.value, + dirty: false + } + }) + ); + + case 'menu': + return Observable.from(client.getMenu({ + name: action.payload.name + + })).map(data => + loadEditorTab.done({ + params: action.payload, + result: { + uuid: uuid.v4(), + type: 'menu', + id: data.id.toString(), + new: false, + name: data.name, + tool: 'editor', + value: data.value, + initialValue: data.value, + dirty: false + } + }) + ); + + case 'block': + return Observable.from(client.getBlock({ + name: action.payload.name + + })).map(data => + loadEditorTab.done({ + params: action.payload, + result: { + uuid: uuid.v4(), + type: 'block', + id: data.id.toString(), + new: false, + name: data.name, + tool: 'editor', + value: data.value, + initialValue: data.value, + dirty: false + } + }) + ); + + default: + throw { error: 'E_FAILED' }; + } + + }).catch(error => Observable.of(loadEditorTab.failed({ + params: action.payload, error - })) - ); + }))); + }); export default loadEditorTabEpic; \ No newline at end of file diff --git a/src/app/modules/editor/epics/newBlockEpic.ts b/src/app/modules/editor/epics/newBlockEpic.ts index a0c21ccfc..78cf6c326 100644 --- a/src/app/modules/editor/epics/newBlockEpic.ts +++ b/src/app/modules/editor/epics/newBlockEpic.ts @@ -20,12 +20,17 @@ const newBlockEpic: Epic = (action$, store, { api }) => action$.ofAction(editorS sessionToken: state.auth.session.sessionToken }); - return ModalObservable<{ name: string, conditions: string }>(action$, { + return Observable.from(client.getData({ + name: 'applications', + columns: ['id', 'deleted', 'name'] + + })).flatMap(apps => ModalObservable<{ name: string, app: string, conditions: string }>(action$, { modal: { id, type: 'CREATE_INTERFACE', params: { - type: 'block' + type: 'block', + apps: apps.list.filter(l => '0' === l.deleted) } }, success: result => TxObservable(action$, { @@ -37,7 +42,7 @@ const newBlockEpic: Epic = (action$, store, { api }) => action$.ofAction(editorS Name: result.name, Value: action.payload.value, Conditions: result.conditions, - ApplicationId: action.payload.appId || 0 + ApplicationId: result.app || 0 }] }] }, @@ -55,7 +60,7 @@ const newBlockEpic: Epic = (action$, store, { api }) => action$.ofAction(editorS } })) }) - }); + })); }); export default newBlockEpic; \ No newline at end of file diff --git a/src/app/modules/editor/epics/newContractEpic.ts b/src/app/modules/editor/epics/newContractEpic.ts index 753e7e561..9bf2f31d2 100644 --- a/src/app/modules/editor/epics/newContractEpic.ts +++ b/src/app/modules/editor/epics/newContractEpic.ts @@ -8,6 +8,7 @@ import { Observable } from 'rxjs'; import { Epic } from 'modules'; import { editorSave, reloadEditorTab } from '../actions'; import TxObservable from 'modules/tx/util/TxObservable'; +import ModalObservable from 'modules/modal/util/ModalObservable'; const newContractEpic: Epic = (action$, store, { api }) => action$.ofAction(editorSave) .filter(l => l.payload.new && 'contract' === l.payload.type) @@ -19,33 +20,46 @@ const newContractEpic: Epic = (action$, store, { api }) => action$.ofAction(edit sessionToken: state.auth.session.sessionToken }); - return TxObservable(action$, { - tx: { - uuid: id, - contracts: [{ - name: '@1NewContract', - params: [{ - Value: action.payload.value, - Conditions: 'true', - ApplicationId: action.payload.appId ? action.payload.appId : 0 - }] - }] - }, - success: results => Observable.from(results).flatMap(result => Observable.fromPromise(client.getRow({ - table: 'contracts', - id: result.status.result + return Observable.from(client.getData({ + name: 'applications', + columns: ['id', 'deleted', 'name'] - })).map(response => reloadEditorTab({ - type: action.payload.type, - id: action.payload.id, - data: { - new: false, - id: String(result.status.result), - name: response.value.name, - initialValue: action.payload.value + })).flatMap(apps => ModalObservable<{ app: string, conditions: string }>(action$, { + modal: { + id, + type: 'CREATE_CONTRACT', + params: { + apps: apps.list.filter(l => '0' === l.deleted) } - }))) - }); + }, + success: result => TxObservable(action$, { + tx: { + uuid: id, + contracts: [{ + name: '@1NewContract', + params: [{ + Value: action.payload.value, + Conditions: result.conditions, + ApplicationId: result.app || 0 + }] + }] + }, + success: results => Observable.from(results).flatMap(tx => Observable.fromPromise(client.getRow({ + table: 'contracts', + id: tx.status.result + + })).map(response => reloadEditorTab({ + type: action.payload.type, + id: action.payload.id, + data: { + new: false, + id: String(tx.status.result), + name: response.value.name, + initialValue: action.payload.value + } + }))) + }) + })); }); export default newContractEpic; \ No newline at end of file diff --git a/src/app/modules/editor/epics/newMenuEpic.ts b/src/app/modules/editor/epics/newMenuEpic.ts index 7c90cd907..6d2a001be 100644 --- a/src/app/modules/editor/epics/newMenuEpic.ts +++ b/src/app/modules/editor/epics/newMenuEpic.ts @@ -20,12 +20,17 @@ const newMenuEpic: Epic = (action$, store, { api }) => action$.ofAction(editorSa sessionToken: state.auth.session.sessionToken }); - return ModalObservable<{ name: string, conditions: string }>(action$, { + return Observable.from(client.getData({ + name: 'applications', + columns: ['id', 'deleted', 'name'] + + })).flatMap(apps => ModalObservable<{ name: string, app: string, conditions: string }>(action$, { modal: { id, type: 'CREATE_INTERFACE', params: { - type: 'menu' + type: 'menu', + apps: apps.list.filter(l => '0' === l.deleted) } }, success: result => TxObservable(action$, { @@ -37,7 +42,7 @@ const newMenuEpic: Epic = (action$, store, { api }) => action$.ofAction(editorSa Name: result.name, Value: action.payload.value, Conditions: result.conditions, - ApplicationId: action.payload.appId ? action.payload.appId : 0 + ApplicationId: result.app || 0 }] }] }, @@ -55,7 +60,7 @@ const newMenuEpic: Epic = (action$, store, { api }) => action$.ofAction(editorSa } })) }) - }); + })); }); export default newMenuEpic; \ No newline at end of file diff --git a/src/app/modules/editor/epics/newPageEpic.ts b/src/app/modules/editor/epics/newPageEpic.ts index e765e25fe..0e1aa748c 100644 --- a/src/app/modules/editor/epics/newPageEpic.ts +++ b/src/app/modules/editor/epics/newPageEpic.ts @@ -20,16 +20,22 @@ const newPageEpic: Epic = (action$, store, { api }) => action$.ofAction(editorSa }); const id = uuid.v4(); - return Observable.fromPromise(client.getData({ - name: 'menu', - columns: ['name'] - - })).flatMap(menus => ModalObservable<{ name: string, menu: string, conditions: string }>(action$, { + return Observable.zip( + Observable.from(client.getData({ + name: 'menu', + columns: ['name'] + })), + Observable.from(client.getData({ + name: 'applications', + columns: ['id', 'deleted', 'name'] + })) + ).flatMap(([menus, apps]) => ModalObservable<{ name: string, app: string, menu: string, conditions: string }>(action$, { modal: { id, type: 'CREATE_PAGE', params: { - menus: menus.list.map(l => l.name) + menus: menus.list.map(l => l.name), + apps: apps.list.filter(l => '0' === l.deleted) } }, success: result => TxObservable(action$, { @@ -42,7 +48,7 @@ const newPageEpic: Epic = (action$, store, { api }) => action$.ofAction(editorSa Value: action.payload.value, Menu: result.menu, Conditions: result.conditions, - ApplicationId: action.payload.appId || 0 + ApplicationId: result.app || 0 }] }] }, diff --git a/src/app/modules/gui/epics/switchWindowOnLoginEpic.ts b/src/app/modules/editor/epics/openEditorEpic.ts similarity index 51% rename from src/app/modules/gui/epics/switchWindowOnLoginEpic.ts rename to src/app/modules/editor/epics/openEditorEpic.ts index 91bedfd4a..47808094f 100644 --- a/src/app/modules/gui/epics/switchWindowOnLoginEpic.ts +++ b/src/app/modules/editor/epics/openEditorEpic.ts @@ -4,14 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import { Epic } from 'modules'; -import { switchWindow } from '../actions'; -import { login, loginGuest } from 'modules/auth/actions'; +import { createEditorTab, loadEditorTab } from '../actions'; import { isType } from 'typescript-fsa'; +import { push } from 'connected-react-router'; -const switchWindowOnLoginEpic: Epic = - (action$, store) => action$.filter(action => isType(action, login.done) || isType(action, loginGuest.done)) - .map(action => - switchWindow.started('main') - ); +const openEditorEpic: Epic = (action$, store) => action$ + .filter(action => isType(action, createEditorTab.done) || isType(action, loadEditorTab.done)) + .map(() => push('/editor')); -export default switchWindowOnLoginEpic; \ No newline at end of file +export default openEditorEpic; \ No newline at end of file diff --git a/src/app/modules/editor/epics/revertEditorTabEpic.ts b/src/app/modules/editor/epics/revertEditorTabEpic.ts new file mode 100644 index 000000000..b422b8ce0 --- /dev/null +++ b/src/app/modules/editor/epics/revertEditorTabEpic.ts @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Action } from 'redux'; +import { Epic } from 'redux-observable'; +import { IRootState } from 'modules'; +import { resetEditorTab, revertEditorTab } from '../actions'; +import { Observable } from 'rxjs'; +import { modalShow } from 'modules/modal/actions'; + +const revertEditorTabEpic: Epic = (action$, store) => action$.ofAction(revertEditorTab) + .flatMap(action => { + const state = store.getState(); + const tab = state.editor.tabs.find(t => t.uuid === action.payload); + + if (!tab) { + return Observable.empty(); + } + + if (tab.dirty) { + return Observable.of(modalShow({ + id: 'EDITOR_REVERT', + type: 'EDITOR_REVERT_UNSAVED', + params: { + uuid: tab.uuid + } + })); + } + + return Observable.of(resetEditorTab(tab.uuid)); + }); + +export default revertEditorTabEpic; \ No newline at end of file diff --git a/src/app/modules/editor/reducer.ts b/src/app/modules/editor/reducer.ts index fdebadebc..72e1ac3f7 100644 --- a/src/app/modules/editor/reducer.ts +++ b/src/app/modules/editor/reducer.ts @@ -9,12 +9,11 @@ import { reducerWithInitialState } from 'typescript-fsa-reducers/dist'; import changeEditorToolDoneHandler from './reducers/changeEditorToolDoneHandler'; import changeEditorTabHandler from './reducers/changeEditorTabHandler'; import changeEditorToolStartedHandler from './reducers/changeEditorToolStartedHandler'; -import closeEditorTabHandler from './reducers/closeEditorTabHandler'; import closeAllEditorTabHandler from './reducers/closeAllEditorTabHandler'; import closeSavedEditorTabHandler from './reducers/closeSavedEditorTabHandler'; import createEditorTabDoneHandler from './reducers/createEditorTabDoneHandler'; import reloadEditorTabHandler from './reducers/reloadEditorTabHandler'; -import revertEditorTabHandler from './reducers/revertEditorTabHandler'; +import resetEditorTabHandler from './reducers/resetEditorTabHandler'; import updateEditorTabHandler from './reducers/updateEditorTabHandler'; import loadEditorTabDoneHandler from './reducers/loadEditorTabDoneHandler'; import getPageTreeDoneHandler from './reducers/getPageTreeDoneHandler'; @@ -30,6 +29,7 @@ import setTagCanDropPositionDoneHandler from './reducers/setTagCanDropPositionDo import constructorUndoDoneHandler from './reducers/constructorUndoDoneHandler'; import constructorRedoDoneHandler from './reducers/constructorRedoDoneHandler'; import setPageTemplateHandler from './reducers/setPageTemplateHandler'; +import destroyEditorTabHandler from './reducers/destroyEditorTabHandler'; export type State = { readonly pending: boolean; @@ -47,13 +47,13 @@ export default reducerWithInitialState(initialState) .case(actions.changeEditorTab, changeEditorTabHandler) .case(actions.changeEditorTool.done, changeEditorToolDoneHandler) .case(actions.changeEditorTool.started, changeEditorToolStartedHandler) - .case(actions.closeEditorTab, closeEditorTabHandler) - .case(actions.closeAllEditorTab, closeAllEditorTabHandler) + .case(actions.destroyEditorTab, destroyEditorTabHandler) + .case(actions.closeAllEditorTabs, closeAllEditorTabHandler) .case(actions.closeSavedEditorTab, closeSavedEditorTabHandler) .case(actions.createEditorTab.done, createEditorTabDoneHandler) .case(actions.loadEditorTab.done, loadEditorTabDoneHandler) .case(actions.reloadEditorTab, reloadEditorTabHandler) - .case(actions.revertEditorTab, revertEditorTabHandler) + .case(actions.resetEditorTab, resetEditorTabHandler) .case(actions.updateEditorTab, updateEditorTabHandler) .case(actions.getPageTree.done, getPageTreeDoneHandler) .case(actions.getPageTree.failed, getPageTreeFailedHandler) diff --git a/src/app/modules/editor/reducers/changeEditorTabHandler.ts b/src/app/modules/editor/reducers/changeEditorTabHandler.ts index b0129153b..7fa583710 100644 --- a/src/app/modules/editor/reducers/changeEditorTabHandler.ts +++ b/src/app/modules/editor/reducers/changeEditorTabHandler.ts @@ -7,9 +7,17 @@ import { State } from '../reducer'; import { changeEditorTab } from '../actions'; import { Reducer } from 'modules'; -const changeEditorTabHandler: Reducer = (state, payload) => ({ - ...state, - tabIndex: payload -}); +const changeEditorTabHandler: Reducer = (state, payload) => { + const tabIndex = state.tabs.findIndex(t => t.uuid === payload); + + if (-1 === tabIndex) { + return state; + } + + return { + ...state, + tabIndex + }; +}; export default changeEditorTabHandler; \ No newline at end of file diff --git a/src/app/modules/editor/reducers/closeEditorTabHandler.ts b/src/app/modules/editor/reducers/closeEditorTabHandler.ts deleted file mode 100644 index 61d0dfa88..000000000 --- a/src/app/modules/editor/reducers/closeEditorTabHandler.ts +++ /dev/null @@ -1,19 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { State } from '../reducer'; -import { closeEditorTab } from '../actions'; -import { Reducer } from 'modules'; - -const closeEditorTabHandler: Reducer = (state, payload) => ({ - ...state, - tabIndex: state.tabIndex >= state.tabs.length - 1 ? state.tabs.length - 2 : state.tabIndex, - tabs: [ - ...state.tabs.slice(0, payload), - ...state.tabs.slice(payload + 1), - ] -}); - -export default closeEditorTabHandler; \ No newline at end of file diff --git a/src/app/modules/editor/reducers/createEditorTabDoneHandler.ts b/src/app/modules/editor/reducers/createEditorTabDoneHandler.ts index c97bc83c3..ce18e8e1c 100644 --- a/src/app/modules/editor/reducers/createEditorTabDoneHandler.ts +++ b/src/app/modules/editor/reducers/createEditorTabDoneHandler.ts @@ -12,15 +12,15 @@ const createEditorTabDoneHandler: Reducer = tabs: [ ...state.tabs, { - type: payload.params.type, + uuid: payload.result.uuid, + type: payload.params, id: payload.result.id, new: true, name: payload.result.name, tool: 'editor', value: payload.result.value, initialValue: payload.result.value, - dirty: false, - appId: payload.params.appId + dirty: false } ], tabIndex: state.tabs.length diff --git a/src/app/modules/editor/reducers/destroyEditorTabHandler.ts b/src/app/modules/editor/reducers/destroyEditorTabHandler.ts new file mode 100644 index 000000000..3d5796d33 --- /dev/null +++ b/src/app/modules/editor/reducers/destroyEditorTabHandler.ts @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { State } from '../reducer'; +import { destroyEditorTab } from '../actions'; +import { Reducer } from 'modules'; + +const destroyEditorTabHandler: Reducer = (state, payload) => { + const tabIndex = state.tabs.findIndex(t => t.uuid === payload); + + if (-1 === tabIndex) { + return state; + } + + return { + ...state, + tabIndex: state.tabIndex >= state.tabs.length - 1 ? state.tabs.length - 2 : state.tabIndex, + tabs: [ + ...state.tabs.slice(0, tabIndex), + ...state.tabs.slice(tabIndex + 1), + ] + }; +}; + +export default destroyEditorTabHandler; \ No newline at end of file diff --git a/src/app/modules/editor/reducers/resetEditorTabHandler.ts b/src/app/modules/editor/reducers/resetEditorTabHandler.ts new file mode 100644 index 000000000..d0a6d6b8c --- /dev/null +++ b/src/app/modules/editor/reducers/resetEditorTabHandler.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { State } from '../reducer'; +import { resetEditorTab } from '../actions'; +import { Reducer } from 'modules'; + +const resetEditorTabHandler: Reducer = (state, payload) => { + const tabIndex = state.tabs.findIndex(t => t.uuid === payload); + + if (-1 === tabIndex) { + return state; + } + + return { + ...state, + tabs: [ + ...state.tabs.slice(0, tabIndex), + { + ...state.tabs[tabIndex], + value: state.tabs[tabIndex].initialValue, + dirty: false + }, + ...state.tabs.slice(tabIndex + 1), + ] + }; +}; + +export default resetEditorTabHandler; \ No newline at end of file diff --git a/src/app/modules/editor/reducers/revertEditorTabHandler.ts b/src/app/modules/editor/reducers/revertEditorTabHandler.ts deleted file mode 100644 index da1672395..000000000 --- a/src/app/modules/editor/reducers/revertEditorTabHandler.ts +++ /dev/null @@ -1,23 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { State } from '../reducer'; -import { revertEditorTab } from '../actions'; -import { Reducer } from 'modules'; - -const revertEditorTabHandler: Reducer = (state, payload) => ({ - ...state, - tabs: [ - ...state.tabs.slice(0, payload), - { - ...state.tabs[payload], - value: state.tabs[payload].initialValue, - dirty: false - }, - ...state.tabs.slice(payload + 1), - ] -}); - -export default revertEditorTabHandler; \ No newline at end of file diff --git a/src/app/modules/engine/actions.ts b/src/app/modules/engine/actions.ts index 281d8638f..cd18a342a 100644 --- a/src/app/modules/engine/actions.ts +++ b/src/app/modules/engine/actions.ts @@ -14,5 +14,4 @@ export const navigate = (url: string) => push(url); export const initialize = actionCreator.async<{}, { defaultNetwork: string, preconfiguredNetworks: INetwork[], locales: ILocale[] }, IFatalError>('INITIALIZE'); export const discoverNetwork = actionCreator.async<{ uuid: string }, { session: ISession }, NetworkError>('DISCOVER_NETWORK'); export const addNetwork = actionCreator.async<{ name: string, networkID?: number, apiHost: string }, void>('ADD_NETWORK'); -export const setCollapsed = actionCreator('SET_COLLAPSED'); export const setLocale = actionCreator.async('SET_LOCALE'); \ No newline at end of file diff --git a/src/app/modules/engine/epics/discoverNetworkEpic.ts b/src/app/modules/engine/epics/discoverNetworkEpic.ts index 0238d36d9..481ca23ec 100644 --- a/src/app/modules/engine/epics/discoverNetworkEpic.ts +++ b/src/app/modules/engine/epics/discoverNetworkEpic.ts @@ -11,45 +11,47 @@ import { discover } from 'services/network'; import { mergeFullNodes } from 'modules/storage/actions'; import NetworkError from 'services/network/errors'; -const setNetworkEpic: Epic = (action$, store, { api, defaultKey }) => action$.ofAction(discoverNetwork.started) +const discoverNetworkEpic: Epic = (action$, store, { api, defaultKey }) => action$.ofAction(discoverNetwork.started) .flatMap(action => { const network = store.getState().storage.networks.find(l => l.uuid === action.payload.uuid); return NodeObservable({ nodes: network.fullNodes, count: 1, - timeout: 5000, + timeout: 10000, concurrency: 10, api - }).flatMap(node => - Observable.from(discover({ uuid: network.uuid, apiHost: node }, defaultKey, network.id)) - .flatMap(result => Observable.concat( - Observable.of(discoverNetwork.done({ - params: action.payload, - result: { - session: { - network: { - uuid: network.uuid, - apiHost: node - }, - sessionToken: result.loginResult.token + }).defaultIfEmpty(null).flatMap(node => + Observable.if( + () => null !== node, + Observable.defer(() => Observable.from(discover({ uuid: network.uuid, apiHost: node }, defaultKey, network.id)) + .flatMap(result => Observable.concat( + Observable.of(discoverNetwork.done({ + params: action.payload, + result: { + session: { + network: { + uuid: network.uuid, + apiHost: node + }, + sessionToken: result.loginResult.token + } } - } - })), - Observable.of(mergeFullNodes({ - uuid: network.uuid, - fullNodes: result.fullNodes - })) - )) + })), + Observable.of(mergeFullNodes({ + uuid: network.uuid, + fullNodes: result.fullNodes + })) + ))), + Observable.defer(() => Observable.throw(NetworkError.Offline)) + ) ).catch((error: NetworkError) => Observable.of(discoverNetwork.failed({ params: action.payload, error - }))).timeout(10000).catch(timeout => Observable.of(discoverNetwork.failed({ - params: action.payload, - error: NetworkError.Offline + }))); }); -export default setNetworkEpic; +export default discoverNetworkEpic; \ No newline at end of file diff --git a/src/app/modules/engine/epics/initializeEpic.ts b/src/app/modules/engine/epics/initializeEpic.ts index c40ab6f0d..4812c8b40 100644 --- a/src/app/modules/engine/epics/initializeEpic.ts +++ b/src/app/modules/engine/epics/initializeEpic.ts @@ -14,6 +14,8 @@ import { INetwork } from 'apla/auth'; import webConfig from 'lib/settings/webConfig'; import localeConfig from 'lib/settings/localeConfig'; import ConfigObservable from '../util/ConfigObservable'; +import { acquireSession } from 'modules/auth/actions'; +import { Action } from 'redux'; const DEFAULT_NETWORK = '__DEFAULT'; @@ -64,7 +66,7 @@ const initializeEpic: Epic = (action$, store, { defaultPassword }) => action$.of demoEnabled: network.enableDemoMode })); - return Observable.concat( + return Observable.concat( Observable.if( () => !!preconfiguredKey, Observable.of(saveWallet(preconfiguredKey)), @@ -79,7 +81,12 @@ const initializeEpic: Epic = (action$, store, { defaultPassword }) => action$.of locales: locales.locales } })), - Observable.of(setLocale.started(state.storage.locale || config.defaultLocale)) + Observable.of(setLocale.started(state.storage.locale || config.defaultLocale)), + Observable.if( + () => store.getState().auth.isAuthenticated && !!store.getState().auth.session, + Observable.of(acquireSession.started(store.getState().auth.session)), + Observable.empty() + ) ); }).catch(e => Observable.of(initialize.failed({ params: action.payload, diff --git a/src/app/modules/engine/reducer.ts b/src/app/modules/engine/reducer.ts index c8f5d2187..61a006fb3 100644 --- a/src/app/modules/engine/reducer.ts +++ b/src/app/modules/engine/reducer.ts @@ -10,7 +10,6 @@ import { IFatalError, ILocale } from 'apla'; import { reducerWithInitialState } from 'typescript-fsa-reducers/dist'; import initializeDoneHandler from './reducers/initializeDoneHandler'; import setLocaleDoneHandler from './reducers/setLocaleDoneHandler'; -import setCollapsedHandler from './reducers/setCollapsedHandler'; import discoverNetworkHandler from './reducers/discoverNetworkHandler'; import discoverNetworkDoneHandler from './reducers/discoverNetworkDoneHandler'; import discoverNetworkFailedHandler from './reducers/discoverNetworkFailedHandler'; @@ -24,12 +23,12 @@ export type State = { readonly fatalError?: IFatalError; readonly guestSession: ISession; readonly localeMessages: { [key: string]: string }; - readonly isCollapsed: boolean; readonly isLoaded: boolean; readonly isConnecting: boolean; readonly preconfiguredNetworks: INetwork[]; readonly locales: ILocale[]; readonly locale: string; + readonly panel?: string; }; export const initialState: State = { @@ -37,18 +36,17 @@ export const initialState: State = { fatalError: null, guestSession: null, localeMessages: {}, - isCollapsed: true, isLoaded: false, isConnecting: false, preconfiguredNetworks: [], locales: [], - locale: null + locale: null, + panel: null }; export default reducerWithInitialState(initialState) .case(actions.initialize.done, initializeDoneHandler) .case(actions.initialize.failed, initializeFailedHandler) - .case(actions.setCollapsed, setCollapsedHandler) .case(actions.setLocale.done, setLocaleDoneHandler) .case(actions.discoverNetwork.started, discoverNetworkHandler) .case(actions.discoverNetwork.done, discoverNetworkDoneHandler) diff --git a/src/app/modules/gui/epic.ts b/src/app/modules/gui/epic.ts deleted file mode 100644 index 22bee1456..000000000 --- a/src/app/modules/gui/epic.ts +++ /dev/null @@ -1,17 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { combineEpics } from 'redux-observable'; -import switchWindowEpic from './epics/switchWindowEpic'; -import setBadgeCountEpic from './epics/setBadgeCountEpic'; -import switchWindowOnLoginEpic from './epics/switchWindowOnLoginEpic'; -import setBadgeCountOnLogoutEpic from './epics/setBadgeCountOnLogoutEpic'; - -export default combineEpics( - setBadgeCountEpic, - switchWindowEpic, - switchWindowOnLoginEpic, - setBadgeCountOnLogoutEpic -); \ No newline at end of file diff --git a/src/app/modules/gui/epics/setBadgeCountEpic.ts b/src/app/modules/gui/epics/setBadgeCountEpic.ts deleted file mode 100644 index 8ccccde98..000000000 --- a/src/app/modules/gui/epics/setBadgeCountEpic.ts +++ /dev/null @@ -1,24 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Action } from 'redux'; -import { Epic } from 'redux-observable'; -import { IRootState } from 'modules'; -import { setBadgeCount } from '../actions'; -import { Observable } from 'rxjs/Observable'; -import platform from 'lib/platform'; - -const setBadgeCountEpic: Epic = - (action$, store) => action$.ofAction(setBadgeCount) - .flatMap(action => { - platform.on('desktop', () => { - const Electron = require('electron'); - Electron.remote.app.setBadgeCount(action.payload); - }); - - return Observable.empty(); - }); - -export default setBadgeCountEpic; \ No newline at end of file diff --git a/src/app/modules/gui/epics/setBadgeCountOnLogoutEpic.ts b/src/app/modules/gui/epics/setBadgeCountOnLogoutEpic.ts deleted file mode 100644 index 7818460a8..000000000 --- a/src/app/modules/gui/epics/setBadgeCountOnLogoutEpic.ts +++ /dev/null @@ -1,19 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Action } from 'redux'; -import { Epic } from 'redux-observable'; -import { IRootState } from 'modules'; -import { setBadgeCount } from '../actions'; -import { login, loginGuest } from 'modules/auth/actions'; -import { isType } from 'typescript-fsa'; - -const setBadgeCountOnLogoutEpic: Epic = - (action$, store) => action$.filter(action => isType(action, login.done) || isType(action, loginGuest.done)) - .map(action => - setBadgeCount(0) - ); - -export default setBadgeCountOnLogoutEpic; \ No newline at end of file diff --git a/src/app/modules/gui/epics/switchWindowEpic.ts b/src/app/modules/gui/epics/switchWindowEpic.ts deleted file mode 100644 index c679bc07e..000000000 --- a/src/app/modules/gui/epics/switchWindowEpic.ts +++ /dev/null @@ -1,26 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Action } from 'redux'; -import { Epic } from 'redux-observable'; -import { IRootState } from 'modules'; -import { switchWindow } from '../actions'; -import platform from 'lib/platform'; - -const switchWindowEpic: Epic = - (action$, store) => action$.ofAction(switchWindow.started) - .map(action => { - platform.on('desktop', () => { - const Electron = require('electron'); - Electron.ipcRenderer.sendSync('switchWindow', action.payload); - }); - - return switchWindow.done({ - params: action.payload, - result: action.payload - }); - }); - -export default switchWindowEpic; \ No newline at end of file diff --git a/src/app/modules/gui/reducer.ts b/src/app/modules/gui/reducer.ts deleted file mode 100644 index da78f3a26..000000000 --- a/src/app/modules/gui/reducer.ts +++ /dev/null @@ -1,20 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as actions from './actions'; -import { reducerWithInitialState } from 'typescript-fsa-reducers'; -import { TWindowType } from 'apla/gui'; -import switchWindowDoneHandler from './reducers/switchWindowDoneHandler'; - -export type State = { - readonly window: TWindowType; -}; - -export const initialState: State = { - window: 'general' -}; - -export default reducerWithInitialState(initialState) - .case(actions.switchWindow.done, switchWindowDoneHandler); \ No newline at end of file diff --git a/src/app/modules/gui/reducers/switchWindowDoneHandler.ts b/src/app/modules/gui/reducers/switchWindowDoneHandler.ts deleted file mode 100644 index 2d732f7b6..000000000 --- a/src/app/modules/gui/reducers/switchWindowDoneHandler.ts +++ /dev/null @@ -1,15 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { State } from '../reducer'; -import { switchWindow } from '../actions'; -import { Reducer } from 'modules'; - -const switchWindowDoneHandler: Reducer = (state, payload) => ({ - ...state, - window: payload.result -}); - -export default switchWindowDoneHandler; \ No newline at end of file diff --git a/src/app/modules/index.ts b/src/app/modules/index.ts index 8abf9664e..2a042b2d4 100644 --- a/src/app/modules/index.ts +++ b/src/app/modules/index.ts @@ -8,8 +8,6 @@ import { Epic as NativeEpic } from 'redux-observable'; import { IStoreDependencies } from './dependencies'; import { combineReducers } from 'redux'; import { combineEpics } from 'redux-observable'; -import { RouterState } from 'connected-react-router'; -import { loadingBarReducer } from 'react-redux-loading-bar'; import * as auth from './auth'; import * as content from './content'; import * as sections from './sections'; @@ -17,11 +15,11 @@ import * as modal from './modal'; import * as engine from './engine'; import * as editor from './editor'; import * as tx from './tx'; -import * as gui from './gui'; import * as io from './io'; import * as notifications from './notifications'; import * as storage from './storage'; import * as socket from './socket'; +import * as router from './router'; import { ActionCreator, Failure, Success } from 'typescript-fsa'; export type Epic = NativeEpic; @@ -39,13 +37,11 @@ export interface IRootState { engine: engine.State; editor: editor.State; tx: tx.State; - gui: gui.State; io: io.State; notifications: notifications.State; storage: storage.State; socket: socket.State; - loadingBar: number; - router: RouterState; + router: router.State; } export const rootEpic = combineEpics( @@ -56,11 +52,11 @@ export const rootEpic = combineEpics( engine.epic, editor.epic, tx.epic, - gui.epic, io.epic, notifications.epic, storage.epic, - socket.epic + socket.epic, + router.epic ); export default combineReducers({ @@ -71,9 +67,8 @@ export default combineReducers({ engine: engine.reducer, editor: editor.reducer, tx: tx.reducer, - gui: gui.reducer, notifications: notifications.reducer, storage: storage.reducer, socket: socket.reducer, - loadingBar: loadingBarReducer + router: router.reducer }); \ No newline at end of file diff --git a/src/app/modules/modal/actions.ts b/src/app/modules/modal/actions.ts index ffaf8f694..620137985 100644 --- a/src/app/modules/modal/actions.ts +++ b/src/app/modules/modal/actions.ts @@ -4,10 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import actionCreatorFactory from 'typescript-fsa'; -import { IModalCall, IModalCloseCall, IModalPageCall } from 'apla/modal'; +import { IModalCall, IModalCloseCall } from 'apla/modal'; const actionCreator = actionCreatorFactory('modal'); export const modalShow = actionCreator('MODAL_SHOW'); -export const modalClose = actionCreator('MODAL_CLOSE'); -export const modalPage = actionCreator('MODAL_PAGE'); \ No newline at end of file +export const modalClose = actionCreator('MODAL_CLOSE'); \ No newline at end of file diff --git a/src/app/modules/modal/epic.ts b/src/app/modules/modal/epic.ts index 40a8c7348..e5dc800f5 100644 --- a/src/app/modules/modal/epic.ts +++ b/src/app/modules/modal/epic.ts @@ -4,12 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { combineEpics } from 'redux-observable'; -import modalPageEpic from './epics/modalPageEpic'; import closeModalOnInteractionEpic from './epics/closeModalOnInteractionEpic'; import removeNetworkEpic from './epics/removeNetworkEpic'; export default combineEpics( - modalPageEpic, closeModalOnInteractionEpic, removeNetworkEpic ); \ No newline at end of file diff --git a/src/app/modules/modal/epics/closeModalOnInteractionEpic.ts b/src/app/modules/modal/epics/closeModalOnInteractionEpic.ts index 5ce6a13e6..ad2aae875 100644 --- a/src/app/modules/modal/epics/closeModalOnInteractionEpic.ts +++ b/src/app/modules/modal/epics/closeModalOnInteractionEpic.ts @@ -7,10 +7,10 @@ import { Epic } from 'modules'; import { isType } from 'typescript-fsa'; import { Observable } from 'rxjs/Observable'; import { modalClose } from '../actions'; -import { navigatePage } from 'modules/sections/actions'; import { logout } from 'modules/auth/actions'; +import { locationChange } from 'modules/router/actions'; -const closeModalOnInteractionEpic: Epic = (action$, store, { api }) => action$.filter(action => isType(action, navigatePage.started) || isType(action, logout.started)) +const closeModalOnInteractionEpic: Epic = (action$, store) => action$.filter(action => isType(action, locationChange) || isType(action, logout.started)) .flatMap(() => { const state = store.getState(); diff --git a/src/app/modules/modal/epics/modalPageEpic.ts b/src/app/modules/modal/epics/modalPageEpic.ts deleted file mode 100644 index d848b57ec..000000000 --- a/src/app/modules/modal/epics/modalPageEpic.ts +++ /dev/null @@ -1,42 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Epic } from 'modules'; -import { modalPage, modalShow } from '../actions'; -import { Observable } from 'rxjs/Observable'; - -const modalPageEpic: Epic = (action$, store, { api }) => action$.ofAction(modalPage) - .flatMap(action => { - const state = store.getState(); - const client = api({ - apiHost: state.auth.session.network.apiHost, - sessionToken: state.auth.session.sessionToken - }); - - return Observable.fromPromise(client.content({ - type: 'page', - name: action.payload.name, - params: action.payload.params, - locale: state.storage.locale - - })).map(payload => - modalShow({ - id: 'PAGE_MODAL:' + action.payload.name, - type: 'PAGE_MODAL', - params: { - name: action.payload.name, - title: action.payload.title || action.payload.name, - width: action.payload.width, - tree: payload.tree - } - - }) - - ).catch(e => - Observable.empty() - ); - }); - -export default modalPageEpic; \ No newline at end of file diff --git a/src/app/modules/gui/actions.ts b/src/app/modules/router/actions.ts similarity index 57% rename from src/app/modules/gui/actions.ts rename to src/app/modules/router/actions.ts index 3413457e3..9cee7a40d 100644 --- a/src/app/modules/gui/actions.ts +++ b/src/app/modules/router/actions.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import actionCreatorFactory from 'typescript-fsa'; -import { TWindowType } from 'apla/gui'; +import { RouterState } from 'connected-react-router'; -const actionCreator = actionCreatorFactory('gui'); -export const switchWindow = actionCreator.async('SWITCH_WINDOW'); -export const setBadgeCount = actionCreator('SET_BADGE_COUNT'); \ No newline at end of file +const actionCreator = actionCreatorFactory('@@router'); + +export const locationChange = actionCreator('LOCATION_CHANGE'); \ No newline at end of file diff --git a/src/app/modules/router/epic.ts b/src/app/modules/router/epic.ts new file mode 100644 index 000000000..5b9d050f1 --- /dev/null +++ b/src/app/modules/router/epic.ts @@ -0,0 +1,11 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { combineEpics } from 'redux-observable'; +import sectionLoadEpic from './epics/sectionLoadEpic'; + +export default combineEpics( + sectionLoadEpic +); \ No newline at end of file diff --git a/src/app/modules/router/epics/sectionLoadEpic.ts b/src/app/modules/router/epics/sectionLoadEpic.ts new file mode 100644 index 000000000..197329760 --- /dev/null +++ b/src/app/modules/router/epics/sectionLoadEpic.ts @@ -0,0 +1,93 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Epic } from 'modules'; +import { locationChange } from '../actions'; +import { renderPage } from 'modules/sections/actions'; +import { Observable } from 'rxjs'; +import { state$ } from 'store'; +import { initialize } from 'modules/engine/actions'; +import { isType, Action } from 'typescript-fsa'; +import { RouterState, replace } from 'connected-react-router'; +import { createEditorTab, loadEditorTab } from 'modules/editor/actions'; + +const sectionLoadEpic: Epic = (action$, store, { routerService }) => action$ + .filter(action => isType(action, initialize.started) || isType(action, locationChange)) + .map((action: Action) => { + if (isType(action, initialize.started)) { + return store.getState().router; + } + + return action.payload; + }) + .delayWhen(() => state$.filter(l => l.auth.isAcquired).take(1)) + .flatMap((routerState: RouterState) => { + const match = routerService.matchRoute('/browse(/:section)(/:page)', routerState.location.pathname + routerState.location.search); + const state = store.getState(); + + if (state.auth.isAuthenticated && match) { + const section = state.sections.sections[match.parts.section || state.sections.mainSection]; + + if (!section) { + return Observable.of(replace( + routerService.routeToBrowser(state.sections.mainSection, state.sections.sections[state.sections.mainSection].defaultPage) + )); + } + + const pageName = match.parts.page || section.defaultPage; + + // TODO: OLD EDITOR API COMPAT + if ('editor' === pageName) { + if (match.query.create) { + return Observable.of( + createEditorTab.started(match.query.create), + replace('/editor') + ); + } + else if (match.query.open) { + return Observable.of( + loadEditorTab.started({ type: match.query.open, name: match.query.name }), + replace('/editor') + ); + } + } + + // TODO: refactoring + // must ignore navigation when page and params are equal + // if ('POP' === action.payload.action) { + // const pageIndex = findPage(section, pageName); + // if (-1 !== pageIndex) { + // const page = store.value.navigator.sections[section.name].pages[pageIndex]; + // if (page.content || page.error) { + // return of(popPage({ + // location: action.payload.location, + // section: section.name, + // name: pageName + // })); + // } + // } + // } + + return Observable.of(renderPage.started({ + location: { + state: {}, + ...routerState.location + }, + section: section.name, + name: pageName, + params: match.query + })); + } + else { + return Observable.empty(); + } + + }).catch(e => { + // tslint:disable-next-line: no-console + console.log(e); + return Observable.of(e); + }); + +export default sectionLoadEpic; \ No newline at end of file diff --git a/src/app/modules/gui/index.ts b/src/app/modules/router/index.ts similarity index 100% rename from src/app/modules/gui/index.ts rename to src/app/modules/router/index.ts diff --git a/src/app/modules/router/reducer.ts b/src/app/modules/router/reducer.ts new file mode 100644 index 000000000..830efd0c8 --- /dev/null +++ b/src/app/modules/router/reducer.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { reducerWithInitialState } from 'typescript-fsa-reducers'; +import { RouterState } from 'connected-react-router'; + +export type State = + RouterState; + +export const initialState: RouterState = { + location: { + key: '', + pathname: '', + search: '', + hash: '' + }, + action: 'PUSH' +}; + +export default reducerWithInitialState(initialState); \ No newline at end of file diff --git a/src/app/modules/sections/actions.ts b/src/app/modules/sections/actions.ts index 8c7c03432..8abd05ad0 100644 --- a/src/app/modules/sections/actions.ts +++ b/src/app/modules/sections/actions.ts @@ -4,21 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import actionCreatorFactory from 'typescript-fsa'; -import { TMenu, TPage, TSection } from 'apla/content'; +import { IMenu, ISection } from 'apla/content'; +import { TProtypoElement } from 'apla/protypo'; +import { Location } from 'history'; -const actionCreator = actionCreatorFactory('section'); +const actionCreator = actionCreatorFactory('sections'); -// Navigation -export const renderSection = actionCreator('RENDER_SECTION'); -export const updateSection = actionCreator('UPDATE_SECTION'); -export const closeSection = actionCreator('CLOSE_SECTION'); -export const switchSection = actionCreator('SWITCH_SECTION'); -export const reset = actionCreator('RESET'); -export const menuPop = actionCreator('MENU_POP'); -export const menuPush = actionCreator('MENU_PUSH'); -export const navigatePage = actionCreator.async<{ name?: string, section?: string, force?: boolean, params: { [key: string]: any } }, { section: string }, undefined>('NAVIGATE_PAGE'); -export const navigationToggle = actionCreator('NAVIGATION_TOGGLE'); -export const renderPage = actionCreator.async<{ key: string, section: string, name: string, params?: { [key: string]: any } }, { defaultMenu: TMenu, menu: TMenu, page: TPage }, string>('RENDER_PAGE'); -export const renderLegacyPage = actionCreator.async<{ section: string, name: string, menu: string, params?: { [key: string]: any } }, { menu: TMenu, page: TPage }>('RENDER_LEGACY_PAGE'); -export const reloadPage = actionCreator.async<{}, { params: { [key: string]: any }, menu: TMenu, page: TPage }, string>('RELOAD_PAGE'); -export const sectionsInit = actionCreator.async('SECTIONS_INIT'); +export const updateSection = actionCreator('UPDATE_SECTION'); +export const menuPop = actionCreator('MENU_POP'); +export const menuPush = actionCreator<{ section: string, menu: IMenu }>('MENU_PUSH'); +export const renderPage = actionCreator.async<{ section: string, name: string, popup?: { title?: string, width?: number }, params: { [key: string]: string }, location: Location }, { tree: TProtypoElement[], menu: string, menuTree: TProtypoElement[], static: boolean }, string>('RENDER_PAGE'); +export const reloadPage = actionCreator<{ section: string }>('RELOAD_PAGE'); +export const sectionsInit = actionCreator<{ mainSection: string, sections: { [name: string]: ISection } }>('SECTIONS_INIT'); diff --git a/src/app/modules/sections/epic.ts b/src/app/modules/sections/epic.ts index 7416cea75..bb3a61f57 100644 --- a/src/app/modules/sections/epic.ts +++ b/src/app/modules/sections/epic.ts @@ -4,22 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { combineEpics } from 'redux-observable'; -import closeSectionEpic from './epics/closeSectionEpic'; -import renderSectionEpic from './epics/renderSectionEpic'; -import resetOnLoginEpic from './epics/resetOnLoginEpic'; -import navigatePageEpic from './epics/navigatePageEpic'; -import reloadPageEpic from './epics/reloadPageEpic'; -import renderLegacyPageEpic from './epics/renderLegacyPageEpic'; import renderPageEpic from './epics/renderPageEpic'; -import sectionsInitEpic from './epics/sectionsInitEpic'; +import reloadPageEpic from './epics/reloadPageEpic'; export default combineEpics( - closeSectionEpic, - renderSectionEpic, - resetOnLoginEpic, - navigatePageEpic, - reloadPageEpic, - renderLegacyPageEpic, renderPageEpic, - sectionsInitEpic + reloadPageEpic ); \ No newline at end of file diff --git a/src/app/modules/sections/epics/closeSectionEpic.ts b/src/app/modules/sections/epics/closeSectionEpic.ts deleted file mode 100644 index 3f36d2187..000000000 --- a/src/app/modules/sections/epics/closeSectionEpic.ts +++ /dev/null @@ -1,21 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Epic } from 'modules'; -import { closeSection, renderSection } from '..//actions'; -import { Observable } from 'rxjs/Observable'; - -const closeSectionEpic: Epic = (action$, store) => action$.ofAction(closeSection) - .flatMap(action => { - const state = store.getState(); - if (action.payload === state.sections.section) { - return Observable.of(renderSection('home')); - } - else { - return Observable.empty(); - } - }); - -export default closeSectionEpic; \ No newline at end of file diff --git a/src/app/modules/sections/epics/navigatePageEpic.ts b/src/app/modules/sections/epics/navigatePageEpic.ts deleted file mode 100644 index 8062d11e1..000000000 --- a/src/app/modules/sections/epics/navigatePageEpic.ts +++ /dev/null @@ -1,32 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import queryString from 'query-string'; -import { Action } from 'redux'; -import { push } from 'connected-react-router'; -import { Epic } from 'modules'; -import { Observable } from 'rxjs/Observable'; -import { navigatePage } from '../actions'; -import { LEGACY_PAGES } from 'lib/legacyPages'; - -const navigatePageEpic: Epic = (action$, store) => action$.ofAction(navigatePage.started) - .flatMap(action => { - const state = store.getState(); - const sectionName = (LEGACY_PAGES[action.payload.name] && LEGACY_PAGES[action.payload.name].section) || action.payload.section || state.sections.section; - const section = state.sections.sections[sectionName]; - const params = queryString.stringify(action.payload.params); - - return Observable.of( - push(`/${sectionName}/${action.payload.name || section.defaultPage}${params ? '?' + params : ''}`), - navigatePage.done({ - params: action.payload, - result: { - section: sectionName - } - }) - ); - }); - -export default navigatePageEpic; \ No newline at end of file diff --git a/src/app/modules/sections/epics/reloadPageEpic.ts b/src/app/modules/sections/epics/reloadPageEpic.ts index 4827b2e1b..a535d176b 100644 --- a/src/app/modules/sections/epics/reloadPageEpic.ts +++ b/src/app/modules/sections/epics/reloadPageEpic.ts @@ -4,47 +4,23 @@ *--------------------------------------------------------------------------------------------*/ import { Epic } from 'modules'; -import { Observable } from 'rxjs/Observable'; -import { reloadPage } from '../actions'; +import { reloadPage, renderPage } from '../actions'; +import { Observable } from 'rxjs'; -const reloadPageEpic: Epic = (action$, store, { api }) => action$.ofAction(reloadPage.started) - .flatMap(action => { - const state = store.getState(); - const section = state.sections.sections[state.sections.section]; - const client = api({ - apiHost: state.auth.session.network.apiHost, - sessionToken: state.auth.session.sessionToken - }); +const reloadPageEpic: Epic = (action$, store) => action$.ofAction(reloadPage) + .switchMap(action => { + const section = store.getState().sections.sections[action.payload.section]; - return Observable.fromPromise(client.content({ - type: 'page', + if (!section || !section.page) { + return Observable.empty(); + } + + return Observable.of(renderPage.started({ + section: section.name, name: section.page.name, params: section.page.params, - locale: state.storage.locale, - - })).map(payload => - reloadPage.done({ - params: action.payload, - result: { - params: section.page.params, - menu: { - name: payload.menu, - content: payload.menutree - }, - page: { - params: section.page.params, - name: section.page.name, - content: payload.tree - } - } - }) - - ).catch(e => - Observable.of(reloadPage.failed({ - params: action.payload, - error: e.error - })) - ); + location: section.page.location + })); }); export default reloadPageEpic; \ No newline at end of file diff --git a/src/app/modules/sections/epics/renderLegacyPageEpic.ts b/src/app/modules/sections/epics/renderLegacyPageEpic.ts deleted file mode 100644 index fbc2ae56f..000000000 --- a/src/app/modules/sections/epics/renderLegacyPageEpic.ts +++ /dev/null @@ -1,78 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Epic } from 'modules'; -import { Observable } from 'rxjs/Observable'; -import { renderLegacyPage } from '../actions'; -import { LEGACY_PAGES } from 'lib/legacyPages'; -import { IContentResponse } from 'apla/api'; - -const renderLegacyPageEpic: Epic = (action$, store, { api }) => action$.ofAction(renderLegacyPage.started) - .flatMap(action => { - const state = store.getState(); - const client = api({ - apiHost: state.auth.session.network.apiHost, - sessionToken: state.auth.session.sessionToken - }); - - const LEGACY_PAGE = LEGACY_PAGES[action.payload.name]; - const substitute = LEGACY_PAGE.renderSubstitute && LEGACY_PAGE.renderSubstitute(action.payload.params); - - return Observable.zip( - Observable.if( - () => !!action.payload.menu, - Observable.defer(() => client.content({ - type: 'menu', - name: action.payload.menu, - params: {}, - locale: state.storage.locale - })), - Observable.of(null) - ), - Observable.if( - () => !!substitute, - Observable.defer(() => client.content({ - type: 'page', - name: substitute.name, - params: substitute.params, - locale: state.storage.locale - })), - Observable.of(null) - ) - - ).map(([menu, page]) => renderLegacyPage.done({ - params: action.payload, - result: { - menu: { - name: action.payload.menu, - content: menu ? menu.tree : [] - }, - page: page ? - { - name: substitute.name, - params: substitute.params, - content: page.tree - } : - { - name: '', - params: {}, - content: [] - } - } - - })).catch(e => Observable.of(renderLegacyPage.done({ - params: action.payload, - result: { - menu: null, - page: { - name: '', - params: {}, - content: [] - } - } - }))); - }); - -export default renderLegacyPageEpic; \ No newline at end of file diff --git a/src/app/modules/sections/epics/renderPageEpic.ts b/src/app/modules/sections/epics/renderPageEpic.ts index 9ec5396d6..999ffb8ff 100644 --- a/src/app/modules/sections/epics/renderPageEpic.ts +++ b/src/app/modules/sections/epics/renderPageEpic.ts @@ -6,60 +6,63 @@ import { Epic } from 'modules'; import { Observable } from 'rxjs/Observable'; import { renderPage } from '../actions'; +import { STATIC_PAGES } from 'lib/staticPages'; +import { modalShow } from 'modules/modal/actions'; +import { Action } from 'redux'; const renderPageEpic: Epic = (action$, store, { api }) => action$.ofAction(renderPage.started) - .flatMap(action => { + .switchMap(action => { const state = store.getState(); const client = api({ apiHost: state.auth.session.network.apiHost, sessionToken: state.auth.session.sessionToken }); - const section = state.sections.sections[action.payload.section || state.sections.section]; - return Observable.from(Promise.all([ - client.content({ - type: 'page', - name: action.payload.name, - params: action.payload.params, - locale: state.storage.locale + const staticPage = STATIC_PAGES[action.payload.name]; + const substitute = staticPage && staticPage.renderSubstitute && staticPage.renderSubstitute(action.payload.params); + const requestPage = staticPage ? substitute : { + name: action.payload.name, + params: action.payload.params + }; - }), - (!section.menus.length && section.defaultPage !== action.payload.name) ? client.content({ - type: 'page', - name: section.defaultPage, - params: {}, - locale: state.storage.locale - }) : Promise.resolve(null) + return Observable.from(client.content({ + type: 'page', + locale: state.storage.locale, + ...requestPage - ])).map(payload => { - const page = payload[0]; - const defaultPage = payload[1]; - - return renderPage.done({ - params: action.payload, - result: { - defaultMenu: defaultPage && defaultPage.menu !== page.menu && { - name: defaultPage.menu, - content: defaultPage.menutree - }, - menu: { - name: page.menu, - content: page.menutree - }, - page: { - params: action.payload.params, - name: action.payload.name, - content: page.tree + })).flatMap(content => { + return Observable.concat( + Observable.of(renderPage.done({ + params: action.payload, + result: { + tree: content.tree, + menu: content.menu, + menuTree: content ? content.menutree : [], + static: !!staticPage } - } - }); + })), + Observable.if( + () => !!action.payload.popup, + Observable.defer(() => Observable.of(modalShow({ + id: 'PAGE_MODAL' + action.payload.name, + type: 'PAGE_MODAL', + params: { + name: action.payload.name, + section: action.payload.section, + title: action.payload.popup.title || action.payload.name, + width: action.payload.popup.width, + tree: content.tree, + params: action.payload.params, + static: !!staticPage + } + }))) + ), + ); - }).catch(e => - Observable.of(renderPage.failed({ - params: action.payload, - error: e.error - })) - ); + }).catch(e => Observable.of(renderPage.failed({ + params: action.payload, + error: e.error + }))); }); export default renderPageEpic; \ No newline at end of file diff --git a/src/app/modules/sections/epics/renderSectionEpic.ts b/src/app/modules/sections/epics/renderSectionEpic.ts deleted file mode 100644 index db83a3a87..000000000 --- a/src/app/modules/sections/epics/renderSectionEpic.ts +++ /dev/null @@ -1,19 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import queryString from 'query-string'; -import { Epic } from 'modules'; -import { push } from 'connected-react-router'; -import { renderSection } from '../actions'; - -const renderSectionEpic: Epic = (action$, store) => action$.ofAction(renderSection) - .map(action => { - const state = store.getState(); - const section = state.sections.sections[action.payload]; - const params = section.page ? queryString.stringify(section.page.params) : ''; - return push(`/${section.name}/${section.page ? section.page.name : ''}${params ? '?' + params : ''}`); - }); - -export default renderSectionEpic; \ No newline at end of file diff --git a/src/app/modules/sections/epics/resetOnLoginEpic.ts b/src/app/modules/sections/epics/resetOnLoginEpic.ts deleted file mode 100644 index 20e0ea944..000000000 --- a/src/app/modules/sections/epics/resetOnLoginEpic.ts +++ /dev/null @@ -1,19 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Action } from 'redux'; -import { Epic } from 'redux-observable'; -import { IRootState } from 'modules'; -import { reset } from '../actions'; -import { login, loginGuest } from 'modules/auth/actions'; -import { isType } from 'typescript-fsa'; - -const resetOnWalletSelectEpic: Epic = - (action$, store) => action$.filter(action => isType(action, login.done) || isType(action, loginGuest.done)) - .map(action => - reset() - ); - -export default resetOnWalletSelectEpic; \ No newline at end of file diff --git a/src/app/modules/sections/epics/sectionsInitEpic.ts b/src/app/modules/sections/epics/sectionsInitEpic.ts deleted file mode 100644 index a79caecbe..000000000 --- a/src/app/modules/sections/epics/sectionsInitEpic.ts +++ /dev/null @@ -1,93 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Epic } from 'modules'; -import { Observable } from 'rxjs/Observable'; -import { sectionsInit, navigatePage } from 'modules/sections/actions'; - -enum RemoteSectionStatus { - Removed = '0', - Default = '1', - Main = '2' -} - -interface IRemoteSectionProto { - id: string; - title: string; - urlname: string; - page: string; - roles_access: string; - status: RemoteSectionStatus; - order: string; -} - -const sectionsInitEpic: Epic = (action$, store, { api }) => action$.ofAction(sectionsInit.started) - .flatMap(action => { - const state = store.getState(); - const client = api({ - apiHost: state.auth.session.network.apiHost, - sessionToken: state.auth.session.sessionToken - }); - const roleID = state.auth.wallet.role && state.auth.wallet.role.id; - - return Observable.from(Promise.all([ - client.sections({ - locale: state.storage.locale - }), - client.getRow({ - table: 'roles', - id: (roleID && roleID.toString()) || null, - }).then(row => row.value.default_page).catch(e => null) - - ])).flatMap(payload => { - const remoteSections = (payload[0].list as object as IRemoteSectionProto[]).filter(l => - RemoteSectionStatus.Removed !== l.status - ); - - const mainSection = remoteSections.find(l => RemoteSectionStatus.Main === l.status) || remoteSections[0]; - const sections = [ - { - ...mainSection, - page: payload[1] || mainSection.page - }, - ...remoteSections.filter(l => l.id !== mainSection.id) - ].sort((a, b) => - Number(a.order) - Number(b.order) || Number(a.id) - Number(b.id) - ); - - return Observable.concat( - Observable.of(sectionsInit.done({ - params: action.payload, - result: { - mainSection: mainSection.urlname, - section: action.payload, - sections: [ - ...sections.map(l => ({ - key: null, - name: l.urlname, - title: l.title, - visible: true, - defaultPage: l.page, - pending: false, - force: false, - menus: [], - menuVisible: true, - page: null - })) - ] - }, - })), - action.payload ? Observable.empty() : Observable.of(navigatePage.started({ - section: mainSection.urlname, - params: {} - })) - ); - }).catch(e => Observable.of(sectionsInit.failed({ - params: action.payload, - error: e - }))); - }).catch(e => Observable.empty()); - -export default sectionsInitEpic; \ No newline at end of file diff --git a/src/app/modules/sections/reducer.ts b/src/app/modules/sections/reducer.ts index 3571ba74d..bbc350431 100644 --- a/src/app/modules/sections/reducer.ts +++ b/src/app/modules/sections/reducer.ts @@ -5,67 +5,32 @@ import * as actions from './actions'; import { reducerWithInitialState } from 'typescript-fsa-reducers'; -import { TSection } from 'apla/content'; -import closeSectionHandler from './reducers/closeSectionHandler'; -import renderSectionHandler from './reducers/renderSectionHandler'; -import switchSectionHandler from './reducers/switchSectionHandler'; +import { ISection } from 'apla/content'; import updateSectionHandler from './reducers/updateSectionHandler'; -import resetHandler from './reducers/resetHandler'; -import sectionsInitDoneHandler from './reducers/sectionsInitDoneHandler'; +import sectionsInitHandler from './reducers/sectionsInitHandler'; import menuPopHandler from './reducers/menuPopHandler'; import menuPushHandler from './reducers/menuPushHandler'; -import navigatePageDoneHandler from './reducers/navigatePageDoneHandler'; -import navigationToggleHandler from './reducers/navigationToggleHandler'; -import renderLegacyPageDoneHandler from './reducers/renderLegacyPageDoneHandler'; -import renderLegacyPageHandler from './reducers/renderLegacyPageHandler'; import renderPageDoneHandler from './reducers/renderPageDoneHandler'; import renderPageFailedHandler from './reducers/renderPageFailedHandler'; import renderPageHandler from './reducers/renderPageHandler'; export type State = { readonly mainSection: string; - readonly section: string; readonly sections: { - readonly [name: string]: TSection; + readonly [name: string]: ISection; }; - readonly systemSections: TSection[]; - readonly inited: boolean; }; export const initialState: State = { - mainSection: null, - section: null, - sections: {}, - systemSections: [{ - key: 'editor', - name: 'editor', - title: 'Editor', - visible: false, - closeable: true, - defaultPage: 'editor', - pending: false, - force: false, - menus: [], - menuDisabled: true, - menuVisible: true, - page: null - }], - inited: false + mainSection: 'home', + sections: {} }; export default reducerWithInitialState(initialState) - .case(actions.closeSection, closeSectionHandler) - .case(actions.renderSection, renderSectionHandler) - .case(actions.switchSection, switchSectionHandler) .case(actions.updateSection, updateSectionHandler) - .case(actions.reset, resetHandler) .case(actions.menuPop, menuPopHandler) .case(actions.menuPush, menuPushHandler) - .case(actions.navigatePage.done, navigatePageDoneHandler) - .case(actions.navigationToggle, navigationToggleHandler) - .case(actions.renderLegacyPage.done, renderLegacyPageDoneHandler) - .case(actions.renderLegacyPage.started, renderLegacyPageHandler) .case(actions.renderPage.done, renderPageDoneHandler) .case(actions.renderPage.failed, renderPageFailedHandler) .case(actions.renderPage.started, renderPageHandler) - .case(actions.sectionsInit.done, sectionsInitDoneHandler); + .case(actions.sectionsInit, sectionsInitHandler); \ No newline at end of file diff --git a/src/app/modules/sections/reducers/closeSectionHandler.ts b/src/app/modules/sections/reducers/closeSectionHandler.ts deleted file mode 100644 index 46554ebbd..000000000 --- a/src/app/modules/sections/reducers/closeSectionHandler.ts +++ /dev/null @@ -1,23 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { State } from '../reducer'; -import { renderSection } from '../actions'; -import { Reducer } from 'modules'; - -const closeSectionHandler: Reducer = (state, payload) => { - return state.sections[payload] ? { - ...state, - sections: { - ...state.sections, - [payload]: { - ...state.sections[payload], - visible: false - } - } - } : state; -}; - -export default closeSectionHandler; \ No newline at end of file diff --git a/src/app/modules/sections/reducers/menuPopHandler.ts b/src/app/modules/sections/reducers/menuPopHandler.ts index c5f498199..ac832c116 100644 --- a/src/app/modules/sections/reducers/menuPopHandler.ts +++ b/src/app/modules/sections/reducers/menuPopHandler.ts @@ -7,22 +7,21 @@ import { State } from '../reducer'; import { menuPop } from '../actions'; import { Reducer } from 'modules'; -const menuPopHandler: Reducer = (state, payload) => { - if (1 >= state.sections[state.section].menus.length) { +const menuPopHandler: Reducer = (state, section): State => { + if (1 >= state.sections[section].menus.length) { return state; } - else { - return { - ...state, - sections: { - ...state.sections, - [state.section]: { - ...state.sections[state.section], - menus: state.sections[state.section].menus.slice(0, -1) - } + + return { + ...state, + sections: { + ...state.sections, + [section]: { + ...state.sections[section], + menus: state.sections[section].menus.slice(0, -1) } - }; - } + } + }; }; export default menuPopHandler; \ No newline at end of file diff --git a/src/app/modules/sections/reducers/menuPushHandler.ts b/src/app/modules/sections/reducers/menuPushHandler.ts index 97dbeefc0..bc9559a0c 100644 --- a/src/app/modules/sections/reducers/menuPushHandler.ts +++ b/src/app/modules/sections/reducers/menuPushHandler.ts @@ -6,25 +6,14 @@ import { State } from '../reducer'; import { menuPush } from '../actions'; import { Reducer } from 'modules'; +import upsertSectionMenu from '../util/upsertSectionMenu'; -const menuPushHandler: Reducer = (state, payload) => { - const menuIndex = state.sections[state.section].menus.findIndex(l => - l.name === payload.name); - - return { - ...state, - sections: { - ...state.sections, - [state.section]: { - ...state.sections[state.section], - menus: -1 === menuIndex ? [...state.sections[state.section].menus, payload] : [ - ...state.sections[state.section].menus.slice(0, menuIndex), - payload, - ...state.sections[state.section].menus.slice(menuIndex + 1) - ] - } - } - }; -}; +const menuPushHandler: Reducer = (state, payload): State => ({ + ...state, + sections: { + ...state.sections, + [payload.section]: upsertSectionMenu(state.sections[payload.section], payload.menu) + } +}); export default menuPushHandler; \ No newline at end of file diff --git a/src/app/modules/sections/reducers/navigatePageDoneHandler.ts b/src/app/modules/sections/reducers/navigatePageDoneHandler.ts deleted file mode 100644 index 8ce80fced..000000000 --- a/src/app/modules/sections/reducers/navigatePageDoneHandler.ts +++ /dev/null @@ -1,24 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { State } from '../reducer'; -import { navigatePage } from '../actions'; -import { Reducer } from 'modules'; - -const navigatePageDoneHandler: Reducer = (state, payload) => ({ - ...state, - section: payload.result.section, - sections: { - ...state.sections, - [payload.result.section]: { - ...state.sections[payload.result.section], - page: state.sections[payload.result.section].page, - force: payload.params.force, - pending: false - } - } -}); - -export default navigatePageDoneHandler; \ No newline at end of file diff --git a/src/app/modules/sections/reducers/navigationToggleHandler.ts b/src/app/modules/sections/reducers/navigationToggleHandler.ts deleted file mode 100644 index fce1f9bc1..000000000 --- a/src/app/modules/sections/reducers/navigationToggleHandler.ts +++ /dev/null @@ -1,21 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { State } from '../reducer'; -import { navigationToggle } from '../actions'; -import { Reducer } from 'modules'; - -const navigationToggleHandler: Reducer = (state, payload) => ({ - ...state, - sections: { - ...state.sections, - [state.section]: { - ...state.sections[state.section], - menuVisible: !state.sections[state.section].menuVisible - } - } -}); - -export default navigationToggleHandler; \ No newline at end of file diff --git a/src/app/modules/sections/reducers/renderLegacyPageDoneHandler.ts b/src/app/modules/sections/reducers/renderLegacyPageDoneHandler.ts deleted file mode 100644 index 9345922a4..000000000 --- a/src/app/modules/sections/reducers/renderLegacyPageDoneHandler.ts +++ /dev/null @@ -1,42 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { State } from '../reducer'; -import { renderLegacyPage } from '../actions'; -import { Reducer } from 'modules'; - -const renderLegacyPageDoneHandler: Reducer = (state, payload) => { - const section = payload.params.section; - const menuIndex = payload.result.menu ? state.sections[section].menus.findIndex(l => - l.name === payload.result.menu.name) : -1; - - return { - ...state, - sections: { - ...state.sections, - [section]: { - ...state.sections[section], - menus: -1 === menuIndex ? - (payload.params.menu ? [ - ...state.sections[section].menus, - payload.result.menu - ] : state.sections[section].menus) : [ - ...state.sections[section].menus.slice(0, menuIndex), - payload.result.menu, - ...state.sections[section].menus.slice(menuIndex + 1), - ], - page: { - name: payload.params.name, - content: payload.result.page.content, - legacy: true, - params: payload.params.params - }, - pending: false - } - } - }; -}; - -export default renderLegacyPageDoneHandler; \ No newline at end of file diff --git a/src/app/modules/sections/reducers/renderLegacyPageHandler.ts b/src/app/modules/sections/reducers/renderLegacyPageHandler.ts deleted file mode 100644 index cec8bc7b5..000000000 --- a/src/app/modules/sections/reducers/renderLegacyPageHandler.ts +++ /dev/null @@ -1,22 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { State } from '../reducer'; -import { renderLegacyPage } from '../actions'; -import { Reducer } from 'modules'; - -const renderLegacyPageHandler: Reducer = (state, payload) => ({ - ...state, - sections: { - ...state.sections, - [payload.section]: { - ...state.sections[payload.section], - force: false, - pending: true - } - } -}); - -export default renderLegacyPageHandler; \ No newline at end of file diff --git a/src/app/modules/sections/reducers/renderPageDoneHandler.ts b/src/app/modules/sections/reducers/renderPageDoneHandler.ts index 5802dd9cc..79e8995ea 100644 --- a/src/app/modules/sections/reducers/renderPageDoneHandler.ts +++ b/src/app/modules/sections/reducers/renderPageDoneHandler.ts @@ -6,54 +6,31 @@ import { State } from '../reducer'; import { renderPage } from '../actions'; import { Reducer } from 'modules'; -import { TMenu } from 'apla/content'; -const renderPageDoneHandler: Reducer = (state, payload) => { - const section = payload.params.section; - const menuIndex = state.sections[section].menus.findIndex(l => - l.name === payload.result.menu.name); - - let menus: TMenu[] = []; - - if (state.sections[section].menus.length) { - if (-1 === menuIndex) { - menus = [ - ...state.sections[section].menus, - payload.result.menu - ]; - } - else { - menus = [ - ...state.sections[section].menus.slice(0, menuIndex), - payload.result.menu, - ...state.sections[section].menus.slice(menuIndex + 1), - ]; - } - } - else if (payload.result.defaultMenu) { - menus = [ - payload.result.defaultMenu, - payload.result.menu - ]; - } - else { - menus = [ - payload.result.menu - ]; +const renderPageDoneHandler: Reducer = (state, payload): State => { + if (payload.params.popup) { + return state; } return { ...state, sections: { ...state.sections, - [section]: { - ...state.sections[section], - menus, + [payload.params.section]: { + ...state.sections[payload.params.section], page: { - ...payload.result.page, - params: payload.params.params + name: payload.params.name, + status: 'LOADED', + content: payload.result.tree, + static: payload.result.static, + params: payload.params.params, + error: undefined, + location: payload.params.location, }, - pending: false + menus: [{ + name: payload.result.menu, + content: payload.result.menuTree + }] } } }; diff --git a/src/app/modules/sections/reducers/renderPageFailedHandler.ts b/src/app/modules/sections/reducers/renderPageFailedHandler.ts index 028bfa27e..bbb2a4895 100644 --- a/src/app/modules/sections/reducers/renderPageFailedHandler.ts +++ b/src/app/modules/sections/reducers/renderPageFailedHandler.ts @@ -6,22 +6,32 @@ import { State } from '../reducer'; import { renderPage } from '../actions'; import { Reducer } from 'modules'; +import changeBreadcrumbType from '../util/changeBreadcrumbType'; -const renderPageFailedHandler: Reducer = (state, payload) => ({ - ...state, - sections: { - ...state.sections, - [payload.params.section]: { - ...state.sections[payload.params.section], - page: { - params: payload.params.params, - name: payload.params.name, - content: null, - error: payload.error - }, - pending: false - } +const renderPageFailedHandler: Reducer = (state, payload): State => { + if (payload.params.popup) { + return state; } -}); + + return { + ...state, + sections: { + ...state.sections, + [payload.params.section]: { + ...state.sections[payload.params.section], + page: { + name: payload.params.name, + status: 'ERROR', + content: [], + static: false, + params: payload.params.params, + error: payload.error, + location: payload.params.location, + }, + breadcrumbs: changeBreadcrumbType(state.sections[payload.params.section], payload.params.name, 'IGNORE') + } + } + }; +}; export default renderPageFailedHandler; \ No newline at end of file diff --git a/src/app/modules/sections/reducers/renderPageHandler.ts b/src/app/modules/sections/reducers/renderPageHandler.ts index 7f28cf42f..8b8e8a4d4 100644 --- a/src/app/modules/sections/reducers/renderPageHandler.ts +++ b/src/app/modules/sections/reducers/renderPageHandler.ts @@ -6,18 +6,42 @@ import { State } from '../reducer'; import { renderPage } from '../actions'; import { Reducer } from 'modules'; +import upsertSectionBreadcrumb from '../util/upsertSectionBreadcrumb'; -const renderPageHandler: Reducer = (state, payload) => ({ - ...state, - sections: { - ...state.sections, - [payload.section]: { - ...state.sections[payload.section], - force: false, - pending: true, - visible: true - } +const renderPageHandler: Reducer = (state, payload): State => { + if (payload.popup) { + return state; } -}); + + return { + ...state, + sections: { + ...state.sections, + [payload.section]: { + ...state.sections[payload.section], + page: { + name: payload.name, + status: 'PENDING', + content: [], + static: false, + params: payload.params, + error: undefined, + location: payload.location, + }, + breadcrumbs: upsertSectionBreadcrumb( + state.sections[payload.section], + { + caller: payload.location.state && payload.location.state.from && payload.location.state.from.name, + type: (payload.location.state && payload.location.state.from) ? payload.location.state.from.type : 'IGNORE', + title: payload.location.state && payload.location.state.from && payload.location.state.from.title, + section: payload.section, + page: payload.name, + params: payload.params + } + ) + } + } + }; +}; export default renderPageHandler; \ No newline at end of file diff --git a/src/app/modules/sections/reducers/renderSectionHandler.ts b/src/app/modules/sections/reducers/renderSectionHandler.ts deleted file mode 100644 index 8d9310da7..000000000 --- a/src/app/modules/sections/reducers/renderSectionHandler.ts +++ /dev/null @@ -1,17 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { State } from '../reducer'; -import { renderSection } from '../actions'; -import { Reducer } from 'modules'; - -const renderSectionHandler: Reducer = (state, payload) => { - return state.sections[payload] ? { - ...state, - section: payload - } : state; -}; - -export default renderSectionHandler; \ No newline at end of file diff --git a/src/app/modules/sections/reducers/sectionsInitDoneHandler.ts b/src/app/modules/sections/reducers/sectionsInitDoneHandler.ts deleted file mode 100644 index f693df0e3..000000000 --- a/src/app/modules/sections/reducers/sectionsInitDoneHandler.ts +++ /dev/null @@ -1,30 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { State } from '../reducer'; -import { sectionsInit } from '../actions'; -import { Reducer } from 'modules'; -import { TSection } from 'apla/content'; - -const sectionsInitDoneHandler: Reducer = (state, payload) => { - const sections: { [key: string]: TSection } = {}; - payload.result.sections.forEach(section => { - sections[section.name] = section; - }); - - state.systemSections.forEach(section => { - sections[section.name] = section; - }); - - return { - ...state, - inited: true, - mainSection: payload.result.mainSection, - section: payload.result.section, - sections - }; -}; - -export default sectionsInitDoneHandler; \ No newline at end of file diff --git a/src/app/modules/auth/reducers/changeSeedConfirmation.ts b/src/app/modules/sections/reducers/sectionsInitHandler.ts similarity index 62% rename from src/app/modules/auth/reducers/changeSeedConfirmation.ts rename to src/app/modules/sections/reducers/sectionsInitHandler.ts index 6a5ef902e..37ce5ef89 100644 --- a/src/app/modules/auth/reducers/changeSeedConfirmation.ts +++ b/src/app/modules/sections/reducers/sectionsInitHandler.ts @@ -4,12 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { State } from '../reducer'; -import { changeSeed } from '../actions'; +import { sectionsInit } from '../actions'; import { Reducer } from 'modules'; -const changeSeedConfirmationHandler: Reducer = (state, payload) => ({ +const sectionsInitHandler: Reducer = (state, payload): State => ({ ...state, - seedConfirm: payload + mainSection: payload.mainSection, + sections: payload.sections }); -export default changeSeedConfirmationHandler; \ No newline at end of file +export default sectionsInitHandler; \ No newline at end of file diff --git a/src/app/modules/sections/reducers/switchSectionHandler.ts b/src/app/modules/sections/reducers/switchSectionHandler.ts deleted file mode 100644 index d8e937010..000000000 --- a/src/app/modules/sections/reducers/switchSectionHandler.ts +++ /dev/null @@ -1,17 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { State } from '../reducer'; -import { renderSection } from '../actions'; -import { Reducer } from 'modules'; - -const switchSectionHandler: Reducer = (state, payload) => { - return state.sections[payload] ? { - ...state, - section: payload - } : state; -}; - -export default switchSectionHandler; \ No newline at end of file diff --git a/src/app/modules/sections/reducers/updateSectionHandler.ts b/src/app/modules/sections/reducers/updateSectionHandler.ts index ac2fef714..dcf1dfc33 100644 --- a/src/app/modules/sections/reducers/updateSectionHandler.ts +++ b/src/app/modules/sections/reducers/updateSectionHandler.ts @@ -7,7 +7,7 @@ import { State } from '../reducer'; import { updateSection } from '../actions'; import { Reducer } from 'modules'; -const updateSectionHandler: Reducer = (state, payload) => ({ +const updateSectionHandler: Reducer = (state, payload): State => ({ ...state, sections: { ...state.sections, diff --git a/src/app/modules/sections/util/changeBreadcrumbType.ts b/src/app/modules/sections/util/changeBreadcrumbType.ts new file mode 100644 index 000000000..bc5a9a024 --- /dev/null +++ b/src/app/modules/sections/util/changeBreadcrumbType.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ISection, TBreadcrumbType } from 'apla/content'; + +const changeBreadcrumbType = (section: ISection, page: string, type: TBreadcrumbType) => { + return section.breadcrumbs.map(b => { + if (page === b.page) { + return { + ...b, + type + }; + } + else { + return b; + } + }); +}; + +export default changeBreadcrumbType; \ No newline at end of file diff --git a/src/app/modules/sections/util/findBreadcrumb.ts b/src/app/modules/sections/util/findBreadcrumb.ts new file mode 100644 index 000000000..c181b0c9e --- /dev/null +++ b/src/app/modules/sections/util/findBreadcrumb.ts @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ISection, IBreadcrumb } from 'apla/content'; + +const findBreadcrumb = (section: ISection, breadcrumb: IBreadcrumb) => { + const pageIndex = section.breadcrumbs.findIndex(m => + m.page === breadcrumb.page + ); + + if (-1 !== pageIndex) { + return pageIndex; + } + + const callerIndex = section.breadcrumbs.findIndex(m => + m.caller === breadcrumb.caller && m.type === breadcrumb.type + ); + + return callerIndex; +}; + +export default findBreadcrumb; \ No newline at end of file diff --git a/src/app/modules/sections/reducers/resetHandler.ts b/src/app/modules/sections/util/findMenu.ts similarity index 55% rename from src/app/modules/sections/reducers/resetHandler.ts rename to src/app/modules/sections/util/findMenu.ts index 16900d1e2..afd09817d 100644 --- a/src/app/modules/sections/reducers/resetHandler.ts +++ b/src/app/modules/sections/util/findMenu.ts @@ -3,13 +3,9 @@ * See LICENSE in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { State } from '../reducer'; -import { reset } from '../actions'; -import { Reducer } from 'modules'; +import { ISection } from 'apla/content'; -const resetHandler: Reducer = (state, payload) => ({ - ...state, - inited: false -}); +const findMenu = (section: ISection, name: string) => + section.menus.findIndex(m => m.name === name); -export default resetHandler; \ No newline at end of file +export default findMenu; \ No newline at end of file diff --git a/src/app/modules/sections/util/upsertSectionBreadcrumb.ts b/src/app/modules/sections/util/upsertSectionBreadcrumb.ts new file mode 100644 index 000000000..eeae4b3e9 --- /dev/null +++ b/src/app/modules/sections/util/upsertSectionBreadcrumb.ts @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ISection, IBreadcrumb } from 'apla/content'; +import findBreadcrumb from './findBreadcrumb'; + +const upsertSectionBreadcrumb = (section: ISection, crumb: IBreadcrumb) => { + const crumbIndex = findBreadcrumb(section, crumb); + let breadcrumbs: IBreadcrumb[]; + + if (-1 !== crumbIndex) { + const oldValue = section.breadcrumbs[crumbIndex]; + + breadcrumbs = [ + ...section.breadcrumbs.slice(0, crumbIndex).filter(l => 'IGNORE' !== l.type), + { + ...section.breadcrumbs[crumbIndex], + page: crumb.page, + title: crumb.title || oldValue.title, + params: crumb.params + } + ]; + } + else { + breadcrumbs = [ + ...section.breadcrumbs.filter(l => 'IGNORE' !== l.type), + crumb + ]; + } + + return breadcrumbs; +}; + +export default upsertSectionBreadcrumb; \ No newline at end of file diff --git a/src/app/modules/sections/util/upsertSectionMenu.ts b/src/app/modules/sections/util/upsertSectionMenu.ts new file mode 100644 index 000000000..477461857 --- /dev/null +++ b/src/app/modules/sections/util/upsertSectionMenu.ts @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IMenu, ISection } from 'apla/content'; +import findMenu from './findMenu'; + +const upsertSectionMenu = (section: ISection, menu: IMenu): ISection => { + const menuIndex = findMenu(section, menu.name); + let menus: IMenu[]; + + if (-1 !== menuIndex) { + menus = [ + ...section.menus.slice(0, menuIndex), + { + ...section.menus[menuIndex], + ...menu + } + ]; + } + else { + menus = [ + ...section.menus, + menu + ]; + } + + return { + ...section, + menus + }; +}; + +export default upsertSectionMenu; \ No newline at end of file diff --git a/src/app/modules/socket/epics/subscribeEpic.ts b/src/app/modules/socket/epics/subscribeEpic.ts index 28cfc81df..159a5a71d 100644 --- a/src/app/modules/socket/epics/subscribeEpic.ts +++ b/src/app/modules/socket/epics/subscribeEpic.ts @@ -7,8 +7,10 @@ import { Action } from 'redux'; import { Observable } from 'rxjs/Observable'; import { Epic } from 'modules'; import { Observer } from 'rxjs'; -import { setBadgeCount } from 'modules/gui/actions'; import { subscribe, setNotificationsCount } from '../actions'; +import { fetchNotifications } from 'modules/content/actions'; +import findNotificationsCount from '../util/findNotificationsCount'; +import platform from 'lib/platform'; const subscribeEpic: Epic = (action$, store) => action$.ofAction(subscribe.started) .flatMap(action => { @@ -32,6 +34,8 @@ const subscribeEpic: Epic = (action$, store) => action$.ofAction(subscribe.start message.data.forEach(n => { const subState = store.getState(); + const notifications = findNotificationsCount(subState.socket, subState.auth.wallet); + if (subState.auth.isAuthenticated && ( subState.auth.wallet.role && subState.auth.wallet.role.id === n.role_id || @@ -50,9 +54,17 @@ const subscribeEpic: Epic = (action$, store) => action$.ofAction(subscribe.start role: n.role_id, count: n.count })); + + const notificationsNew = findNotificationsCount(subState.socket, subState.auth.wallet); + if (notifications !== notificationsNew) { + observer.next(fetchNotifications.started(undefined)); + } }); - observer.next(setBadgeCount(count)); + platform.on('desktop', () => { + const Electron = require('electron'); + Electron.remote.app.setBadgeCount(count); + }); }); observer.next(subscribe.done({ diff --git a/src/app/modules/socket/util/findNotifications.ts b/src/app/modules/socket/util/findNotifications.ts new file mode 100644 index 000000000..1979655eb --- /dev/null +++ b/src/app/modules/socket/util/findNotifications.ts @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { State } from 'modules/socket'; +import { IAccountContext } from 'apla/auth'; + +const findNotifications = (state: State, session: IAccountContext) => { + if (!session) { + return []; + } + + if (!session.access) { + return []; + } + + if (!session.wallet) { + return []; + } + + return state.notifications.filter(notification => + notification.id === session.wallet.id && + notification.ecosystem === session.access.ecosystem && + (session.role && notification.role === session.role.id || notification.role === '0') + ); +}; + +export default findNotifications; \ No newline at end of file diff --git a/src/app/modules/socket/util/findNotificationsCount.ts b/src/app/modules/socket/util/findNotificationsCount.ts new file mode 100644 index 000000000..1bc57de05 --- /dev/null +++ b/src/app/modules/socket/util/findNotificationsCount.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { State } from 'modules/socket'; +import { IAccountContext } from 'apla/auth'; +import findNotifications from './findNotifications'; + +const findNotificationsCount = (state: State, session: IAccountContext) => { + const notifications = findNotifications(state, session); + + if (!notifications.length) { + return 0; + } + + return notifications + .map(notification => notification.count) + .reduce((a, b) => a + b); +}; + +export default findNotificationsCount; \ No newline at end of file diff --git a/src/app/modules/storage/actions.ts b/src/app/modules/storage/actions.ts index 7a4e2416e..a0f6a4eae 100644 --- a/src/app/modules/storage/actions.ts +++ b/src/app/modules/storage/actions.ts @@ -16,7 +16,7 @@ export const savePreconfiguredNetworks = actionCreator('SAVE_PRECONF export const removeNetwork = actionCreator('REMOVE_NETWORK'); export const saveWallet = actionCreator('SAVE_WALLET'); export const removeWallet = actionCreator('REMOVE_WALLET'); -export const saveNavigationSize = actionCreator('SAVE_NAVIGATION_SIZE'); export const mergeFullNodes = actionCreator<{ uuid: string, fullNodes: string[] }>('MERGE_FULL_NODES'); export const saveEncKey = actionCreator('SAVE_ENC_KEY'); -export const closeSecurityWarning = actionCreator('CLOSE_SECURITY_WARNING'); \ No newline at end of file +export const closeSecurityWarning = actionCreator('CLOSE_SECURITY_WARNING'); +export const setMenuFolded = actionCreator('SET_MENU_FOLDED'); \ No newline at end of file diff --git a/src/app/modules/storage/reducer.ts b/src/app/modules/storage/reducer.ts index 657f603a6..6688a1d53 100644 --- a/src/app/modules/storage/reducer.ts +++ b/src/app/modules/storage/reducer.ts @@ -9,38 +9,38 @@ import { IWallet, INetwork } from 'apla/auth'; import saveLocaleHandler from './reducers/saveLocaleHandler'; import saveWalletHandler from './reducers/saveWalletHandler'; import removeWalletHandler from './reducers/removeWalletHandler'; -import saveNavigationSizeHandler from './reducers/saveNavigationSizeHandler'; import mergeFullNodesHandler from './reducers/mergeFullNodesHandler'; import closeSecurityWarningHandler from './reducers/closeSecurityWarningHandler'; import saveNetworkHandler from './reducers/saveNetworkHandler'; import removeNetworkHandler from './reducers/removeNetworkHandler'; import savePreconfiguredNetworksHandler from './reducers/savePreconfiguredNetworksHandler'; import rehydrateHandler from './reducers/rehydrateHandler'; +import setMenuFoldedHandler from './reducers/setMenuFoldedHandler'; export type State = { readonly locale: string; readonly wallets: IWallet[]; readonly networks: INetwork[]; - readonly navigationSize: number; readonly securityWarningClosed: boolean; + readonly menuFolded: boolean; }; export const initialState: State = { locale: null, wallets: [], networks: [], - navigationSize: 230, - securityWarningClosed: false + securityWarningClosed: false, + menuFolded: false }; export default reducerWithInitialState(initialState) .case(actions.saveLocale, saveLocaleHandler) .case(actions.saveWallet, saveWalletHandler) .case(actions.removeWallet, removeWalletHandler) - .case(actions.saveNavigationSize, saveNavigationSizeHandler) .case(actions.mergeFullNodes, mergeFullNodesHandler) .case(actions.closeSecurityWarning, closeSecurityWarningHandler) .case(actions.saveNetwork, saveNetworkHandler) .case(actions.removeNetwork, removeNetworkHandler) .case(actions.savePreconfiguredNetworks, savePreconfiguredNetworksHandler) - .case(actions.localstorageInit, rehydrateHandler); \ No newline at end of file + .case(actions.localstorageInit, rehydrateHandler) + .case(actions.setMenuFolded, setMenuFoldedHandler); \ No newline at end of file diff --git a/src/app/modules/storage/reducers/saveNavigationSizeHandler.ts b/src/app/modules/storage/reducers/saveNavigationSizeHandler.ts deleted file mode 100644 index beefd301f..000000000 --- a/src/app/modules/storage/reducers/saveNavigationSizeHandler.ts +++ /dev/null @@ -1,18 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { State } from '../reducer'; -import { saveNavigationSize } from '../actions'; -import { Reducer } from 'modules'; - -const saveNavigationSizeHandler: Reducer = (state, payload) => ({ - ...state, - navigationSize: Math.max( - payload, - 200 - ) -}); - -export default saveNavigationSizeHandler; \ No newline at end of file diff --git a/src/app/modules/content/reducers/setResizingHandler.ts b/src/app/modules/storage/reducers/setMenuFoldedHandler.ts similarity index 67% rename from src/app/modules/content/reducers/setResizingHandler.ts rename to src/app/modules/storage/reducers/setMenuFoldedHandler.ts index b112131ee..a7e802b4e 100644 --- a/src/app/modules/content/reducers/setResizingHandler.ts +++ b/src/app/modules/storage/reducers/setMenuFoldedHandler.ts @@ -4,12 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import { State } from '../reducer'; -import { setResizing } from '../actions'; +import { setMenuFolded } from '../actions'; import { Reducer } from 'modules'; -const setResizingHandler: Reducer = (state, payload) => ({ +const setMenuFoldedHandler: Reducer = (state, payload) => ({ ...state, - navigationResizing: payload + menuFolded: payload }); -export default setResizingHandler; \ No newline at end of file +export default setMenuFoldedHandler; \ No newline at end of file diff --git a/src/app/modules/tx/epics/txExecFailedEpic.ts b/src/app/modules/tx/epics/txExecFailedEpic.ts index 8b6e9dc8f..34b9e2897 100644 --- a/src/app/modules/tx/epics/txExecFailedEpic.ts +++ b/src/app/modules/tx/epics/txExecFailedEpic.ts @@ -3,32 +3,27 @@ * See LICENSE in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IRootState } from 'modules'; -import { Epic } from 'redux-observable'; -import { Action } from 'redux'; +import { Epic } from 'modules'; import { txExec } from '../actions'; import { modalShow } from '../../modal/actions'; -import { navigatePage } from '../../sections/actions'; +import { push } from 'connected-react-router'; -export const txExecFailedEpic: Epic = - (action$, store) => action$.ofAction(txExec.failed) - .filter(l => !l.payload.params.silent) - .map(action => { - if (action.payload.error.id && action.payload.params.errorRedirects) { - const errorRedirect = action.payload.params.errorRedirects[action.payload.error.id]; - if (errorRedirect) { - return navigatePage.started({ - name: errorRedirect.pagename, - params: errorRedirect.pageparams, - force: true - }); - } +export const txExecFailedEpic: Epic = (action$, store, { routerService }) => action$.ofAction(txExec.failed) + .filter(l => !l.payload.params.silent) + .map(action => { + if (action.payload.params.section && action.payload.error.id && action.payload.params.errorRedirects) { + const errorRedirect = action.payload.params.errorRedirects[action.payload.error.id]; + if (errorRedirect) { + const route = routerService.routeToBrowser(action.payload.params.section, errorRedirect.pagename, errorRedirect.pageparams); + return push(route); } - return modalShow({ - id: 'TX_ERROR', - type: 'TX_ERROR', - params: action.payload.error - }); + } + + return modalShow({ + id: 'TX_ERROR', + type: 'TX_ERROR', + params: action.payload.error }); + }); export default txExecFailedEpic; \ No newline at end of file diff --git a/src/app/services/network/errors.ts b/src/app/services/network/errors.ts index 41bf49272..d6b3bba18 100644 --- a/src/app/services/network/errors.ts +++ b/src/app/services/network/errors.ts @@ -3,19 +3,6 @@ * See LICENSE in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - enum NetworkError { Offline = 'E_OFFLINE', IDMismatch = 'E_IDMISMATCH', diff --git a/src/app/services/router/index.ts b/src/app/services/router/index.ts new file mode 100644 index 000000000..c8ad7f2e7 --- /dev/null +++ b/src/app/services/router/index.ts @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) EGAAS S.A. All rights reserved. + * See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import Route from 'route-parser'; +import querystring from 'query-string'; + +export interface IRouteMatch { + parts: { + [name: string]: string; + }; + query: { + [param: string]: string; + }; +} + +export const matchRoute = (path: string, match: string): IRouteMatch | undefined => { + const route = new Route(path).match(match); + if (!route) { + return undefined; + } + + return { + parts: route, + query: querystring.parseUrl(match).query + }; +}; + +export const generateRoute = (path: string, params?: { [name: string]: string }) => { + const query = params ? querystring.stringify(params) : ''; + return `${path}${query && '?' + query}`; +}; + +export const routeToBrowser = (section: string, page: string, params?: { [name: string]: string }) => + generateRoute(`/browse/${section}/${page}`, params); \ No newline at end of file diff --git a/src/app/store.ts b/src/app/store.ts index f520a5d4b..d3bce229a 100644 --- a/src/app/store.ts +++ b/src/app/store.ts @@ -8,7 +8,6 @@ import 'lib/external/fsa'; import { createStore, applyMiddleware, compose } from 'redux'; import { connectRouter, routerMiddleware } from 'connected-react-router'; import { createEpicMiddleware } from 'redux-observable'; -import { loadingBarMiddleware } from 'react-redux-loading-bar'; import persistState, { mergePersistedState } from 'redux-localstorage'; import adapter from 'redux-localstorage/lib/adapters/localStorage'; import filter from 'redux-localstorage-filter'; @@ -21,6 +20,7 @@ import rootReducer, { rootEpic, IRootState } from './modules'; import platform from 'lib/platform'; import dependencies from 'modules/dependencies'; import rehydrateHandler from 'modules/storage/reducers/rehydrateHandler'; +import { Observable } from 'rxjs'; export const history = platform.select<() => History>({ desktop: createMemoryHistory, @@ -58,9 +58,6 @@ const configureStore = (initialState?: IRootState) => { routerMiddleware(history), createEpicMiddleware(rootEpic, { dependencies - }), - loadingBarMiddleware({ - promiseTypeSuffixes: ['STARTED', 'DONE', 'FAILED'] }) ]; @@ -111,4 +108,18 @@ const store = platform.select({ } })(); +// This is a stub value for observable store. It will be removed in the near future +const getState$ = (stateStore: typeof store) => + new Observable(observer => { + observer.next(stateStore.getState()); + + const unsubscribe = store.subscribe(() => { + observer.next(stateStore.getState()); + }); + + return unsubscribe; + }); + +export const state$ = getState$(store); + export default store; \ No newline at end of file diff --git a/src/app/styles/index.css b/src/app/styles/index.css index 61bb0ec28..f8e8f1727 100644 --- a/src/app/styles/index.css +++ b/src/app/styles/index.css @@ -115,10 +115,6 @@ body { list-style: none; } -.component-main { - background: #f6f7fa; -} - .content-wrapper { border-top: none; } diff --git a/src/app/styles/scss/fluent.scss b/src/app/styles/scss/fluent.scss index bde357867..f62d7f641 100644 --- a/src/app/styles/scss/fluent.scss +++ b/src/app/styles/scss/fluent.scss @@ -9,8 +9,7 @@ flex-direction: column; > section { - margin-left: 350px; - margin-bottom: 0 !important; + margin: 0 !important; display: flex; flex: 1; flex-direction: column; @@ -151,6 +150,7 @@ textarea.flex-stretch { display: flex; flex: 1; flex-direction: column; + min-height: 0; &.noscroll { overflow: hidden; diff --git a/src/app/test/mockStore.ts b/src/app/test/mockStore.ts index 8c3ca384c..583621811 100644 --- a/src/app/test/mockStore.ts +++ b/src/app/test/mockStore.ts @@ -42,7 +42,6 @@ const state: IRootState = { preloading: true, preloadingError: null, stylesheet: 'body {\n\t\t /* You can define your custom styles here or create custom CSS rules */\n\t\t}', - navigationResizing: false, notifications: [] }, sections: { @@ -754,7 +753,6 @@ const state: IRootState = { storage: { locale: 'en-US', wallets: [], - navigationSize: 230, fullNodes: [ 'http://127.0.0.1:7079' ] @@ -766,7 +764,6 @@ const state: IRootState = { notifications: [], subscriptions: [] }, - loadingBar: 0, router: { location: { pathname: '/editor', diff --git a/src/defs/content.d.ts b/src/defs/content.d.ts index 800536de8..2045abb0f 100644 --- a/src/defs/content.d.ts +++ b/src/defs/content.d.ts @@ -5,32 +5,49 @@ declare module 'apla/content' { import { TProtypoElement } from 'apla/protypo'; + import { Location } from 'history'; - type TMenu = { + interface IMenu { readonly name: string; readonly content: TProtypoElement[]; - }; + } + + type TBreadcrumbType = + 'MENU' | 'PAGE' | 'IGNORE'; + + type TPageParams = { + [key: string]: string; + } + + interface IBreadcrumb { + readonly caller: string; + readonly type: TBreadcrumbType; + readonly section: string; + readonly title: string; + readonly page: string; + readonly params: TPageParams; + } + + type TPageStatus = + 'PENDING' | 'LOADED' | 'ERROR'; - type TPage = { + interface IPage { readonly name: string; - readonly legacy?: boolean; + readonly status: TPageStatus; + readonly static: boolean; readonly content: TProtypoElement[]; - readonly params: { [key: string]: any }; + readonly params: TPageParams; readonly error?: string; - }; - - type TSection = { - readonly key: string; - readonly visible: boolean; - readonly closeable?: boolean; - readonly menuDisabled?: boolean; - readonly menuVisible: boolean; - readonly pending: boolean; + readonly location: Location; + } + + interface ISection { + readonly index: number; readonly name: string; readonly title: string; - readonly force: boolean; readonly defaultPage: string; - readonly menus: TMenu[]; - readonly page: TPage; + readonly breadcrumbs: IBreadcrumb[]; + readonly menus: IMenu[]; + readonly page?: IPage; } } \ No newline at end of file diff --git a/src/defs/editor.d.ts b/src/defs/editor.d.ts index f5aa303b0..3d94815f8 100644 --- a/src/defs/editor.d.ts +++ b/src/defs/editor.d.ts @@ -27,6 +27,7 @@ declare module 'apla/editor' { type TEditorTab = { readonly type: string; + readonly uuid: string; readonly id: string; readonly new: boolean; readonly name: string; @@ -48,16 +49,12 @@ declare module 'apla/editor' { }; interface IEditorTabCreateCall { + uuid: string; id: string; name: string; value: string } - interface ICreateEditorTabCall{ - type: string; - appId: number; - } - interface ILoadEditorTabCall { type: string; name: string; diff --git a/src/defs/gui.d.ts b/src/defs/gui.d.ts index 5fdfe1920..7fb45fbd4 100644 --- a/src/defs/gui.d.ts +++ b/src/defs/gui.d.ts @@ -4,9 +4,6 @@ *--------------------------------------------------------------------------------------------*/ declare module 'apla/gui' { - type TWindowType = - 'general' | 'main'; - interface IInferredArguments { readonly privateKey?: string; readonly fullNode?: string[]; diff --git a/src/defs/modal.d.ts b/src/defs/modal.d.ts index 50b2a2da7..b0500a6bc 100644 --- a/src/defs/modal.d.ts +++ b/src/defs/modal.d.ts @@ -35,15 +35,6 @@ declare module 'apla/modal' { data: any; } - interface IModalPageCall { - name: string; - title?: string; - width?: number; - params: { - [key: string]: any; - }; - } - interface IModal { id: string; type: string; diff --git a/src/defs/protypo.d.ts b/src/defs/protypo.d.ts index 8087920b8..544c06592 100644 --- a/src/defs/protypo.d.ts +++ b/src/defs/protypo.d.ts @@ -1,9 +1,11 @@ /*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ +* Copyright (c) EGAAS S.A. All rights reserved. +* See LICENSE in the project root for license information. +*--------------------------------------------------------------------------------------------*/ declare module 'apla/protypo' { + import { TBreadcrumbType } from 'apla/content'; + type TProtypoElement = { readonly tag: string; readonly id?: string; @@ -38,6 +40,7 @@ declare module 'apla/protypo' { interface IButtonPage { name: string; + section: string; params: { [key: string]: any; }; @@ -50,9 +53,21 @@ declare module 'apla/protypo' { }; } + interface IAction { + name: string; + params?: { + [key: string]: string; + }; + } + interface IButtonInteraction { uuid: string; silent?: boolean; + from: { + name: string; + section: string; + type: TBreadcrumbType; + }; confirm?: IButtonConfirm; contracts: { name: string; @@ -64,6 +79,7 @@ declare module 'apla/protypo' { popup?: IButtonPopup; errorRedirects?: { [key: string]: IErrorRedirect - } + }, + actions: IAction[]; } } \ No newline at end of file diff --git a/src/defs/theme.d.ts b/src/defs/theme.d.ts index 23af09c88..14a020cdc 100644 --- a/src/defs/theme.d.ts +++ b/src/defs/theme.d.ts @@ -6,31 +6,39 @@ declare module 'apla/theme' { interface IThemeDefinition { windowBorder: string; - + headerBackground: string; headerForeground: string; - headerBackgroundActive: string; - headerForegroundActive: string; headerHeight: number; + menubarSize: number; + menubarBackground: string; + menubarBackgroundActive: string; + menubarBackgroundFocused: string; + menubarBackgroundSecondary: string; + menubarForeground: string; + menubarForegroundActive: string; + toolbarBackground: string; + toolbarBackgroundActive: string; + toolbarBackgroundFocused: string; + toolbarForegroundActive: string; + toolbarForegroundPrimary: string; + toolbarForegroundDisabled: string; toolbarForeground: string; - toolbarOutline: string; - toolbarDisabled: string; - toolbarIconColor: string; - toolbarIconHighlight: string; toolbarHeight: number; + toolbarSpacerForeground: string; - progressBarForeground: string; - - menuHeight: number; menuBackground: string; menuForeground: string; menuBackgroundActive: string; + menuBorder: string; menuOutline: string; menuIconColor: string; menuPrimaryForeground: string; menuPrimaryActive: string; + menuSize: number; + menuSizeFolded: number; contentForeground: string; contentBackground: string; @@ -55,16 +63,14 @@ declare module 'apla/theme' { dropdownMenuBackground: string; dropdownMenuForeground: string; dropdownMenuDisabled: string; - dropdownMenuOutline: string; dropdownMenuActive: string; dropdownMenuSeparator: string; dropdownMenuPrimary: string; dropdownMenuSecondary: string; - systemButtonSecondary: string; - systemButtonActive: string; - securityWarningBackground: string; securityWarningForeground: string; + + uiBorderLight: string; } } \ No newline at end of file diff --git a/src/defs/tx.d.ts b/src/defs/tx.d.ts index 3fd7fa19f..237b17551 100644 --- a/src/defs/tx.d.ts +++ b/src/defs/tx.d.ts @@ -61,6 +61,7 @@ declare module 'apla/tx' { interface ITransactionCall { uuid: string; silent?: boolean; + section?: string; contracts: { name: string; params: { @@ -68,7 +69,7 @@ declare module 'apla/tx' { }[]; }[]; errorRedirects?: { - [key: string]: IErrorRedirect + [key: string]: IErrorRedirect; } } diff --git a/src/electron/index.ts b/src/electron/index.ts index bcdec43db..791916ee2 100644 --- a/src/electron/index.ts +++ b/src/electron/index.ts @@ -7,15 +7,10 @@ require('module').globalPaths.push(__dirname); import { app } from 'electron'; import { spawnWindow, window } from './windows/index'; -import { state } from './ipc'; +import './ipc'; app.on('ready', () => { - if (state && state.auth.isAuthenticated) { - spawnWindow('main'); - } - else { - spawnWindow('general'); - } + spawnWindow(); }); app.on('window-all-closed', () => { @@ -26,11 +21,6 @@ app.on('window-all-closed', () => { app.on('activate', () => { if (null === window) { - if (state && state.auth.isAuthenticated) { - spawnWindow('main'); - } - else { - spawnWindow('general'); - } + spawnWindow(); } }); \ No newline at end of file diff --git a/src/electron/ipc.ts b/src/electron/ipc.ts index 23323914b..148237f84 100644 --- a/src/electron/ipc.ts +++ b/src/electron/ipc.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { ipcMain, Event } from 'electron'; -import { spawnWindow } from './windows/index'; import config from './config'; import args from './args'; import * as _ from 'lodash'; @@ -12,6 +11,30 @@ import * as _ from 'lodash'; export let state: any = null; let saveState = () => null as any; +const truncateState = (value: any) => { + if (!value) { + return value; + } + + const storage = value.storage || {}; + const engine = value.engine || {}; + const auth = value.auth || {}; + + return { + storage, + engine: { + guestSession: engine.guestSession, + }, + auth: { + isAuthenticated: auth.isAuthenticated, + isDefaultWallet: auth.isDefaultWallet, + session: auth.session, + id: auth.id, + wallet: auth.wallet + } + }; +}; + if (!args.dry) { try { state = JSON.parse(config.get('persistentData')); @@ -21,19 +44,7 @@ if (!args.dry) { } saveState = _.throttle(() => { - config.set('persistentData', JSON.stringify({ - storage: state.storage, - engine: { - guestSession: state.engine.guestSession, - }, - auth: { - isAuthenticated: state.auth.isAuthenticated, - isDefaultWallet: state.auth.isDefaultWallet, - session: state.auth.session, - id: state.auth.id, - wallet: state.auth.wallet - } - })); + config.set('persistentData', JSON.stringify(truncateState(state))); }, 1000, { leading: true }); } @@ -43,12 +54,7 @@ ipcMain.on('setState', (e: Event, updatedState: any) => { }); ipcMain.on('getState', (e: Event) => { - e.returnValue = state; -}); - -ipcMain.on('switchWindow', (e: Event, wnd: string) => { - e.returnValue = null; - spawnWindow(wnd); + e.returnValue = truncateState(state); }); ipcMain.on('getArgs', (e: Event) => { diff --git a/src/electron/windows/general.ts b/src/electron/windows/general.ts deleted file mode 100644 index 0770c8878..000000000 --- a/src/electron/windows/general.ts +++ /dev/null @@ -1,17 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) EGAAS S.A. All rights reserved. - * See LICENSE in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { BrowserWindow } from 'electron'; -import calcScreenOffset from '../util/calcScreenOffset'; - -export default () => { - return new BrowserWindow({ - frame: false, - backgroundColor: '#3d2c77', - resizable: false, - show: false, - ...calcScreenOffset({ width: 640, height: 485 }) - }); -}; \ No newline at end of file diff --git a/src/electron/windows/index.ts b/src/electron/windows/index.ts index 37a75dabd..eec7fb21f 100644 --- a/src/electron/windows/index.ts +++ b/src/electron/windows/index.ts @@ -7,11 +7,9 @@ import { BrowserWindow, Menu } from 'electron'; import * as path from 'path'; import * as url from 'url'; import menu from '../menu'; -import generalWindow from './general'; import mainWindow from './main'; export let window: BrowserWindow; -export let windowName: string; const ENV = process.env.NODE_ENV || 'production'; const PROTOCOL = process.env.HTTPS === 'true' ? 'https' : 'http'; @@ -27,24 +25,8 @@ export const appUrl = slashes: true, }); -export const spawnWindow = (name: string) => { - if (window && windowName === name) { - return; - } - - let wnd: BrowserWindow; - switch (name) { - case 'general': - wnd = generalWindow(); - break; - - case 'main': - wnd = mainWindow(); - break; - - default: - return; - } +export const spawnWindow = () => { + let wnd: BrowserWindow = mainWindow(); if (window) { window.close(); @@ -57,15 +39,10 @@ export const spawnWindow = (name: string) => { } wnd.loadURL(appUrl); - wnd.once('ready-to-show', () => { - wnd.show(); - }); wnd.on('closed', () => { window = null; - windowName = null; }); window = wnd; - windowName = name; }; \ No newline at end of file diff --git a/src/electron/windows/main.ts b/src/electron/windows/main.ts index 5c6e1e505..f47be6ff8 100644 --- a/src/electron/windows/main.ts +++ b/src/electron/windows/main.ts @@ -3,7 +3,7 @@ * See LICENSE in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BrowserWindow } from 'electron'; +import { BrowserWindow, shell } from 'electron'; import config from '../config'; import calcScreenOffset from '../util/calcScreenOffset'; @@ -12,7 +12,7 @@ export default () => { minWidth: 800, minHeight: 600, frame: false, - backgroundColor: '#3d2c77', + backgroundColor: '#272D44', resizable: true, show: false, maximized: config.get('maximized') || false, @@ -21,10 +21,19 @@ export default () => { const window = new BrowserWindow(options); + window.once('ready-to-show', () => { + window.show(); + }); + window.on('close', () => { config.set('dimensions', window.getBounds()); config.set('maximized', window.isMaximized() || window.isMaximized); }); + window.webContents.on('new-window', (event, url) => { + event.preventDefault(); + shell.openExternal(url); + }); + return window; }; \ No newline at end of file diff --git a/tsconfig.react.json b/tsconfig.react.json index bb90f655b..e32c91f94 100644 --- a/tsconfig.react.json +++ b/tsconfig.react.json @@ -8,6 +8,7 @@ "webpack", "jest", "src/app/setupTests.ts", + "**/*.test.ts", "src/electron" ] } \ No newline at end of file diff --git a/tslint.json b/tslint.json index 967c41e5f..ea4869410 100644 --- a/tslint.json +++ b/tslint.json @@ -18,8 +18,7 @@ 4 ], "interface-name": [ - true, - "always-prefix" + false ], "jsdoc-format": true, "jsx-no-lambda": false, diff --git a/yarn.lock b/yarn.lock index 254468718..a95619c83 100644 --- a/yarn.lock +++ b/yarn.lock @@ -354,6 +354,11 @@ dependencies: redux "^3.6.0" +"@types/route-parser@^0.1.3": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@types/route-parser/-/route-parser-0.1.3.tgz#f8af16886ebe0b525879628c04f81433ac617af0" + integrity sha512-1AQYpsMbxangSnApsyIHzck5TP8cfas8fzmemljLi2APssJvlZiHkTar/ZtcZwOtK/Ory/xwLg2X8dwhkbnM+g== + "@types/rx-core-binding@*": version "4.0.4" resolved "https://registry.yarnpkg.com/@types/rx-core-binding/-/rx-core-binding-4.0.4.tgz#d969d32f15a62b89e2862c17b3ee78fe329818d3" @@ -9135,13 +9140,6 @@ react-proxy@^3.0.0-alpha.0: dependencies: lodash "^4.6.1" -react-redux-loading-bar@^2.9.3: - version "2.9.3" - resolved "https://registry.yarnpkg.com/react-redux-loading-bar/-/react-redux-loading-bar-2.9.3.tgz#65865dddcbf597169e787edec15eec7ebfb84149" - integrity sha1-ZYZd3cv1lxaeeH7ewV7sfr+4QUk= - dependencies: - prop-types "^15.5.6" - react-redux@^5.0.6: version "5.0.6" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.6.tgz#23ed3a4f986359d68b5212eaaa681e60d6574946" @@ -9976,6 +9974,11 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^2.0.0" inherits "^2.0.1" +route-parser@^0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/route-parser/-/route-parser-0.0.5.tgz#7d1d09d335e49094031ea16991a4a79b01bbe1f4" + integrity sha1-fR0J0zXkkJQDHqFpkaSnmwG74fQ= + run-async@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0"