diff --git a/.eslintrc.js b/.eslintrc.js index ca9d164..ac7f796 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,174 +1,168 @@ module.exports = { - parser: '@typescript-eslint/parser', - parserOptions: { - ecmaVersion: 2018, - sourceType: 'module', - }, - - extends: [ - 'eslint:recommended', - 'plugin:jsdoc/recommended', - 'plugin:@typescript-eslint/eslint-recommended', - 'plugin:@typescript-eslint/recommended', - 'prettier/@typescript-eslint', - 'plugin:prettier/recommended', - ], - - plugins: ['react'], - - env: { - browser: true, - es6: true, - commonjs: true, - }, - - overrides: [ - { - files: ['packages/webpack-utils/**', 'packages/scripts/**', 'jest/**'], - env: { - node: true, - }, + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaVersion: 2018, + sourceType: 'module', }, - { - files: ['*.test.js'], - env: { - jest: true, - }, + + extends: [ + 'eslint:recommended', + 'plugin:jsdoc/recommended', + 'plugin:@typescript-eslint/eslint-recommended', + 'plugin:@typescript-eslint/recommended', + 'prettier/@typescript-eslint', + 'plugin:prettier/recommended', + ], + + plugins: ['react'], + + env: { + browser: true, + es6: true, + commonjs: true, }, - { - files: ['tests-e2e/**'], - env: { - mocha: true, - }, - globals: { - cy: 'readonly', - Cypress: 'readonly', - }, - rules: { - 'no-restricted-globals': 'off', - }, + + overrides: [ + { + files: ['packages/webpack-utils/**', 'packages/scripts/**', 'jest/**'], + env: { + node: true, + }, + }, + { + files: ['*.test.js'], + env: { + jest: true, + }, + }, + { + files: ['tests-e2e/**'], + env: { + mocha: true, + }, + globals: { + cy: 'readonly', + Cypress: 'readonly', + }, + rules: { + 'no-restricted-globals': 'off', + }, + }, + ], + + settings: { + react: { + version: 'detect', + }, }, - ], - settings: { - react: { - version: 'detect', + // @see: http://eslint.org/docs/rules/ + rules: { + 'no-prototype-builtins': 'warn', // temporary set to warn + 'no-restricted-globals': [ + 'error', + 'localStorage', + 'sessionStorage', // we have our own localStorage module + 'event', + ], + 'id-length': ['error', { min: 2, exceptions: ['x', 'y', 'i', 'k', 'l', 'm', 'n', '$', '_'] }], + 'guard-for-in': ['error'], + 'no-var': ['error'], + 'prefer-const': ['error'], + 'prefer-template': ['error'], + 'no-template-curly-in-string': ['error'], + 'no-multi-assign': ['error'], + eqeqeq: ['error'], + 'prefer-rest-params': ['error'], + 'prefer-object-spread': 'warn', + 'prefer-destructuring': 'warn', + 'no-bitwise': 'warn', + 'no-negated-condition': 'warn', + 'no-nested-ternary': 'warn', + 'no-unneeded-ternary': 'warn', + 'no-shadow': 'warn', + 'no-else-return': 'warn', + radix: 'warn', + 'prefer-promise-reject-errors': 'warn', + 'object-shorthand': 'warn', + 'require-atomic-updates': 'off', + + // force extra lines around if, else, for, while, switch, return etc + 'padding-line-between-statements': [ + 'error', + { + blankLine: 'always', + prev: '*', + next: ['if', 'for', 'while', 'switch', 'return'], + }, + { + blankLine: 'always', + prev: ['if', 'for', 'while', 'switch', 'return'], + next: '*', + }, + { + blankLine: 'never', + prev: ['if', 'for', 'while', 'switch', 'return'], + next: 'break', + }, + ], + + 'jsdoc/require-param-description': 'off', + 'jsdoc/require-returns-description': 'off', + 'jsdoc/require-jsdoc': 'off', + 'jsdoc/valid-types': 'off', + 'jsdoc/no-undefined-types': 'off', + 'jsdoc/require-returns': 'off', + + // react + 'react/display-name': 'off', + 'react/react-in-jsx-scope': 'warn', + 'react/forbid-prop-types': 'warn', + 'react/jsx-boolean-value': 'warn', + 'react/jsx-closing-bracket-location': 'off', // can not configure for our code style + 'react/jsx-curly-spacing': 'warn', + 'react/jsx-handler-names': ['warn', { eventHandlerPrefix: 'on', eventHandlerPropPrefix: 'on' }], + 'react/jsx-key': 'warn', + 'react/jsx-max-props-per-line': 'off', + 'react/jsx-no-bind': 'off', + 'react/jsx-no-duplicate-props': 'warn', + 'react/jsx-no-literals': 'off', + 'react/jsx-no-undef': 'error', + 'react/jsx-pascal-case': 'warn', + 'react/jsx-uses-react': 'warn', + 'react/jsx-uses-vars': 'warn', + 'react/jsx-no-comment-textnodes': 'warn', + 'react/jsx-wrap-multilines': 'warn', + 'react/no-deprecated': 'error', + 'react/no-did-mount-set-state': 'warn', + 'react/no-did-update-set-state': 'warn', + 'react/no-direct-mutation-state': 'warn', + 'react/require-render-return': 'warn', + 'react/no-is-mounted': 'warn', + 'react/no-multi-comp': 'off', + 'react/no-string-refs': 'warn', + 'react/no-unknown-property': 'warn', + 'react/prefer-es6-class': 'warn', + 'react/prop-types': 'off', // using ts for this task + 'react/self-closing-comp': 'warn', + 'react/sort-comp': 'off', + + // ts + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-use-before-define': 'off', + '@typescript-eslint/ban-ts-ignore': 'off', + '@typescript-eslint/ban-ts-comment': 'off', + '@typescript-eslint/ban-types': 'off', + '@typescript-eslint/no-empty-function': 'off', + '@typescript-eslint/no-inferrable-types': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + vars: 'all', + args: 'after-used', + argsIgnorePattern: '^_', + }, + ], }, - }, - - // @see: http://eslint.org/docs/rules/ - rules: { - 'no-prototype-builtins': 'warn', // temporary set to warn - 'no-restricted-globals': [ - 'error', - 'localStorage', - 'sessionStorage', // we have our own localStorage module - 'event', - ], - 'id-length': [ - 'error', - { min: 2, exceptions: ['x', 'y', 'i', 'k', 'l', 'm', 'n', '$', '_'] }, - ], - 'guard-for-in': ['error'], - 'no-var': ['error'], - 'prefer-const': ['error'], - 'prefer-template': ['error'], - 'no-template-curly-in-string': ['error'], - 'no-multi-assign': ['error'], - eqeqeq: ['error'], - 'prefer-rest-params': ['error'], - 'prefer-object-spread': 'warn', - 'prefer-destructuring': 'warn', - 'no-bitwise': 'warn', - 'no-negated-condition': 'warn', - 'no-nested-ternary': 'warn', - 'no-unneeded-ternary': 'warn', - 'no-shadow': 'warn', - 'no-else-return': 'warn', - radix: 'warn', - 'prefer-promise-reject-errors': 'warn', - 'object-shorthand': 'warn', - 'require-atomic-updates': 'off', - - // force extra lines around if, else, for, while, switch, return etc - 'padding-line-between-statements': [ - 'error', - { - blankLine: 'always', - prev: '*', - next: ['if', 'for', 'while', 'switch', 'return'], - }, - { - blankLine: 'always', - prev: ['if', 'for', 'while', 'switch', 'return'], - next: '*', - }, - { - blankLine: 'never', - prev: ['if', 'for', 'while', 'switch', 'return'], - next: 'break', - }, - ], - - 'jsdoc/require-param-description': 'off', - 'jsdoc/require-returns-description': 'off', - 'jsdoc/require-jsdoc': 'off', - 'jsdoc/valid-types': 'off', - 'jsdoc/no-undefined-types': 'off', - 'jsdoc/require-returns': 'off', - - // react - 'react/display-name': 'off', - 'react/react-in-jsx-scope': 'warn', - 'react/forbid-prop-types': 'warn', - 'react/jsx-boolean-value': 'warn', - 'react/jsx-closing-bracket-location': 'off', // can not configure for our code style - 'react/jsx-curly-spacing': 'warn', - 'react/jsx-handler-names': [ - 'warn', - { eventHandlerPrefix: 'on', eventHandlerPropPrefix: 'on' }, - ], - 'react/jsx-key': 'warn', - 'react/jsx-max-props-per-line': 'off', - 'react/jsx-no-bind': 'off', - 'react/jsx-no-duplicate-props': 'warn', - 'react/jsx-no-literals': 'off', - 'react/jsx-no-undef': 'error', - 'react/jsx-pascal-case': 'warn', - 'react/jsx-uses-react': 'warn', - 'react/jsx-uses-vars': 'warn', - 'react/jsx-no-comment-textnodes': 'warn', - 'react/jsx-wrap-multilines': 'warn', - 'react/no-deprecated': 'error', - 'react/no-did-mount-set-state': 'warn', - 'react/no-did-update-set-state': 'warn', - 'react/no-direct-mutation-state': 'warn', - 'react/require-render-return': 'warn', - 'react/no-is-mounted': 'warn', - 'react/no-multi-comp': 'off', - 'react/no-string-refs': 'warn', - 'react/no-unknown-property': 'warn', - 'react/prefer-es6-class': 'warn', - 'react/prop-types': 'off', // using ts for this task - 'react/self-closing-comp': 'warn', - 'react/sort-comp': 'off', - - // ts - '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-use-before-define': 'off', - '@typescript-eslint/ban-ts-ignore': 'off', - '@typescript-eslint/ban-ts-comment': 'off', - '@typescript-eslint/ban-types': 'off', - '@typescript-eslint/no-empty-function': 'off', - '@typescript-eslint/no-inferrable-types': 'off', - '@typescript-eslint/no-unused-vars': [ - 'error', - { - vars: 'all', - args: 'after-used', - argsIgnorePattern: '^_', - }, - ], - }, }; diff --git a/.prettierrc b/.prettierrc index 009dddb..26ffcf3 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,6 +1,8 @@ { - "trailingComma": "all", - "singleQuote": true, - "proseWrap": "always", - "endOfLine": "lf" + "trailingComma": "all", + "singleQuote": true, + "proseWrap": "always", + "endOfLine": "lf", + "tabWidth": 4, + "printWidth": 120 } diff --git a/.storybook/config.tsx b/.storybook/config.tsx index 85daf2f..830a5ec 100644 --- a/.storybook/config.tsx +++ b/.storybook/config.tsx @@ -5,7 +5,7 @@ import storyDecorator from './storyDecorator'; const req = require.context('../packages/app', true, /\.story\.[tj]sx?$/); function loadStories() { - req.keys().forEach((filename) => req(filename)); + req.keys().forEach((filename) => req(filename)); } addDecorator(storyDecorator); diff --git a/.storybook/decorators/IntlDecorator.tsx b/.storybook/decorators/IntlDecorator.tsx index f79b855..36a3675 100644 --- a/.storybook/decorators/IntlDecorator.tsx +++ b/.storybook/decorators/IntlDecorator.tsx @@ -2,40 +2,37 @@ import React from 'react'; import { useDispatch } from 'react-redux'; import { Channel } from '@storybook/channels'; import { setIntlConfig } from 'storybook-addon-intl'; -import { - EVENT_SET_LOCALE_ID, - EVENT_GET_LOCALE_ID, -} from 'storybook-addon-intl/dist/shared'; +import { EVENT_SET_LOCALE_ID, EVENT_GET_LOCALE_ID } from 'storybook-addon-intl/dist/shared'; import { SUPPORTED_LANGUAGES, DEFAULT_LANGUAGE } from 'app/services/i18n'; import { setLocale } from 'app/components/i18n/actions'; setIntlConfig({ - locales: SUPPORTED_LANGUAGES, - defaultLocale: DEFAULT_LANGUAGE, + locales: SUPPORTED_LANGUAGES, + defaultLocale: DEFAULT_LANGUAGE, }); const IntlDecorator: React.ComponentType<{ - channel: Channel; + channel: Channel; }> = ({ channel, children }) => { - const dispatch = useDispatch(); + const dispatch = useDispatch(); - React.useEffect(() => { - const onLocaleChange = (locale: string) => { - dispatch(setLocale(locale)); - }; + React.useEffect(() => { + const onLocaleChange = (locale: string) => { + dispatch(setLocale(locale)); + }; - // Listen for change of locale - channel.on(EVENT_SET_LOCALE_ID, onLocaleChange); + // Listen for change of locale + channel.on(EVENT_SET_LOCALE_ID, onLocaleChange); - // Request the current locale - channel.emit(EVENT_GET_LOCALE_ID); + // Request the current locale + channel.emit(EVENT_GET_LOCALE_ID); - return () => { - channel.removeListener(EVENT_SET_LOCALE_ID, onLocaleChange); - }; - }, [channel]); + return () => { + channel.removeListener(EVENT_SET_LOCALE_ID, onLocaleChange); + }; + }, [channel]); - return children as React.ReactElement; + return children as React.ReactElement; }; export default IntlDecorator; diff --git a/.storybook/storyDecorator.tsx b/.storybook/storyDecorator.tsx index 5431c6a..c683e8b 100644 --- a/.storybook/storyDecorator.tsx +++ b/.storybook/storyDecorator.tsx @@ -10,11 +10,11 @@ import { IntlDecorator } from './decorators'; const store = storeFactory(); export default ((story) => { - const channel = addons.getChannel(); + const channel = addons.getChannel(); - return ( - - {story()} - - ); + return ( + + {story()} + + ); }) as DecoratorFunction; diff --git a/.storybook/webpack.config.js b/.storybook/webpack.config.js index 0e93f7b..8fa7048 100644 --- a/.storybook/webpack.config.js +++ b/.storybook/webpack.config.js @@ -1,14 +1,14 @@ const rootConfig = require('../webpack.config'); module.exports = async ({ config }) => ({ - ...config, - resolve: rootConfig.resolve, - module: { - ...config.module, - // our rules should satisfy all storybook needs, - // so replace all storybook defaults with our rules - rules: rootConfig.module.rules, - }, + ...config, + resolve: rootConfig.resolve, + module: { + ...config.module, + // our rules should satisfy all storybook needs, + // so replace all storybook defaults with our rules + rules: rootConfig.module.rules, + }, - resolveLoader: rootConfig.resolveLoader, + resolveLoader: rootConfig.resolveLoader, }); diff --git a/.vscode/settings.json b/.vscode/settings.json index 25fa621..72446f4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,3 @@ { - "typescript.tsdk": "node_modules/typescript/lib" + "typescript.tsdk": "node_modules/typescript/lib" } diff --git a/@types/chalk.d.ts b/@types/chalk.d.ts index b12930f..4d5d419 100644 --- a/@types/chalk.d.ts +++ b/@types/chalk.d.ts @@ -4,141 +4,141 @@ */ declare module 'chalk' { - const enum LevelEnum { - /** + const enum LevelEnum { + /** All colors disabled. */ - None = 0, + None = 0, - /** + /** Basic 16 colors support. */ - Basic = 1, + Basic = 1, - /** + /** ANSI 256 colors support. */ - Ansi256 = 2, + Ansi256 = 2, - /** + /** Truecolor 16 million colors support. */ - TrueColor = 3, - } + TrueColor = 3, + } - /** + /** Basic foreground colors. [More colors here.](https://github.com/chalk/chalk/blob/master/readme.md#256-and-truecolor-color-support) */ - type ForegroundColor = - | 'black' - | 'red' - | 'green' - | 'yellow' - | 'blue' - | 'magenta' - | 'cyan' - | 'white' - | 'gray' - | 'grey' - | 'blackBright' - | 'redBright' - | 'greenBright' - | 'yellowBright' - | 'blueBright' - | 'magentaBright' - | 'cyanBright' - | 'whiteBright'; + type ForegroundColor = + | 'black' + | 'red' + | 'green' + | 'yellow' + | 'blue' + | 'magenta' + | 'cyan' + | 'white' + | 'gray' + | 'grey' + | 'blackBright' + | 'redBright' + | 'greenBright' + | 'yellowBright' + | 'blueBright' + | 'magentaBright' + | 'cyanBright' + | 'whiteBright'; - /** + /** Basic background colors. [More colors here.](https://github.com/chalk/chalk/blob/master/readme.md#256-and-truecolor-color-support) */ - type BackgroundColor = - | 'bgBlack' - | 'bgRed' - | 'bgGreen' - | 'bgYellow' - | 'bgBlue' - | 'bgMagenta' - | 'bgCyan' - | 'bgWhite' - | 'bgGray' - | 'bgGrey' - | 'bgBlackBright' - | 'bgRedBright' - | 'bgGreenBright' - | 'bgYellowBright' - | 'bgBlueBright' - | 'bgMagentaBright' - | 'bgCyanBright' - | 'bgWhiteBright'; + type BackgroundColor = + | 'bgBlack' + | 'bgRed' + | 'bgGreen' + | 'bgYellow' + | 'bgBlue' + | 'bgMagenta' + | 'bgCyan' + | 'bgWhite' + | 'bgGray' + | 'bgGrey' + | 'bgBlackBright' + | 'bgRedBright' + | 'bgGreenBright' + | 'bgYellowBright' + | 'bgBlueBright' + | 'bgMagentaBright' + | 'bgCyanBright' + | 'bgWhiteBright'; - /** + /** Basic colors. [More colors here.](https://github.com/chalk/chalk/blob/master/readme.md#256-and-truecolor-color-support) */ - type Color = ForegroundColor | BackgroundColor; + type Color = ForegroundColor | BackgroundColor; - type Modifiers = - | 'reset' - | 'bold' - | 'dim' - | 'italic' - | 'underline' - | 'inverse' - | 'hidden' - | 'strikethrough' - | 'visible'; + type Modifiers = + | 'reset' + | 'bold' + | 'dim' + | 'italic' + | 'underline' + | 'inverse' + | 'hidden' + | 'strikethrough' + | 'visible'; - namespace chalk { - type Level = LevelEnum; + namespace chalk { + type Level = LevelEnum; - interface Options { - /** + interface Options { + /** Specify the color support for Chalk. By default, color support is automatically detected based on the environment. */ - level?: Level; - } + level?: Level; + } - interface Instance { - /** + interface Instance { + /** Return a new Chalk instance. */ - new (options?: Options): Chalk; - } + new (options?: Options): Chalk; + } - /** + /** Detect whether the terminal supports color. */ - interface ColorSupport { - /** + interface ColorSupport { + /** The color level used by Chalk. */ - level: Level; + level: Level; - /** + /** Return whether Chalk supports basic 16 colors. */ - hasBasic: boolean; + hasBasic: boolean; - /** + /** Return whether Chalk supports ANSI 256 colors. */ - has256: boolean; + has256: boolean; - /** + /** Return whether Chalk supports Truecolor 16 million colors. */ - has16m: boolean; - } + has16m: boolean; + } - interface ChalkFunction { - /** + interface ChalkFunction { + /** Use a template string. @remarks Template literals are unsupported for nested calls (see [issue #341](https://github.com/chalk/chalk/issues/341)) @@ -154,24 +154,24 @@ declare module 'chalk' { `); ``` */ - (text: TemplateStringsArray, ...placeholders: unknown[]): string; + (text: TemplateStringsArray, ...placeholders: unknown[]): string; - (...text: unknown[]): string; - } + (...text: unknown[]): string; + } - interface Chalk extends ChalkFunction { - /** + interface Chalk extends ChalkFunction { + /** Return a new Chalk instance. */ - Instance: Instance; + Instance: Instance; - /** + /** The color support for Chalk. By default, color support is automatically detected based on the environment. */ - level: Level; + level: Level; - /** + /** Use HEX value to set text color. @param color - Hexadecimal value representing the desired color. @@ -183,9 +183,9 @@ declare module 'chalk' { chalk.hex('#DEADED'); ``` */ - hex(color: string): Chalk; + hex(color: string): Chalk; - /** + /** Use keyword color value to set text color. @param color - Keyword value representing the desired color. @@ -197,42 +197,42 @@ declare module 'chalk' { chalk.keyword('orange'); ``` */ - keyword(color: string): Chalk; + keyword(color: string): Chalk; - /** + /** Use RGB values to set text color. */ - rgb(red: number, green: number, blue: number): Chalk; + rgb(red: number, green: number, blue: number): Chalk; - /** + /** Use HSL values to set text color. */ - hsl(hue: number, saturation: number, lightness: number): Chalk; + hsl(hue: number, saturation: number, lightness: number): Chalk; - /** + /** Use HSV values to set text color. */ - hsv(hue: number, saturation: number, value: number): Chalk; + hsv(hue: number, saturation: number, value: number): Chalk; - /** + /** Use HWB values to set text color. */ - hwb(hue: number, whiteness: number, blackness: number): Chalk; + hwb(hue: number, whiteness: number, blackness: number): Chalk; - /** + /** Use a [Select/Set Graphic Rendition](https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters) (SGR) [color code number](https://en.wikipedia.org/wiki/ANSI_escape_code#3/4_bit) to set text color. 30 <= code && code < 38 || 90 <= code && code < 98 For example, 31 for red, 91 for redBright. */ - ansi(code: number): Chalk; + ansi(code: number): Chalk; - /** + /** Use a [8-bit unsigned number](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit) to set text color. */ - ansi256(index: number): Chalk; + ansi256(index: number): Chalk; - /** + /** Use HEX value to set background color. @param color - Hexadecimal value representing the desired color. @@ -244,9 +244,9 @@ declare module 'chalk' { chalk.bgHex('#DEADED'); ``` */ - bgHex(color: string): Chalk; + bgHex(color: string): Chalk; - /** + /** Use keyword color value to set background color. @param color - Keyword value representing the desired color. @@ -258,162 +258,162 @@ declare module 'chalk' { chalk.bgKeyword('orange'); ``` */ - bgKeyword(color: string): Chalk; + bgKeyword(color: string): Chalk; - /** + /** Use RGB values to set background color. */ - bgRgb(red: number, green: number, blue: number): Chalk; + bgRgb(red: number, green: number, blue: number): Chalk; - /** + /** Use HSL values to set background color. */ - bgHsl(hue: number, saturation: number, lightness: number): Chalk; + bgHsl(hue: number, saturation: number, lightness: number): Chalk; - /** + /** Use HSV values to set background color. */ - bgHsv(hue: number, saturation: number, value: number): Chalk; + bgHsv(hue: number, saturation: number, value: number): Chalk; - /** + /** Use HWB values to set background color. */ - bgHwb(hue: number, whiteness: number, blackness: number): Chalk; + bgHwb(hue: number, whiteness: number, blackness: number): Chalk; - /** + /** Use a [Select/Set Graphic Rendition](https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters) (SGR) [color code number](https://en.wikipedia.org/wiki/ANSI_escape_code#3/4_bit) to set background color. 30 <= code && code < 38 || 90 <= code && code < 98 For example, 31 for red, 91 for redBright. Use the foreground code, not the background code (for example, not 41, nor 101). */ - bgAnsi(code: number): Chalk; + bgAnsi(code: number): Chalk; - /** + /** Use a [8-bit unsigned number](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit) to set background color. */ - bgAnsi256(index: number): Chalk; + bgAnsi256(index: number): Chalk; - /** + /** Modifier: Resets the current color chain. */ - readonly reset: Chalk; + readonly reset: Chalk; - /** + /** Modifier: Make text bold. */ - readonly bold: Chalk; + readonly bold: Chalk; - /** + /** Modifier: Emitting only a small amount of light. */ - readonly dim: Chalk; + readonly dim: Chalk; - /** + /** Modifier: Make text italic. (Not widely supported) */ - readonly italic: Chalk; + readonly italic: Chalk; - /** + /** Modifier: Make text underline. (Not widely supported) */ - readonly underline: Chalk; + readonly underline: Chalk; - /** + /** Modifier: Inverse background and foreground colors. */ - readonly inverse: Chalk; + readonly inverse: Chalk; - /** + /** Modifier: Prints the text, but makes it invisible. */ - readonly hidden: Chalk; + readonly hidden: Chalk; - /** + /** Modifier: Puts a horizontal line through the center of the text. (Not widely supported) */ - readonly strikethrough: Chalk; + readonly strikethrough: Chalk; - /** + /** Modifier: Prints the text only when Chalk has a color support level > 0. Can be useful for things that are purely cosmetic. */ - readonly visible: Chalk; + readonly visible: Chalk; - readonly black: Chalk; - readonly red: Chalk; - readonly green: Chalk; - readonly yellow: Chalk; - readonly blue: Chalk; - readonly magenta: Chalk; - readonly cyan: Chalk; - readonly white: Chalk; + readonly black: Chalk; + readonly red: Chalk; + readonly green: Chalk; + readonly yellow: Chalk; + readonly blue: Chalk; + readonly magenta: Chalk; + readonly cyan: Chalk; + readonly white: Chalk; - /* + /* Alias for `blackBright`. */ - readonly gray: Chalk; + readonly gray: Chalk; - /* + /* Alias for `blackBright`. */ - readonly grey: Chalk; + readonly grey: Chalk; - readonly blackBright: Chalk; - readonly redBright: Chalk; - readonly greenBright: Chalk; - readonly yellowBright: Chalk; - readonly blueBright: Chalk; - readonly magentaBright: Chalk; - readonly cyanBright: Chalk; - readonly whiteBright: Chalk; + readonly blackBright: Chalk; + readonly redBright: Chalk; + readonly greenBright: Chalk; + readonly yellowBright: Chalk; + readonly blueBright: Chalk; + readonly magentaBright: Chalk; + readonly cyanBright: Chalk; + readonly whiteBright: Chalk; - readonly bgBlack: Chalk; - readonly bgRed: Chalk; - readonly bgGreen: Chalk; - readonly bgYellow: Chalk; - readonly bgBlue: Chalk; - readonly bgMagenta: Chalk; - readonly bgCyan: Chalk; - readonly bgWhite: Chalk; + readonly bgBlack: Chalk; + readonly bgRed: Chalk; + readonly bgGreen: Chalk; + readonly bgYellow: Chalk; + readonly bgBlue: Chalk; + readonly bgMagenta: Chalk; + readonly bgCyan: Chalk; + readonly bgWhite: Chalk; - /* + /* Alias for `bgBlackBright`. */ - readonly bgGray: Chalk; + readonly bgGray: Chalk; - /* + /* Alias for `bgBlackBright`. */ - readonly bgGrey: Chalk; + readonly bgGrey: Chalk; - readonly bgBlackBright: Chalk; - readonly bgRedBright: Chalk; - readonly bgGreenBright: Chalk; - readonly bgYellowBright: Chalk; - readonly bgBlueBright: Chalk; - readonly bgMagentaBright: Chalk; - readonly bgCyanBright: Chalk; - readonly bgWhiteBright: Chalk; + readonly bgBlackBright: Chalk; + readonly bgRedBright: Chalk; + readonly bgGreenBright: Chalk; + readonly bgYellowBright: Chalk; + readonly bgBlueBright: Chalk; + readonly bgMagentaBright: Chalk; + readonly bgCyanBright: Chalk; + readonly bgWhiteBright: Chalk; + } } - } - /** + /** Main Chalk object that allows to chain styles together. Call the last one as a method with a string argument. Order doesn't matter, and later styles take precedent in case of a conflict. This simply means that `chalk.red.yellow.green` is equivalent to `chalk.green`. */ - const chalk: chalk.Chalk & - chalk.ChalkFunction & { - supportsColor: chalk.ColorSupport | false; - Level: LevelEnum; - Color: Color; - ForegroundColor: ForegroundColor; - BackgroundColor: BackgroundColor; - Modifiers: Modifiers; - stderr: chalk.Chalk & { supportsColor: chalk.ColorSupport | false }; - }; + const chalk: chalk.Chalk & + chalk.ChalkFunction & { + supportsColor: chalk.ColorSupport | false; + Level: LevelEnum; + Color: Color; + ForegroundColor: ForegroundColor; + BackgroundColor: BackgroundColor; + Modifiers: Modifiers; + stderr: chalk.Chalk & { supportsColor: chalk.ColorSupport | false }; + }; - export = chalk; + export = chalk; } diff --git a/@types/cwordin-api.d.ts b/@types/cwordin-api.d.ts index 0ebb860..9ca2289 100644 --- a/@types/cwordin-api.d.ts +++ b/@types/cwordin-api.d.ts @@ -1,102 +1,98 @@ declare module 'crowdin-api' { - export interface ProjectInfoFile { - node_type: 'file'; - id: number; - name: string; - created: string; - last_updated: string; - last_accessed: string; - last_revision: string; - } - - export interface ProjectInfoDirectory { - node_type: 'directory'; - id: number; - name: string; - files: Array; - } - - export interface ProjectInfoResponse { - details: { - source_language: { + export interface ProjectInfoFile { + node_type: 'file'; + id: number; name: string; - code: string; - }; - name: string; - identifier: string; - created: string; - description: string; - join_policy: string; - last_build: string | null; - last_activity: string; - participants_count: string; // it's number, but string in the response - logo_url: string | null; - total_strings_count: string; // it's number, but string in the response - total_words_count: string; // it's number, but string in the response - duplicate_strings_count: number; - duplicate_words_count: number; - invite_url: { - translator: string; - proofreader: string; - }; - }; - languages: Array<{ - name: string; // English language name - code: string; - can_translate: 0 | 1; - can_approve: 0 | 1; - }>; - files: Array; - } + created: string; + last_updated: string; + last_accessed: string; + last_revision: string; + } - export interface LanguageStatusNode { - node_type: 'directory' | 'file'; - id: number; - name: string; - phrases: number; - translated: number; - approved: number; - words: number; - words_translated: number; - words_approved: number; - files: Array; - } + export interface ProjectInfoDirectory { + node_type: 'directory'; + id: number; + name: string; + files: Array; + } - export interface LanguageStatusResponse { - files: Array; - } + export interface ProjectInfoResponse { + details: { + source_language: { + name: string; + code: string; + }; + name: string; + identifier: string; + created: string; + description: string; + join_policy: string; + last_build: string | null; + last_activity: string; + participants_count: string; // it's number, but string in the response + logo_url: string | null; + total_strings_count: string; // it's number, but string in the response + total_words_count: string; // it's number, but string in the response + duplicate_strings_count: number; + duplicate_words_count: number; + invite_url: { + translator: string; + proofreader: string; + }; + }; + languages: Array<{ + name: string; // English language name + code: string; + can_translate: 0 | 1; + can_approve: 0 | 1; + }>; + files: Array; + } - type FilesList = Record; + export interface LanguageStatusNode { + node_type: 'directory' | 'file'; + id: number; + name: string; + phrases: number; + translated: number; + approved: number; + words: number; + words_translated: number; + words_approved: number; + files: Array; + } - export default class CrowdinApi { - constructor(params: { - apiKey: string; - projectName: string; - baseUrl?: string; - }); - projectInfo(): Promise; - languageStatus(language: string): Promise; - exportFile( - file: string, - language: string, - params?: { - branch?: string; - format?: 'xliff'; - export_translated_only?: boolean; - export_approved_only?: boolean; - }, - ): Promise; // TODO: not sure about Promise return type - updateFile( - files: FilesList, - params: { - titles?: Record; - export_patterns?: Record; - new_names?: Record; - first_line_contains_header?: string; - scheme?: string; - update_option?: 'update_as_unapproved' | 'update_without_changes'; - branch?: string; - }, - ): Promise; - } + export interface LanguageStatusResponse { + files: Array; + } + + type FilesList = Record; + + export default class CrowdinApi { + constructor(params: { apiKey: string; projectName: string; baseUrl?: string }); + projectInfo(): Promise; + languageStatus(language: string): Promise; + exportFile( + file: string, + language: string, + params?: { + branch?: string; + format?: 'xliff'; + export_translated_only?: boolean; + export_approved_only?: boolean; + }, + ): Promise; // TODO: not sure about Promise return type + updateFile( + files: FilesList, + params: { + titles?: Record; + export_patterns?: Record; + new_names?: Record; + first_line_contains_header?: string; + scheme?: string; + update_option?: 'update_as_unapproved' | 'update_without_changes'; + branch?: string; + }, + ): Promise; + } } diff --git a/@types/multi-progress.d.ts b/@types/multi-progress.d.ts index b3ab5e5..67df384 100644 --- a/@types/multi-progress.d.ts +++ b/@types/multi-progress.d.ts @@ -1,13 +1,10 @@ declare module 'multi-progress' { - export default class MultiProgress { - constructor(stream?: string); - newBar( - schema: string, - options: ProgressBar.ProgressBarOptions, - ): ProgressBar; - terminate(): void; - move(index: number): void; - tick(index: number, value?: number, options?: any): void; - update(index: number, value?: number, options?: any): void; - } + export default class MultiProgress { + constructor(stream?: string); + newBar(schema: string, options: ProgressBar.ProgressBarOptions): ProgressBar; + terminate(): void; + move(index: number): void; + tick(index: number, value?: number, options?: any): void; + update(index: number, value?: number, options?: any): void; + } } diff --git a/@types/prompt.d.ts b/@types/prompt.d.ts index 83bd868..14a2e70 100644 --- a/@types/prompt.d.ts +++ b/@types/prompt.d.ts @@ -2,65 +2,55 @@ // Project: https://github.com/flatiron/prompt declare module 'prompt' { - type PropertiesType = - | Array - | prompt.PromptSchema - | Array; + type PropertiesType = Array | prompt.PromptSchema | Array; - namespace prompt { - interface PromptSchema { - properties: PromptProperties; + namespace prompt { + interface PromptSchema { + properties: PromptProperties; + } + + interface PromptProperties { + [propName: string]: PromptPropertyOptions; + } + + interface PromptPropertyOptions { + name?: string; + pattern?: RegExp; + message?: string; + required?: boolean; + hidden?: boolean; + description?: string; + type?: string; + default?: string; + before?: (value: any) => any; + conform?: (result: any) => boolean; + } + + export function start(): void; + + export function get( + properties: T, + callback: ( + err: Error, + result: T extends Array + ? Record + : T extends PromptSchema + ? Record + : T extends Array + ? Record + : never, + ) => void, + ): void; + + export function addProperties(obj: any, properties: PropertiesType, callback: (err: Error) => void): void; + + export function history(propertyName: string): any; + + export let override: any; + export let colors: boolean; + export let message: string; + export let delimiter: string; } - interface PromptProperties { - [propName: string]: PromptPropertyOptions; - } - - interface PromptPropertyOptions { - name?: string; - pattern?: RegExp; - message?: string; - required?: boolean; - hidden?: boolean; - description?: string; - type?: string; - default?: string; - before?: (value: any) => any; - conform?: (result: any) => boolean; - } - - export function start(): void; - - export function get( - properties: T, - callback: ( - err: Error, - result: T extends Array - ? Record - : T extends PromptSchema - ? Record - : T extends Array - ? Record< - T[number]['name'] extends string ? T[number]['name'] : number, - string - > - : never, - ) => void, - ): void; - - export function addProperties( - obj: any, - properties: PropertiesType, - callback: (err: Error) => void, - ): void; - - export function history(propertyName: string): any; - - export let override: any; - export let colors: boolean; - export let message: string; - export let delimiter: string; - } - - export = prompt; + export = prompt; } diff --git a/@types/redux-localstorage.d.ts b/@types/redux-localstorage.d.ts index 07130b3..39d5770 100644 --- a/@types/redux-localstorage.d.ts +++ b/@types/redux-localstorage.d.ts @@ -3,23 +3,13 @@ // import * as Redux from 'redux'; declare module 'redux-localstorage' { - export interface ConfigRS { - key: string; - merge?: any; - slicer?: any; - serialize?: ( - value: any, - replacer?: (key: string, value: any) => any, - space?: string | number, - ) => string; - deserialize?: ( - text: string, - reviver?: (key: any, value: any) => any, - ) => any; - } + export interface ConfigRS { + key: string; + merge?: any; + slicer?: any; + serialize?: (value: any, replacer?: (key: string, value: any) => any, space?: string | number) => string; + deserialize?: (text: string, reviver?: (key: any, value: any) => any) => any; + } - export default function persistState( - paths: string | string[], - config: ConfigRS, - ): () => any; + export default function persistState(paths: string | string[], config: ConfigRS): () => any; } diff --git a/@types/unexpected.d.ts b/@types/unexpected.d.ts index 491d67c..18b3715 100644 --- a/@types/unexpected.d.ts +++ b/@types/unexpected.d.ts @@ -1,86 +1,77 @@ declare module 'unexpected' { - namespace unexpected { - interface EnchantedPromise extends Promise { - and = []>( - assertionName: string, - subject: unknown, - ...args: A - ): EnchantedPromise; + namespace unexpected { + interface EnchantedPromise extends Promise { + and = []>( + assertionName: string, + subject: unknown, + ...args: A + ): EnchantedPromise; + } + + interface Expect { + /** + * @see http://unexpected.js.org/api/expect/ + */ + = []>(subject: unknown, assertionName: string, ...args: A): EnchantedPromise; + + it = []>( + assertionName: string, + subject?: unknown, + ...args: A + ): EnchantedPromise; + + /** + * @see http://unexpected.js.org/api/clone/ + */ + clone(): this; + + /** + * @see http://unexpected.js.org/api/addAssertion/ + */ + addAssertion = []>( + pattern: string, + handler: (expect: Expect, subject: T, ...args: A) => void, + ): this; + + /** + * @see http://unexpected.js.org/api/addType/ + */ + addType(typeDefinition: unexpected.TypeDefinition): this; + + /** + * @see http://unexpected.js.org/api/fail/ + */ + fail = []>(format: string, ...args: A): void; + fail(error: E): void; + + /** + * @see http://unexpected.js.org/api/freeze/ + */ + freeze(): this; + + /** + * @see http://unexpected.js.org/api/use/ + */ + use(plugin: unexpected.PluginDefinition): this; + } + + interface PluginDefinition { + name?: string; + version?: string; + dependencies?: Array; + installInto(expect: Expect): void; + } + + interface TypeDefinition { + name: string; + identify(value: unknown): value is T; + base?: string; + equal?(a: T, b: T, equal: (a: unknown, b: unknown) => boolean): boolean; + inspect?(value: T, depth: number, output: any, inspect: (value: unknown, depth: number) => any): void; + } } - interface Expect { - /** - * @see http://unexpected.js.org/api/expect/ - */ - = []>( - subject: unknown, - assertionName: string, - ...args: A - ): EnchantedPromise; + const unexpected: unexpected.Expect; - it = []>( - assertionName: string, - subject?: unknown, - ...args: A - ): EnchantedPromise; - - /** - * @see http://unexpected.js.org/api/clone/ - */ - clone(): this; - - /** - * @see http://unexpected.js.org/api/addAssertion/ - */ - addAssertion = []>( - pattern: string, - handler: (expect: Expect, subject: T, ...args: A) => void, - ): this; - - /** - * @see http://unexpected.js.org/api/addType/ - */ - addType(typeDefinition: unexpected.TypeDefinition): this; - - /** - * @see http://unexpected.js.org/api/fail/ - */ - fail = []>(format: string, ...args: A): void; - fail(error: E): void; - - /** - * @see http://unexpected.js.org/api/freeze/ - */ - freeze(): this; - - /** - * @see http://unexpected.js.org/api/use/ - */ - use(plugin: unexpected.PluginDefinition): this; - } - - interface PluginDefinition { - name?: string; - version?: string; - dependencies?: Array; - installInto(expect: Expect): void; - } - - interface TypeDefinition { - name: string; - identify(value: unknown): value is T; - base?: string; - equal?(a: T, b: T, equal: (a: unknown, b: unknown) => boolean): boolean; - inspect?( - value: T, - depth: number, - output: any, - inspect: (value: unknown, depth: number) => any, - ): void; - } - } - - const unexpected: unexpected.Expect; - - export = unexpected; + export = unexpected; } diff --git a/@types/webpack-loaders.d.ts b/@types/webpack-loaders.d.ts index b198f9a..3995219 100644 --- a/@types/webpack-loaders.d.ts +++ b/@types/webpack-loaders.d.ts @@ -1,59 +1,59 @@ declare module '*.html' { - const url: string; - export = url; + const url: string; + export = url; } declare module '*.svg' { - const url: string; - export = url; + const url: string; + export = url; } declare module '*.png' { - const url: string; - export = url; + const url: string; + export = url; } declare module '*.gif' { - const url: string; - export = url; + const url: string; + export = url; } declare module '*.jpg' { - const url: string; - export = url; + const url: string; + export = url; } declare module '*.intl.json' { - import { MessageDescriptor } from 'react-intl'; + import { MessageDescriptor } from 'react-intl'; - const descriptor: Record; + const descriptor: Record; - export = descriptor; + export = descriptor; } declare module '*.json' { - const jsonContents: { - [key: string]: any; - }; + const jsonContents: { + [key: string]: any; + }; - export = jsonContents; + export = jsonContents; } declare module '*.scss' { - // TODO: replace with: - // https://www.npmjs.com/package/css-modules-typescript-loader - // https://github.com/Jimdo/typings-for-css-modules-loader - const classNames: { - [className: string]: string; - }; + // TODO: replace with: + // https://www.npmjs.com/package/css-modules-typescript-loader + // https://github.com/Jimdo/typings-for-css-modules-loader + const classNames: { + [className: string]: string; + }; - export = classNames; + export = classNames; } declare module '*.css' { - const classNames: { - [className: string]: string; - }; + const classNames: { + [className: string]: string; + }; - export = classNames; + export = classNames; } diff --git a/README.md b/README.md index 0c27de3..19753d0 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,7 @@ [![Build Status](https://travis-ci.org/elyby/accounts-frontend.svg?branch=master)](https://travis-ci.org/elyby/accounts-frontend) [![Ely.by translation on Crowdin](https://d322cqt584bo4o.cloudfront.net/elyby/localized.svg)](https://translate.ely.by/project/elyby) -Web interface for Ely.by Accounts service. Developed using ReactJS and Flow -typing. +Web interface for Ely.by Accounts service. Developed using ReactJS and Flow typing. ## Development @@ -19,16 +18,15 @@ cd accounts-frontend yarn install ``` -After that you need to copy `config/template.env.js` into `config/env.js` and -adjust it for yourself. Then you can start the application in dev mode: +After that you need to copy `config/template.env.js` into `config/env.js` and adjust it for yourself. Then you can start +the application in dev mode: ```bash yarn start ``` -This will start the dev server on port 8080, which will automatically apply all -changes in project files, as well as proxy all requests to the backend on the -domain specified in `env.js`. +This will start the dev server on port 8080, which will automatically apply all changes in project files, as well as +proxy all requests to the backend on the domain specified in `env.js`. To run the tests execute: @@ -42,8 +40,7 @@ yarn test 2. Place your code in a separate branch `git checkout -b `. -3. Add your fork as a remote - `git remote add fork https://github.com//accounts-frontend.git`. +3. Add your fork as a remote `git remote add fork https://github.com//accounts-frontend.git`. 4. Push to your fork repository `git push -u fork `. @@ -52,5 +49,4 @@ yarn test ## Translating Ely.by translation is done through the [Crowdin](https://crowdin.com) service. -[Click here](https://translate.ely.by/project/elyby/invite) to participate in -the translation of the project. +[Click here](https://translate.ely.by/project/elyby/invite) to participate in the translation of the project. diff --git a/babel.config.js b/babel.config.js index 424f3a8..028a217 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,40 +1,40 @@ /* eslint-env node */ module.exports = { - presets: [ - [ - '@babel/preset-typescript', - { - allowDeclareFields: true, - }, - ], - '@babel/preset-react', - '@babel/preset-env', - ], - plugins: [ - '@babel/plugin-syntax-dynamic-import', - '@babel/plugin-proposal-function-bind', - '@babel/plugin-proposal-class-properties', - [ - '@babel/plugin-transform-runtime', - { - corejs: 3, - }, - ], - ['react-intl', { messagesDir: './build/messages/' }], - ], - env: { - webpack: { - plugins: ['react-hot-loader/babel'], - presets: [ + presets: [ [ - '@babel/preset-env', - { - modules: false, - useBuiltIns: 'usage', // or "entry" - corejs: 3, - }, + '@babel/preset-typescript', + { + allowDeclareFields: true, + }, ], - ], + '@babel/preset-react', + '@babel/preset-env', + ], + plugins: [ + '@babel/plugin-syntax-dynamic-import', + '@babel/plugin-proposal-function-bind', + '@babel/plugin-proposal-class-properties', + [ + '@babel/plugin-transform-runtime', + { + corejs: 3, + }, + ], + ['react-intl', { messagesDir: './build/messages/' }], + ], + env: { + webpack: { + plugins: ['react-hot-loader/babel'], + presets: [ + [ + '@babel/preset-env', + { + modules: false, + useBuiltIns: 'usage', // or "entry" + corejs: 3, + }, + ], + ], + }, }, - }, }; diff --git a/config.js b/config.js index b6c1fab..fe65a79 100644 --- a/config.js +++ b/config.js @@ -5,9 +5,9 @@ require('dotenv').config(); const { env } = process; module.exports = { - version: env.VERSION || env.NODE_ENV, - apiHost: env.API_HOST || 'https://dev.account.ely.by', - ga: env.GA_ID && { id: env.GA_ID }, - sentryDSN: env.SENTRY_DSN, - crowdinApiKey: env.CROWDIN_API_KEY, + version: env.VERSION || env.NODE_ENV, + apiHost: env.API_HOST || 'https://dev.account.ely.by', + ga: env.GA_ID && { id: env.GA_ID }, + sentryDSN: env.SENTRY_DSN, + crowdinApiKey: env.CROWDIN_API_KEY, }; diff --git a/jest/__mocks__/intlMock.js b/jest/__mocks__/intlMock.js index 6bc5105..9e05125 100644 --- a/jest/__mocks__/intlMock.js +++ b/jest/__mocks__/intlMock.js @@ -5,15 +5,15 @@ const path = require('path'); const { transform } = require('webpack-utils/intl-loader'); module.exports = { - /** - * @param {string} src - transformed module source code - * @param {string} filename - transformed module file path - * @param {{[key: string]: any}} config - jest config - * @param {{instrument: boolean}} options - additional options - * - * @returns {string} - */ - process(src, filename, config, options) { - return transform(src, filename, path.resolve(`${__dirname}/../../..`)); - }, + /** + * @param {string} src - transformed module source code + * @param {string} filename - transformed module file path + * @param {{[key: string]: any}} config - jest config + * @param {{instrument: boolean}} options - additional options + * + * @returns {string} + */ + process(src, filename, config, options) { + return transform(src, filename, path.resolve(`${__dirname}/../../..`)); + }, }; diff --git a/jest/setupAfterEnv.js b/jest/setupAfterEnv.js index a267223..eb76edd 100644 --- a/jest/setupAfterEnv.js +++ b/jest/setupAfterEnv.js @@ -2,19 +2,19 @@ import 'app/polyfills'; import '@testing-library/jest-dom'; if (!window.localStorage) { - window.localStorage = { - getItem(key) { - return this[key] || null; - }, - setItem(key, value) { - this[key] = value; - }, - removeItem(key) { - delete this[key]; - }, - }; + window.localStorage = { + getItem(key) { + return this[key] || null; + }, + setItem(key, value) { + this[key] = value; + }, + removeItem(key) { + delete this[key]; + }, + }; - window.sessionStorage = { - ...window.localStorage, - }; + window.sessionStorage = { + ...window.localStorage, + }; } diff --git a/package.json b/package.json index cc1f83e..d66a2b4 100644 --- a/package.json +++ b/package.json @@ -1,174 +1,174 @@ { - "name": "@elyby/accounts-frontend", - "description": "", - "author": "SleepWalker ", - "private": true, - "maintainers": [ - { - "name": "ErickSkrauch", - "email": "erickskrauch@ely.by" + "name": "@elyby/accounts-frontend", + "description": "", + "author": "SleepWalker ", + "private": true, + "maintainers": [ + { + "name": "ErickSkrauch", + "email": "erickskrauch@ely.by" + }, + { + "name": "SleepWalker", + "email": "mybox@udf.su" + } + ], + "license": "Apache-2.0", + "repository": "https://github.com/elyby/accounts-frontend", + "engines": { + "node": ">=10.0.0" }, - { - "name": "SleepWalker", - "email": "mybox@udf.su" - } - ], - "license": "Apache-2.0", - "repository": "https://github.com/elyby/accounts-frontend", - "engines": { - "node": ">=10.0.0" - }, - "workspaces": [ - "packages/*", - "tests-e2e" - ], - "scripts": { - "start": "yarn run clean && yarn run build:dll && webpack-dev-server --colors", - "clean": "rm -rf ./build && mkdir ./build", - "e2e": "yarn --cwd ./tests-e2e test", - "test": "jest", - "test:watch": "yarn test --watch", - "lint": "eslint --ext js,ts,tsx --fix .", - "lint:check": "eslint --ext js,ts,tsx --quiet .", - "prettier": "prettier --write .", - "prettier:check": "prettier --check .", - "ts:check": "tsc", - "ci:check": "yarn lint:check && yarn ts:check && yarn test", - "analyze": "yarn run clean && yarn run build:webpack --analyze", - "i18n:collect": "babel-node --extensions \".ts\" ./packages/scripts/i18n-collect.ts", - "i18n:push": "babel-node --extensions \".ts\" ./packages/scripts/i18n-crowdin.ts push", - "i18n:pull": "babel-node --extensions \".ts\" ./packages/scripts/i18n-crowdin.ts pull", - "build": "yarn run clean && yarn run build:webpack", - "build:install": "yarn install", - "build:webpack": "NODE_ENV=production webpack --colors -p --bail", - "build:quiet": "yarn run clean && yarn run build:webpack --quiet", - "build:dll": "babel-node --extensions '.ts,.d.ts' ./packages/scripts/build-dll.ts", - "build:serve": "http-server --proxy https://dev.account.ely.by ./build", - "sb": "APP_ENV=storybook start-storybook -p 9009 --ci", - "sb:build": "APP_ENV=storybook build-storybook" - }, - "husky": { - "hooks": { - "pre-commit": "lint-staged", - "pre-push": "yarn ci:check" - } - }, - "lint-staged": { - "*.{json,scss,css,md}": [ - "prettier --write" + "workspaces": [ + "packages/*", + "tests-e2e" ], - "*.{js,ts,tsx}": [ - "eslint --fix" - ] - }, - "jest": { - "roots": [ - "/packages/app" - ], - "setupFilesAfterEnv": [ - "/jest/setupAfterEnv.js" - ], - "resetMocks": true, - "restoreMocks": true, - "watchPlugins": [ - "jest-watch-typeahead/filename", - "jest-watch-typeahead/testname" - ], - "moduleNameMapper": { - "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/jest/__mocks__/mockStrExport.js", - "\\.(css|less|scss)$": "identity-obj-proxy" + "scripts": { + "start": "yarn run clean && yarn run build:dll && webpack-dev-server --colors", + "clean": "rm -rf ./build && mkdir ./build", + "e2e": "yarn --cwd ./tests-e2e test", + "test": "jest", + "test:watch": "yarn test --watch", + "lint": "eslint --ext js,ts,tsx --fix .", + "lint:check": "eslint --ext js,ts,tsx --quiet .", + "prettier": "prettier --write .", + "prettier:check": "prettier --check .", + "ts:check": "tsc", + "ci:check": "yarn lint:check && yarn ts:check && yarn test", + "analyze": "yarn run clean && yarn run build:webpack --analyze", + "i18n:collect": "babel-node --extensions \".ts\" ./packages/scripts/i18n-collect.ts", + "i18n:push": "babel-node --extensions \".ts\" ./packages/scripts/i18n-crowdin.ts push", + "i18n:pull": "babel-node --extensions \".ts\" ./packages/scripts/i18n-crowdin.ts pull", + "build": "yarn run clean && yarn run build:webpack", + "build:install": "yarn install", + "build:webpack": "NODE_ENV=production webpack --colors -p --bail", + "build:quiet": "yarn run clean && yarn run build:webpack --quiet", + "build:dll": "babel-node --extensions '.ts,.d.ts' ./packages/scripts/build-dll.ts", + "build:serve": "http-server --proxy https://dev.account.ely.by ./build", + "sb": "APP_ENV=storybook start-storybook -p 9009 --ci", + "sb:build": "APP_ENV=storybook build-storybook" }, - "transform": { - "\\.intl\\.json$": "/jest/__mocks__/intlMock.js", - "^.+\\.[tj]sx?$": "babel-jest" + "husky": { + "hooks": { + "pre-commit": "lint-staged", + "pre-push": "yarn ci:check" + } + }, + "lint-staged": { + "*.{json,scss,css,md}": [ + "prettier --write" + ], + "*.{js,ts,tsx}": [ + "eslint --fix" + ] + }, + "jest": { + "roots": [ + "/packages/app" + ], + "setupFilesAfterEnv": [ + "/jest/setupAfterEnv.js" + ], + "resetMocks": true, + "restoreMocks": true, + "watchPlugins": [ + "jest-watch-typeahead/filename", + "jest-watch-typeahead/testname" + ], + "moduleNameMapper": { + "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/jest/__mocks__/mockStrExport.js", + "\\.(css|less|scss)$": "identity-obj-proxy" + }, + "transform": { + "\\.intl\\.json$": "/jest/__mocks__/intlMock.js", + "^.+\\.[tj]sx?$": "babel-jest" + } + }, + "dependencies": { + "react": "^16.13.1", + "react-dom": "^16.13.1", + "react-hot-loader": "^4.12.18", + "react-intl": "^4.5.7", + "regenerator-runtime": "^0.13.3" + }, + "devDependencies": { + "@babel/cli": "^7.8.3", + "@babel/core": "^7.8.3", + "@babel/node": "^7.8.3", + "@babel/plugin-proposal-class-properties": "^7.8.3", + "@babel/plugin-proposal-decorators": "^7.8.3", + "@babel/plugin-proposal-do-expressions": "^7.8.3", + "@babel/plugin-proposal-export-default-from": "^7.8.3", + "@babel/plugin-proposal-export-namespace-from": "^7.8.3", + "@babel/plugin-proposal-function-bind": "^7.8.3", + "@babel/plugin-proposal-function-sent": "^7.8.3", + "@babel/plugin-proposal-json-strings": "^7.8.3", + "@babel/plugin-proposal-logical-assignment-operators": "^7.8.3", + "@babel/plugin-proposal-numeric-separator": "^7.8.3", + "@babel/plugin-proposal-pipeline-operator": "^7.8.3", + "@babel/plugin-proposal-throw-expressions": "^7.8.3", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-transform-runtime": "^7.8.3", + "@babel/preset-env": "^7.8.3", + "@babel/preset-react": "^7.8.3", + "@babel/preset-typescript": "^7.8.3", + "@babel/runtime-corejs3": "^7.8.3", + "@storybook/addon-actions": "^5.3.18", + "@storybook/addon-links": "^5.3.18", + "@storybook/addon-viewport": "^5.3.18", + "@storybook/addons": "^5.3.18", + "@storybook/react": "^5.3.18", + "@types/jest": "^25.2.3", + "@types/sinon": "^9.0.3", + "@typescript-eslint/eslint-plugin": "^3.0.0", + "@typescript-eslint/parser": "^3.0.0", + "babel-loader": "^8.0.0", + "babel-plugin-react-intl": "^7.5.10", + "core-js": "3.6.5", + "csp-webpack-plugin": "^2.0.2", + "css-loader": "^3.5.3", + "cssnano": "^4.1.10", + "dotenv": "^8.2.0", + "eager-imports-webpack-plugin": "^1.0.0", + "eslint": "^7.0.0", + "eslint-config-prettier": "^6.11.0", + "eslint-plugin-jsdoc": "^25.4.2", + "eslint-plugin-prettier": "^3.1.3", + "eslint-plugin-react": "^7.20.0", + "exports-loader": "^0.7.0", + "file-loader": "^6.0.0", + "html-loader": "^1.1.0", + "html-webpack-plugin": "^4.3.0", + "husky": "^4.2.5", + "identity-obj-proxy": "^3.0.0", + "imports-loader": "^0.8.0", + "jest": "^26.0.1", + "jest-watch-typeahead": "^0.6.0", + "json-loader": "^0.5.4", + "lint-staged": "^10.2.4", + "loader-utils": "^2.0.0", + "mini-css-extract-plugin": "^0.9.0", + "node-sass": "^4.14.1", + "postcss-import": "^12.0.1", + "postcss-loader": "^3.0.0", + "postcss-scss": "^2.1.1", + "prettier": "^2.0.5", + "raw-loader": "^4.0.1", + "sass-loader": "^8.0.2", + "sinon": "^9.0.2", + "sitemap-webpack-plugin": "^0.8.0", + "speed-measure-webpack-plugin": "^1.3.3", + "storybook-addon-intl": "^2.4.1", + "style-loader": "~1.2.1", + "typescript": "^3.9.3", + "unexpected": "^11.14.0", + "unexpected-sinon": "^10.5.1", + "url-loader": "^4.1.0", + "wait-on": "^5.0.0", + "webpack": "^4.41.5", + "webpack-bundle-analyzer": "^3.8.0", + "webpack-cli": "^3.3.10", + "webpack-dev-server": "^3.10.1", + "webpackbar": "^4.0.0" } - }, - "dependencies": { - "react": "^16.13.1", - "react-dom": "^16.13.1", - "react-hot-loader": "^4.12.18", - "react-intl": "^4.5.7", - "regenerator-runtime": "^0.13.3" - }, - "devDependencies": { - "@babel/cli": "^7.8.3", - "@babel/core": "^7.8.3", - "@babel/node": "^7.8.3", - "@babel/plugin-proposal-class-properties": "^7.8.3", - "@babel/plugin-proposal-decorators": "^7.8.3", - "@babel/plugin-proposal-do-expressions": "^7.8.3", - "@babel/plugin-proposal-export-default-from": "^7.8.3", - "@babel/plugin-proposal-export-namespace-from": "^7.8.3", - "@babel/plugin-proposal-function-bind": "^7.8.3", - "@babel/plugin-proposal-function-sent": "^7.8.3", - "@babel/plugin-proposal-json-strings": "^7.8.3", - "@babel/plugin-proposal-logical-assignment-operators": "^7.8.3", - "@babel/plugin-proposal-numeric-separator": "^7.8.3", - "@babel/plugin-proposal-pipeline-operator": "^7.8.3", - "@babel/plugin-proposal-throw-expressions": "^7.8.3", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-import-meta": "^7.8.3", - "@babel/plugin-transform-runtime": "^7.8.3", - "@babel/preset-env": "^7.8.3", - "@babel/preset-react": "^7.8.3", - "@babel/preset-typescript": "^7.8.3", - "@babel/runtime-corejs3": "^7.8.3", - "@storybook/addon-actions": "^5.3.18", - "@storybook/addon-links": "^5.3.18", - "@storybook/addon-viewport": "^5.3.18", - "@storybook/addons": "^5.3.18", - "@storybook/react": "^5.3.18", - "@types/jest": "^25.2.3", - "@types/sinon": "^9.0.3", - "@typescript-eslint/eslint-plugin": "^3.0.0", - "@typescript-eslint/parser": "^3.0.0", - "babel-loader": "^8.0.0", - "babel-plugin-react-intl": "^7.5.10", - "core-js": "3.6.5", - "csp-webpack-plugin": "^2.0.2", - "css-loader": "^3.5.3", - "cssnano": "^4.1.10", - "dotenv": "^8.2.0", - "eager-imports-webpack-plugin": "^1.0.0", - "eslint": "^7.0.0", - "eslint-config-prettier": "^6.11.0", - "eslint-plugin-jsdoc": "^25.4.2", - "eslint-plugin-prettier": "^3.1.3", - "eslint-plugin-react": "^7.20.0", - "exports-loader": "^0.7.0", - "file-loader": "^6.0.0", - "html-loader": "^1.1.0", - "html-webpack-plugin": "^4.3.0", - "husky": "^4.2.5", - "identity-obj-proxy": "^3.0.0", - "imports-loader": "^0.8.0", - "jest": "^26.0.1", - "jest-watch-typeahead": "^0.6.0", - "json-loader": "^0.5.4", - "lint-staged": "^10.2.4", - "loader-utils": "^2.0.0", - "mini-css-extract-plugin": "^0.9.0", - "node-sass": "^4.14.1", - "postcss-import": "^12.0.1", - "postcss-loader": "^3.0.0", - "postcss-scss": "^2.1.1", - "prettier": "^2.0.5", - "raw-loader": "^4.0.1", - "sass-loader": "^8.0.2", - "sinon": "^9.0.2", - "sitemap-webpack-plugin": "^0.8.0", - "speed-measure-webpack-plugin": "^1.3.3", - "storybook-addon-intl": "^2.4.1", - "style-loader": "~1.2.1", - "typescript": "^3.9.3", - "unexpected": "^11.14.0", - "unexpected-sinon": "^10.5.1", - "url-loader": "^4.1.0", - "wait-on": "^5.0.0", - "webpack": "^4.41.5", - "webpack-bundle-analyzer": "^3.8.0", - "webpack-cli": "^3.3.10", - "webpack-dev-server": "^3.10.1", - "webpackbar": "^4.0.0" - } } diff --git a/packages/app/components/MeasureHeight.tsx b/packages/app/components/MeasureHeight.tsx index b54900c..6429bb5 100644 --- a/packages/app/components/MeasureHeight.tsx +++ b/packages/app/components/MeasureHeight.tsx @@ -26,47 +26,46 @@ type ChildState = any; // TODO: this may be rewritten in more efficient way using resize/mutation observer export default class MeasureHeight extends React.PureComponent< - { - shouldMeasure: (prevState: ChildState, newState: ChildState) => boolean; - onMeasure: (height: number) => void; - state: ChildState; - } & React.HTMLAttributes + { + shouldMeasure: (prevState: ChildState, newState: ChildState) => boolean; + onMeasure: (height: number) => void; + state: ChildState; + } & React.HTMLAttributes > { - static defaultProps = { - shouldMeasure: (prevState: ChildState, newState: ChildState) => - prevState !== newState, - onMeasure: () => {}, - }; + static defaultProps = { + shouldMeasure: (prevState: ChildState, newState: ChildState) => prevState !== newState, + onMeasure: () => {}, + }; - el: HTMLDivElement | null = null; + el: HTMLDivElement | null = null; - componentDidMount() { - // we want to measure height immediately on first mount to avoid ui laggs - this.measure(); - window.addEventListener('resize', this.enqueueMeasurement); - } - - componentDidUpdate(prevProps: typeof MeasureHeight.prototype.props) { - if (this.props.shouldMeasure(prevProps.state, this.props.state)) { - this.enqueueMeasurement(); + componentDidMount() { + // we want to measure height immediately on first mount to avoid ui laggs + this.measure(); + window.addEventListener('resize', this.enqueueMeasurement); } - } - componentWillUnmount() { - window.removeEventListener('resize', this.enqueueMeasurement); - } + componentDidUpdate(prevProps: typeof MeasureHeight.prototype.props) { + if (this.props.shouldMeasure(prevProps.state, this.props.state)) { + this.enqueueMeasurement(); + } + } - render() { - const props = omit(this.props, ['shouldMeasure', 'onMeasure', 'state']); + componentWillUnmount() { + window.removeEventListener('resize', this.enqueueMeasurement); + } - return
(this.el = el)} />; - } + render() { + const props = omit(this.props, ['shouldMeasure', 'onMeasure', 'state']); - measure = () => { - requestAnimationFrame(() => { - this.el && this.props.onMeasure(this.el.offsetHeight); - }); - }; + return
(this.el = el)} />; + } - enqueueMeasurement = debounce(this.measure); + measure = () => { + requestAnimationFrame(() => { + this.el && this.props.onMeasure(this.el.offsetHeight); + }); + }; + + enqueueMeasurement = debounce(this.measure); } diff --git a/packages/app/components/accounts/AccountSwitcher.intl.json b/packages/app/components/accounts/AccountSwitcher.intl.json index d45f32e..e2ceb9c 100644 --- a/packages/app/components/accounts/AccountSwitcher.intl.json +++ b/packages/app/components/accounts/AccountSwitcher.intl.json @@ -1,5 +1,5 @@ { - "addAccount": "Add account", - "goToEly": "Go to Ely.by profile", - "logout": "Log out" + "addAccount": "Add account", + "goToEly": "Go to Ely.by profile", + "logout": "Log out" } diff --git a/packages/app/components/accounts/AccountSwitcher.tsx b/packages/app/components/accounts/AccountSwitcher.tsx index cca5873..60113fd 100644 --- a/packages/app/components/accounts/AccountSwitcher.tsx +++ b/packages/app/components/accounts/AccountSwitcher.tsx @@ -14,190 +14,164 @@ import styles from './accountSwitcher.scss'; import messages from './AccountSwitcher.intl.json'; interface Props { - switchAccount: (account: Account) => Promise; - removeAccount: (account: Account) => Promise; - // called after each action performed - onAfterAction: () => void; - // called after switching an account. The active account will be passed as arg - onSwitch: (account: Account) => void; - accounts: RootState['accounts']; - skin: Skin; - // whether active account should be expanded and shown on the top - highlightActiveAccount: boolean; - // whether to show logout icon near each account - allowLogout: boolean; - // whether to show add account button - allowAdd: boolean; + switchAccount: (account: Account) => Promise; + removeAccount: (account: Account) => Promise; + // called after each action performed + onAfterAction: () => void; + // called after switching an account. The active account will be passed as arg + onSwitch: (account: Account) => void; + accounts: RootState['accounts']; + skin: Skin; + // whether active account should be expanded and shown on the top + highlightActiveAccount: boolean; + // whether to show logout icon near each account + allowLogout: boolean; + // whether to show add account button + allowAdd: boolean; } export class AccountSwitcher extends React.Component { - static defaultProps: Partial = { - skin: SKIN_DARK, - highlightActiveAccount: true, - allowLogout: true, - allowAdd: true, - onAfterAction() {}, - onSwitch() {}, - }; + static defaultProps: Partial = { + skin: SKIN_DARK, + highlightActiveAccount: true, + allowLogout: true, + allowAdd: true, + onAfterAction() {}, + onSwitch() {}, + }; - render() { - const { - accounts, - skin, - allowAdd, - allowLogout, - highlightActiveAccount, - } = this.props; - const activeAccount = getActiveAccount({ accounts }); + render() { + const { accounts, skin, allowAdd, allowLogout, highlightActiveAccount } = this.props; + const activeAccount = getActiveAccount({ accounts }); - if (!activeAccount) { - return null; + if (!activeAccount) { + return null; + } + + let { available } = accounts; + + if (highlightActiveAccount) { + available = available.filter((account) => account.id !== activeAccount.id); + } + + return ( +
+ {highlightActiveAccount && ( +
+ + )} + + {available.map((account, index) => ( +
+
+ + {allowLogout ? ( +
+ ) : ( +
+ )} + +
+
{account.username}
+
{account.email}
+
+
+ ))} + {allowAdd ? ( + + - ); + willLeave = (config: TransitionStyle): Style => { + const transform = this.getTransformForPanel(config.key); - return ( -
- {hasBackButton ? backButton : null} -
{Title}
-
- ); - } + return { + transformSpring: spring(transform, transformSpringConfig), + opacitySpring: spring(0, opacitySpringConfig), + }; + }; - getBody({ key, style, data }: TransitionPlainStyle): ReactElement { - const { Body } = data as AnimationData; - const { transformSpring } = (style as unknown) as AnimationStyle; - const { direction } = this.state; + getTransformForPanel(key: PanelId): number { + const { panelId, prevPanelId } = this.state; - let transform = this.translate(transformSpring, direction); - let verticalOrigin = 'top'; + const fromLeft = -1; + const fromRight = 1; - if (direction === 'Y') { - verticalOrigin = 'bottom'; - transform = {}; + const currentContext = contexts.find((context) => context.includes(key)); + + if (!currentContext) { + throw new Error(`Can not find settings for ${key} panel`); + } + + let sign = + prevPanelId && panelId && currentContext.indexOf(panelId) > currentContext.indexOf(prevPanelId) + ? fromRight + : fromLeft; + + if (prevPanelId === key) { + sign *= -1; + } + + return sign * 100; } - const transitionStyle: CSSProperties = { - ...this.getDefaultTransitionStyles( - key, - (style as unknown) as AnimationStyle, - ), - top: 'auto', // reset default - [verticalOrigin]: 0, - ...transform, + getDirection(next: PanelId, prev: PanelId): 'X' | 'Y' { + const context = contexts.find((item) => item.includes(prev)); + + if (!context) { + throw new Error(`Can not find context for transition ${prev} -> ${next}`); + } + + return context.includes(next) ? 'X' : 'Y'; + } + + onUpdateHeight = (height: number, key: PanelId): void => { + this.setState({ + formsHeights: { + ...this.state.formsHeights, + [key]: height, + }, + }); }; - return ( - this.onUpdateHeight(height, key)} - > - {React.cloneElement(Body, { - // @ts-ignore - ref: (body) => { - this.body = body; - }, - })} - - ); - } - - getFooter({ key, style, data }: TransitionPlainStyle): ReactElement { - const { Footer } = data as AnimationData; - - const transitionStyle = this.getDefaultTransitionStyles( - key, - (style as unknown) as AnimationStyle, - ); - - return ( -
- {Footer} -
- ); - } - - getLinks({ key, style, data }: TransitionPlainStyle): ReactElement { - const { Links } = data as AnimationData; - - const transitionStyle = this.getDefaultTransitionStyles( - key, - (style as unknown) as AnimationStyle, - ); - - return ( -
- {Links} -
- ); - } - - getDefaultTransitionStyles( - key: string, - { opacitySpring }: Readonly, - ): { - position: 'absolute'; - top: number; - left: number; - width: string; - opacity: number; - pointerEvents: 'none' | 'auto'; - } { - return { - position: 'absolute', - top: 0, - left: 0, - width: '100%', - opacity: opacitySpring, - pointerEvents: key === this.state.panelId ? 'auto' : 'none', + onUpdateContextHeight = (height: number): void => { + this.setState({ + contextHeight: height, + }); }; - } - translate( - value: number, - direction: 'X' | 'Y' = 'X', - unit: '%' | 'px' = '%', - ): CSSProperties { - return { - WebkitTransform: `translate${direction}(${value}${unit})`, - transform: `translate${direction}(${value}${unit})`, + onGoBack: MouseEventHandler = (event): void => { + event.preventDefault(); + authFlow.goBack(); }; - } - requestRedraw = (): Promise => - new Promise((resolve) => - this.setState({ isHeightDirty: true }, () => { - this.setState({ isHeightDirty: false }); + /** + * Tries to auto focus form fields after transition end + * + * @param {number} length number of panels transitioned + */ + tryToAutoFocus(length: number): void { + if (!this.body) { + return; + } - // wait till transition end - this.timerIds.push(setTimeout(resolve, 200)); - }), - ); + if (length === 1) { + if (!this.wasAutoFocused) { + this.body.autoFocus(); + } + + this.wasAutoFocused = true; + } else if (this.wasAutoFocused) { + this.wasAutoFocused = false; + } + } + + shouldMeasureHeight(): string { + const { user, accounts, auth } = this.props; + const { isHeightDirty } = this.state; + + const errorString = Object.values(auth.error || {}).reduce((acc, item) => { + if (typeof item === 'string') { + return acc + item; + } + + return acc + item.type; + }, '') as string; + + return [errorString, isHeightDirty, user.lang, accounts.available.length].join(''); + } + + getHeader({ key, style, data }: TransitionPlainStyle): ReactElement { + const { Title } = data as AnimationData; + const { transformSpring } = (style as unknown) as AnimationStyle; + + let { hasBackButton } = data; + + if (typeof hasBackButton === 'function') { + hasBackButton = hasBackButton(this.props); + } + + const transitionStyle = { + ...this.getDefaultTransitionStyles(key, (style as unknown) as AnimationStyle), + opacity: 1, // reset default + }; + + const scrollStyle = this.translate(transformSpring, 'Y'); + + const sideScrollStyle = { + position: 'relative' as const, + zIndex: 2, + ...this.translate(-Math.abs(transformSpring)), + }; + + const backButton = ( + + ); + + return ( +
+ {hasBackButton ? backButton : null} +
{Title}
+
+ ); + } + + getBody({ key, style, data }: TransitionPlainStyle): ReactElement { + const { Body } = data as AnimationData; + const { transformSpring } = (style as unknown) as AnimationStyle; + const { direction } = this.state; + + let transform = this.translate(transformSpring, direction); + let verticalOrigin = 'top'; + + if (direction === 'Y') { + verticalOrigin = 'bottom'; + transform = {}; + } + + const transitionStyle: CSSProperties = { + ...this.getDefaultTransitionStyles(key, (style as unknown) as AnimationStyle), + top: 'auto', // reset default + [verticalOrigin]: 0, + ...transform, + }; + + return ( + this.onUpdateHeight(height, key)} + > + {React.cloneElement(Body, { + // @ts-ignore + ref: (body) => { + this.body = body; + }, + })} + + ); + } + + getFooter({ key, style, data }: TransitionPlainStyle): ReactElement { + const { Footer } = data as AnimationData; + + const transitionStyle = this.getDefaultTransitionStyles(key, (style as unknown) as AnimationStyle); + + return ( +
+ {Footer} +
+ ); + } + + getLinks({ key, style, data }: TransitionPlainStyle): ReactElement { + const { Links } = data as AnimationData; + + const transitionStyle = this.getDefaultTransitionStyles(key, (style as unknown) as AnimationStyle); + + return ( +
+ {Links} +
+ ); + } + + getDefaultTransitionStyles( + key: string, + { opacitySpring }: Readonly, + ): { + position: 'absolute'; + top: number; + left: number; + width: string; + opacity: number; + pointerEvents: 'none' | 'auto'; + } { + return { + position: 'absolute', + top: 0, + left: 0, + width: '100%', + opacity: opacitySpring, + pointerEvents: key === this.state.panelId ? 'auto' : 'none', + }; + } + + translate(value: number, direction: 'X' | 'Y' = 'X', unit: '%' | 'px' = '%'): CSSProperties { + return { + WebkitTransform: `translate${direction}(${value}${unit})`, + transform: `translate${direction}(${value}${unit})`, + }; + } + + requestRedraw = (): Promise => + new Promise((resolve) => + this.setState({ isHeightDirty: true }, () => { + this.setState({ isHeightDirty: false }); + + // wait till transition end + this.timerIds.push(setTimeout(resolve, 200)); + }), + ); } export default connect( - (state: RootState) => { - const login = getLogin(state); - let user = { - ...state.user, - }; + (state: RootState) => { + const login = getLogin(state); + let user = { + ...state.user, + }; - if (login) { - user = { - ...user, - isGuest: true, - email: '', - username: '', - }; + if (login) { + user = { + ...user, + isGuest: true, + email: '', + username: '', + }; - if (/[@.]/.test(login)) { - user.email = login; - } else { - user.username = login; - } - } + if (/[@.]/.test(login)) { + user.email = login; + } else { + user.username = login; + } + } - return { - user, - accounts: state.accounts, // need this, to re-render height - auth: state.auth, - resolve: authFlow.resolve.bind(authFlow), - reject: authFlow.reject.bind(authFlow), - }; - }, - { - clearErrors: actions.clearErrors, - setErrors: actions.setErrors, - }, + return { + user, + accounts: state.accounts, // need this, to re-render height + auth: state.auth, + resolve: authFlow.resolve.bind(authFlow), + reject: authFlow.reject.bind(authFlow), + }; + }, + { + clearErrors: actions.clearErrors, + setErrors: actions.setErrors, + }, )(PanelTransition); diff --git a/packages/app/components/auth/README.md b/packages/app/components/auth/README.md index 09fa172..9288b6e 100644 --- a/packages/app/components/auth/README.md +++ b/packages/app/components/auth/README.md @@ -2,16 +2,13 @@ To add new panel you need to: -- create panel component at `components/auth/[panelId]` -- add new context in `components/auth/PanelTransition` -- connect component to router in `pages/auth/AuthPage` -- add new state to `services/authFlow` and coresponding test to - `tests/services/authFlow` -- connect state to `authFlow`. Update `services/authFlow/AuthFlow.test` and - `services/authFlow/AuthFlow.functional.test` (the last one for some complex - flow) -- add new actions to `components/auth/actions` and api endpoints to - `services/api` -- whatever else you need +- create panel component at `components/auth/[panelId]` +- add new context in `components/auth/PanelTransition` +- connect component to router in `pages/auth/AuthPage` +- add new state to `services/authFlow` and coresponding test to `tests/services/authFlow` +- connect state to `authFlow`. Update `services/authFlow/AuthFlow.test` and + `services/authFlow/AuthFlow.functional.test` (the last one for some complex flow) +- add new actions to `components/auth/actions` and api endpoints to `services/api` +- whatever else you need Commit id with example implementation: f4d315c diff --git a/packages/app/components/auth/RejectionLink.tsx b/packages/app/components/auth/RejectionLink.tsx index e767f9c..7385592 100644 --- a/packages/app/components/auth/RejectionLink.tsx +++ b/packages/app/components/auth/RejectionLink.tsx @@ -4,36 +4,32 @@ import { FormattedMessage as Message, MessageDescriptor } from 'react-intl'; import Context, { AuthContext } from './Context'; interface Props { - isAvailable?: (context: AuthContext) => boolean; - payload?: Record; - label: MessageDescriptor; + isAvailable?: (context: AuthContext) => boolean; + payload?: Record; + label: MessageDescriptor; } -const RejectionLink: ComponentType = ({ - isAvailable, - payload, - label, -}) => { - const context = useContext(Context); +const RejectionLink: ComponentType = ({ isAvailable, payload, label }) => { + const context = useContext(Context); - if (isAvailable && !isAvailable(context)) { - // TODO: if want to properly support multiple links, we should control - // the dividers ' | ' rendered from factory too - return null; - } + if (isAvailable && !isAvailable(context)) { + // TODO: if want to properly support multiple links, we should control + // the dividers ' | ' rendered from factory too + return null; + } - return ( - { - event.preventDefault(); + return ( + { + event.preventDefault(); - context.reject(payload); - }} - > - - - ); + context.reject(payload); + }} + > + + + ); }; export default RejectionLink; diff --git a/packages/app/components/auth/acceptRules/AcceptRules.intl.json b/packages/app/components/auth/acceptRules/AcceptRules.intl.json index c703230..e2a0943 100644 --- a/packages/app/components/auth/acceptRules/AcceptRules.intl.json +++ b/packages/app/components/auth/acceptRules/AcceptRules.intl.json @@ -1,8 +1,8 @@ { - "title": "User Agreement", - "accept": "Accept", - "declineAndLogout": "Decline and logout", - "description1": "We have updated our {link}.", - "termsOfService": "terms of service", - "description2": "In order to continue using {name} service, you need to accept them." + "title": "User Agreement", + "accept": "Accept", + "declineAndLogout": "Decline and logout", + "description1": "We have updated our {link}.", + "termsOfService": "terms of service", + "description2": "In order to continue using {name} service, you need to accept them." } diff --git a/packages/app/components/auth/acceptRules/AcceptRules.ts b/packages/app/components/auth/acceptRules/AcceptRules.ts index 0325b82..a8cd377 100644 --- a/packages/app/components/auth/acceptRules/AcceptRules.ts +++ b/packages/app/components/auth/acceptRules/AcceptRules.ts @@ -3,14 +3,14 @@ import Body from './AcceptRulesBody'; import messages from './AcceptRules.intl.json'; export default factory({ - title: messages.title, - body: Body, - footer: { - color: 'darkBlue', - autoFocus: true, - label: messages.accept, - }, - links: { - label: messages.declineAndLogout, - }, + title: messages.title, + body: Body, + footer: { + color: 'darkBlue', + autoFocus: true, + label: messages.accept, + }, + links: { + label: messages.declineAndLogout, + }, }); diff --git a/packages/app/components/auth/acceptRules/AcceptRulesBody.tsx b/packages/app/components/auth/acceptRules/AcceptRulesBody.tsx index b150c92..c690545 100644 --- a/packages/app/components/auth/acceptRules/AcceptRulesBody.tsx +++ b/packages/app/components/auth/acceptRules/AcceptRulesBody.tsx @@ -11,38 +11,38 @@ import styles from './acceptRules.scss'; import messages from './AcceptRules.intl.json'; export default class AcceptRulesBody extends BaseAuthBody { - static displayName = 'AcceptRulesBody'; - static panelId = 'acceptRules'; + static displayName = 'AcceptRulesBody'; + static panelId = 'acceptRules'; - render() { - return ( -
- {this.renderErrors()} + render() { + return ( +
+ {this.renderErrors()} -
- -
+
+ +
-

- - - - ), - }} - /> -
- , - }} - /> -

-
- ); - } +

+ + + + ), + }} + /> +
+ , + }} + /> +

