Merge pull request #350 from ONLYOFFICE/bugfix/folder-header-menu
Web: Files: added move, copy and download actions in folder header menu
This commit is contained in:
commit
3d5228c8a7
@ -16,8 +16,10 @@ const OperationsPanelComponent = (props) => {
|
||||
visible,
|
||||
provider,
|
||||
selection,
|
||||
isFolderActions,
|
||||
isRecycleBin,
|
||||
setDestFolderId,
|
||||
setIsFolderActions,
|
||||
currentFolderId,
|
||||
operationsFolders,
|
||||
setCopyPanelVisible,
|
||||
@ -25,6 +27,7 @@ const OperationsPanelComponent = (props) => {
|
||||
setMoveToPanelVisible,
|
||||
checkOperationConflict,
|
||||
setThirdPartyMoveDialogVisible,
|
||||
parentFolderId,
|
||||
} = props;
|
||||
|
||||
const zIndex = 310;
|
||||
@ -33,7 +36,12 @@ const OperationsPanelComponent = (props) => {
|
||||
const expandedKeys = props.expandedKeys.map((item) => item.toString());
|
||||
|
||||
const onClose = () => {
|
||||
isCopy ? setCopyPanelVisible(false) : setMoveToPanelVisible(false);
|
||||
if (isCopy) {
|
||||
setCopyPanelVisible(false);
|
||||
setIsFolderActions(false);
|
||||
} else {
|
||||
setMoveToPanelVisible(false);
|
||||
}
|
||||
setExpandedPanelKeys(null);
|
||||
};
|
||||
|
||||
@ -41,6 +49,10 @@ const OperationsPanelComponent = (props) => {
|
||||
const folderTitle = treeNode.node.props.title;
|
||||
const destFolderId = isNaN(+folder[0]) ? folder[0] : +folder[0];
|
||||
|
||||
if (isFolderActions && destFolderId === parentFolderId) {
|
||||
return onClose();
|
||||
}
|
||||
|
||||
if (currentFolderId === destFolderId) {
|
||||
return onClose();
|
||||
}
|
||||
@ -68,8 +80,9 @@ const OperationsPanelComponent = (props) => {
|
||||
? selection.filter((x) => !x.providerKey)
|
||||
: selection;
|
||||
|
||||
const fileIds = [];
|
||||
const folderIds = [];
|
||||
let fileIds = [];
|
||||
let folderIds = [];
|
||||
|
||||
|
||||
for (let item of items) {
|
||||
if (item.fileExst || item.contentLength) {
|
||||
@ -81,6 +94,13 @@ const OperationsPanelComponent = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (isFolderActions) {
|
||||
fileIds = [];
|
||||
folderIds = [];
|
||||
|
||||
folderIds.push(currentFolderId);
|
||||
}
|
||||
|
||||
if (!folderIds.length && !fileIds.length) return;
|
||||
|
||||
checkOperationConflict({
|
||||
@ -152,10 +172,12 @@ export default inject(
|
||||
const {
|
||||
moveToPanelVisible,
|
||||
copyPanelVisible,
|
||||
isFolderActions,
|
||||
setCopyPanelVisible,
|
||||
setMoveToPanelVisible,
|
||||
setDestFolderId,
|
||||
setThirdPartyMoveDialogVisible,
|
||||
setIsFolderActions,
|
||||
} = dialogsStore;
|
||||
|
||||
const selections = selection.length ? selection : [bufferSelection];
|
||||
@ -167,16 +189,19 @@ export default inject(
|
||||
? expandedPanelKeys
|
||||
: selectedFolderStore.pathParts,
|
||||
currentFolderId: selectedFolderStore.id,
|
||||
parentFolderId: selectedFolderStore.parentId,
|
||||
isRecycleBin: isRecycleBinFolder,
|
||||
filter,
|
||||
operationsFolders,
|
||||
visible: copyPanelVisible || moveToPanelVisible,
|
||||
provider,
|
||||
selection: selections,
|
||||
isFolderActions,
|
||||
|
||||
setCopyPanelVisible,
|
||||
setMoveToPanelVisible,
|
||||
setDestFolderId,
|
||||
setIsFolderActions,
|
||||
setThirdPartyMoveDialogVisible,
|
||||
checkOperationConflict,
|
||||
setExpandedPanelKeys,
|
||||
|
@ -78,7 +78,11 @@ class SharingPanelComponent extends React.Component {
|
||||
};
|
||||
|
||||
updateRowData = (newRowData) => {
|
||||
const { getFileInfo, getFolderInfo } = this.props;
|
||||
const { getFileInfo, getFolderInfo, isFolderActions, id } = this.props;
|
||||
|
||||
if (isFolderActions) {
|
||||
return getFolderInfo(id);
|
||||
}
|
||||
|
||||
for (let item of newRowData) {
|
||||
!item.fileExst ? getFolderInfo(item.id) : getFileInfo(item.id);
|
||||
@ -105,7 +109,9 @@ class SharingPanelComponent extends React.Component {
|
||||
isDesktop,
|
||||
setEncryptionAccess,
|
||||
setShareFiles,
|
||||
setIsFolderActions,
|
||||
onSuccess,
|
||||
isFolderActions,
|
||||
} = this.props;
|
||||
|
||||
let folderIds = [];
|
||||
@ -153,6 +159,13 @@ class SharingPanelComponent extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
if (isFolderActions) {
|
||||
folderIds = [];
|
||||
fileIds = [];
|
||||
|
||||
folderIds.push(selection[0]);
|
||||
}
|
||||
|
||||
const owner = shareDataItems.find((x) => x.isOwner);
|
||||
const ownerId =
|
||||
filesOwnerId !== owner.sharedTo.id ? owner.sharedTo.id : null;
|
||||
@ -205,7 +218,10 @@ class SharingPanelComponent extends React.Component {
|
||||
})
|
||||
.then(() => onSuccess && onSuccess())
|
||||
.catch((err) => toastr.error(err))
|
||||
.finally(() => setIsLoading(false));
|
||||
.finally(() => {
|
||||
setIsFolderActions(false);
|
||||
setIsLoading(false);
|
||||
});
|
||||
};
|
||||
onNotifyUsersChange = () =>
|
||||
this.setState({ isNotifyUsers: !this.state.isNotifyUsers });
|
||||
@ -240,9 +256,10 @@ class SharingPanelComponent extends React.Component {
|
||||
};
|
||||
|
||||
getData = () => {
|
||||
const { selection } = this.props;
|
||||
const folderId = [];
|
||||
const fileId = [];
|
||||
const { selection, id, access } = this.props;
|
||||
|
||||
let folderId = [];
|
||||
let fileId = [];
|
||||
|
||||
for (let item of selection) {
|
||||
if (item.access === 1 || item.access === 0) {
|
||||
@ -254,6 +271,13 @@ class SharingPanelComponent extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.props.isFolderActions) {
|
||||
folderId = [];
|
||||
fileId = [];
|
||||
|
||||
folderId = access === 1 || access === 0 ? [id] : [];
|
||||
}
|
||||
|
||||
return [folderId, fileId];
|
||||
};
|
||||
|
||||
@ -269,7 +293,6 @@ class SharingPanelComponent extends React.Component {
|
||||
const returnValue = this.getData();
|
||||
const folderId = returnValue[0];
|
||||
const fileId = returnValue[1];
|
||||
|
||||
if (folderId.length !== 0 || fileId.length !== 0) {
|
||||
!isMobile && setIsLoading(true);
|
||||
getShareUsers(folderId, fileId)
|
||||
@ -341,10 +364,14 @@ class SharingPanelComponent extends React.Component {
|
||||
onCancel,
|
||||
setSharingPanelVisible,
|
||||
selectUploadedFile,
|
||||
setIsFolderActions,
|
||||
setSelection,
|
||||
setBufferSelection,
|
||||
} = this.props;
|
||||
|
||||
setSharingPanelVisible(false);
|
||||
setSelection([]);
|
||||
|
||||
selectUploadedFile([]);
|
||||
setBufferSelection(null);
|
||||
onCancel && onCancel();
|
||||
@ -618,12 +645,21 @@ class SharingPanelComponent extends React.Component {
|
||||
|
||||
const SharingPanel = inject(
|
||||
(
|
||||
{ auth, filesStore, uploadDataStore, dialogsStore, treeFoldersStore },
|
||||
{
|
||||
auth,
|
||||
filesStore,
|
||||
uploadDataStore,
|
||||
dialogsStore,
|
||||
treeFoldersStore,
|
||||
selectedFolderStore,
|
||||
},
|
||||
{ uploadPanelVisible }
|
||||
) => {
|
||||
const { replaceFileStream, setEncryptionAccess } = auth;
|
||||
const { personal, customNames, isDesktopClient } = auth.settingsStore;
|
||||
|
||||
const { id, access } = selectedFolderStore;
|
||||
|
||||
const {
|
||||
selection,
|
||||
bufferSelection,
|
||||
@ -635,13 +671,19 @@ const SharingPanel = inject(
|
||||
getShareUsers,
|
||||
setShareFiles,
|
||||
setIsLoading,
|
||||
setSelection,
|
||||
getFileInfo,
|
||||
getFolderInfo,
|
||||
isLoading,
|
||||
setBufferSelection,
|
||||
} = filesStore;
|
||||
const { isPrivacyFolder } = treeFoldersStore;
|
||||
const { setSharingPanelVisible, sharingPanelVisible } = dialogsStore;
|
||||
const {
|
||||
setSharingPanelVisible,
|
||||
sharingPanelVisible,
|
||||
setIsFolderActions,
|
||||
isFolderActions,
|
||||
} = dialogsStore;
|
||||
const {
|
||||
selectedUploadFile,
|
||||
selectUploadedFile,
|
||||
@ -661,11 +703,14 @@ const SharingPanel = inject(
|
||||
: [bufferSelection],
|
||||
isLoading,
|
||||
isPrivacy: isPrivacyFolder,
|
||||
isFolderActions,
|
||||
selectedUploadFile,
|
||||
canShareOwnerChange,
|
||||
|
||||
setIsLoading,
|
||||
setSharingPanelVisible,
|
||||
setIsFolderActions,
|
||||
setSelection,
|
||||
sharingPanelVisible,
|
||||
selectUploadedFile,
|
||||
updateUploadedItem,
|
||||
@ -679,7 +724,9 @@ const SharingPanel = inject(
|
||||
setShareFiles,
|
||||
getFileInfo,
|
||||
getFolderInfo,
|
||||
id,
|
||||
setBufferSelection,
|
||||
access,
|
||||
};
|
||||
}
|
||||
)(
|
||||
|
@ -208,15 +208,32 @@ class SectionHeaderContent extends React.Component {
|
||||
toastr.success(t("Translations:LinkCopySuccess"));
|
||||
};
|
||||
|
||||
onMoveAction = () => this.props.setMoveToPanelVisible(true);
|
||||
onCopyAction = () => this.props.setCopyPanelVisible(true);
|
||||
downloadAction = () =>
|
||||
onMoveAction = () => {
|
||||
this.props.setIsFolderActions(true);
|
||||
this.props.setBufferSelection(this.props.currentFolderId);
|
||||
return this.props.setMoveToPanelVisible(true);
|
||||
};
|
||||
onCopyAction = () => {
|
||||
this.props.setIsFolderActions(true);
|
||||
this.props.setBufferSelection(this.props.currentFolderId);
|
||||
return this.props.setCopyPanelVisible(true);
|
||||
};
|
||||
downloadAction = () => {
|
||||
this.props.setBufferSelection(this.props.currentFolderId);
|
||||
this.props.setIsFolderActions(true);
|
||||
this.props
|
||||
.downloadAction(this.props.t("Translations:ArchivingData"))
|
||||
.downloadAction(this.props.t("Translations:ArchivingData"), [
|
||||
this.props.currentFolderId,
|
||||
])
|
||||
.catch((err) => toastr.error(err));
|
||||
};
|
||||
|
||||
renameAction = () => console.log("renameAction click");
|
||||
onOpenSharingPanel = () => this.props.setSharingPanelVisible(true);
|
||||
onOpenSharingPanel = () => {
|
||||
this.props.setBufferSelection(this.props.currentFolderId);
|
||||
this.props.setIsFolderActions(true);
|
||||
return this.props.setSharingPanelVisible(true);
|
||||
};
|
||||
|
||||
onDeleteAction = () => {
|
||||
const {
|
||||
@ -225,10 +242,18 @@ class SectionHeaderContent extends React.Component {
|
||||
confirmDelete,
|
||||
setDeleteDialogVisible,
|
||||
isThirdPartySelection,
|
||||
currentFolderId,
|
||||
getFolderInfo,
|
||||
setBufferSelection,
|
||||
} = this.props;
|
||||
|
||||
this.props.setIsFolderActions(true);
|
||||
|
||||
if (confirmDelete || isThirdPartySelection) {
|
||||
setDeleteDialogVisible(true);
|
||||
getFolderInfo(currentFolderId).then((data) => {
|
||||
setBufferSelection(data);
|
||||
setDeleteDialogVisible(true);
|
||||
});
|
||||
} else {
|
||||
const translations = {
|
||||
deleteOperation: t("Translations:DeleteOperation"),
|
||||
@ -236,45 +261,48 @@ class SectionHeaderContent extends React.Component {
|
||||
deleteSelectedElem: t("Translations:DeleteSelectedElem"),
|
||||
};
|
||||
|
||||
deleteAction(translations);
|
||||
deleteAction(translations, [currentFolderId], true).catch((err) =>
|
||||
toastr.error(err)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
onEmptyTrashAction = () => this.props.setEmptyTrashDialogVisible(true);
|
||||
|
||||
getContextOptionsFolder = () => {
|
||||
const { t } = this.props;
|
||||
const { t, personal } = this.props;
|
||||
|
||||
return [
|
||||
{
|
||||
key: "sharing-settings",
|
||||
label: t("SharingSettings"),
|
||||
onClick: this.onOpenSharingPanel,
|
||||
disabled: true,
|
||||
disabled: personal ? true : false,
|
||||
},
|
||||
{
|
||||
key: "link-portal-users",
|
||||
label: t("LinkForPortalUsers"),
|
||||
onClick: this.createLinkForPortalUsers,
|
||||
disabled: false,
|
||||
disabled: personal ? true : false,
|
||||
},
|
||||
{ key: "separator-2", isSeparator: true },
|
||||
{
|
||||
key: "move-to",
|
||||
label: t("MoveTo"),
|
||||
onClick: this.onMoveAction,
|
||||
disabled: true,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
key: "copy",
|
||||
label: t("Translations:Copy"),
|
||||
onClick: this.onCopyAction,
|
||||
disabled: true,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
key: "download",
|
||||
label: t("Common:Download"),
|
||||
onClick: this.downloadAction,
|
||||
disabled: true,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
key: "rename",
|
||||
@ -286,7 +314,7 @@ class SectionHeaderContent extends React.Component {
|
||||
key: "delete",
|
||||
label: t("Common:Delete"),
|
||||
onClick: this.onDeleteAction,
|
||||
disabled: true,
|
||||
disabled: false,
|
||||
},
|
||||
];
|
||||
};
|
||||
@ -418,19 +446,18 @@ class SectionHeaderContent extends React.Component {
|
||||
getData={this.getContextOptionsPlus}
|
||||
isDisabled={false}
|
||||
/>
|
||||
{!personal && (
|
||||
<ContextMenuButton
|
||||
className="option-button"
|
||||
directionX="right"
|
||||
iconName="images/vertical-dots.react.svg"
|
||||
size={17}
|
||||
color="#A3A9AE"
|
||||
hoverColor="#657077"
|
||||
isFill
|
||||
getData={this.getContextOptionsFolder}
|
||||
isDisabled={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ContextMenuButton
|
||||
className="option-button"
|
||||
directionX="right"
|
||||
iconName="images/vertical-dots.react.svg"
|
||||
size={17}
|
||||
color="#A3A9AE"
|
||||
hoverColor="#657077"
|
||||
isFill
|
||||
getData={this.getContextOptionsFolder}
|
||||
isDisabled={false}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
canCreate && (
|
||||
@ -469,6 +496,7 @@ export default inject(
|
||||
}) => {
|
||||
const {
|
||||
setSelected,
|
||||
setSelection,
|
||||
fileActionStore,
|
||||
fetchFiles,
|
||||
filter,
|
||||
@ -481,6 +509,8 @@ export default inject(
|
||||
viewAs,
|
||||
cbMenuItems,
|
||||
getCheckboxItemLabel,
|
||||
getFolderInfo,
|
||||
setBufferSelection,
|
||||
} = filesStore;
|
||||
const { setAction } = fileActionStore;
|
||||
const {
|
||||
@ -488,6 +518,7 @@ export default inject(
|
||||
setMoveToPanelVisible,
|
||||
setCopyPanelVisible,
|
||||
setDeleteDialogVisible,
|
||||
setIsFolderActions,
|
||||
} = dialogsStore;
|
||||
|
||||
const { deleteAction, downloadAction, getHeaderMenu } = filesActionsStore;
|
||||
@ -509,14 +540,18 @@ export default inject(
|
||||
personal: auth.settingsStore.personal,
|
||||
viewAs,
|
||||
cbMenuItems,
|
||||
getFolderInfo,
|
||||
|
||||
setSelected,
|
||||
setSelection,
|
||||
setAction,
|
||||
setIsLoading,
|
||||
fetchFiles,
|
||||
setSharingPanelVisible,
|
||||
setMoveToPanelVisible,
|
||||
setCopyPanelVisible,
|
||||
setBufferSelection,
|
||||
setIsFolderActions,
|
||||
deleteAction,
|
||||
setDeleteDialogVisible,
|
||||
downloadAction,
|
||||
|
@ -20,6 +20,7 @@ class DialogsStore {
|
||||
newFilesPanelVisible = false;
|
||||
conflictResolveDialogVisible = false;
|
||||
convertDialogVisible = false;
|
||||
isFolderActions = false;
|
||||
|
||||
removeItem = null;
|
||||
connectItem = null;
|
||||
@ -44,6 +45,10 @@ class DialogsStore {
|
||||
this.sharingPanelVisible = sharingPanelVisible;
|
||||
};
|
||||
|
||||
setIsFolderActions = (isFolderActions) => {
|
||||
this.isFolderActions = isFolderActions;
|
||||
};
|
||||
|
||||
setChangeOwnerPanelVisible = (ownerPanelVisible) => {
|
||||
this.ownerPanelVisible = ownerPanelVisible;
|
||||
};
|
||||
|
@ -61,13 +61,24 @@ class FilesActionStore {
|
||||
clearSecondaryProgressData,
|
||||
} = this.uploadDataStore.secondaryProgressDataStore;
|
||||
|
||||
let updatedFolder = this.selectedFolderStore.id;
|
||||
|
||||
if (this.dialogsStore.isFolderActions) {
|
||||
updatedFolder = this.selectedFolderStore.parentId;
|
||||
}
|
||||
|
||||
const { filter, fetchFiles } = this.filesStore;
|
||||
fetchFiles(this.selectedFolderStore.id, filter, true, true).finally(() =>
|
||||
setTimeout(() => clearSecondaryProgressData(), TIMEOUT)
|
||||
);
|
||||
fetchFiles(updatedFolder, filter, true, true).finally(() => {
|
||||
this.dialogsStore.setIsFolderActions(false);
|
||||
return setTimeout(() => clearSecondaryProgressData(), TIMEOUT);
|
||||
});
|
||||
};
|
||||
|
||||
deleteAction = async (translations, newSelection = null) => {
|
||||
deleteAction = async (
|
||||
translations,
|
||||
newSelection = null,
|
||||
withoutDialog = false
|
||||
) => {
|
||||
const { isRecycleBinFolder, isPrivacyFolder } = this.treeFoldersStore;
|
||||
|
||||
const selection = newSelection ? newSelection : this.filesStore.selection;
|
||||
@ -88,8 +99,8 @@ class FilesActionStore {
|
||||
const deleteAfter = false; //Delete after finished TODO: get from settings
|
||||
const immediately = isRecycleBinFolder || isPrivacyFolder ? true : false; //Don't move to the Recycle Bin
|
||||
|
||||
const folderIds = [];
|
||||
const fileIds = [];
|
||||
let folderIds = [];
|
||||
let fileIds = [];
|
||||
|
||||
let i = 0;
|
||||
while (selection.length !== i) {
|
||||
@ -101,6 +112,13 @@ class FilesActionStore {
|
||||
i++;
|
||||
}
|
||||
|
||||
if (this.dialogsStore.isFolderActions && withoutDialog) {
|
||||
folderIds = [];
|
||||
fileIds = [];
|
||||
|
||||
folderIds.push(selection[0]);
|
||||
}
|
||||
|
||||
if (folderIds.length || fileIds.length) {
|
||||
this.isMediaOpen();
|
||||
|
||||
@ -211,18 +229,18 @@ class FilesActionStore {
|
||||
}
|
||||
};
|
||||
|
||||
downloadAction = (label) => {
|
||||
downloadAction = (label, folderId) => {
|
||||
const { bufferSelection } = this.filesStore;
|
||||
|
||||
const selection = this.filesStore.selection.length
|
||||
? this.filesStore.selection
|
||||
: [bufferSelection];
|
||||
|
||||
const fileIds = [];
|
||||
const folderIds = [];
|
||||
let fileIds = [];
|
||||
let folderIds = [];
|
||||
const items = [];
|
||||
|
||||
if (selection.length === 1 && selection[0].fileExst) {
|
||||
if (selection.length === 1 && selection[0].fileExst && !folderId) {
|
||||
window.open(selection[0].viewUrl, "_self");
|
||||
return Promise.resolve();
|
||||
}
|
||||
@ -237,6 +255,14 @@ class FilesActionStore {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.dialogsStore.isFolderActions) {
|
||||
fileIds = [];
|
||||
folderIds = [];
|
||||
|
||||
folderIds.push(bufferSelection);
|
||||
this.dialogsStore.setIsFolderActions(false);
|
||||
}
|
||||
|
||||
return this.downloadFiles(fileIds, folderIds, label);
|
||||
};
|
||||
|
||||
|
@ -1573,6 +1573,7 @@ class FilesStore {
|
||||
getFolderInfo = async (id) => {
|
||||
const folderInfo = await api.files.getFolderInfo(id);
|
||||
this.setFolder(folderInfo);
|
||||
return folderInfo;
|
||||
};
|
||||
|
||||
openDocEditor = (id, providerKey = null, tab = null, url = null) => {
|
||||
|
@ -904,7 +904,15 @@ class UploadDataStore {
|
||||
label,
|
||||
} = this.secondaryProgressDataStore;
|
||||
|
||||
getFolder(destFolderId).then((data) => {
|
||||
let receivedFolder = destFolderId;
|
||||
let updatedFolder = this.selectedFolderStore.id;
|
||||
|
||||
if (this.dialogsStore.isFolderActions) {
|
||||
receivedFolder = this.selectedFolderStore.parentId;
|
||||
updatedFolder = destFolderId;
|
||||
}
|
||||
|
||||
getFolder(receivedFolder).then((data) => {
|
||||
let newTreeFolders = treeFolders;
|
||||
let path = data.pathParts.slice(0);
|
||||
let folders = data.folders;
|
||||
@ -912,11 +920,12 @@ class UploadDataStore {
|
||||
loopTreeFolders(path, newTreeFolders, folders, foldersCount);
|
||||
|
||||
if (!isCopy || destFolderId === this.selectedFolderStore.id) {
|
||||
fetchFiles(this.selectedFolderStore.id, filter, true, true).finally(
|
||||
() => {
|
||||
this.filesStore
|
||||
.fetchFiles(updatedFolder, this.filesStore.filter, true, true)
|
||||
.finally(() => {
|
||||
setTimeout(() => clearSecondaryProgressData(), TIMEOUT);
|
||||
}
|
||||
);
|
||||
this.dialogsStore.setIsFolderActions(false);
|
||||
});
|
||||
} else {
|
||||
setSecondaryProgressBarData({
|
||||
icon: pbData.icon,
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit b1063eae56d183b5c0b6eb887115c378f3941ebe
|
||||
Subproject commit 8177bad15d567d997a79478a65d32662a6f773b1
|
Loading…
Reference in New Issue
Block a user