mirror of
https://github.com/haiwen/seahub.git
synced 2025-08-01 23:38:37 +00:00
Wiki add new page below or above (#7118)
* Wiki add new page below or above * change var name * remove sibling page * update create wiki page logic * fix page does not exist --------- Co-authored-by: 孙永强 <11704063+s-yongqiang@user.noreply.gitee.com>
This commit is contained in:
parent
51d5706aa6
commit
d63b68e8ab
@ -132,7 +132,7 @@
|
||||
background-color: #DFDFDD;
|
||||
}
|
||||
|
||||
.wiki-nav .wiki-page-item .sf3-font.sf3-font-enlarge,
|
||||
.wiki-nav .wiki-page-item .wiki-add-page-btn .sf3-font.sf3-font-enlarge,
|
||||
.wiki-nav .wiki-page-item .more-wiki-page-operation .seafile-multicolor-icon-more-level {
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
@ -270,13 +270,13 @@
|
||||
}
|
||||
|
||||
.wiki-nav,
|
||||
.wiki-nav .wiki-page-item .sf3-font.sf3-font-enlarge:hover,
|
||||
.wiki-nav .wiki-page-item .wiki-add-page-btn .sf3-font.sf3-font-enlarge:hover,
|
||||
.wiki-nav .wiki-page-item .seafile-multicolor-icon-more-level:hover {
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.wiki-nav .wiki2-trash .sf3-font,
|
||||
.wiki-nav .wiki-page-item .sf3-font.sf3-font-enlarge,
|
||||
.wiki-nav .wiki-page-item .wiki-add-page-btn .sf3-font.sf3-font-enlarge,
|
||||
.wiki-nav .wiki-page-item .seafile-multicolor-icon-more-level {
|
||||
color: #666;
|
||||
}
|
||||
|
@ -83,16 +83,16 @@ class SidePanel extends PureComponent {
|
||||
});
|
||||
};
|
||||
|
||||
addPage = (page, parentId, successCallback, errorCallback, jumpToNewPage = true) => {
|
||||
addPage = (page, parent_id, successCallback, errorCallback, jumpToNewPage = true) => {
|
||||
const { config } = this.props;
|
||||
const navigation = config.navigation;
|
||||
const pageId = page.id;
|
||||
const page_id = page.id;
|
||||
config.pages.push(page);
|
||||
PageUtils.addPage(navigation, pageId, parentId);
|
||||
PageUtils.addPage({ navigation, page_id, parent_id });
|
||||
config.navigation = navigation;
|
||||
JSON.stringify(config);
|
||||
this.props.updateWikiConfig(config);
|
||||
jumpToNewPage && this.props.setCurrentPage(pageId, successCallback);
|
||||
jumpToNewPage && this.props.setCurrentPage(page_id, successCallback);
|
||||
successCallback && successCallback();
|
||||
};
|
||||
|
||||
@ -104,6 +104,19 @@ class SidePanel extends PureComponent {
|
||||
this.props.updateWikiConfig(config);
|
||||
};
|
||||
|
||||
addSiblingPage = (page, parent_id, insert_position, sibling_page_id, successCallback) => {
|
||||
const { config } = this.props;
|
||||
const navigation = config.navigation;
|
||||
const page_id = page.page_id;
|
||||
config.pages.push(page);
|
||||
PageUtils.addPage({ navigation, page_id, parent_id, insert_position, sibling_page_id });
|
||||
config.navigation = navigation;
|
||||
JSON.stringify(config);
|
||||
this.props.updateWikiConfig(config);
|
||||
this.props.setCurrentPage(page_id, successCallback);
|
||||
successCallback && successCallback();
|
||||
};
|
||||
|
||||
toggleTrashDialog = () => {
|
||||
this.setState({ isShowTrashDialog: !this.state.isShowTrashDialog });
|
||||
};
|
||||
@ -128,6 +141,7 @@ class SidePanel extends PureComponent {
|
||||
getCurrentPageId={this.props.getCurrentPageId}
|
||||
addPageInside={this.addPageInside}
|
||||
toggleTrashDialog={this.toggleTrashDialog}
|
||||
addSiblingPage={this.addSiblingPage}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
@ -6,14 +6,16 @@ import { Utils } from '../../../utils/utils';
|
||||
import toaster from '../../../components/toast';
|
||||
import Loading from '../../../components/loading';
|
||||
import wikiAPI from '../../../utils/wiki-api';
|
||||
import { INSERT_POSITION } from './constants';
|
||||
|
||||
import '../css/add-new-page-dialog.css';
|
||||
|
||||
const propTypes = {
|
||||
page: PropTypes.object,
|
||||
title: PropTypes.node,
|
||||
toggle: PropTypes.func.isRequired,
|
||||
onAddNewPage: PropTypes.func,
|
||||
getCurrentPageId: PropTypes.func.isRequired,
|
||||
insertPosition: PropTypes.string,
|
||||
};
|
||||
|
||||
|
||||
@ -71,9 +73,11 @@ class AddNewPageDialog extends React.Component {
|
||||
};
|
||||
|
||||
createPage = (pageName) => {
|
||||
wikiAPI.createWiki2Page(wikiId, pageName, this.props.getCurrentPageId()).then(res => {
|
||||
const { insertPosition, page } = this.props;
|
||||
wikiAPI.createWiki2Page(wikiId, pageName, page.id, insertPosition).then(res => {
|
||||
const { page_id, obj_name, doc_uuid, parent_dir } = res.data.file_info;
|
||||
this.props.onAddNewPage({
|
||||
id: page_id,
|
||||
page_id: page_id,
|
||||
name: pageName,
|
||||
icon: '',
|
||||
@ -126,4 +130,8 @@ class AddNewPageDialog extends React.Component {
|
||||
|
||||
AddNewPageDialog.propTypes = propTypes;
|
||||
|
||||
AddNewPageDialog.defaultProps = {
|
||||
insertPosition: INSERT_POSITION.INNER,
|
||||
};
|
||||
|
||||
export default AddNewPageDialog;
|
||||
|
5
frontend/src/pages/wiki2/wiki-nav/constants.js
Normal file
5
frontend/src/pages/wiki2/wiki-nav/constants.js
Normal file
@ -0,0 +1,5 @@
|
||||
export const INSERT_POSITION = {
|
||||
ABOVE: 'above',
|
||||
BELOW: 'below',
|
||||
INNER: 'inner',
|
||||
};
|
@ -1,3 +1,5 @@
|
||||
import { INSERT_POSITION } from './constants';
|
||||
|
||||
class NewPage {
|
||||
constructor(id) {
|
||||
this.id = id;
|
||||
@ -8,26 +10,52 @@ class NewPage {
|
||||
|
||||
export default class PageUtils {
|
||||
|
||||
static addPage(navigation, page_id, parentId) {
|
||||
if (!parentId) {
|
||||
navigation.push(new NewPage(page_id));
|
||||
static addPage({ navigation, page_id, parent_id, insert_position, sibling_page_id }) {
|
||||
if (!parent_id) {
|
||||
const newPage = new NewPage(page_id);
|
||||
if (sibling_page_id) {
|
||||
let insertIndex = navigation.findIndex(item => item.id === sibling_page_id);
|
||||
if (insertIndex > -1) {
|
||||
if (insert_position === INSERT_POSITION.ABOVE) {
|
||||
insertIndex -= 1;
|
||||
}
|
||||
navigation.splice(insertIndex + 1, 0, newPage);
|
||||
} else {
|
||||
navigation.push(newPage);
|
||||
}
|
||||
} else {
|
||||
navigation.push(newPage);
|
||||
}
|
||||
} else {
|
||||
navigation.forEach(item => {
|
||||
this._addPageRecursion(page_id, item, parentId);
|
||||
this._addPageRecursion({ page_id, item, parent_id, insert_position, sibling_page_id });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static _addPageRecursion(page_id, item, parentId) {
|
||||
static _addPageRecursion({ page_id, item, parent_id, insert_position, sibling_page_id }) {
|
||||
if (!Array.isArray(item.children)) {
|
||||
item.children = [];
|
||||
}
|
||||
if (item.id === parentId) {
|
||||
item.children.push(new NewPage(page_id));
|
||||
if (item.id === parent_id) {
|
||||
const newPage = new NewPage(page_id);
|
||||
if (sibling_page_id) {
|
||||
let insertIndex = item.children.findIndex(item => item.id === sibling_page_id);
|
||||
if (insertIndex > -1) {
|
||||
if (insert_position === INSERT_POSITION.ABOVE) {
|
||||
insertIndex -= 1;
|
||||
}
|
||||
item.children.splice(insertIndex + 1, 0, newPage);
|
||||
} else {
|
||||
item.children.push(newPage);
|
||||
}
|
||||
} else {
|
||||
item.children.push(newPage);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
item.children && item.children.forEach(item => {
|
||||
this._addPageRecursion(page_id, item, parentId);
|
||||
this._addPageRecursion({ page_id, item, parent_id, insert_position, sibling_page_id });
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap
|
||||
import toaster from '../../../../components/toast';
|
||||
import { gettext } from '../../../../utils/constants';
|
||||
import { getWikPageLink } from '../../utils';
|
||||
import { INSERT_POSITION } from '../constants';
|
||||
|
||||
export default class PageDropdownMenu extends Component {
|
||||
|
||||
@ -13,6 +14,7 @@ export default class PageDropdownMenu extends Component {
|
||||
pagesLength: PropTypes.number,
|
||||
toggle: PropTypes.func,
|
||||
toggleNameEditor: PropTypes.func,
|
||||
toggleInsertSiblingPage: PropTypes.func,
|
||||
duplicatePage: PropTypes.func,
|
||||
onDeletePage: PropTypes.func,
|
||||
isOnlyOnePage: PropTypes.bool,
|
||||
@ -46,6 +48,14 @@ export default class PageDropdownMenu extends Component {
|
||||
this.props.onDeletePage();
|
||||
};
|
||||
|
||||
addPageAbove = () => {
|
||||
this.props.toggleInsertSiblingPage(INSERT_POSITION.ABOVE);
|
||||
};
|
||||
|
||||
addPageBelow = () => {
|
||||
this.props.toggleInsertSiblingPage(INSERT_POSITION.BELOW);
|
||||
};
|
||||
|
||||
duplicatePage = () => {
|
||||
const { page } = this.props;
|
||||
this.props.duplicatePage({ from_page_id: page.id }, () => {}, this.duplicatePageFailure);
|
||||
@ -98,6 +108,14 @@ export default class PageDropdownMenu extends Component {
|
||||
<i className="sf3-font sf3-font-rename" />
|
||||
<span className="item-text">{gettext('Modify name')}</span>
|
||||
</DropdownItem>
|
||||
<DropdownItem onClick={this.addPageAbove}>
|
||||
<i className="sf3-font sf3-font-enlarge" />
|
||||
<span className="item-text">{gettext('Add page above')}</span>
|
||||
</DropdownItem>
|
||||
<DropdownItem onClick={this.addPageBelow}>
|
||||
<i className="sf3-font sf3-font-enlarge" />
|
||||
<span className="item-text">{gettext('Add page below')}</span>
|
||||
</DropdownItem>
|
||||
<DropdownItem onClick={this.duplicatePage}>
|
||||
<i className="sf3-font sf3-font-copy1" />
|
||||
<span className="item-text">{gettext('Duplicate page')}</span>
|
||||
|
@ -11,6 +11,7 @@ import Icon from '../../../../components/icon';
|
||||
import DraggedPageItem from './dragged-page-item';
|
||||
import CustomIcon from '../../custom-icon';
|
||||
import { eventBus } from '../../../../components/common/event-bus';
|
||||
import { INSERT_POSITION } from '../constants';
|
||||
|
||||
class PageItem extends Component {
|
||||
|
||||
@ -21,6 +22,8 @@ class PageItem extends Component {
|
||||
isShowOperationDropdown: false,
|
||||
isShowDeleteDialog: false,
|
||||
isShowInsertPage: false,
|
||||
isShowAddSiblingPage: false,
|
||||
insertPosition: '',
|
||||
pageName: props.page.name || '',
|
||||
isSelected: props.getCurrentPageId() === props.page.id,
|
||||
isMouseEnter: false,
|
||||
@ -73,6 +76,17 @@ class PageItem extends Component {
|
||||
this.setState({ isShowInsertPage: !this.state.isShowInsertPage });
|
||||
};
|
||||
|
||||
toggleInsertSiblingPage = (position) => {
|
||||
let insertPosition = null;
|
||||
if (position === INSERT_POSITION.BELOW || position === INSERT_POSITION.ABOVE) {
|
||||
insertPosition = position;
|
||||
}
|
||||
this.setState({
|
||||
insertPosition,
|
||||
isShowAddSiblingPage: !this.state.isShowAddSiblingPage,
|
||||
});
|
||||
};
|
||||
|
||||
savePageProperties = () => {
|
||||
const { name, id } = this.props.page;
|
||||
const pageName = this.state.pageName.trim();
|
||||
@ -134,6 +148,7 @@ class PageItem extends Component {
|
||||
isOnlyOnePage={isOnlyOnePage}
|
||||
page={Object.assign({}, pages.find(item => item.id === id), page)}
|
||||
pageIndex={index}
|
||||
parentPageId={this.props.page.id}
|
||||
isEditMode={isEditMode}
|
||||
duplicatePage={this.props.duplicatePage}
|
||||
setCurrentPage={this.props.setCurrentPage}
|
||||
@ -150,6 +165,7 @@ class PageItem extends Component {
|
||||
setClassName={this.props.setClassName}
|
||||
getClassName={this.props.getClassName}
|
||||
layerDragProps={this.props.layerDragProps}
|
||||
addSiblingPage={this.props.addSiblingPage}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -176,6 +192,11 @@ class PageItem extends Component {
|
||||
this.props.addPageInside(Object.assign({ parentPageId: this.props.page.id }, newPage));
|
||||
};
|
||||
|
||||
onAddSiblingPage = (newPage) => {
|
||||
const { page } = this.props;
|
||||
this.props.addSiblingPage(newPage, this.props.parentPageId, this.state.insertPosition, page.id, this.toggleInsertSiblingPage);
|
||||
};
|
||||
|
||||
getPageClassName = () => {
|
||||
const { isOver, canDrop, isEditMode, layerDragProps } = this.props;
|
||||
const isOverPage = isOver && canDrop;
|
||||
@ -257,6 +278,7 @@ class PageItem extends Component {
|
||||
toggleNameEditor={this.toggleNameEditor}
|
||||
duplicatePage={this.props.duplicatePage}
|
||||
onDeletePage={this.openDeleteDialog}
|
||||
toggleInsertSiblingPage={this.toggleInsertSiblingPage}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
@ -275,9 +297,18 @@ class PageItem extends Component {
|
||||
{this.state.isShowInsertPage &&
|
||||
<AddNewPageDialog
|
||||
toggle={this.toggleInsertPage}
|
||||
getCurrentPageId={this.props.getCurrentPageId}
|
||||
onAddNewPage={this.onAddNewPage}
|
||||
title={gettext('Add page inside')}
|
||||
page={this.props.page}
|
||||
/>
|
||||
}
|
||||
{this.state.isShowAddSiblingPage &&
|
||||
<AddNewPageDialog
|
||||
toggle={this.toggleInsertSiblingPage}
|
||||
onAddNewPage={this.onAddSiblingPage}
|
||||
title={gettext('Add page')}
|
||||
insertPosition={this.state.insertPosition}
|
||||
page={this.props.page}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
@ -19,6 +19,7 @@ class WikiNav extends Component {
|
||||
onDeletePage: PropTypes.func,
|
||||
onMovePage: PropTypes.func,
|
||||
duplicatePage: PropTypes.func,
|
||||
addSiblingPage: PropTypes.func,
|
||||
getCurrentPageId: PropTypes.func,
|
||||
addPageInside: PropTypes.func,
|
||||
updateWikiConfig: PropTypes.func.isRequired,
|
||||
@ -91,6 +92,7 @@ class WikiNav extends Component {
|
||||
pathStr={page.id}
|
||||
getCurrentPageId={this.props.getCurrentPageId}
|
||||
addPageInside={this.props.addPageInside}
|
||||
addSiblingPage={this.props.addSiblingPage}
|
||||
getFoldState={this.getFoldState}
|
||||
toggleExpand={this.toggleExpand}
|
||||
id_page_map={id_page_map}
|
||||
|
@ -181,13 +181,16 @@ class WikiAPI {
|
||||
return this.req.get(url);
|
||||
}
|
||||
|
||||
createWiki2Page(wikiId, pageName, currentId) {
|
||||
createWiki2Page(wikiId, pageName, currentId, insertPosition) {
|
||||
const url = this.server + '/api/v2.1/wiki2/' + wikiId + '/pages/';
|
||||
let form = new FormData();
|
||||
form.append('page_name', pageName);
|
||||
if (currentId) {
|
||||
form.append('current_id', currentId);
|
||||
}
|
||||
if (insertPosition) {
|
||||
form.append('insert_position', insertPosition);
|
||||
}
|
||||
return this._sendPostRequest(url, form);
|
||||
}
|
||||
|
||||
|
@ -500,6 +500,12 @@ class Wiki2PagesView(APIView):
|
||||
error_msg = 'page_name invalid.'
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||
|
||||
current_id = request.data.get('current_id', None)
|
||||
insert_position = request.data.get('insert_position', None)
|
||||
positions = ['above', 'below', 'inner']
|
||||
if insert_position and insert_position not in positions:
|
||||
error_msg = 'insert_position invalid.'
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||
|
||||
wiki = Wiki.objects.get(wiki_id=wiki_id)
|
||||
if not wiki:
|
||||
@ -514,16 +520,15 @@ class Wiki2PagesView(APIView):
|
||||
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||
|
||||
repo_id = wiki.repo_id
|
||||
|
||||
# resource check
|
||||
repo = seafile_api.get_repo(repo_id)
|
||||
if not repo:
|
||||
error_msg = 'Library %s not found.' % repo_id
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
|
||||
current_id = request.data.get('current_id', None)
|
||||
|
||||
wiki_config = get_wiki_config(repo_id, request.user.username)
|
||||
navigation = wiki_config.get('navigation', [])
|
||||
# side panel create Untitled page
|
||||
if not current_id:
|
||||
page_ids = {element.get('id') for element in navigation if element.get('type') != 'folder'}
|
||||
else:
|
||||
@ -537,22 +542,6 @@ class Wiki2PagesView(APIView):
|
||||
new_file_name = page_name + '.sdoc'
|
||||
parent_dir = os.path.join(WIKI_PAGES_DIR, str(sdoc_uuid))
|
||||
path = os.path.join(parent_dir, new_file_name)
|
||||
seafile_api.mkdir_with_parents(repo_id, '/', parent_dir.strip('/'), request.user.username)
|
||||
# create new empty file
|
||||
if not is_valid_dirent_name(new_file_name):
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, 'name invalid.')
|
||||
|
||||
try:
|
||||
seafile_api.post_empty_file(repo_id, parent_dir, new_file_name, request.user.username)
|
||||
except Exception as e:
|
||||
if str(e) == 'Too many files in library.':
|
||||
error_msg = _("The number of files in library exceeds the limit")
|
||||
return api_error(HTTP_447_TOO_MANY_FILES_IN_LIBRARY, error_msg)
|
||||
else:
|
||||
logger.error(e)
|
||||
error_msg = 'Internal Server Error'
|
||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||
|
||||
new_file_path = posixpath.join(parent_dir, new_file_name)
|
||||
file_info = self.get_file_info(repo_id, new_file_path)
|
||||
file_info['doc_uuid'] = sdoc_uuid
|
||||
@ -565,7 +554,27 @@ class Wiki2PagesView(APIView):
|
||||
id_set = get_all_wiki_ids(navigation)
|
||||
new_page_id = gen_unique_id(id_set)
|
||||
file_info['page_id'] = new_page_id
|
||||
gen_new_page_nav_by_id(navigation, new_page_id, current_id)
|
||||
is_find = [False]
|
||||
gen_new_page_nav_by_id(navigation, new_page_id, current_id, insert_position, is_find)
|
||||
if not is_find[0]:
|
||||
error_msg = 'Current page does not exist'
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
# create new empty file
|
||||
seafile_api.mkdir_with_parents(repo_id, '/', parent_dir.strip('/'), request.user.username)
|
||||
if not is_valid_dirent_name(new_file_name):
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, 'name invalid.')
|
||||
|
||||
try:
|
||||
seafile_api.post_empty_file(repo_id, parent_dir, new_file_name, request.user.username)
|
||||
except Exception as e:
|
||||
if str(e) == 'Too many files in library.':
|
||||
error_msg = _("The number of files in library exceeds the limit")
|
||||
return api_error(HTTP_447_TOO_MANY_FILES_IN_LIBRARY, error_msg)
|
||||
else:
|
||||
logger.error(e)
|
||||
error_msg = 'Internal Server Error'
|
||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||
|
||||
new_page = {
|
||||
'id': new_page_id,
|
||||
'name': page_name,
|
||||
|
@ -173,22 +173,41 @@ def get_and_gen_page_nav_by_id(id_set, navigation, page_id, old_to_new):
|
||||
get_and_gen_page_nav_by_id(id_set, new_navigation, page_id, old_to_new)
|
||||
|
||||
|
||||
def gen_new_page_nav_by_id(navigation, page_id, current_id):
|
||||
def gen_new_page_nav_by_id(navigation, page_id, current_id, insert_position, is_find):
|
||||
new_nav = {
|
||||
'id': page_id,
|
||||
'type': 'page',
|
||||
}
|
||||
if current_id:
|
||||
for nav in navigation:
|
||||
if nav.get('type') == 'page' and nav.get('id') == current_id:
|
||||
sub_nav = nav.get('children', [])
|
||||
sub_nav.append(new_nav)
|
||||
nav['children'] = sub_nav
|
||||
return
|
||||
else:
|
||||
gen_new_page_nav_by_id(nav.get('children', []), page_id, current_id)
|
||||
if insert_position == 'inner':
|
||||
for nav in navigation:
|
||||
if nav.get('type') == 'page' and nav.get('id') == current_id:
|
||||
sub_nav = nav.get('children', [])
|
||||
sub_nav.append(new_nav)
|
||||
nav['children'] = sub_nav
|
||||
is_find[0] = True
|
||||
return True
|
||||
else:
|
||||
gen_new_page_nav_by_id(nav.get('children', []), page_id, current_id, insert_position, is_find)
|
||||
elif insert_position == 'above':
|
||||
for index, nav in enumerate(navigation):
|
||||
if nav.get('type') == 'page' and nav.get('id') == current_id:
|
||||
navigation.insert(index, new_nav)
|
||||
is_find[0] = True
|
||||
return True
|
||||
else:
|
||||
gen_new_page_nav_by_id(nav.get('children', []), page_id, current_id, insert_position, is_find)
|
||||
elif insert_position == 'below':
|
||||
for index, nav in enumerate(navigation):
|
||||
if nav.get('type') == 'page' and nav.get('id') == current_id:
|
||||
navigation.insert(index+1, new_nav)
|
||||
is_find[0] = True
|
||||
return True
|
||||
else:
|
||||
gen_new_page_nav_by_id(nav.get('children', []), page_id, current_id, insert_position, is_find)
|
||||
else:
|
||||
navigation.append(new_nav)
|
||||
return True
|
||||
|
||||
|
||||
def get_current_level_page_ids(navigation, page_id, ids=[]):
|
||||
|
Loading…
Reference in New Issue
Block a user