Update frontend
* change root dit and change way to import modules * update deps to latest versions * use latest tools for building bundles * rewrite webpack config * use nomodule technique for loading modern bundle * inject polyfills by babel usebuiltins * update eslint rules * rename all style files to CSS * use postcss preset env for building styles * proper typescript typing * put all html files to templates folder * etc
This commit is contained in:
@@ -462,16 +462,8 @@ Add this snippet to the bottom of web page:
|
|||||||
// if you set this param in `false` you will get notifications email notifications as admin
|
// if you set this param in `false` you will get notifications email notifications as admin
|
||||||
// but your users won't have interface for subscription
|
// but your users won't have interface for subscription
|
||||||
};
|
};
|
||||||
|
|
||||||
(function(c) {
|
|
||||||
for(var i = 0; i < c.length; i++){
|
|
||||||
var d = document, s = d.createElement('script');
|
|
||||||
s.src = remark_config.host + '/web/' +c[i] +'.js';
|
|
||||||
s.defer = true;
|
|
||||||
(d.head || d.body).appendChild(s);
|
|
||||||
}
|
|
||||||
})(remark_config.components || ['embed']);
|
|
||||||
</script>
|
</script>
|
||||||
|
<script>!function(e,n){for(var o=0;o<e.length;o++){var r=n.createElement("script"),c=".js",d=n.head||n.body;"noModule"in r?(r.type="module",c=".mjs"):r.async=!0,r.defer=!0,r.src=remark_config.host+"/web/"+e[o]+c,d.appendChild(r)}}(remark_config.components||["embed"],document);</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
And then add this node in the place where you want to see Remark42 widget:
|
And then add this node in the place where you want to see Remark42 widget:
|
||||||
|
|||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
echo "prepare environment"
|
echo "prepare environment"
|
||||||
# replace BASE_URL constant by REMARK_URL
|
# replace BASE_URL constant by REMARK_URL
|
||||||
sed -i "s|https://demo.remark42.com|${REMARK_URL}|g" /srv/web/*.html
|
sed -i "s|{% REMARK_URL %}|${REMARK_URL}|g" /srv/web/*.html
|
||||||
sed -i "s|https://demo.remark42.com|${REMARK_URL}|g" /srv/web/*.js
|
sed -i "s|{% REMARK_URL %}|${REMARK_URL}|g" /srv/web/*.js
|
||||||
|
|
||||||
if [ -n "${SITE_ID}" ]; then
|
if [ -n "${SITE_ID}" ]; then
|
||||||
#replace "site_id: 'remark'" by SITE_ID
|
#replace "site_id: 'remark'" by SITE_ID
|
||||||
|
|||||||
+30
-14
@@ -1,18 +1,34 @@
|
|||||||
|
const getPresetEnv = options => ['@babel/preset-env', options];
|
||||||
|
const preactPreset = [
|
||||||
|
'@babel/preset-react',
|
||||||
|
{
|
||||||
|
pragma: 'h',
|
||||||
|
pragmaFrag: 'Fragment',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const plugins = ['module:fast-async'];
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
presets: [
|
presets: [
|
||||||
[
|
getPresetEnv({
|
||||||
'@babel/preset-env',
|
targets: 'defaults, not IE 11, not samsung 12',
|
||||||
{
|
useBuiltIns: 'usage',
|
||||||
bugfixes: true,
|
corejs: 3,
|
||||||
loose: true,
|
bugfixes: true,
|
||||||
},
|
loose: true,
|
||||||
],
|
}),
|
||||||
[
|
preactPreset,
|
||||||
'@babel/preset-react',
|
|
||||||
{
|
|
||||||
pragma: 'h',
|
|
||||||
pragmaFrag: 'Fragment',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
|
plugins,
|
||||||
|
env: {
|
||||||
|
modern: {
|
||||||
|
presets: [getPresetEnv({ targets: { esmodules: true }, loose: true, bugfixes: true }), preactPreset],
|
||||||
|
plugins,
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
presets: [getPresetEnv({ targets: { node: 'current' } }), preactPreset],
|
||||||
|
plugins,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
defaults
|
|
||||||
+19
-106
@@ -1,127 +1,40 @@
|
|||||||
/* eslint-disable @typescript-eslint/camelcase */
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
parser: 'babel-eslint',
|
|
||||||
extends: [
|
extends: [
|
||||||
'eslint:recommended',
|
'react-app',
|
||||||
|
'preact',
|
||||||
'plugin:jsx-a11y/recommended',
|
'plugin:jsx-a11y/recommended',
|
||||||
'plugin:@typescript-eslint/recommended',
|
'prettier',
|
||||||
'plugin:prettier/recommended',
|
'prettier/@typescript-eslint',
|
||||||
|
'prettier/babel',
|
||||||
|
'prettier/prettier',
|
||||||
|
'prettier/react',
|
||||||
],
|
],
|
||||||
plugins: ['react', 'jsx-a11y', 'prettier'],
|
plugins: ['jsx-a11y', 'prettier'],
|
||||||
|
rules: {
|
||||||
|
'prettier/prettier': 'error',
|
||||||
|
},
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
files: ['*.ts', '*.tsx'],
|
files: ['*.ts?(x)'],
|
||||||
plugins: ['@typescript-eslint'],
|
|
||||||
parser: '@typescript-eslint/parser',
|
parser: '@typescript-eslint/parser',
|
||||||
parserOptions: {
|
|
||||||
project: './tsconfig.json',
|
|
||||||
tsconfigRootDir: __dirname,
|
|
||||||
},
|
|
||||||
rules: {
|
rules: {
|
||||||
// needs for using optional chaining
|
'no-undef': 'off',
|
||||||
'no-undef': 0,
|
'no-redeclare': 'off',
|
||||||
'jsx-a11y/no-autofocus': 0,
|
'no-unused-vars': 'off',
|
||||||
// disabling because typescipt uses it's own lint (see next rule)
|
|
||||||
'no-unused-vars': 0,
|
|
||||||
// allow Rust-like var starting with _underscore
|
|
||||||
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
|
||||||
// disabling because it's bad practice to mark accessibility in react classes
|
|
||||||
'@typescript-eslint/explicit-member-accessibility': 0,
|
|
||||||
// doesn't work in real world
|
|
||||||
'@typescript-eslint/no-non-null-assertion': 0,
|
|
||||||
// disabling because store actions use WATCH_ME_IM_SPECIAL case
|
|
||||||
'@typescript-eslint/class-name-casing': 0,
|
|
||||||
// disabling because server response contains snake case
|
|
||||||
'@typescript-eslint/camelcase': 0,
|
|
||||||
// disabling because it's standard behaviour that function is hoisted to top
|
|
||||||
'@typescript-eslint/no-use-before-define': 0,
|
|
||||||
// well
|
|
||||||
'@typescript-eslint/ban-ts-ignore': 0,
|
|
||||||
// better to be explicit here maybe?
|
|
||||||
'@typescript-eslint/no-inferrable-types': 0,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ['*.d.ts'],
|
files: ['*.d.ts'],
|
||||||
rules: {
|
rules: {
|
||||||
'no-var': 0,
|
'@typescript-eslint/no-unused-vars': 'off',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ['*.test.ts', '*.test.tsx'],
|
files: ['*.(spec|test).ts?(x)'],
|
||||||
|
extends: ['react-app/jest'],
|
||||||
rules: {
|
rules: {
|
||||||
'@typescript-eslint/no-explicit-any': 0,
|
'import/first': 'off',
|
||||||
'@typescript-eslint/no-object-literal-type-assertion': 0,
|
|
||||||
},
|
|
||||||
globals: {
|
|
||||||
fail: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: ['*.test.ts', '*.test.tsx', '*.test.js', '*.test.jsx'],
|
|
||||||
rules: {
|
|
||||||
'max-nested-callbacks': ['warn', { max: 10 }],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
env: {
|
|
||||||
browser: true,
|
|
||||||
node: true,
|
|
||||||
es6: true,
|
|
||||||
jest: true,
|
|
||||||
},
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 6,
|
|
||||||
sourceType: 'module',
|
|
||||||
ecmaFeatures: {
|
|
||||||
modules: true,
|
|
||||||
jsx: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
globals: {
|
|
||||||
remark_config: true,
|
|
||||||
__webpack_public_path__: true,
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
'@typescript-eslint/indent': 0,
|
|
||||||
'react/jsx-uses-react': 2,
|
|
||||||
'react/jsx-uses-vars': 2,
|
|
||||||
'no-cond-assign': 1,
|
|
||||||
'no-empty': ['error', { allowEmptyCatch: true }],
|
|
||||||
'no-console': 1,
|
|
||||||
camelcase: 0,
|
|
||||||
'comma-style': 2,
|
|
||||||
'max-nested-callbacks': [2, 3],
|
|
||||||
'no-eval': 2,
|
|
||||||
'no-implied-eval': 2,
|
|
||||||
'no-new-func': 2,
|
|
||||||
'guard-for-in': 2,
|
|
||||||
eqeqeq: 2,
|
|
||||||
'no-else-return': 2,
|
|
||||||
'no-redeclare': 2,
|
|
||||||
'no-dupe-keys': 2,
|
|
||||||
radix: 2,
|
|
||||||
strict: [2, 'never'],
|
|
||||||
'no-shadow': 0,
|
|
||||||
'callback-return': [1, ['callback', 'cb', 'next', 'done']],
|
|
||||||
'no-delete-var': 2,
|
|
||||||
'no-undef-init': 2,
|
|
||||||
'no-shadow-restricted-names': 2,
|
|
||||||
'handle-callback-err': 2,
|
|
||||||
'no-lonely-if': 2,
|
|
||||||
'constructor-super': 2,
|
|
||||||
'no-this-before-super': 2,
|
|
||||||
'no-dupe-class-members': 2,
|
|
||||||
'no-const-assign': 2,
|
|
||||||
'prefer-spread': 2,
|
|
||||||
'prefer-const': 2,
|
|
||||||
'no-useless-concat': 2,
|
|
||||||
'no-var': 2,
|
|
||||||
'object-shorthand': 2,
|
|
||||||
'prefer-arrow-callback': 2,
|
|
||||||
'prettier/prettier': 2,
|
|
||||||
'@typescript-eslint/no-var-requires': 0,
|
|
||||||
'@typescript-eslint/explicit-function-return-type': 0,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|||||||
+5
-1
@@ -1,2 +1,6 @@
|
|||||||
.env
|
node_modules
|
||||||
extracted-messages
|
extracted-messages
|
||||||
|
/public
|
||||||
|
/*.log
|
||||||
|
.env
|
||||||
|
tsconfig.tsbuildinfo
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
'./**/*.{ts,tsx,js,jsx}': ['eslint --fix --max-warnings=0', 'prettier --write'],
|
'./**/*.{ts,tsx,js,jsx}': ['eslint --fix --max-warnings=0', 'prettier --write'],
|
||||||
'./**/*.{scss,pcss,css}': ['prettier --write', 'stylelint'],
|
'./**/*.css': ['prettier --write', 'stylelint'],
|
||||||
'./iframe.html': ['prettier --write', 'stylelint'],
|
'./templates/**.html': ['prettier --write', 'stylelint'],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
node_modules
|
node_modules
|
||||||
public
|
public
|
||||||
|
package.json
|
||||||
|
package-lock.json
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
printWidth: 120,
|
printWidth: 120,
|
||||||
useTabs: false,
|
useTabs: false,
|
||||||
|
tabWidth: 2,
|
||||||
semi: true,
|
semi: true,
|
||||||
singleQuote: true,
|
singleQuote: true,
|
||||||
trailingComma: 'es5',
|
trailingComma: 'es5',
|
||||||
@@ -8,7 +9,7 @@ module.exports = {
|
|||||||
arrowParens: 'avoid',
|
arrowParens: 'avoid',
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
files: ['*.ejs', '*.html'],
|
files: ['*.html'],
|
||||||
options: {
|
options: {
|
||||||
trailingComma: 'none',
|
trailingComma: 'none',
|
||||||
},
|
},
|
||||||
|
|||||||
+26
-6
@@ -1,22 +1,42 @@
|
|||||||
module.exports = [
|
module.exports = [
|
||||||
{
|
{
|
||||||
path: 'public/embed.js',
|
path: 'public/embed.mjs',
|
||||||
limit: '2.7 KB',
|
limit: '2.5 KB',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
limit: '86 KB',
|
path: 'public/embed.js',
|
||||||
|
limit: '2.5 KB',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
limit: '70 KB',
|
||||||
|
path: 'public/remark.mjs',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
limit: '73.5 KB',
|
||||||
path: 'public/remark.js',
|
path: 'public/remark.js',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
limit: '44 KB',
|
limit: '31 KB',
|
||||||
|
path: 'public/last-comments.mjs',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
limit: '38 KB',
|
||||||
path: 'public/last-comments.js',
|
path: 'public/last-comments.js',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'public/deleteme.mjs',
|
||||||
|
limit: '11 KB',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'public/deleteme.js',
|
path: 'public/deleteme.js',
|
||||||
limit: '35 KB',
|
limit: '18 KB',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'public/counter.mjs',
|
||||||
|
limit: '0.7 KB',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'public/counter.js',
|
path: 'public/counter.js',
|
||||||
limit: '1.05 KB',
|
limit: '0.7 KB',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
node_modules
|
node_modules
|
||||||
public
|
public
|
||||||
|
extracted-messages
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
const path = require('path');
|
const { CUSTOM_PROPERTIES_PATH } = require('./webpack.config');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
extends: ['stylelint-config-standard', 'stylelint-config-prettier'],
|
extends: ['stylelint-config-standard', 'stylelint-config-prettier'],
|
||||||
plugins: ['stylelint-value-no-unknown-custom-properties', '@mavrin/stylelint-declaration-use-css-custom-properties'],
|
plugins: ['stylelint-value-no-unknown-custom-properties', '@mavrin/stylelint-declaration-use-css-custom-properties'],
|
||||||
@@ -19,7 +20,7 @@ module.exports = {
|
|||||||
'csstools/value-no-unknown-custom-properties': [
|
'csstools/value-no-unknown-custom-properties': [
|
||||||
true,
|
true,
|
||||||
{
|
{
|
||||||
importFrom: path.resolve(__dirname, './app/custom-properties.css'),
|
importFrom: CUSTOM_PROPERTIES_PATH,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
- if you want IDE integration, you need `eslint` and `stylelint` plugin to be installed.
|
- if you want IDE integration, you need `eslint` and `stylelint` plugin to be installed.
|
||||||
|
|
||||||
### CSS Styles
|
### CSS Styles
|
||||||
|
|
||||||
- although styles have `scss` extension, it is actually pack of post-css plugins, so syntax differs, for example in `calc` function.
|
- although styles have `scss` extension, it is actually pack of post-css plugins, so syntax differs, for example in `calc` function.
|
||||||
- now we are migrating to css-modules and this is recomended way to stylization. A file with styles should be named like `component.module.css`
|
- now we are migrating to css-modules and this is recomended way to stylization. A file with styles should be named like `component.module.css`
|
||||||
- old component styles use BEM notation (at least it should): `block__element_modifier`. Also there are `mix` classes: `block_modifier`.
|
- old component styles use BEM notation (at least it should): `block__element_modifier`. Also there are `mix` classes: `block_modifier`.
|
||||||
@@ -19,7 +20,7 @@
|
|||||||
|
|
||||||
- imports for typescript, javascript files should be without extension: `./index`, not `./index.ts`
|
- imports for typescript, javascript files should be without extension: `./index`, not `./index.ts`
|
||||||
- if file resides in the same directory or in subdirectory import should be relative: `./types/something`
|
- if file resides in the same directory or in subdirectory import should be relative: `./types/something`
|
||||||
- otherwise it should start from `@app` namespace: `@app/common/store` which mapped to `/app/common/store.ts` in webpack, tsconfig and jest
|
- otherwise it should start from ``namespace:`common/store`which mapped to`/appcommon/store.ts` in webpack, tsconfig and jest
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
/**
|
|
||||||
* Veriable responsive for dynamic setting public path for
|
|
||||||
* assets. Dynamic imports with relative url will be resolved over this path.
|
|
||||||
*
|
|
||||||
* https://webpack.js.org/guides/public-path/#on-the-fly
|
|
||||||
*/
|
|
||||||
declare var __webpack_public_path__: string;
|
|
||||||
-14
@@ -1,14 +0,0 @@
|
|||||||
declare module 'bem-react-helper' {
|
|
||||||
export interface Mods {
|
|
||||||
[key: string]: string | number | boolean | undefined | null;
|
|
||||||
}
|
|
||||||
export type Mix = Array<string | undefined> | string;
|
|
||||||
export default function b(
|
|
||||||
classname: string,
|
|
||||||
props?: {
|
|
||||||
mods?: Mods;
|
|
||||||
mix?: Mix;
|
|
||||||
},
|
|
||||||
override_props?: Mods
|
|
||||||
): string;
|
|
||||||
}
|
|
||||||
-2
@@ -1,2 +0,0 @@
|
|||||||
declare module 'intersection-observer';
|
|
||||||
declare module 'whatwg-fetch';
|
|
||||||
Vendored
-28
@@ -1,28 +0,0 @@
|
|||||||
import { Theme } from '@app/common/types';
|
|
||||||
import { CommentsConfig } from '@app/common/config-types';
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
remark_config: CommentsConfig;
|
|
||||||
REMARK42: {
|
|
||||||
changeTheme?: (theme: Theme) => void;
|
|
||||||
destroy?: () => void;
|
|
||||||
createInstance: (
|
|
||||||
remark_config: CommentsConfig
|
|
||||||
) =>
|
|
||||||
| {
|
|
||||||
changeTheme(theme: Theme): void;
|
|
||||||
destroy(): void;
|
|
||||||
}
|
|
||||||
| undefined;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace NodeJS {
|
|
||||||
interface Global {
|
|
||||||
Headers: typeof Headers;
|
|
||||||
localStorage: typeof Storage;
|
|
||||||
fetch: typeof fetch;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
global.Headers = class HeadersMock implements Headers {
|
global.Headers = class HeadersMock extends Headers implements Headers {
|
||||||
private headers = new Map();
|
private headers = new Map();
|
||||||
|
|
||||||
append(key: string, value: string) {
|
append(key: string, value: string) {
|
||||||
@@ -16,7 +16,6 @@ global.Headers = class HeadersMock implements Headers {
|
|||||||
delete(key: string) {
|
delete(key: string) {
|
||||||
this.headers.delete(key);
|
this.headers.delete(key);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
forEach(callbackfn: (value: string, key: string, parent: Headers) => void, thisArg?: any) {
|
forEach(callbackfn: (value: string, key: string, parent: Headers) => void, thisArg?: any) {
|
||||||
this.headers.forEach((value, key) => {
|
this.headers.forEach((value, key) => {
|
||||||
callbackfn.call(thisArg || this, value, key, this);
|
callbackfn.call(thisArg || this, value, key, this);
|
||||||
@@ -1,15 +1,4 @@
|
|||||||
import 'jest-extended';
|
import { StaticStore } from 'common/static-store';
|
||||||
import 'jest-enzyme';
|
|
||||||
import { configure } from 'enzyme';
|
|
||||||
import PreactAdapter from 'enzyme-adapter-preact-pure';
|
|
||||||
|
|
||||||
import { StaticStore } from '@app/common/static_store';
|
|
||||||
|
|
||||||
import './mockHeaders';
|
|
||||||
|
|
||||||
configure({ adapter: new PreactAdapter() });
|
|
||||||
|
|
||||||
require('document-register-element/pony')(window);
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
StaticStore.config = {
|
StaticStore.config = {
|
||||||
@@ -2,4 +2,6 @@ import createMockStore from 'redux-mock-store';
|
|||||||
import thunk from 'redux-thunk';
|
import thunk from 'redux-thunk';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export const mockStore = createMockStore<any, any>([thunk]);
|
const mockStore = createMockStore<any, any>([thunk]);
|
||||||
|
|
||||||
|
export default mockStore;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { User } from '@app/common/types';
|
import type { User } from 'common/types';
|
||||||
|
|
||||||
const user: Readonly<User> = {
|
const user: Readonly<User> = {
|
||||||
id: 'email_1',
|
id: 'email_1',
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const mock: typeof import('@app/common/constants') = {
|
const mock: typeof import('common/constants') = {
|
||||||
...jest.requireActual('@app/common/constants'),
|
...jest.requireActual('common/constants'),
|
||||||
BASE_URL: 'https://demo.remark42.com/',
|
BASE_URL: 'https://demo.remark42.com/',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const mock: typeof import('@app/common/settings') = {
|
const mock: typeof import('common/settings') = {
|
||||||
...jest.requireActual('@app/common/settings'),
|
...jest.requireActual('common/settings'),
|
||||||
siteId: 'remark',
|
siteId: 'remark',
|
||||||
pageTitle: 'remark test',
|
pageTitle: 'remark test',
|
||||||
url: 'https://remark42.com/test',
|
url: 'https://remark42.com/test',
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import jestFetchMock from 'jest-fetch-mock';
|
|||||||
|
|
||||||
import { emailVerificationForSubscribe } from './api';
|
import { emailVerificationForSubscribe } from './api';
|
||||||
|
|
||||||
jest.mock('@app/common/constants', () => ({
|
jest.mock('common/constants', () => ({
|
||||||
BASE_URL: 'https://example.com',
|
BASE_URL: 'https://example.com',
|
||||||
API_BASE: '/api',
|
API_BASE: '/api',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('@app/common/settings', () => ({
|
jest.mock('common/settings', () => ({
|
||||||
siteId: 'remark42',
|
siteId: 'remark42',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -29,11 +29,11 @@ describe('api', () => {
|
|||||||
expect(jestFetchMock.mock.calls.length).toEqual(1);
|
expect(jestFetchMock.mock.calls.length).toEqual(1);
|
||||||
|
|
||||||
const url = jestFetchMock.mock.calls[0][0] as string;
|
const url = jestFetchMock.mock.calls[0][0] as string;
|
||||||
const match = url.match(/address=(\S+)$/);
|
const match = url.match(/address=(\S+)$/) as string[];
|
||||||
|
|
||||||
expect(match).toBeArray();
|
expect(Array.isArray(match)).toBe(true);
|
||||||
expect((match as string[]).length).toBeGreaterThan(1);
|
expect(match.length).toBeGreaterThan(1);
|
||||||
expect((match as string[])[1]).toBe(
|
expect(match[1]).toBe(
|
||||||
"address.!%23%24%25%26'*%2B-%2F%3D%3F%5E_%60%7B%7C%7D~()%2C%3A%3B%3C%3E%5B%5C%5D%40example.com"
|
"address.!%23%24%25%26'*%2B-%2F%3D%3F%5E_%60%7B%7C%7D~()%2C%3A%3B%3C%3E%5B%5C%5D%40example.com"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import fetcher from './fetcher';
|
|||||||
/* common */
|
/* common */
|
||||||
const __loginAnonymously = (username: string): Promise<User | null> => {
|
const __loginAnonymously = (username: string): Promise<User | null> => {
|
||||||
const url = `/auth/anonymous/login?user=${encodeURIComponent(username)}&aud=${siteId}&from=${encodeURIComponent(
|
const url = `/auth/anonymous/login?user=${encodeURIComponent(username)}&aud=${siteId}&from=${encodeURIComponent(
|
||||||
location.origin + location.pathname + '?selfClose'
|
`${window.location.origin}${window.location.pathname}?selfClose`
|
||||||
)}`;
|
)}`;
|
||||||
return fetcher.get<User>({ url, withCredentials: true, overriddenApiBase: '' });
|
return fetcher.get<User>({ url, withCredentials: true, overriddenApiBase: '' });
|
||||||
};
|
};
|
||||||
@@ -35,7 +35,7 @@ export const logIn = (provider: AuthProvider): Promise<User | null> => {
|
|||||||
|
|
||||||
return new Promise<User | null>((resolve, reject) => {
|
return new Promise<User | null>((resolve, reject) => {
|
||||||
const url = `${BASE_URL}/auth/${provider.name}/login?from=${encodeURIComponent(
|
const url = `${BASE_URL}/auth/${provider.name}/login?from=${encodeURIComponent(
|
||||||
location.origin + location.pathname + '?selfClose'
|
`${window.location.origin}${window.location.pathname}?selfClose`
|
||||||
)}&site=${siteId}`;
|
)}&site=${siteId}`;
|
||||||
|
|
||||||
const newWindow = window.open(url);
|
const newWindow = window.open(url);
|
||||||
@@ -267,7 +267,7 @@ export const uploadImage = (image: File): Promise<Image> => {
|
|||||||
name: image.name,
|
name: image.name,
|
||||||
size: image.size,
|
size: image.size,
|
||||||
type: image.type,
|
type: image.type,
|
||||||
url: BASE_URL + API_BASE + '/picture/' + resp.id,
|
url: `${BASE_URL + API_BASE}/picture/${resp.id}`,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
if (!Element.prototype.matches) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
Element.prototype.matches = (Element.prototype as any).msMatchesSelector || Element.prototype.webkitMatchesSelector;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Element.prototype.closest) {
|
|
||||||
Element.prototype.closest = function (s: string) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-this-alias
|
|
||||||
let el: any = this;
|
|
||||||
|
|
||||||
do {
|
|
||||||
if (el.matches(s)) return el;
|
|
||||||
el = el.parentElement || el.parentNode;
|
|
||||||
} while (el !== null && el.nodeType === 1);
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import { UserInfo, Theme } from './types';
|
|
||||||
|
|
||||||
export interface CounterConfig {
|
|
||||||
host: string;
|
|
||||||
site_id: string;
|
|
||||||
url?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type UserInfoConfig = UserInfo;
|
|
||||||
|
|
||||||
export interface CommentsConfig {
|
|
||||||
host: string;
|
|
||||||
site_id: string;
|
|
||||||
url?: string;
|
|
||||||
max_shown_comments?: number;
|
|
||||||
theme?: Theme;
|
|
||||||
page_title?: string;
|
|
||||||
node?: string | HTMLElement;
|
|
||||||
locale?: string;
|
|
||||||
show_email_subscription?: boolean;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
__colors__?: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LastCommentsConfig {
|
|
||||||
host: string;
|
|
||||||
site_id: string;
|
|
||||||
max_last_comments: number;
|
|
||||||
locale?: string;
|
|
||||||
}
|
|
||||||
@@ -1,16 +1,15 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
|
|
||||||
// based on https://github.com/sindresorhus/copy-text-to-clipboard, but improved to copy text styles too
|
// based on https://github.com/sindresorhus/copy-text-to-clipboard, but improved to copy text styles too
|
||||||
export default (input: string): boolean => {
|
export default function copy(input: string): boolean {
|
||||||
const el = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
|
|
||||||
el.innerHTML = input;
|
el.innerHTML = input;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
Object.assign(el.style, {
|
||||||
(el.style as any).contain = 'strict';
|
contain: 'strict',
|
||||||
el.style.position = 'absolute';
|
position: 'absolute',
|
||||||
el.style.left = '-9999px';
|
left: '-9999px',
|
||||||
el.style.fontSize = '12pt'; // Prevent zooming on iOS
|
fontSize: '12pt', // Prevent zooming on iOS
|
||||||
|
});
|
||||||
|
|
||||||
document.body.appendChild(el);
|
document.body.appendChild(el);
|
||||||
|
|
||||||
@@ -60,4 +59,4 @@ export default (input: string): boolean => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return success;
|
return success;
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ describe('fetcher', () => {
|
|||||||
error: 'you just cant',
|
error: 'you just cant',
|
||||||
details: 'you just cant at all',
|
details: 'you just cant at all',
|
||||||
};
|
};
|
||||||
(window.fetch as any) = jest.fn().mockImplementation(async () => ({
|
window.fetch = jest.fn().mockImplementation(async () => ({
|
||||||
status: 400,
|
status: 400,
|
||||||
headers: new (window as any).Headers(),
|
headers: new Headers(),
|
||||||
json: async () => response,
|
json: async () => response,
|
||||||
text: async () => JSON.stringify(response),
|
text: async () => JSON.stringify(response),
|
||||||
}));
|
}));
|
||||||
@@ -18,7 +18,7 @@ describe('fetcher', () => {
|
|||||||
return fetcher
|
return fetcher
|
||||||
.get('/api/some')
|
.get('/api/some')
|
||||||
.then(data => {
|
.then(data => {
|
||||||
fail(data);
|
throw new Error('Request should be failed');
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
expect(e.code).toBe(2);
|
expect(e.code).toBe(2);
|
||||||
@@ -40,7 +40,7 @@ describe('fetcher', () => {
|
|||||||
return fetcher
|
return fetcher
|
||||||
.get('/api/some')
|
.get('/api/some')
|
||||||
.then(data => {
|
.then(data => {
|
||||||
fail(data);
|
throw new Error('Request should be failed');
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
expect(e.code).toBe(401);
|
expect(e.code).toBe(401);
|
||||||
@@ -51,16 +51,18 @@ describe('fetcher', () => {
|
|||||||
(jest.spyOn(window, 'fetch') as any).mockImplementation(async () => ({
|
(jest.spyOn(window, 'fetch') as any).mockImplementation(async () => ({
|
||||||
status: 400,
|
status: 400,
|
||||||
headers: new (window as any).Headers(),
|
headers: new (window as any).Headers(),
|
||||||
json: async () => {
|
async json() {
|
||||||
throw new Error('json parse error');
|
throw new Error('json parse error');
|
||||||
},
|
},
|
||||||
text: async () => 'you given me something wrong',
|
async text() {
|
||||||
|
return 'you given me something wrong';
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return fetcher
|
return fetcher
|
||||||
.get({ url: '/api/some', logError: false })
|
.get({ url: '/api/some', logError: false })
|
||||||
.then(data => {
|
.then(data => {
|
||||||
fail(data);
|
throw new Error('Request should be failed');
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
expect(e.code).toBe(0);
|
expect(e.code).toBe(0);
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import { httpErrorMap, isFailedFetch, httpMessages, RequestError } from 'utils/errorUtils';
|
||||||
|
|
||||||
import { BASE_URL, API_BASE, HEADER_X_JWT } from './constants';
|
import { BASE_URL, API_BASE, HEADER_X_JWT } from './constants';
|
||||||
import { siteId } from './settings';
|
import { siteId } from './settings';
|
||||||
import { StaticStore } from './static_store';
|
import { StaticStore } from './static-store';
|
||||||
import { getCookie } from './cookies';
|
import { getCookie } from './cookies';
|
||||||
import { httpErrorMap, isFailedFetch, httpMessages } from '@app/utils/errorUtils';
|
|
||||||
|
|
||||||
export type FetcherMethod = 'get' | 'post' | 'put' | 'patch' | 'delete' | 'head';
|
export type FetcherMethod = 'get' | 'post' | 'put' | 'patch' | 'delete' | 'head';
|
||||||
const methods: FetcherMethod[] = ['get', 'post', 'put', 'patch', 'delete', 'head'];
|
const methods: FetcherMethod[] = ['get', 'post', 'put', 'patch', 'delete', 'head'];
|
||||||
@@ -33,7 +34,7 @@ type FetcherObject = { [K in FetcherMethod]: <T = unknown>(data: FetcherInit) =>
|
|||||||
let activeJwtToken: string | undefined;
|
let activeJwtToken: string | undefined;
|
||||||
|
|
||||||
const fetcher = methods.reduce<Partial<FetcherObject>>((acc, method) => {
|
const fetcher = methods.reduce<Partial<FetcherObject>>((acc, method) => {
|
||||||
acc[method] = <T = unknown>(data: FetcherInit): Promise<T> => {
|
acc[method] = async <T = unknown>(data: FetcherInit): Promise<T> => {
|
||||||
const {
|
const {
|
||||||
url,
|
url,
|
||||||
body = undefined,
|
body = undefined,
|
||||||
@@ -77,7 +78,7 @@ const fetcher = methods.reduce<Partial<FetcherObject>>((acc, method) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (siteId && method !== 'post' && !rurl.includes('?site=') && !rurl.includes('&site=')) {
|
if (siteId && method !== 'post' && !rurl.includes('?site=') && !rurl.includes('&site=')) {
|
||||||
rurl += (rurl.includes('?') ? '&' : '?') + `site=${siteId}`;
|
rurl += `${rurl.includes('?') ? '&' : '?'}site=${siteId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return fetch(rurl, parameters)
|
return fetch(rurl, parameters)
|
||||||
@@ -99,10 +100,8 @@ const fetcher = methods.reduce<Partial<FetcherObject>>((acc, method) => {
|
|||||||
if (res.status >= 400) {
|
if (res.status >= 400) {
|
||||||
if (httpErrorMap.has(res.status)) {
|
if (httpErrorMap.has(res.status)) {
|
||||||
const descriptor = httpErrorMap.get(res.status) || httpMessages.unexpectedError;
|
const descriptor = httpErrorMap.get(res.status) || httpMessages.unexpectedError;
|
||||||
throw {
|
|
||||||
code: res.status,
|
throw new RequestError(descriptor.defaultMessage, res.status);
|
||||||
error: descriptor.defaultMessage,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return res.text().then(text => {
|
return res.text().then(text => {
|
||||||
let err;
|
let err;
|
||||||
@@ -110,13 +109,10 @@ const fetcher = methods.reduce<Partial<FetcherObject>>((acc, method) => {
|
|||||||
err = JSON.parse(text);
|
err = JSON.parse(text);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (logError) {
|
if (logError) {
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
throw {
|
|
||||||
code: 0,
|
throw new RequestError(httpMessages.unexpectedError.defaultMessage, 0);
|
||||||
error: httpMessages.unexpectedError.defaultMessage,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
@@ -130,11 +126,9 @@ const fetcher = methods.reduce<Partial<FetcherObject>>((acc, method) => {
|
|||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
if (isFailedFetch(e)) {
|
if (isFailedFetch(e)) {
|
||||||
throw {
|
throw new RequestError(e.message, -2);
|
||||||
code: -2,
|
|
||||||
error: e.message,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw e;
|
throw e;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ describe('updateJsonItem', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should set data to empty localStorage', () => {
|
it('should set data to empty localStorage', () => {
|
||||||
updateJsonItem<Record<string, string>>(LS_KEY, {});
|
updateJsonItem(LS_KEY, {});
|
||||||
|
|
||||||
expect(localStorage.getItem(LS_KEY)).toBe(JSON.stringify({}));
|
expect(localStorage.getItem(LS_KEY)).toBe(JSON.stringify({}));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
import { IS_STORAGE_AVAILABLE } from './constants';
|
import { IS_STORAGE_AVAILABLE } from './constants';
|
||||||
|
|
||||||
const failMessage = 'remark42: localStorage access denied, check browser preferences';
|
const failMessage = 'remark42: localStorage access denied, check browser preferences';
|
||||||
@@ -47,9 +46,9 @@ export function setJsonItem<T = any>(key: string, data: T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateJsonItem<T = Record<string, any> | any[]>(key: string, value: (data: T) => T): void;
|
export function updateJsonItem<T extends {}>(key: string, value: (data: T) => T): void;
|
||||||
export function updateJsonItem<T = any[]>(key: string, value: T): void;
|
export function updateJsonItem<T extends {}>(key: string, value: T): void;
|
||||||
export function updateJsonItem<T = Record<string, any>>(key: string, value: T) {
|
export function updateJsonItem<T = Record<string, unknown>>(key: string, value: T) {
|
||||||
const savedData = getJsonItem<any>(key);
|
const savedData = getJsonItem<any>(key);
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
import 'regenerator-runtime/runtime';
|
|
||||||
import 'es6-promise/auto';
|
|
||||||
import 'focus-visible';
|
|
||||||
import '@webcomponents/custom-elements';
|
|
||||||
import './closest-polyfill';
|
|
||||||
|
|
||||||
export default async function loadPolyfills() {
|
|
||||||
function fillCoreJs() {
|
|
||||||
if (
|
|
||||||
'startsWith' in String.prototype &&
|
|
||||||
'endsWith' in String.prototype &&
|
|
||||||
'includes' in Array.prototype &&
|
|
||||||
'assign' in Object &&
|
|
||||||
'keys' in Object
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return import(/* webpackChunkName: "core-js" */ 'core-js');
|
|
||||||
}
|
|
||||||
|
|
||||||
function fillFetch() {
|
|
||||||
if ('fetch' in window) return;
|
|
||||||
|
|
||||||
return import(/* webpackChunkName: "whatwg-fetch" */ 'whatwg-fetch');
|
|
||||||
}
|
|
||||||
|
|
||||||
function fillIntersectionObserver() {
|
|
||||||
if (
|
|
||||||
'IntersectionObserver' in window &&
|
|
||||||
'IntersectionObserverEntry' in window &&
|
|
||||||
'intersectionRatio' in window.IntersectionObserverEntry.prototype
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return import(/* webpackChunkName: "intersection-observer" */ 'intersection-observer');
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all([fillCoreJs(), fillFetch(), fillIntersectionObserver()]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Theme } from './types';
|
import parseQuery from 'utils/parseQuery';
|
||||||
|
|
||||||
|
import type { Theme } from './types';
|
||||||
import { THEMES, MAX_SHOWN_ROOT_COMMENTS } from './constants';
|
import { THEMES, MAX_SHOWN_ROOT_COMMENTS } from './constants';
|
||||||
import parseQuery from '@app/utils/parseQuery';
|
|
||||||
|
|
||||||
export interface QuerySettingsType {
|
export interface QuerySettingsType {
|
||||||
site_id?: string;
|
site_id?: string;
|
||||||
@@ -29,9 +30,9 @@ if (!querySettings.theme || THEMES.indexOf(querySettings.theme) === -1) {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
querySettings.show_email_subscription = (querySettings.show_email_subscription as any) !== 'false';
|
querySettings.show_email_subscription = (querySettings.show_email_subscription as any) !== 'false';
|
||||||
|
|
||||||
export const siteId = querySettings.site_id;
|
export const siteId = querySettings.site_id!;
|
||||||
export const pageTitle = querySettings.page_title;
|
export const pageTitle = querySettings.page_title;
|
||||||
export const url = querySettings.url;
|
export const url = querySettings.url;
|
||||||
export const maxShownComments = querySettings.max_shown_comments;
|
export const maxShownComments = querySettings.max_shown_comments;
|
||||||
export const token = querySettings.token;
|
export const token = querySettings.token!;
|
||||||
export const theme = querySettings.theme;
|
export const theme = querySettings.theme;
|
||||||
|
|||||||
@@ -118,13 +118,6 @@ export interface Config {
|
|||||||
emoji_enabled: boolean;
|
emoji_enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RemarkConfig {
|
|
||||||
site_id: string;
|
|
||||||
url: string;
|
|
||||||
/** used in last comments widget */
|
|
||||||
max_last_comments?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Sorting = '-time' | '+time' | '-active' | '+active' | '-score' | '+score' | '-controversy' | '+controversy';
|
export type Sorting = '-time' | '+time' | '-active' | '+active' | '-score' | '+score' | '-controversy' | '+controversy';
|
||||||
|
|
||||||
export type AuthProvider =
|
export type AuthProvider =
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { UserInfo } from './types';
|
import parseQuery from 'utils/parseQuery';
|
||||||
import parseQuery from '@app/utils/parseQuery';
|
|
||||||
|
|
||||||
export const userInfo: Partial<UserInfo> = parseQuery();
|
import type { UserInfo } from './types';
|
||||||
|
|
||||||
|
export const userInfo: UserInfo = parseQuery();
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export const isDefaultPicture = ((userInfo.isDefaultPicture as any) as string) !== '1';
|
export const isDefaultPicture = ((userInfo.isDefaultPicture as any) as string) !== '1';
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
.auth-panel__column:last-child {
|
||||||
|
margin-left: 8px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
.auth-panel__column {
|
|
||||||
&:last-child {
|
|
||||||
margin-left: 8px;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
.auth-panel_logged-in {
|
.auth-panel_logged-in {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|
||||||
.auth-panel__column {
|
& .auth-panel__column {
|
||||||
&:first-child {
|
&:first-child {
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
.auth-panel_theme_dark {
|
.auth-panel_theme_dark {
|
||||||
.auth-panel__user-id {
|
& .auth-panel__user-id {
|
||||||
color: var(--color5);
|
color: var(--color5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
.auth-panel_theme_light {
|
.auth-panel_theme_light {
|
||||||
.auth-panel__user-id {
|
& .auth-panel__user-id {
|
||||||
color: var(--color13);
|
color: var(--color13);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,15 @@
|
|||||||
/** @jsx createElement */
|
import { h } from 'preact';
|
||||||
import { createElement } from 'preact';
|
|
||||||
import { mount } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
import createMockStore from 'redux-mock-store';
|
import createMockStore from 'redux-mock-store';
|
||||||
import { Middleware } from 'redux';
|
import { Middleware } from 'redux';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { IntlProvider } from 'react-intl';
|
import { IntlProvider } from 'react-intl';
|
||||||
|
|
||||||
import enMessages from '@app/locales/en.json';
|
import enMessages from 'locales/en.json';
|
||||||
|
|
||||||
import AuthPanel, { Props } from './auth-panel';
|
import AuthPanel, { Props } from './auth-panel';
|
||||||
import { Button } from '../button';
|
import { Button } from '../button';
|
||||||
import { StaticStore } from '@app/common/static_store';
|
import { StaticStore } from 'common/static-store';
|
||||||
|
|
||||||
const DefaultProps = {
|
const DefaultProps = {
|
||||||
providers: ['google', 'github'],
|
providers: ['google', 'github'],
|
||||||
@@ -83,7 +82,7 @@ describe('<AuthPanel />', () => {
|
|||||||
const firstCol = element.find('.auth-panel__column').first();
|
const firstCol = element.find('.auth-panel__column').first();
|
||||||
const providerButtons = firstCol.find(Button);
|
const providerButtons = firstCol.find(Button);
|
||||||
|
|
||||||
expect(firstCol.text()).toStartWith('Login:');
|
expect(firstCol.text().startsWith('Login:')).toBe(true);
|
||||||
expect(providerButtons.at(0).text()).toBe('Google');
|
expect(providerButtons.at(0).text()).toBe('Google');
|
||||||
expect(providerButtons.at(1).text()).toBe('GitHub');
|
expect(providerButtons.at(1).text()).toBe('GitHub');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,22 +1,21 @@
|
|||||||
/** @jsx createElement */
|
import { h, Component, Fragment } from 'preact';
|
||||||
import { createElement, Component, Fragment } from 'preact';
|
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { FormattedMessage, defineMessages, IntlShape, useIntl } from 'react-intl';
|
import { FormattedMessage, defineMessages, IntlShape, useIntl } from 'react-intl';
|
||||||
import b from 'bem-react-helper';
|
import b from 'bem-react-helper';
|
||||||
|
|
||||||
import { User, AuthProvider, Sorting, Theme, PostInfo } from '@app/common/types';
|
import { User, AuthProvider, Sorting, Theme, PostInfo } from 'common/types';
|
||||||
import { IS_STORAGE_AVAILABLE, IS_THIRD_PARTY } from '@app/common/constants';
|
import { IS_STORAGE_AVAILABLE, IS_THIRD_PARTY } from 'common/constants';
|
||||||
import { requestDeletion } from '@app/utils/email';
|
import { requestDeletion } from 'utils/email';
|
||||||
import postMessage from '@app/utils/postMessage';
|
import postMessage from 'utils/postMessage';
|
||||||
import { getHandleClickProps } from '@app/common/accessibility';
|
import { getHandleClickProps } from 'common/accessibility';
|
||||||
import { StoreState } from '@app/store';
|
import { StoreState } from 'store';
|
||||||
import { ProviderState } from '@app/store/provider/reducers';
|
import { ProviderState } from 'store/provider/reducers';
|
||||||
import { Dropdown, DropdownItem } from '@app/components/dropdown';
|
import { Dropdown, DropdownItem } from 'components/dropdown';
|
||||||
import { Button } from '@app/components/button';
|
import { Button } from 'components/button';
|
||||||
import Auth from '@app/components/auth';
|
import Auth from 'components/auth';
|
||||||
|
|
||||||
import useTheme from '@app/hooks/useTheme';
|
import useTheme from 'hooks/useTheme';
|
||||||
import { StaticStore } from '@app/common/static_store';
|
import { StaticStore } from 'common/static-store';
|
||||||
|
|
||||||
export interface OwnProps {
|
export interface OwnProps {
|
||||||
user: User | null;
|
user: User | null;
|
||||||
@@ -132,6 +131,7 @@ export class AuthPanel extends Component<Props, State> {
|
|||||||
className="auth-panel__pseudo-link"
|
className="auth-panel__pseudo-link"
|
||||||
href={`${window.location.origin}/web/comments.html${window.location.search}`}
|
href={`${window.location.origin}/web/comments.html${window.location.search}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
<FormattedMessage id="authPanel.new-page" defaultMessage="new page" />
|
<FormattedMessage id="authPanel.new-page" defaultMessage="new page" />
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
|
import './auth-panel.css';
|
||||||
|
|
||||||
|
import './__readonly-label/auth-panel__readonly-label.css';
|
||||||
|
|
||||||
|
import './__column/auth-panel__column.css';
|
||||||
|
import './__select/auth-panel__select.css';
|
||||||
|
import './__select-label/auth-panel__select-label.css';
|
||||||
|
import './__select-label-value/auth-panel__select-label-value.css';
|
||||||
|
import './__sort/auth-panel__sort.css';
|
||||||
|
|
||||||
|
import './__user-id/auth-panel__user-id.css';
|
||||||
|
|
||||||
|
import './_theme/_dark/auth-panel_theme_dark.css';
|
||||||
|
import './_theme/_light/auth-panel_theme_light.css';
|
||||||
|
|
||||||
|
import './_logged-in/auth-panel_logged-in.css';
|
||||||
|
|
||||||
export { default } from './auth-panel';
|
export { default } from './auth-panel';
|
||||||
|
|
||||||
import './auth-panel.scss';
|
|
||||||
|
|
||||||
import './__readonly-label/auth-panel__readonly-label.scss';
|
|
||||||
|
|
||||||
import './__column/auth-panel__column.scss';
|
|
||||||
import './__select/auth-panel__select.scss';
|
|
||||||
import './__select-label/auth-panel__select-label.scss';
|
|
||||||
import './__select-label-value/auth-panel__select-label-value.scss';
|
|
||||||
import './__sort/auth-panel__sort.scss';
|
|
||||||
|
|
||||||
import './__user-id/auth-panel__user-id.scss';
|
|
||||||
|
|
||||||
import './_theme/_dark/auth-panel_theme_dark.scss';
|
|
||||||
import './_theme/_light/auth-panel_theme_light.scss';
|
|
||||||
|
|
||||||
import './_logged-in/auth-panel_logged-in.scss';
|
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
/** @jsx createElement */
|
import { h, Component, createRef } from 'preact';
|
||||||
import { createElement, Component, createRef } from 'preact';
|
|
||||||
import b from 'bem-react-helper';
|
import b from 'bem-react-helper';
|
||||||
import { IntlShape, defineMessages, FormattedMessage } from 'react-intl';
|
import { IntlShape, defineMessages, FormattedMessage } from 'react-intl';
|
||||||
import { Theme } from '@app/common/types';
|
import { Theme } from 'common/types';
|
||||||
|
|
||||||
import { Input } from '@app/components/input';
|
import { Input } from 'components/input';
|
||||||
import { Button } from '@app/components/button';
|
import { Button } from 'components/button';
|
||||||
|
|
||||||
import { validateUserName } from '../validateUserName';
|
import { validateUserName } from '../validateUserName';
|
||||||
|
|
||||||
@@ -89,7 +88,7 @@ export class AnonymousLoginForm extends Component<Props, State> {
|
|||||||
// TODO: will be great to `b` to accept `string | undefined | (string|undefined)[]` as classname
|
// TODO: will be great to `b` to accept `string | undefined | (string|undefined)[]` as classname
|
||||||
let className = b('auth-anonymous-login-form', {}, { theme: props.theme });
|
let className = b('auth-anonymous-login-form', {}, { theme: props.theme });
|
||||||
if (props.className) {
|
if (props.className) {
|
||||||
className += ' ' + b('auth-anonymous-login-form', {}, { theme: props.theme });
|
className += ` ${b('auth-anonymous-login-form', {}, { theme: props.theme })}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const usernameInvalidReason = this.getUsernameInvalidReason();
|
const usernameInvalidReason = this.getUsernameInvalidReason();
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export { AnonymousLoginForm } from './auth__anonymous-login-form';
|
import './auth__anonymous-login-form.css';
|
||||||
|
|
||||||
import './auth__anonymous-login-form.scss';
|
export { AnonymousLoginForm } from './auth__anonymous-login-form';
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
/** @jsx createElement */
|
import { h } from 'preact';
|
||||||
import { createElement } from 'preact';
|
|
||||||
import { mount, ReactWrapper } from 'enzyme';
|
import { mount, ReactWrapper } from 'enzyme';
|
||||||
import { EmailLoginFormConnected as EmailLoginForm, Props, State } from './auth__email-login-form';
|
import { EmailLoginFormConnected as EmailLoginForm, Props, State } from './auth__email-login-form';
|
||||||
import { User } from '@app/common/types';
|
|
||||||
import { sleep } from '@app/utils/sleep';
|
|
||||||
import { validToken } from '@app/testUtils/mocks/jwt';
|
|
||||||
import { sendEmailVerificationRequest } from '@app/common/api';
|
|
||||||
import { IntlProvider } from 'react-intl';
|
import { IntlProvider } from 'react-intl';
|
||||||
import enMessages from '../../../locales/en.json';
|
|
||||||
import { LS_EMAIL_KEY } from '@app/common/constants';
|
|
||||||
|
|
||||||
jest.mock('@app/utils/jwt', () => ({
|
import { validToken } from '__stubs__/jwt';
|
||||||
|
import { LS_EMAIL_KEY } from 'common/constants';
|
||||||
|
import { User } from 'common/types';
|
||||||
|
import { sleep } from 'utils/sleep';
|
||||||
|
import { sendEmailVerificationRequest } from 'common/api';
|
||||||
|
import enMessages from 'locales/en.json';
|
||||||
|
|
||||||
|
jest.mock('utils/jwt', () => ({
|
||||||
isJwtExpired: jest
|
isJwtExpired: jest
|
||||||
.fn()
|
.fn()
|
||||||
.mockImplementationOnce(() => true)
|
.mockImplementationOnce(() => true)
|
||||||
@@ -18,7 +18,7 @@ jest.mock('@app/utils/jwt', () => ({
|
|||||||
.mockImplementationOnce(() => true),
|
.mockImplementationOnce(() => true),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('@app/common/api');
|
jest.mock('common/api');
|
||||||
|
|
||||||
function simulateInput(input: ReactWrapper, value: string) {
|
function simulateInput(input: ReactWrapper, value: string) {
|
||||||
input.getDOMNode<HTMLTextAreaElement>().value = value;
|
input.getDOMNode<HTMLTextAreaElement>().value = value;
|
||||||
|
|||||||
@@ -1,22 +1,20 @@
|
|||||||
/** @jsx createElement */
|
import { h, Component, createRef } from 'preact';
|
||||||
import { createElement, Component, createRef } from 'preact';
|
|
||||||
import { forwardRef } from 'preact/compat';
|
import { forwardRef } from 'preact/compat';
|
||||||
import b from 'bem-react-helper';
|
import b from 'bem-react-helper';
|
||||||
import { Theme, User } from '@app/common/types';
|
import { Theme, User } from 'common/types';
|
||||||
import { sendEmailVerificationRequest } from '@app/common/api';
|
import { LS_EMAIL_KEY } from 'common/constants';
|
||||||
import { extractErrorMessageFromResponse } from '@app/utils/errorUtils';
|
import { sendEmailVerificationRequest } from 'common/api';
|
||||||
import { getHandleClickProps } from '@app/common/accessibility';
|
import { extractErrorMessageFromResponse } from 'utils/errorUtils';
|
||||||
import { sleep } from '@app/utils/sleep';
|
import { getHandleClickProps } from 'common/accessibility';
|
||||||
import TextareaAutosize from '@app/components/comment-form/textarea-autosize';
|
import { sleep } from 'utils/sleep';
|
||||||
import { Input } from '@app/components/input';
|
import TextareaAutosize from 'components/comment-form/textarea-autosize';
|
||||||
import { Button } from '@app/components/button';
|
import { Input } from 'components/input';
|
||||||
import { isJwtExpired } from '@app/utils/jwt';
|
import { Button } from 'components/button';
|
||||||
|
import { isJwtExpired } from 'utils/jwt';
|
||||||
import { defineMessages, IntlShape, useIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, IntlShape, useIntl, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import { validateUserName } from '../validateUserName';
|
import { validateUserName } from '../validateUserName';
|
||||||
|
|
||||||
import { messages as loginForm } from '../__anonymous-login-form/auth__anonymous-login-form';
|
import { messages as loginForm } from '../__anonymous-login-form/auth__anonymous-login-form';
|
||||||
import { LS_EMAIL_KEY } from '@app/common/constants';
|
|
||||||
|
|
||||||
interface OwnProps {
|
interface OwnProps {
|
||||||
onSignIn(token: string): Promise<User | null>;
|
onSignIn(token: string): Promise<User | null>;
|
||||||
@@ -199,7 +197,7 @@ export class EmailLoginForm extends Component<Props, State> {
|
|||||||
// TODO: will be great to `b` to accept `string | undefined | (string|undefined)[]` as classname
|
// TODO: will be great to `b` to accept `string | undefined | (string|undefined)[]` as classname
|
||||||
let className = b('auth-email-login-form', {}, { theme: props.theme });
|
let className = b('auth-email-login-form', {}, { theme: props.theme });
|
||||||
if (props.className) {
|
if (props.className) {
|
||||||
className += ' ' + b('auth-email-login-form', {}, { theme: props.theme });
|
className += ` ${b('auth-email-login-form', {}, { theme: props.theme })}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const form1InvalidReason = this.getForm1InvalidReason();
|
const form1InvalidReason = this.getForm1InvalidReason();
|
||||||
@@ -208,7 +206,7 @@ export class EmailLoginForm extends Component<Props, State> {
|
|||||||
return (
|
return (
|
||||||
<form className={className} onSubmit={this.onVerificationSubmit}>
|
<form className={className} onSubmit={this.onVerificationSubmit}>
|
||||||
<Input
|
<Input
|
||||||
autoFocus
|
autofocus
|
||||||
name="username"
|
name="username"
|
||||||
mix="auth-email-login-form__input"
|
mix="auth-email-login-form__input"
|
||||||
ref={this.usernameInputRef}
|
ref={this.usernameInputRef}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
import './auth__email-login-form.scss';
|
import './auth__email-login-form.css';
|
||||||
|
|
||||||
export { EmailLoginForm, EmailLoginFormConnected, EmailLoginFormRef } from './auth__email-login-form';
|
export { EmailLoginForm, EmailLoginFormConnected } from './auth__email-login-form';
|
||||||
|
export type { EmailLoginFormRef } from './auth__email-login-form';
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
/** @jsx createElement */
|
import { h } from 'preact';
|
||||||
import { createElement } from 'preact';
|
|
||||||
import { IntlProvider } from 'react-intl';
|
import { IntlProvider } from 'react-intl';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
import enMessages from '@app/locales/en.json';
|
import enMessages from 'locales/en.json';
|
||||||
import { mockStore } from '@app/testUtils/mockStore';
|
import stubStore from '__stubs__/store';
|
||||||
import { StaticStore } from '@app/common/static_store';
|
import { StaticStore } from 'common/static-store';
|
||||||
|
|
||||||
import Auth from './auth';
|
import Auth from './auth';
|
||||||
import { mount } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
import { Button } from '../button';
|
import { Button } from '../button';
|
||||||
import { StoreState } from '@app/store';
|
import { StoreState } from 'store';
|
||||||
|
|
||||||
const initialStore = {
|
const initialStore = {
|
||||||
provider: { name: 'google' },
|
provider: { name: 'google' },
|
||||||
@@ -24,7 +23,7 @@ describe('<Auth/>', () => {
|
|||||||
const createWrapper = (store?: Partial<StoreState>) =>
|
const createWrapper = (store?: Partial<StoreState>) =>
|
||||||
mount(
|
mount(
|
||||||
<IntlProvider locale="en" messages={enMessages}>
|
<IntlProvider locale="en" messages={enMessages}>
|
||||||
<Provider store={mockStore(store || initialStore)}>
|
<Provider store={stubStore(store || initialStore)}>
|
||||||
<Auth />
|
<Auth />
|
||||||
</Provider>
|
</Provider>
|
||||||
</IntlProvider>
|
</IntlProvider>
|
||||||
|
|||||||
@@ -1,26 +1,25 @@
|
|||||||
/** @jsx createElement */
|
import { h, Component, createRef } from 'preact';
|
||||||
import { createElement, Component, createRef } from 'preact';
|
|
||||||
import { useCallback } from 'preact/hooks';
|
import { useCallback } from 'preact/hooks';
|
||||||
import { IntlShape, FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
import { IntlShape, FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { AuthProvider, Theme, User } from '@app/common/types';
|
import { AuthProvider, Theme, User } from 'common/types';
|
||||||
import { PROVIDER_NAMES, IS_STORAGE_AVAILABLE } from '@app/common/constants';
|
import { PROVIDER_NAMES, IS_STORAGE_AVAILABLE } from 'common/constants';
|
||||||
import { getHandleClickProps } from '@app/common/accessibility';
|
import { getHandleClickProps } from 'common/accessibility';
|
||||||
import { Button } from '@app/components/button';
|
import { Button } from 'components/button';
|
||||||
import { Dropdown, DropdownItem } from '@app/components/dropdown';
|
import { Dropdown, DropdownItem } from 'components/dropdown';
|
||||||
|
|
||||||
import debounce from '@app/utils/debounce';
|
import debounce from 'utils/debounce';
|
||||||
import { ProviderState } from '@app/store/provider/reducers';
|
import { ProviderState } from 'store/provider/reducers';
|
||||||
import { StaticStore } from '@app/common/static_store';
|
import { StaticStore } from 'common/static-store';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
import { StoreState } from '@app/store';
|
import { StoreState } from 'store';
|
||||||
import useTheme from '@app/hooks/useTheme';
|
import useTheme from 'hooks/useTheme';
|
||||||
import { logIn } from '@app/store/user/actions';
|
import { logIn } from 'store/user/actions';
|
||||||
|
|
||||||
import { AnonymousLoginForm } from './__anonymous-login-form';
|
import { AnonymousLoginForm } from './__anonymous-login-form';
|
||||||
import { EmailLoginFormConnected, EmailLoginFormRef } from './__email-login-form';
|
import { EmailLoginFormConnected, EmailLoginFormRef } from './__email-login-form';
|
||||||
|
|
||||||
import styles from './auth.module.pcss';
|
import styles from './auth.module.css';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
intl: IntlShape;
|
intl: IntlShape;
|
||||||
@@ -186,13 +185,13 @@ const authPanelMessages = defineMessages({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function () {
|
export default function AuthWrapper() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const provider = useSelector<StoreState, ProviderState>(store => store.provider);
|
const provider = useSelector<StoreState, ProviderState>(store => store.provider);
|
||||||
const user = useSelector<StoreState, User | null>(store => store.user);
|
const user = useSelector<StoreState, User | null>(store => store.user);
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const handleSignin = useCallback((provider: AuthProvider) => dispatch(logIn(provider)), []);
|
const handleSignin = useCallback((provider: AuthProvider) => dispatch(logIn(provider)), [dispatch]);
|
||||||
|
|
||||||
return <Auth provider={provider} theme={theme} onSignIn={handleSignin} intl={intl} user={user} />;
|
return <Auth provider={provider} theme={theme} onSignIn={handleSignin} intl={intl} user={user} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
/** @jsx createElement */
|
import { h, JSX } from 'preact';
|
||||||
import { createElement, JSX } from 'preact';
|
|
||||||
import b from 'bem-react-helper';
|
import b from 'bem-react-helper';
|
||||||
import { Theme } from '@app/common/types';
|
import { Theme } from 'common/types';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
picture?: string;
|
picture?: string;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export { AvatarIcon } from './avatar-icon';
|
import './avatar-icon.css';
|
||||||
|
import './_default/avatar-icon_default.css';
|
||||||
|
|
||||||
import './avatar-icon.scss';
|
export { AvatarIcon } from './avatar-icon';
|
||||||
import './_default/avatar-icon_default.scss';
|
|
||||||
|
|||||||
@@ -1,26 +1,22 @@
|
|||||||
/** @jsx createElement */
|
import { h, JSX } from 'preact';
|
||||||
import { createElement, JSX } from 'preact';
|
|
||||||
import { forwardRef } from 'preact/compat';
|
import { forwardRef } from 'preact/compat';
|
||||||
import b, { Mods, Mix } from 'bem-react-helper';
|
import b, { Mods, Mix } from 'bem-react-helper';
|
||||||
import { Theme } from '@app/common/types';
|
|
||||||
|
|
||||||
interface Props extends Omit<JSX.HTMLAttributes, 'size' | 'className'> {
|
import type { Theme } from 'common/types';
|
||||||
|
|
||||||
|
export type ButtonProps = Omit<JSX.HTMLAttributes, 'size' | 'className'> & {
|
||||||
kind?: 'primary' | 'secondary' | 'link';
|
kind?: 'primary' | 'secondary' | 'link';
|
||||||
size?: 'middle' | 'large';
|
size?: 'middle' | 'large';
|
||||||
theme?: Theme;
|
theme?: Theme;
|
||||||
mods?: Mods;
|
mods?: Mods;
|
||||||
mix?: Mix;
|
mix?: Mix;
|
||||||
type?: string;
|
type?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const Button = forwardRef<HTMLButtonElement, Props>(
|
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
({ children, theme, mods, mix, kind, type = 'button', size, ...props }, ref) => {
|
({ children, theme, mods, mix, kind, type = 'button', size, ...props }, ref) => (
|
||||||
const className = b('button', { mods: { kind, size }, mix }, { theme, ...mods });
|
<button className={b('button', { mods: { kind, size, theme }, mix }, { ...mods })} type={type} {...props} ref={ref}>
|
||||||
|
{children}
|
||||||
return (
|
</button>
|
||||||
<button className={className} type={type} {...props} ref={ref}>
|
)
|
||||||
{children}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
|
import './button.css';
|
||||||
|
|
||||||
|
import './_kind/_link/button_kind_link.css';
|
||||||
|
import './_kind/_primary/button_kind_primary.css';
|
||||||
|
import './_kind/_secondary/button_kind_secondary.css';
|
||||||
|
|
||||||
|
import './_size/_large/button_size_large.css';
|
||||||
|
import './_size/_middle/button_size_middle.css';
|
||||||
|
|
||||||
|
import './_theme/_dark/button_theme_dark.css';
|
||||||
|
|
||||||
export { Button } from './button';
|
export { Button } from './button';
|
||||||
|
|
||||||
import './button.scss';
|
|
||||||
|
|
||||||
import './_kind/_link/button_kind_link.scss';
|
|
||||||
import './_kind/_primary/button_kind_primary.scss';
|
|
||||||
import './_kind/_secondary/button_kind_secondary.scss';
|
|
||||||
|
|
||||||
import './_size/_large/button_size_large.scss';
|
|
||||||
import './_size/_middle/button_size_middle.scss';
|
|
||||||
|
|
||||||
import './_theme/_dark/button_theme_dark.scss';
|
|
||||||
|
|||||||
+7
-10
@@ -1,20 +1,17 @@
|
|||||||
.comment-form__field {
|
.comment-form__field {
|
||||||
$lineHeight: 1.4;
|
/* font-size * line-height * lines * vertical-padding */
|
||||||
$fontSize: 16px;
|
--height: calc(16px * 1.4 * 4 + 10px * 2);
|
||||||
$paddingVrt: 10px;
|
|
||||||
$lines: 4;
|
|
||||||
$height: calc($fontSize * $lineHeight * $lines + $paddingVrt * 2);
|
|
||||||
|
|
||||||
display: block;
|
display: block;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: $height;
|
height: var(--height);
|
||||||
min-height: $height;
|
min-height: var(--height);
|
||||||
padding: $paddingVrt 12px;
|
padding: 10px 12px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: 'PT Sans', Helvetica, Arial, sans-serif;
|
font-family: 'PT Sans', Helvetica, Arial, sans-serif;
|
||||||
font-size: $fontSize;
|
font-size: 16px;
|
||||||
line-height: $lineHeight;
|
line-height: 1.4;
|
||||||
border: 0;
|
border: 0;
|
||||||
resize: none;
|
resize: none;
|
||||||
backface-visibility: hidden; /* let's try to fix blinking in Safari */
|
backface-visibility: hidden; /* let's try to fix blinking in Safari */
|
||||||
+44
-45
@@ -1,26 +1,35 @@
|
|||||||
/** @jsx createElement */
|
import { h } from 'preact';
|
||||||
import { createElement } from 'preact';
|
|
||||||
import { mount } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
import { act } from 'preact/test-utils';
|
import { act } from 'preact/test-utils';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { Middleware } from 'redux';
|
import { Middleware } from 'redux';
|
||||||
import createMockStore from 'redux-mock-store';
|
import createMockStore from 'redux-mock-store';
|
||||||
|
|
||||||
import '@app/testUtils/mockApi';
|
|
||||||
import { user, anonymousUser } from '@app/testUtils/mocks/user';
|
|
||||||
import { validToken } from '@app/testUtils/mocks/jwt';
|
|
||||||
|
|
||||||
import * as api from '@app/common/api';
|
|
||||||
import { sleep } from '@app/utils/sleep';
|
|
||||||
import { Input } from '@app/components/input';
|
|
||||||
import { Button } from '@app/components/button';
|
|
||||||
import { Dropdown } from '@app/components/dropdown';
|
|
||||||
import TextareaAutosize from '@app/components/comment-form/textarea-autosize';
|
|
||||||
import { IntlProvider } from 'react-intl';
|
import { IntlProvider } from 'react-intl';
|
||||||
import enMessages from '../../../locales/en.json';
|
|
||||||
|
|
||||||
import { SubscribeByEmail, SubscribeByEmailForm } from './';
|
jest.mock('common/api');
|
||||||
import { LS_EMAIL_KEY } from '@app/common/constants';
|
|
||||||
|
import { user, anonymousUser } from '__stubs__/user';
|
||||||
|
import { validToken } from '__stubs__/jwt';
|
||||||
|
import { emailVerificationForSubscribe, emailConfirmationForSubscribe, unsubscribeFromEmailUpdates } from 'common/api';
|
||||||
|
import { sleep } from 'utils/sleep';
|
||||||
|
import { Input } from 'components/input';
|
||||||
|
import { Button } from 'components/button';
|
||||||
|
import { Dropdown } from 'components/dropdown';
|
||||||
|
import TextareaAutosize from 'components/comment-form/textarea-autosize';
|
||||||
|
import enMessages from 'locales/en.json';
|
||||||
|
import { LS_EMAIL_KEY } from 'common/constants';
|
||||||
|
|
||||||
|
import { SubscribeByEmail, SubscribeByEmailForm } from '.';
|
||||||
|
|
||||||
|
const emailVerificationForSubscribeMock = (emailVerificationForSubscribe as unknown) as jest.Mock<
|
||||||
|
ReturnType<typeof emailVerificationForSubscribe>
|
||||||
|
>;
|
||||||
|
const emailConfirmationForSubscribeMock = (emailConfirmationForSubscribe as unknown) as jest.Mock<
|
||||||
|
ReturnType<typeof emailConfirmationForSubscribe>
|
||||||
|
>;
|
||||||
|
const unsubscribeFromEmailUpdatesMock = (unsubscribeFromEmailUpdates as unknown) as jest.Mock<
|
||||||
|
ReturnType<typeof unsubscribeFromEmailUpdates>
|
||||||
|
>;
|
||||||
|
|
||||||
const initialStore = {
|
const initialStore = {
|
||||||
user,
|
user,
|
||||||
@@ -36,7 +45,7 @@ const makeInputEvent = (value: string) => ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
jest.mock('@app/utils/jwt', () => ({
|
jest.mock('utils/jwt', () => ({
|
||||||
isJwtExpired: jest.fn(() => false),
|
isJwtExpired: jest.fn(() => false),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -94,49 +103,40 @@ describe('<SubscribeByEmailForm/>', () => {
|
|||||||
const wrapper = createWrapper(store);
|
const wrapper = createWrapper(store);
|
||||||
|
|
||||||
expect(wrapper.find('.comment-form__subscribe-by-email_subscribed')).toHaveLength(1);
|
expect(wrapper.find('.comment-form__subscribe-by-email_subscribed')).toHaveLength(1);
|
||||||
expect(wrapper.text()).toStartWith('You are subscribed on updates by email');
|
expect(wrapper.text().startsWith('You are subscribed on updates by email')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should pass throw subscribe process', async () => {
|
it('should pass throw subscribe process', async () => {
|
||||||
const wrapper = createWrapper();
|
const wrapper = createWrapper();
|
||||||
|
|
||||||
const emailVerificationForSubscribe = jest.spyOn(api, 'emailVerificationForSubscribe');
|
const input = wrapper.find('input');
|
||||||
const emailConfirmationForSubscribe = jest.spyOn(api, 'emailConfirmationForSubscribe');
|
|
||||||
const onInputEmail = wrapper.find(Input).prop('onInput');
|
|
||||||
const form = wrapper.find('form');
|
const form = wrapper.find('form');
|
||||||
|
|
||||||
expect(onInputEmail).toBeFunction();
|
input.getDOMNode<HTMLInputElement>().value = 'some@email.com';
|
||||||
|
input.simulate('input');
|
||||||
act(() => onInputEmail(makeInputEvent('some@email.com')));
|
|
||||||
|
|
||||||
expect(form).toHaveLength(1);
|
|
||||||
|
|
||||||
form.simulate('submit');
|
form.simulate('submit');
|
||||||
|
|
||||||
expect(emailVerificationForSubscribe).toHaveBeenCalledWith('some@email.com');
|
expect(emailVerificationForSubscribeMock).toHaveBeenCalledWith('some@email.com');
|
||||||
|
|
||||||
await sleep(0);
|
await sleep();
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
|
|
||||||
const textarea = wrapper.find(TextareaAutosize);
|
const textarea = wrapper.find('textarea');
|
||||||
const onInputToken = textarea.prop('onInput') as (e: any) => void;
|
const button = wrapper.find('button');
|
||||||
const button = wrapper.find(Button);
|
|
||||||
|
|
||||||
expect(textarea).toHaveLength(1);
|
|
||||||
expect(onInputToken).toBeFunction();
|
|
||||||
expect(button.at(0).text()).toEqual('Back');
|
expect(button.at(0).text()).toEqual('Back');
|
||||||
expect(button.at(1).text()).toEqual('Subscribe');
|
expect(button.at(1).text()).toEqual('Subscribe');
|
||||||
|
|
||||||
act(() => onInputToken(makeInputEvent('tokentokentoken')));
|
textarea.getDOMNode<HTMLTextAreaElement>().value = 'tokentokentoken';
|
||||||
|
textarea.simulate('input');
|
||||||
|
form.simulate('submit');
|
||||||
|
|
||||||
wrapper.find('form').simulate('submit');
|
expect(emailConfirmationForSubscribeMock).toHaveBeenCalledWith('tokentokentoken');
|
||||||
|
|
||||||
expect(emailConfirmationForSubscribe).toHaveBeenCalledWith('tokentokentoken');
|
|
||||||
|
|
||||||
await sleep(0);
|
await sleep(0);
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
|
|
||||||
expect(wrapper.text()).toStartWith('You have been subscribed on updates by email');
|
expect(wrapper.text().startsWith('You have been subscribed on updates by email')).toBe(true);
|
||||||
expect(wrapper.find(Button).text()).toEqual('Unsubscribe');
|
expect(wrapper.find(Button).text()).toEqual('Unsubscribe');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -152,7 +152,7 @@ describe('<SubscribeByEmailForm/>', () => {
|
|||||||
const onInputEmail = wrapper.find(Input).prop('onInput');
|
const onInputEmail = wrapper.find(Input).prop('onInput');
|
||||||
const form = wrapper.find('form');
|
const form = wrapper.find('form');
|
||||||
|
|
||||||
expect(onInputEmail).toBeFunction();
|
expect(typeof onInputEmail === 'function').toBe(true);
|
||||||
|
|
||||||
act(() => onInputEmail(makeInputEvent('some@email.com')));
|
act(() => onInputEmail(makeInputEvent('some@email.com')));
|
||||||
|
|
||||||
@@ -169,7 +169,7 @@ describe('<SubscribeByEmailForm/>', () => {
|
|||||||
await sleep(0);
|
await sleep(0);
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
|
|
||||||
expect(wrapper.text()).toStartWith('You have been subscribed on updates by email');
|
expect(wrapper.text().startsWith('You have been subscribed on updates by email')).toBe(true);
|
||||||
expect(wrapper.find(Button).text()).toEqual('Unsubscribe');
|
expect(wrapper.find(Button).text()).toEqual('Unsubscribe');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -177,18 +177,17 @@ describe('<SubscribeByEmailForm/>', () => {
|
|||||||
const store = mockStore({ ...initialStore, user: { email_subscription: true } });
|
const store = mockStore({ ...initialStore, user: { email_subscription: true } });
|
||||||
const wrapper = createWrapper(store);
|
const wrapper = createWrapper(store);
|
||||||
const onClick = wrapper.find(Button).prop('onClick');
|
const onClick = wrapper.find(Button).prop('onClick');
|
||||||
const unsubscribeFromEmailUpdates = jest.spyOn(api, 'unsubscribeFromEmailUpdates');
|
|
||||||
|
|
||||||
expect(onClick).toBeFunction();
|
expect(typeof onClick === 'function').toBe(true);
|
||||||
|
|
||||||
act(() => onClick());
|
act(() => onClick());
|
||||||
|
|
||||||
expect(unsubscribeFromEmailUpdates).toHaveBeenCalled();
|
expect(unsubscribeFromEmailUpdatesMock).toHaveBeenCalled();
|
||||||
|
|
||||||
await sleep(0);
|
await sleep(0);
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
|
|
||||||
expect(wrapper.text()).toStartWith('You have been unsubscribed by email to updates');
|
expect(wrapper.text().startsWith('You have been unsubscribed by email to updates')).toBe(true);
|
||||||
expect(wrapper.find(Button).text()).toEqual('Close');
|
expect(wrapper.find(Button).text()).toEqual('Close');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+37
-44
@@ -1,30 +1,25 @@
|
|||||||
/** @jsx createElement */
|
import { h, FunctionComponent, Fragment } from 'preact';
|
||||||
import { createElement, FunctionComponent, Fragment } from 'preact';
|
|
||||||
import { useState, useCallback, useEffect, useRef, PropRef } from 'preact/hooks';
|
import { useState, useCallback, useEffect, useRef, PropRef } from 'preact/hooks';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
import b from 'bem-react-helper';
|
import b from 'bem-react-helper';
|
||||||
|
|
||||||
import { User } from '@app/common/types';
|
|
||||||
import { StoreState } from '@app/store';
|
|
||||||
import { setUserSubscribed } from '@app/store/user/actions';
|
|
||||||
import { sleep } from '@app/utils/sleep';
|
|
||||||
import { extractErrorMessageFromResponse } from '@app/utils/errorUtils';
|
|
||||||
import useTheme from '@app/hooks/useTheme';
|
|
||||||
import { getHandleClickProps } from '@app/common/accessibility';
|
|
||||||
import {
|
|
||||||
emailVerificationForSubscribe,
|
|
||||||
emailConfirmationForSubscribe,
|
|
||||||
unsubscribeFromEmailUpdates,
|
|
||||||
} from '@app/common/api';
|
|
||||||
import { Input } from '@app/components/input';
|
|
||||||
import { Button } from '@app/components/button';
|
|
||||||
import { Dropdown } from '@app/components/dropdown';
|
|
||||||
import { Preloader } from '@app/components/preloader';
|
|
||||||
import TextareaAutosize from '@app/components/comment-form/textarea-autosize';
|
|
||||||
import { isUserAnonymous } from '@app/utils/isUserAnonymous';
|
|
||||||
import { isJwtExpired } from '@app/utils/jwt';
|
|
||||||
import { useIntl, defineMessages, IntlShape, FormattedMessage } from 'react-intl';
|
import { useIntl, defineMessages, IntlShape, FormattedMessage } from 'react-intl';
|
||||||
import { LS_EMAIL_KEY } from '@app/common/constants';
|
|
||||||
|
import { User } from 'common/types';
|
||||||
|
import { LS_EMAIL_KEY } from 'common/constants';
|
||||||
|
import { StoreState } from 'store';
|
||||||
|
import { setUserSubscribed } from 'store/user/actions';
|
||||||
|
import { sleep } from 'utils/sleep';
|
||||||
|
import { extractErrorMessageFromResponse } from 'utils/errorUtils';
|
||||||
|
import useTheme from 'hooks/useTheme';
|
||||||
|
import { getHandleClickProps } from 'common/accessibility';
|
||||||
|
import { emailVerificationForSubscribe, emailConfirmationForSubscribe, unsubscribeFromEmailUpdates } from 'common/api';
|
||||||
|
import { Input } from 'components/input';
|
||||||
|
import { Button } from 'components/button';
|
||||||
|
import { Dropdown } from 'components/dropdown';
|
||||||
|
import Preloader from 'components/preloader';
|
||||||
|
import TextareaAutosize from 'components/comment-form/textarea-autosize';
|
||||||
|
import { isUserAnonymous } from 'utils/isUserAnonymous';
|
||||||
|
import { isJwtExpired } from 'utils/jwt';
|
||||||
|
|
||||||
const emailRegex = /[^@]+@[^.]+\..+/;
|
const emailRegex = /[^@]+@[^.]+\..+/;
|
||||||
|
|
||||||
@@ -105,7 +100,7 @@ const renderTokenPart = (
|
|||||||
handleChangeToken: (e: Event) => void,
|
handleChangeToken: (e: Event) => void,
|
||||||
setEmailStep: () => void
|
setEmailStep: () => void
|
||||||
) => (
|
) => (
|
||||||
<Fragment>
|
<>
|
||||||
<Button kind="link" mix="auth-email-login-form__back-button" {...getHandleClickProps(setEmailStep)}>
|
<Button kind="link" mix="auth-email-login-form__back-button" {...getHandleClickProps(setEmailStep)}>
|
||||||
<FormattedMessage id="subscribeByEmail.back" defaultMessage="Back" />
|
<FormattedMessage id="subscribeByEmail.back" defaultMessage="Back" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -117,7 +112,7 @@ const renderTokenPart = (
|
|||||||
disabled={loading}
|
disabled={loading}
|
||||||
value={token}
|
value={token}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const SubscribeByEmailForm: FunctionComponent = () => {
|
export const SubscribeByEmailForm: FunctionComponent = () => {
|
||||||
@@ -165,7 +160,7 @@ export const SubscribeByEmailForm: FunctionComponent = () => {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[setLoading, setError, setStep, step, emailAddress, token]
|
[setLoading, setError, setStep, step, emailAddress, token, dispatch, intl]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleChangeEmail = useCallback((e: Event) => {
|
const handleChangeEmail = useCallback((e: Event) => {
|
||||||
@@ -193,7 +188,7 @@ export const SubscribeByEmailForm: FunctionComponent = () => {
|
|||||||
|
|
||||||
setToken(value);
|
setToken(value);
|
||||||
},
|
},
|
||||||
[sendForm, setError, setToken]
|
[sendForm, setError, setToken, intl]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
@@ -212,6 +207,20 @@ export const SubscribeByEmailForm: FunctionComponent = () => {
|
|||||||
setStep(Step.Email);
|
setStep(Step.Email);
|
||||||
}, [setStep]);
|
}, [setStep]);
|
||||||
|
|
||||||
|
const handleUnsubscribe = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await unsubscribeFromEmailUpdates();
|
||||||
|
dispatch(setUserSubscribed(false));
|
||||||
|
previousStep.current = Step.Subscribed;
|
||||||
|
setStep(Step.Unsubscribed);
|
||||||
|
} catch (e) {
|
||||||
|
setError(extractErrorMessageFromResponse(e, intl));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [setLoading, setStep, setError, dispatch, intl]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (emailAddressRef.current) {
|
if (emailAddressRef.current) {
|
||||||
emailAddressRef.current.focus();
|
emailAddressRef.current.focus();
|
||||||
@@ -227,20 +236,6 @@ export const SubscribeByEmailForm: FunctionComponent = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (step === Step.Subscribed) {
|
if (step === Step.Subscribed) {
|
||||||
const handleUnsubscribe = useCallback(async () => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
await unsubscribeFromEmailUpdates();
|
|
||||||
dispatch(setUserSubscribed(false));
|
|
||||||
previousStep.current = Step.Subscribed;
|
|
||||||
setStep(Step.Unsubscribed);
|
|
||||||
} catch (e) {
|
|
||||||
setError(extractErrorMessageFromResponse(e, intl));
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [setLoading, setStep, setError]);
|
|
||||||
|
|
||||||
const text =
|
const text =
|
||||||
previousStep.current === Step.Token
|
previousStep.current === Step.Token
|
||||||
? intl.formatMessage(messages.haveSubscribed)
|
? intl.formatMessage(messages.haveSubscribed)
|
||||||
@@ -318,9 +313,7 @@ export const SubscribeByEmail: FunctionComponent = () => {
|
|||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const user = useSelector<StoreState, User | null>(({ user }) => user);
|
const user = useSelector<StoreState, User | null>(({ user }) => user);
|
||||||
const isAnonymous = isUserAnonymous(user);
|
const isAnonymous = isUserAnonymous(user);
|
||||||
const buttonTitle = isAnonymous
|
const buttonTitle = intl.formatMessage(isAnonymous ? messages.onlyRegisteredUsers : messages.subscribeByEmail);
|
||||||
? intl.formatMessage(messages.onlyRegisteredUsers)
|
|
||||||
: intl.formatMessage(messages.subscribeByEmail);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
|
|||||||
+11
-15
@@ -1,37 +1,33 @@
|
|||||||
/** @jsx createElement */
|
import { h } from 'preact';
|
||||||
import { createElement } from 'preact';
|
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import enMessages from '../../../locales/en.json';
|
|
||||||
|
|
||||||
import { SubscribeByRSS, createSubscribeUrl } from './';
|
import { SubscribeByRSS, createSubscribeUrl } from '.';
|
||||||
|
|
||||||
jest.mock('react-redux', () => ({
|
jest.mock('react-redux', () => ({
|
||||||
useSelector: jest.fn(fn => fn({ theme: 'light' })),
|
useSelector: jest.fn(fn => fn({ theme: 'light' })),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('react-intl', () => {
|
jest.mock('react-intl', () => {
|
||||||
// Require the original module to not be mocked...
|
const reactIntl = jest.requireActual('react-intl');
|
||||||
const originalModule = jest.requireActual('react-intl');
|
const messages = require('locales/en.json');
|
||||||
|
const intlProvider = new reactIntl.IntlProvider({ locale: 'en', messages }, {});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...originalModule,
|
...reactIntl,
|
||||||
useIntl: () => originalModule.createIntl({ locale: `en`, messages: enMessages }),
|
useIntl: () => intlProvider.state.intl,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('<SubscribeByRSS/>', () => {
|
describe('<SubscribeByRSS/>', () => {
|
||||||
let wrapper: ReturnType<typeof shallow>;
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
wrapper = shallow(<SubscribeByRSS userId="user-1" />);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be render links in dropdown', () => {
|
it('should be render links in dropdown', () => {
|
||||||
wrapper.update();
|
const wrapper = shallow(<SubscribeByRSS userId="user-1" />);
|
||||||
|
|
||||||
expect(wrapper.find('.comment-form__rss-dropdown__link')).toHaveLength(3);
|
expect(wrapper.find('.comment-form__rss-dropdown__link')).toHaveLength(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have userId in replies link', () => {
|
it('should have userId in replies link', () => {
|
||||||
|
const wrapper = shallow(<SubscribeByRSS userId="user-1" />);
|
||||||
|
|
||||||
expect(wrapper.find('.comment-form__rss-dropdown__link').at(2).prop('href')).toBe(
|
expect(wrapper.find('.comment-form__rss-dropdown__link').at(2).prop('href')).toBe(
|
||||||
createSubscribeUrl('reply', '&user=user-1')
|
createSubscribeUrl('reply', '&user=user-1')
|
||||||
);
|
);
|
||||||
|
|||||||
+8
-9
@@ -1,14 +1,13 @@
|
|||||||
/** @jsx createElement */
|
import { h, FunctionComponent } from 'preact';
|
||||||
import { createElement, FunctionComponent } from 'preact';
|
|
||||||
import { useMemo } from 'preact/hooks';
|
import { useMemo } from 'preact/hooks';
|
||||||
import { useIntl, defineMessages } from 'react-intl';
|
import { useIntl, defineMessages } from 'react-intl';
|
||||||
|
|
||||||
import useTheme from '@app/hooks/useTheme';
|
import useTheme from 'hooks/useTheme';
|
||||||
import { siteId, url } from '@app/common/settings';
|
import { siteId, url } from 'common/settings';
|
||||||
import { BASE_URL, API_BASE } from '@app/common/constants';
|
import { BASE_URL, API_BASE } from 'common/constants';
|
||||||
import { Dropdown, DropdownItem } from '@app/components/dropdown';
|
import { Dropdown, DropdownItem } from 'components/dropdown';
|
||||||
|
|
||||||
export const createSubscribeUrl = (type: 'post' | 'site' | 'reply', urlParams: string = '') =>
|
export const createSubscribeUrl = (type: 'post' | 'site' | 'reply', urlParams = '') =>
|
||||||
`${BASE_URL}${API_BASE}/rss/${type}?site=${siteId}${urlParams}`;
|
`${BASE_URL}${API_BASE}/rss/${type}?site=${siteId}${urlParams}`;
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
@@ -43,7 +42,7 @@ export const SubscribeByRSS: FunctionComponent<{ userId: string | null }> = ({ u
|
|||||||
[createSubscribeUrl('site'), intl.formatMessage(messages.site)],
|
[createSubscribeUrl('site'), intl.formatMessage(messages.site)],
|
||||||
[createSubscribeUrl('reply', `&user=${userId}`), intl.formatMessage(messages.replies)],
|
[createSubscribeUrl('reply', `&user=${userId}`), intl.formatMessage(messages.replies)],
|
||||||
],
|
],
|
||||||
[userId]
|
[userId, intl]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -56,7 +55,7 @@ export const SubscribeByRSS: FunctionComponent<{ userId: string | null }> = ({ u
|
|||||||
>
|
>
|
||||||
{items.map(([href, label]) => (
|
{items.map(([href, label]) => (
|
||||||
<DropdownItem>
|
<DropdownItem>
|
||||||
<a href={href} className="comment-form__rss-dropdown__link" target="_blank">
|
<a href={href} className="comment-form__rss-dropdown__link" target="_blank" rel="noreferrer">
|
||||||
{label}
|
{label}
|
||||||
</a>
|
</a>
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
|||||||
+9
-9
@@ -2,41 +2,41 @@
|
|||||||
border-color: var(--color7);
|
border-color: var(--color7);
|
||||||
background: var(--color8); /* try to fix textarea blinking in Safari */
|
background: var(--color8); /* try to fix textarea blinking in Safari */
|
||||||
|
|
||||||
.comment-form__actions {
|
& .comment-form__actions {
|
||||||
background: var(--color7);
|
background: var(--color7);
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-form__button_type_preview {
|
& .comment-form__button_type_preview {
|
||||||
background: var(--color8);
|
background: var(--color8);
|
||||||
color: var(--color20);
|
color: var(--color20);
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-form__button_type_send {
|
& .comment-form__button_type_send {
|
||||||
color: var(--color20);
|
color: var(--color20);
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-form__error {
|
& .comment-form__error {
|
||||||
border-top: 8px solid var(--color7);
|
border-top: 8px solid var(--color7);
|
||||||
background: var(--color28);
|
background: var(--color28);
|
||||||
color: var(--color27);
|
color: var(--color27);
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-form__field {
|
& .comment-form__field {
|
||||||
background: var(--color8);
|
background: var(--color8);
|
||||||
color: var(--color5);
|
color: var(--color5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-form__preview {
|
& .comment-form__preview {
|
||||||
border-color: var(--color8);
|
border-color: var(--color8);
|
||||||
background: var(--color8);
|
background: var(--color8);
|
||||||
color: var(--color20);
|
color: var(--color20);
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-form__preview-wrapper {
|
& .comment-form__preview-wrapper {
|
||||||
background: var(--color7);
|
background: var(--color7);
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-form__toolbar-item {
|
& .comment-form__toolbar-item {
|
||||||
color: var(--color20);
|
color: var(--color20);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-form__control-panel {
|
& .comment-form__control-panel {
|
||||||
background-color: var(--color7);
|
background-color: var(--color7);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user