The single-dropdown work well
This commit is contained in:
parent
c676bb727e
commit
b2639bcebe
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,3 +2,4 @@
|
|||||||
/dist
|
/dist
|
||||||
/.tmp
|
/.tmp
|
||||||
/.vscode
|
/.vscode
|
||||||
|
webpack.statistics.*
|
@ -18,10 +18,6 @@
|
|||||||
"icon": "assets/icon.png"
|
"icon": "assets/icon.png"
|
||||||
},
|
},
|
||||||
"externalJS": [
|
"externalJS": [
|
||||||
"node_modules/jquery/dist/jquery.min.js",
|
|
||||||
"src/coverJquery.js",
|
|
||||||
"node_modules/bootstrap/dist/js/bootstrap.min.js",
|
|
||||||
"node_modules/bootstrap-multiselect/dist/js/bootstrap-multiselect.min.js"
|
|
||||||
],
|
],
|
||||||
"style": "style/visual.less",
|
"style": "style/visual.less",
|
||||||
"capabilities": "capabilities.json",
|
"capabilities": "capabilities.json",
|
||||||
|
@ -14,6 +14,15 @@ export class FilterManager implements IFilterManager{
|
|||||||
constructor(host:IVisualHost){
|
constructor(host:IVisualHost){
|
||||||
this.host=host;
|
this.host=host;
|
||||||
}
|
}
|
||||||
|
public dispose():void{
|
||||||
|
console.debug('filterManager disposing');
|
||||||
|
|
||||||
|
if(this.target.table&&this.target.column){
|
||||||
|
this.clear();
|
||||||
|
}
|
||||||
|
console.debug('filterManager disposed');
|
||||||
|
|
||||||
|
}
|
||||||
public clear():void{
|
public clear():void{
|
||||||
console.debug("clear start:");
|
console.debug("clear start:");
|
||||||
this.host.applyJsonFilter(null,"general","filter",powerbi.FilterAction.remove);
|
this.host.applyJsonFilter(null,"general","filter",powerbi.FilterAction.remove);
|
||||||
|
@ -11,13 +11,22 @@ export class LayoutManager implements ILayoutManager{
|
|||||||
// constructor(layout:Selection<HTMLElement>){
|
// constructor(layout:Selection<HTMLElement>){
|
||||||
// this.layout=layout;
|
// this.layout=layout;
|
||||||
// }
|
// }
|
||||||
header:Selection<HTMLDivElement>
|
headerContainer:Selection<HTMLDivElement>
|
||||||
constructor(layout:Selection<HTMLElement>,filterManager:IFilterManager){
|
constructor(layout:Selection<HTMLElement>,filterManager:IFilterManager){
|
||||||
this.layout=layout;
|
this.layout=layout;
|
||||||
this.header=this.layout.append('div').attr('header',true);
|
this.headerContainer=layout.append("div").classed("header-container",true);
|
||||||
this.header.append('button').attr('clear',true).text('clear').on('click',function(){
|
this.headerContainer.append('button').attr('clear',true).text('clear').on('click',function(){
|
||||||
filterManager.clear();
|
filterManager.clear();
|
||||||
});
|
});
|
||||||
|
this.headerContainer.style('display','none');
|
||||||
|
}
|
||||||
|
public dispose():void{
|
||||||
|
console.debug('layoutManager disposing');
|
||||||
|
|
||||||
|
this.headerContainer.remove();
|
||||||
|
this.layout.remove();
|
||||||
|
console.debug('layoutManager disposed');
|
||||||
|
|
||||||
}
|
}
|
||||||
update(dataView:DataView,width:number,height:number):void{
|
update(dataView:DataView,width:number,height:number):void{
|
||||||
//this.layout.style("width",width+"px").style("height",height+"px");
|
//this.layout.style("width",width+"px").style("height",height+"px");
|
||||||
|
@ -21,6 +21,10 @@ export class SelectorManager implements ISelectorManager{
|
|||||||
this.selectorContainer=selectorContainer;
|
this.selectorContainer=selectorContainer;
|
||||||
this.filterManager=filterManager;
|
this.filterManager=filterManager;
|
||||||
}
|
}
|
||||||
|
public dispose():void{
|
||||||
|
this.selector.dispose();
|
||||||
|
this.selectorContainer.remove();
|
||||||
|
}
|
||||||
public switchSelector<T extends ISelector>(classSelector:new ()=>T){
|
public switchSelector<T extends ISelector>(classSelector:new ()=>T){
|
||||||
let newSelector=new classSelector();
|
let newSelector=new classSelector();
|
||||||
this.selector?.dispose();
|
this.selector?.dispose();
|
||||||
@ -56,6 +60,30 @@ interface ISelector{
|
|||||||
}
|
}
|
||||||
|
|
||||||
abstract class Selector implements ISelector{
|
abstract class Selector implements ISelector{
|
||||||
|
protected field: powerbi.DataViewCategoryColumn;
|
||||||
|
protected defaultSelect: powerbi.DataViewValueColumn;
|
||||||
|
protected defaultStart: powerbi.DataViewValueColumn;
|
||||||
|
protected defaultEnd: powerbi.DataViewValueColumn;
|
||||||
|
protected checkFieldChange(field: powerbi.DataViewCategoryColumn):boolean{
|
||||||
|
//Field is not null, so first check the old field
|
||||||
|
if(!this.field){
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if(field.source.queryName==this.field.source.queryName){
|
||||||
|
console.debug('checkFieldChange: false');
|
||||||
|
return false;
|
||||||
|
}else{
|
||||||
|
console.debug('checkFieldChange: true');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
protected abstract checkDefaultSelectionChange(defaultSelect: powerbi.DataViewValueColumn, defaultStart: powerbi.DataViewValueColumn, defaultEnd: powerbi.DataViewValueColumn):boolean;
|
||||||
|
protected checkFieldAndDefaultSelectionChange(field: powerbi.DataViewCategoryColumn, defaultSelect: powerbi.DataViewValueColumn, defaultStart: powerbi.DataViewValueColumn, defaultEnd: powerbi.DataViewValueColumn){
|
||||||
|
console.debug('checkFieldAndDefaultSelectionChange start');
|
||||||
|
let flag:boolean=this.checkFieldChange(field)||this.checkDefaultSelectionChange(defaultSelect,defaultStart,defaultEnd)
|
||||||
|
console.debug('checkFieldAndDefaultSelectionChange end, result:',flag);
|
||||||
|
return flag;
|
||||||
|
}
|
||||||
constructor(manager: ISelectorManager){
|
constructor(manager: ISelectorManager){
|
||||||
console.debug("Abstract selector:","constructor start");
|
console.debug("Abstract selector:","constructor start");
|
||||||
this.manager=manager;
|
this.manager=manager;
|
||||||
@ -67,59 +95,105 @@ abstract class Selector implements ISelector{
|
|||||||
public abstract update(field: powerbi.DataViewCategoryColumn, defaultSelect: powerbi.DataViewValueColumn, defaultStart: powerbi.DataViewValueColumn, defaultEnd: powerbi.DataViewValueColumn):void;
|
public abstract update(field: powerbi.DataViewCategoryColumn, defaultSelect: powerbi.DataViewValueColumn, defaultStart: powerbi.DataViewValueColumn, defaultEnd: powerbi.DataViewValueColumn):void;
|
||||||
}
|
}
|
||||||
class DropDownSelector extends Selector{
|
class DropDownSelector extends Selector{
|
||||||
|
protected checkDefaultSelectionChange(defaultSelect: powerbi.DataViewValueColumn, defaultStart: powerbi.DataViewValueColumn, defaultEnd: powerbi.DataViewValueColumn): boolean {
|
||||||
|
//The defaultSelect is nullable, so first check the new defaultSelect
|
||||||
|
console.debug('checkDefaultSelectionChange start');
|
||||||
|
let flag:boolean;
|
||||||
|
if(!defaultSelect||!defaultSelect.values||!defaultSelect.values[0]){
|
||||||
|
console.debug('new defaultSelect is null, set flag=false')
|
||||||
|
flag=flag||false;
|
||||||
|
}else{
|
||||||
|
if(!this.defaultSelect||!this.defaultSelect.values||!defaultSelect.values[0]){
|
||||||
|
console.debug('previous defaultSelect is null, set flag=true');
|
||||||
|
flag=flag||true;
|
||||||
|
}else{
|
||||||
|
if(defaultSelect.values[0].toString()!=this.defaultSelect.values[0].toString()){
|
||||||
|
console.debug('new defaultSelect not equal to the previous, set flag=true');
|
||||||
|
flag=flag||true;
|
||||||
|
}else{
|
||||||
|
console.debug('new defaultSelect equal to the previous, set flag=false');
|
||||||
|
flag=flag||false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.debug('checkDefaultSelectionChange end, result:',flag);
|
||||||
|
return flag;
|
||||||
|
}
|
||||||
private dropDown:Selection<HTMLSelectElement>;
|
private dropDown:Selection<HTMLSelectElement>;
|
||||||
protected createView() {
|
protected createView() {
|
||||||
console.debug('dropDownViewManager','createView start');
|
console.debug('dropDownViewManager','createView start');
|
||||||
this.dropDown=this.manager.selectorContainer.append("select").classed("dropDown-selector",true).classed("selector",true).attr("multiple",true);
|
this.dropDown=this.manager.selectorContainer.append("select").classed("dropDown-selector",true).classed("selector",true);//.attr("multiple",false);
|
||||||
|
let filterManager:IFilterManager=this.manager.filterManager;
|
||||||
|
this.dropDown.on("input change",function(){
|
||||||
|
console.debug('DropDownSelector:',"input change");
|
||||||
|
console.debug('this',this);
|
||||||
|
console.debug('filterManager:',filterManager);
|
||||||
|
let selectedValues:string[]=[];
|
||||||
|
for(let i=0;i<this.selectedOptions.length;i++){
|
||||||
|
let option=this.selectedOptions.item(i);
|
||||||
|
if(option.selected){
|
||||||
|
console.debug("option selected",option);
|
||||||
|
console.debug('option.text:',option.text);
|
||||||
|
selectedValues.push(option.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(selectedValues.length==0){
|
||||||
|
}else{
|
||||||
|
console.debug("selection.length",selectedValues.length);
|
||||||
|
filterManager.filterStringField(selectedValues);
|
||||||
|
}
|
||||||
|
console.debug("this",this.selectedOptions);
|
||||||
|
});
|
||||||
console.debug('dropDownViewManager','createView end');
|
console.debug('dropDownViewManager','createView end');
|
||||||
|
|
||||||
}
|
}
|
||||||
public dispose(){
|
public dispose(){
|
||||||
|
console.debug('dropDown-selector disposing');
|
||||||
this.dropDown.remove();
|
this.dropDown.remove();
|
||||||
|
console.debug('dropDown-selector disposed');
|
||||||
|
|
||||||
}
|
}
|
||||||
public update(field: powerbi.DataViewCategoryColumn, defaultSelect: powerbi.DataViewValueColumn, defaultStart: powerbi.DataViewValueColumn, defaultEnd: powerbi.DataViewValueColumn) {
|
public update(field: powerbi.DataViewCategoryColumn, defaultSelect: powerbi.DataViewValueColumn, defaultStart: powerbi.DataViewValueColumn, defaultEnd: powerbi.DataViewValueColumn) {
|
||||||
console.debug('dropDownViewManager','update start');
|
console.debug('dropDownViewManager','update start');
|
||||||
let options=this.dropDown.selectAll("option").data(field.values,function(d){return d.toString();});//map data
|
//Check field, defaultSelect are changed or not
|
||||||
options.enter().append("option").text(function(d){return d.toString();}).attr('dropDown-option');//add
|
let needUpdateDefaultSelection:boolean=this.checkFieldAndDefaultSelectionChange(field,defaultSelect,defaultStart,defaultEnd);
|
||||||
|
let newDefaultSelect:string;
|
||||||
|
if(needUpdateDefaultSelection&&defaultSelect&&defaultSelect.values&&defaultSelect.values[0]){
|
||||||
|
newDefaultSelect=defaultSelect.values[0].toString();
|
||||||
|
console.debug('newDefaultSelect:',newDefaultSelect);
|
||||||
|
}
|
||||||
|
let options:d3.Selection<HTMLOptionElement, powerbi.PrimitiveValue, HTMLSelectElement, any>=<d3.Selection<HTMLOptionElement, powerbi.PrimitiveValue, HTMLSelectElement, any>>this.dropDown.selectAll("option").data(field.values,function(d){return d.toString();});//map data
|
||||||
options.exit().remove();//delete
|
options.exit().remove();//delete
|
||||||
console.debug("dropDown:",this.dropDown);
|
|
||||||
let filterManager:IFilterManager=this.manager.filterManager;
|
|
||||||
// this.dropDown.on("input change",function(){
|
|
||||||
// console.debug('DropDownSelector:',"input change");
|
|
||||||
// console.debug('this',this);
|
|
||||||
// console.debug('filterManager:',filterManager);
|
|
||||||
// let selectedValues:string[]=[];
|
|
||||||
// for(let i=0;i<this.selectedOptions.length;i++){
|
|
||||||
// let option=this.selectedOptions.item(i);
|
|
||||||
// if(option.selected){
|
|
||||||
// console.debug("option selected",option);
|
|
||||||
// console.debug('option.text:',option.text);
|
|
||||||
// selectedValues.push(option.text);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// if(selectedValues.length==0){
|
|
||||||
// }else{
|
|
||||||
// console.debug("selection.length",selectedValues.length);
|
|
||||||
// filterManager.filterStringField(selectedValues);
|
|
||||||
// }
|
|
||||||
// console.debug("this",this.selectedOptions);
|
|
||||||
// });
|
|
||||||
|
|
||||||
$('.dropDown-selector').multiselect({
|
if(needUpdateDefaultSelection&&newDefaultSelect){
|
||||||
onChange:function(){
|
console.debug('reset defaultSelection start');
|
||||||
let selectedValues:string[]=[];
|
options=options.enter().append("option")
|
||||||
$('option:selected').map(function(a,item){
|
.text(function(d){return d.toString();})
|
||||||
selectedValues.push(item.textContent);
|
.attr('label',function(d){return d.toString();})
|
||||||
console.debug('item:',item.textContent);
|
.classed('dropDown-option',true)
|
||||||
filterManager.filterStringField(selectedValues);
|
.merge(options)
|
||||||
});
|
.property("selected",function(d){
|
||||||
},
|
console.debug('d.toString():',d.toString());
|
||||||
maxHeight:100,
|
console.debug('newDefaultSelect:',newDefaultSelect);
|
||||||
includeSelectAllOption:true
|
return (d.toString()==newDefaultSelect)?'selected':null;
|
||||||
});
|
});
|
||||||
|
this.dropDown.dispatch('change');
|
||||||
|
console.debug('reset defaultSelection end');
|
||||||
|
}else{
|
||||||
|
console.debug('defaultSelection not reset, start update options');
|
||||||
|
options.enter().append("option").text(function(d){return d.toString();}).attr('label',function(d){return d.toString();}).classed('dropDown-option',true);//add
|
||||||
|
console.debug('defaultSelection not reset, end update options');
|
||||||
|
}
|
||||||
|
console.debug("dropDown:",this.dropDown);
|
||||||
|
this.field=field;
|
||||||
|
this.defaultSelect=defaultSelect;
|
||||||
console.debug('dropDownViewManager','update end');
|
console.debug('dropDownViewManager','update end');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class ListSelector extends Selector{
|
class ListSelector extends Selector{
|
||||||
|
protected checkDefaultSelectionChange(defaultSelect: powerbi.DataViewValueColumn, defaultStart: powerbi.DataViewValueColumn, defaultEnd: powerbi.DataViewValueColumn): boolean {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
public dispose() {
|
public dispose() {
|
||||||
throw new Error("Method not implemented.");
|
throw new Error("Method not implemented.");
|
||||||
}
|
}
|
||||||
@ -132,6 +206,9 @@ class ListSelector extends Selector{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
class CalendarSelector extends Selector{
|
class CalendarSelector extends Selector{
|
||||||
|
protected checkDefaultSelectionChange(defaultSelect: powerbi.DataViewValueColumn, defaultStart: powerbi.DataViewValueColumn, defaultEnd: powerbi.DataViewValueColumn): boolean {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
public dispose() {
|
public dispose() {
|
||||||
throw new Error("Method not implemented.");
|
throw new Error("Method not implemented.");
|
||||||
}
|
}
|
||||||
|
@ -54,7 +54,6 @@ export class Visual implements IVisual {
|
|||||||
private settings: VisualSettings;
|
private settings: VisualSettings;
|
||||||
private layoutManager: visualInterfaces.ILayoutManager;
|
private layoutManager: visualInterfaces.ILayoutManager;
|
||||||
private selectorManager: visualInterfaces.ISelectorManager;
|
private selectorManager: visualInterfaces.ISelectorManager;
|
||||||
private view:SVGElement;
|
|
||||||
private filterManager:visualInterfaces.IFilterManager;
|
private filterManager:visualInterfaces.IFilterManager;
|
||||||
private selectionManager:powerbi.extensibility.ISelectionManager;
|
private selectionManager:powerbi.extensibility.ISelectionManager;
|
||||||
constructor(options: VisualConstructorOptions) {
|
constructor(options: VisualConstructorOptions) {
|
||||||
@ -63,6 +62,8 @@ export class Visual implements IVisual {
|
|||||||
if (document) {
|
if (document) {
|
||||||
this.selectionManager =options.host.createSelectionManager();
|
this.selectionManager =options.host.createSelectionManager();
|
||||||
let container=d3.select(options.element).append("div").classed("container",true);
|
let container=d3.select(options.element).append("div").classed("container",true);
|
||||||
|
|
||||||
|
//overload context menu
|
||||||
// d3.select(options.element).on('contextmenu', () => {
|
// d3.select(options.element).on('contextmenu', () => {
|
||||||
// const mouseEvent: MouseEvent = <MouseEvent>d3.event;
|
// const mouseEvent: MouseEvent = <MouseEvent>d3.event;
|
||||||
// //const eventTarget: EventTarget = mouseEvent.target;
|
// //const eventTarget: EventTarget = mouseEvent.target;
|
||||||
@ -73,12 +74,14 @@ export class Visual implements IVisual {
|
|||||||
// });
|
// });
|
||||||
// //mouseEvent.preventDefault();
|
// //mouseEvent.preventDefault();
|
||||||
// });
|
// });
|
||||||
let headerContainer=container.append("div").classed("header-container",true);
|
|
||||||
let selectorContainer=container.append("div").classed("selector-container",true);
|
|
||||||
//First, create the filterManager
|
//First, create the filterManager
|
||||||
this.filterManager=ManagerFactory.CreateFilterManager(FilterManager,options.host);
|
this.filterManager=ManagerFactory.CreateFilterManager(FilterManager,options.host);
|
||||||
//Then, layoutManager and selectorManager
|
//Then, layoutManager and selectorManager, both use the filterManager
|
||||||
|
//Lagyoutmanager manage the whole div and the slicer-header
|
||||||
this.layoutManager=ManagerFactory.CreateLayoutManager(LayoutManager,container,this.filterManager);
|
this.layoutManager=ManagerFactory.CreateLayoutManager(LayoutManager,container,this.filterManager);
|
||||||
|
//SelectorManager manage selectors
|
||||||
|
let selectorContainer=container.append("div").classed("selector-container",true);
|
||||||
this.selectorManager=ManagerFactory.CreateSelectorManager(SelectorManager,selectorContainer,this.filterManager);
|
this.selectorManager=ManagerFactory.CreateSelectorManager(SelectorManager,selectorContainer,this.filterManager);
|
||||||
}
|
}
|
||||||
console.debug('end constructor');
|
console.debug('end constructor');
|
||||||
@ -112,12 +115,14 @@ export class Visual implements IVisual {
|
|||||||
console.debug("start layoutManager updateView");
|
console.debug("start layoutManager updateView");
|
||||||
this.layoutManager.update(dataView,width-10,height-10);
|
this.layoutManager.update(dataView,width-10,height-10);
|
||||||
|
|
||||||
console.debug("start selectorManager updateView");
|
|
||||||
this.selectorManager.updateData(field,defaultSelect,defaultStart,defaultEnd);
|
|
||||||
|
|
||||||
console.debug("start filterManager updateView");
|
console.debug("start filterManager updateView");
|
||||||
this.filterManager.update(field);
|
this.filterManager.update(field);
|
||||||
this.events.renderingFinished(options);
|
this.events.renderingFinished(options);
|
||||||
|
|
||||||
|
console.debug("start selectorManager updateView");
|
||||||
|
this.selectorManager.updateData(field,defaultSelect,defaultStart,defaultEnd);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static parseSettings(dataView: DataView): VisualSettings {
|
private static parseSettings(dataView: DataView): VisualSettings {
|
||||||
|
@ -14,6 +14,7 @@ Main interfaces
|
|||||||
export interface ILayoutManager{
|
export interface ILayoutManager{
|
||||||
layout:Selection<HTMLElement>;
|
layout:Selection<HTMLElement>;
|
||||||
update(dataView:DataView,width:number,height:number):void;
|
update(dataView:DataView,width:number,height:number):void;
|
||||||
|
dispose():void;
|
||||||
}
|
}
|
||||||
export interface ILayoutManagerConstructor{
|
export interface ILayoutManagerConstructor{
|
||||||
new(container:Selection<HTMLElement>,filterManager:IFilterManager):ILayoutManager;
|
new(container:Selection<HTMLElement>,filterManager:IFilterManager):ILayoutManager;
|
||||||
@ -23,19 +24,20 @@ export interface ILayoutManagerConstructor{
|
|||||||
export interface ISelectorManager{
|
export interface ISelectorManager{
|
||||||
selectorContainer:Selection<HTMLElement>;
|
selectorContainer:Selection<HTMLElement>;
|
||||||
filterManager:IFilterManager;
|
filterManager:IFilterManager;
|
||||||
updateData(field:powerbi.DataViewCategoryColumn,defaultSelect:powerbi.DataViewValueColumn,defaultStart:powerbi.DataViewValueColumn,defaultEnd:powerbi.DataViewValueColumn);
|
dispose():void;
|
||||||
|
updateData(field:powerbi.DataViewCategoryColumn,defaultSelect:powerbi.DataViewValueColumn,defaultStart:powerbi.DataViewValueColumn,defaultEnd:powerbi.DataViewValueColumn):void;
|
||||||
}
|
}
|
||||||
export interface ISelectorManagerConstructor{
|
export interface ISelectorManagerConstructor{
|
||||||
new(selectorContainer:Selection<HTMLElement>,filterManager:IFilterManager):ISelectorManager;
|
new(selectorContainer:Selection<HTMLElement>,filterManager:IFilterManager):ISelectorManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//IFilterManager
|
//IFilterManager
|
||||||
export interface IFilterManager{
|
export interface IFilterManager{
|
||||||
update(field:powerbi.DataViewCategoryColumn):void;
|
update(field:powerbi.DataViewCategoryColumn):void;
|
||||||
filterStringField(selection:string[]):void;
|
filterStringField(selection:string[]):void;
|
||||||
clear():void;
|
clear():void;
|
||||||
|
dispose():void;
|
||||||
}
|
}
|
||||||
export interface IFilterManagerConstructor{
|
export interface IFilterManagerConstructor{
|
||||||
new(host:IVisualHost):IFilterManager;
|
new(host:IVisualHost):IFilterManager;
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
@import "/node_modules/bootstrap/dist/css/bootstrap.min.css";
|
|
||||||
@import "/node_modules/bootstrap-multiselect/dist/css/bootstrap-multiselect.css";
|
|
||||||
p {
|
p {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user