+
+ ); + } } diff --git a/packages/app/components/auth/acceptRules/acceptRules.scss b/packages/app/components/auth/acceptRules/acceptRules.scss index 0d79be3..338bb50 100644 --- a/packages/app/components/auth/acceptRules/acceptRules.scss +++ b/packages/app/components/auth/acceptRules/acceptRules.scss @@ -1,16 +1,16 @@ @import '~app/components/ui/colors.scss'; .descriptionText { - font-size: 15px; - line-height: 1.4; - padding-bottom: 8px; - color: #aaa; + font-size: 15px; + line-height: 1.4; + padding-bottom: 8px; + color: #aaa; } // TODO: вынести иконки такого типа в какую-то внешнюю структуру? .security { - color: #fff; - font-size: 90px; - line-height: 1; - margin-bottom: 15px; + color: #fff; + font-size: 90px; + line-height: 1; + margin-bottom: 15px; } diff --git a/packages/app/components/auth/actions.test.ts b/packages/app/components/auth/actions.test.ts index 630cf4a..2aa64ca 100644 --- a/packages/app/components/auth/actions.test.ts +++ b/packages/app/components/auth/actions.test.ts @@ -5,200 +5,190 @@ import expect from 'app/test/unexpected'; import request from 'app/services/request'; import { - setLoadingState, - oAuthValidate, - oAuthComplete, - setClient, - setOAuthRequest, - setScopes, - setOAuthCode, - requirePermissionsAccept, - login, - setLogin, + setLoadingState, + oAuthValidate, + oAuthComplete, + setClient, + setOAuthRequest, + setScopes, + setOAuthCode, + requirePermissionsAccept, + login, + setLogin, } from 'app/components/auth/actions'; import { OauthData, OAuthValidateResponse } from '../../services/api/oauth'; const oauthData: OauthData = { - clientId: '', - redirectUrl: '', - responseType: '', - scope: '', - state: '', - prompt: 'none', + clientId: '', + redirectUrl: '', + responseType: '', + scope: '', + state: '', + prompt: 'none', }; describe('components/auth/actions', () => { - const dispatch = sinon.stub().named('store.dispatch'); - const getState = sinon.stub().named('store.getState'); + const dispatch = sinon.stub().named('store.dispatch'); + const getState = sinon.stub().named('store.getState'); - function callThunk, F extends (...args: A) => any>( - fn: F, - ...args: A - ): Promise { - const thunk = fn(...args); + function callThunk, F extends (...args: A) => any>(fn: F, ...args: A): Promise { + const thunk = fn(...args); - return thunk(dispatch, getState); - } + return thunk(dispatch, getState); + } - function expectDispatchCalls(calls: Array>) { - expect(dispatch, 'to have calls satisfying', [ - [setLoadingState(true)], - ...calls, - [setLoadingState(false)], - ]); - } - - beforeEach(() => { - dispatch.reset(); - getState.reset(); - getState.returns({}); - sinon.stub(request, 'get').named('request.get'); - sinon.stub(request, 'post').named('request.post'); - }); - - afterEach(() => { - (request.get as any).restore(); - (request.post as any).restore(); - }); - - describe('#oAuthValidate()', () => { - let resp: OAuthValidateResponse; + function expectDispatchCalls(calls: Array>) { + expect(dispatch, 'to have calls satisfying', [[setLoadingState(true)], ...calls, [setLoadingState(false)]]); + } beforeEach(() => { - resp = { - client: { - id: '123', - name: '', - description: '', - }, - oAuth: { - state: 123, - }, - session: { - scopes: ['account_info'], - }, - }; - - (request.get as any).returns(Promise.resolve(resp)); + dispatch.reset(); + getState.reset(); + getState.returns({}); + sinon.stub(request, 'get').named('request.get'); + sinon.stub(request, 'post').named('request.post'); }); - it('should send get request to an api', () => - callThunk(oAuthValidate, oauthData).then(() => { - expect(request.get, 'to have a call satisfying', [ - '/api/oauth2/v1/validate', - {}, - ]); - })); - - it('should dispatch setClient, setOAuthRequest and setScopes', () => - callThunk(oAuthValidate, oauthData).then(() => { - expectDispatchCalls([ - [setClient(resp.client)], - [ - setOAuthRequest({ - ...resp.oAuth, - prompt: 'none', - loginHint: undefined, - }), - ], - [setScopes(resp.session.scopes)], - ]); - })); - }); - - describe('#oAuthComplete()', () => { - beforeEach(() => { - getState.returns({ - auth: { - oauth: oauthData, - }, - }); + afterEach(() => { + (request.get as any).restore(); + (request.post as any).restore(); }); - it('should post to api/oauth2/complete', () => { - (request.post as any).returns( - Promise.resolve({ - redirectUri: '', - }), - ); + describe('#oAuthValidate()', () => { + let resp: OAuthValidateResponse; - return callThunk(oAuthComplete).then(() => { - expect(request.post, 'to have a call satisfying', [ - '/api/oauth2/v1/complete?client_id=&redirect_uri=&response_type=&description=&scope=&prompt=none&login_hint=&state=', - {}, - ]); - }); + beforeEach(() => { + resp = { + client: { + id: '123', + name: '', + description: '', + }, + oAuth: { + state: 123, + }, + session: { + scopes: ['account_info'], + }, + }; + + (request.get as any).returns(Promise.resolve(resp)); + }); + + it('should send get request to an api', () => + callThunk(oAuthValidate, oauthData).then(() => { + expect(request.get, 'to have a call satisfying', ['/api/oauth2/v1/validate', {}]); + })); + + it('should dispatch setClient, setOAuthRequest and setScopes', () => + callThunk(oAuthValidate, oauthData).then(() => { + expectDispatchCalls([ + [setClient(resp.client)], + [ + setOAuthRequest({ + ...resp.oAuth, + prompt: 'none', + loginHint: undefined, + }), + ], + [setScopes(resp.session.scopes)], + ]); + })); }); - it('should dispatch setOAuthCode for static_page redirect', () => { - const resp = { - success: true, - redirectUri: 'static_page?code=123&state=', - }; + describe('#oAuthComplete()', () => { + beforeEach(() => { + getState.returns({ + auth: { + oauth: oauthData, + }, + }); + }); - (request.post as any).returns(Promise.resolve(resp)); + it('should post to api/oauth2/complete', () => { + (request.post as any).returns( + Promise.resolve({ + redirectUri: '', + }), + ); - return callThunk(oAuthComplete).then(() => { - expectDispatchCalls([ - [ - setOAuthCode({ - success: true, - code: '123', - displayCode: false, - }), - ], - ]); - }); + return callThunk(oAuthComplete).then(() => { + expect(request.post, 'to have a call satisfying', [ + '/api/oauth2/v1/complete?client_id=&redirect_uri=&response_type=&description=&scope=&prompt=none&login_hint=&state=', + {}, + ]); + }); + }); + + it('should dispatch setOAuthCode for static_page redirect', () => { + const resp = { + success: true, + redirectUri: 'static_page?code=123&state=', + }; + + (request.post as any).returns(Promise.resolve(resp)); + + return callThunk(oAuthComplete).then(() => { + expectDispatchCalls([ + [ + setOAuthCode({ + success: true, + code: '123', + displayCode: false, + }), + ], + ]); + }); + }); + + it('should resolve to with success false and redirectUri for access_denied', async () => { + const resp = { + statusCode: 401, + error: 'access_denied', + redirectUri: 'redirectUri', + }; + + (request.post as any).returns(Promise.reject(resp)); + + const data = await callThunk(oAuthComplete); + + expect(data, 'to equal', { + success: false, + redirectUri: 'redirectUri', + }); + }); + + it('should dispatch requirePermissionsAccept if accept_required', () => { + const resp = { + statusCode: 401, + error: 'accept_required', + }; + + (request.post as any).returns(Promise.reject(resp)); + + return callThunk(oAuthComplete).catch((error) => { + expect(error.acceptRequired, 'to be true'); + expectDispatchCalls([[requirePermissionsAccept()]]); + }); + }); }); - it('should resolve to with success false and redirectUri for access_denied', async () => { - const resp = { - statusCode: 401, - error: 'access_denied', - redirectUri: 'redirectUri', - }; + describe('#login()', () => { + describe('when correct login was entered', () => { + beforeEach(() => { + (request.post as any).returns( + Promise.reject({ + errors: { + password: 'error.password_required', + }, + }), + ); + }); - (request.post as any).returns(Promise.reject(resp)); - - const data = await callThunk(oAuthComplete); - - expect(data, 'to equal', { - success: false, - redirectUri: 'redirectUri', - }); + it('should set login', () => + callThunk(login, { login: 'foo' }).then(() => { + expectDispatchCalls([[setLogin('foo')]]); + })); + }); }); - - it('should dispatch requirePermissionsAccept if accept_required', () => { - const resp = { - statusCode: 401, - error: 'accept_required', - }; - - (request.post as any).returns(Promise.reject(resp)); - - return callThunk(oAuthComplete).catch((error) => { - expect(error.acceptRequired, 'to be true'); - expectDispatchCalls([[requirePermissionsAccept()]]); - }); - }); - }); - - describe('#login()', () => { - describe('when correct login was entered', () => { - beforeEach(() => { - (request.post as any).returns( - Promise.reject({ - errors: { - password: 'error.password_required', - }, - }), - ); - }); - - it('should set login', () => - callThunk(login, { login: 'foo' }).then(() => { - expectDispatchCalls([[setLogin('foo')]]); - })); - }); - }); }); diff --git a/packages/app/components/auth/actions.ts b/packages/app/components/auth/actions.ts index a18ef0b..a2ae2c5 100644 --- a/packages/app/components/auth/actions.ts +++ b/packages/app/components/auth/actions.ts @@ -4,23 +4,20 @@ import logger from 'app/services/logger'; import localStorage from 'app/services/localStorage'; import * as loader from 'app/services/loader'; import history from 'app/services/history'; -import { - updateUser, - acceptRules as userAcceptRules, -} from 'app/components/user/actions'; +import { updateUser, acceptRules as userAcceptRules } from 'app/components/user/actions'; import { authenticate, logoutAll } from 'app/components/accounts/actions'; import { getActiveAccount } from 'app/components/accounts/reducer'; import { - login as loginEndpoint, - forgotPassword as forgotPasswordEndpoint, - recoverPassword as recoverPasswordEndpoint, - OAuthResponse, + login as loginEndpoint, + forgotPassword as forgotPasswordEndpoint, + recoverPassword as recoverPasswordEndpoint, + OAuthResponse, } from 'app/services/api/authentication'; import oauth, { OauthData, Scope } from 'app/services/api/oauth'; import { - register as registerEndpoint, - activate as activateEndpoint, - resendActivation as resendActivationEndpoint, + register as registerEndpoint, + activate as activateEndpoint, + resendActivation as resendActivationEndpoint, } from 'app/services/api/signup'; import dispatchBsod from 'app/components/ui/bsod/dispatchBsod'; import { create as createPopup } from 'app/components/ui/popup/actions'; @@ -32,8 +29,8 @@ import { Resp } from 'app/services/request'; import { Credentials, Client, OAuthState, getCredentials } from './reducer'; interface ValidationErrorLiteral { - type: string; - payload: Record; + type: string; + payload: Record; } type ValidationError = string | ValidationErrorLiteral; @@ -47,28 +44,28 @@ type ValidationError = string | ValidationErrorLiteral; * @returns {object} - action definition */ export function goBack(options: { fallbackUrl?: string }) { - const { fallbackUrl } = options || {}; + const { fallbackUrl } = options || {}; - if (history.canGoBack()) { - browserHistory.goBack(); - } else if (fallbackUrl) { - browserHistory.push(fallbackUrl); - } + if (history.canGoBack()) { + browserHistory.goBack(); + } else if (fallbackUrl) { + browserHistory.push(fallbackUrl); + } - return { - type: 'noop', - }; + return { + type: 'noop', + }; } export function redirect(url: string): () => Promise { - loader.show(); + loader.show(); - return () => - new Promise(() => { - // do not resolve promise to make loader visible and - // overcome app rendering - location.href = url; - }); + return () => + new Promise(() => { + // do not resolve promise to make loader visible and + // overcome app rendering + location.href = url; + }); } const PASSWORD_REQUIRED = 'error.password_required'; @@ -77,177 +74,159 @@ const ACTIVATION_REQUIRED = 'error.account_not_activated'; const TOTP_REQUIRED = 'error.totp_required'; export function login({ - login = '', - password = '', - totp, - rememberMe = false, + login = '', + password = '', + totp, + rememberMe = false, }: { - login: string; - password?: string; - totp?: string; - rememberMe?: boolean; + login: string; + password?: string; + totp?: string; + rememberMe?: boolean; }) { - return wrapInLoader((dispatch) => - loginEndpoint({ login, password, totp, rememberMe }) - .then(authHandler(dispatch)) - .catch((resp) => { - if (resp.errors) { - if (resp.errors.password === PASSWORD_REQUIRED) { - return dispatch(setLogin(login)); - } else if (resp.errors.login === ACTIVATION_REQUIRED) { - return dispatch(needActivation()); - } else if (resp.errors.totp === TOTP_REQUIRED) { - return dispatch( - requestTotp({ - login, - password, - rememberMe, - }), - ); - } else if (resp.errors.login === LOGIN_REQUIRED && password) { - logger.warn('No login on password panel'); + return wrapInLoader((dispatch) => + loginEndpoint({ login, password, totp, rememberMe }) + .then(authHandler(dispatch)) + .catch((resp) => { + if (resp.errors) { + if (resp.errors.password === PASSWORD_REQUIRED) { + return dispatch(setLogin(login)); + } else if (resp.errors.login === ACTIVATION_REQUIRED) { + return dispatch(needActivation()); + } else if (resp.errors.totp === TOTP_REQUIRED) { + return dispatch( + requestTotp({ + login, + password, + rememberMe, + }), + ); + } else if (resp.errors.login === LOGIN_REQUIRED && password) { + logger.warn('No login on password panel'); - return dispatch(logoutAll()); - } - } + return dispatch(logoutAll()); + } + } - return Promise.reject(resp); - }) - .catch(validationErrorsHandler(dispatch)), - ); + return Promise.reject(resp); + }) + .catch(validationErrorsHandler(dispatch)), + ); } export function acceptRules() { - return wrapInLoader((dispatch) => - dispatch(userAcceptRules()).catch(validationErrorsHandler(dispatch)), - ); + return wrapInLoader((dispatch) => dispatch(userAcceptRules()).catch(validationErrorsHandler(dispatch))); } -export function forgotPassword({ - login = '', - captcha = '', -}: { - login: string; - captcha: string; -}) { - return wrapInLoader((dispatch, getState) => - forgotPasswordEndpoint(login, captcha) - .then(({ data = {} }) => - dispatch( - updateUser({ - maskedEmail: data.emailMask || getState().user.email, - }), - ), - ) - .catch(validationErrorsHandler(dispatch)), - ); +export function forgotPassword({ login = '', captcha = '' }: { login: string; captcha: string }) { + return wrapInLoader((dispatch, getState) => + forgotPasswordEndpoint(login, captcha) + .then(({ data = {} }) => + dispatch( + updateUser({ + maskedEmail: data.emailMask || getState().user.email, + }), + ), + ) + .catch(validationErrorsHandler(dispatch)), + ); } export function recoverPassword({ - key = '', - newPassword = '', - newRePassword = '', + key = '', + newPassword = '', + newRePassword = '', }: { - key: string; - newPassword: string; - newRePassword: string; + key: string; + newPassword: string; + newRePassword: string; }) { - return wrapInLoader((dispatch) => - recoverPasswordEndpoint(key, newPassword, newRePassword) - .then(authHandler(dispatch)) - .catch(validationErrorsHandler(dispatch, '/forgot-password')), - ); + return wrapInLoader((dispatch) => + recoverPasswordEndpoint(key, newPassword, newRePassword) + .then(authHandler(dispatch)) + .catch(validationErrorsHandler(dispatch, '/forgot-password')), + ); } export function register({ - email = '', - username = '', - password = '', - rePassword = '', - captcha = '', - rulesAgreement = false, + email = '', + username = '', + password = '', + rePassword = '', + captcha = '', + rulesAgreement = false, }: { - email: string; - username: string; - password: string; - rePassword: string; - captcha: string; - rulesAgreement: boolean; + email: string; + username: string; + password: string; + rePassword: string; + captcha: string; + rulesAgreement: boolean; }) { - return wrapInLoader((dispatch, getState) => - registerEndpoint({ - email, - username, - password, - rePassword, - rulesAgreement, - lang: getState().user.lang, - captcha, - }) - .then(() => { - dispatch( - updateUser({ + return wrapInLoader((dispatch, getState) => + registerEndpoint({ + email, username, - email, - }), - ); + password, + rePassword, + rulesAgreement, + lang: getState().user.lang, + captcha, + }) + .then(() => { + dispatch( + updateUser({ + username, + email, + }), + ); - dispatch(needActivation()); + dispatch(needActivation()); - browserHistory.push('/activation'); - }) - .catch(validationErrorsHandler(dispatch)), - ); + browserHistory.push('/activation'); + }) + .catch(validationErrorsHandler(dispatch)), + ); } -export function activate({ - key = '', -}: { - key: string; -}): ThunkAction> { - return wrapInLoader((dispatch) => - activateEndpoint(key) - .then(authHandler(dispatch)) - .catch(validationErrorsHandler(dispatch, '/resend-activation')), - ); +export function activate({ key = '' }: { key: string }): ThunkAction> { + return wrapInLoader((dispatch) => + activateEndpoint(key) + .then(authHandler(dispatch)) + .catch(validationErrorsHandler(dispatch, '/resend-activation')), + ); } -export function resendActivation({ - email = '', - captcha, -}: { - email: string; - captcha: string; -}) { - return wrapInLoader((dispatch) => - resendActivationEndpoint(email, captcha) - .then((resp) => { - dispatch( - updateUser({ - email, - }), - ); +export function resendActivation({ email = '', captcha }: { email: string; captcha: string }) { + return wrapInLoader((dispatch) => + resendActivationEndpoint(email, captcha) + .then((resp) => { + dispatch( + updateUser({ + email, + }), + ); - return resp; - }) - .catch(validationErrorsHandler(dispatch)), - ); + return resp; + }) + .catch(validationErrorsHandler(dispatch)), + ); } export function contactUs() { - return createPopup({ Popup: ContactForm }); + return createPopup({ Popup: ContactForm }); } interface SetCredentialsAction extends ReduxAction { - type: 'auth:setCredentials'; - payload: Credentials | null; + type: 'auth:setCredentials'; + payload: Credentials | null; } function setCredentials(payload: Credentials | null): SetCredentialsAction { - return { - type: 'auth:setCredentials', - payload, - }; + return { + type: 'auth:setCredentials', + payload, + }; } /** @@ -257,95 +236,92 @@ function setCredentials(payload: Credentials | null): SetCredentialsAction { * @param login */ export function setLogin(login: string | null): SetCredentialsAction { - return setCredentials(login ? { login } : null); + return setCredentials(login ? { login } : null); } export function relogin(login: string | null): ThunkAction { - return (dispatch, getState) => { - const credentials = getCredentials(getState()); - const returnUrl = - credentials.returnUrl || location.pathname + location.search; + return (dispatch, getState) => { + const credentials = getCredentials(getState()); + const returnUrl = credentials.returnUrl || location.pathname + location.search; - dispatch( - setCredentials({ - login, - returnUrl, - isRelogin: true, - }), - ); + dispatch( + setCredentials({ + login, + returnUrl, + isRelogin: true, + }), + ); - browserHistory.push('/login'); - }; + browserHistory.push('/login'); + }; } export type CredentialsAction = SetCredentialsAction; function requestTotp({ - login, - password, - rememberMe, + login, + password, + rememberMe, }: { - login: string; - password: string; - rememberMe: boolean; + login: string; + password: string; + rememberMe: boolean; }): ThunkAction { - return (dispatch, getState) => { - // merging with current credentials to propogate returnUrl - const credentials = getCredentials(getState()); + return (dispatch, getState) => { + // merging with current credentials to propogate returnUrl + const credentials = getCredentials(getState()); - dispatch( - setCredentials({ - ...credentials, - login, - password, - rememberMe, - isTotpRequired: true, - }), - ); - }; + dispatch( + setCredentials({ + ...credentials, + login, + password, + rememberMe, + isTotpRequired: true, + }), + ); + }; } interface SetSwitcherAction extends ReduxAction { - type: 'auth:setAccountSwitcher'; - payload: boolean; + type: 'auth:setAccountSwitcher'; + payload: boolean; } export function setAccountSwitcher(isOn: boolean): SetSwitcherAction { - return { - type: 'auth:setAccountSwitcher', - payload: isOn, - }; + return { + type: 'auth:setAccountSwitcher', + payload: isOn, + }; } export type AccountSwitcherAction = SetSwitcherAction; interface SetErrorAction extends ReduxAction { - type: 'auth:error'; - payload: Record | null; - error: boolean; + type: 'auth:error'; + payload: Record | null; + error: boolean; } -export function setErrors( - errors: Record | null, -): SetErrorAction { - return { - type: 'auth:error', - payload: errors, - error: true, - }; +export function setErrors(errors: Record | null): SetErrorAction { + return { + type: 'auth:error', + payload: errors, + error: true, + }; } export function clearErrors(): SetErrorAction { - return setErrors(null); + return setErrors(null); } export type ErrorAction = SetErrorAction; const KNOWN_SCOPES: ReadonlyArray = [ - 'minecraft_server_session', - 'offline_access', - 'account_info', - 'account_email', + 'minecraft_server_session', + 'offline_access', + 'account_info', + 'account_email', ]; /** * @param {object} oauthData @@ -366,50 +342,46 @@ const KNOWN_SCOPES: ReadonlyArray = [ * @returns {Promise} */ export function oAuthValidate(oauthData: OauthData) { - // TODO: move to oAuth actions? - // test request: /oauth?client_id=ely&redirect_uri=http%3A%2F%2Fely.by&response_type=code&scope=minecraft_server_session&description=foo - return wrapInLoader((dispatch) => - oauth - .validate(oauthData) - .then((resp) => { - const { scopes } = resp.session; - const invalidScopes = scopes.filter( - (scope) => !KNOWN_SCOPES.includes(scope), - ); - let prompt = (oauthData.prompt || 'none') - .split(',') - .map((item) => item.trim()); + // TODO: move to oAuth actions? + // test request: /oauth?client_id=ely&redirect_uri=http%3A%2F%2Fely.by&response_type=code&scope=minecraft_server_session&description=foo + return wrapInLoader((dispatch) => + oauth + .validate(oauthData) + .then((resp) => { + const { scopes } = resp.session; + const invalidScopes = scopes.filter((scope) => !KNOWN_SCOPES.includes(scope)); + let prompt = (oauthData.prompt || 'none').split(',').map((item) => item.trim()); - if (prompt.includes('none')) { - prompt = ['none']; - } + if (prompt.includes('none')) { + prompt = ['none']; + } - if (invalidScopes.length) { - logger.error('Got invalid scopes after oauth validation', { - invalidScopes, - }); - } + if (invalidScopes.length) { + logger.error('Got invalid scopes after oauth validation', { + invalidScopes, + }); + } - dispatch(setClient(resp.client)); - dispatch( - setOAuthRequest({ - ...resp.oAuth, - prompt: oauthData.prompt || 'none', - loginHint: oauthData.loginHint, - }), - ); - dispatch(setScopes(scopes)); - localStorage.setItem( - 'oauthData', - JSON.stringify({ - // @see services/authFlow/AuthFlow - timestamp: Date.now(), - payload: oauthData, - }), - ); - }) - .catch(handleOauthParamsValidation), - ); + dispatch(setClient(resp.client)); + dispatch( + setOAuthRequest({ + ...resp.oAuth, + prompt: oauthData.prompt || 'none', + loginHint: oauthData.loginHint, + }), + ); + dispatch(setScopes(scopes)); + localStorage.setItem( + 'oauthData', + JSON.stringify({ + // @see services/authFlow/AuthFlow + timestamp: Date.now(), + payload: oauthData, + }), + ); + }) + .catch(handleOauthParamsValidation), + ); } /** @@ -419,304 +391,283 @@ export function oAuthValidate(oauthData: OauthData) { * @returns {Promise} */ export function oAuthComplete(params: { accept?: boolean } = {}) { - return wrapInLoader( - async ( - dispatch, - getState, - ): Promise<{ - success: boolean; - redirectUri: string; - }> => { - const oauthData = getState().auth.oauth; + return wrapInLoader( + async ( + dispatch, + getState, + ): Promise<{ + success: boolean; + redirectUri: string; + }> => { + const oauthData = getState().auth.oauth; - if (!oauthData) { - throw new Error('Can not complete oAuth. Oauth data does not exist'); - } - - try { - const resp = await oauth.complete(oauthData, params); - localStorage.removeItem('oauthData'); - - if (resp.redirectUri.startsWith('static_page')) { - const displayCode = /static_page_with_code/.test(resp.redirectUri); - - const [, code] = resp.redirectUri.match(/code=(.+)&/) || []; - [, resp.redirectUri] = resp.redirectUri.match(/^(.+)\?/) || []; - - dispatch( - setOAuthCode({ - success: resp.success, - code, - displayCode, - }), - ); - } - - return resp; - } catch (error) { - const resp: - | { - acceptRequired: boolean; + if (!oauthData) { + throw new Error('Can not complete oAuth. Oauth data does not exist'); } - | { - unauthorized: boolean; - } = error; - if ('acceptRequired' in resp) { - dispatch(requirePermissionsAccept()); + try { + const resp = await oauth.complete(oauthData, params); + localStorage.removeItem('oauthData'); - return Promise.reject(resp); - } + if (resp.redirectUri.startsWith('static_page')) { + const displayCode = /static_page_with_code/.test(resp.redirectUri); - return handleOauthParamsValidation(resp); - } - }, - ); + const [, code] = resp.redirectUri.match(/code=(.+)&/) || []; + [, resp.redirectUri] = resp.redirectUri.match(/^(.+)\?/) || []; + + dispatch( + setOAuthCode({ + success: resp.success, + code, + displayCode, + }), + ); + } + + return resp; + } catch (error) { + const resp: + | { + acceptRequired: boolean; + } + | { + unauthorized: boolean; + } = error; + + if ('acceptRequired' in resp) { + dispatch(requirePermissionsAccept()); + + return Promise.reject(resp); + } + + return handleOauthParamsValidation(resp); + } + }, + ); } function handleOauthParamsValidation( - resp: { - [key: string]: any; - userMessage?: string; - } = {}, + resp: { + [key: string]: any; + userMessage?: string; + } = {}, ) { - dispatchBsod(); - localStorage.removeItem('oauthData'); + dispatchBsod(); + localStorage.removeItem('oauthData'); - // eslint-disable-next-line no-alert - resp.userMessage && setTimeout(() => alert(resp.userMessage), 500); // 500 ms to allow re-render + // eslint-disable-next-line no-alert + resp.userMessage && setTimeout(() => alert(resp.userMessage), 500); // 500 ms to allow re-render - return Promise.reject(resp); + return Promise.reject(resp); } interface SetClientAction extends ReduxAction { - type: 'set_client'; - payload: Client; + type: 'set_client'; + payload: Client; } export function setClient(payload: Client): SetClientAction { - return { - type: 'set_client', - payload, - }; + return { + type: 'set_client', + payload, + }; } export type ClientAction = SetClientAction; interface SetOauthAction extends ReduxAction { - type: 'set_oauth'; - payload: Pick< - OAuthState, - | 'clientId' - | 'redirectUrl' - | 'responseType' - | 'scope' - | 'prompt' - | 'loginHint' - | 'state' - >; + type: 'set_oauth'; + payload: Pick; } // Input data is coming right from the query string, so the names // are the same, as used for initializing OAuth2 request export function setOAuthRequest(data: { - client_id?: string; - redirect_uri?: string; - response_type?: string; - scope?: string; - prompt?: string; - loginHint?: string; - state?: string; + client_id?: string; + redirect_uri?: string; + response_type?: string; + scope?: string; + prompt?: string; + loginHint?: string; + state?: string; }): SetOauthAction { - return { - type: 'set_oauth', - payload: { - // TODO: there is too much default empty string. Maybe we can somehow validate it - // on the level, where this action is called? - clientId: data.client_id || '', - redirectUrl: data.redirect_uri || '', - responseType: data.response_type || '', - scope: data.scope || '', - prompt: data.prompt || '', - loginHint: data.loginHint || '', - state: data.state || '', - }, - }; + return { + type: 'set_oauth', + payload: { + // TODO: there is too much default empty string. Maybe we can somehow validate it + // on the level, where this action is called? + clientId: data.client_id || '', + redirectUrl: data.redirect_uri || '', + responseType: data.response_type || '', + scope: data.scope || '', + prompt: data.prompt || '', + loginHint: data.loginHint || '', + state: data.state || '', + }, + }; } interface SetOAuthResultAction extends ReduxAction { - type: 'set_oauth_result'; - payload: Pick; + type: 'set_oauth_result'; + payload: Pick; } export const SET_OAUTH_RESULT = 'set_oauth_result'; // TODO: remove -export function setOAuthCode(payload: { - success: boolean; - code: string; - displayCode: boolean; -}): SetOAuthResultAction { - return { - type: 'set_oauth_result', - payload, - }; +export function setOAuthCode(payload: { success: boolean; code: string; displayCode: boolean }): SetOAuthResultAction { + return { + type: 'set_oauth_result', + payload, + }; } export function resetOAuth(): ThunkAction { - return (dispatch): void => { - localStorage.removeItem('oauthData'); - dispatch(setOAuthRequest({})); - }; + return (dispatch): void => { + localStorage.removeItem('oauthData'); + dispatch(setOAuthRequest({})); + }; } /** * Resets all temporary state related to auth */ export function resetAuth(): ThunkAction { - return (dispatch, getSate): Promise => { - dispatch(setLogin(null)); - dispatch(resetOAuth()); - // ensure current account is valid - const activeAccount = getActiveAccount(getSate()); + return (dispatch, getSate): Promise => { + dispatch(setLogin(null)); + dispatch(resetOAuth()); + // ensure current account is valid + const activeAccount = getActiveAccount(getSate()); - if (activeAccount) { - return Promise.resolve(dispatch(authenticate(activeAccount))) - .then(() => {}) - .catch(() => { - // its okay. user will be redirected to an appropriate place - }); - } + if (activeAccount) { + return Promise.resolve(dispatch(authenticate(activeAccount))) + .then(() => {}) + .catch(() => { + // its okay. user will be redirected to an appropriate place + }); + } - return Promise.resolve(); - }; + return Promise.resolve(); + }; } interface RequestPermissionsAcceptAction extends ReduxAction { - type: 'require_permissions_accept'; + type: 'require_permissions_accept'; } export function requirePermissionsAccept(): RequestPermissionsAcceptAction { - return { - type: 'require_permissions_accept', - }; + return { + type: 'require_permissions_accept', + }; } -export type OAuthAction = - | SetOauthAction - | SetOAuthResultAction - | RequestPermissionsAcceptAction; +export type OAuthAction = SetOauthAction | SetOAuthResultAction | RequestPermissionsAcceptAction; interface SetScopesAction extends ReduxAction { - type: 'set_scopes'; - payload: Array; + type: 'set_scopes'; + payload: Array; } export function setScopes(payload: Array): SetScopesAction { - return { - type: 'set_scopes', - payload, - }; + return { + type: 'set_scopes', + payload, + }; } export type ScopesAction = SetScopesAction; interface SetLoadingAction extends ReduxAction { - type: 'set_loading_state'; - payload: boolean; + type: 'set_loading_state'; + payload: boolean; } export function setLoadingState(isLoading: boolean): SetLoadingAction { - return { - type: 'set_loading_state', - payload: isLoading, - }; + return { + type: 'set_loading_state', + payload: isLoading, + }; } function wrapInLoader(fn: ThunkAction>): ThunkAction> { - return (dispatch, getState) => { - dispatch(setLoadingState(true)); - const endLoading = () => dispatch(setLoadingState(false)); + return (dispatch, getState) => { + dispatch(setLoadingState(true)); + const endLoading = () => dispatch(setLoadingState(false)); - return fn(dispatch, getState, undefined).then( - (resp) => { - endLoading(); + return fn(dispatch, getState, undefined).then( + (resp) => { + endLoading(); - return resp; - }, - (resp) => { - endLoading(); + return resp; + }, + (resp) => { + endLoading(); - return Promise.reject(resp); - }, - ); - }; + return Promise.reject(resp); + }, + ); + }; } export type LoadingAction = SetLoadingAction; function needActivation() { - return updateUser({ - isActive: false, - isGuest: false, - }); -} - -function authHandler(dispatch: Dispatch) { - return (oAuthResp: OAuthResponse): Promise => - dispatch( - authenticate({ - token: oAuthResp.access_token, - refreshToken: oAuthResp.refresh_token || null, - }), - ).then((resp) => { - dispatch(setLogin(null)); - - return resp; + return updateUser({ + isActive: false, + isGuest: false, }); } -function validationErrorsHandler( - dispatch: Dispatch, - repeatUrl?: string, -): ( - resp: Resp<{ - errors?: Record; - data?: Record; - }>, -) => Promise { - return (resp) => { - if (resp.errors) { - const [firstError] = Object.keys(resp.errors); - const firstErrorObj: ValidationError = { - type: resp.errors[firstError] as string, - payload: { - isGuest: true, - repeatUrl: '', - }, - }; +function authHandler(dispatch: Dispatch) { + return (oAuthResp: OAuthResponse): Promise => + dispatch( + authenticate({ + token: oAuthResp.access_token, + refreshToken: oAuthResp.refresh_token || null, + }), + ).then((resp) => { + dispatch(setLogin(null)); - if (resp.data) { - // TODO: this should be formatted on backend - Object.assign(firstErrorObj.payload, resp.data); - } - - if ( - ['error.key_not_exists', 'error.key_expire'].includes( - firstErrorObj.type, - ) && - repeatUrl - ) { - // TODO: this should be formatted on backend - firstErrorObj.payload.repeatUrl = repeatUrl; - } - - // TODO: can I clone the object or its necessary to catch modified errors list on corresponding catches? - const { errors } = resp; - errors[firstError] = firstErrorObj; - - dispatch(setErrors(errors)); - } - - return Promise.reject(resp); - }; + return resp; + }); +} + +function validationErrorsHandler( + dispatch: Dispatch, + repeatUrl?: string, +): ( + resp: Resp<{ + errors?: Record; + data?: Record; + }>, +) => Promise { + return (resp) => { + if (resp.errors) { + const [firstError] = Object.keys(resp.errors); + const firstErrorObj: ValidationError = { + type: resp.errors[firstError] as string, + payload: { + isGuest: true, + repeatUrl: '', + }, + }; + + if (resp.data) { + // TODO: this should be formatted on backend + Object.assign(firstErrorObj.payload, resp.data); + } + + if (['error.key_not_exists', 'error.key_expire'].includes(firstErrorObj.type) && repeatUrl) { + // TODO: this should be formatted on backend + firstErrorObj.payload.repeatUrl = repeatUrl; + } + + // TODO: can I clone the object or its necessary to catch modified errors list on corresponding catches? + const { errors } = resp; + errors[firstError] = firstErrorObj; + + dispatch(setErrors(errors)); + } + + return Promise.reject(resp); + }; } diff --git a/packages/app/components/auth/activation/Activation.intl.json b/packages/app/components/auth/activation/Activation.intl.json index adec44a..0c5efd3 100644 --- a/packages/app/components/auth/activation/Activation.intl.json +++ b/packages/app/components/auth/activation/Activation.intl.json @@ -1,8 +1,8 @@ { - "accountActivationTitle": "Account activation", - "activationMailWasSent": "Please check {email} for the message with further instructions", - "activationMailWasSentNoEmail": "Please check your E‑mail for the message with further instructions", - "confirmEmail": "Confirm E‑mail", - "didNotReceivedEmail": "Did not received E‑mail?", - "enterTheCode": "Enter the code from E‑mail here" + "accountActivationTitle": "Account activation", + "activationMailWasSent": "Please check {email} for the message with further instructions", + "activationMailWasSentNoEmail": "Please check your E‑mail for the message with further instructions", + "confirmEmail": "Confirm E‑mail", + "didNotReceivedEmail": "Did not received E‑mail?", + "enterTheCode": "Enter the code from E‑mail here" } diff --git a/packages/app/components/auth/activation/Activation.ts b/packages/app/components/auth/activation/Activation.ts index d43e1cf..9c2e737 100644 --- a/packages/app/components/auth/activation/Activation.ts +++ b/packages/app/components/auth/activation/Activation.ts @@ -3,13 +3,13 @@ import messages from './Activation.intl.json'; import Body from './ActivationBody'; export default factory({ - title: messages.accountActivationTitle, - body: Body, - footer: { - color: 'blue', - label: messages.confirmEmail, - }, - links: { - label: messages.didNotReceivedEmail, - }, + title: messages.accountActivationTitle, + body: Body, + footer: { + color: 'blue', + label: messages.confirmEmail, + }, + links: { + label: messages.didNotReceivedEmail, + }, }); diff --git a/packages/app/components/auth/activation/ActivationBody.tsx b/packages/app/components/auth/activation/ActivationBody.tsx index 45dc4cd..a27f4cb 100644 --- a/packages/app/components/auth/activation/ActivationBody.tsx +++ b/packages/app/components/auth/activation/ActivationBody.tsx @@ -9,49 +9,48 @@ import styles from './activation.scss'; import messages from './Activation.intl.json'; export default class ActivationBody extends BaseAuthBody { - static displayName = 'ActivationBody'; - static panelId = 'activation'; + static displayName = 'ActivationBody'; + static panelId = 'activation'; - autoFocusField = - this.props.match.params && this.props.match.params.key ? null : 'key'; + autoFocusField = this.props.match.params && this.props.match.params.key ? null : 'key'; - render() { - const { key } = this.props.match.params; - const { email } = this.context.user; + render() { + const { key } = this.props.match.params; + const { email } = this.context.user; - return ( -
- {this.renderErrors()} + return ( +
+ {this.renderErrors()} -
-
+
+
-
- {email ? ( - {email}, - }} - /> - ) : ( - - )} -
-
-
- -
-
- ); - } +
+ {email ? ( + {email}, + }} + /> + ) : ( + + )} +
+
+
+ +
+
+ ); + } } diff --git a/packages/app/components/auth/activation/activation.scss b/packages/app/components/auth/activation/activation.scss index dd817d1..7d2746b 100644 --- a/packages/app/components/auth/activation/activation.scss +++ b/packages/app/components/auth/activation/activation.scss @@ -5,15 +5,15 @@ } .descriptionImage { - composes: envelope from '~app/components/ui/icons.scss'; + composes: envelope from '~app/components/ui/icons.scss'; - font-size: 100px; - color: $blue; + font-size: 100px; + color: $blue; } .descriptionText { - font-family: $font-family-title; - margin: 5px 0 19px; - line-height: 1.4; - font-size: 16px; + font-family: $font-family-title; + margin: 5px 0 19px; + line-height: 1.4; + font-size: 16px; } diff --git a/packages/app/components/auth/appInfo/AppInfo.intl.json b/packages/app/components/auth/appInfo/AppInfo.intl.json index 8cd2c53..1f25d49 100644 --- a/packages/app/components/auth/appInfo/AppInfo.intl.json +++ b/packages/app/components/auth/appInfo/AppInfo.intl.json @@ -1,7 +1,7 @@ { - "appName": "Ely Accounts", - "goToAuth": "Go to auth", - "appDescription": "You are on the Ely.by authorization service, that allows you to safely perform any operations on your account. This single entry point for websites and desktop software, including game launchers.", - "useItYourself": "Visit our {link}, to learn how to use this service in you projects.", - "documentation": "documentation" + "appName": "Ely Accounts", + "goToAuth": "Go to auth", + "appDescription": "You are on the Ely.by authorization service, that allows you to safely perform any operations on your account. This single entry point for websites and desktop software, including game launchers.", + "useItYourself": "Visit our {link}, to learn how to use this service in you projects.", + "documentation": "documentation" } diff --git a/packages/app/components/auth/appInfo/AppInfo.tsx b/packages/app/components/auth/appInfo/AppInfo.tsx index eec0634..004f819 100644 --- a/packages/app/components/auth/appInfo/AppInfo.tsx +++ b/packages/app/components/auth/appInfo/AppInfo.tsx @@ -7,51 +7,49 @@ import styles from './appInfo.scss'; import messages from './AppInfo.intl.json'; export default class AppInfo extends React.Component<{ - name?: string; - description?: string; - onGoToAuth: () => void; + name?: string; + description?: string; + onGoToAuth: () => void; }> { - render() { - const { name, description, onGoToAuth } = this.props; + render() { + const { name, description, onGoToAuth } = this.props; - return ( -
-
-

- {name ? name : } -

-
-
- {description ? ( -

{description}

- ) : ( -
-

- -

-

- - - - ), - }} - /> -

+ return ( +
+
+

{name ? name : }

+
+
+ {description ? ( +

{description}

+ ) : ( +
+

+ +

+

+ + + + ), + }} + /> +

+
+ )} +
+
+
+ +
+ +
- )} -
-
-
- -
- -
-
- ); - } + ); + } } diff --git a/packages/app/components/auth/appInfo/appInfo.scss b/packages/app/components/auth/appInfo/appInfo.scss index 5242235..febbe2c 100644 --- a/packages/app/components/auth/appInfo/appInfo.scss +++ b/packages/app/components/auth/appInfo/appInfo.scss @@ -2,71 +2,71 @@ @import '~app/components/ui/fonts.scss'; .appInfo { - max-width: 270px; - margin: 0 auto; - padding: 55px 25px; + max-width: 270px; + margin: 0 auto; + padding: 55px 25px; } .logoContainer { - position: relative; - padding: 15px 0; + position: relative; + padding: 15px 0; - &:after { - content: ''; - display: block; + &:after { + content: ''; + display: block; - position: absolute; - left: 0; - bottom: 0; - height: 3px; - width: 40px; + position: absolute; + left: 0; + bottom: 0; + height: 3px; + width: 40px; - background: $green; - } + background: $green; + } } .logo { - font-family: $font-family-title; - color: #fff; - font-size: 36px; + font-family: $font-family-title; + color: #fff; + font-size: 36px; } .descriptionContainer { - margin: 20px 0; + margin: 20px 0; } .description { - $font-color: #ccc; - font-family: $font-family-base; - color: $font-color; - font-size: 13px; - line-height: 1.7; - margin-top: 7px; + $font-color: #ccc; + font-family: $font-family-base; + color: $font-color; + font-size: 13px; + line-height: 1.7; + margin-top: 7px; - a { - color: lighten($font-color, 10%); - border-bottom-color: #666; + a { + color: lighten($font-color, 10%); + border-bottom-color: #666; - &:hover { - color: $font-color; + &:hover { + color: $font-color; + } } - } } .goToAuth { } @media (min-width: 720px) { - .goToAuth { - display: none; - } + .goToAuth { + display: none; + } } .footer { - position: absolute; - bottom: 10px; - left: 0; - right: 0; - text-align: center; - line-height: 1.5; + position: absolute; + bottom: 10px; + left: 0; + right: 0; + text-align: center; + line-height: 1.5; } diff --git a/packages/app/components/auth/auth.scss b/packages/app/components/auth/auth.scss index b3bf69c..877a295 100644 --- a/packages/app/components/auth/auth.scss +++ b/packages/app/components/auth/auth.scss @@ -1,3 +1,3 @@ .checkboxInput { - margin-top: 15px; + margin-top: 15px; } diff --git a/packages/app/components/auth/authError/AuthError.tsx b/packages/app/components/auth/authError/AuthError.tsx index 08b29b0..b3582e8 100644 --- a/packages/app/components/auth/authError/AuthError.tsx +++ b/packages/app/components/auth/authError/AuthError.tsx @@ -5,41 +5,36 @@ import { PanelBodyHeader } from 'app/components/ui/Panel'; import { ValidationError } from 'app/components/ui/form/FormModel'; interface Props { - error: ValidationError; - onClose?: () => void; + error: ValidationError; + onClose?: () => void; } let autoHideTimer: number | null = null; function resetTimeout(): void { - if (autoHideTimer) { - clearTimeout(autoHideTimer); - autoHideTimer = null; - } + if (autoHideTimer) { + clearTimeout(autoHideTimer); + autoHideTimer = null; + } } const AuthError: ComponentType = ({ error, onClose }) => { - useEffect(() => { - resetTimeout(); + useEffect(() => { + resetTimeout(); - if ( - onClose && - typeof error !== 'string' && - error.payload && - error.payload.canRepeatIn - ) { - const msLeft = error.payload.canRepeatIn * 1000; - // 1500 to let the user see, that time is elapsed - setTimeout(onClose, msLeft - Date.now() + 1500); - } + if (onClose && typeof error !== 'string' && error.payload && error.payload.canRepeatIn) { + const msLeft = error.payload.canRepeatIn * 1000; + // 1500 to let the user see, that time is elapsed + setTimeout(onClose, msLeft - Date.now() + 1500); + } - return resetTimeout; - }, [error, onClose]); + return resetTimeout; + }, [error, onClose]); - return ( - - {resolveError(error)} - - ); + return ( + + {resolveError(error)} + + ); }; export default AuthError; diff --git a/packages/app/components/auth/chooseAccount/ChooseAccount.intl.json b/packages/app/components/auth/chooseAccount/ChooseAccount.intl.json index ab70d46..1770688 100644 --- a/packages/app/components/auth/chooseAccount/ChooseAccount.intl.json +++ b/packages/app/components/auth/chooseAccount/ChooseAccount.intl.json @@ -1,7 +1,7 @@ { - "chooseAccountTitle": "Choose an account", - "addAccount": "Log into another account", - "logoutAll": "Log out from all accounts", - "pleaseChooseAccount": "Please select an account you're willing to use", - "pleaseChooseAccountForApp": "Please select an account that you want to use to authorize {appName}" + "chooseAccountTitle": "Choose an account", + "addAccount": "Log into another account", + "logoutAll": "Log out from all accounts", + "pleaseChooseAccount": "Please select an account you're willing to use", + "pleaseChooseAccountForApp": "Please select an account that you want to use to authorize {appName}" } diff --git a/packages/app/components/auth/chooseAccount/ChooseAccount.ts b/packages/app/components/auth/chooseAccount/ChooseAccount.ts index be84023..cfa04ee 100644 --- a/packages/app/components/auth/chooseAccount/ChooseAccount.ts +++ b/packages/app/components/auth/chooseAccount/ChooseAccount.ts @@ -3,14 +3,14 @@ import messages from './ChooseAccount.intl.json'; import Body from './ChooseAccountBody'; export default factory({ - title: messages.chooseAccountTitle, - body: Body, - footer: { - label: messages.addAccount, - }, - links: [ - { - label: messages.logoutAll, + title: messages.chooseAccountTitle, + body: Body, + footer: { + label: messages.addAccount, }, - ], + links: [ + { + label: messages.logoutAll, + }, + ], }); diff --git a/packages/app/components/auth/chooseAccount/ChooseAccountBody.tsx b/packages/app/components/auth/chooseAccount/ChooseAccountBody.tsx index 7ea8286..906e457 100644 --- a/packages/app/components/auth/chooseAccount/ChooseAccountBody.tsx +++ b/packages/app/components/auth/chooseAccount/ChooseAccountBody.tsx @@ -10,44 +10,44 @@ import styles from './chooseAccount.scss'; import messages from './ChooseAccount.intl.json'; export default class ChooseAccountBody extends BaseAuthBody { - static displayName = 'ChooseAccountBody'; - static panelId = 'chooseAccount'; + static displayName = 'ChooseAccountBody'; + static panelId = 'chooseAccount'; - render() { - const { client } = this.context.auth; + render() { + const { client } = this.context.auth; - return ( -
- {this.renderErrors()} + return ( +
+ {this.renderErrors()} -
- {client ? ( - {client.name}, - }} - /> - ) : ( -
- +
+ {client ? ( + {client.name}, + }} + /> + ) : ( +
+ +
+ )} +
+ +
+ +
- )} -
+ ); + } -
- -
-
- ); - } - - onSwitch = (account: Account): void => { - this.context.resolve(account); - }; + onSwitch = (account: Account): void => { + this.context.resolve(account); + }; } diff --git a/packages/app/components/auth/chooseAccount/chooseAccount.scss b/packages/app/components/auth/chooseAccount/chooseAccount.scss index afe267d..9dfa7bd 100644 --- a/packages/app/components/auth/chooseAccount/chooseAccount.scss +++ b/packages/app/components/auth/chooseAccount/chooseAccount.scss @@ -2,17 +2,17 @@ @import '~app/components/ui/fonts.scss'; .accountSwitcherContainer { - margin-left: -$bodyLeftRightPadding; - margin-right: -$bodyLeftRightPadding; + margin-left: -$bodyLeftRightPadding; + margin-right: -$bodyLeftRightPadding; } .description { - font-family: $font-family-title; - margin: 5px 0 19px; - line-height: 1.4; - font-size: 16px; + font-family: $font-family-title; + margin: 5px 0 19px; + line-height: 1.4; + font-size: 16px; } .appName { - color: #fff; + color: #fff; } diff --git a/packages/app/components/auth/factory.tsx b/packages/app/components/auth/factory.tsx index 9cbef7f..a8a4395 100644 --- a/packages/app/components/auth/factory.tsx +++ b/packages/app/components/auth/factory.tsx @@ -7,44 +7,36 @@ import { Color } from 'app/components/ui'; import BaseAuthBody from './BaseAuthBody'; export type Factory = () => { - Title: ComponentType; - Body: typeof BaseAuthBody; - Footer: ComponentType; - Links: ComponentType; + Title: ComponentType; + Body: typeof BaseAuthBody; + Footer: ComponentType; + Links: ComponentType; }; type RejectionLinkProps = ComponentProps; interface FactoryParams { - title: MessageDescriptor; - body: typeof BaseAuthBody; - footer: { - color?: Color; - label: string | MessageDescriptor; - autoFocus?: boolean; - }; - links?: RejectionLinkProps | Array; + title: MessageDescriptor; + body: typeof BaseAuthBody; + footer: { + color?: Color; + label: string | MessageDescriptor; + autoFocus?: boolean; + }; + links?: RejectionLinkProps | Array; } -export default function ({ - title, - body, - footer, - links, -}: FactoryParams): Factory { - return () => ({ - Title: () => , - Body: body, - Footer: () =>
+ ) : ( +
+ +
+ )} +
+ ) : ( +
+
+
+ {appName}, + }} + /> +
+
+ +
+
+ )}
- {displayCode ? ( -
-
- -
-
-
{code}
-
-
- ) : ( -
- -
- )} -
- ) : ( -
-
-
- {appName}, - }} - /> -
-
- -
-
- )} -
- ); - } - - onCopyClick: MouseEventHandler = (event) => { - event.preventDefault(); - - const { code } = this.props; - - if (code) { - copy(code); + ); } - }; + + onCopyClick: MouseEventHandler = (event) => { + event.preventDefault(); + + const { code } = this.props; + + if (code) { + copy(code); + } + }; } export default connect(({ auth }: RootState) => { - if (!auth || !auth.client || !auth.oauth) { - throw new Error('Can not connect Finish component. No auth data in state'); - } + if (!auth || !auth.client || !auth.oauth) { + throw new Error('Can not connect Finish component. No auth data in state'); + } - return { - appName: auth.client.name, - code: auth.oauth.code, - displayCode: auth.oauth.displayCode, - state: auth.oauth.state, - success: auth.oauth.success, - }; + return { + appName: auth.client.name, + code: auth.oauth.code, + displayCode: auth.oauth.displayCode, + state: auth.oauth.state, + success: auth.oauth.success, + }; })(Finish); diff --git a/packages/app/components/auth/finish/finish.scss b/packages/app/components/auth/finish/finish.scss index 36e8719..52d1a24 100644 --- a/packages/app/components/auth/finish/finish.scss +++ b/packages/app/components/auth/finish/finish.scss @@ -2,75 +2,75 @@ @import '~app/components/ui/fonts.scss'; .finishPage { - font-family: $font-family-title; - position: relative; - max-width: 515px; - padding-top: 40px; - margin: 0 auto; - text-align: center; + font-family: $font-family-title; + position: relative; + max-width: 515px; + padding-top: 40px; + margin: 0 auto; + text-align: center; } .iconBackground { - position: absolute; - top: -15px; - transform: translateX(-50%); - font-size: 200px; - color: #e0d9cf; - z-index: -1; + position: absolute; + top: -15px; + transform: translateX(-50%); + font-size: 200px; + color: #e0d9cf; + z-index: -1; } .successBackground { - composes: checkmark from '~app/components/ui/icons.scss'; - @extend .iconBackground; + composes: checkmark from '~app/components/ui/icons.scss'; + @extend .iconBackground; } .failBackground { - composes: close from '~app/components/ui/icons.scss'; - @extend .iconBackground; + composes: close from '~app/components/ui/icons.scss'; + @extend .iconBackground; } .title { - font-size: 22px; - margin-bottom: 10px; + font-size: 22px; + margin-bottom: 10px; } .greenTitle { - composes: title; + composes: title; - color: $green; + color: $green; - .appName { - color: darker($green); - } + .appName { + color: darker($green); + } } .redTitle { - composes: title; + composes: title; - color: $red; + color: $red; - .appName { - color: darker($red); - } + .appName { + color: darker($red); + } } .description { - font-size: 18px; - margin-bottom: 10px; + font-size: 18px; + margin-bottom: 10px; } .codeContainer { - margin-bottom: 5px; - margin-top: 35px; + margin-bottom: 5px; + margin-top: 35px; } .code { - $border: 5px solid darker($green); + $border: 5px solid darker($green); - display: inline-block; - border-right: $border; - border-left: $border; - padding: 5px 10px; - word-break: break-all; - text-align: center; + display: inline-block; + border-right: $border; + border-left: $border; + padding: 5px 10px; + word-break: break-all; + text-align: center; } diff --git a/packages/app/components/auth/forgotPassword/ForgotPassword.intl.json b/packages/app/components/auth/forgotPassword/ForgotPassword.intl.json index 571a5bb..04eca46 100644 --- a/packages/app/components/auth/forgotPassword/ForgotPassword.intl.json +++ b/packages/app/components/auth/forgotPassword/ForgotPassword.intl.json @@ -1,7 +1,7 @@ { - "title": "Forgot password", - "sendMail": "Send mail", - "specifyEmail": "Specify the registration E‑mail address or last used username for your account and we will send an E‑mail with instructions for further password recovery.", - "pleasePressButton": "Please press the button bellow to get an E‑mail with password recovery code.", - "alreadyHaveCode": "Already have a code" + "title": "Forgot password", + "sendMail": "Send mail", + "specifyEmail": "Specify the registration E‑mail address or last used username for your account and we will send an E‑mail with instructions for further password recovery.", + "pleasePressButton": "Please press the button bellow to get an E‑mail with password recovery code.", + "alreadyHaveCode": "Already have a code" } diff --git a/packages/app/components/auth/forgotPassword/ForgotPassword.ts b/packages/app/components/auth/forgotPassword/ForgotPassword.ts index 73725a5..79641fb 100644 --- a/packages/app/components/auth/forgotPassword/ForgotPassword.ts +++ b/packages/app/components/auth/forgotPassword/ForgotPassword.ts @@ -3,14 +3,14 @@ import messages from './ForgotPassword.intl.json'; import Body from './ForgotPasswordBody'; export default factory({ - title: messages.title, - body: Body, - footer: { - color: 'lightViolet', - autoFocus: true, - label: messages.sendMail, - }, - links: { - label: messages.alreadyHaveCode, - }, + title: messages.title, + body: Body, + footer: { + color: 'lightViolet', + autoFocus: true, + label: messages.sendMail, + }, + links: { + label: messages.alreadyHaveCode, + }, }); diff --git a/packages/app/components/auth/forgotPassword/ForgotPasswordBody.tsx b/packages/app/components/auth/forgotPassword/ForgotPasswordBody.tsx index f2448cc..2dfc790 100644 --- a/packages/app/components/auth/forgotPassword/ForgotPasswordBody.tsx +++ b/packages/app/components/auth/forgotPassword/ForgotPasswordBody.tsx @@ -11,87 +11,83 @@ import styles from './forgotPassword.scss'; import messages from './ForgotPassword.intl.json'; export default class ForgotPasswordBody extends BaseAuthBody { - static displayName = 'ForgotPasswordBody'; - static panelId = 'forgotPassword'; - static hasGoBack = true; + static displayName = 'ForgotPasswordBody'; + static panelId = 'forgotPassword'; + static hasGoBack = true; - state = { - isLoginEdit: false, - }; + state = { + isLoginEdit: false, + }; - autoFocusField = 'login'; + autoFocusField = 'login'; - render() { - const { isLoginEdit } = this.state; + render() { + const { isLoginEdit } = this.state; - const login = this.getLogin(); - const isLoginEditShown = isLoginEdit || !login; + const login = this.getLogin(); + const isLoginEditShown = isLoginEdit || !login; - return ( -
- {this.renderErrors()} + return ( +
+ {this.renderErrors()} - + - {isLoginEditShown ? ( -
-

- -

- -
- ) : ( -
-
- {login} - + {isLoginEditShown ? ( +
+

+ +

+ +
+ ) : ( +
+
+ {login} + +
+

+ +

+
+ )} + +
-

- -

-
- )} - - -
- ); - } - - serialize() { - const data = super.serialize(); - - if (!data.login) { - data.login = this.getLogin(); + ); } - return data; - } + serialize() { + const data = super.serialize(); - getLogin() { - const login = getLogin(this.context); - const { user } = this.context; + if (!data.login) { + data.login = this.getLogin(); + } - return login || user.username || user.email || ''; - } + return data; + } - onClickEdit = async () => { - this.setState({ - isLoginEdit: true, - }); + getLogin() { + const login = getLogin(this.context); + const { user } = this.context; - await this.context.requestRedraw(); + return login || user.username || user.email || ''; + } - this.form.focus('login'); - }; + onClickEdit = async () => { + this.setState({ + isLoginEdit: true, + }); + + await this.context.requestRedraw(); + + this.form.focus('login'); + }; } diff --git a/packages/app/components/auth/forgotPassword/forgotPassword.scss b/packages/app/components/auth/forgotPassword/forgotPassword.scss index 889c3d4..de7757a 100644 --- a/packages/app/components/auth/forgotPassword/forgotPassword.scss +++ b/packages/app/components/auth/forgotPassword/forgotPassword.scss @@ -1,31 +1,31 @@ @import '~app/components/ui/colors.scss'; .descriptionText { - font-size: 15px; - line-height: 1.4; - padding-bottom: 8px; - color: #aaa; + font-size: 15px; + line-height: 1.4; + padding-bottom: 8px; + color: #aaa; } .login { - composes: email from '~app/components/auth/password/password.scss'; + composes: email from '~app/components/auth/password/password.scss'; } .editLogin { - composes: pencil from '~app/components/ui/icons.scss'; + composes: pencil from '~app/components/ui/icons.scss'; - position: relative; - bottom: 1px; - padding-left: 3px; + position: relative; + bottom: 1px; + padding-left: 3px; - color: #666666; - font-size: 10px; + color: #666666; + font-size: 10px; - transition: color 0.3s; + transition: color 0.3s; - cursor: pointer; + cursor: pointer; - &:hover { - color: #ccc; - } + &:hover { + color: #ccc; + } } diff --git a/packages/app/components/auth/helpLinks.scss b/packages/app/components/auth/helpLinks.scss index c2d2293..9769a7e 100644 --- a/packages/app/components/auth/helpLinks.scss +++ b/packages/app/components/auth/helpLinks.scss @@ -1,9 +1,9 @@ .helpLinks { - margin: 8px 0; - position: relative; - height: 20px; + margin: 8px 0; + position: relative; + height: 20px; - color: #444; - text-align: center; - font-size: 16px; + color: #444; + text-align: center; + font-size: 16px; } diff --git a/packages/app/components/auth/login/Login.intl.json b/packages/app/components/auth/login/Login.intl.json index a3f6d6e..93f82eb 100644 --- a/packages/app/components/auth/login/Login.intl.json +++ b/packages/app/components/auth/login/Login.intl.json @@ -1,6 +1,6 @@ { - "createNewAccount": "Create new account", - "loginTitle": "Sign in", - "emailOrUsername": "E‑mail or username", - "next": "Next" + "createNewAccount": "Create new account", + "loginTitle": "Sign in", + "emailOrUsername": "E‑mail or username", + "next": "Next" } diff --git a/packages/app/components/auth/login/Login.ts b/packages/app/components/auth/login/Login.ts index cb28913..86a9575 100644 --- a/packages/app/components/auth/login/Login.ts +++ b/packages/app/components/auth/login/Login.ts @@ -3,14 +3,14 @@ import Body from './LoginBody'; import messages from './Login.intl.json'; export default factory({ - title: messages.loginTitle, - body: Body, - footer: { - color: 'green', - label: messages.next, - }, - links: { - isAvailable: (context) => !context.user.isGuest, - label: messages.createNewAccount, - }, + title: messages.loginTitle, + body: Body, + footer: { + color: 'green', + label: messages.next, + }, + links: { + isAvailable: (context) => !context.user.isGuest, + label: messages.createNewAccount, + }, }); diff --git a/packages/app/components/auth/login/LoginBody.tsx b/packages/app/components/auth/login/LoginBody.tsx index a139801..421efe5 100644 --- a/packages/app/components/auth/login/LoginBody.tsx +++ b/packages/app/components/auth/login/LoginBody.tsx @@ -7,26 +7,21 @@ import { User } from 'app/components/user/reducer'; import messages from './Login.intl.json'; export default class LoginBody extends BaseAuthBody { - static displayName = 'LoginBody'; - static panelId = 'login'; - static hasGoBack = (state: { user: User }) => { - return !state.user.isGuest; - }; + static displayName = 'LoginBody'; + static panelId = 'login'; + static hasGoBack = (state: { user: User }) => { + return !state.user.isGuest; + }; - autoFocusField = 'login'; + autoFocusField = 'login'; - render() { - return ( -
- {this.renderErrors()} + render() { + return ( +
+ {this.renderErrors()} - -
- ); - } + +
+ ); + } } diff --git a/packages/app/components/auth/mfa/Mfa.intl.json b/packages/app/components/auth/mfa/Mfa.intl.json index de053eb..b19e7e8 100644 --- a/packages/app/components/auth/mfa/Mfa.intl.json +++ b/packages/app/components/auth/mfa/Mfa.intl.json @@ -1,4 +1,4 @@ { - "enterTotp": "Enter code", - "description": "In order to sign in this account, you need to enter a one-time password from mobile application" + "enterTotp": "Enter code", + "description": "In order to sign in this account, you need to enter a one-time password from mobile application" } diff --git a/packages/app/components/auth/mfa/Mfa.tsx b/packages/app/components/auth/mfa/Mfa.tsx index 79d58bf..ffda313 100644 --- a/packages/app/components/auth/mfa/Mfa.tsx +++ b/packages/app/components/auth/mfa/Mfa.tsx @@ -4,10 +4,10 @@ import messages from './Mfa.intl.json'; import passwordMessages from '../password/Password.intl.json'; export default factory({ - title: messages.enterTotp, - body: Body, - footer: { - color: 'green', - label: passwordMessages.signInButton, - }, + title: messages.enterTotp, + body: Body, + footer: { + color: 'green', + label: passwordMessages.signInButton, + }, }); diff --git a/packages/app/components/auth/mfa/MfaBody.tsx b/packages/app/components/auth/mfa/MfaBody.tsx index 16eb9fc..02ea0f3 100644 --- a/packages/app/components/auth/mfa/MfaBody.tsx +++ b/packages/app/components/auth/mfa/MfaBody.tsx @@ -8,30 +8,30 @@ import styles from './mfa.scss'; import messages from './Mfa.intl.json'; export default class MfaBody extends BaseAuthBody { - static panelId = 'mfa'; - static hasGoBack = true; + static panelId = 'mfa'; + static hasGoBack = true; - autoFocusField = 'totp'; + autoFocusField = 'totp'; - render() { - return ( -
- {this.renderErrors()} + render() { + return ( +
+ {this.renderErrors()} - + -

- -

+

+ +

- -
- ); - } + +
+ ); + } } diff --git a/packages/app/components/auth/mfa/mfa.scss b/packages/app/components/auth/mfa/mfa.scss index 70aff73..8edb590 100644 --- a/packages/app/components/auth/mfa/mfa.scss +++ b/packages/app/components/auth/mfa/mfa.scss @@ -1,6 +1,6 @@ .descriptionText { - font-size: 15px; - line-height: 1.4; - padding-bottom: 8px; - color: #aaa; + font-size: 15px; + line-height: 1.4; + padding-bottom: 8px; + color: #aaa; } diff --git a/packages/app/components/auth/password/Password.intl.json b/packages/app/components/auth/password/Password.intl.json index 6b3eb35..c2e9388 100644 --- a/packages/app/components/auth/password/Password.intl.json +++ b/packages/app/components/auth/password/Password.intl.json @@ -1,7 +1,7 @@ { - "passwordTitle": "Enter password", - "signInButton": "Sign in", - "forgotPassword": "Forgot password", - "accountPassword": "Account password", - "rememberMe": "Remember me on this device" + "passwordTitle": "Enter password", + "signInButton": "Sign in", + "forgotPassword": "Forgot password", + "accountPassword": "Account password", + "rememberMe": "Remember me on this device" } diff --git a/packages/app/components/auth/password/Password.ts b/packages/app/components/auth/password/Password.ts index d0ab75d..ffa58c1 100644 --- a/packages/app/components/auth/password/Password.ts +++ b/packages/app/components/auth/password/Password.ts @@ -3,13 +3,13 @@ import Body from './PasswordBody'; import messages from './Password.intl.json'; export default factory({ - title: messages.passwordTitle, - body: Body, - footer: { - color: 'green', - label: messages.signInButton, - }, - links: { - label: messages.forgotPassword, - }, + title: messages.passwordTitle, + body: Body, + footer: { + color: 'green', + label: messages.signInButton, + }, + links: { + label: messages.forgotPassword, + }, }); diff --git a/packages/app/components/auth/password/PasswordBody.tsx b/packages/app/components/auth/password/PasswordBody.tsx index a13e379..b67b3a3 100644 --- a/packages/app/components/auth/password/PasswordBody.tsx +++ b/packages/app/components/auth/password/PasswordBody.tsx @@ -8,46 +8,38 @@ import styles from './password.scss'; import messages from './Password.intl.json'; export default class PasswordBody extends BaseAuthBody { - static displayName = 'PasswordBody'; - static panelId = 'password'; - static hasGoBack = true; + static displayName = 'PasswordBody'; + static panelId = 'password'; + static hasGoBack = true; - autoFocusField = 'password'; + autoFocusField = 'password'; - render() { - const { user } = this.context; + render() { + const { user } = this.context; - return ( -
- {this.renderErrors()} + return ( +
+ {this.renderErrors()} -
-
- {user.avatar ? ( - - ) : ( - - )} -
-
{user.email || user.username}
-
+
+
+ {user.avatar ? : } +
+
{user.email || user.username}
+
- + -
- -
-
- ); - } +
+ +
+
+ ); + } } diff --git a/packages/app/components/auth/password/password.scss b/packages/app/components/auth/password/password.scss index d3c084c..7f7c03d 100644 --- a/packages/app/components/auth/password/password.scss +++ b/packages/app/components/auth/password/password.scss @@ -1,22 +1,22 @@ @import '~app/components/ui/fonts.scss'; .avatar { - width: 90px; - height: 90px; - font-size: 90px; - line-height: 1; - margin: 0 auto; + width: 90px; + height: 90px; + font-size: 90px; + line-height: 1; + margin: 0 auto; - img { - width: 100%; - } + img { + width: 100%; + } } .email { - font-family: $font-family-title; - font-size: 18px; - color: #fff; + font-family: $font-family-title; + font-size: 18px; + color: #fff; - margin-bottom: 15px; - margin-top: 10px; + margin-bottom: 15px; + margin-top: 10px; } diff --git a/packages/app/components/auth/permissions/Permissions.intl.json b/packages/app/components/auth/permissions/Permissions.intl.json index 6143d48..8af4b87 100644 --- a/packages/app/components/auth/permissions/Permissions.intl.json +++ b/packages/app/components/auth/permissions/Permissions.intl.json @@ -1,12 +1,12 @@ { - "permissionsTitle": "Application permissions", - "youAuthorizedAs": "You authorized as:", - "theAppNeedsAccess1": "This application needs access", - "theAppNeedsAccess2": "to your data", - "decline": "Decline", - "approve": "Approve", - "scope_minecraft_server_session": "Authorization data for minecraft server", - "scope_offline_access": "Access to your profile data, when you offline", - "scope_account_info": "Access to your profile data (except E‑mail)", - "scope_account_email": "Access to your E‑mail address" + "permissionsTitle": "Application permissions", + "youAuthorizedAs": "You authorized as:", + "theAppNeedsAccess1": "This application needs access", + "theAppNeedsAccess2": "to your data", + "decline": "Decline", + "approve": "Approve", + "scope_minecraft_server_session": "Authorization data for minecraft server", + "scope_offline_access": "Access to your profile data, when you offline", + "scope_account_info": "Access to your profile data (except E‑mail)", + "scope_account_email": "Access to your E‑mail address" } diff --git a/packages/app/components/auth/permissions/Permissions.ts b/packages/app/components/auth/permissions/Permissions.ts index 053bc23..c630a5a 100644 --- a/packages/app/components/auth/permissions/Permissions.ts +++ b/packages/app/components/auth/permissions/Permissions.ts @@ -3,14 +3,14 @@ import messages from './Permissions.intl.json'; import Body from './PermissionsBody'; export default factory({ - title: messages.permissionsTitle, - body: Body, - footer: { - color: 'orange', - autoFocus: true, - label: messages.approve, - }, - links: { - label: messages.decline, - }, + title: messages.permissionsTitle, + body: Body, + footer: { + color: 'orange', + autoFocus: true, + label: messages.approve, + }, + links: { + label: messages.decline, + }, }); diff --git a/packages/app/components/auth/permissions/PermissionsBody.tsx b/packages/app/components/auth/permissions/PermissionsBody.tsx index 7c4cd06..90992c6 100644 --- a/packages/app/components/auth/permissions/PermissionsBody.tsx +++ b/packages/app/components/auth/permissions/PermissionsBody.tsx @@ -8,58 +8,52 @@ import styles from './permissions.scss'; import messages from './Permissions.intl.json'; export default class PermissionsBody extends BaseAuthBody { - static displayName = 'PermissionsBody'; - static panelId = 'permissions'; + static displayName = 'PermissionsBody'; + static panelId = 'permissions'; - render() { - const { user } = this.context; - const { scopes } = this.context.auth; + render() { + const { user } = this.context; + const { scopes } = this.context.auth; - return ( -
- {this.renderErrors()} + return ( +
+ {this.renderErrors()} - -
-
- {user.avatar ? ( - - ) : ( - - )} + +
+
+ {user.avatar ? : } +
+
+ +
+
{user.username}
+
+
+
+
+ +
+ +
+
    + {scopes.map((scope) => { + const key = `scope_${scope}`; + const message = messages[key]; + + return ( +
  • + {message ? ( + + ) : ( + scope.replace(/^\w|_/g, (match) => match.replace('_', ' ').toUpperCase()) + )} +
  • + ); + })} +
+
-
- -
-
{user.username}
-
-
-
-
- -
- -
-
    - {scopes.map((scope) => { - const key = `scope_${scope}`; - const message = messages[key]; - - return ( -
  • - {message ? ( - - ) : ( - scope.replace(/^\w|_/g, (match) => - match.replace('_', ' ').toUpperCase(), - ) - )} -
  • - ); - })} -
-
-
- ); - } + ); + } } diff --git a/packages/app/components/auth/permissions/permissions.scss b/packages/app/components/auth/permissions/permissions.scss index 5c24898..c3400d6 100644 --- a/packages/app/components/auth/permissions/permissions.scss +++ b/packages/app/components/auth/permissions/permissions.scss @@ -2,76 +2,76 @@ @import '~app/components/ui/fonts.scss'; .authInfo { - // Отступы сверху и снизу разные т.к. мы ужимаем высоту линии строки с логином на 2 пикселя и из-за этого теряем отступ снизу - padding: 5px 20px 7px; - text-align: left; + // Отступы сверху и снизу разные т.к. мы ужимаем высоту линии строки с логином на 2 пикселя и из-за этого теряем отступ снизу + padding: 5px 20px 7px; + text-align: left; } .authInfoAvatar { - $size: 30px; + $size: 30px; - float: left; - height: $size; - width: $size; - font-size: $size; - line-height: 1; - margin-right: 10px; - margin-top: 2px; - color: #aaa; + float: left; + height: $size; + width: $size; + font-size: $size; + line-height: 1; + margin-right: 10px; + margin-top: 2px; + color: #aaa; - img { - width: 100%; - } + img { + width: 100%; + } } .authInfoTitle { - font-size: 14px; - color: #666; + font-size: 14px; + color: #666; } .authInfoEmail { - font-family: $font-family-title; - font-size: 20px; - line-height: 16px; - color: #fff; + font-family: $font-family-title; + font-size: 20px; + line-height: 16px; + color: #fff; } .permissionsContainer { - padding: 15px 12px; - text-align: left; + padding: 15px 12px; + text-align: left; } .permissionsTitle { - font-family: $font-family-title; - font-size: 18px; - color: #dd8650; - padding-bottom: 6px; + font-family: $font-family-title; + font-size: 18px; + color: #dd8650; + padding-bottom: 6px; } .permissionsList { - list-style: none; - margin-top: 10px; + list-style: none; + margin-top: 10px; - li { - color: #a9a9a9; - font-size: 14px; - line-height: 1.4; - padding-bottom: 4px; - padding-left: 17px; - position: relative; + li { + color: #a9a9a9; + font-size: 14px; + line-height: 1.4; + padding-bottom: 4px; + padding-left: 17px; + position: relative; - &:last-of-type { - padding-bottom: 0; + &:last-of-type { + padding-bottom: 0; + } + + &:before { + content: '• '; + color: lighter($light_violet); + font-size: 39px; // ~ 9px + line-height: 9px; + position: absolute; + top: 6px; + left: -4px; + } } - - &:before { - content: '• '; - color: lighter($light_violet); - font-size: 39px; // ~ 9px - line-height: 9px; - position: absolute; - top: 6px; - left: -4px; - } - } } diff --git a/packages/app/components/auth/recoverPassword/RecoverPassword.intl.json b/packages/app/components/auth/recoverPassword/RecoverPassword.intl.json index b769994..ce776a5 100644 --- a/packages/app/components/auth/recoverPassword/RecoverPassword.intl.json +++ b/packages/app/components/auth/recoverPassword/RecoverPassword.intl.json @@ -1,12 +1,12 @@ { - "title": "Restore password", - "contactSupport": "Contact support", - "messageWasSent": "The recovery code was sent to your account E‑mail.", - "messageWasSentTo": "The recovery code was sent to your E‑mail {email}.", - "enterCodeBelow": "Please enter the code received into the field below:", - "enterNewPasswordBelow": "Enter and repeat new password below:", - "change": "Change password", - "newPassword": "Enter new password", - "newRePassword": "Repeat new password", - "enterTheCode": "Enter confirmation code" + "title": "Restore password", + "contactSupport": "Contact support", + "messageWasSent": "The recovery code was sent to your account E‑mail.", + "messageWasSentTo": "The recovery code was sent to your E‑mail {email}.", + "enterCodeBelow": "Please enter the code received into the field below:", + "enterNewPasswordBelow": "Enter and repeat new password below:", + "change": "Change password", + "newPassword": "Enter new password", + "newRePassword": "Repeat new password", + "enterTheCode": "Enter confirmation code" } diff --git a/packages/app/components/auth/recoverPassword/RecoverPassword.ts b/packages/app/components/auth/recoverPassword/RecoverPassword.ts index 6a8137f..fb54884 100644 --- a/packages/app/components/auth/recoverPassword/RecoverPassword.ts +++ b/packages/app/components/auth/recoverPassword/RecoverPassword.ts @@ -3,13 +3,13 @@ import messages from './RecoverPassword.intl.json'; import Body from './RecoverPasswordBody'; export default factory({ - title: messages.title, - body: Body, - footer: { - color: 'lightViolet', - label: messages.change, - }, - links: { - label: messages.contactSupport, - }, + title: messages.title, + body: Body, + footer: { + color: 'lightViolet', + label: messages.change, + }, + links: { + label: messages.contactSupport, + }, }); diff --git a/packages/app/components/auth/recoverPassword/RecoverPasswordBody.tsx b/packages/app/components/auth/recoverPassword/RecoverPasswordBody.tsx index 2a252a8..e75a338 100644 --- a/packages/app/components/auth/recoverPassword/RecoverPasswordBody.tsx +++ b/packages/app/components/auth/recoverPassword/RecoverPasswordBody.tsx @@ -11,70 +11,67 @@ import messages from './RecoverPassword.intl.json'; // TODO: activation code field may be decoupled into common component and reused here and in activation panel export default class RecoverPasswordBody extends BaseAuthBody { - static displayName = 'RecoverPasswordBody'; - static panelId = 'recoverPassword'; - static hasGoBack = true; + static displayName = 'RecoverPasswordBody'; + static panelId = 'recoverPassword'; + static hasGoBack = true; - autoFocusField = - this.props.match.params && this.props.match.params.key - ? 'newPassword' - : 'key'; + autoFocusField = this.props.match.params && this.props.match.params.key ? 'newPassword' : 'key'; - render() { - const { user } = this.context; - const { key } = this.props.match.params; + render() { + const { user } = this.context; + const { key } = this.props.match.params; - return ( -
- {this.renderErrors()} + return ( +
+ {this.renderErrors()} -

- {user.maskedEmail ? ( - {user.maskedEmail}, - }} - /> - ) : ( - - )}{' '} - -

