chore(prettier): full repo linting and code reformatting once and for all (#3627)
This commit is contained in:
1 parent
01500a2d4c
commit
9045ddaedc
656 files changed
+21737
-20628
No files matched your search
+15
-51
@@ -26,22 +26,10 @@ const DISPLAY_STUDY_SUMMARY_INITIAL_VALUE = {
|
||||
|
||||
function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
|
||||
const [viewportGrid] = useViewportGrid();
|
||||
const [measurementChangeTimestamp, setMeasurementsUpdated] = useState(
|
||||
Date.now().toString()
|
||||
);
|
||||
const debouncedMeasurementChangeTimestamp = useDebounce(
|
||||
measurementChangeTimestamp,
|
||||
200
|
||||
);
|
||||
const {
|
||||
measurementService,
|
||||
uiDialogService,
|
||||
displaySetService,
|
||||
} = servicesManager.services;
|
||||
const [
|
||||
trackedMeasurements,
|
||||
sendTrackedMeasurementsEvent,
|
||||
] = useTrackedMeasurements();
|
||||
const [measurementChangeTimestamp, setMeasurementsUpdated] = useState(Date.now().toString());
|
||||
const debouncedMeasurementChangeTimestamp = useDebounce(measurementChangeTimestamp, 200);
|
||||
const { measurementService, uiDialogService, displaySetService } = servicesManager.services;
|
||||
const [trackedMeasurements, sendTrackedMeasurementsEvent] = useTrackedMeasurements();
|
||||
const { trackedStudy, trackedSeries } = trackedMeasurements.context;
|
||||
const [displayStudySummary, setDisplayStudySummary] = useState(
|
||||
DISPLAY_STUDY_SUMMARY_INITIAL_VALUE
|
||||
@@ -52,26 +40,15 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
|
||||
useEffect(() => {
|
||||
const measurements = measurementService.getMeasurements();
|
||||
const filteredMeasurements = measurements.filter(
|
||||
m =>
|
||||
trackedStudy === m.referenceStudyUID &&
|
||||
trackedSeries.includes(m.referenceSeriesUID)
|
||||
m => trackedStudy === m.referenceStudyUID && trackedSeries.includes(m.referenceSeriesUID)
|
||||
);
|
||||
|
||||
const mappedMeasurements = filteredMeasurements.map(m =>
|
||||
_mapMeasurementToDisplay(
|
||||
m,
|
||||
measurementService.VALUE_TYPES,
|
||||
displaySetService
|
||||
)
|
||||
_mapMeasurementToDisplay(m, measurementService.VALUE_TYPES, displaySetService)
|
||||
);
|
||||
setDisplayMeasurements(mappedMeasurements);
|
||||
// eslint-ignore-next-line
|
||||
}, [
|
||||
measurementService,
|
||||
trackedStudy,
|
||||
trackedSeries,
|
||||
debouncedMeasurementChangeTimestamp,
|
||||
]);
|
||||
}, [measurementService, trackedStudy, trackedSeries, debouncedMeasurementChangeTimestamp]);
|
||||
|
||||
const updateDisplayStudySummary = async () => {
|
||||
if (trackedMeasurements.matches('tracking')) {
|
||||
@@ -104,12 +81,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
|
||||
// ~~ DisplayStudySummary
|
||||
useEffect(() => {
|
||||
updateDisplayStudySummary();
|
||||
}, [
|
||||
displayStudySummary.key,
|
||||
trackedMeasurements,
|
||||
trackedStudy,
|
||||
updateDisplayStudySummary,
|
||||
]);
|
||||
}, [displayStudySummary.key, trackedMeasurements, trackedStudy, updateDisplayStudySummary]);
|
||||
|
||||
// TODO: Better way to consolidated, debounce, check on change?
|
||||
// Are we exposing the right API for measurementService?
|
||||
@@ -130,8 +102,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
|
||||
setMeasurementsUpdated(Date.now().toString());
|
||||
if (evt === added) {
|
||||
debounce(() => {
|
||||
measurementsPanelRef.current.scrollTop =
|
||||
measurementsPanelRef.current.scrollHeight;
|
||||
measurementsPanelRef.current.scrollTop = measurementsPanelRef.current.scrollHeight;
|
||||
}, 300)();
|
||||
}
|
||||
}).unsubscribe
|
||||
@@ -148,9 +119,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
|
||||
async function exportReport() {
|
||||
const measurements = measurementService.getMeasurements();
|
||||
const trackedMeasurements = measurements.filter(
|
||||
m =>
|
||||
trackedStudy === m.referenceStudyUID &&
|
||||
trackedSeries.includes(m.referenceSeriesUID)
|
||||
m => trackedStudy === m.referenceStudyUID && trackedSeries.includes(m.referenceSeriesUID)
|
||||
);
|
||||
|
||||
downloadCSVReport(trackedMeasurements, measurementService);
|
||||
@@ -209,7 +178,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
|
||||
labelClassName="text-white grow text-[14px] leading-[1.2]"
|
||||
autoFocus
|
||||
id="annotation"
|
||||
className="bg-black border-primary-main"
|
||||
className="border-primary-main bg-black"
|
||||
type="text"
|
||||
value={value.label}
|
||||
onChange={onChangeHandler}
|
||||
@@ -247,7 +216,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="overflow-x-hidden overflow-y-auto invisible-scrollbar"
|
||||
className="invisible-scrollbar overflow-y-auto overflow-x-hidden"
|
||||
ref={measurementsPanelRef}
|
||||
data-cy={'trackedMeasurements-panel'}
|
||||
>
|
||||
@@ -285,8 +254,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
|
||||
});
|
||||
}}
|
||||
disabled={
|
||||
additionalFindings.length === 0 &&
|
||||
displayMeasurementsWithoutFindings.length === 0
|
||||
additionalFindings.length === 0 && displayMeasurementsWithoutFindings.length === 0
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
@@ -318,14 +286,10 @@ function _mapMeasurementToDisplay(measurement, types, displaySetService) {
|
||||
SOPInstanceUID
|
||||
);
|
||||
|
||||
const displaySets = displaySetService.getDisplaySetsForSeries(
|
||||
referenceSeriesUID
|
||||
);
|
||||
const displaySets = displaySetService.getDisplaySetsForSeries(referenceSeriesUID);
|
||||
|
||||
if (!displaySets[0] || !displaySets[0].images) {
|
||||
throw new Error(
|
||||
'The tracked measurements panel should only be tracking "stack" displaySets.'
|
||||
);
|
||||
throw new Error('The tracked measurements panel should only be tracking "stack" displaySets.');
|
||||
}
|
||||
|
||||
const {
|
||||
|
||||
+31
-116
@@ -3,13 +3,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import PropTypes from 'prop-types';
|
||||
import { utils } from '@ohif/core';
|
||||
import {
|
||||
StudyBrowser,
|
||||
useImageViewer,
|
||||
useViewportGrid,
|
||||
Dialog,
|
||||
ButtonEnums,
|
||||
} from '@ohif/ui';
|
||||
import { StudyBrowser, useImageViewer, useViewportGrid, Dialog, ButtonEnums } from '@ohif/ui';
|
||||
import { useTrackedMeasurements } from '../../getContextModule';
|
||||
|
||||
const { formatDate } = utils;
|
||||
@@ -25,12 +19,8 @@ function PanelStudyBrowserTracking({
|
||||
requestDisplaySetCreationForStudy,
|
||||
dataSource,
|
||||
}) {
|
||||
const {
|
||||
displaySetService,
|
||||
uiDialogService,
|
||||
hangingProtocolService,
|
||||
uiNotificationService,
|
||||
} = servicesManager.services;
|
||||
const { displaySetService, uiDialogService, hangingProtocolService, uiNotificationService } =
|
||||
servicesManager.services;
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { t } = useTranslation('Common');
|
||||
@@ -39,14 +29,8 @@ function PanelStudyBrowserTracking({
|
||||
// doesn't have to have such an intense shape. This works well enough for now.
|
||||
// Tabs --> Studies --> DisplaySets --> Thumbnails
|
||||
const { StudyInstanceUIDs } = useImageViewer();
|
||||
const [
|
||||
{ activeViewportId, viewports },
|
||||
viewportGridService,
|
||||
] = useViewportGrid();
|
||||
const [
|
||||
trackedMeasurements,
|
||||
sendTrackedMeasurementsEvent,
|
||||
] = useTrackedMeasurements();
|
||||
const [{ activeViewportId, viewports }, viewportGridService] = useViewportGrid();
|
||||
const [trackedMeasurements, sendTrackedMeasurementsEvent] = useTrackedMeasurements();
|
||||
const [activeTabName, setActiveTabName] = useState('primary');
|
||||
const [expandedStudyInstanceUIDs, setExpandedStudyInstanceUIDs] = useState([
|
||||
...StudyInstanceUIDs,
|
||||
@@ -78,8 +62,8 @@ function PanelStudyBrowserTracking({
|
||||
viewportGridService.setDisplaySetsForViewports(updatedViewports);
|
||||
};
|
||||
|
||||
const activeViewportDisplaySetInstanceUIDs = viewports.get(activeViewportId)
|
||||
?.displaySetInstanceUIDs;
|
||||
const activeViewportDisplaySetInstanceUIDs =
|
||||
viewports.get(activeViewportId)?.displaySetInstanceUIDs;
|
||||
|
||||
const { trackedSeries } = trackedMeasurements.context;
|
||||
|
||||
@@ -102,9 +86,7 @@ function PanelStudyBrowserTracking({
|
||||
// try to fetch the prior studies based on the patientID if the
|
||||
// server can respond.
|
||||
try {
|
||||
qidoStudiesForPatient = await getStudiesForPatientByMRN(
|
||||
qidoForStudyUID
|
||||
);
|
||||
qidoStudiesForPatient = await getStudiesForPatientByMRN(qidoForStudyUID);
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
}
|
||||
@@ -123,11 +105,7 @@ function PanelStudyBrowserTracking({
|
||||
setStudyDisplayList(prevArray => {
|
||||
const ret = [...prevArray];
|
||||
for (const study of actuallyMappedStudies) {
|
||||
if (
|
||||
!prevArray.find(
|
||||
it => it.studyInstanceUid === study.studyInstanceUid
|
||||
)
|
||||
) {
|
||||
if (!prevArray.find(it => it.studyInstanceUid === study.studyInstanceUid)) {
|
||||
ret.push(study);
|
||||
}
|
||||
}
|
||||
@@ -149,18 +127,14 @@ function PanelStudyBrowserTracking({
|
||||
|
||||
currentDisplaySets.forEach(async dSet => {
|
||||
const newImageSrcEntry = {};
|
||||
const displaySet = displaySetService.getDisplaySetByUID(
|
||||
dSet.displaySetInstanceUID
|
||||
);
|
||||
const displaySet = displaySetService.getDisplaySetByUID(dSet.displaySetInstanceUID);
|
||||
const imageIds = dataSource.getImageIdsForDisplaySet(displaySet);
|
||||
const imageId = imageIds[Math.floor(imageIds.length / 2)];
|
||||
|
||||
// TODO: Is it okay that imageIds are not returned here for SR displaysets?
|
||||
if (imageId && !displaySet?.unsupported) {
|
||||
// When the image arrives, render it and store the result in the thumbnailImgSrcMap
|
||||
newImageSrcEntry[dSet.displaySetInstanceUID] = await getImageSrc(
|
||||
imageId
|
||||
);
|
||||
newImageSrcEntry[dSet.displaySetInstanceUID] = await getImageSrc(imageId);
|
||||
setThumbnailImageSrcMap(prevState => {
|
||||
return { ...prevState, ...newImageSrcEntry };
|
||||
});
|
||||
@@ -209,9 +183,7 @@ function PanelStudyBrowserTracking({
|
||||
const displaySetInstanceUID = dSet.displaySetInstanceUID;
|
||||
|
||||
const newImageSrcEntry = {};
|
||||
const displaySet = displaySetService.getDisplaySetByUID(
|
||||
displaySetInstanceUID
|
||||
);
|
||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
if (!displaySet?.unsupported) {
|
||||
if (options.madeInClient) {
|
||||
setJumpToDisplaySet(displaySetInstanceUID);
|
||||
@@ -223,9 +195,7 @@ function PanelStudyBrowserTracking({
|
||||
// TODO: Is it okay that imageIds are not returned here for SR displaysets?
|
||||
if (imageId) {
|
||||
// When the image arrives, render it and store the result in the thumbnailImgSrcMap
|
||||
newImageSrcEntry[displaySetInstanceUID] = await getImageSrc(
|
||||
imageId
|
||||
);
|
||||
newImageSrcEntry[displaySetInstanceUID] = await getImageSrc(imageId);
|
||||
setThumbnailImageSrcMap(prevState => {
|
||||
return { ...prevState, ...newImageSrcEntry };
|
||||
});
|
||||
@@ -281,14 +251,7 @@ function PanelStudyBrowserTracking({
|
||||
SubscriptionDisplaySetMetaDataInvalidated.unsubscribe();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
displaySetService,
|
||||
dataSource,
|
||||
getImageSrc,
|
||||
thumbnailImageSrcMap,
|
||||
trackedSeries,
|
||||
viewports,
|
||||
]);
|
||||
}, [displaySetService, dataSource, getImageSrc, thumbnailImageSrcMap, trackedSeries, viewports]);
|
||||
|
||||
const tabs = _createStudyBrowserTabs(
|
||||
StudyInstanceUIDs,
|
||||
@@ -299,26 +262,16 @@ function PanelStudyBrowserTracking({
|
||||
|
||||
// TODO: Should not fire this on "close"
|
||||
function _handleStudyClick(StudyInstanceUID) {
|
||||
const shouldCollapseStudy = expandedStudyInstanceUIDs.includes(
|
||||
StudyInstanceUID
|
||||
);
|
||||
const shouldCollapseStudy = expandedStudyInstanceUIDs.includes(StudyInstanceUID);
|
||||
const updatedExpandedStudyInstanceUIDs = shouldCollapseStudy
|
||||
? [
|
||||
...expandedStudyInstanceUIDs.filter(
|
||||
stdyUid => stdyUid !== StudyInstanceUID
|
||||
),
|
||||
]
|
||||
? [...expandedStudyInstanceUIDs.filter(stdyUid => stdyUid !== StudyInstanceUID)]
|
||||
: [...expandedStudyInstanceUIDs, StudyInstanceUID];
|
||||
|
||||
setExpandedStudyInstanceUIDs(updatedExpandedStudyInstanceUIDs);
|
||||
|
||||
if (!shouldCollapseStudy) {
|
||||
const madeInClient = true;
|
||||
requestDisplaySetCreationForStudy(
|
||||
displaySetService,
|
||||
StudyInstanceUID,
|
||||
madeInClient
|
||||
);
|
||||
requestDisplaySetCreationForStudy(displaySetService, StudyInstanceUID, madeInClient);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,9 +279,7 @@ function PanelStudyBrowserTracking({
|
||||
if (jumpToDisplaySet) {
|
||||
// Get element by displaySetInstanceUID
|
||||
const displaySetInstanceUID = jumpToDisplaySet;
|
||||
const element = document.getElementById(
|
||||
`thumbnail-${displaySetInstanceUID}`
|
||||
);
|
||||
const element = document.getElementById(`thumbnail-${displaySetInstanceUID}`);
|
||||
|
||||
if (element && typeof element.scrollIntoView === 'function') {
|
||||
// TODO: Any way to support IE here?
|
||||
@@ -346,10 +297,7 @@ function PanelStudyBrowserTracking({
|
||||
|
||||
const displaySetInstanceUID = jumpToDisplaySet;
|
||||
// Set the activeTabName and expand the study
|
||||
const thumbnailLocation = _findTabAndStudyOfDisplaySet(
|
||||
displaySetInstanceUID,
|
||||
tabs
|
||||
);
|
||||
const thumbnailLocation = _findTabAndStudyOfDisplaySet(displaySetInstanceUID, tabs);
|
||||
if (!thumbnailLocation) {
|
||||
console.warn('jumpToThumbnail: displaySet thumbnail not found.');
|
||||
|
||||
@@ -359,10 +307,7 @@ function PanelStudyBrowserTracking({
|
||||
setActiveTabName(tabName);
|
||||
const studyExpanded = expandedStudyInstanceUIDs.includes(StudyInstanceUID);
|
||||
if (!studyExpanded) {
|
||||
const updatedExpandedStudyInstanceUIDs = [
|
||||
...expandedStudyInstanceUIDs,
|
||||
StudyInstanceUID,
|
||||
];
|
||||
const updatedExpandedStudyInstanceUIDs = [...expandedStudyInstanceUIDs, StudyInstanceUID];
|
||||
setExpandedStudyInstanceUIDs(updatedExpandedStudyInstanceUIDs);
|
||||
}
|
||||
}, [expandedStudyInstanceUIDs, jumpToDisplaySet, tabs]);
|
||||
@@ -378,9 +323,7 @@ function PanelStudyBrowserTracking({
|
||||
setActiveTabName(clickedTabName);
|
||||
}}
|
||||
onClickUntrack={displaySetInstanceUID => {
|
||||
const displaySet = displaySetService.getDisplaySetByUID(
|
||||
displaySetInstanceUID
|
||||
);
|
||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
// TODO: shift this somewhere else where we're centralizing this logic?
|
||||
// Potentially a helper from displaySetInstanceUID to this
|
||||
sendTrackedMeasurementsEvent('UNTRACK_SERIES', {
|
||||
@@ -451,20 +394,14 @@ function _mapDisplaySets(
|
||||
|
||||
if (numPanes !== 1) {
|
||||
viewports.forEach(viewportData => {
|
||||
if (
|
||||
viewportData?.displaySetInstanceUIDs?.includes(
|
||||
ds.displaySetInstanceUID
|
||||
)
|
||||
) {
|
||||
if (viewportData?.displaySetInstanceUIDs?.includes(ds.displaySetInstanceUID)) {
|
||||
viewportIdentificator.push(viewportData.viewportLabel);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const array =
|
||||
componentType === 'thumbnailTracked'
|
||||
? thumbnailDisplaySets
|
||||
: thumbnailNoImageDisplaySets;
|
||||
componentType === 'thumbnailTracked' ? thumbnailDisplaySets : thumbnailNoImageDisplaySets;
|
||||
|
||||
const { displaySetInstanceUID } = ds;
|
||||
|
||||
@@ -503,7 +440,7 @@ function _mapDisplaySets(
|
||||
contentProps: {
|
||||
title: 'Delete Report',
|
||||
body: () => (
|
||||
<div className="p-4 text-white bg-primary-dark">
|
||||
<div className="bg-primary-dark p-4 text-white">
|
||||
<p>Are you sure you want to delete this report?</p>
|
||||
<p>This action cannot be undone.</p>
|
||||
</div>
|
||||
@@ -523,9 +460,7 @@ function _mapDisplaySets(
|
||||
],
|
||||
onClose: () => uiDialogService.dismiss({ id: 'ds-reject-sr' }),
|
||||
onShow: () => {
|
||||
const yesButton = document.querySelector(
|
||||
'.reject-yes-button'
|
||||
);
|
||||
const yesButton = document.querySelector('.reject-yes-button');
|
||||
|
||||
yesButton.focus();
|
||||
},
|
||||
@@ -533,13 +468,8 @@ function _mapDisplaySets(
|
||||
switch (action.id) {
|
||||
case 'yes':
|
||||
try {
|
||||
await dataSource.reject.series(
|
||||
ds.StudyInstanceUID,
|
||||
ds.SeriesInstanceUID
|
||||
);
|
||||
displaySetService.deleteDisplaySet(
|
||||
displaySetInstanceUID
|
||||
);
|
||||
await dataSource.reject.series(ds.StudyInstanceUID, ds.SeriesInstanceUID);
|
||||
displaySetService.deleteDisplaySet(displaySetInstanceUID);
|
||||
uiDialogService.dismiss({ id: 'ds-reject-sr' });
|
||||
uiNotificationService.show({
|
||||
title: 'Delete Report',
|
||||
@@ -574,16 +504,7 @@ function _mapDisplaySets(
|
||||
return [...thumbnailDisplaySets, ...thumbnailNoImageDisplaySets];
|
||||
}
|
||||
|
||||
const thumbnailNoImageModalities = [
|
||||
'SR',
|
||||
'SEG',
|
||||
'SM',
|
||||
'RTSTRUCT',
|
||||
'RTPLAN',
|
||||
'RTDOSE',
|
||||
'DOC',
|
||||
'OT',
|
||||
];
|
||||
const thumbnailNoImageModalities = ['SR', 'SEG', 'SM', 'RTSTRUCT', 'RTPLAN', 'RTDOSE', 'DOC', 'OT'];
|
||||
|
||||
function _getComponentType(ds) {
|
||||
if (thumbnailNoImageModalities.includes(ds.Modality) || ds?.unsupported) {
|
||||
@@ -667,23 +588,17 @@ function _createStudyBrowserTabs(
|
||||
{
|
||||
name: 'primary',
|
||||
label: 'Primary',
|
||||
studies: primaryStudies.sort((studyA, studyB) =>
|
||||
_byDate(studyA.date, studyB.date)
|
||||
),
|
||||
studies: primaryStudies.sort((studyA, studyB) => _byDate(studyA.date, studyB.date)),
|
||||
},
|
||||
{
|
||||
name: 'recent',
|
||||
label: 'Recent',
|
||||
studies: recentStudies.sort((studyA, studyB) =>
|
||||
_byDate(studyA.date, studyB.date)
|
||||
),
|
||||
studies: recentStudies.sort((studyA, studyB) => _byDate(studyA.date, studyB.date)),
|
||||
},
|
||||
{
|
||||
name: 'all',
|
||||
label: 'All',
|
||||
studies: allStudies.sort((studyA, studyB) =>
|
||||
_byDate(studyA.date, studyB.date)
|
||||
),
|
||||
studies: allStudies.sort((studyA, studyB) => _byDate(studyA.date, studyB.date)),
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -21,23 +21,12 @@ function _getStudyForPatientUtility(extensionManager) {
|
||||
* @param {object} commandsManager
|
||||
* @param {object} extensionManager
|
||||
*/
|
||||
function WrappedPanelStudyBrowserTracking({
|
||||
commandsManager,
|
||||
extensionManager,
|
||||
servicesManager,
|
||||
}) {
|
||||
function WrappedPanelStudyBrowserTracking({ commandsManager, extensionManager, servicesManager }) {
|
||||
const dataSource = extensionManager.getActiveDataSource()[0];
|
||||
|
||||
const getStudiesForPatientByMRN = _getStudyForPatientUtility(
|
||||
extensionManager
|
||||
);
|
||||
const _getStudiesForPatientByMRN = getStudiesForPatientByMRN.bind(
|
||||
null,
|
||||
dataSource
|
||||
);
|
||||
const _getImageSrcFromImageId = _createGetImageSrcFromImageIdFn(
|
||||
extensionManager
|
||||
);
|
||||
const getStudiesForPatientByMRN = _getStudyForPatientUtility(extensionManager);
|
||||
const _getStudiesForPatientByMRN = getStudiesForPatientByMRN.bind(null, dataSource);
|
||||
const _getImageSrcFromImageId = _createGetImageSrcFromImageIdFn(extensionManager);
|
||||
const _requestDisplaySetCreationForStudy = requestDisplaySetCreationForStudy.bind(
|
||||
null,
|
||||
dataSource
|
||||
|
||||
Reference in new issue
Block a user