LT-242: Sharing same timepoints source among all the sub-templates that use it
This commit is contained in:
parent
a7ca825755
commit
a4354efa15
@ -1,10 +1,10 @@
|
|||||||
<template name="flexboxLayout">
|
<template name="flexboxLayout">
|
||||||
<div class="viewerSection">
|
<div class="viewerSection">
|
||||||
<div class="sidebarMenu sidebar-left {{#if leftSidebarOpen}}sidebar-open{{/if}}">
|
<div class="sidebarMenu sidebar-left {{#if leftSidebarOpen}}sidebar-open{{/if}}">
|
||||||
{{>studyTimepointBrowser}}
|
{{>studyTimepointBrowser (extend this)}}
|
||||||
</div>
|
</div>
|
||||||
<div class="mainContent {{#if leftSidebarOpen}}sidebar-left-open{{/if}} {{#if rightSidebarOpen}}sidebar-right-open{{/if}}">
|
<div class="mainContent {{#if leftSidebarOpen}}sidebar-left-open{{/if}} {{#if rightSidebarOpen}}sidebar-right-open{{/if}}">
|
||||||
{{>viewerMain}}
|
{{>viewerMain (extend this)}}
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebarMenu sidebar-right {{#if or lesionSidebarOpen additionalFindingsSidebarOpen}}sidebar-open{{/if}}">
|
<div class="sidebarMenu sidebar-right {{#if or lesionSidebarOpen additionalFindingsSidebarOpen}}sidebar-open{{/if}}">
|
||||||
<div class="sidebar-option {{#if lesionSidebarOpen}}active{{/if}}">
|
<div class="sidebar-option {{#if lesionSidebarOpen}}active{{/if}}">
|
||||||
|
|||||||
@ -4,10 +4,10 @@
|
|||||||
{{>roundedButtonGroup buttonGroupData}}
|
{{>roundedButtonGroup buttonGroupData}}
|
||||||
</div>
|
</div>
|
||||||
<div class="lesionTableTimepointHeaderRow">
|
<div class="lesionTableTimepointHeaderRow">
|
||||||
{{#each timepoints.get}}
|
{{#each this.timepoints.get}}
|
||||||
{{>lesionTableTimepointHeader}}
|
{{>lesionTableTimepointHeader}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</div>
|
</div>
|
||||||
{{>lesionTableView this}}
|
{{>lesionTableView (extend this)}}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -4,23 +4,16 @@ Template.lesionTable.onCreated(() => {
|
|||||||
instance.data.lesionTableLayout = new ReactiveVar('comparison');
|
instance.data.lesionTableLayout = new ReactiveVar('comparison');
|
||||||
instance.data.timepoints = new ReactiveVar([]);
|
instance.data.timepoints = new ReactiveVar([]);
|
||||||
|
|
||||||
|
// Run this computation everytime table layout changes
|
||||||
instance.autorun(() => {
|
instance.autorun(() => {
|
||||||
// Get the current table layout
|
// Get the current table layout
|
||||||
const tableLayout = instance.data.lesionTableLayout.get();
|
const tableLayout = instance.data.lesionTableLayout.get();
|
||||||
|
|
||||||
// Get all the timepoints
|
let timepoints;
|
||||||
const allTimepoints = Timepoints.find({}, {
|
if (tableLayout === 'key') {
|
||||||
sort: {
|
timepoints = instance.data.timepointApi.key();
|
||||||
latestDate: -1
|
} else {
|
||||||
}
|
timepoints = instance.data.timepointApi.latest();
|
||||||
}).fetch();
|
|
||||||
|
|
||||||
// Get the last 2 timepoints
|
|
||||||
let timepoints = allTimepoints.slice(0, 2);
|
|
||||||
|
|
||||||
// Concatenate the baseline if the table layout is for key timepoints
|
|
||||||
if (tableLayout === 'key' && allTimepoints.length > 2) {
|
|
||||||
timepoints = timepoints.concat(_.last(allTimepoints));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return key timepoints
|
// Return key timepoints
|
||||||
@ -35,7 +28,7 @@ Template.lesionTable.onRendered(() => {
|
|||||||
// Run this computation everytime the lesion table layout is changed
|
// Run this computation everytime the lesion table layout is changed
|
||||||
instance.data.lesionTableLayout.dep.depend();
|
instance.data.lesionTableLayout.dep.depend();
|
||||||
|
|
||||||
if(instance.data.state.get('rightSidebar') !== 'lesions') {
|
if (instance.data.state.get('rightSidebar') !== 'lesions') {
|
||||||
// Remove the amount attribute from sidebar element tag
|
// Remove the amount attribute from sidebar element tag
|
||||||
instance.$('#lesionTableContainer').closest('.sidebarMenu').removeAttr('data-timepoints');
|
instance.$('#lesionTableContainer').closest('.sidebarMenu').removeAttr('data-timepoints');
|
||||||
return;
|
return;
|
||||||
@ -82,10 +75,6 @@ Template.lesionTable.events({
|
|||||||
});
|
});
|
||||||
|
|
||||||
Template.lesionTable.helpers({
|
Template.lesionTable.helpers({
|
||||||
dataContainer() {
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
|
|
||||||
buttonGroupData() {
|
buttonGroupData() {
|
||||||
const instance = Template.instance();
|
const instance = Template.instance();
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
<use xlink:href="/packages/lesiontracker/assets/icons.svg#icon-ui-close"></use>
|
<use xlink:href="/packages/lesiontracker/assets/icons.svg#icon-ui-close"></use>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
{{>lesionTableView}}
|
{{>lesionTableView (extend this)}}
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
{{#each toolbarButton in toolbarButtons}}
|
{{#each toolbarButton in toolbarButtons}}
|
||||||
{{>toolbarSectionButton toolbarButton}}
|
{{>toolbarSectionButton toolbarButton}}
|
||||||
|
|||||||
@ -1,3 +1,9 @@
|
|||||||
|
Template.lesionTableHUD.onCreated(() => {
|
||||||
|
const instance = Template.instance();
|
||||||
|
|
||||||
|
instance.data.timepoints = new ReactiveVar(instance.data.timepointApi.latest());
|
||||||
|
});
|
||||||
|
|
||||||
Template.lesionTableHUD.onRendered(() => {
|
Template.lesionTableHUD.onRendered(() => {
|
||||||
const instance = Template.instance();
|
const instance = Template.instance();
|
||||||
instance.$('#lesionTableHUD').resizable().draggable();
|
instance.$('#lesionTableHUD').resizable().draggable();
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
<div class="studyBox"></div>
|
<div class="studyBox"></div>
|
||||||
<div class="switchHover studyHover clearfix">
|
<div class="switchHover studyHover clearfix">
|
||||||
<div class="scrollArea">
|
<div class="scrollArea">
|
||||||
{{>studyTimepointBrowser timepointViewType='key' currentStudy=this.currentStudy viewportIndex=this.viewportIndex}}
|
{{>studyTimepointBrowser (extend this timepointViewType='key')}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -8,24 +8,22 @@
|
|||||||
<div class="p-l-1">
|
<div class="p-l-1">
|
||||||
{{#if timepointList.length}}
|
{{#if timepointList.length}}
|
||||||
{{#each timepoint in timepointList}}
|
{{#each timepoint in timepointList}}
|
||||||
{{#if shallShowTimepoint timepoint @index}}
|
<div class="timepointEntry p-y-2">
|
||||||
<div class="timepointEntry p-y-2">
|
<div class="timepointHeader">
|
||||||
<div class="timepointHeader">
|
<div class="timepointDetails clearfix">
|
||||||
<div class="timepointDetails clearfix">
|
<div class="timepointFollowupTitle pull-left">
|
||||||
<div class="timepointFollowupTitle pull-left">
|
{{timepointApi.title timepoint}}
|
||||||
{{timepointTitle timepoint timepoints.count @index}}
|
|
||||||
</div>
|
|
||||||
<div class="expandIcon pull-right">
|
|
||||||
<i class="fa fa-chevron-down"></i>
|
|
||||||
</div>
|
|
||||||
<div class="timepointDate pull-right m-r-1">{{formatDA timepoint.earliestDate 'D-MMM-YYYY'}}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="timepointModalities">{{modalitiesSummary timepoint}}</div>
|
<div class="expandIcon pull-right">
|
||||||
|
<i class="fa fa-chevron-down"></i>
|
||||||
|
</div>
|
||||||
|
<div class="timepointDate pull-right m-r-1">{{formatDA timepoint.earliestDate 'D-MMM-YYYY'}}</div>
|
||||||
</div>
|
</div>
|
||||||
{{>studyTimepoint studies=(studies timepoint) index=@index viewportIndex=this.viewportIndex currentStudy=this.currentStudy}}
|
<div class="timepointModalities">{{modalitiesSummary timepoint}}</div>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
{{>studyTimepoint studies=(studies timepoint) index=@index viewportIndex=this.viewportIndex currentStudy=this.currentStudy}}
|
||||||
{{/if}}
|
</div>
|
||||||
|
<hr>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
{{#if and this.currentStudy (not showAdditionalTimepoints)}}
|
{{#if and this.currentStudy (not showAdditionalTimepoints)}}
|
||||||
<div class="studyModality additional">
|
<div class="studyModality additional">
|
||||||
|
|||||||
@ -116,21 +116,22 @@ Template.studyTimepointBrowser.helpers({
|
|||||||
const instance = Template.instance();
|
const instance = Template.instance();
|
||||||
// Get the current study
|
// Get the current study
|
||||||
const currentStudy = instance.getCurrentStudy();
|
const currentStudy = instance.getCurrentStudy();
|
||||||
// Build the query
|
// Declare the timepoints
|
||||||
const query = {};
|
let timepoints;
|
||||||
if (currentStudy && !instance.showAdditionalTimepoints.get()) {
|
if (currentStudy && !instance.showAdditionalTimepoints.get()) {
|
||||||
query['studyInstanceUids'] = {
|
// Show only the current study's timepoint
|
||||||
$in: [currentStudy.studyInstanceUid]
|
timepoints = instance.data.timepointApi.study(currentStudy.studyInstanceUid);
|
||||||
};
|
} else {
|
||||||
}
|
if (instance.timepointViewType.get() === 'all') {
|
||||||
// Sort timepoints based on timeline and type
|
// Show all timepoints
|
||||||
const sort = {
|
timepoints = instance.data.timepointApi.all();
|
||||||
sort: {
|
} else {
|
||||||
latestDate: 1
|
// Show only key timepoints
|
||||||
|
timepoints = instance.data.timepointApi.key();
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
// Returns all timepoints with sorting
|
// Returns the timepoints
|
||||||
return Timepoints.find(query, sort).fetch().reverse();
|
return timepoints;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Get the studies for a specific timepoint
|
// Get the studies for a specific timepoint
|
||||||
@ -138,38 +139,6 @@ Template.studyTimepointBrowser.helpers({
|
|||||||
return Template.instance().getStudies(timepoint);
|
return Template.instance().getStudies(timepoint);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Decides if a timepoint shall be shown or omitted
|
|
||||||
shallShowTimepoint(timepoint, index) {
|
|
||||||
const instance = Template.instance();
|
|
||||||
|
|
||||||
// Show all timepoints when view type is all
|
|
||||||
if (instance.timepointViewType.get() === 'all') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always Show the timepoint for current study
|
|
||||||
const currentStudy = instance.getCurrentStudy();
|
|
||||||
if (currentStudy && _.contains(timepoint.studyInstanceUids, currentStudy.studyInstanceUid)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show only the latest timepoints and baseline
|
|
||||||
return index < 3 || timepoint.timepointType === 'baseline';
|
|
||||||
},
|
|
||||||
|
|
||||||
// Build the timepoint title based on its date
|
|
||||||
timepointTitle(timepoint, total, index) {
|
|
||||||
const timepointName = getTimepointName(timepoint);
|
|
||||||
|
|
||||||
const states = {
|
|
||||||
0: '(Current)',
|
|
||||||
1: '(Prior)'
|
|
||||||
};
|
|
||||||
// TODO: [design] find out how to define the nadir timepoint
|
|
||||||
const parenthesis = states[index] || '';
|
|
||||||
return `${timepointName} ${parenthesis}`;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Build the modalities summary for all timepoint's studies
|
// Build the modalities summary for all timepoint's studies
|
||||||
modalitiesSummary(timepoint) {
|
modalitiesSummary(timepoint) {
|
||||||
const instance = Template.instance();
|
const instance = Template.instance();
|
||||||
|
|||||||
@ -27,10 +27,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{#if splitView}}
|
{{#if splitView}}
|
||||||
{{>studySeriesQuickSwitch side="right" viewportIndex=1}}
|
{{>studySeriesQuickSwitch (extend this side="right" viewportIndex=1)}}
|
||||||
{{>studySeriesQuickSwitch side="left" viewportIndex=0}}
|
{{>studySeriesQuickSwitch (extend this side="left" viewportIndex=0)}}
|
||||||
{{else}}
|
{{else}}
|
||||||
{{>studySeriesQuickSwitch side="middle" viewportIndex=0}}
|
{{>studySeriesQuickSwitch (extend this side="middle" viewportIndex=0)}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<div class="toolbarSectionEntry pull-right rm-l-1 p-x-1">
|
<div class="toolbarSectionEntry pull-right rm-l-1 p-x-1">
|
||||||
|
|||||||
@ -5,12 +5,12 @@
|
|||||||
{{>lesionLocationDialog}}
|
{{>lesionLocationDialog}}
|
||||||
{{>nonTargetLesionDialog}}
|
{{>nonTargetLesionDialog}}
|
||||||
{{>nonTargetResponseDialog}}
|
{{>nonTargetResponseDialog}}
|
||||||
{{>lesionTableHUD}}
|
{{>lesionTableHUD (extend this)}}
|
||||||
{{! >conformanceCheckFeedback}}
|
{{! >conformanceCheckFeedback}}
|
||||||
</div>
|
</div>
|
||||||
<div id="viewer">
|
<div id="viewer">
|
||||||
{{>toolbarSection}}
|
{{>toolbarSection (extend this)}}
|
||||||
{{>flexboxLayout}}
|
{{>flexboxLayout (extend this)}}
|
||||||
</div>
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
{{>loadingText}}
|
{{>loadingText}}
|
||||||
|
|||||||
@ -1,21 +1,26 @@
|
|||||||
|
import { TimepointApi } from 'meteor/lesiontracker/lib/api/timepoint';
|
||||||
|
|
||||||
Session.setDefault('activeViewport', false);
|
Session.setDefault('activeViewport', false);
|
||||||
Session.setDefault('leftSidebar', null);
|
Session.setDefault('leftSidebar', null);
|
||||||
Session.setDefault('rightSidebar', null);
|
Session.setDefault('rightSidebar', null);
|
||||||
|
|
||||||
Template.viewer.onCreated(function() {
|
Template.viewer.onCreated(() => {
|
||||||
|
const instance = Template.instance();
|
||||||
|
|
||||||
|
instance.data.timepointApi = new TimepointApi();
|
||||||
|
|
||||||
// Attach the Window resize listener
|
// Attach the Window resize listener
|
||||||
$(window).on('resize', handleResize);
|
$(window).on('resize', handleResize);
|
||||||
|
|
||||||
ValidationErrors.remove({});
|
ValidationErrors.remove({});
|
||||||
|
|
||||||
var instance = this;
|
|
||||||
instance.data.state = new ReactiveDict();
|
instance.data.state = new ReactiveDict();
|
||||||
instance.data.state.set('leftSidebar', Session.get('leftSidebar'));
|
instance.data.state.set('leftSidebar', Session.get('leftSidebar'));
|
||||||
instance.data.state.set('rightSidebar', Session.get('rightSidebar'));
|
instance.data.state.set('rightSidebar', Session.get('rightSidebar'));
|
||||||
|
|
||||||
Session.set('currentTimepointId', instance.data.currentTimepointId);
|
Session.set('currentTimepointId', instance.data.currentTimepointId);
|
||||||
|
|
||||||
var contentId = this.data.contentId;
|
var contentId = instance.data.contentId;
|
||||||
|
|
||||||
OHIF = OHIF || window.OHIF || {
|
OHIF = OHIF || window.OHIF || {
|
||||||
viewer: {}
|
viewer: {}
|
||||||
@ -92,11 +97,11 @@ Template.viewer.onCreated(function() {
|
|||||||
ViewerData[contentId].loadedSeriesData = OHIF.viewer.loadedSeriesData;
|
ViewerData[contentId].loadedSeriesData = OHIF.viewer.loadedSeriesData;
|
||||||
|
|
||||||
// Update the viewer data object
|
// Update the viewer data object
|
||||||
if (!this.data.timepointIds || this.data.timepointIds.length <= 1) {
|
if (!instance.data.timepointIds || instance.data.timepointIds.length <= 1) {
|
||||||
// Update the viewer data object
|
// Update the viewer data object
|
||||||
ViewerData[contentId].viewportColumns = 1;
|
ViewerData[contentId].viewportColumns = 1;
|
||||||
ViewerData[contentId].viewportRows = 1;
|
ViewerData[contentId].viewportRows = 1;
|
||||||
} else if (this.data.timepointIds.length > 1) {
|
} else if (instance.data.timepointIds.length > 1) {
|
||||||
ViewerData[contentId].viewportColumns = 2;
|
ViewerData[contentId].viewportColumns = 2;
|
||||||
ViewerData[contentId].viewportRows = 1;
|
ViewerData[contentId].viewportRows = 1;
|
||||||
}
|
}
|
||||||
@ -113,12 +118,12 @@ Template.viewer.onCreated(function() {
|
|||||||
// Update the ViewerStudies collection with the loaded studies
|
// Update the ViewerStudies collection with the loaded studies
|
||||||
ViewerStudies.remove({});
|
ViewerStudies.remove({});
|
||||||
|
|
||||||
this.data.studies.forEach(function(study) {
|
instance.data.studies.forEach(function(study) {
|
||||||
study.selected = true;
|
study.selected = true;
|
||||||
ViewerStudies.insert(study);
|
ViewerStudies.insert(study);
|
||||||
});
|
});
|
||||||
|
|
||||||
var patientId = this.data.studies[0].patientId;
|
var patientId = instance.data.studies[0].patientId;
|
||||||
Session.set('patientId', patientId);
|
Session.set('patientId', patientId);
|
||||||
|
|
||||||
instance.autorun(function() {
|
instance.autorun(function() {
|
||||||
|
|||||||
88
Packages/lesiontracker/lib/api/timepoint.js
Normal file
88
Packages/lesiontracker/lib/api/timepoint.js
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
class TimepointApi {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// Run this computation everytime the timepoints are changed
|
||||||
|
Tracker.autorun(() => {
|
||||||
|
// Get all the timepoints and store it
|
||||||
|
this.timepoints = new Mongo.Collection(null);
|
||||||
|
const timepoints = Timepoints.find({}, {
|
||||||
|
sort: {
|
||||||
|
latestDate: -1
|
||||||
|
}
|
||||||
|
}).fetch();
|
||||||
|
_.each(timepoints, timepoint => this.timepoints.insert(timepoint));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return all timepoints
|
||||||
|
all() {
|
||||||
|
return this.timepoints.find().fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return only the current and prior timepoints
|
||||||
|
latest() {
|
||||||
|
const options = {
|
||||||
|
limit: 2
|
||||||
|
};
|
||||||
|
return this.timepoints.find({}, options).fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return only the key timepoints (current, prior, nadir and baseline)
|
||||||
|
key() {
|
||||||
|
// Create a new Mini Mongo Collection to store the result
|
||||||
|
const result = new Mongo.Collection(null);
|
||||||
|
|
||||||
|
// Get all the timepoints
|
||||||
|
const all = this.all();
|
||||||
|
|
||||||
|
// Iterate over each timepoint and insert the key ones in the result
|
||||||
|
_.each(all, (timepoint, index) => {
|
||||||
|
if (index < 2 || index === (all.length - 1)) {
|
||||||
|
result.insert(timepoint);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return the resulting timepoints
|
||||||
|
return result.find().fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return only the timepoints for the given study
|
||||||
|
study(studyInstanceUid) {
|
||||||
|
// Create a new Mini Mongo Collection to store the result
|
||||||
|
const result = new Mongo.Collection(null);
|
||||||
|
|
||||||
|
// Iterate over each timepoint and insert the key ones in the result
|
||||||
|
_.each(this.all(), (timepoint, index) => {
|
||||||
|
if (_.contains(timepoint.studyInstanceUids, studyInstanceUid)) {
|
||||||
|
result.insert(timepoint);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return the resulting timepoints
|
||||||
|
return result.find().fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the timepoint title based on its date
|
||||||
|
title(timepoint) {
|
||||||
|
const timepointName = getTimepointName(timepoint);
|
||||||
|
|
||||||
|
const all = this.all();
|
||||||
|
let index = -1;
|
||||||
|
_.each(all, (currentTimepoint, currentIndex) => {
|
||||||
|
if (currentTimepoint.timepointId === timepoint.timepointId) {
|
||||||
|
index = currentIndex;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const states = {
|
||||||
|
0: '(Current)',
|
||||||
|
1: '(Prior)'
|
||||||
|
};
|
||||||
|
// TODO: [design] find out how to define the nadir timepoint
|
||||||
|
const parenthesis = states[index] || '';
|
||||||
|
return `${timepointName} ${parenthesis}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export { TimepointApi };
|
||||||
@ -246,6 +246,9 @@ Package.onUse(function(api) {
|
|||||||
api.addFiles('lib/handleMeasurementModified.js', 'client');
|
api.addFiles('lib/handleMeasurementModified.js', 'client');
|
||||||
api.addFiles('lib/handleMeasurementRemoved.js', 'client');
|
api.addFiles('lib/handleMeasurementRemoved.js', 'client');
|
||||||
|
|
||||||
|
// API classes
|
||||||
|
api.addFiles('lib/api/timepoint.js');
|
||||||
|
|
||||||
// Export global functions
|
// Export global functions
|
||||||
api.export('pixelSpacingAutorunCheck', 'client');
|
api.export('pixelSpacingAutorunCheck', 'client');
|
||||||
api.export('handleMeasurementAdded', 'client');
|
api.export('handleMeasurementAdded', 'client');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user