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:
1 parent
919fb7af62
commit
5825a55b69
325 files changed
+10488
-18795
No files matched your search
@@ -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
|
||||
// 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>!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:
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
#!/bin/sh
|
||||
echo "prepare environment"
|
||||
# replace BASE_URL constant by REMARK_URL
|
||||
sed -i "s|https://demo.remark42.com|${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/*.html
|
||||
sed -i "s|{% REMARK_URL %}|${REMARK_URL}|g" /srv/web/*.js
|
||||
|
||||
if [ -n "${SITE_ID}" ]; then
|
||||
#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 = {
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{
|
||||
bugfixes: true,
|
||||
loose: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'@babel/preset-react',
|
||||
{
|
||||
pragma: 'h',
|
||||
pragmaFrag: 'Fragment',
|
||||
},
|
||||
],
|
||||
getPresetEnv({
|
||||
targets: 'defaults, not IE 11, not samsung 12',
|
||||
useBuiltIns: 'usage',
|
||||
corejs: 3,
|
||||
bugfixes: true,
|
||||
loose: true,
|
||||
}),
|
||||
preactPreset,
|
||||
],
|
||||
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 = {
|
||||
parser: 'babel-eslint',
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'react-app',
|
||||
'preact',
|
||||
'plugin:jsx-a11y/recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
'prettier',
|
||||
'prettier/@typescript-eslint',
|
||||
'prettier/babel',
|
||||
'prettier/prettier',
|
||||
'prettier/react',
|
||||
],
|
||||
plugins: ['react', 'jsx-a11y', 'prettier'],
|
||||
plugins: ['jsx-a11y', 'prettier'],
|
||||
rules: {
|
||||
'prettier/prettier': 'error',
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.ts', '*.tsx'],
|
||||
plugins: ['@typescript-eslint'],
|
||||
files: ['*.ts?(x)'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: './tsconfig.json',
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
rules: {
|
||||
// needs for using optional chaining
|
||||
'no-undef': 0,
|
||||
'jsx-a11y/no-autofocus': 0,
|
||||
// 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,
|
||||
'no-undef': 'off',
|
||||
'no-redeclare': 'off',
|
||||
'no-unused-vars': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.d.ts'],
|
||||
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: {
|
||||
'@typescript-eslint/no-explicit-any': 0,
|
||||
'@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 }],
|
||||
'import/first': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
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
|
||||
/public
|
||||
/*.log
|
||||
.env
|
||||
tsconfig.tsbuildinfo
|
||||
@@ -1,5 +1,5 @@
|
||||
module.exports = {
|
||||
'./**/*.{ts,tsx,js,jsx}': ['eslint --fix --max-warnings=0', 'prettier --write'],
|
||||
'./**/*.{scss,pcss,css}': ['prettier --write', 'stylelint'],
|
||||
'./iframe.html': ['prettier --write', 'stylelint'],
|
||||
'./**/*.css': ['prettier --write', 'stylelint'],
|
||||
'./templates/**.html': ['prettier --write', 'stylelint'],
|
||||
};
|
||||
@@ -1,2 +1,4 @@
|
||||
node_modules
|
||||
public
|
||||
package.json
|
||||
package-lock.json
|
||||
@@ -1,6 +1,7 @@
|
||||
module.exports = {
|
||||
printWidth: 120,
|
||||
useTabs: false,
|
||||
tabWidth: 2,
|
||||
semi: true,
|
||||
singleQuote: true,
|
||||
trailingComma: 'es5',
|
||||
@@ -8,7 +9,7 @@ module.exports = {
|
||||
arrowParens: 'avoid',
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.ejs', '*.html'],
|
||||
files: ['*.html'],
|
||||
options: {
|
||||
trailingComma: 'none',
|
||||
},
|
||||
|
||||
+26
-6
@@ -1,22 +1,42 @@
|
||||
module.exports = [
|
||||
{
|
||||
path: 'public/embed.js',
|
||||
limit: '2.7 KB',
|
||||
path: 'public/embed.mjs',
|
||||
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',
|
||||
},
|
||||
{
|
||||
limit: '44 KB',
|
||||
limit: '31 KB',
|
||||
path: 'public/last-comments.mjs',
|
||||
},
|
||||
{
|
||||
limit: '38 KB',
|
||||
path: 'public/last-comments.js',
|
||||
},
|
||||
{
|
||||
path: 'public/deleteme.mjs',
|
||||
limit: '11 KB',
|
||||
},
|
||||
{
|
||||
path: 'public/deleteme.js',
|
||||
limit: '35 KB',
|
||||
limit: '18 KB',
|
||||
},
|
||||
{
|
||||
path: 'public/counter.mjs',
|
||||
limit: '0.7 KB',
|
||||
},
|
||||
{
|
||||
path: 'public/counter.js',
|
||||
limit: '1.05 KB',
|
||||
limit: '0.7 KB',
|
||||
},
|
||||
];
|
||||
@@ -1,2 +1,3 @@
|
||||
node_modules
|
||||
public
|
||||
extracted-messages
|
||||
@@ -1,4 +1,5 @@
|
||||
const path = require('path');
|
||||
const { CUSTOM_PROPERTIES_PATH } = require('./webpack.config');
|
||||
|
||||
module.exports = {
|
||||
extends: ['stylelint-config-standard', 'stylelint-config-prettier'],
|
||||
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': [
|
||||
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.
|
||||
|
||||
### CSS Styles
|
||||
|
||||
- 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`
|
||||
- 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`
|
||||
- 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
|
||||
|
||||
@@ -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();
|
||||
|
||||
append(key: string, value: string) {
|
||||
@@ -16,7 +16,6 @@ global.Headers = class HeadersMock implements Headers {
|
||||
delete(key: string) {
|
||||
this.headers.delete(key);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
forEach(callbackfn: (value: string, key: string, parent: Headers) => void, thisArg?: any) {
|
||||
this.headers.forEach((value, key) => {
|
||||
callbackfn.call(thisArg || this, value, key, this);
|
||||
File renamed without changes.
@@ -1,15 +1,4 @@
|
||||
import 'jest-extended';
|
||||
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);
|
||||
import { StaticStore } from 'common/static-store';
|
||||
|
||||
beforeEach(() => {
|
||||
StaticStore.config = {
|
||||
@@ -2,4 +2,6 @@ import createMockStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
// 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> = {
|
||||
id: 'email_1',
|
||||
@@ -1,6 +1,6 @@
|
||||
// @ts-ignore
|
||||
const mock: typeof import('@app/common/constants') = {
|
||||
...jest.requireActual('@app/common/constants'),
|
||||
const mock: typeof import('common/constants') = {
|
||||
...jest.requireActual('common/constants'),
|
||||
BASE_URL: 'https://demo.remark42.com/',
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @ts-ignore
|
||||
const mock: typeof import('@app/common/settings') = {
|
||||
...jest.requireActual('@app/common/settings'),
|
||||
const mock: typeof import('common/settings') = {
|
||||
...jest.requireActual('common/settings'),
|
||||
siteId: 'remark',
|
||||
pageTitle: 'remark test',
|
||||
url: 'https://remark42.com/test',
|
||||
|
||||
@@ -2,12 +2,12 @@ import jestFetchMock from 'jest-fetch-mock';
|
||||
|
||||
import { emailVerificationForSubscribe } from './api';
|
||||
|
||||
jest.mock('@app/common/constants', () => ({
|
||||
jest.mock('common/constants', () => ({
|
||||
BASE_URL: 'https://example.com',
|
||||
API_BASE: '/api',
|
||||
}));
|
||||
|
||||
jest.mock('@app/common/settings', () => ({
|
||||
jest.mock('common/settings', () => ({
|
||||
siteId: 'remark42',
|
||||
}));
|
||||
|
||||
@@ -29,11 +29,11 @@ describe('api', () => {
|
||||
expect(jestFetchMock.mock.calls.length).toEqual(1);
|
||||
|
||||
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((match as string[]).length).toBeGreaterThan(1);
|
||||
expect((match as string[])[1]).toBe(
|
||||
expect(Array.isArray(match)).toBe(true);
|
||||
expect(match.length).toBeGreaterThan(1);
|
||||
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"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ import fetcher from './fetcher';
|
||||
/* common */
|
||||
const __loginAnonymously = (username: string): Promise<User | null> => {
|
||||
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: '' });
|
||||
};
|
||||
@@ -35,7 +35,7 @@ export const logIn = (provider: AuthProvider): Promise<User | null> => {
|
||||
|
||||
return new Promise<User | null>((resolve, reject) => {
|
||||
const url = `${BASE_URL}/auth/${provider.name}/login?from=${encodeURIComponent(
|
||||
location.origin + location.pathname + '?selfClose'
|
||||
`${window.location.origin}${window.location.pathname}?selfClose`
|
||||
)}&site=${siteId}`;
|
||||
|
||||
const newWindow = window.open(url);
|
||||
@@ -267,7 +267,7 @@ export const uploadImage = (image: File): Promise<Image> => {
|
||||
name: image.name,
|
||||
size: image.size,
|
||||
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
|
||||
export default (input: string): boolean => {
|
||||
export default function copy(input: string): boolean {
|
||||
const el = document.createElement('div');
|
||||
|
||||
el.innerHTML = input;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(el.style as any).contain = 'strict';
|
||||
el.style.position = 'absolute';
|
||||
el.style.left = '-9999px';
|
||||
el.style.fontSize = '12pt'; // Prevent zooming on iOS
|
||||
Object.assign(el.style, {
|
||||
contain: 'strict',
|
||||
position: 'absolute',
|
||||
left: '-9999px',
|
||||
fontSize: '12pt', // Prevent zooming on iOS
|
||||
});
|
||||
|
||||
document.body.appendChild(el);
|
||||
|
||||
@@ -60,4 +59,4 @@ export default (input: string): boolean => {
|
||||
}
|
||||
|
||||
return success;
|
||||
};
|
||||
}
|
||||
@@ -8,9 +8,9 @@ describe('fetcher', () => {
|
||||
error: 'you just cant',
|
||||
details: 'you just cant at all',
|
||||
};
|
||||
(window.fetch as any) = jest.fn().mockImplementation(async () => ({
|
||||
window.fetch = jest.fn().mockImplementation(async () => ({
|
||||
status: 400,
|
||||
headers: new (window as any).Headers(),
|
||||
headers: new Headers(),
|
||||
json: async () => response,
|
||||
text: async () => JSON.stringify(response),
|
||||
}));
|
||||
@@ -18,7 +18,7 @@ describe('fetcher', () => {
|
||||
return fetcher
|
||||
.get('/api/some')
|
||||
.then(data => {
|
||||
fail(data);
|
||||
throw new Error('Request should be failed');
|
||||
})
|
||||
.catch(e => {
|
||||
expect(e.code).toBe(2);
|
||||
@@ -40,7 +40,7 @@ describe('fetcher', () => {
|
||||
return fetcher
|
||||
.get('/api/some')
|
||||
.then(data => {
|
||||
fail(data);
|
||||
throw new Error('Request should be failed');
|
||||
})
|
||||
.catch(e => {
|
||||
expect(e.code).toBe(401);
|
||||
@@ -51,16 +51,18 @@ describe('fetcher', () => {
|
||||
(jest.spyOn(window, 'fetch') as any).mockImplementation(async () => ({
|
||||
status: 400,
|
||||
headers: new (window as any).Headers(),
|
||||
json: async () => {
|
||||
async json() {
|
||||
throw new Error('json parse error');
|
||||
},
|
||||
text: async () => 'you given me something wrong',
|
||||
async text() {
|
||||
return 'you given me something wrong';
|
||||
},
|
||||
}));
|
||||
|
||||
return fetcher
|
||||
.get({ url: '/api/some', logError: false })
|
||||
.then(data => {
|
||||
fail(data);
|
||||
throw new Error('Request should be failed');
|
||||
})
|
||||
.catch(e => {
|
||||
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 { siteId } from './settings';
|
||||
import { StaticStore } from './static_store';
|
||||
import { StaticStore } from './static-store';
|
||||
import { getCookie } from './cookies';
|
||||
import { httpErrorMap, isFailedFetch, httpMessages } from '@app/utils/errorUtils';
|
||||
|
||||
export type 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;
|
||||
|
||||
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 {
|
||||
url,
|
||||
body = undefined,
|
||||
@@ -77,7 +78,7 @@ const fetcher = methods.reduce<Partial<FetcherObject>>((acc, method) => {
|
||||
}
|
||||
|
||||
if (siteId && method !== 'post' && !rurl.includes('?site=') && !rurl.includes('&site=')) {
|
||||
rurl += (rurl.includes('?') ? '&' : '?') + `site=${siteId}`;
|
||||
rurl += `${rurl.includes('?') ? '&' : '?'}site=${siteId}`;
|
||||
}
|
||||
|
||||
return fetch(rurl, parameters)
|
||||
@@ -99,10 +100,8 @@ const fetcher = methods.reduce<Partial<FetcherObject>>((acc, method) => {
|
||||
if (res.status >= 400) {
|
||||
if (httpErrorMap.has(res.status)) {
|
||||
const descriptor = httpErrorMap.get(res.status) || httpMessages.unexpectedError;
|
||||
throw {
|
||||
code: res.status,
|
||||
error: descriptor.defaultMessage,
|
||||
};
|
||||
|
||||
throw new RequestError(descriptor.defaultMessage, res.status);
|
||||
}
|
||||
return res.text().then(text => {
|
||||
let err;
|
||||
@@ -110,13 +109,10 @@ const fetcher = methods.reduce<Partial<FetcherObject>>((acc, method) => {
|
||||
err = JSON.parse(text);
|
||||
} catch (e) {
|
||||
if (logError) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
}
|
||||
throw {
|
||||
code: 0,
|
||||
error: httpMessages.unexpectedError.defaultMessage,
|
||||
};
|
||||
|
||||
throw new RequestError(httpMessages.unexpectedError.defaultMessage, 0);
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
@@ -130,11 +126,9 @@ const fetcher = methods.reduce<Partial<FetcherObject>>((acc, method) => {
|
||||
})
|
||||
.catch(e => {
|
||||
if (isFailedFetch(e)) {
|
||||
throw {
|
||||
code: -2,
|
||||
error: e.message,
|
||||
};
|
||||
throw new RequestError(e.message, -2);
|
||||
}
|
||||
|
||||
throw e;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -67,7 +67,7 @@ describe('updateJsonItem', () => {
|
||||
});
|
||||
|
||||
it('should set data to empty localStorage', () => {
|
||||
updateJsonItem<Record<string, string>>(LS_KEY, {});
|
||||
updateJsonItem(LS_KEY, {});
|
||||
|
||||
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';
|
||||
|
||||
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 = any[]>(key: string, value: T): void;
|
||||
export function updateJsonItem<T = Record<string, any>>(key: string, value: T) {
|
||||
export function updateJsonItem<T extends {}>(key: string, value: (data: T) => T): void;
|
||||
export function updateJsonItem<T extends {}>(key: string, value: T): void;
|
||||
export function updateJsonItem<T = Record<string, unknown>>(key: string, value: T) {
|
||||
const savedData = getJsonItem<any>(key);
|
||||
|
||||
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 parseQuery from '@app/utils/parseQuery';
|
||||
|
||||
export interface QuerySettingsType {
|
||||
site_id?: string;
|
||||
@@ -29,9 +30,9 @@ if (!querySettings.theme || THEMES.indexOf(querySettings.theme) === -1) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
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 url = querySettings.url;
|
||||
export const maxShownComments = querySettings.max_shown_comments;
|
||||
export const token = querySettings.token;
|
||||
export const token = querySettings.token!;
|
||||
export const theme = querySettings.theme;
|
||||
File renamed without changes.
@@ -118,13 +118,6 @@ export interface Config {
|
||||
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 AuthProvider =
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { UserInfo } from './types';
|
||||
import parseQuery from '@app/utils/parseQuery';
|
||||
import parseQuery from '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
|
||||
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;
|
||||
}
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+1
-1
@@ -1,7 +1,7 @@
|
||||
.auth-panel_logged-in {
|
||||
font-size: 12px;
|
||||
|
||||
.auth-panel__column {
|
||||
& .auth-panel__column {
|
||||
&:first-child {
|
||||
font-weight: 400;
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
.auth-panel_theme_dark {
|
||||
.auth-panel__user-id {
|
||||
& .auth-panel__user-id {
|
||||
color: var(--color5);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
.auth-panel_theme_light {
|
||||
.auth-panel__user-id {
|
||||
& .auth-panel__user-id {
|
||||
color: var(--color13);
|
||||
}
|
||||
}
|
||||
File renamed without changes.
@@ -1,16 +1,15 @@
|
||||
/** @jsx createElement */
|
||||
import { createElement } from 'preact';
|
||||
import { h } from 'preact';
|
||||
import { mount } from 'enzyme';
|
||||
import createMockStore from 'redux-mock-store';
|
||||
import { Middleware } from 'redux';
|
||||
import { Provider } from 'react-redux';
|
||||
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 { Button } from '../button';
|
||||
import { StaticStore } from '@app/common/static_store';
|
||||
import { StaticStore } from 'common/static-store';
|
||||
|
||||
const DefaultProps = {
|
||||
providers: ['google', 'github'],
|
||||
@@ -83,7 +82,7 @@ describe('<AuthPanel />', () => {
|
||||
const firstCol = element.find('.auth-panel__column').first();
|
||||
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(1).text()).toBe('GitHub');
|
||||
});
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
/** @jsx createElement */
|
||||
import { createElement, Component, Fragment } from 'preact';
|
||||
import { h, Component, Fragment } from 'preact';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { FormattedMessage, defineMessages, IntlShape, useIntl } from 'react-intl';
|
||||
import b from 'bem-react-helper';
|
||||
|
||||
import { User, AuthProvider, Sorting, Theme, PostInfo } from '@app/common/types';
|
||||
import { IS_STORAGE_AVAILABLE, IS_THIRD_PARTY } from '@app/common/constants';
|
||||
import { requestDeletion } from '@app/utils/email';
|
||||
import postMessage from '@app/utils/postMessage';
|
||||
import { getHandleClickProps } from '@app/common/accessibility';
|
||||
import { StoreState } from '@app/store';
|
||||
import { ProviderState } from '@app/store/provider/reducers';
|
||||
import { Dropdown, DropdownItem } from '@app/components/dropdown';
|
||||
import { Button } from '@app/components/button';
|
||||
import Auth from '@app/components/auth';
|
||||
import { User, AuthProvider, Sorting, Theme, PostInfo } from 'common/types';
|
||||
import { IS_STORAGE_AVAILABLE, IS_THIRD_PARTY } from 'common/constants';
|
||||
import { requestDeletion } from 'utils/email';
|
||||
import postMessage from 'utils/postMessage';
|
||||
import { getHandleClickProps } from 'common/accessibility';
|
||||
import { StoreState } from 'store';
|
||||
import { ProviderState } from 'store/provider/reducers';
|
||||
import { Dropdown, DropdownItem } from 'components/dropdown';
|
||||
import { Button } from 'components/button';
|
||||
import Auth from 'components/auth';
|
||||
|
||||
import useTheme from '@app/hooks/useTheme';
|
||||
import { StaticStore } from '@app/common/static_store';
|
||||
import useTheme from 'hooks/useTheme';
|
||||
import { StaticStore } from 'common/static-store';
|
||||
|
||||
export interface OwnProps {
|
||||
user: User | null;
|
||||
@@ -132,6 +131,7 @@ export class AuthPanel extends Component<Props, State> {
|
||||
className="auth-panel__pseudo-link"
|
||||
href={`${window.location.origin}/web/comments.html${window.location.search}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<FormattedMessage id="authPanel.new-page" defaultMessage="new page" />
|
||||
</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';
|
||||
|
||||
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';
|
||||
File renamed without changes.
@@ -1,11 +1,10 @@
|
||||
/** @jsx createElement */
|
||||
import { createElement, Component, createRef } from 'preact';
|
||||
import { h, Component, createRef } from 'preact';
|
||||
import b from 'bem-react-helper';
|
||||
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 { Button } from '@app/components/button';
|
||||
import { Input } from 'components/input';
|
||||
import { Button } from 'components/button';
|
||||
|
||||
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
|
||||
let className = b('auth-anonymous-login-form', {}, { theme: props.theme });
|
||||
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();
|
||||
|
||||
@@ -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';
|
||||
File renamed without changes.
@@ -1,16 +1,16 @@
|
||||
/** @jsx createElement */
|
||||
import { createElement } from 'preact';
|
||||
import { h } from 'preact';
|
||||
import { mount, ReactWrapper } from 'enzyme';
|
||||
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 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
|
||||
.fn()
|
||||
.mockImplementationOnce(() => true)
|
||||
@@ -18,7 +18,7 @@ jest.mock('@app/utils/jwt', () => ({
|
||||
.mockImplementationOnce(() => true),
|
||||
}));
|
||||
|
||||
jest.mock('@app/common/api');
|
||||
jest.mock('common/api');
|
||||
|
||||
function simulateInput(input: ReactWrapper, value: string) {
|
||||
input.getDOMNode<HTMLTextAreaElement>().value = value;
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
/** @jsx createElement */
|
||||
import { createElement, Component, createRef } from 'preact';
|
||||
import { h, Component, createRef } from 'preact';
|
||||
import { forwardRef } from 'preact/compat';
|
||||
import b from 'bem-react-helper';
|
||||
import { Theme, User } from '@app/common/types';
|
||||
import { sendEmailVerificationRequest } from '@app/common/api';
|
||||
import { extractErrorMessageFromResponse } from '@app/utils/errorUtils';
|
||||
import { getHandleClickProps } from '@app/common/accessibility';
|
||||
import { sleep } from '@app/utils/sleep';
|
||||
import TextareaAutosize from '@app/components/comment-form/textarea-autosize';
|
||||
import { Input } from '@app/components/input';
|
||||
import { Button } from '@app/components/button';
|
||||
import { isJwtExpired } from '@app/utils/jwt';
|
||||
import { Theme, User } from 'common/types';
|
||||
import { LS_EMAIL_KEY } from 'common/constants';
|
||||
import { sendEmailVerificationRequest } from 'common/api';
|
||||
import { extractErrorMessageFromResponse } from 'utils/errorUtils';
|
||||
import { getHandleClickProps } from 'common/accessibility';
|
||||
import { sleep } from 'utils/sleep';
|
||||
import TextareaAutosize from 'components/comment-form/textarea-autosize';
|
||||
import { Input } from 'components/input';
|
||||
import { Button } from 'components/button';
|
||||
import { isJwtExpired } from 'utils/jwt';
|
||||
import { defineMessages, IntlShape, useIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { validateUserName } from '../validateUserName';
|
||||
|
||||
import { messages as loginForm } from '../__anonymous-login-form/auth__anonymous-login-form';
|
||||
import { LS_EMAIL_KEY } from '@app/common/constants';
|
||||
|
||||
interface OwnProps {
|
||||
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
|
||||
let className = b('auth-email-login-form', {}, { theme: props.theme });
|
||||
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();
|
||||
@@ -208,7 +206,7 @@ export class EmailLoginForm extends Component<Props, State> {
|
||||
return (
|
||||
<form className={className} onSubmit={this.onVerificationSubmit}>
|
||||
<Input
|
||||
autoFocus
|
||||
autofocus
|
||||
name="username"
|
||||
mix="auth-email-login-form__input"
|
||||
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';
|
||||
File renamed without changes.
@@ -1,16 +1,15 @@
|
||||
/** @jsx createElement */
|
||||
import { createElement } from 'preact';
|
||||
import { h } from 'preact';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import enMessages from '@app/locales/en.json';
|
||||
import { mockStore } from '@app/testUtils/mockStore';
|
||||
import { StaticStore } from '@app/common/static_store';
|
||||
import enMessages from 'locales/en.json';
|
||||
import stubStore from '__stubs__/store';
|
||||
import { StaticStore } from 'common/static-store';
|
||||
|
||||
import Auth from './auth';
|
||||
import { mount } from 'enzyme';
|
||||
import { Button } from '../button';
|
||||
import { StoreState } from '@app/store';
|
||||
import { StoreState } from 'store';
|
||||
|
||||
const initialStore = {
|
||||
provider: { name: 'google' },
|
||||
@@ -24,7 +23,7 @@ describe('<Auth/>', () => {
|
||||
const createWrapper = (store?: Partial<StoreState>) =>
|
||||
mount(
|
||||
<IntlProvider locale="en" messages={enMessages}>
|
||||
<Provider store={mockStore(store || initialStore)}>
|
||||
<Provider store={stubStore(store || initialStore)}>
|
||||
<Auth />
|
||||
</Provider>
|
||||
</IntlProvider>
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
/** @jsx createElement */
|
||||
import { createElement, Component, createRef } from 'preact';
|
||||
import { h, Component, createRef } from 'preact';
|
||||
import { useCallback } from 'preact/hooks';
|
||||
import { IntlShape, FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { AuthProvider, Theme, User } from '@app/common/types';
|
||||
import { PROVIDER_NAMES, IS_STORAGE_AVAILABLE } from '@app/common/constants';
|
||||
import { getHandleClickProps } from '@app/common/accessibility';
|
||||
import { Button } from '@app/components/button';
|
||||
import { Dropdown, DropdownItem } from '@app/components/dropdown';
|
||||
import { AuthProvider, Theme, User } from 'common/types';
|
||||
import { PROVIDER_NAMES, IS_STORAGE_AVAILABLE } from 'common/constants';
|
||||
import { getHandleClickProps } from 'common/accessibility';
|
||||
import { Button } from 'components/button';
|
||||
import { Dropdown, DropdownItem } from 'components/dropdown';
|
||||
|
||||
import debounce from '@app/utils/debounce';
|
||||
import { ProviderState } from '@app/store/provider/reducers';
|
||||
import { StaticStore } from '@app/common/static_store';
|
||||
import debounce from 'utils/debounce';
|
||||
import { ProviderState } from 'store/provider/reducers';
|
||||
import { StaticStore } from 'common/static-store';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { StoreState } from '@app/store';
|
||||
import useTheme from '@app/hooks/useTheme';
|
||||
import { logIn } from '@app/store/user/actions';
|
||||
import { StoreState } from 'store';
|
||||
import useTheme from 'hooks/useTheme';
|
||||
import { logIn } from 'store/user/actions';
|
||||
|
||||
import { AnonymousLoginForm } from './__anonymous-login-form';
|
||||
import { EmailLoginFormConnected, EmailLoginFormRef } from './__email-login-form';
|
||||
|
||||
import styles from './auth.module.pcss';
|
||||
import styles from './auth.module.css';
|
||||
|
||||
interface Props {
|
||||
intl: IntlShape;
|
||||
@@ -186,13 +185,13 @@ const authPanelMessages = defineMessages({
|
||||
},
|
||||
});
|
||||
|
||||
export default function () {
|
||||
export default function AuthWrapper() {
|
||||
const dispatch = useDispatch();
|
||||
const provider = useSelector<StoreState, ProviderState>(store => store.provider);
|
||||
const user = useSelector<StoreState, User | null>(store => store.user);
|
||||
const theme = useTheme();
|
||||
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} />;
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
@@ -1,7 +1,6 @@
|
||||
/** @jsx createElement */
|
||||
import { createElement, JSX } from 'preact';
|
||||
import { h, JSX } from 'preact';
|
||||
import b from 'bem-react-helper';
|
||||
import { Theme } from '@app/common/types';
|
||||
import { Theme } from 'common/types';
|
||||
|
||||
interface Props {
|
||||
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';
|
||||
import './_default/avatar-icon_default.scss';
|
||||
export { AvatarIcon } from './avatar-icon';
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,26 +1,22 @@
|
||||
/** @jsx createElement */
|
||||
import { createElement, JSX } from 'preact';
|
||||
import { h, JSX } from 'preact';
|
||||
import { forwardRef } from 'preact/compat';
|
||||
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';
|
||||
size?: 'middle' | 'large';
|
||||
theme?: Theme;
|
||||
mods?: Mods;
|
||||
mix?: Mix;
|
||||
type?: string;
|
||||
}
|
||||
};
|
||||
|
||||
export const Button = forwardRef<HTMLButtonElement, Props>(
|
||||
({ children, theme, mods, mix, kind, type = 'button', size, ...props }, ref) => {
|
||||
const className = b('button', { mods: { kind, size }, mix }, { theme, ...mods });
|
||||
|
||||
return (
|
||||
<button className={className} type={type} {...props} ref={ref}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ children, theme, mods, mix, kind, type = 'button', size, ...props }, ref) => (
|
||||
<button className={b('button', { mods: { kind, size, theme }, mix }, { ...mods })} 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';
|
||||
|
||||
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';
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+7
-10
@@ -1,20 +1,17 @@
|
||||
.comment-form__field {
|
||||
$lineHeight: 1.4;
|
||||
$fontSize: 16px;
|
||||
$paddingVrt: 10px;
|
||||
$lines: 4;
|
||||
$height: calc($fontSize * $lineHeight * $lines + $paddingVrt * 2);
|
||||
/* font-size * line-height * lines * vertical-padding */
|
||||
--height: calc(16px * 1.4 * 4 + 10px * 2);
|
||||
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: $height;
|
||||
min-height: $height;
|
||||
padding: $paddingVrt 12px;
|
||||
height: var(--height);
|
||||
min-height: var(--height);
|
||||
padding: 10px 12px;
|
||||
margin: 0;
|
||||
font-family: 'PT Sans', Helvetica, Arial, sans-serif;
|
||||
font-size: $fontSize;
|
||||
line-height: $lineHeight;
|
||||
font-size: 16px;
|
||||
line-height: 1.4;
|
||||
border: 0;
|
||||
resize: none;
|
||||
backface-visibility: hidden; /* let's try to fix blinking in Safari */
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+44
-45
@@ -1,26 +1,35 @@
|
||||
/** @jsx createElement */
|
||||
import { createElement } from 'preact';
|
||||
import { h } from 'preact';
|
||||
import { mount } from 'enzyme';
|
||||
import { act } from 'preact/test-utils';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Middleware } from 'redux';
|
||||
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 enMessages from '../../../locales/en.json';
|
||||
|
||||
import { SubscribeByEmail, SubscribeByEmailForm } from './';
|
||||
import { LS_EMAIL_KEY } from '@app/common/constants';
|
||||
jest.mock('common/api');
|
||||
|
||||
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 = {
|
||||
user,
|
||||
@@ -36,7 +45,7 @@ const makeInputEvent = (value: string) => ({
|
||||
},
|
||||
});
|
||||
|
||||
jest.mock('@app/utils/jwt', () => ({
|
||||
jest.mock('utils/jwt', () => ({
|
||||
isJwtExpired: jest.fn(() => false),
|
||||
}));
|
||||
|
||||
@@ -94,49 +103,40 @@ describe('<SubscribeByEmailForm/>', () => {
|
||||
const wrapper = createWrapper(store);
|
||||
|
||||
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 () => {
|
||||
const wrapper = createWrapper();
|
||||
|
||||
const emailVerificationForSubscribe = jest.spyOn(api, 'emailVerificationForSubscribe');
|
||||
const emailConfirmationForSubscribe = jest.spyOn(api, 'emailConfirmationForSubscribe');
|
||||
const onInputEmail = wrapper.find(Input).prop('onInput');
|
||||
const input = wrapper.find('input');
|
||||
const form = wrapper.find('form');
|
||||
|
||||
expect(onInputEmail).toBeFunction();
|
||||
|
||||
act(() => onInputEmail(makeInputEvent('some@email.com')));
|
||||
|
||||
expect(form).toHaveLength(1);
|
||||
|
||||
input.getDOMNode<HTMLInputElement>().value = 'some@email.com';
|
||||
input.simulate('input');
|
||||
form.simulate('submit');
|
||||
|
||||
expect(emailVerificationForSubscribe).toHaveBeenCalledWith('some@email.com');
|
||||
expect(emailVerificationForSubscribeMock).toHaveBeenCalledWith('some@email.com');
|
||||
|
||||
await sleep(0);
|
||||
await sleep();
|
||||
wrapper.update();
|
||||
|
||||
const textarea = wrapper.find(TextareaAutosize);
|
||||
const onInputToken = textarea.prop('onInput') as (e: any) => void;
|
||||
const button = wrapper.find(Button);
|
||||
const textarea = wrapper.find('textarea');
|
||||
const button = wrapper.find('button');
|
||||
|
||||
expect(textarea).toHaveLength(1);
|
||||
expect(onInputToken).toBeFunction();
|
||||
expect(button.at(0).text()).toEqual('Back');
|
||||
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(emailConfirmationForSubscribe).toHaveBeenCalledWith('tokentokentoken');
|
||||
expect(emailConfirmationForSubscribeMock).toHaveBeenCalledWith('tokentokentoken');
|
||||
|
||||
await sleep(0);
|
||||
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');
|
||||
});
|
||||
|
||||
@@ -152,7 +152,7 @@ describe('<SubscribeByEmailForm/>', () => {
|
||||
const onInputEmail = wrapper.find(Input).prop('onInput');
|
||||
const form = wrapper.find('form');
|
||||
|
||||
expect(onInputEmail).toBeFunction();
|
||||
expect(typeof onInputEmail === 'function').toBe(true);
|
||||
|
||||
act(() => onInputEmail(makeInputEvent('some@email.com')));
|
||||
|
||||
@@ -169,7 +169,7 @@ describe('<SubscribeByEmailForm/>', () => {
|
||||
await sleep(0);
|
||||
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');
|
||||
});
|
||||
|
||||
@@ -177,18 +177,17 @@ describe('<SubscribeByEmailForm/>', () => {
|
||||
const store = mockStore({ ...initialStore, user: { email_subscription: true } });
|
||||
const wrapper = createWrapper(store);
|
||||
const onClick = wrapper.find(Button).prop('onClick');
|
||||
const unsubscribeFromEmailUpdates = jest.spyOn(api, 'unsubscribeFromEmailUpdates');
|
||||
|
||||
expect(onClick).toBeFunction();
|
||||
expect(typeof onClick === 'function').toBe(true);
|
||||
|
||||
act(() => onClick());
|
||||
|
||||
expect(unsubscribeFromEmailUpdates).toHaveBeenCalled();
|
||||
expect(unsubscribeFromEmailUpdatesMock).toHaveBeenCalled();
|
||||
|
||||
await sleep(0);
|
||||
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');
|
||||
});
|
||||
});
|
||||
+37
-44
@@ -1,30 +1,25 @@
|
||||
/** @jsx createElement */
|
||||
import { createElement, FunctionComponent, Fragment } from 'preact';
|
||||
import { h, FunctionComponent, Fragment } from 'preact';
|
||||
import { useState, useCallback, useEffect, useRef, PropRef } from 'preact/hooks';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
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 { 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 = /[^@]+@[^.]+\..+/;
|
||||
|
||||
@@ -105,7 +100,7 @@ const renderTokenPart = (
|
||||
handleChangeToken: (e: Event) => void,
|
||||
setEmailStep: () => void
|
||||
) => (
|
||||
<Fragment>
|
||||
<>
|
||||
<Button kind="link" mix="auth-email-login-form__back-button" {...getHandleClickProps(setEmailStep)}>
|
||||
<FormattedMessage id="subscribeByEmail.back" defaultMessage="Back" />
|
||||
</Button>
|
||||
@@ -117,7 +112,7 @@ const renderTokenPart = (
|
||||
disabled={loading}
|
||||
value={token}
|
||||
/>
|
||||
</Fragment>
|
||||
</>
|
||||
);
|
||||
|
||||
export const SubscribeByEmailForm: FunctionComponent = () => {
|
||||
@@ -165,7 +160,7 @@ export const SubscribeByEmailForm: FunctionComponent = () => {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[setLoading, setError, setStep, step, emailAddress, token]
|
||||
[setLoading, setError, setStep, step, emailAddress, token, dispatch, intl]
|
||||
);
|
||||
|
||||
const handleChangeEmail = useCallback((e: Event) => {
|
||||
@@ -193,7 +188,7 @@ export const SubscribeByEmailForm: FunctionComponent = () => {
|
||||
|
||||
setToken(value);
|
||||
},
|
||||
[sendForm, setError, setToken]
|
||||
[sendForm, setError, setToken, intl]
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
@@ -212,6 +207,20 @@ export const SubscribeByEmailForm: FunctionComponent = () => {
|
||||
setStep(Step.Email);
|
||||
}, [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(() => {
|
||||
if (emailAddressRef.current) {
|
||||
emailAddressRef.current.focus();
|
||||
@@ -227,20 +236,6 @@ export const SubscribeByEmailForm: FunctionComponent = () => {
|
||||
}
|
||||
|
||||
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 =
|
||||
previousStep.current === Step.Token
|
||||
? intl.formatMessage(messages.haveSubscribed)
|
||||
@@ -318,9 +313,7 @@ export const SubscribeByEmail: FunctionComponent = () => {
|
||||
const intl = useIntl();
|
||||
const user = useSelector<StoreState, User | null>(({ user }) => user);
|
||||
const isAnonymous = isUserAnonymous(user);
|
||||
const buttonTitle = isAnonymous
|
||||
? intl.formatMessage(messages.onlyRegisteredUsers)
|
||||
: intl.formatMessage(messages.subscribeByEmail);
|
||||
const buttonTitle = intl.formatMessage(isAnonymous ? messages.onlyRegisteredUsers : messages.subscribeByEmail);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
|
||||
File renamed without changes.
+11
-15
@@ -1,37 +1,33 @@
|
||||
/** @jsx createElement */
|
||||
import { createElement } from 'preact';
|
||||
import { h } from 'preact';
|
||||
import { shallow } from 'enzyme';
|
||||
import enMessages from '../../../locales/en.json';
|
||||
|
||||
import { SubscribeByRSS, createSubscribeUrl } from './';
|
||||
import { SubscribeByRSS, createSubscribeUrl } from '.';
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
useSelector: jest.fn(fn => fn({ theme: 'light' })),
|
||||
}));
|
||||
|
||||
jest.mock('react-intl', () => {
|
||||
// Require the original module to not be mocked...
|
||||
const originalModule = jest.requireActual('react-intl');
|
||||
const reactIntl = jest.requireActual('react-intl');
|
||||
const messages = require('locales/en.json');
|
||||
const intlProvider = new reactIntl.IntlProvider({ locale: 'en', messages }, {});
|
||||
|
||||
return {
|
||||
...originalModule,
|
||||
useIntl: () => originalModule.createIntl({ locale: `en`, messages: enMessages }),
|
||||
...reactIntl,
|
||||
useIntl: () => intlProvider.state.intl,
|
||||
};
|
||||
});
|
||||
|
||||
describe('<SubscribeByRSS/>', () => {
|
||||
let wrapper: ReturnType<typeof shallow>;
|
||||
|
||||
beforeAll(() => {
|
||||
wrapper = shallow(<SubscribeByRSS userId="user-1" />);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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(
|
||||
createSubscribeUrl('reply', '&user=user-1')
|
||||
);
|
||||
|
||||
+8
-9
@@ -1,14 +1,13 @@
|
||||
/** @jsx createElement */
|
||||
import { createElement, FunctionComponent } from 'preact';
|
||||
import { h, FunctionComponent } from 'preact';
|
||||
import { useMemo } from 'preact/hooks';
|
||||
import { useIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import useTheme from '@app/hooks/useTheme';
|
||||
import { siteId, url } from '@app/common/settings';
|
||||
import { BASE_URL, API_BASE } from '@app/common/constants';
|
||||
import { Dropdown, DropdownItem } from '@app/components/dropdown';
|
||||
import useTheme from 'hooks/useTheme';
|
||||
import { siteId, url } from 'common/settings';
|
||||
import { BASE_URL, API_BASE } from 'common/constants';
|
||||
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}`;
|
||||
|
||||
const messages = defineMessages({
|
||||
@@ -43,7 +42,7 @@ export const SubscribeByRSS: FunctionComponent<{ userId: string | null }> = ({ u
|
||||
[createSubscribeUrl('site'), intl.formatMessage(messages.site)],
|
||||
[createSubscribeUrl('reply', `&user=${userId}`), intl.formatMessage(messages.replies)],
|
||||
],
|
||||
[userId]
|
||||
[userId, intl]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -56,7 +55,7 @@ export const SubscribeByRSS: FunctionComponent<{ userId: string | null }> = ({ u
|
||||
>
|
||||
{items.map(([href, label]) => (
|
||||
<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}
|
||||
</a>
|
||||
</DropdownItem>
|
||||
|
||||
File renamed without changes.
+9
-9
@@ -2,41 +2,41 @@
|
||||
border-color: var(--color7);
|
||||
background: var(--color8); /* try to fix textarea blinking in Safari */
|
||||
|
||||
.comment-form__actions {
|
||||
& .comment-form__actions {
|
||||
background: var(--color7);
|
||||
}
|
||||
|
||||
.comment-form__button_type_preview {
|
||||
& .comment-form__button_type_preview {
|
||||
background: var(--color8);
|
||||
color: var(--color20);
|
||||
}
|
||||
|
||||
.comment-form__button_type_send {
|
||||
& .comment-form__button_type_send {
|
||||
color: var(--color20);
|
||||
}
|
||||
|
||||
.comment-form__error {
|
||||
& .comment-form__error {
|
||||
border-top: 8px solid var(--color7);
|
||||
background: var(--color28);
|
||||
color: var(--color27);
|
||||
}
|
||||
|
||||
.comment-form__field {
|
||||
& .comment-form__field {
|
||||
background: var(--color8);
|
||||
color: var(--color5);
|
||||
}
|
||||
|
||||
.comment-form__preview {
|
||||
& .comment-form__preview {
|
||||
border-color: var(--color8);
|
||||
background: var(--color8);
|
||||
color: var(--color20);
|
||||
}
|
||||
|
||||
.comment-form__preview-wrapper {
|
||||
& .comment-form__preview-wrapper {
|
||||
background: var(--color7);
|
||||
}
|
||||
|
||||
.comment-form__toolbar-item {
|
||||
& .comment-form__toolbar-item {
|
||||
color: var(--color20);
|
||||
|
||||
&:hover {
|
||||
@@ -44,7 +44,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.comment-form__control-panel {
|
||||
& .comment-form__control-panel {
|
||||
background-color: var(--color7);
|
||||
}
|
||||
}
|
||||
Loaded 100 of 325 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user