Final adjustments to routing behaviors
This commit is contained in:
1 parent
9ccebb76bd
commit
e12fb0de35
24 files changed
+226
-362
No files matched your search
@@ -1,5 +1,4 @@
|
||||
// StudyList-related functions
|
||||
import './studylist/openNewTabWithTimepoint.js';
|
||||
import './studylist/studylistModification.js';
|
||||
|
||||
// Library functions
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
/**
|
||||
* Opens a new tab in the tabbed studylist environment using
|
||||
* a given timepoint and new tab title.
|
||||
*
|
||||
* @param timepointId The UID of the Timepoint to be opened
|
||||
* @param title The title to be used for the tab heading
|
||||
*/
|
||||
OHIF.lesiontracker.openNewTabWithTimepoint = timepointId => {
|
||||
const Timepoints = OHIF.studylist.timepointApi.timepoints;
|
||||
const timepoint = Timepoints.findOne({
|
||||
timepointId: timepointId
|
||||
});
|
||||
|
||||
if (!timepoint) {
|
||||
throw 'No such timepoint exists';
|
||||
}
|
||||
|
||||
// Get the relevant studyInstanceUids given the timepoints
|
||||
const data = getDataFromTimepoint(timepoint);
|
||||
if (!data.studyInstanceUids) {
|
||||
throw 'No studies found that are related to this timepoint';
|
||||
}
|
||||
|
||||
// Update the OHIF.viewer.data global object
|
||||
OHIF.viewer.data = {
|
||||
studyInstanceUids: data.studyInstanceUids,
|
||||
timepointIds: data.timepointIds,
|
||||
currentTimepointId: timepointId
|
||||
};
|
||||
|
||||
// Switch to the new tab
|
||||
switchToTab(contentId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves related studies given a Baseline or Follow-up Timepoint
|
||||
*
|
||||
* @param timepoint
|
||||
* @returns {Array}
|
||||
*/
|
||||
function getDataFromTimepoint(timepoint) {
|
||||
let relatedStudies = [timepoint.studyInstanceUids[0]];
|
||||
|
||||
// If this is the baseline, we should stop here and return the relevant studies
|
||||
if (isBaseline(timepoint)) {
|
||||
return {
|
||||
studyInstanceUids: relatedStudies,
|
||||
timepointIds: [timepoint.timepointId]
|
||||
};
|
||||
}
|
||||
|
||||
// Otherwise, this is a follow-up exam, so we should also find the baseline timepoint,
|
||||
// and all studies related to it. We also enforce that the Baseline should have a studyDate
|
||||
// prior to the latest studyDate in the current (Follow-up) Timepoint.
|
||||
const Timepoints = OHIF.studylist.timepointApi.timepoints;
|
||||
const baseline = Timepoints.findOne({
|
||||
timepointType: 'baseline',
|
||||
patientId: timepoint.patientId,
|
||||
latestDate: {
|
||||
$lte: timepoint.latestDate
|
||||
}
|
||||
});
|
||||
|
||||
let timepointIds = [];
|
||||
if (baseline) {
|
||||
// NOTE: Temporarily added [0], since we only need one study per timepoint to load immediately?
|
||||
relatedStudies = relatedStudies.concat(baseline.studyInstanceUids[0]);
|
||||
timepointIds.push(baseline.timepointId);
|
||||
} else {
|
||||
OHIF.log.warn('No Baseline found while opening a Follow-up Timepoint');
|
||||
}
|
||||
|
||||
timepointIds.push(timepoint.timepointId);
|
||||
|
||||
return {
|
||||
studyInstanceUids: relatedStudies,
|
||||
timepointIds: timepointIds
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a Timepoints is a baseline or not
|
||||
* (abstracting this for later use, since I expect it to get more complex)
|
||||
*
|
||||
* @param timepoint a document from the Timepoints Collection
|
||||
* @returns {boolean} Whether or not the timepoint is stored as a Baseline
|
||||
*/
|
||||
function isBaseline(timepoint) {
|
||||
return (timepoint.timepointType === 'baseline');
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Router } from 'meteor/iron:router';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
Meteor.startup(function() {
|
||||
@@ -22,28 +23,8 @@ const dblClickOnStudy = data => {
|
||||
|
||||
const timepoint = timepointApi.study(data.studyInstanceUid)[0];
|
||||
if (timepoint) {
|
||||
OHIF.lesiontracker.openNewTabWithTimepoint(timepoint.timepointId);
|
||||
Router.go('viewerTimepoint', { timepointId: timepoint.timepointId });
|
||||
} else {
|
||||
openTab(data.studyInstanceUid);
|
||||
Router.go('viewerStudies', { studyInstanceUids: data.studyInstanceUid });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Opens a study
|
||||
*
|
||||
* @param studyInstanceUid The UID of the Study to be opened
|
||||
* @param title The title to be used for the tab heading
|
||||
*/
|
||||
const openTab = studyInstanceUid => {
|
||||
const contentId = 'viewerTab';
|
||||
|
||||
// Update the OHIF.viewer.data global object
|
||||
OHIF.viewer.data = {
|
||||
contentId: contentId,
|
||||
isUnassociatedStudy: true,
|
||||
studyInstanceUids: [studyInstanceUid]
|
||||
};
|
||||
|
||||
// Switch to the new tab
|
||||
switchToTab(contentId);
|
||||
};
|
||||
@@ -181,6 +181,6 @@ Template.caseProgress.events({
|
||||
return;
|
||||
}
|
||||
|
||||
instance.saveData().then(() => switchToTab('studylistTab'));
|
||||
instance.saveData();
|
||||
}
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Router } from 'meteor/iron:router';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
/**
|
||||
@@ -43,17 +44,9 @@ const viewStudies = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const studyInstanceUids = selectedStudies.map(study => study.studyInstanceUid);
|
||||
const contentId = 'viewerTab';
|
||||
const studyInstanceUids = selectedStudies.map(study => study.studyInstanceUid).join(';');
|
||||
|
||||
// Update the OHIF.viewer.data global object
|
||||
OHIF.viewer.data = {
|
||||
contentId: contentId,
|
||||
studyInstanceUids: studyInstanceUids
|
||||
};
|
||||
|
||||
// Switch to the new tab
|
||||
switchToTab(contentId);
|
||||
Router.go('viewerStudies', { studyInstanceUids });
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+9
-2
@@ -245,7 +245,7 @@ Template.studylistResult.onRendered(() => {
|
||||
endDate = today;
|
||||
}
|
||||
|
||||
const datePicker = $studyDate.daterangepicker({
|
||||
instance.datePicker = $studyDate.daterangepicker({
|
||||
maxDate: today,
|
||||
autoUpdateInput: true,
|
||||
startDate: startDate,
|
||||
@@ -258,7 +258,7 @@ Template.studylistResult.onRendered(() => {
|
||||
}).data('daterangepicker');
|
||||
|
||||
if (startDate && endDate) {
|
||||
datePicker.updateInputText();
|
||||
instance.datePicker.updateInputText();
|
||||
$studyDate.trigger('change');
|
||||
} else {
|
||||
// Retrieve all studies
|
||||
@@ -266,6 +266,13 @@ Template.studylistResult.onRendered(() => {
|
||||
}
|
||||
});
|
||||
|
||||
Template.studylistResult.onDestroyed(() => {
|
||||
const instance = Template.instance();
|
||||
|
||||
// Destroy the daterangepicker to prevent residual elements on DOM
|
||||
instance.datePicker.remove();
|
||||
});
|
||||
|
||||
function resetSortingColumns(instance, sortingColumn) {
|
||||
Object.keys(instance.sortingColumns.keys).forEach(key => {
|
||||
if (key !== sortingColumn) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
import { Blaze } from 'meteor/blaze';
|
||||
import * as Hammer from 'hammerjs';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
// Clear all selected studies
|
||||
|
||||
@@ -1,41 +1,27 @@
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
import { Viewerbase } from 'meteor/ohif:viewerbase';
|
||||
import { Router } from 'meteor/iron:router';
|
||||
|
||||
// Classes
|
||||
import { OHIFStudyMetadataSource } from './OHIFStudyMetadataSource';
|
||||
import { OHIFStudySummary } from './OHIFStudySummary';
|
||||
|
||||
// Functions
|
||||
import { getStudyPriors } from './getStudyPriors';
|
||||
import { getStudyPriorsMap } from './getStudyPriorsMap';
|
||||
|
||||
StudyList = {
|
||||
functions: {
|
||||
getStudyPriors,
|
||||
getStudyPriorsMap
|
||||
},
|
||||
classes: {
|
||||
OHIFStudyMetadataSource,
|
||||
OHIFStudySummary
|
||||
}
|
||||
OHIF.studylist.functions = {
|
||||
getStudyPriors,
|
||||
getStudyPriorsMap
|
||||
};
|
||||
|
||||
OHIF.studylist.classes = {
|
||||
OHIFStudyMetadataSource,
|
||||
OHIFStudySummary
|
||||
};
|
||||
|
||||
const dblClickOnStudy = data => {
|
||||
Router.go('viewerStudies', { studyInstanceUids: data.studyInstanceUid });
|
||||
};
|
||||
|
||||
OHIF.studylist.callbacks.dblClickOnStudy = dblClickOnStudy;
|
||||
OHIF.studylist.callbacks.middleClickOnStudy = dblClickOnStudy;
|
||||
|
||||
function dblClickOnStudy(data) {
|
||||
openNewTab(data.studyInstanceUid);
|
||||
}
|
||||
|
||||
let currentServerChangeHandlerFirstRun = true;
|
||||
const currentServerChangeHandler = () => {
|
||||
if (currentServerChangeHandlerFirstRun) {
|
||||
currentServerChangeHandlerFirstRun = false;
|
||||
return;
|
||||
}
|
||||
|
||||
switchToTab('studylistTab');
|
||||
};
|
||||
|
||||
CurrentServer.find().observe({
|
||||
added: currentServerChangeHandler,
|
||||
changed: currentServerChangeHandler
|
||||
});
|
||||
@@ -52,10 +52,4 @@ Package.onUse(function(api) {
|
||||
api.export('getCurrentServer', ['client', 'server']);
|
||||
|
||||
api.export('Services', 'server');
|
||||
|
||||
// Export StudyList helper functions for usage in Routes
|
||||
api.export('StudyList');
|
||||
|
||||
// Export the Collections
|
||||
api.export('StudyListSelectedStudies', 'client');
|
||||
});
|
||||
+15
-26
@@ -46,20 +46,11 @@ const loadDisplaySetIntoViewport = (data, templateData) => {
|
||||
layoutManager.viewportData[viewportIndex] = layoutManager.viewportData[viewportIndex] || {};
|
||||
layoutManager.viewportData[viewportIndex].viewportIndex = viewportIndex;
|
||||
|
||||
// Get the contentID of the current study list tab, if the viewport is running
|
||||
// alongside the study list package
|
||||
const contentId = Session.get('activeContentId');
|
||||
// Stop here if no data was defined for the viewer
|
||||
if (!OHIF.viewer.data) return;
|
||||
|
||||
// If the viewer is inside a tab, create an object related to the specified viewport
|
||||
// This data will be saved so that the tab can be reloaded to the same state after tabs
|
||||
// are switched
|
||||
if (contentId) {
|
||||
if (!OHIF.viewer.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
OHIF.viewer.data.loadedSeriesData[viewportIndex] = {};
|
||||
}
|
||||
// This data will be saved so that the viewport can be reloaded to the same state later
|
||||
OHIF.viewer.data.loadedSeriesData[viewportIndex] = {};
|
||||
|
||||
// Create shortcut to displaySet
|
||||
const displaySet = data.displaySet;
|
||||
@@ -67,7 +58,7 @@ const loadDisplaySetIntoViewport = (data, templateData) => {
|
||||
// Get stack from Stack Manager
|
||||
let stack = StackManager.findStack(displaySet.displaySetInstanceUid);
|
||||
// Make sure if the stack is already loaded in the stack manager, otherwise create it
|
||||
if(!stack || !stack.imageIds) {
|
||||
if (!stack || !stack.imageIds) {
|
||||
stack = StackManager.makeAndAddStack(data.study, displaySet);
|
||||
}
|
||||
|
||||
@@ -144,7 +135,7 @@ const loadDisplaySetIntoViewport = (data, templateData) => {
|
||||
try {
|
||||
imagePromise = cornerstone.loadAndCacheImage(imageId);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
OHIF.log.info(error);
|
||||
if (!imagePromise) {
|
||||
errorLoadingHandler(element, imageId, error);
|
||||
return;
|
||||
@@ -163,7 +154,7 @@ const loadDisplaySetIntoViewport = (data, templateData) => {
|
||||
const callbacks = imageViewerViewportData.callbacks;
|
||||
|
||||
// Check if it has before loadAndCacheImage callback
|
||||
if(typeof callbacks.before === 'function') {
|
||||
if (typeof callbacks.before === 'function') {
|
||||
OHIF.log.info('imageViewerViewport before loadAndCacheImage callback');
|
||||
callbacks.before(imagePromise, templateData);
|
||||
}
|
||||
@@ -262,8 +253,7 @@ const loadDisplaySetIntoViewport = (data, templateData) => {
|
||||
let fps;
|
||||
if (multiframeMetadata && multiframeMetadata.averageFrameRate > 0) {
|
||||
fps = multiframeMetadata.averageFrameRate;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
fps = OHIF.viewer.cine.framesPerSecond;
|
||||
}
|
||||
|
||||
@@ -431,7 +421,6 @@ const loadDisplaySetIntoViewport = (data, templateData) => {
|
||||
// Check if image plane (orientation / location) data is present for the current image
|
||||
const imagePlane = cornerstoneTools.metaData.get('imagePlane', image.imageId);
|
||||
if (imagePlane && imagePlane.frameOfReferenceUID) {
|
||||
|
||||
// If it is, add this element to the global synchronizer...
|
||||
OHIF.viewer.updateImageSynchronizer.add(element);
|
||||
|
||||
@@ -449,7 +438,6 @@ const loadDisplaySetIntoViewport = (data, templateData) => {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Set the active viewport based on the Session variable
|
||||
// This is done to ensure that the active element has the current
|
||||
// focus, so that keyboard events are triggered.
|
||||
@@ -465,8 +453,8 @@ const loadDisplaySetIntoViewport = (data, templateData) => {
|
||||
// Update the LayoutManagerUpdated session key
|
||||
layoutManager.updateSession();
|
||||
|
||||
// Check if it has after loadAndCacheImage callback
|
||||
if(typeof callbacks.after === 'function') {
|
||||
// Check if it has after loadAndCacheImage callback
|
||||
if (typeof callbacks.after === 'function') {
|
||||
OHIF.log.info('imageViewerViewport after callback');
|
||||
callbacks.after(image, templateData, element);
|
||||
}
|
||||
@@ -531,7 +519,7 @@ Template.imageViewerViewport.onRendered(function() {
|
||||
let { currentImageIdIndex } = templateData;
|
||||
const { viewport, studyInstanceUid, seriesInstanceUid, renderedCallback, displaySetInstanceUid } = templateData;
|
||||
|
||||
if(!currentImageIdIndex) {
|
||||
if (!currentImageIdIndex) {
|
||||
currentImageIdIndex = 0;
|
||||
}
|
||||
|
||||
@@ -570,8 +558,9 @@ Template.imageViewerViewport.onDestroyed(function() {
|
||||
OHIF.log.info('imageViewerViewport onDestroyed');
|
||||
|
||||
// When a viewport element is being destroyed
|
||||
var element = this.find('.imageViewerViewport');
|
||||
if (!element || $(element).hasClass('empty') || !$(element).find('canvas').length) {
|
||||
const element = this.find('.imageViewerViewport');
|
||||
const $element = $(element);
|
||||
if (!element || $element.hasClass('empty') || !$element.find('canvas').length) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -600,7 +589,7 @@ Template.imageViewerViewport.onDestroyed(function() {
|
||||
|
||||
// Trigger custom Destroy Viewport event
|
||||
// for compatibility with other systems
|
||||
$(element).trigger('OHIFDestroyedViewport');
|
||||
$element.trigger('OHIFDestroyedViewport');
|
||||
|
||||
// Disable the viewport element with Cornerstone
|
||||
// This also triggers the removal of the element from all available
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
@import "{ohif:design}/app"
|
||||
|
||||
#viewer
|
||||
height: "calc(100% - %s)" % $topBarHeight
|
||||
|
||||
&>.loadingTextDiv
|
||||
theme('color', '$textSecondaryColor')
|
||||
font-size: 30px
|
||||
height: 100%
|
||||
line-height: "calc(100vh - %s)" % $topBarHeight
|
||||
|
||||
.viewerMain
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
@@ -14,65 +14,65 @@ export class ResizeViewportManager {
|
||||
repositionStudySeriesQuickSwitch() {
|
||||
OHIF.log.info('ResizeViewportManager repositionStudySeriesQuickSwitch');
|
||||
|
||||
const activeTab = Session.get('activeContentId');
|
||||
if(activeTab === 'viewerTab') {
|
||||
const nViewports = OHIF.viewerbase.layoutManager.viewportData.length;
|
||||
// Stop here if viewer is not displayed
|
||||
const isViewer = Session.get('ViewerOpened');
|
||||
if (!isViewer) return;
|
||||
|
||||
if(nViewports && nViewports > 1) {
|
||||
const viewer = $('#viewer');
|
||||
const leftSidebar = viewer.find('.sidebar-left.sidebar-open');
|
||||
const rightSidebar = viewer.find('.sidebar-right.sidebar-open');
|
||||
// Stop here if there is no one or only one viewport
|
||||
const nViewports = OHIF.viewerbase.layoutManager.viewportData.length;
|
||||
if (!nViewports || nViewports <= 1) return;
|
||||
|
||||
const leftQuickSwitch = $('.quickSwitchWrapper.left');
|
||||
const rightQuickSwitch = $('.quickSwitchWrapper.right');
|
||||
const $viewer = $('#viewer');
|
||||
const leftSidebar = $viewer.find('.sidebar-left.sidebar-open');
|
||||
const rightSidebar = $viewer.find('.sidebar-right.sidebar-open');
|
||||
|
||||
const hasLeftSidebar = leftSidebar.length > 0;
|
||||
const hasRightSidebar = rightSidebar.length > 0;
|
||||
const $leftQuickSwitch = $('.quickSwitchWrapper.left');
|
||||
const $rightQuickSwitch = $('.quickSwitchWrapper.right');
|
||||
|
||||
rightQuickSwitch.removeClass('left-sidebar-only');
|
||||
leftQuickSwitch.removeClass('right-sidebar-only');
|
||||
const hasLeftSidebar = leftSidebar.length > 0;
|
||||
const hasRightSidebar = rightSidebar.length > 0;
|
||||
|
||||
let leftOffset = 0;
|
||||
$rightQuickSwitch.removeClass('left-sidebar-only');
|
||||
$leftQuickSwitch.removeClass('right-sidebar-only');
|
||||
|
||||
if(hasLeftSidebar) {
|
||||
leftOffset = ( leftSidebar.width()/$(window).width() ) * 100;
|
||||
let leftOffset = 0;
|
||||
|
||||
if(!hasRightSidebar) {
|
||||
rightQuickSwitch.addClass('left-sidebar-only');
|
||||
}
|
||||
}
|
||||
if (hasLeftSidebar) {
|
||||
leftOffset = (leftSidebar.width() / $(window).width()) * 100;
|
||||
|
||||
if(hasRightSidebar && !hasLeftSidebar) {
|
||||
leftQuickSwitch.addClass('right-sidebar-only');
|
||||
}
|
||||
|
||||
const leftPosition = ( ($('#imageViewerViewports').width() / nViewports) / $(window).width() ) * 100 + leftOffset;
|
||||
const rightPosition = 100 - leftPosition;
|
||||
|
||||
leftQuickSwitch.css('right', rightPosition + '%');
|
||||
rightQuickSwitch.css('left', leftPosition + '%');
|
||||
if (!hasRightSidebar) {
|
||||
$rightQuickSwitch.addClass('left-sidebar-only');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (hasRightSidebar && !hasLeftSidebar) {
|
||||
$leftQuickSwitch.addClass('right-sidebar-only');
|
||||
}
|
||||
|
||||
const leftPosition = (($('#imageViewerViewports').width() / nViewports) / $(window).width()) * 100 + leftOffset;
|
||||
const rightPosition = 100 - leftPosition;
|
||||
|
||||
$leftQuickSwitch.css('right', rightPosition + '%');
|
||||
$rightQuickSwitch.css('left', leftPosition + '%');
|
||||
}
|
||||
|
||||
// Relocate dialogs positions
|
||||
relocateDialogs(){
|
||||
OHIF.log.info('ResizeViewportManager relocateDialogs');
|
||||
|
||||
const bottomRightDialogs = $('#cineDialog, #annotationDialog, #textMarkerOptionsDialog');
|
||||
bottomRightDialogs.css({
|
||||
const $bottomRightDialogs = $('#cineDialog, #annotationDialog, #textMarkerOptionsDialog');
|
||||
$bottomRightDialogs.css({
|
||||
top: '', // This removes the CSS property completely
|
||||
left: '',
|
||||
bottom: 0,
|
||||
right: 0
|
||||
});
|
||||
|
||||
const centerDialogs = $('.draggableDialog').not(bottomRightDialogs);
|
||||
const centerDialogs = $('.draggableDialog').not($bottomRightDialogs);
|
||||
|
||||
centerDialogs.css({
|
||||
top: 0,
|
||||
left:0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0
|
||||
});
|
||||
@@ -83,7 +83,6 @@ export class ResizeViewportManager {
|
||||
OHIF.log.info('ResizeViewportManager resizeScrollbars');
|
||||
|
||||
const currentOverlay = $(element).siblings('.imageViewerViewportOverlay');
|
||||
const imageControls = currentOverlay.find('.imageControls');
|
||||
currentOverlay.find('.imageControls').height($(element).height());
|
||||
|
||||
// Set it's width to its parent's height
|
||||
@@ -125,7 +124,7 @@ export class ResizeViewportManager {
|
||||
resizeViewportElements() {
|
||||
this.relocateDialogs();
|
||||
|
||||
const viewportResizeTimer = setTimeout(() => {
|
||||
setTimeout(() => {
|
||||
this.repositionStudySeriesQuickSwitch();
|
||||
|
||||
const elements = $('.imageViewerViewport').not('.empty');
|
||||
@@ -162,6 +161,7 @@ export class ResizeViewportManager {
|
||||
resizeHandler = this.handleResize.bind(this);
|
||||
this._resizeHandler = resizeHandler;
|
||||
}
|
||||
|
||||
return resizeHandler;
|
||||
}
|
||||
}
|
||||
@@ -15,11 +15,11 @@ export const renderViewer = (context, params) => {
|
||||
|
||||
// Render the viewer when the data is ready
|
||||
promise.then(({ studies, viewerData }) => {
|
||||
OHIF.viewer.data = viewerData;
|
||||
context.render('app', {
|
||||
data: {
|
||||
template: 'viewer',
|
||||
studies,
|
||||
viewerData
|
||||
studies
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user