mirror of
https://github.com/reduxjs/redux-devtools.git
synced 2025-07-27 00:19:55 +03:00
More awesomeness
This commit is contained in:
parent
eaf03c4bf6
commit
72029d8046
|
@ -2,8 +2,8 @@
|
|||
"private": true,
|
||||
"devDependencies": {
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-config-prettier": "^3.6.0",
|
||||
"eslint": "^7.6.0",
|
||||
"eslint-config-prettier": "^6.11.0",
|
||||
"eslint-plugin-babel": "^5.3.1",
|
||||
"eslint-plugin-prettier": "^3.1.4",
|
||||
"eslint-plugin-react": "^7.20.5",
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
"enzyme": "^3.11.0",
|
||||
"enzyme-adapter-react-16": "^1.15.3",
|
||||
"enzyme-to-json": "^3.5.0",
|
||||
"git-url-parse": "^7.2.0",
|
||||
"git-url-parse": "^11.1.2",
|
||||
"jest": "^24.9.0",
|
||||
"jsdom": "^11.12.0",
|
||||
"react": "^16.13.1",
|
||||
|
|
4
packages/react-base16-styling/src/index.js
vendored
4
packages/react-base16-styling/src/index.js
vendored
|
@ -208,7 +208,9 @@ export const getBase16Theme = (theme, base16Themes) => {
|
|||
}
|
||||
}
|
||||
|
||||
return theme && theme.hasOwnProperty('base00') ? theme : undefined;
|
||||
return theme && Object.prototype.hasOwnProperty.call(theme, 'base00')
|
||||
? theme
|
||||
: undefined;
|
||||
};
|
||||
|
||||
export const invertTheme = theme => {
|
||||
|
|
|
@ -48,9 +48,9 @@
|
|||
"enzyme": "^3.11.0",
|
||||
"enzyme-adapter-react-16": "^1.15.3",
|
||||
"enzyme-to-json": "^3.5.0",
|
||||
"file-loader": "^3.0.1",
|
||||
"html-loader": "^0.4.5",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"file-loader": "^6.0.0",
|
||||
"html-loader": "^1.1.0",
|
||||
"html-webpack-plugin": "^4.3.0",
|
||||
"jest": "^24.9.0",
|
||||
"raw-loader": "^1.0.0",
|
||||
"react": "^16.13.1",
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import commitExcessActions from './commitExcessActions';
|
||||
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
export function recompute(
|
||||
previousLiftedState,
|
||||
storeState,
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
"clean-webpack-plugin": "^3.0.0",
|
||||
"cross-env": "^7.0.2",
|
||||
"export-files-webpack-plugin": "^0.0.1",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"html-webpack-plugin": "^4.3.0",
|
||||
"lodash.shuffle": "^4.2.0",
|
||||
"raw-loader": "^1.0.0",
|
||||
"react": "^16.13.1",
|
||||
|
|
|
@ -27,7 +27,6 @@ module.exports = {
|
|||
new HtmlWebpackPlugin({
|
||||
inject: true,
|
||||
template: 'demo/src/index.html',
|
||||
filename: 'index.html',
|
||||
package: pkg
|
||||
}),
|
||||
new webpack.DefinePlugin({
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
"@babel/core": "^7.11.1",
|
||||
"@babel/preset-env": "^7.11.0",
|
||||
"babel-loader": "^8.1.0",
|
||||
"expect": "^1.20.2",
|
||||
"expect": "^26.2.0",
|
||||
"jest": "^24.9.0",
|
||||
"redux": "^4.0.5",
|
||||
"rimraf": "^2.7.1",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"presets": ["@babel/preset-env", "@babel/preset-react"],
|
||||
"plugins": ["@babel/plugin-proposal-class-properties"]
|
||||
"plugins": ["@babel/plugin-proposal-class-properties", "@babel/plugin-proposal-do-expressions"]
|
||||
}
|
||||
|
|
12
packages/redux-devtools-test-generator/demo/src/index.html
Normal file
12
packages/redux-devtools-test-generator/demo/src/index.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
|
||||
<title><%= htmlWebpackPlugin.options.package.name %></title>
|
||||
<meta name="description" content="<%= htmlWebpackPlugin.options.package.description %>">
|
||||
<link href="//maxcdn.bootstrapcdn.com/bootswatch/3.3.5/paper/bootstrap.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
162
packages/redux-devtools-test-generator/demo/src/js/DemoApp.jsx
Normal file
162
packages/redux-devtools-test-generator/demo/src/js/DemoApp.jsx
Normal file
|
@ -0,0 +1,162 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import pkg from '../../../package.json';
|
||||
import { Button, Toolbar, Spacer } from 'devui';
|
||||
import getOptions from './getOptions';
|
||||
import { push as pushRoute } from 'react-router-redux';
|
||||
|
||||
const styles = {
|
||||
wrapper: {
|
||||
height: '100vh',
|
||||
width: '450px',
|
||||
margin: 'auto',
|
||||
textAlign: 'center'
|
||||
},
|
||||
muted: {
|
||||
color: '#CCCCCC'
|
||||
},
|
||||
link: {
|
||||
margin: '0 0.5rem',
|
||||
cursor: 'pointer',
|
||||
display: 'block'
|
||||
}
|
||||
};
|
||||
|
||||
const ROOT = '/'; // process.env.NODE_ENV === 'production' ? '/' : '/';
|
||||
|
||||
function buildUrl(options) {
|
||||
return `${ROOT}?` + [
|
||||
options.useExtension ? 'ext' : '',
|
||||
options.theme ? 'theme=' + options.theme : '',
|
||||
options.dark ? 'dark' : ''
|
||||
].filter(s => s).join('&');
|
||||
}
|
||||
|
||||
class DemoApp extends React.Component {
|
||||
render() {
|
||||
const options = getOptions();
|
||||
|
||||
return (
|
||||
<div style={styles.wrapper}>
|
||||
<h3>
|
||||
{pkg.name || <span style={styles.muted}>Package Name</span>}
|
||||
</h3>
|
||||
<h5>{pkg.description || <span style={styles.muted}>Package Description</span>}</h5>
|
||||
<Toolbar>
|
||||
<Spacer />
|
||||
<Button onClick={this.props.increment}>
|
||||
Increment
|
||||
</Button>
|
||||
<Button onClick={this.props.push}>
|
||||
Push
|
||||
</Button>
|
||||
<Button onClick={this.props.pop}>
|
||||
Pop
|
||||
</Button>
|
||||
<Button onClick={this.props.replace}>
|
||||
Replace
|
||||
</Button>
|
||||
<Button onClick={this.props.changeNested}>
|
||||
Change Nested
|
||||
</Button>
|
||||
<Spacer />
|
||||
</Toolbar>
|
||||
<Toolbar>
|
||||
<Spacer />
|
||||
<Button onClick={this.props.pushHugeArray}>
|
||||
Push Huge Array
|
||||
</Button>
|
||||
<Button onClick={this.props.addHugeObect}>
|
||||
Add Huge Object
|
||||
</Button>
|
||||
<Button onClick={this.props.hugePayload}>
|
||||
Huge Payload
|
||||
</Button>
|
||||
<Spacer />
|
||||
</Toolbar>
|
||||
<Toolbar>
|
||||
<Spacer />
|
||||
<Button onClick={this.props.addIterator}>
|
||||
Add Iterator
|
||||
</Button>
|
||||
<Button onClick={this.props.addImmutableMap}>
|
||||
Add Immutable Map
|
||||
</Button>
|
||||
<Button onClick={this.props.changeImmutableNested}>
|
||||
Change Immutable Nested
|
||||
</Button>
|
||||
<Spacer />
|
||||
</Toolbar>
|
||||
<Toolbar>
|
||||
<Spacer />
|
||||
<Button onClick={this.props.addRecursive}>
|
||||
Add Recursive
|
||||
</Button>
|
||||
<Button onClick={this.props.addFunction}>
|
||||
Add Function
|
||||
</Button>
|
||||
<Button onClick={this.props.addSymbol}>
|
||||
Add Symbol
|
||||
</Button>
|
||||
<Spacer />
|
||||
</Toolbar>
|
||||
<Toolbar>
|
||||
<Spacer />
|
||||
<Button onClick={this.toggleTimeoutUpdate}>
|
||||
Timeout Update {this.props.timeoutUpdateEnabled ? 'On' : 'Off'}
|
||||
</Button>
|
||||
<Button onClick={this.props.shuffleArray}>
|
||||
Shuffle Array
|
||||
</Button>
|
||||
<Spacer />
|
||||
</Toolbar>
|
||||
<div>
|
||||
{options.useExtension ?
|
||||
<a href={`${ROOT}`} style={styles.link}>Disable browser extension</a> :
|
||||
<a href={`${ROOT}?ext`} style={styles.link}>Use browser extension</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
toggleTimeoutUpdate = () => {
|
||||
const enabled = !this.props.timeoutUpdateEnabled;
|
||||
this.props.toggleTimeoutUpdate(enabled);
|
||||
|
||||
if (enabled) {
|
||||
this.timeout = setInterval(this.props.timeoutUpdate, 1000);
|
||||
} else {
|
||||
clearTimeout(this.timeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => state,
|
||||
{
|
||||
toggleTimeoutUpdate: timeoutUpdateEnabled => ({
|
||||
type: 'TOGGLE_TIMEOUT_UPDATE', timeoutUpdateEnabled
|
||||
}),
|
||||
timeoutUpdate: () => ({ type: 'TIMEOUT_UPDATE' }),
|
||||
increment: () => ({ type: 'INCREMENT' }),
|
||||
push: () => ({ type: 'PUSH' }),
|
||||
pop: () => ({ type: 'POP' }),
|
||||
replace: () => ({ type: 'REPLACE' }),
|
||||
changeNested: () => ({ type: 'CHANGE_NESTED' }),
|
||||
pushHugeArray: () => ({ type: 'PUSH_HUGE_ARRAY' }),
|
||||
addIterator: () => ({ type: 'ADD_ITERATOR' }),
|
||||
addHugeObect: () => ({ type: 'ADD_HUGE_OBJECT' }),
|
||||
addRecursive: () => ({ type: 'ADD_RECURSIVE' }),
|
||||
addImmutableMap: () => ({ type: 'ADD_IMMUTABLE_MAP' }),
|
||||
changeImmutableNested: () => ({ type: 'CHANGE_IMMUTABLE_NESTED' }),
|
||||
hugePayload: () => ({
|
||||
type: 'HUGE_PAYLOAD',
|
||||
payload: Array.from({ length: 10000 }).map((_, i) => i)
|
||||
}),
|
||||
addFunction: () => ({ type: 'ADD_FUNCTION' }),
|
||||
addSymbol: () => ({ type: 'ADD_SYMBOL' }),
|
||||
shuffleArray: () => ({ type: 'SHUFFLE_ARRAY' }),
|
||||
pushRoute
|
||||
}
|
||||
)(DemoApp);
|
|
@ -0,0 +1,11 @@
|
|||
export default function getOptions() {
|
||||
return {
|
||||
useExtension: window.location.search.indexOf('ext') !== -1,
|
||||
supportImmutable: window.location.search.indexOf('immutable') !== -1,
|
||||
theme: do {
|
||||
const match = window.location.search.match(/theme=([^&]+)/);
|
||||
match ? match[1] : 'inspector'
|
||||
},
|
||||
dark: window.location.search.indexOf('dark') !== -1
|
||||
};
|
||||
}
|
111
packages/redux-devtools-test-generator/demo/src/js/index.js
Normal file
111
packages/redux-devtools-test-generator/demo/src/js/index.js
Normal file
|
@ -0,0 +1,111 @@
|
|||
import '@babel/polyfill';
|
||||
import 'devui/lib/presets';
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { Container } from 'devui';
|
||||
import DemoApp from './DemoApp';
|
||||
import { Provider } from 'react-redux';
|
||||
import reducers from './reducers';
|
||||
import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
|
||||
import createLogger from 'redux-logger';
|
||||
import { Router, Route, browserHistory } from 'react-router';
|
||||
import {
|
||||
syncHistoryWithStore,
|
||||
routerReducer,
|
||||
routerMiddleware
|
||||
} from 'react-router-redux';
|
||||
import { createDevTools, persistState } from 'redux-devtools';
|
||||
import DevtoolsInspector from 'redux-devtools-inspector';
|
||||
import DockMonitor from 'redux-devtools-dock-monitor';
|
||||
import getOptions from './getOptions';
|
||||
import TestGenerator from '../../../src';
|
||||
|
||||
function getDebugSessionKey() {
|
||||
const matches = window.location.href.match(/[?&]debug_session=([^&#]+)\b/);
|
||||
return matches && matches.length > 0 ? matches[1] : null;
|
||||
}
|
||||
|
||||
const getDevTools = options =>
|
||||
createDevTools(
|
||||
<DockMonitor
|
||||
defaultIsVisible
|
||||
toggleVisibilityKey="ctrl-h"
|
||||
changePositionKey="ctrl-q"
|
||||
changeMonitorKey="ctrl-m"
|
||||
>
|
||||
<DevtoolsInspector
|
||||
theme={options.theme}
|
||||
shouldPersistState
|
||||
invertTheme={!options.dark}
|
||||
supportImmutable={options.supportImmutable}
|
||||
tabs={defaultTabs => [
|
||||
{
|
||||
name: 'Test',
|
||||
component: TestGenerator
|
||||
},
|
||||
...defaultTabs
|
||||
]}
|
||||
/>
|
||||
</DockMonitor>
|
||||
);
|
||||
|
||||
const ROOT =
|
||||
process.env.NODE_ENV === 'production' ? '/redux-devtools-inspector/' : '/';
|
||||
|
||||
let DevTools = getDevTools(getOptions());
|
||||
|
||||
const reduxRouterMiddleware = routerMiddleware(browserHistory);
|
||||
|
||||
const enhancer = compose(
|
||||
applyMiddleware(createLogger(), reduxRouterMiddleware),
|
||||
(...args) => {
|
||||
const useDevtoolsExtension =
|
||||
!!window.__REDUX_DEVTOOLS_EXTENSION__ && getOptions().useExtension;
|
||||
const instrument = useDevtoolsExtension
|
||||
? window.__REDUX_DEVTOOLS_EXTENSION__()
|
||||
: DevTools.instrument();
|
||||
return instrument(...args);
|
||||
},
|
||||
persistState(getDebugSessionKey())
|
||||
);
|
||||
|
||||
const store = createStore(
|
||||
combineReducers({
|
||||
...reducers,
|
||||
routing: routerReducer
|
||||
}),
|
||||
{},
|
||||
enhancer
|
||||
);
|
||||
|
||||
const history = syncHistoryWithStore(browserHistory, store);
|
||||
|
||||
const handleRouterUpdate = () => {
|
||||
renderApp(getOptions());
|
||||
};
|
||||
|
||||
const router = (
|
||||
<Router history={history} onUpdate={handleRouterUpdate}>
|
||||
<Route path={ROOT} component={DemoApp} />
|
||||
</Router>
|
||||
);
|
||||
|
||||
const renderApp = options => {
|
||||
DevTools = getDevTools(options);
|
||||
const useDevtoolsExtension =
|
||||
!!window.__REDUX_DEVTOOLS_EXTENSION__ && options.useExtension;
|
||||
|
||||
return render(
|
||||
<Provider store={store}>
|
||||
<Container
|
||||
themeData={{ theme: 'default', scheme: 'default', light: true }}
|
||||
>
|
||||
{router}
|
||||
{!useDevtoolsExtension && <DevTools />}
|
||||
</Container>
|
||||
</Provider>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
};
|
||||
|
||||
renderApp(getOptions());
|
103
packages/redux-devtools-test-generator/demo/src/js/reducers.js
Normal file
103
packages/redux-devtools-test-generator/demo/src/js/reducers.js
Normal file
|
@ -0,0 +1,103 @@
|
|||
import Immutable from 'immutable';
|
||||
import shuffle from 'lodash.shuffle';
|
||||
|
||||
const NESTED = {
|
||||
long: {
|
||||
nested: [{
|
||||
path: {
|
||||
to: {
|
||||
a: 'key'
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
const IMMUTABLE_NESTED = Immutable.fromJS(NESTED);
|
||||
|
||||
/* eslint-disable babel/new-cap */
|
||||
|
||||
const IMMUTABLE_MAP = Immutable.Map({
|
||||
map: Immutable.Map({ a:1, b: 2, c: 3 }),
|
||||
list: Immutable.List(['a', 'b', 'c']),
|
||||
set: Immutable.Set(['a', 'b', 'c']),
|
||||
stack: Immutable.Stack(['a', 'b', 'c']),
|
||||
seq: Immutable.Seq.of(1, 2, 3, 4, 5, 6, 7, 8)
|
||||
});
|
||||
|
||||
/* eslint-enable babel/new-cap */
|
||||
|
||||
const HUGE_ARRAY = Array.from({ length: 5000 })
|
||||
.map((_, key) => ({ str: 'key ' + key }));
|
||||
|
||||
const HUGE_OBJECT = Array.from({ length: 5000 })
|
||||
.reduce((o, _, key) => (o['key ' + key] = 'item ' + key, o), {});
|
||||
|
||||
const FUNC = function (a, b, c) { return a + b + c; };
|
||||
|
||||
const RECURSIVE = {};
|
||||
RECURSIVE.obj = RECURSIVE;
|
||||
|
||||
function createIterator() {
|
||||
const iterable = {};
|
||||
iterable[window.Symbol.iterator] = function *iterator() {
|
||||
for (var i = 0; i < 333; i++) {
|
||||
yield 'item ' + i;
|
||||
}
|
||||
}
|
||||
|
||||
return iterable;
|
||||
}
|
||||
|
||||
const DEFAULT_SHUFFLE_ARRAY = [0, 1, null, { id: 1 }, { id: 2 }, 'string'];
|
||||
|
||||
export default {
|
||||
timeoutUpdateEnabled: (state=false, action) => action.type === 'TOGGLE_TIMEOUT_UPDATE' ?
|
||||
action.timeoutUpdateEnabled : state,
|
||||
store: (state=0, action) => action.type === 'INCREMENT' ? state + 1 : state,
|
||||
undefined: (state={ val: undefined }) => state,
|
||||
null: (state=null) => state,
|
||||
func: (state=() => {}) => state,
|
||||
array: (state=[], action) => action.type === 'PUSH' ?
|
||||
[...state, Math.random()] : (
|
||||
action.type === 'POP' ? state.slice(0, state.length - 1) : (
|
||||
action.type === 'REPLACE' ? [Math.random(), ...state.slice(1)] : state
|
||||
)
|
||||
),
|
||||
hugeArrays: (state=[], action) => action.type === 'PUSH_HUGE_ARRAY' ?
|
||||
[ ...state, ...HUGE_ARRAY ] : state,
|
||||
hugeObjects: (state=[], action) => action.type === 'ADD_HUGE_OBJECT' ?
|
||||
[ ...state, HUGE_OBJECT ] : state,
|
||||
iterators: (state=[], action) => action.type === 'ADD_ITERATOR' ?
|
||||
[...state, createIterator()] : state,
|
||||
nested: (state=NESTED, action) =>
|
||||
action.type === 'CHANGE_NESTED' ?
|
||||
{
|
||||
...state,
|
||||
long: {
|
||||
nested: [{
|
||||
path: {
|
||||
to: {
|
||||
a: state.long.nested[0].path.to.a + '!'
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
} : state,
|
||||
recursive: (state=[], action) => action.type === 'ADD_RECURSIVE' ?
|
||||
[...state, { ...RECURSIVE }] : state,
|
||||
immutables: (state=[], action) => action.type === 'ADD_IMMUTABLE_MAP' ?
|
||||
[...state, IMMUTABLE_MAP] : state,
|
||||
immutableNested: (state=IMMUTABLE_NESTED, action) => action.type === 'CHANGE_IMMUTABLE_NESTED' ?
|
||||
state.updateIn(
|
||||
['long', 'nested', 0, 'path', 'to', 'a'],
|
||||
str => str + '!'
|
||||
) : state,
|
||||
addFunction: (state=null, action) => action.type === 'ADD_FUNCTION' ?
|
||||
{ f: FUNC } : state,
|
||||
addSymbol: (state=null, action) => action.type === 'ADD_SYMBOL' ?
|
||||
{ s: window.Symbol('symbol') } : state,
|
||||
shuffleArray: (state=DEFAULT_SHUFFLE_ARRAY, action) =>
|
||||
action.type === 'SHUFFLE_ARRAY' ?
|
||||
shuffle(state) : state
|
||||
};
|
|
@ -38,6 +38,8 @@
|
|||
"@babel/cli": "^7.10.5",
|
||||
"@babel/core": "^7.11.1",
|
||||
"@babel/plugin-proposal-class-properties": "^7.10.4",
|
||||
"@babel/plugin-proposal-do-expressions": "^7.10.4",
|
||||
"@babel/polyfill": "^7.10.4",
|
||||
"@babel/preset-env": "^7.11.0",
|
||||
"@babel/preset-react": "^7.10.4",
|
||||
"babel-loader": "^8.1.0",
|
||||
|
@ -46,10 +48,10 @@
|
|||
"enzyme": "^3.11.0",
|
||||
"enzyme-adapter-react-16": "^1.15.3",
|
||||
"enzyme-to-json": "^3.5.0",
|
||||
"expect": "^1.20.2",
|
||||
"expect": "^26.2.0",
|
||||
"export-files-webpack-plugin": "^0.0.1",
|
||||
"file-loader": "^3.0.1",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"file-loader": "^6.0.0",
|
||||
"html-webpack-plugin": "^4.3.0",
|
||||
"jest": "^24.9.0",
|
||||
"lodash.shuffle": "^4.2.0",
|
||||
"react-dom": "^16.13.1",
|
||||
|
@ -63,7 +65,9 @@
|
|||
"redux-logger": "^2.10.2",
|
||||
"rimraf": "^2.7.1",
|
||||
"style-loader": "^0.13.2",
|
||||
"url-loader": "^1.1.2"
|
||||
"url-loader": "^1.1.2",
|
||||
"webpack": "^4.44.1",
|
||||
"webpack-dev-server": "^3.11.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"devui": "^1.0.0-4",
|
||||
|
|
|
@ -26,7 +26,6 @@ module.exports = {
|
|||
new HtmlWebpackPlugin({
|
||||
inject: true,
|
||||
template: 'demo/src/index.html',
|
||||
filename: 'index.html',
|
||||
package: pkg
|
||||
}),
|
||||
new webpack.DefinePlugin({
|
||||
|
|
|
@ -36,7 +36,7 @@ function removeCss(document: Document, ref: number) {
|
|||
function applyStyles(element: HTMLElement, styles: Object) {
|
||||
element.setAttribute('style', '');
|
||||
for (const key in styles) {
|
||||
if (!styles.hasOwnProperty(key)) {
|
||||
if (!Object.prototype.hasOwnProperty.call(styles, key)) {
|
||||
continue;
|
||||
}
|
||||
// $FlowFixMe
|
||||
|
|
Loading…
Reference in New Issue
Block a user