+

+ {user.maskedEmail ? ( + {user.maskedEmail}, + }} + /> + ) : ( + + )}{' '} + +

- + -

- -

+

+ +

- + - -
- ); - } + +
+ ); + } } diff --git a/packages/app/components/auth/recoverPassword/recoverPassword.scss b/packages/app/components/auth/recoverPassword/recoverPassword.scss index 7da264e..ddc9bed 100644 --- a/packages/app/components/auth/recoverPassword/recoverPassword.scss +++ b/packages/app/components/auth/recoverPassword/recoverPassword.scss @@ -1,8 +1,8 @@ @import '~app/components/ui/colors.scss'; .descriptionText { - font-size: 15px; - line-height: 1.4; - margin-bottom: 8px; - color: #aaa; + font-size: 15px; + line-height: 1.4; + margin-bottom: 8px; + color: #aaa; } diff --git a/packages/app/components/auth/reducer.test.ts b/packages/app/components/auth/reducer.test.ts index 1b5ba5d..e7b9fec 100644 --- a/packages/app/components/auth/reducer.test.ts +++ b/packages/app/components/auth/reducer.test.ts @@ -3,40 +3,36 @@ import auth from './reducer'; import { setLogin, setAccountSwitcher } from './actions'; describe('components/auth/reducer', () => { - describe('auth:setCredentials', () => { - it('should set login', () => { - const expectedLogin = 'foo'; + describe('auth:setCredentials', () => { + it('should set login', () => { + const expectedLogin = 'foo'; - expect( - auth(undefined, setLogin(expectedLogin)).credentials, - 'to satisfy', - { - login: expectedLogin, - }, - ); - }); - }); - - describe('auth:setAccountSwitcher', () => { - it('should be enabled by default', () => - expect(auth(undefined, {} as any), 'to satisfy', { - isSwitcherEnabled: true, - })); - - it('should enable switcher', () => { - const expectedValue = true; - - expect(auth(undefined, setAccountSwitcher(expectedValue)), 'to satisfy', { - isSwitcherEnabled: expectedValue, - }); + expect(auth(undefined, setLogin(expectedLogin)).credentials, 'to satisfy', { + login: expectedLogin, + }); + }); }); - it('should disable switcher', () => { - const expectedValue = false; + describe('auth:setAccountSwitcher', () => { + it('should be enabled by default', () => + expect(auth(undefined, {} as any), 'to satisfy', { + isSwitcherEnabled: true, + })); - expect(auth(undefined, setAccountSwitcher(expectedValue)), 'to satisfy', { - isSwitcherEnabled: expectedValue, - }); + it('should enable switcher', () => { + const expectedValue = true; + + expect(auth(undefined, setAccountSwitcher(expectedValue)), 'to satisfy', { + isSwitcherEnabled: expectedValue, + }); + }); + + it('should disable switcher', () => { + const expectedValue = false; + + expect(auth(undefined, setAccountSwitcher(expectedValue)), 'to satisfy', { + isSwitcherEnabled: expectedValue, + }); + }); }); - }); }); diff --git a/packages/app/components/auth/reducer.ts b/packages/app/components/auth/reducer.ts index 8be2f20..b173059 100644 --- a/packages/app/components/auth/reducer.ts +++ b/packages/app/components/auth/reducer.ts @@ -3,173 +3,156 @@ import { RootState } from 'app/reducers'; import { Scope } from '../../services/api/oauth'; import { - ErrorAction, - CredentialsAction, - AccountSwitcherAction, - LoadingAction, - ClientAction, - OAuthAction, - ScopesAction, + ErrorAction, + CredentialsAction, + AccountSwitcherAction, + LoadingAction, + ClientAction, + OAuthAction, + ScopesAction, } from './actions'; export interface Credentials { - login?: string | null; // By some reasons there is can be null value. Need to investigate. - password?: string; - rememberMe?: boolean; - returnUrl?: string; - isRelogin?: boolean; - isTotpRequired?: boolean; + login?: string | null; // By some reasons there is can be null value. Need to investigate. + password?: string; + rememberMe?: boolean; + returnUrl?: string; + isRelogin?: boolean; + isTotpRequired?: boolean; } type Error = Record< - string, - | string - | { - type: string; - payload: Record; - } + string, + | string + | { + type: string; + payload: Record; + } > | null; export interface Client { - id: string; - name: string; - description: string; + id: string; + name: string; + description: string; } export interface OAuthState { - clientId: string; - redirectUrl: string; - responseType: string; - description?: string; - scope: string; - prompt: string; - loginHint: string; - state: string; - success?: boolean; - code?: string; - displayCode?: boolean; - acceptRequired?: boolean; + clientId: string; + redirectUrl: string; + responseType: string; + description?: string; + scope: string; + prompt: string; + loginHint: string; + state: string; + success?: boolean; + code?: string; + displayCode?: boolean; + acceptRequired?: boolean; } type Scopes = Array; export interface State { - credentials: Credentials; - error: Error; - isLoading: boolean; - isSwitcherEnabled: boolean; - client: Client | null; - oauth: OAuthState | null; - scopes: Scopes; + credentials: Credentials; + error: Error; + isLoading: boolean; + isSwitcherEnabled: boolean; + client: Client | null; + oauth: OAuthState | null; + scopes: Scopes; } -const error: Reducer = ( - state = null, - { type, payload }, -) => { - if (type === 'auth:error') { - return payload; - } - - return state; -}; - -const credentials: Reducer = ( - state = {}, - { type, payload }, -) => { - if (type === 'auth:setCredentials') { - if (payload) { - return { - ...payload, - }; +const error: Reducer = (state = null, { type, payload }) => { + if (type === 'auth:error') { + return payload; } - return {}; - } - - return state; + return state; }; -const isSwitcherEnabled: Reducer< - State['isSwitcherEnabled'], - AccountSwitcherAction -> = (state = true, { type, payload }) => { - if (type === 'auth:setAccountSwitcher') { - return payload; - } +const credentials: Reducer = (state = {}, { type, payload }) => { + if (type === 'auth:setCredentials') { + if (payload) { + return { + ...payload, + }; + } - return state; + return {}; + } + + return state; }; -const isLoading: Reducer = ( - state = false, - { type, payload }, +const isSwitcherEnabled: Reducer = ( + state = true, + { type, payload }, ) => { - if (type === 'set_loading_state') { - return payload; - } + if (type === 'auth:setAccountSwitcher') { + return payload; + } - return state; + return state; }; -const client: Reducer = ( - state = null, - { type, payload }, -) => { - if (type === 'set_client') { - return payload; - } +const isLoading: Reducer = (state = false, { type, payload }) => { + if (type === 'set_loading_state') { + return payload; + } - return state; + return state; +}; + +const client: Reducer = (state = null, { type, payload }) => { + if (type === 'set_client') { + return payload; + } + + return state; }; const oauth: Reducer = (state = null, action) => { - switch (action.type) { - case 'set_oauth': - return action.payload; - case 'set_oauth_result': - return { - ...(state as OAuthState), - ...action.payload, - }; - case 'require_permissions_accept': - return { - ...(state as OAuthState), - acceptRequired: true, - }; - default: - return state; - } + switch (action.type) { + case 'set_oauth': + return action.payload; + case 'set_oauth_result': + return { + ...(state as OAuthState), + ...action.payload, + }; + case 'require_permissions_accept': + return { + ...(state as OAuthState), + acceptRequired: true, + }; + default: + return state; + } }; -const scopes: Reducer = ( - state = [], - { type, payload }, -) => { - if (type === 'set_scopes') { - return payload; - } +const scopes: Reducer = (state = [], { type, payload }) => { + if (type === 'set_scopes') { + return payload; + } - return state; + return state; }; export default combineReducers({ - credentials, - error, - isLoading, - isSwitcherEnabled, - client, - oauth, - scopes, + credentials, + error, + isLoading, + isSwitcherEnabled, + client, + oauth, + scopes, }); -export function getLogin( - state: RootState | Pick, -): string | null { - return state.auth.credentials.login || null; +export function getLogin(state: RootState | Pick): string | null { + return state.auth.credentials.login || null; } export function getCredentials(state: RootState): Credentials { - return state.auth.credentials; + return state.auth.credentials; } diff --git a/packages/app/components/auth/register/Register.intl.json b/packages/app/components/auth/register/Register.intl.json index 7a83ef3..39e167b 100644 --- a/packages/app/components/auth/register/Register.intl.json +++ b/packages/app/components/auth/register/Register.intl.json @@ -1,10 +1,10 @@ { - "registerTitle": "Sign Up", - "yourNickname": "Your nickname", - "yourEmail": "Your E‑mail", - "accountPassword": "Account password", - "repeatPassword": "Repeat password", - "signUpButton": "Register", - "acceptRules": "I agree with {link}", - "termsOfService": "terms of service" + "registerTitle": "Sign Up", + "yourNickname": "Your nickname", + "yourEmail": "Your E‑mail", + "accountPassword": "Account password", + "repeatPassword": "Repeat password", + "signUpButton": "Register", + "acceptRules": "I agree with {link}", + "termsOfService": "terms of service" } diff --git a/packages/app/components/auth/register/Register.ts b/packages/app/components/auth/register/Register.ts index ac50d6b..6536bd1 100644 --- a/packages/app/components/auth/register/Register.ts +++ b/packages/app/components/auth/register/Register.ts @@ -5,19 +5,19 @@ import messages from './Register.intl.json'; import Body from './RegisterBody'; export default factory({ - title: messages.registerTitle, - body: Body, - footer: { - color: 'blue', - label: messages.signUpButton, - }, - links: [ - { - label: activationMessages.didNotReceivedEmail, - payload: { requestEmail: true }, + title: messages.registerTitle, + body: Body, + footer: { + color: 'blue', + label: messages.signUpButton, }, - { - label: forgotPasswordMessages.alreadyHaveCode, - }, - ], + links: [ + { + label: activationMessages.didNotReceivedEmail, + payload: { requestEmail: true }, + }, + { + label: forgotPasswordMessages.alreadyHaveCode, + }, + ], }); diff --git a/packages/app/components/auth/register/RegisterBody.tsx b/packages/app/components/auth/register/RegisterBody.tsx index 9578975..80aad09 100644 --- a/packages/app/components/auth/register/RegisterBody.tsx +++ b/packages/app/components/auth/register/RegisterBody.tsx @@ -11,73 +11,73 @@ import messages from './Register.intl.json'; // TODO: password and username can be validate for length and sameness export default class RegisterBody extends BaseAuthBody { - static panelId = 'register'; + static panelId = 'register'; - autoFocusField = 'username'; + autoFocusField = 'username'; - render() { - return ( -
- {this.renderErrors()} + render() { + return ( +
+ {this.renderErrors()} - + - + - + - + - + -
- - - - ), - }} - /> - } - /> -
-
- ); - } +
+ + + + ), + }} + /> + } + /> +
+
+ ); + } } diff --git a/packages/app/components/auth/resendActivation/ResendActivation.intl.json b/packages/app/components/auth/resendActivation/ResendActivation.intl.json index 99803c0..b2d7652 100644 --- a/packages/app/components/auth/resendActivation/ResendActivation.intl.json +++ b/packages/app/components/auth/resendActivation/ResendActivation.intl.json @@ -1,5 +1,5 @@ { - "title": "Did not received an E‑mail", - "specifyYourEmail": "Please, enter an E‑mail you've registered with and we will send you new activation code", - "sendNewEmail": "Send new E‑mail" + "title": "Did not received an E‑mail", + "specifyYourEmail": "Please, enter an E‑mail you've registered with and we will send you new activation code", + "sendNewEmail": "Send new E‑mail" } diff --git a/packages/app/components/auth/resendActivation/ResendActivation.ts b/packages/app/components/auth/resendActivation/ResendActivation.ts index b33b817..d56a772 100644 --- a/packages/app/components/auth/resendActivation/ResendActivation.ts +++ b/packages/app/components/auth/resendActivation/ResendActivation.ts @@ -4,13 +4,13 @@ import messages from './ResendActivation.intl.json'; import Body from './ResendActivationBody'; export default factory({ - title: messages.title, - body: Body, - footer: { - color: 'blue', - label: messages.sendNewEmail, - }, - links: { - label: forgotPasswordMessages.alreadyHaveCode, - }, + title: messages.title, + body: Body, + footer: { + color: 'blue', + label: messages.sendNewEmail, + }, + links: { + label: forgotPasswordMessages.alreadyHaveCode, + }, }); diff --git a/packages/app/components/auth/resendActivation/ResendActivationBody.tsx b/packages/app/components/auth/resendActivation/ResendActivationBody.tsx index 773e6d3..9fd24cd 100644 --- a/packages/app/components/auth/resendActivation/ResendActivationBody.tsx +++ b/packages/app/components/auth/resendActivation/ResendActivationBody.tsx @@ -8,33 +8,33 @@ import styles from './resendActivation.scss'; import messages from './ResendActivation.intl.json'; export default class ResendActivation extends BaseAuthBody { - static displayName = 'ResendActivation'; - static panelId = 'resendActivation'; - static hasGoBack = true; + static displayName = 'ResendActivation'; + static panelId = 'resendActivation'; + static hasGoBack = true; - autoFocusField = 'email'; + autoFocusField = 'email'; - render() { - return ( -
- {this.renderErrors()} + render() { + return ( +
+ {this.renderErrors()} -
- -
+
+ +
- + - -
- ); - } + +
+ ); + } } diff --git a/packages/app/components/auth/resendActivation/resendActivation.scss b/packages/app/components/auth/resendActivation/resendActivation.scss index 69b43c3..f1ba029 100644 --- a/packages/app/components/auth/resendActivation/resendActivation.scss +++ b/packages/app/components/auth/resendActivation/resendActivation.scss @@ -1,8 +1,8 @@ @import '~app/components/ui/fonts.scss'; .description { - font-family: $font-family-title; - margin: 5px 0 19px; - line-height: 1.4; - font-size: 16px; + font-family: $font-family-title; + margin: 5px 0 19px; + line-height: 1.4; + font-size: 16px; } diff --git a/packages/app/components/contact/ContactForm.test.tsx b/packages/app/components/contact/ContactForm.test.tsx index 99327b5..6fe4ebe 100644 --- a/packages/app/components/contact/ContactForm.test.tsx +++ b/packages/app/components/contact/ContactForm.test.tsx @@ -9,162 +9,143 @@ import { TestContextProvider } from 'app/shell'; import { ContactForm } from './ContactForm'; beforeEach(() => { - sinon.stub(feedback, 'send').returns(Promise.resolve() as any); + sinon.stub(feedback, 'send').returns(Promise.resolve() as any); }); afterEach(() => { - (feedback.send as any).restore(); + (feedback.send as any).restore(); }); describe('ContactForm', () => { - it('should contain Form', () => { - const user = {} as User; + it('should contain Form', () => { + const user = {} as User; - render( - - - , - ); + render( + + + , + ); - expect(screen.getAllByRole('textbox').length, 'to be greater than', 1); + expect(screen.getAllByRole('textbox').length, 'to be greater than', 1); - expect( - screen.getByRole('button', { name: /Send/ }), - 'to have property', - 'type', - 'submit', - ); + expect(screen.getByRole('button', { name: /Send/ }), 'to have property', 'type', 'submit'); - [ - { - label: 'subject', - name: 'subject', - }, - { - label: 'E‑mail', - name: 'email', - }, - { - label: 'message', - name: 'message', - }, - ].forEach((el) => { - expect( - screen.getByLabelText(el.label, { exact: false }), - 'to have property', - 'name', - el.name, - ); - }); - }); - - describe('when rendered with user', () => { - const user = { - email: 'foo@bar.com', - } as User; - - it('should render email field with user email', () => { - render( - - - , - ); - - expect(screen.getByDisplayValue(user.email), 'to be a', HTMLInputElement); - }); - }); - - it('should submit and then hide form and display success message', async () => { - const user = { - email: 'foo@bar.com', - } as User; - - render( - - - , - ); - - fireEvent.change(screen.getByLabelText(/subject/i), { - target: { - value: 'subject', - }, + [ + { + label: 'subject', + name: 'subject', + }, + { + label: 'E‑mail', + name: 'email', + }, + { + label: 'message', + name: 'message', + }, + ].forEach((el) => { + expect(screen.getByLabelText(el.label, { exact: false }), 'to have property', 'name', el.name); + }); }); - fireEvent.change(screen.getByLabelText(/message/i), { - target: { - value: 'the message', - }, + describe('when rendered with user', () => { + const user = { + email: 'foo@bar.com', + } as User; + + it('should render email field with user email', () => { + render( + + + , + ); + + expect(screen.getByDisplayValue(user.email), 'to be a', HTMLInputElement); + }); }); - const button = screen.getByRole('button', { name: 'Send' }); + it('should submit and then hide form and display success message', async () => { + const user = { + email: 'foo@bar.com', + } as User; - expect(button, 'to have property', 'disabled', false); + render( + + + , + ); - fireEvent.click(button); + fireEvent.change(screen.getByLabelText(/subject/i), { + target: { + value: 'subject', + }, + }); - expect(button, 'to have property', 'disabled', true); - expect(feedback.send, 'to have a call exhaustively satisfying', [ - { - subject: 'subject', - email: user.email, - category: '', - message: 'the message', - }, - ]); + fireEvent.change(screen.getByLabelText(/message/i), { + target: { + value: 'the message', + }, + }); - await waitFor(() => { - expect( - screen.getByText('Your message was received', { exact: false }), - 'to be a', - HTMLElement, - ); + const button = screen.getByRole('button', { name: 'Send' }); + + expect(button, 'to have property', 'disabled', false); + + fireEvent.click(button); + + expect(button, 'to have property', 'disabled', true); + expect(feedback.send, 'to have a call exhaustively satisfying', [ + { + subject: 'subject', + email: user.email, + category: '', + message: 'the message', + }, + ]); + + await waitFor(() => { + expect(screen.getByText('Your message was received', { exact: false }), 'to be a', HTMLElement); + }); + + expect(screen.getByText(user.email), 'to be a', HTMLElement); + + expect(screen.queryByRole('button', { name: /Send/ }), 'to be null'); }); - expect(screen.getByText(user.email), 'to be a', HTMLElement); + it('should show validation messages', async () => { + const user = { + email: 'foo@bar.com', + } as User; - expect(screen.queryByRole('button', { name: /Send/ }), 'to be null'); - }); + (feedback.send as any).callsFake(() => + Promise.reject({ + success: false, + errors: { email: 'error.email_invalid' }, + }), + ); - it('should show validation messages', async () => { - const user = { - email: 'foo@bar.com', - } as User; + render( + + + , + ); - (feedback.send as any).callsFake(() => - Promise.reject({ - success: false, - errors: { email: 'error.email_invalid' }, - }), - ); + fireEvent.change(screen.getByLabelText(/subject/i), { + target: { + value: 'subject', + }, + }); - render( - - - , - ); + fireEvent.change(screen.getByLabelText(/message/i), { + target: { + value: 'the message', + }, + }); - fireEvent.change(screen.getByLabelText(/subject/i), { - target: { - value: 'subject', - }, + fireEvent.click(screen.getByRole('button', { name: 'Send' })); + + await waitFor(() => { + expect(screen.getByRole('alert'), 'to have property', 'innerHTML', 'E‑mail is invalid'); + }); }); - - fireEvent.change(screen.getByLabelText(/message/i), { - target: { - value: 'the message', - }, - }); - - fireEvent.click(screen.getByRole('button', { name: 'Send' })); - - await waitFor(() => { - expect( - screen.getByRole('alert'), - 'to have property', - 'innerHTML', - 'E‑mail is invalid', - ); - }); - }); }); diff --git a/packages/app/components/contact/ContactForm.tsx b/packages/app/components/contact/ContactForm.tsx index 461f7ed..2bbe241 100644 --- a/packages/app/components/contact/ContactForm.tsx +++ b/packages/app/components/contact/ContactForm.tsx @@ -2,14 +2,7 @@ import React from 'react'; import { connect } from 'react-redux'; import clsx from 'clsx'; import { FormattedMessage as Message } from 'react-intl'; -import { - Input, - TextArea, - Button, - Form, - FormModel, - Dropdown, -} from 'app/components/ui/form'; +import { Input, TextArea, Button, Form, FormModel, Dropdown } from 'app/components/ui/form'; import feedback from 'app/services/api/feedback'; import icons from 'app/components/ui/icons.scss'; import popupStyles from 'app/components/ui/popup/popup.scss'; @@ -21,190 +14,170 @@ import styles from './contactForm.scss'; import messages from './contactForm.intl.json'; const CONTACT_CATEGORIES = { - // TODO: сюда позже проставить реальные id категорий с backend - 0: , - 1: , - 2: , - 3: , - 4: , + // TODO: сюда позже проставить реальные id категорий с backend + 0: , + 1: , + 2: , + 3: , + 4: , }; export class ContactForm extends React.Component< - { - onClose: () => void; - user: User; - }, - { - isLoading: boolean; - isSuccessfullySent: boolean; - lastEmail: string | null; - } + { + onClose: () => void; + user: User; + }, + { + isLoading: boolean; + isSuccessfullySent: boolean; + lastEmail: string | null; + } > { - static defaultProps = { - onClose() {}, - }; + static defaultProps = { + onClose() {}, + }; - state = { - isLoading: false, - isSuccessfullySent: false, - lastEmail: null, - }; + state = { + isLoading: false, + isSuccessfullySent: false, + lastEmail: null, + }; - form = new FormModel(); + form = new FormModel(); - render() { - const { isSuccessfullySent } = this.state || {}; - const { onClose } = this.props; + render() { + const { isSuccessfullySent } = this.state || {}; + const { onClose } = this.props; - return ( -
-
-
-

- -

- -
+ return ( +
+
+
+

+ +

+ +
- {isSuccessfullySent ? this.renderSuccess() : this.renderForm()} -
-
- ); - } - - renderForm() { - const { form } = this; - const { user } = this.props; - const { isLoading } = this.state; - - return ( -
-
-
- -
- -
- -
-
- -
-
- + {isSuccessfullySent ? this.renderSuccess() : this.renderForm()} +
- -
- -
-
- -
- -
- -