🎉 1.0.0-RC2
This commit is contained in:
parent
ff3eb7eddd
commit
53f8528b9e
6
.gitpod.yml
Normal file
6
.gitpod.yml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
ports:
|
||||
- port: 8000
|
||||
onOpen: open-preview
|
||||
tasks:
|
||||
- init: npm install
|
||||
command: npm start
|
||||
|
|
@ -20,6 +20,7 @@ const plugins = [
|
|||
},
|
||||
dynamicImport: {
|
||||
loadingComponent: './components/PageLoading/index',
|
||||
webpackChunkName: true,
|
||||
},
|
||||
pwa: {
|
||||
workboxPluginMode: 'InjectManifest',
|
||||
|
|
@ -33,7 +34,7 @@ const plugins = [
|
|||
include: ['dva', 'dva/router', 'dva/saga', 'dva/fetch'],
|
||||
exclude: ['@babel/runtime'],
|
||||
},
|
||||
hardSource: true,
|
||||
hardSource: false,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
|
|
@ -58,6 +59,7 @@ export default {
|
|||
define: {
|
||||
APP_TYPE: process.env.APP_TYPE || '',
|
||||
},
|
||||
treeShaking: true,
|
||||
targets: {
|
||||
ie: 11,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
// [START functionsimport]
|
||||
const functions = require('firebase-functions');
|
||||
const express = require('express');
|
||||
|
||||
const matchMock = require('./matchMock');
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(matchMock);
|
||||
|
||||
exports.api = functions.https.onRequest(app);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
const mockFile = require('./mock/index');
|
||||
const pathToRegexp = require('path-to-regexp');
|
||||
const debug = console.log;
|
||||
const bodyParser = require('body-parser');
|
||||
|
||||
const mockFile = require('./mock/index');
|
||||
|
||||
const BODY_PARSED_METHODS = ['post', 'put', 'patch'];
|
||||
|
||||
const debug = console.log;
|
||||
function parseKey(key) {
|
||||
let method = 'get';
|
||||
let path = key;
|
||||
|
|
@ -20,7 +21,14 @@ function parseKey(key) {
|
|||
}
|
||||
|
||||
function createHandler(method, path, handler) {
|
||||
return function(req, res, next) {
|
||||
return (req, res, next) => {
|
||||
function sendData() {
|
||||
if (typeof handler === 'function') {
|
||||
handler(req, res, next);
|
||||
} else {
|
||||
res.json(handler);
|
||||
}
|
||||
}
|
||||
if (BODY_PARSED_METHODS.includes(method)) {
|
||||
bodyParser.json({ limit: '5mb', strict: false })(req, res, () => {
|
||||
bodyParser.urlencoded({ limit: '5mb', extended: true })(req, res, () => {
|
||||
|
|
@ -30,14 +38,6 @@ function createHandler(method, path, handler) {
|
|||
} else {
|
||||
sendData();
|
||||
}
|
||||
|
||||
function sendData() {
|
||||
if (typeof handler === 'function') {
|
||||
handler(req, res, next);
|
||||
} else {
|
||||
res.json(handler);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -63,6 +63,24 @@ const mockData = normalizeConfig(mockFile);
|
|||
function matchMock(req) {
|
||||
const { path: exceptPath } = req;
|
||||
const exceptMethod = req.method.toLowerCase();
|
||||
function decodeParam(val) {
|
||||
if (typeof val !== 'string' || val.length === 0) {
|
||||
return val;
|
||||
}
|
||||
|
||||
try {
|
||||
return decodeURIComponent(val);
|
||||
} catch (err) {
|
||||
if (err instanceof URIError) {
|
||||
err.message = `Failed to decode param ' ${val} '`;
|
||||
err.statusCode = 400;
|
||||
err.status = 400;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const mock of mockData) {
|
||||
const { method, re, keys } = mock;
|
||||
if (method === exceptMethod) {
|
||||
|
|
@ -70,7 +88,7 @@ function matchMock(req) {
|
|||
if (match) {
|
||||
const params = {};
|
||||
|
||||
for (let i = 1; i < match.length; i = i + 1) {
|
||||
for (let i = 1; i < match.length; i += 1) {
|
||||
const key = keys[i - 1];
|
||||
const prop = key.name;
|
||||
const val = decodeParam(match[i]);
|
||||
|
|
@ -85,33 +103,13 @@ function matchMock(req) {
|
|||
}
|
||||
}
|
||||
|
||||
function decodeParam(val) {
|
||||
if (typeof val !== 'string' || val.length === 0) {
|
||||
return val;
|
||||
}
|
||||
|
||||
try {
|
||||
return decodeURIComponent(val);
|
||||
} catch (err) {
|
||||
if (err instanceof URIError) {
|
||||
err.message = `Failed to decode param ' ${val} '`;
|
||||
err.status = err.statusCode = 400;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
return mockData.filter(({ method, re }) => {
|
||||
return method === exceptMethod && re.test(exceptPath);
|
||||
})[0];
|
||||
return mockData.filter(({ method, re }) => method === exceptMethod && re.test(exceptPath))[0];
|
||||
}
|
||||
module.exports = (req, res, next) => {
|
||||
const match = matchMock(req);
|
||||
if (match) {
|
||||
debug(`mock matched: [${match.method}] ${match.path}`);
|
||||
return match.handler(req, res, next);
|
||||
} else {
|
||||
return next();
|
||||
}
|
||||
return next();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,19 +2,19 @@
|
|||
"name": "functions",
|
||||
"description": "Cloud Functions for Firebase",
|
||||
"scripts": {
|
||||
"serve": "npm run mock && firebase serve --only functions",
|
||||
"serve": "firebase serve --only functions",
|
||||
"shell": "firebase functions:shell",
|
||||
"start": "npm run shell",
|
||||
"deploy": "firebase deploy --only functions",
|
||||
"deploy": "npm run mock && firebase deploy --only functions",
|
||||
"logs": "firebase functions:log",
|
||||
"mock": "node ../scripts/generateMock.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.0.0",
|
||||
"body-parser": "^1.18.3",
|
||||
"express": "^4.16.3",
|
||||
"firebase-admin": "^5.12.1",
|
||||
"firebase-functions": "^2.0.5",
|
||||
"express": "^4.16.4",
|
||||
"firebase-admin": "^6.4.0",
|
||||
"firebase-functions": "^2.1.0",
|
||||
"mockjs": "^1.0.1-beta3",
|
||||
"moment": "^2.22.2",
|
||||
"path-to-regexp": "^2.2.1"
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ module.exports = {
|
|||
'--no-first-run',
|
||||
'--no-sandbox',
|
||||
'--no-zygote',
|
||||
'--single-process',
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
|
|||
209
mock/notices.js
209
mock/notices.js
|
|
@ -1,104 +1,113 @@
|
|||
const fakeNotices = [
|
||||
{
|
||||
id: '000000001',
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
|
||||
title: '你收到了 14 份新周报',
|
||||
datetime: '2017-08-09',
|
||||
type: 'notification',
|
||||
},
|
||||
{
|
||||
id: '000000002',
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png',
|
||||
title: '你推荐的 曲妮妮 已通过第三轮面试',
|
||||
datetime: '2017-08-08',
|
||||
type: 'notification',
|
||||
},
|
||||
{
|
||||
id: '000000003',
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png',
|
||||
title: '这种模板可以区分多种通知类型',
|
||||
datetime: '2017-08-07',
|
||||
read: true,
|
||||
type: 'notification',
|
||||
},
|
||||
{
|
||||
id: '000000004',
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
|
||||
title: '左侧图标用于区分不同的类型',
|
||||
datetime: '2017-08-07',
|
||||
type: 'notification',
|
||||
},
|
||||
{
|
||||
id: '000000005',
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
|
||||
title: '内容不要超过两行字,超出时自动截断',
|
||||
datetime: '2017-08-07',
|
||||
type: 'notification',
|
||||
},
|
||||
{
|
||||
id: '000000006',
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
|
||||
title: '曲丽丽 评论了你',
|
||||
description: '描述信息描述信息描述信息',
|
||||
datetime: '2017-08-07',
|
||||
type: 'message',
|
||||
clickClose: true,
|
||||
},
|
||||
{
|
||||
id: '000000007',
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
|
||||
title: '朱偏右 回复了你',
|
||||
description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
|
||||
datetime: '2017-08-07',
|
||||
type: 'message',
|
||||
clickClose: true,
|
||||
},
|
||||
{
|
||||
id: '000000008',
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
|
||||
title: '标题',
|
||||
description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
|
||||
datetime: '2017-08-07',
|
||||
type: 'message',
|
||||
clickClose: true,
|
||||
},
|
||||
{
|
||||
id: '000000009',
|
||||
title: '任务名称',
|
||||
description: '任务需要在 2017-01-12 20:00 前启动',
|
||||
extra: '未开始',
|
||||
status: 'todo',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
id: '000000010',
|
||||
title: '第三方紧急代码变更',
|
||||
description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
|
||||
extra: '马上到期',
|
||||
status: 'urgent',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
id: '000000011',
|
||||
title: '信息安全考试',
|
||||
description: '指派竹尔于 2017-01-09 前完成更新并发布',
|
||||
extra: '已耗时 8 天',
|
||||
status: 'doing',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
id: '000000012',
|
||||
title: 'ABCD 版本发布',
|
||||
description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
|
||||
extra: '进行中',
|
||||
status: 'processing',
|
||||
type: 'event',
|
||||
},
|
||||
];
|
||||
|
||||
const getNotices = (req, res) => {
|
||||
const json = { code: 200, success: true, msg: '操作成功' };
|
||||
const data = [
|
||||
{
|
||||
id: '000000001',
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
|
||||
title: '你收到了 14 份新周报',
|
||||
datetime: '2017-08-09',
|
||||
type: 'notification',
|
||||
},
|
||||
{
|
||||
id: '000000002',
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png',
|
||||
title: '你推荐的 曲妮妮 已通过第三轮面试',
|
||||
datetime: '2017-08-08',
|
||||
type: 'notification',
|
||||
},
|
||||
{
|
||||
id: '000000003',
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png',
|
||||
title: '这种模板可以区分多种通知类型',
|
||||
datetime: '2017-08-07',
|
||||
read: true,
|
||||
type: 'notification',
|
||||
},
|
||||
{
|
||||
id: '000000004',
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
|
||||
title: '左侧图标用于区分不同的类型',
|
||||
datetime: '2017-08-07',
|
||||
type: 'notification',
|
||||
},
|
||||
{
|
||||
id: '000000005',
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
|
||||
title: '内容不要超过两行字,超出时自动截断',
|
||||
datetime: '2017-08-07',
|
||||
type: 'notification',
|
||||
},
|
||||
{
|
||||
id: '000000006',
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
|
||||
title: '曲丽丽 评论了你',
|
||||
description: '描述信息描述信息描述信息',
|
||||
datetime: '2017-08-07',
|
||||
type: 'message',
|
||||
clickClose: true,
|
||||
},
|
||||
{
|
||||
id: '000000007',
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
|
||||
title: '朱偏右 回复了你',
|
||||
description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
|
||||
datetime: '2017-08-07',
|
||||
type: 'message',
|
||||
clickClose: true,
|
||||
},
|
||||
{
|
||||
id: '000000008',
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
|
||||
title: '标题',
|
||||
description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
|
||||
datetime: '2017-08-07',
|
||||
type: 'message',
|
||||
clickClose: true,
|
||||
},
|
||||
{
|
||||
id: '000000009',
|
||||
title: '任务名称',
|
||||
description: '任务需要在 2017-01-12 20:00 前启动',
|
||||
extra: '未开始',
|
||||
status: 'todo',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
id: '000000010',
|
||||
title: '第三方紧急代码变更',
|
||||
description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
|
||||
extra: '马上到期',
|
||||
status: 'urgent',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
id: '000000011',
|
||||
title: '信息安全考试',
|
||||
description: '指派竹尔于 2017-01-09 前完成更新并发布',
|
||||
extra: '已耗时 8 天',
|
||||
status: 'doing',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
id: '000000012',
|
||||
title: 'ABCD 版本发布',
|
||||
description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
|
||||
extra: '进行中',
|
||||
status: 'processing',
|
||||
type: 'event',
|
||||
},
|
||||
];
|
||||
json.data = data;
|
||||
return res.json(json);
|
||||
if (req.query && req.query.type) {
|
||||
const startFrom = parseInt(req.query.lastItemId, 10) + 1;
|
||||
const result = fakeNotices
|
||||
.filter(({ type }) => type === req.query.type)
|
||||
.map((notice, index) => ({
|
||||
...notice,
|
||||
id: `0000000${startFrom + index}`,
|
||||
}));
|
||||
return res.json(startFrom > 24 ? result.concat(null) : result);
|
||||
}
|
||||
return res.json(fakeNotices);
|
||||
};
|
||||
|
||||
export default {
|
||||
|
|
|
|||
5
mock/route.js
Normal file
5
mock/route.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export default {
|
||||
'/api/auth_routes': {
|
||||
'/form/advanced-form': { authority: ['admin', 'user'] },
|
||||
},
|
||||
};
|
||||
12
package.json
12
package.json
|
|
@ -4,7 +4,7 @@
|
|||
"description": "An out-of-box UI solution for enterprise applications",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"presite": "node ./scripts/generateMock.js && cd functions && npm install",
|
||||
"presite": "cd functions && npm install",
|
||||
"start": "cross-env APP_TYPE=site PORT=8888 umi dev",
|
||||
"start:no-mock": "cross-env MOCK=none PORT=8888 umi dev",
|
||||
"build": "umi build",
|
||||
|
|
@ -27,8 +27,8 @@
|
|||
"docker-prod:dev": "docker-compose -f ./docker/docker-compose.yml up",
|
||||
"docker-prod:build": "docker-compose -f ./docker/docker-compose.yml build",
|
||||
"docker-hub:build": "docker build -f Dockerfile.hub -t ant-design-pro ./",
|
||||
"docker:tag": "docker tag ant-design-pro chenshuai2144/ant-design-pro",
|
||||
"docker:push": "npm run docker-hub:build && npm run docker:tag && docker push chenshuai2144/ant-design-pro"
|
||||
"docker:tag": "docker tag ant-design-pro antdesign/ant-design-pro",
|
||||
"docker:push": "npm run docker-hub:build && npm run docker:tag && docker push antdesign/ant-design-pro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@antv/data-set": "^0.10.0",
|
||||
|
|
@ -36,6 +36,7 @@
|
|||
"antd": "^3.11.6",
|
||||
"bizcharts": "^3.4.2",
|
||||
"bizcharts-plugin-slider": "^2.1.1-beta.1",
|
||||
"chalk": "^2.4.2",
|
||||
"classnames": "^2.2.6",
|
||||
"dva": "^2.4.0",
|
||||
"enquire-js": "^0.2.1",
|
||||
|
|
@ -83,6 +84,7 @@
|
|||
"gh-pages": "^2.0.1",
|
||||
"husky": "^1.2.0",
|
||||
"jest-puppeteer": "^3.5.1",
|
||||
"less": "^3.9.0",
|
||||
"lint-staged": "^8.1.0",
|
||||
"merge-umi-mock-data": "^0.0.3",
|
||||
"mockjs": "^1.0.1-beta3",
|
||||
|
|
@ -95,9 +97,9 @@
|
|||
"tslint": "^5.10.0",
|
||||
"tslint-config-prettier": "^1.10.0",
|
||||
"tslint-react": "^3.6.0",
|
||||
"umi": "^2.3.1",
|
||||
"umi": "^2.4.2",
|
||||
"umi-plugin-ga": "^1.1.3",
|
||||
"umi-plugin-react": "^1.2.0"
|
||||
"umi-plugin-react": "^1.3.4"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"puppeteer": "^1.10.0"
|
||||
|
|
|
|||
|
|
@ -6,43 +6,45 @@
|
|||
*----------*****--------------
|
||||
*/
|
||||
|
||||
const glob = require('glob');
|
||||
const prettier = require('prettier');
|
||||
const fs = require('fs');
|
||||
const chalk = require('chalk');
|
||||
const prettierConfigPath = require.resolve('../.prettierrc');
|
||||
|
||||
const files = process.argv.slice(2);
|
||||
|
||||
let didError = false;
|
||||
let didWarn = false;
|
||||
|
||||
files.forEach(file => {
|
||||
const options = prettier.resolveConfig.sync(file, {
|
||||
config: prettierConfigPath,
|
||||
});
|
||||
try {
|
||||
const fileInfo = prettier.getFileInfo.sync(file);
|
||||
if (fileInfo.ignored) {
|
||||
return;
|
||||
}
|
||||
const input = fs.readFileSync(file, 'utf8');
|
||||
const withParserOptions = {
|
||||
...options,
|
||||
parser: fileInfo.inferredParser,
|
||||
};
|
||||
const isPrettier = prettier.check(input, withParserOptions);
|
||||
if (!isPrettier) {
|
||||
console.log(
|
||||
`\x1b[31m ${file} is no prettier, please use npm run prettier and git add !\x1b[0m`
|
||||
);
|
||||
didWarn = true;
|
||||
}
|
||||
} catch (e) {
|
||||
didError = true;
|
||||
}
|
||||
Promise.all([
|
||||
prettier.resolveConfig(file, {
|
||||
config: prettierConfigPath,
|
||||
}),
|
||||
prettier.getFileInfo(file),
|
||||
])
|
||||
.then(resolves => {
|
||||
const [options, fileInfo] = resolves;
|
||||
if (fileInfo.ignored) {
|
||||
return;
|
||||
}
|
||||
const input = fs.readFileSync(file, 'utf8');
|
||||
const withParserOptions = {
|
||||
...options,
|
||||
parser: fileInfo.inferredParser,
|
||||
};
|
||||
const output = prettier.format(input, withParserOptions);
|
||||
if (output !== input) {
|
||||
fs.writeFileSync(file, output, 'utf8');
|
||||
console.log(chalk.green(`${file} is prettier`));
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
didError = true;
|
||||
})
|
||||
.finally(() => {
|
||||
if (didError) {
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(chalk.hex('#1890FF')('prettier success!'));
|
||||
});
|
||||
});
|
||||
|
||||
if (didWarn || didError) {
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('\x1b[32m lint prettier success!\x1b[0m');
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@
|
|||
*----------*****--------------
|
||||
*/
|
||||
|
||||
const glob = require('glob');
|
||||
const prettier = require('prettier');
|
||||
const fs = require('fs');
|
||||
const getPrettierFiles = require('./getPrettierFiles');
|
||||
const prettierConfigPath = require.resolve('../.prettierrc');
|
||||
const chalk = require('chalk');
|
||||
|
||||
let didError = false;
|
||||
|
||||
|
|
@ -33,7 +33,7 @@ files.forEach(file => {
|
|||
const output = prettier.format(input, withParserOptions);
|
||||
if (output !== input) {
|
||||
fs.writeFileSync(file, output, 'utf8');
|
||||
console.log(`\x1b[34m ${file} is prettier`);
|
||||
console.log(chalk.green(`${file} is prettier`));
|
||||
}
|
||||
} catch (e) {
|
||||
didError = true;
|
||||
|
|
@ -43,4 +43,4 @@ files.forEach(file => {
|
|||
if (didError) {
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('\x1b[32m prettier success!');
|
||||
console.log(chalk.hex('#1890FF')('prettier success!'));
|
||||
|
|
|
|||
|
|
@ -24,7 +24,11 @@ startServer.on('exit', () => {
|
|||
console.log('Starting development server for e2e tests...');
|
||||
startServer.stdout.on('data', data => {
|
||||
console.log(data.toString());
|
||||
if (!once && data.toString().indexOf('Compiled successfully') >= 0) {
|
||||
// hack code , wait umi
|
||||
if (
|
||||
(!once && data.toString().indexOf('Compiled successfully') >= 0) ||
|
||||
data.toString().indexOf('Theme generated successfully') >= 0
|
||||
) {
|
||||
// eslint-disable-next-line
|
||||
once = true;
|
||||
console.log('Development server is started, ready to run tests.');
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user