mirror of
https://github.com/wintercms/winter.git
synced 2024-06-28 05:33:29 +02:00
Rebuilding the Inspector
This commit is contained in:
parent
58e6d9971f
commit
dba955da3a
@ -45,16 +45,6 @@ class ServiceProvider extends ModuleServiceProvider
|
||||
public function boot()
|
||||
{
|
||||
parent::boot('backend');
|
||||
|
||||
Event::listen('pages.builder.registerControls', function($controlLibrary) {
|
||||
$controlLibrary->registerControl('text',
|
||||
'backend::lang.form.control_text',
|
||||
$controlLibrary::GROUP_STANDARD,
|
||||
'icon-terminal',
|
||||
$controlLibrary->getStandardProperties(),
|
||||
null
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -202,8 +202,7 @@ return [
|
||||
'insert_row_below' => 'Insert Row Below',
|
||||
'delete_row' => 'Delete Row',
|
||||
'concurrency_file_changed_title' => 'File was changed',
|
||||
'concurrency_file_changed_description' => "The file you're editing has been changed on disk by another user. You can either reload the file and lose your changes or override the file on the disk.",
|
||||
'control_text' => 'Text field',
|
||||
'concurrency_file_changed_description' => "The file you're editing has been changed on disk by another user. You can either reload the file and lose your changes or override the file on the disk."
|
||||
],
|
||||
'relation' => [
|
||||
'missing_config' => "Relation behavior does not have any configuration for ':config'.",
|
||||
|
@ -26,13 +26,19 @@
|
||||
},
|
||||
|
||||
addClass: function(el, className) {
|
||||
if (this.hasClass(el, className))
|
||||
return
|
||||
var classes = className.split(' ')
|
||||
|
||||
if (el.classList)
|
||||
el.classList.add(className);
|
||||
else
|
||||
el.className += ' ' + className;
|
||||
for (var i = 0, len = classes.length; i < len; i++) {
|
||||
var currentClass = classes[i].trim()
|
||||
|
||||
if (this.hasClass(el, currentClass))
|
||||
return
|
||||
|
||||
if (el.classList)
|
||||
el.classList.add(currentClass);
|
||||
else
|
||||
el.className += ' ' + currentClass;
|
||||
}
|
||||
},
|
||||
|
||||
removeClass: function(el, className) {
|
||||
|
58
modules/system/assets/ui/js/inspector.editor.base.js
Normal file
58
modules/system/assets/ui/js/inspector.editor.base.js
Normal file
@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Inspector editor base class.
|
||||
*/
|
||||
+function ($) { "use strict";
|
||||
|
||||
// NAMESPACES
|
||||
// ============================
|
||||
|
||||
if ($.oc === undefined)
|
||||
$.oc = {}
|
||||
|
||||
if ($.oc.inspector === undefined)
|
||||
$.oc.inspector = {}
|
||||
|
||||
if ($.oc.inspector.propertyEditors === undefined)
|
||||
$.oc.inspector.propertyEditors = {}
|
||||
|
||||
// CLASS DEFINITION
|
||||
// ============================
|
||||
|
||||
var Base = $.oc.foundation.base,
|
||||
BaseProto = Base.prototype
|
||||
|
||||
var BaseEditor = function(inspector, propertyDefinition, containerCell) {
|
||||
this.inspector = inspector
|
||||
this.propertyDefinition = propertyDefinition
|
||||
this.containerCell = containerCell
|
||||
|
||||
Base.call(this)
|
||||
|
||||
this.init()
|
||||
}
|
||||
|
||||
BaseEditor.prototype = Object.create(BaseProto)
|
||||
BaseEditor.prototype.constructor = Base
|
||||
|
||||
BaseEditor.prototype.dispose = function() {
|
||||
this.inspector = null
|
||||
this.propertyDefinition = null
|
||||
this.containerCell = null
|
||||
|
||||
BaseProto.dispose.call(this)
|
||||
}
|
||||
|
||||
BaseEditor.prototype.init = function() {
|
||||
this.build()
|
||||
this.registerHandlers()
|
||||
}
|
||||
|
||||
BaseEditor.prototype.build = function() {
|
||||
return null
|
||||
}
|
||||
|
||||
BaseEditor.prototype.registerHandlers = function() {
|
||||
}
|
||||
|
||||
$.oc.inspector.propertyEditors.base = BaseEditor
|
||||
}(window.jQuery);
|
69
modules/system/assets/ui/js/inspector.editor.string.js
Normal file
69
modules/system/assets/ui/js/inspector.editor.string.js
Normal file
@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Inspector string editor class.
|
||||
*/
|
||||
+function ($) { "use strict";
|
||||
|
||||
var Base = $.oc.inspector.propertyEditors.base,
|
||||
BaseProto = Base.prototype
|
||||
|
||||
var StringEditor = function(inspector, propertyDefinition, containerCell) {
|
||||
Base.call(this, inspector, propertyDefinition, containerCell)
|
||||
}
|
||||
|
||||
StringEditor.prototype = Object.create(BaseProto)
|
||||
StringEditor.prototype.constructor = Base
|
||||
|
||||
StringEditor.prototype.dispose = function() {
|
||||
this.unregisterHandlers()
|
||||
|
||||
BaseProto.dispose.call(this)
|
||||
}
|
||||
|
||||
StringEditor.prototype.build = function() {
|
||||
var editor = document.createElement('input'),
|
||||
placeholder = this.propertyDefinition.placeholder !== undefined ? this.propertyDefinition.placeholder : '',
|
||||
value = this.inspector.getPropertyValue(this.propertyDefinition.property)
|
||||
|
||||
editor.setAttribute('type', 'text')
|
||||
editor.setAttribute('class', 'string-editor')
|
||||
editor.setAttribute('placeholder', 'placeholder')
|
||||
|
||||
if (value === undefined) {
|
||||
value = this.propertyDefinition.default
|
||||
}
|
||||
|
||||
editor.value = value
|
||||
|
||||
$.oc.foundation.element.addClass(this.containerCell, 'text')
|
||||
|
||||
this.containerCell.appendChild(editor)
|
||||
}
|
||||
|
||||
StringEditor.prototype.getInput = function() {
|
||||
return this.containerCell.querySelector('input')
|
||||
}
|
||||
|
||||
StringEditor.prototype.registerHandlers = function() {
|
||||
var input = this.getInput()
|
||||
|
||||
input.addEventListener('focus', this.proxy(this.onInputFocus))
|
||||
input.addEventListener('change', this.proxy(this.onInputChange))
|
||||
}
|
||||
|
||||
StringEditor.prototype.unregisterHandlers = function() {
|
||||
var input = this.getInput()
|
||||
|
||||
input.removeEventListener('focus', this.proxy(this.onInputFocus))
|
||||
input.removeEventListener('change', this.proxy(this.onInputChange))
|
||||
}
|
||||
|
||||
StringEditor.prototype.onInputFocus = function(ev) {
|
||||
this.inspector.makeCellActive(this.containerCell)
|
||||
}
|
||||
|
||||
StringEditor.prototype.onInputChange = function() {
|
||||
|
||||
}
|
||||
|
||||
$.oc.inspector.propertyEditors.string = StringEditor
|
||||
}(window.jQuery);
|
91
modules/system/assets/ui/js/inspector.engine.js
Normal file
91
modules/system/assets/ui/js/inspector.engine.js
Normal file
@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Inspector engine helpers.
|
||||
*
|
||||
* The helpers are used mostly by the Inspector Surface.
|
||||
*
|
||||
*/
|
||||
+function ($) { "use strict";
|
||||
|
||||
// NAMESPACES
|
||||
// ============================
|
||||
|
||||
if ($.oc === undefined)
|
||||
$.oc = {}
|
||||
|
||||
if ($.oc.inspector === undefined)
|
||||
$.oc.inspector = {}
|
||||
|
||||
$.oc.inspector.engine = {}
|
||||
|
||||
// CLASS DEFINITION
|
||||
// ============================
|
||||
|
||||
function findGroup(group, properties) {
|
||||
for (var i = 0, len = properties.length; i < len; i++) {
|
||||
var property = properties[i]
|
||||
|
||||
if (property.itemType !== undefined && property.itemType == 'group' && item.title == group) {
|
||||
return property
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
$.oc.inspector.engine.processPropertyGroups = function(properties) {
|
||||
var fields = [],
|
||||
result = {
|
||||
hasGroups: false,
|
||||
properties: []
|
||||
},
|
||||
groupIndex = 0
|
||||
|
||||
|
||||
for (var i = 0, len = properties.length; i < len; i++) {
|
||||
var property = properties[i]
|
||||
|
||||
property.itemType = 'property'
|
||||
|
||||
if (property.group === undefined) {
|
||||
fields.push(property)
|
||||
}
|
||||
else {
|
||||
var group = findGroup(property.group)
|
||||
|
||||
if (!group) {
|
||||
group = {
|
||||
itemType: 'group',
|
||||
title: property.group,
|
||||
properties: [],
|
||||
groupIndex: groupIndex
|
||||
}
|
||||
|
||||
groupIndex++
|
||||
fields.push(group)
|
||||
}
|
||||
|
||||
property.groupIndex = group.groupIndex
|
||||
group.properties.push(property)
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0, len = properties.length; i < len; i++) {
|
||||
var property = properties[i]
|
||||
|
||||
result.properties.push(property)
|
||||
|
||||
if (property.itemType == 'group') {
|
||||
result.hasGroups = true
|
||||
|
||||
for (var j = 0, propertiesLen = property.properties.length; j < propertiesLen; j++) {
|
||||
result.properties.push(property.properties[j])
|
||||
}
|
||||
|
||||
delete property.properties
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
}(window.jQuery);
|
32
modules/system/assets/ui/js/inspector.helpers.js
Normal file
32
modules/system/assets/ui/js/inspector.helpers.js
Normal file
@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Inspector helper functions.
|
||||
*
|
||||
*/
|
||||
+function ($) { "use strict";
|
||||
|
||||
// NAMESPACES
|
||||
// ============================
|
||||
|
||||
if ($.oc === undefined)
|
||||
$.oc = {}
|
||||
|
||||
if ($.oc.inspector === undefined)
|
||||
$.oc.inspector = {}
|
||||
|
||||
$.oc.inspector.helpers = {}
|
||||
|
||||
$.oc.inspector.helpers.generateElementUniqueId = function(element) {
|
||||
if (element.hasAttribute('data-inspector-id')) {
|
||||
return element.getAttribute('data-inspector-id')
|
||||
}
|
||||
|
||||
var id = $.oc.inspector.helpers.generateUniqueId()
|
||||
element.setAttribute('data-inspector-id', id)
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
$.oc.inspector.helpers.generateUniqueId = function() {
|
||||
return "inspectorid-" + Math.floor(Math.random() * new Date().getTime());
|
||||
}
|
||||
}(window.jQuery)
|
333
modules/system/assets/ui/js/inspector.surface.js
Normal file
333
modules/system/assets/ui/js/inspector.surface.js
Normal file
@ -0,0 +1,333 @@
|
||||
/*
|
||||
* Inspector Surface class.
|
||||
*
|
||||
* The class creates Inspector user interface and all the editors
|
||||
* corresponding to the passed configuration in a specified container
|
||||
* element.
|
||||
*
|
||||
*/
|
||||
+function ($) { "use strict";
|
||||
|
||||
// NAMESPACES
|
||||
// ============================
|
||||
|
||||
if ($.oc === undefined)
|
||||
$.oc = {}
|
||||
|
||||
if ($.oc.inspector === undefined)
|
||||
$.oc.inspector = {}
|
||||
|
||||
// CLASS DEFINITION
|
||||
// ============================
|
||||
|
||||
var Base = $.oc.foundation.base,
|
||||
BaseProto = Base.prototype
|
||||
|
||||
/**
|
||||
* Creates the Inspector surface in a container.
|
||||
* - containerElement container DOM element
|
||||
* - properties array (array of objects)
|
||||
* - values - property values, an object
|
||||
* - inspectorUniqueId - a string containing the unique inspector identifier.
|
||||
* The identifier should be a constant for an inspectable element. Use
|
||||
* $.oc.inspector.helpers.generateElementUniqueId(element) to generate a persistent ID
|
||||
* for an element. Use $.oc.inspector.helpers.generateUniqueId() to generate an ID
|
||||
* not associated with an element. Inspector uses the ID for storing configuration
|
||||
* related to an element in the document DOM.
|
||||
*/
|
||||
var Surface = function(containerElement, properties, values, inspectorUniqueId, options) {
|
||||
this.options = $.extend({}, Surface.DEFAULTS, typeof option == 'object' && option)
|
||||
this.rawProperties = properties
|
||||
this.parsedProperties = $.oc.inspector.engine.processPropertyGroups(properties)
|
||||
this.container = containerElement
|
||||
this.inspectorUniqueId = inspectorUniqueId
|
||||
this.values = values
|
||||
|
||||
this.editors = []
|
||||
this.tableContainer = null
|
||||
|
||||
Base.call(this)
|
||||
|
||||
this.init()
|
||||
}
|
||||
|
||||
Surface.prototype = Object.create(BaseProto)
|
||||
Surface.prototype.constructor = Surface
|
||||
|
||||
Surface.prototype.dispose = function() {
|
||||
this.removeElements()
|
||||
this.disposeEditors()
|
||||
|
||||
this.container = null
|
||||
this.tableContainer = null
|
||||
this.rawProperties = null
|
||||
this.parsedProperties = null
|
||||
this.editors = null
|
||||
this.values = null
|
||||
|
||||
BaseProto.dispose.call(this)
|
||||
}
|
||||
|
||||
// INTERNAL METHODS
|
||||
// ============================
|
||||
|
||||
Surface.prototype.init = function() {
|
||||
this.build()
|
||||
}
|
||||
|
||||
//
|
||||
// Building
|
||||
//
|
||||
|
||||
/**
|
||||
* Builds the Inspector table. The markup generated by this method looks
|
||||
* like this:
|
||||
*
|
||||
* <div>
|
||||
* <table>
|
||||
* <tbody>
|
||||
* <tr>
|
||||
* <th data-property="label">
|
||||
* <div>
|
||||
* <div>
|
||||
* <span class="title-element" title="Label">
|
||||
* <a href="javascript:;" class="expandControl expanded" data-group-index="1">Expand/Collapse</a>
|
||||
* Label
|
||||
* </span>
|
||||
* </div>
|
||||
* </div>
|
||||
* </th>
|
||||
* <td>
|
||||
* Editor markup
|
||||
* </td>
|
||||
* </tr>
|
||||
* </tbody>
|
||||
* </table>
|
||||
* </div>
|
||||
*/
|
||||
Surface.prototype.build = function() {
|
||||
this.tableContainer = document.createElement('div')
|
||||
|
||||
var dataTable = document.createElement('table'),
|
||||
tbody = document.createElement('tbody')
|
||||
|
||||
$.oc.foundation.element.addClass(dataTable, 'inspector-fields')
|
||||
if (this.parsedProperties.hasGroups) {
|
||||
$.oc.foundation.element.addClass(dataTable, 'has-groups')
|
||||
}
|
||||
|
||||
for (var i=0, len = this.parsedProperties.properties.length; i < len; i++) {
|
||||
var property = this.parsedProperties.properties[i],
|
||||
row = document.createElement('tr'),
|
||||
th = document.createElement('th'),
|
||||
titleSpan = document.createElement('span'),
|
||||
description = this.buildPropertyDescription(property)
|
||||
|
||||
// Table row
|
||||
//
|
||||
row.setAttribute('data-property', property.property)
|
||||
this.applyGroupIndexAttribute(property, row)
|
||||
$.oc.foundation.element.addClass(row, this.getRowCssClass(property))
|
||||
|
||||
// Property head
|
||||
//
|
||||
this.applyHeadColspan(th, property)
|
||||
|
||||
titleSpan.setAttribute('class', 'title-element')
|
||||
titleSpan.setAttribute('title', this.escapeJavascriptString(property.title))
|
||||
this.buildGroupExpandControl(titleSpan, property)
|
||||
titleSpan.innerHTML += this.escapeJavascriptString(property.title)
|
||||
|
||||
var outerDiv = document.createElement('div'),
|
||||
innerDiv = document.createElement('div')
|
||||
|
||||
innerDiv.appendChild(titleSpan)
|
||||
|
||||
if (description) {
|
||||
innerDiv.appendChild(description)
|
||||
}
|
||||
|
||||
outerDiv.appendChild(innerDiv)
|
||||
th.appendChild(outerDiv)
|
||||
row.appendChild(th)
|
||||
|
||||
// Editor
|
||||
//
|
||||
this.buildEditor(row, property)
|
||||
|
||||
tbody.appendChild(row)
|
||||
}
|
||||
|
||||
dataTable.appendChild(tbody)
|
||||
this.tableContainer.appendChild(dataTable)
|
||||
|
||||
this.container.appendChild(this.tableContainer)
|
||||
}
|
||||
|
||||
Surface.prototype.applyGroupIndexAttribute = function(property, row) {
|
||||
if (property.groupIndex !== undefined && property.itemType == 'property') {
|
||||
row.setAttribute('data-group-index', property.groupIndex)
|
||||
}
|
||||
}
|
||||
|
||||
Surface.prototype.getRowCssClass = function(property) {
|
||||
var result = property.itemType
|
||||
|
||||
if (property.itemType == 'property' && property.groupIndex !== undefined) {
|
||||
result += ' grouped'
|
||||
result += this.isGroupExpanded(property.group) ? ' expanded' : ' collapsed'
|
||||
}
|
||||
|
||||
if (property.itemType == 'property' && !property.showExternalParam) {
|
||||
result += ' no-external-parameter'
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
Surface.prototype.applyHeadColspan = function(th, property) {
|
||||
if (property.itemType == 'group') {
|
||||
th.setAttribute('colspan', 2)
|
||||
}
|
||||
}
|
||||
|
||||
Surface.prototype.buildGroupExpandControl = function(titleSpan, property) {
|
||||
if (property.itemType !== 'group') {
|
||||
return
|
||||
}
|
||||
|
||||
var statusClass = this.isGroupExpanded(property.title) ? 'expanded' : '',
|
||||
anchor = document.createElement('a')
|
||||
|
||||
anchor.setAttribute('class', 'expandControl ' + statusClass)
|
||||
anchor.setAttribute('href', 'javascript:;')
|
||||
anchor.setAttribute('data-group-index', property.groupIndex)
|
||||
anchor.innerHTML = '<span>Expand/collapse</span>'
|
||||
|
||||
titleSpan.appendChild(anchor)
|
||||
}
|
||||
|
||||
Surface.prototype.buildPropertyDescription = function(property) {
|
||||
if (property.description === undefined || property.description === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
var span = document.createElement('span')
|
||||
span.setAttribute('title', this.escapeJavascriptString(property.description))
|
||||
span.setAttribute('class', 'info oc-icon-info with-tooltip')
|
||||
|
||||
return span
|
||||
}
|
||||
|
||||
//
|
||||
// Field grouping
|
||||
//
|
||||
|
||||
Surface.prototype.isGroupExpanded = function(group) {
|
||||
var statuses = this.loadGroupStatuses()
|
||||
|
||||
if (statuses[group] !== undefined)
|
||||
return statuses[group]
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
Surface.prototype.loadGroupStatuses = function() {
|
||||
var statuses = this.getInspectorGroupStatuses()
|
||||
|
||||
if (statuses[this.inspectorUniqueId] !== undefined) {
|
||||
return statuses[this.inspectorUniqueId]
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
|
||||
Surface.prototype.getInspectorGroupStatuses = function() {
|
||||
var statuses = document.body.getAttribute('data-inspector-group-statuses')
|
||||
|
||||
if (statuses !== null) {
|
||||
return JSON.parse(statuses)
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
|
||||
//
|
||||
// Editors
|
||||
//
|
||||
|
||||
Surface.prototype.buildEditor = function(row, property) {
|
||||
if (property.itemType !== 'property') {
|
||||
return
|
||||
}
|
||||
|
||||
if ($.oc.inspector.propertyEditors[property.type] === undefined)
|
||||
throw new Error('The Inspector editor class "' + property.type +
|
||||
'" is not defined in the $.oc.inspector.propertyEditors namespace.')
|
||||
|
||||
var cell = document.createElement('td'),
|
||||
editor = new $.oc.inspector.propertyEditors[property.type](this, property, cell)
|
||||
|
||||
this.editors.push(editor)
|
||||
|
||||
row.appendChild(cell)
|
||||
}
|
||||
|
||||
//
|
||||
// Internal API for the editors
|
||||
//
|
||||
|
||||
Surface.prototype.getPropertyValue = function(property) {
|
||||
return this.values[property]
|
||||
}
|
||||
|
||||
Surface.prototype.makeCellActive = function(cell) {
|
||||
var tbody = cell.parentNode.parentNode.parentNode, // cell / row / tbody
|
||||
cells = tbody.querySelectorAll('tr td')
|
||||
|
||||
for (var i = 0, len = cells.length; i < len; i++) {
|
||||
$.oc.foundation.element.removeClass(cells[i], 'active')
|
||||
}
|
||||
|
||||
$.oc.foundation.element.addClass(cell, 'active')
|
||||
}
|
||||
|
||||
//
|
||||
// Disposing
|
||||
//
|
||||
|
||||
Surface.prototype.removeElements = function() {
|
||||
this.tableContainer.parentNode.removeChild(this.tableContainer);
|
||||
}
|
||||
|
||||
Surface.prototype.disposeEditors = function() {
|
||||
for (var i = 0, len = this.editors.length; i < len; i++) {
|
||||
var editor = this.editors[i]
|
||||
|
||||
editor.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Helpers
|
||||
//
|
||||
|
||||
Surface.prototype.escapeJavascriptString = function(str) {
|
||||
var div = document.createElement('div')
|
||||
div.appendChild(document.createTextNode(str))
|
||||
return div.innerHTML
|
||||
}
|
||||
|
||||
// DEFAULT OPTIONS
|
||||
// ============================
|
||||
|
||||
Surface.DEFAULTS = {
|
||||
showExternalParam: false
|
||||
}
|
||||
|
||||
// REGISTRATION
|
||||
// ============================
|
||||
|
||||
$.oc.inspector.surface = Surface
|
||||
|
||||
}(window.jQuery);
|
8
modules/system/assets/ui/storm-min.js
vendored
8
modules/system/assets/ui/storm-min.js
vendored
@ -19,11 +19,13 @@ $.oc={}
|
||||
if($.oc.foundation===undefined)
|
||||
$.oc.foundation={}
|
||||
var Element={hasClass:function(el,className){if(el.classList)
|
||||
return el.classList.contains(className);return new RegExp('(^| )'+className+'( |$)','gi').test(el.className);},addClass:function(el,className){if(this.hasClass(el,className))
|
||||
return el.classList.contains(className);return new RegExp('(^| )'+className+'( |$)','gi').test(el.className);},addClass:function(el,className){var classes=className.split(' ')
|
||||
for(var i=0,len=classes.length;i<len;i++){var currentClass=classes[i].trim()
|
||||
if(this.hasClass(el,currentClass))
|
||||
return
|
||||
if(el.classList)
|
||||
el.classList.add(className);else
|
||||
el.className+=' '+className;},removeClass:function(el,className){if(el.classList)
|
||||
el.classList.add(currentClass);else
|
||||
el.className+=' '+currentClass;}},removeClass:function(el,className){if(el.classList)
|
||||
el.classList.remove(className);else
|
||||
el.className=el.className.replace(new RegExp('(^|\\b)'+className.split(' ').join('|')+'(\\b|$)','gi'),' ');},absolutePosition:function(element,ignoreScrolling){var top=ignoreScrolling===true?0:document.body.scrollTop,left=0
|
||||
do{top+=element.offsetTop||0;if(ignoreScrolling!==true)
|
||||
|
Loading…
x
Reference in New Issue
Block a user