jsdoc
This commit is contained in:
+8
-6
@@ -31,13 +31,15 @@ define('view', [], function () {
|
||||
/**
|
||||
* A base view.
|
||||
*
|
||||
* @class Espo.View
|
||||
* @class
|
||||
* @name Class
|
||||
* @extends Bull.View
|
||||
* @memberOf module:view
|
||||
*/
|
||||
return Bull.View.extend(/** @lends Espo.View.prototype */{
|
||||
return Bull.View.extend(/** @lends module:view.Class.prototype */{
|
||||
|
||||
/**
|
||||
* @callback Espo.View~actionHandlerCallback
|
||||
* @callback module:view.Class~actionHandlerCallback
|
||||
* @param {jQuery.Event} e A DOM event.
|
||||
*/
|
||||
|
||||
@@ -46,7 +48,7 @@ define('view', [], function () {
|
||||
*
|
||||
* @name model
|
||||
* @type {Espo.Model|null}
|
||||
* @memberof Espo.View.prototype
|
||||
* @memberOf module:view.Class.prototype
|
||||
* @public
|
||||
*/
|
||||
|
||||
@@ -55,7 +57,7 @@ define('view', [], function () {
|
||||
*
|
||||
* @name collection
|
||||
* @type {Espo.Collection|null}
|
||||
* @memberof Espo.View.prototype
|
||||
* @memberOf module:view.Class.prototype
|
||||
* @public
|
||||
*/
|
||||
|
||||
@@ -64,7 +66,7 @@ define('view', [], function () {
|
||||
*
|
||||
* @deprecated Use the `events` property.
|
||||
* @param {string} action
|
||||
* @param {Espo.View~actionHandlerCallback} handler
|
||||
* @param {module:view.Class~actionHandlerCallback} handler
|
||||
*/
|
||||
addActionHandler: function (action, handler) {
|
||||
let fullAction = 'click button[data-action=\"'+action+'\"]';
|
||||
|
||||
+180
-7
@@ -28,22 +28,58 @@
|
||||
|
||||
define('views/detail', ['views/main'], function (Dep) {
|
||||
|
||||
return Dep.extend({
|
||||
/**
|
||||
* @class
|
||||
* @name Class
|
||||
* @extends module:views/main.Class
|
||||
* @memberOf module:views/detail
|
||||
*/
|
||||
return Dep.extend(/** @lends module:views/detail.Class.prototype */{
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
template: 'detail',
|
||||
|
||||
scope: null,
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
name: 'Detail',
|
||||
|
||||
optionsToPass: ['attributes', 'returnUrl', 'returnDispatchParams', 'rootUrl'],
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
optionsToPass: [
|
||||
'attributes',
|
||||
'returnUrl',
|
||||
'returnDispatchParams',
|
||||
'rootUrl',
|
||||
],
|
||||
|
||||
/**
|
||||
* A header view name.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
headerView: 'views/header',
|
||||
|
||||
/**
|
||||
* A record view name.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
recordView: 'views/record/detail',
|
||||
|
||||
/**
|
||||
* A root breadcrumb item not to be a link.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
rootLinkDisabled: false,
|
||||
|
||||
/**
|
||||
* Add an un-follow button.
|
||||
*/
|
||||
addUnfollowButtonToMenu: function () {
|
||||
this.removeMenuItem('follow', true);
|
||||
|
||||
@@ -55,6 +91,9 @@ define('views/detail', ['views/main'], function (Dep) {
|
||||
}, true);
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a follow button.
|
||||
*/
|
||||
addFollowButtonToMenu: function () {
|
||||
this.removeMenuItem('unfollow', true);
|
||||
|
||||
@@ -67,6 +106,9 @@ define('views/detail', ['views/main'], function (Dep) {
|
||||
}, true);
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
setup: function () {
|
||||
Dep.prototype.setup.call(this);
|
||||
|
||||
@@ -91,6 +133,9 @@ define('views/detail', ['views/main'], function (Dep) {
|
||||
this.getHelper().processSetupHandlers(this, 'detail');
|
||||
},
|
||||
|
||||
/**
|
||||
* Set up a page title.
|
||||
*/
|
||||
setupPageTitle: function () {
|
||||
this.listenTo(this.model, 'after:save', () => {
|
||||
this.updatePageTitle();
|
||||
@@ -103,6 +148,9 @@ define('views/detail', ['views/main'], function (Dep) {
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Set up a header.
|
||||
*/
|
||||
setupHeader: function () {
|
||||
this.createView('header', this.headerView, {
|
||||
model: this.model,
|
||||
@@ -120,6 +168,9 @@ define('views/detail', ['views/main'], function (Dep) {
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Set up a record.
|
||||
*/
|
||||
setupRecord: function () {
|
||||
var o = {
|
||||
model: this.model,
|
||||
@@ -142,11 +193,19 @@ define('views/detail', ['views/main'], function (Dep) {
|
||||
return this.createView('record', this.getRecordViewName(), o);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a record view name.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
getRecordViewName: function () {
|
||||
return this.getMetadata()
|
||||
.get('clientDefs.' + this.scope + '.recordViews.detail') || this.recordView;
|
||||
},
|
||||
|
||||
/**
|
||||
* Control follow/unfollow buttons visibility..
|
||||
*/
|
||||
handleFollowButton: function () {
|
||||
if (this.model.get('isFollowed')) {
|
||||
this.addUnfollowButtonToMenu();
|
||||
@@ -159,6 +218,9 @@ define('views/detail', ['views/main'], function (Dep) {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Action 'follow'.
|
||||
*/
|
||||
actionFollow: function () {
|
||||
this.disableMenuItem('follow');
|
||||
|
||||
@@ -174,6 +236,9 @@ define('views/detail', ['views/main'], function (Dep) {
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Action 'unfollow'.
|
||||
*/
|
||||
actionUnfollow: function () {
|
||||
this.disableMenuItem('unfollow');
|
||||
|
||||
@@ -189,6 +254,9 @@ define('views/detail', ['views/main'], function (Dep) {
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
getHeader: function () {
|
||||
var name = Handlebars.Utils.escapeExpression(this.model.get('name'));
|
||||
|
||||
@@ -221,6 +289,9 @@ define('views/detail', ['views/main'], function (Dep) {
|
||||
]);
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
updatePageTitle: function () {
|
||||
if (this.model.has('name')) {
|
||||
this.setPageTitle(this.model.get('name'));
|
||||
@@ -230,27 +301,120 @@ define('views/detail', ['views/main'], function (Dep) {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update a relationship panel (fetch data).
|
||||
*
|
||||
* @param {string} name A relationship name.
|
||||
*/
|
||||
updateRelationshipPanel: function (name) {
|
||||
var bottom = this.getView('record').getView('bottom');
|
||||
|
||||
if (bottom) {
|
||||
var rel = bottom.getView(name);
|
||||
|
||||
if (rel) {
|
||||
rel.collection.fetch();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* When a related record created, attributes will be copied from a current entity.
|
||||
*
|
||||
* Example:
|
||||
* ```
|
||||
* {
|
||||
* 'linkName': {
|
||||
* 'attributeNameOfCurrentEntity': 'attributeNameOfCreatedRelatedEntity',
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
relatedAttributeMap: {},
|
||||
|
||||
/**
|
||||
* When a related record created, use a function to obtain some attributes for a created entity.
|
||||
*
|
||||
* Example:
|
||||
* ```
|
||||
* {
|
||||
* 'linkName': function () {
|
||||
* return {
|
||||
* 'someAttribute': this.model.get('attribute1') + ' ' +
|
||||
* this.model.get('attribute2')
|
||||
* };
|
||||
* },
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
relatedAttributeFunctions: {},
|
||||
|
||||
/**
|
||||
* When selecting a related record, field filters can be automatically applied.
|
||||
*
|
||||
* Example:
|
||||
* ```
|
||||
* {
|
||||
* 'linkName': {
|
||||
* 'field1': function () {
|
||||
* return {
|
||||
* attribute: 'field1',
|
||||
* type: 'equals',
|
||||
* value: this.model.get('someField'),
|
||||
* data: {},// Additional filter data specific for a field type.
|
||||
* };
|
||||
* },
|
||||
* },
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
selectRelatedFilters: {},
|
||||
|
||||
/**
|
||||
* When selecting a related record, a primary filter can be automatically applied.
|
||||
*
|
||||
* Example:
|
||||
* ```
|
||||
* {
|
||||
* 'linkName1': 'primaryFilterName',
|
||||
* 'linkName2': function () {
|
||||
* return 'primaryFilterName';
|
||||
* },
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
selectPrimaryFilterNames: {},
|
||||
|
||||
/**
|
||||
* When selecting a related record, bool filters can be automatically applied.
|
||||
*
|
||||
* Example:
|
||||
* ```
|
||||
* {
|
||||
* 'linkName1': ['onlyMy', 'followed'],
|
||||
* 'linkName2': function () {
|
||||
* return ['someBoolFilterName];
|
||||
* },
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
selectBoolFilterLists: [],
|
||||
|
||||
/**
|
||||
* Action 'createRelated'.
|
||||
*
|
||||
* @param {Object} data
|
||||
*/
|
||||
actionCreateRelated: function (data) {
|
||||
data = data || {};
|
||||
|
||||
@@ -267,9 +431,10 @@ define('views/detail', ['views/main'], function (Dep) {
|
||||
attributes = _.extend(this.relatedAttributeFunctions[link].call(this), attributes);
|
||||
}
|
||||
|
||||
Object.keys(this.relatedAttributeMap[link] || {}).forEach((attr) => {
|
||||
attributes[this.relatedAttributeMap[link][attr]] = this.model.get(attr);
|
||||
});
|
||||
Object.keys(this.relatedAttributeMap[link] || {})
|
||||
.forEach(attr => {
|
||||
attributes[this.relatedAttributeMap[link][attr]] = this.model.get(attr);
|
||||
});
|
||||
|
||||
this.notify('Loading...');
|
||||
|
||||
@@ -302,6 +467,11 @@ define('views/detail', ['views/main'], function (Dep) {
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Action 'selectRelated'.
|
||||
*
|
||||
* @param {Object} data
|
||||
*/
|
||||
actionSelectRelated: function (data) {
|
||||
var link = data.link;
|
||||
|
||||
@@ -437,6 +607,9 @@ define('views/detail', ['views/main'], function (Dep) {
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Action 'duplicate'.
|
||||
*/
|
||||
actionDuplicate: function () {
|
||||
Espo.Ui.notify(this.translate('pleaseWait', 'messages'));
|
||||
|
||||
|
||||
@@ -28,16 +28,37 @@
|
||||
|
||||
define('views/edit', ['views/main'], function (Dep) {
|
||||
|
||||
return Dep.extend({
|
||||
/**
|
||||
* @class
|
||||
* @name Class
|
||||
* @extends module:views/main.Class
|
||||
* @memberOf module:views/edit
|
||||
*/
|
||||
return Dep.extend(/** @lends module:views/edit.Class.prototype */{
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
template: 'edit',
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
scope: null,
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
name: 'Edit',
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
menu: null,
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
optionsToPass: [
|
||||
'returnUrl',
|
||||
'returnDispatchParams',
|
||||
@@ -47,12 +68,30 @@ define('views/edit', ['views/main'], function (Dep) {
|
||||
'returnAfterCreate',
|
||||
],
|
||||
|
||||
/**
|
||||
* A header view name.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
headerView: 'views/header',
|
||||
|
||||
/**
|
||||
* A record view name.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
recordView: 'views/record/edit',
|
||||
|
||||
/**
|
||||
* A root breadcrumb item not to be a link.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
rootLinkDisabled: false,
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
setup: function () {
|
||||
this.headerView = this.options.headerView || this.headerView;
|
||||
this.recordView = this.options.recordView || this.recordView;
|
||||
@@ -63,6 +102,9 @@ define('views/edit', ['views/main'], function (Dep) {
|
||||
this.getHelper().processSetupHandlers(this, 'edit');
|
||||
},
|
||||
|
||||
/**
|
||||
* Set up a header.
|
||||
*/
|
||||
setupHeader: function () {
|
||||
this.createView('header', this.headerView, {
|
||||
model: this.model,
|
||||
@@ -71,6 +113,9 @@ define('views/edit', ['views/main'], function (Dep) {
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Set up a record.
|
||||
*/
|
||||
setupRecord: function () {
|
||||
var o = {
|
||||
model: this.model,
|
||||
@@ -89,10 +134,18 @@ define('views/edit', ['views/main'], function (Dep) {
|
||||
return this.createView('record', this.getRecordViewName(), o);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a record view name.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
getRecordViewName: function () {
|
||||
return this.getMetadata().get('clientDefs.' + this.scope + '.recordViews.edit') || this.recordView;
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
getHeader: function () {
|
||||
var headerIconHtml = this.getHeaderIconHtml();
|
||||
|
||||
@@ -133,6 +186,9 @@ define('views/edit', ['views/main'], function (Dep) {
|
||||
return this.buildHeaderHtml(arr);
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
updatePageTitle: function () {
|
||||
var title;
|
||||
|
||||
|
||||
+114
-7
@@ -28,20 +28,49 @@
|
||||
|
||||
define('views/main', ['view'], function (Dep) {
|
||||
|
||||
return Dep.extend({
|
||||
/**
|
||||
* A base main view. The detail, edit, list views to be extended from.
|
||||
*
|
||||
* @class
|
||||
* @name Class
|
||||
* @extends module:view.Class
|
||||
* @memberOf module:views/main
|
||||
*/
|
||||
return Dep.extend(/** @lends module:views/main.Class.prototype */{
|
||||
|
||||
/**
|
||||
* A scope name.
|
||||
*
|
||||
* @type {string} scope
|
||||
*/
|
||||
scope: null,
|
||||
|
||||
/**
|
||||
* A name.
|
||||
*
|
||||
* @type {string} name
|
||||
*/
|
||||
name: null,
|
||||
|
||||
/**
|
||||
* Top-right menu definitions.
|
||||
*
|
||||
* @type {Object} menu
|
||||
*/
|
||||
menu: null,
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
events: {
|
||||
'click .action': function (e) {
|
||||
Espo.Utils.handleAction(this, e);
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
init: function () {
|
||||
this.scope = this.options.scope || this.scope;
|
||||
this.menu = {};
|
||||
@@ -92,10 +121,17 @@ define('views/main', ['view'], function (Dep) {
|
||||
this.updateLastUrl();
|
||||
},
|
||||
|
||||
/**
|
||||
* Update a last history URL.
|
||||
*/
|
||||
updateLastUrl: function () {
|
||||
this.lastUrl = this.getRouter().getCurrentUrl();
|
||||
},
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @returns {Object}
|
||||
*/
|
||||
getMenu: function () {
|
||||
if (this.menuDisabled) {
|
||||
return {};
|
||||
@@ -141,8 +177,21 @@ define('views/main', ['view'], function (Dep) {
|
||||
return menu;
|
||||
},
|
||||
|
||||
getHeader: function () {},
|
||||
/**
|
||||
* Get a header HTML. To be overridden.
|
||||
*
|
||||
* @returns {string} HTML.
|
||||
*/
|
||||
getHeader: function () {
|
||||
return '';
|
||||
},
|
||||
|
||||
/**
|
||||
* Build a header HTML. To be called from the #getHeader method.
|
||||
*
|
||||
* @param {string[]} arr A breadcrumb path. Like: Account > Name > edit.
|
||||
* @returns {string} HTML
|
||||
*/
|
||||
buildHeaderHtml: function (arr) {
|
||||
var a = [];
|
||||
|
||||
@@ -154,16 +203,29 @@ define('views/main', ['view'], function (Dep) {
|
||||
a.join('<div class="breadcrumb-separator"><span class="chevron-right"></span></div>') + '</div>';
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get an icon HTML.
|
||||
*
|
||||
* @returns {string} HTML
|
||||
*/
|
||||
getHeaderIconHtml: function () {
|
||||
return this.getHelper().getScopeColorIconHtml(this.scope);
|
||||
},
|
||||
|
||||
/**
|
||||
* Action 'showModal'.
|
||||
*
|
||||
* @todo Revise. To be removed?
|
||||
*
|
||||
* @param {Object} data
|
||||
*/
|
||||
actionShowModal: function (data) {
|
||||
var view = data.view;
|
||||
|
||||
if (!view) {
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
this.createView('modal', view, {
|
||||
model: this.model,
|
||||
@@ -183,6 +245,14 @@ define('views/main', ['view'], function (Dep) {
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a menu item.
|
||||
*
|
||||
* @param {'buttons'|'dropdown'|'actions'} type A type.
|
||||
* @param {Object} item Item definitions.
|
||||
* @param {boolean} [toBeginning=false] To beginning.
|
||||
* @param {boolean} [doNotReRender=false] Skip re-render.
|
||||
*/
|
||||
addMenuItem: function (type, item, toBeginning, doNotReRender) {
|
||||
if (item) {
|
||||
item.name = item.name || item.action;
|
||||
@@ -197,8 +267,6 @@ define('views/main', ['view'], function (Dep) {
|
||||
|
||||
if (data.name === name) {
|
||||
index = i;
|
||||
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -229,6 +297,12 @@ define('views/main', ['view'], function (Dep) {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a menu item.
|
||||
*
|
||||
* @param {string} name An item name.
|
||||
* @param {boolean} doNotReRender Skip re-render.
|
||||
*/
|
||||
removeMenuItem: function (name, doNotReRender) {
|
||||
var index = -1;
|
||||
var type = false;
|
||||
@@ -264,11 +338,14 @@ define('views/main', ['view'], function (Dep) {
|
||||
|
||||
if (doNotReRender && this.isRendered()) {
|
||||
this.$el.find('.header .header-buttons [data-name="'+name+'"]').remove();
|
||||
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Disable a menu item.
|
||||
*
|
||||
* @param {string} name A name.
|
||||
*/
|
||||
disableMenuItem: function (name) {
|
||||
this.$el
|
||||
.find('.header .header-buttons [data-name="'+name+'"]')
|
||||
@@ -276,6 +353,11 @@ define('views/main', ['view'], function (Dep) {
|
||||
.attr('disabled');
|
||||
},
|
||||
|
||||
/**
|
||||
* Enable a menu item.
|
||||
*
|
||||
* @param {string} name A name.
|
||||
*/
|
||||
enableMenuItem: function (name) {
|
||||
this.$el
|
||||
.find('.header .header-buttons [data-name="'+name+'"]')
|
||||
@@ -283,6 +365,12 @@ define('views/main', ['view'], function (Dep) {
|
||||
.removeAttr('disabled');
|
||||
},
|
||||
|
||||
/**
|
||||
* Action 'navigateToRoot'.
|
||||
*
|
||||
* @param {Object} data
|
||||
* @param {jQuery.Event} e
|
||||
*/
|
||||
actionNavigateToRoot: function (data, e) {
|
||||
e.stopPropagation();
|
||||
|
||||
@@ -298,6 +386,11 @@ define('views/main', ['view'], function (Dep) {
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Hide a menu item.
|
||||
*
|
||||
* @param {string} name A name.
|
||||
*/
|
||||
hideHeaderActionItem: function (name) {
|
||||
['actions', 'dropdown', 'buttons'].forEach((t) => {
|
||||
(this.menu[t] || []).forEach((item, i) => {
|
||||
@@ -319,6 +412,11 @@ define('views/main', ['view'], function (Dep) {
|
||||
this.controlMenuDropdownVisibility();
|
||||
},
|
||||
|
||||
/**
|
||||
* Show a hidden menu item.
|
||||
*
|
||||
* @param {string} name A name.
|
||||
*/
|
||||
showHeaderActionItem: function (name) {
|
||||
['actions', 'dropdown', 'buttons'].forEach((t) => {
|
||||
(this.menu[t] || []).forEach((item, i) => {
|
||||
@@ -340,6 +438,12 @@ define('views/main', ['view'], function (Dep) {
|
||||
this.controlMenuDropdownVisibility();
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether a menu has any non-hidden dropdown items.
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasMenuVisibleDropdownItems: function () {
|
||||
var hasItems = false;
|
||||
|
||||
@@ -352,6 +456,9 @@ define('views/main', ['view'], function (Dep) {
|
||||
return hasItems;
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
controlMenuDropdownVisibility: function () {
|
||||
var $d = this.$el.find('.page-header .dropdown-group');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user