mirror of
https://github.com/Redocly/redoc.git
synced 2025-07-06 03:53:03 +03:00
chore: migrate from babel to esbuild loader (#1848)
* chore: migrate from babel to esbuild loader * fix cypress tests
This commit is contained in:
parent
b74dcde42b
commit
35418b1569
|
@ -1,46 +1,5 @@
|
||||||
import * as webpack from 'webpack';
|
import * as webpack from 'webpack';
|
||||||
|
|
||||||
export function getBabelLoader({useBuiltIns, hot}: {useBuiltIns: boolean, hot?: boolean}) {
|
|
||||||
return {
|
|
||||||
loader: 'babel-loader',
|
|
||||||
options: {
|
|
||||||
babelrc: false,
|
|
||||||
sourceType: 'unambiguous',
|
|
||||||
presets: [
|
|
||||||
[
|
|
||||||
'@babel/preset-env',
|
|
||||||
{
|
|
||||||
useBuiltIns: useBuiltIns ? 'usage' : false,
|
|
||||||
corejs: 3,
|
|
||||||
exclude: ['transform-typeof-symbol'],
|
|
||||||
targets: 'defaults',
|
|
||||||
modules: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
['@babel/preset-react', { development: false, runtime: 'classic' }],
|
|
||||||
'@babel/preset-typescript',
|
|
||||||
],
|
|
||||||
plugins: [
|
|
||||||
['@babel/plugin-proposal-decorators', { legacy: true }],
|
|
||||||
['@babel/plugin-proposal-class-properties', { loose: false }],
|
|
||||||
[
|
|
||||||
'@babel/plugin-transform-runtime',
|
|
||||||
{
|
|
||||||
corejs: false,
|
|
||||||
helpers: true,
|
|
||||||
// eslint-disable-next-line import/no-internal-modules
|
|
||||||
version: require('@babel/runtime/package.json').version,
|
|
||||||
regenerator: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'@babel/plugin-proposal-optional-chaining',
|
|
||||||
'@babel/plugin-proposal-nullish-coalescing-operator',
|
|
||||||
hot ? 'react-hot-loader/babel' : undefined,
|
|
||||||
].filter(Boolean)
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function webpackIgnore(regexp) {
|
export function webpackIgnore(regexp) {
|
||||||
return new webpack.NormalModuleReplacementPlugin(regexp, require.resolve('lodash/noop.js'));
|
return new webpack.NormalModuleReplacementPlugin(regexp, require.resolve('lodash.noop'));
|
||||||
}
|
}
|
||||||
|
|
2
custom.d.ts
vendored
2
custom.d.ts
vendored
|
@ -23,3 +23,5 @@ declare var reactHotLoaderGlobal: any;
|
||||||
interface Element {
|
interface Element {
|
||||||
scrollIntoViewIfNeeded(centerIfNeeded?: boolean): void;
|
scrollIntoViewIfNeeded(centerIfNeeded?: boolean): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GenericObject = Record<string, any>;
|
||||||
|
|
|
@ -3,7 +3,7 @@ import ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
|
||||||
import * as HtmlWebpackPlugin from 'html-webpack-plugin';
|
import * as HtmlWebpackPlugin from 'html-webpack-plugin';
|
||||||
import { resolve } from 'path';
|
import { resolve } from 'path';
|
||||||
import * as webpack from 'webpack';
|
import * as webpack from 'webpack';
|
||||||
import { getBabelLoader, webpackIgnore } from '../config/webpack-utils';
|
import { webpackIgnore } from '../config/webpack-utils';
|
||||||
|
|
||||||
const VERSION = JSON.stringify(require('../package.json').version);
|
const VERSION = JSON.stringify(require('../package.json').version);
|
||||||
const REVISION = JSON.stringify(
|
const REVISION = JSON.stringify(
|
||||||
|
@ -46,6 +46,7 @@ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) =
|
||||||
extensions: ['.ts', '.tsx', '.js', '.json'],
|
extensions: ['.ts', '.tsx', '.js', '.json'],
|
||||||
fallback: {
|
fallback: {
|
||||||
path: require.resolve('path-browserify'),
|
path: require.resolve('path-browserify'),
|
||||||
|
buffer: require.resolve('buffer'),
|
||||||
http: false,
|
http: false,
|
||||||
fs: false,
|
fs: false,
|
||||||
os: false,
|
os: false,
|
||||||
|
@ -72,33 +73,28 @@ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) =
|
||||||
rules: [
|
rules: [
|
||||||
{ test: [/\.eot$/, /\.gif$/, /\.woff$/, /\.svg$/, /\.ttf$/], use: 'null-loader' },
|
{ test: [/\.eot$/, /\.gif$/, /\.woff$/, /\.svg$/, /\.ttf$/], use: 'null-loader' },
|
||||||
{
|
{
|
||||||
test: /\.tsx?$/,
|
test: /\.(tsx?|[cm]?js)$/,
|
||||||
use: [getBabelLoader({ useBuiltIns: true, hot: true })],
|
loader: 'esbuild-loader',
|
||||||
exclude: {
|
options: {
|
||||||
and: [/node_modules/],
|
loader: 'tsx',
|
||||||
not: {
|
target: 'es2015',
|
||||||
or: [
|
tsconfigRaw: require('../tsconfig.json'),
|
||||||
/swagger2openapi/,
|
|
||||||
/reftools/,
|
|
||||||
/openapi-sampler/,
|
|
||||||
/mobx/,
|
|
||||||
/oas-resolver/,
|
|
||||||
/oas-kit-common/,
|
|
||||||
/oas-schema-walker/,
|
|
||||||
/\@redocly\/openapi-core/,
|
|
||||||
/colorette/,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
exclude: [/node_modules/],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.css$/,
|
test: /\.css$/,
|
||||||
use: {
|
use: [
|
||||||
loader: 'css-loader',
|
'style-loader',
|
||||||
|
'css-loader',
|
||||||
|
{
|
||||||
|
loader: 'esbuild-loader',
|
||||||
options: {
|
options: {
|
||||||
sourceMap: true,
|
loader: 'css',
|
||||||
|
minify: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -119,6 +115,9 @@ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) =
|
||||||
? 'benchmark/index.html'
|
? 'benchmark/index.html'
|
||||||
: 'demo/index.html',
|
: 'demo/index.html',
|
||||||
}),
|
}),
|
||||||
|
new webpack.ProvidePlugin({
|
||||||
|
Buffer: ['buffer', 'Buffer'],
|
||||||
|
}),
|
||||||
new ForkTsCheckerWebpackPlugin({ logger: { infrastructure: 'silent', issues: 'console' } }),
|
new ForkTsCheckerWebpackPlugin({ logger: { infrastructure: 'silent', issues: 'console' } }),
|
||||||
webpackIgnore(/js-yaml\/dumper\.js$/),
|
webpackIgnore(/js-yaml\/dumper\.js$/),
|
||||||
webpackIgnore(/json-schema-ref-parser\/lib\/dereference\.js/),
|
webpackIgnore(/json-schema-ref-parser\/lib\/dereference\.js/),
|
||||||
|
|
1201
package-lock.json
generated
1201
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
23
package.json
23
package.json
|
@ -61,18 +61,6 @@
|
||||||
"pre-commit": "pretty-quick --staged && npm run lint"
|
"pre-commit": "pretty-quick --staged && npm run lint"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.14.3",
|
|
||||||
"@babel/plugin-proposal-class-properties": "^7.13.0",
|
|
||||||
"@babel/plugin-proposal-decorators": "^7.14.2",
|
|
||||||
"@babel/plugin-proposal-object-rest-spread": "^7.14.4",
|
|
||||||
"@babel/plugin-syntax-decorators": "^7.12.13",
|
|
||||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
|
||||||
"@babel/plugin-syntax-jsx": "^7.10.4",
|
|
||||||
"@babel/plugin-syntax-typescript": "^7.10.4",
|
|
||||||
"@babel/plugin-transform-runtime": "^7.14.3",
|
|
||||||
"@babel/preset-env": "^7.14.4",
|
|
||||||
"@babel/preset-react": "^7.13.13",
|
|
||||||
"@babel/preset-typescript": "^7.13.0",
|
|
||||||
"@cypress/webpack-preprocessor": "^5.9.0",
|
"@cypress/webpack-preprocessor": "^5.9.0",
|
||||||
"@hot-loader/react-dom": "^17.0.1",
|
"@hot-loader/react-dom": "^17.0.1",
|
||||||
"@size-limit/preset-app": "^7.0.4",
|
"@size-limit/preset-app": "^7.0.4",
|
||||||
|
@ -82,7 +70,6 @@
|
||||||
"@types/enzyme-to-json": "^1.5.3",
|
"@types/enzyme-to-json": "^1.5.3",
|
||||||
"@types/jest": "^26.0.23",
|
"@types/jest": "^26.0.23",
|
||||||
"@types/json-pointer": "^1.0.30",
|
"@types/json-pointer": "^1.0.30",
|
||||||
"@types/lodash": "^4.14.170",
|
|
||||||
"@types/lunr": "^2.3.3",
|
"@types/lunr": "^2.3.3",
|
||||||
"@types/mark.js": "^8.11.5",
|
"@types/mark.js": "^8.11.5",
|
||||||
"@types/marked": "^4.0.1",
|
"@types/marked": "^4.0.1",
|
||||||
|
@ -100,8 +87,6 @@
|
||||||
"@typescript-eslint/eslint-plugin": "^4.26.0",
|
"@typescript-eslint/eslint-plugin": "^4.26.0",
|
||||||
"@typescript-eslint/parser": "^4.26.0",
|
"@typescript-eslint/parser": "^4.26.0",
|
||||||
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.1",
|
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.1",
|
||||||
"babel-loader": "^8.2.2",
|
|
||||||
"babel-plugin-styled-components": "^1.12.0",
|
|
||||||
"beautify-benchmark": "^0.2.4",
|
"beautify-benchmark": "^0.2.4",
|
||||||
"conventional-changelog-cli": "^2.0.34",
|
"conventional-changelog-cli": "^2.0.34",
|
||||||
"copy-webpack-plugin": "^9.0.0",
|
"copy-webpack-plugin": "^9.0.0",
|
||||||
|
@ -111,6 +96,7 @@
|
||||||
"cypress": "^7.4.0",
|
"cypress": "^7.4.0",
|
||||||
"enzyme": "^3.11.0",
|
"enzyme": "^3.11.0",
|
||||||
"enzyme-to-json": "^3.6.2",
|
"enzyme-to-json": "^3.6.2",
|
||||||
|
"esbuild-loader": "^2.18.0",
|
||||||
"eslint": "^7.27.0",
|
"eslint": "^7.27.0",
|
||||||
"eslint-plugin-import": "^2.23.4",
|
"eslint-plugin-import": "^2.23.4",
|
||||||
"eslint-plugin-react": "^7.25.1",
|
"eslint-plugin-react": "^7.25.1",
|
||||||
|
@ -121,7 +107,7 @@
|
||||||
"jest": "^27.0.3",
|
"jest": "^27.0.3",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"license-checker": "^25.0.1",
|
"license-checker": "^25.0.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash.noop": "^3.0.1",
|
||||||
"mobx": "^6.3.2",
|
"mobx": "^6.3.2",
|
||||||
"prettier": "^2.3.2",
|
"prettier": "^2.3.2",
|
||||||
"pretty-quick": "^3.0.0",
|
"pretty-quick": "^3.0.0",
|
||||||
|
@ -135,7 +121,7 @@
|
||||||
"style-loader": "^2.0.0",
|
"style-loader": "^2.0.0",
|
||||||
"styled-components": "^5.3.0",
|
"styled-components": "^5.3.0",
|
||||||
"ts-jest": "^27.0.2",
|
"ts-jest": "^27.0.2",
|
||||||
"ts-loader": "^8.0.1",
|
"ts-loader": "^9.2.6",
|
||||||
"ts-node": "^10.0.0",
|
"ts-node": "^10.0.0",
|
||||||
"typescript": "~4.1.0",
|
"typescript": "~4.1.0",
|
||||||
"unfetch": "^4.2.0",
|
"unfetch": "^4.2.0",
|
||||||
|
@ -154,7 +140,6 @@
|
||||||
"styled-components": "^4.1.1 || ^5.1.1"
|
"styled-components": "^4.1.1 || ^5.1.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.14.0",
|
|
||||||
"@redocly/openapi-core": "^1.0.0-beta.54",
|
"@redocly/openapi-core": "^1.0.0-beta.54",
|
||||||
"@redocly/react-dropdown-aria": "^2.0.11",
|
"@redocly/react-dropdown-aria": "^2.0.11",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
|
@ -166,7 +151,7 @@
|
||||||
"mark.js": "^8.11.1",
|
"mark.js": "^8.11.1",
|
||||||
"marked": "^4.0.10",
|
"marked": "^4.0.10",
|
||||||
"mobx-react": "^7.2.0",
|
"mobx-react": "^7.2.0",
|
||||||
"openapi-sampler": "^1.0.1",
|
"openapi-sampler": "^1.1.1",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"perfect-scrollbar": "^1.5.1",
|
"perfect-scrollbar": "^1.5.1",
|
||||||
"polished": "^4.1.3",
|
"polished": "^4.1.3",
|
||||||
|
|
50
src/utils/__tests__/object.test.ts
Normal file
50
src/utils/__tests__/object.test.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import { objectHas, objectSet } from '../object';
|
||||||
|
|
||||||
|
describe('object utils', () => {
|
||||||
|
let obj;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
obj = {
|
||||||
|
a: {
|
||||||
|
b: {
|
||||||
|
c: {
|
||||||
|
d: 'd',
|
||||||
|
},
|
||||||
|
c1: 'c1',
|
||||||
|
},
|
||||||
|
b1: 'b1',
|
||||||
|
},
|
||||||
|
a1: 'a1',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('objectHas function', () => {
|
||||||
|
it('should check if the obj has path as string', () => {
|
||||||
|
expect(objectHas(obj, 'a.b.c')).toBeTruthy();
|
||||||
|
expect(objectHas(obj, 'a.b.c1')).toBeTruthy();
|
||||||
|
expect(objectHas(obj, 'a.b.c.d')).toBeTruthy();
|
||||||
|
expect(objectHas(obj, 'a.b.c1.d')).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should check if the obj has path as array', () => {
|
||||||
|
expect(objectHas(obj, ['a', 'b', 'c'])).toBeTruthy();
|
||||||
|
expect(objectHas(obj, ['a', 'b', 'c1'])).toBeTruthy();
|
||||||
|
expect(objectHas(obj, ['a', 'b', 'c', 'd'])).toBeTruthy();
|
||||||
|
expect(objectHas(obj, ['a', 'b', 'c1', 'd'])).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('objectSet function', () => {
|
||||||
|
it('should set value by path as string', () => {
|
||||||
|
expect(objectHas(obj, 'a.b.c1.d')).toBeFalsy();
|
||||||
|
objectSet(obj, 'a.b.c1', { d: 'd' });
|
||||||
|
expect(objectHas(obj, 'a.b.c1.d')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set value by path as array', () => {
|
||||||
|
expect(objectHas(obj, ['a', 'b', 'c1', 'd'])).toBeFalsy();
|
||||||
|
objectSet(obj, ['a', 'b', 'c1'], { d: 'd' });
|
||||||
|
expect(objectHas(obj, ['a', 'b', 'c1', 'd'])).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
28
src/utils/object.ts
Normal file
28
src/utils/object.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
export function objectHas(object: GenericObject, path: string | Array<string>): boolean {
|
||||||
|
let _path = <Array<string>>path;
|
||||||
|
|
||||||
|
if (typeof path === 'string') {
|
||||||
|
_path = path.split('.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return _path.every((key: string) => {
|
||||||
|
if (typeof object != 'object' || object === null || !(key in object)) return false;
|
||||||
|
object = object[key];
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function objectSet(object: GenericObject, path: string | Array<string>, value: any): void {
|
||||||
|
let _path = <Array<string>>path;
|
||||||
|
|
||||||
|
if (typeof path === 'string') {
|
||||||
|
_path = path.split('.');
|
||||||
|
}
|
||||||
|
const limit = _path.length - 1;
|
||||||
|
for (let i = 0; i < limit; ++i) {
|
||||||
|
const key = _path[i];
|
||||||
|
object = object[key] ?? (object[key] = {});
|
||||||
|
}
|
||||||
|
const key = _path[limit];
|
||||||
|
object[key] = value;
|
||||||
|
}
|
|
@ -1,6 +1,4 @@
|
||||||
/* tslint:disable:no-implicit-dependencies */
|
import { objectHas, objectSet } from './object';
|
||||||
|
|
||||||
import { has, set } from 'lodash';
|
|
||||||
|
|
||||||
function traverseComponent(root, fn) {
|
function traverseComponent(root, fn) {
|
||||||
if (!root) {
|
if (!root) {
|
||||||
|
@ -20,8 +18,8 @@ export function filterPropsDeep<T extends object>(component: T, paths: string[])
|
||||||
traverseComponent(component, comp => {
|
traverseComponent(component, comp => {
|
||||||
if (comp.props) {
|
if (comp.props) {
|
||||||
for (const path of paths) {
|
for (const path of paths) {
|
||||||
if (has(comp.props, path)) {
|
if (objectHas(comp.props, path)) {
|
||||||
set(comp.props, path, '<<<filtered>>>');
|
objectSet(comp.props, path, '<<<filtered>>>');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
|
import ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
|
||||||
import * as webpack from 'webpack';
|
import * as webpack from 'webpack';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { getBabelLoader, webpackIgnore } from './config/webpack-utils';
|
import { webpackIgnore } from './config/webpack-utils';
|
||||||
|
|
||||||
const nodeExternals = require('webpack-node-externals')({
|
const nodeExternals = require('webpack-node-externals')({
|
||||||
// bundle in modules that need transpiling + non-js (e.g. css)
|
// bundle in modules that need transpiling + non-js (e.g. css)
|
||||||
|
@ -50,6 +50,7 @@ export default (env: { standalone?: boolean; browser?: boolean } = {}) => ({
|
||||||
extensions: ['.ts', '.tsx', '.js', '.mjs', '.json'],
|
extensions: ['.ts', '.tsx', '.js', '.mjs', '.json'],
|
||||||
fallback: {
|
fallback: {
|
||||||
path: require.resolve('path-browserify'),
|
path: require.resolve('path-browserify'),
|
||||||
|
buffer: require.resolve('buffer'),
|
||||||
http: false,
|
http: false,
|
||||||
fs: path.resolve(__dirname, 'src/empty.js'),
|
fs: path.resolve(__dirname, 'src/empty.js'),
|
||||||
os: path.resolve(__dirname, 'src/empty.js'),
|
os: path.resolve(__dirname, 'src/empty.js'),
|
||||||
|
@ -78,32 +79,27 @@ export default (env: { standalone?: boolean; browser?: boolean } = {}) => ({
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
test: /\.(tsx?|[cm]?js)$/,
|
test: /\.(tsx?|[cm]?js)$/,
|
||||||
use: [getBabelLoader({ useBuiltIns: !!env.standalone })],
|
loader: 'esbuild-loader',
|
||||||
exclude: {
|
options: {
|
||||||
and: [/node_modules/],
|
loader: 'tsx',
|
||||||
not: {
|
target: 'es2015',
|
||||||
or: [
|
tsconfigRaw: require('./tsconfig.json'),
|
||||||
/swagger2openapi/,
|
|
||||||
/reftools/,
|
|
||||||
/openapi-sampler/,
|
|
||||||
/mobx/,
|
|
||||||
/oas-resolver/,
|
|
||||||
/oas-kit-common/,
|
|
||||||
/oas-schema-walker/,
|
|
||||||
/\@redocly\/openapi-core/,
|
|
||||||
/colorette/,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
exclude: [/node_modules/],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.css$/,
|
test: /\.css$/,
|
||||||
use: {
|
use: [
|
||||||
loader: 'css-loader',
|
'style-loader',
|
||||||
|
'css-loader',
|
||||||
|
{
|
||||||
|
loader: 'esbuild-loader',
|
||||||
options: {
|
options: {
|
||||||
sourceMap: false,
|
loader: 'css',
|
||||||
|
minify: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -117,6 +113,9 @@ export default (env: { standalone?: boolean; browser?: boolean } = {}) => ({
|
||||||
}),
|
}),
|
||||||
new ForkTsCheckerWebpackPlugin({ logger: { infrastructure: 'silent', issues: 'console' } }),
|
new ForkTsCheckerWebpackPlugin({ logger: { infrastructure: 'silent', issues: 'console' } }),
|
||||||
new webpack.BannerPlugin(BANNER),
|
new webpack.BannerPlugin(BANNER),
|
||||||
|
new webpack.ProvidePlugin({
|
||||||
|
Buffer: ['buffer', 'Buffer'],
|
||||||
|
}),
|
||||||
webpackIgnore(/js-yaml\/dumper\.js$/),
|
webpackIgnore(/js-yaml\/dumper\.js$/),
|
||||||
env.standalone ? webpackIgnore(/^\.\/SearchWorker\.worker$/) : undefined,
|
env.standalone ? webpackIgnore(/^\.\/SearchWorker\.worker$/) : undefined,
|
||||||
].filter(Boolean),
|
].filter(Boolean),
|
||||||
|
|
Loading…
Reference in New Issue
Block a user