mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-07-31 17:38:48 +00:00
[ADF-793] Ability to create PDF renditions in case of non supported formats (#1994)
* Style changes and button * Convert to PDF button * Convert to PDF button part II. * Convert within the Not Supported Format component * Rendition loading skeleton * Conversion is working. * Convert button behaviour tests * Rebasing fix.
This commit is contained in:
committed by
Eugenio Romano
parent
0ff4ff5f24
commit
b457024cab
@@ -70,6 +70,7 @@ describe('RenditionsService', () => {
|
||||
|
||||
it('Create redition service should call the server with the ID passed and the asked encoding', (done) => {
|
||||
service.createRendition('fake-node-id', 'pdf').subscribe((res) => {
|
||||
expect(jasmine.Ajax.requests.mostRecent().method).toBe('POST');
|
||||
expect(jasmine.Ajax.requests.mostRecent().url).toBe('http://localhost:3000/ecm/alfresco/api/-default-/public/alfresco/versions/1/nodes/fake-node-id/renditions');
|
||||
done();
|
||||
});
|
||||
@@ -81,6 +82,17 @@ describe('RenditionsService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('convert', () => {
|
||||
|
||||
it('should call the server with the ID passed and the asked encoding for creation', (done) => {
|
||||
service.convert('fake-node-id', 'pdf');
|
||||
|
||||
expect(jasmine.Ajax.requests.mostRecent().method).toBe('POST');
|
||||
expect(jasmine.Ajax.requests.mostRecent().url).toBe('http://localhost:3000/ecm/alfresco/api/-default-/public/alfresco/versions/1/nodes/fake-node-id/renditions');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Get redition service should catch the error', (done) => {
|
||||
service.getRenditionsListByNodeId('fake-node-id').subscribe((res) => {
|
||||
}, (res) => {
|
||||
|
@@ -76,6 +76,19 @@ export class RenditionsService {
|
||||
.catch(err => this.handleError(err));
|
||||
}
|
||||
|
||||
convert(nodeId: string, encoding: string, pollingInterval: number|undefined) {
|
||||
return this.createRendition(nodeId, encoding)
|
||||
.concatMap(() => this.pollRendition(nodeId, encoding, pollingInterval));
|
||||
}
|
||||
|
||||
private pollRendition(nodeId: string, encoding: string, interval: number = 1000) {
|
||||
return Observable.interval(interval)
|
||||
.switchMap(() => this.getRendition(nodeId, encoding))
|
||||
.takeWhile((data) => {
|
||||
return (data.entry.status !== 'CREATED');
|
||||
});
|
||||
}
|
||||
|
||||
private handleError(error: any): Observable<any> {
|
||||
this.logService.error(error);
|
||||
return Observable.throw(error || 'Server error');
|
||||
|
@@ -35,6 +35,7 @@ import { NotSupportedFormat } from './src/components/notSupportedFormat.componen
|
||||
import { PdfViewerComponent } from './src/components/pdfViewer.component';
|
||||
import { TxtViewerComponent } from './src/components/txtViewer.component';
|
||||
import { ExtensionViewerDirective } from './src/directives/extension-viewer.directive';
|
||||
import { MdIconModule, MdButtonModule, MdProgressSpinnerModule } from '@angular/material';
|
||||
|
||||
export * from './src/components/viewer.component';
|
||||
export * from './src/services/rendering-queue.services';
|
||||
@@ -60,7 +61,10 @@ export const VIEWER_PROVIDERS: any[] = [
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CoreModule
|
||||
CoreModule,
|
||||
MdIconModule,
|
||||
MdButtonModule,
|
||||
MdProgressSpinnerModule
|
||||
],
|
||||
declarations: [
|
||||
...VIEWER_DIRECTIVES
|
||||
|
@@ -10,7 +10,7 @@
|
||||
"test": "karma start karma.conf.js --reporters mocha,coverage --single-run --mode coverage",
|
||||
"test-browser": "karma start karma.conf.js --reporters kjhtml --component",
|
||||
"coverage": "npm run test && wsrv -o -p 9875 ./coverage/report",
|
||||
"prepublish" : "npm run build"
|
||||
"prepublish": "npm run build"
|
||||
},
|
||||
"main": "bundles/ng2-alfresco-viewer.js",
|
||||
"repository": {
|
||||
@@ -45,7 +45,6 @@
|
||||
"@angular/platform-browser": "~4.0.0",
|
||||
"@angular/platform-browser-dynamic": "~4.0.0",
|
||||
"@angular/router": "~4.0.0",
|
||||
|
||||
"@angular/material": "2.0.0-beta.6",
|
||||
"alfresco-js-api": "~1.5.0",
|
||||
"core-js": "2.4.1",
|
||||
|
@@ -1,23 +1,48 @@
|
||||
|
||||
.viewer-download-text {
|
||||
text-align: center;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.viewer-margin-cloud-download{
|
||||
margin-right: 20px;
|
||||
.unsupported-container {
|
||||
width: 600px;
|
||||
}
|
||||
|
||||
.viewer-margin {
|
||||
margin: auto !important;
|
||||
}
|
||||
|
||||
.center-element {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.full_width{
|
||||
width :95% !important;
|
||||
.viewer-download-text {
|
||||
text-align: center;
|
||||
word-wrap: break-word;
|
||||
min-height: 100px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.viewer-download-text h4 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.adf-conversion-spinner {
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.viewer-margin >>> .adf-conversion-spinner.mat-spinner path {
|
||||
stroke: #00BFD4;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.button-container button {
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
.viewer-button-icon {
|
||||
margin-right: 10px;
|
||||
margin-top: -5px;
|
||||
}
|
||||
|
@@ -1,12 +1,21 @@
|
||||
<section class="section--center mdl-grid mdl-grid--no-spacing">
|
||||
<section *ngIf="!isConversionFinished" class="unsupported-container">
|
||||
<div class="viewer-margin mdl-card mdl-cell mdl-cell--9-col-desktop mdl-cell--6-col-tablet mdl-cell--4-col-phone mdl-shadow--2dp">
|
||||
<div class="viewer-download-text mdl-card__supporting-text viewer-margin">
|
||||
<div *ngIf="!isConversionStarted" class="viewer-download-text mdl-card__supporting-text">
|
||||
<h4>File '<span>{{nameFile}}</span>' is of an unsupported format</h4>
|
||||
</div>
|
||||
<div class="center-element mdl-card__actions">
|
||||
<button id="viewer-download-button" aria-label="Download" class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--accent" (click)="download()">
|
||||
<i class="viewer-margin-cloud-download material-icons">cloud_download</i> Download
|
||||
<md-spinner *ngIf="isConversionStarted" id="conversion-spinner" class="adf-conversion-spinner" color="primary"></md-spinner>
|
||||
<div class="button-container mdl-card__actions">
|
||||
<button md-button id="viewer-download-button" aria-label="Download" class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--accent" (click)="download()">
|
||||
<md-icon class="viewer-button-icon">cloud_download</md-icon> Download
|
||||
</button>
|
||||
<button md-button *ngIf="convertible" [disabled]="isConversionStarted" id="viewer-convert-button" aria-label="Convert to PDF" class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--primary" (click)="convertToPdf()">
|
||||
<md-icon class="viewer-button-icon">insert_drive_file</md-icon> Convert to PDF
|
||||
</button>
|
||||
<button md-button *ngIf="displayable" id="viewer-display-button" aria-label="Show PDF" class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--primary" (click)="showPDF()">
|
||||
<md-icon class="viewer-button-icon">insert_drive_file</md-icon> Show PDF
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<pdf-viewer *ngIf="isConversionFinished" id="pdf-rendition-viewer" [showToolbar]="showToolbar" [urlFile]="renditionUrl" [nameFile]="nameFile"></pdf-viewer>
|
||||
|
@@ -17,34 +17,59 @@
|
||||
|
||||
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
|
||||
import { NotSupportedFormat } from './notSupportedFormat.component';
|
||||
import { PdfViewerComponent } from './pdfViewer.component';
|
||||
import { DebugElement } from '@angular/core';
|
||||
import { MdIconModule, MdButtonModule, MdProgressSpinnerModule } from '@angular/material';
|
||||
import { Subject } from 'rxjs';
|
||||
import {
|
||||
AlfrescoAuthenticationService,
|
||||
AlfrescoSettingsService,
|
||||
AlfrescoApiService,
|
||||
CoreModule,
|
||||
ContentService
|
||||
ContentService,
|
||||
AlfrescoApiService,
|
||||
LogService,
|
||||
RenditionsService
|
||||
} from 'ng2-alfresco-core';
|
||||
|
||||
type RenditionResponse = {
|
||||
entry: {
|
||||
status: string
|
||||
}
|
||||
};
|
||||
|
||||
describe('Test ng2-alfresco-viewer Not Supported Format View component', () => {
|
||||
|
||||
const nodeId = 'not-supported-node-id';
|
||||
|
||||
let component: NotSupportedFormat;
|
||||
let service: ContentService;
|
||||
let fixture: ComponentFixture<NotSupportedFormat>;
|
||||
let debug: DebugElement;
|
||||
let element: HTMLElement;
|
||||
let renditionsService: RenditionsService;
|
||||
|
||||
let renditionSubject: Subject<RenditionResponse>,
|
||||
conversionSubject: Subject<any>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
CoreModule
|
||||
CoreModule,
|
||||
MdIconModule,
|
||||
MdButtonModule,
|
||||
MdProgressSpinnerModule
|
||||
],
|
||||
declarations: [
|
||||
NotSupportedFormat,
|
||||
PdfViewerComponent
|
||||
],
|
||||
declarations: [NotSupportedFormat],
|
||||
providers: [
|
||||
AlfrescoSettingsService,
|
||||
AlfrescoAuthenticationService,
|
||||
AlfrescoApiService,
|
||||
ContentService
|
||||
ContentService,
|
||||
RenditionsService,
|
||||
LogService
|
||||
]
|
||||
}).compileComponents();
|
||||
}));
|
||||
@@ -55,11 +80,21 @@ describe('Test ng2-alfresco-viewer Not Supported Format View component', () => {
|
||||
debug = fixture.debugElement;
|
||||
element = fixture.nativeElement;
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
component.nodeId = nodeId;
|
||||
|
||||
renditionSubject = new Subject<RenditionResponse>();
|
||||
conversionSubject = new Subject<any>();
|
||||
renditionsService = TestBed.get(RenditionsService);
|
||||
spyOn(renditionsService, 'getRendition').and.returnValue(renditionSubject);
|
||||
spyOn(renditionsService, 'convert').and.returnValue(conversionSubject);
|
||||
});
|
||||
|
||||
describe('View', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should be present Download button', () => {
|
||||
expect(element.querySelector('#viewer-download-button')).not.toBeNull();
|
||||
});
|
||||
@@ -69,9 +104,69 @@ describe('Test ng2-alfresco-viewer Not Supported Format View component', () => {
|
||||
fixture.detectChanges();
|
||||
expect(element.querySelector('h4 span').innerHTML).toEqual('Example Content.xls');
|
||||
});
|
||||
|
||||
it('should NOT show loading spinner by default', () => {
|
||||
expect(element.querySelector('#conversion-spinner')).toBeNull('Conversion spinner should NOT be shown by default');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Convertibility to pdf', () => {
|
||||
|
||||
it('should not show the "Convert to PDF" button by default', () => {
|
||||
fixture.detectChanges();
|
||||
expect(element.querySelector('#viewer-convert-button')).toBeNull();
|
||||
});
|
||||
|
||||
it('should be checked on ngInit', () => {
|
||||
fixture.detectChanges();
|
||||
expect(renditionsService.getRendition).toHaveBeenCalledWith(nodeId, 'pdf');
|
||||
});
|
||||
|
||||
it('should NOT be checked on ngInit if nodeId is not set', () => {
|
||||
component.nodeId = null;
|
||||
fixture.detectChanges();
|
||||
expect(renditionsService.getRendition).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should show the "Convert to PDF" button if the node is convertible', async(() => {
|
||||
fixture.detectChanges();
|
||||
renditionSubject.next({ entry: { status: 'NOT_CREATED' } });
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(element.querySelector('#viewer-convert-button')).not.toBeNull();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should NOT show the "Convert to PDF" button if the node is NOT convertible', async(() => {
|
||||
component.convertible = true;
|
||||
fixture.detectChanges();
|
||||
renditionSubject.error(new Error('Mocked error'));
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(element.querySelector('#viewer-convert-button')).toBeNull();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should NOT show the "Convert to PDF" button if the node is already converted', async(() => {
|
||||
renditionSubject.next({ entry: { status: 'CREATED' } });
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(element.querySelector('#viewer-convert-button')).toBeNull();
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
describe('User Interaction', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('Download', () => {
|
||||
|
||||
it('should call download method if Click on Download button', () => {
|
||||
spyOn(window, 'open');
|
||||
component.urlFile = 'test';
|
||||
@@ -93,4 +188,47 @@ describe('Test ng2-alfresco-viewer Not Supported Format View component', () => {
|
||||
expect(service.downloadBlob).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Conversion', () => {
|
||||
|
||||
function clickOnConvertButton() {
|
||||
renditionSubject.next({ entry: { status: 'NOT_CREATED' } });
|
||||
fixture.detectChanges();
|
||||
|
||||
let convertButton: any = element.querySelector('#viewer-convert-button');
|
||||
convertButton.click();
|
||||
fixture.detectChanges();
|
||||
}
|
||||
|
||||
it('should show loading spinner and disable the "Convert to PDF button" after the button was clicked', () => {
|
||||
clickOnConvertButton();
|
||||
|
||||
let convertButton: any = element.querySelector('#viewer-convert-button');
|
||||
expect(element.querySelector('#conversion-spinner')).not.toBeNull('Conversion spinner should be shown');
|
||||
expect(convertButton.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should re-enable the "Convert to PDF button" and hide spinner after unsuccessful conversion and hide loading spinner', () => {
|
||||
clickOnConvertButton();
|
||||
|
||||
conversionSubject.error(new Error());
|
||||
fixture.detectChanges();
|
||||
|
||||
let convertButton: any = element.querySelector('#viewer-convert-button');
|
||||
expect(element.querySelector('#conversion-spinner')).toBeNull('Conversion spinner should be shown');
|
||||
expect(convertButton.disabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should show the pdf rendition after successful conversion', () => {
|
||||
clickOnConvertButton();
|
||||
|
||||
conversionSubject.next();
|
||||
conversionSubject.complete();
|
||||
fixture.detectChanges();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(element.querySelector('#pdf-rendition-viewer')).not.toBeNull('Pdf rendition should be shown.');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -15,15 +15,18 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { ContentService } from 'ng2-alfresco-core';
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { ContentService, RenditionsService } from 'ng2-alfresco-core';
|
||||
import { AlfrescoApiService } from 'ng2-alfresco-core';
|
||||
|
||||
const DEFAULT_CONVERSION_ENCODING = 'pdf';
|
||||
|
||||
@Component({
|
||||
selector: 'not-supported-format',
|
||||
templateUrl: './notSupportedFormat.component.html',
|
||||
styleUrls: ['./notSupportedFormat.component.css']
|
||||
})
|
||||
export class NotSupportedFormat {
|
||||
export class NotSupportedFormat implements OnInit {
|
||||
|
||||
@Input()
|
||||
nameFile: string;
|
||||
@@ -34,9 +37,23 @@ export class NotSupportedFormat {
|
||||
@Input()
|
||||
blobFile: Blob;
|
||||
|
||||
constructor(private contentService: ContentService) {
|
||||
@Input()
|
||||
nodeId: string|null = null;
|
||||
|
||||
}
|
||||
@Input()
|
||||
showToolbar: boolean = true;
|
||||
|
||||
convertible: boolean = false;
|
||||
displayable: boolean = false;
|
||||
isConversionStarted: boolean = false;
|
||||
isConversionFinished: boolean = false;
|
||||
renditionUrl: string|null = null;
|
||||
|
||||
constructor(
|
||||
private contentService: ContentService,
|
||||
private renditionsService: RenditionsService,
|
||||
private apiService: AlfrescoApiService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Download file opening it in a new window
|
||||
@@ -48,4 +65,56 @@ export class NotSupportedFormat {
|
||||
this.contentService.downloadBlob(this.blobFile, this.nameFile);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if (this.nodeId) {
|
||||
this.checkRendition();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update component's button according to the given rendition's availability
|
||||
*
|
||||
* @param {string} encoding - the rendition id
|
||||
*/
|
||||
checkRendition(encoding: string = DEFAULT_CONVERSION_ENCODING): void {
|
||||
this.renditionsService.getRendition(this.nodeId, encoding)
|
||||
.subscribe(
|
||||
(response: any) => {
|
||||
if (response.entry.status === 'NOT_CREATED') {
|
||||
this.convertible = true;
|
||||
this.displayable = false;
|
||||
} else if (response.entry.status === 'CREATED') {
|
||||
this.convertible = false;
|
||||
this.displayable = true;
|
||||
}
|
||||
},
|
||||
() => {
|
||||
this.convertible = false;
|
||||
this.displayable = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the component to loading state and send the conversion starting signal to parent component
|
||||
*/
|
||||
convertToPdf(): void {
|
||||
this.isConversionStarted = true;
|
||||
|
||||
this.renditionsService.convert(this.nodeId, DEFAULT_CONVERSION_ENCODING)
|
||||
.subscribe({
|
||||
error: (error) => { this.isConversionStarted = false; },
|
||||
complete: () => { this.showPDF(); }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the PDF rendition of the node
|
||||
*/
|
||||
showPDF(): void {
|
||||
this.renditionUrl = this.apiService.getInstance().content.getRenditionUrl(this.nodeId, DEFAULT_CONVERSION_ENCODING);
|
||||
this.isConversionStarted = false;
|
||||
this.isConversionFinished = true;
|
||||
}
|
||||
}
|
||||
|
@@ -64,7 +64,13 @@
|
||||
</span>
|
||||
|
||||
<div *ngIf="!supportedExtension()">
|
||||
<not-supported-format *ngIf="!extensionTemplate" [urlFile]="urlFileContent" [blobFile]="blobFile" [nameFile]="displayName"></not-supported-format>
|
||||
<not-supported-format *ngIf="!extensionTemplate"
|
||||
[urlFile]="urlFileContent"
|
||||
[blobFile]="blobFile"
|
||||
[nameFile]="displayName"
|
||||
[showToolbar]="showToolbar"
|
||||
[nodeId]="fileNodeId">
|
||||
</not-supported-format>
|
||||
</div>
|
||||
<!-- End View Switch -->
|
||||
|
||||
|
@@ -29,7 +29,8 @@ import {
|
||||
AlfrescoAuthenticationService,
|
||||
AlfrescoSettingsService,
|
||||
AlfrescoApiService,
|
||||
CoreModule
|
||||
CoreModule,
|
||||
RenditionsService
|
||||
} from 'ng2-alfresco-core';
|
||||
|
||||
declare let jasmine: any;
|
||||
@@ -58,7 +59,8 @@ describe('Test ng2-alfresco-viewer ViewerComponent', () => {
|
||||
AlfrescoSettingsService,
|
||||
AlfrescoAuthenticationService,
|
||||
AlfrescoApiService,
|
||||
RenderingQueueServices
|
||||
RenderingQueueServices,
|
||||
RenditionsService
|
||||
]
|
||||
}).compileComponents();
|
||||
}));
|
||||
@@ -74,6 +76,8 @@ describe('Test ng2-alfresco-viewer ViewerComponent', () => {
|
||||
|
||||
component.showToolbar = true;
|
||||
component.urlFile = 'base/src/assets/fake-test-file.pdf';
|
||||
component.mimeType = 'application/pdf';
|
||||
component.ngOnChanges(null);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
@@ -196,7 +200,7 @@ describe('Test ng2-alfresco-viewer ViewerComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Extension Type Test', () => {
|
||||
describe('Exteznsion Type Test', () => {
|
||||
it('should extension file pdf be loaded', (done) => {
|
||||
component.urlFile = 'base/src/assets/fake-test-file.pdf';
|
||||
|
||||
@@ -259,6 +263,7 @@ describe('Test ng2-alfresco-viewer ViewerComponent', () => {
|
||||
|
||||
it('should the not supported div be loaded if the file is a not supported extension', (done) => {
|
||||
component.urlFile = 'fake-url-file.unsupported';
|
||||
component.mimeType = '';
|
||||
|
||||
component.ngOnChanges(null).then(() => {
|
||||
fixture.detectChanges();
|
||||
|
@@ -143,7 +143,9 @@
|
||||
"wsrv": "^0.1.7",
|
||||
"node-sass": "4.5.3",
|
||||
"sass-loader": "6.0.5",
|
||||
"markdown-toc": "1.1.0"
|
||||
"license-check": "1.1.5",
|
||||
"markdown-toc": "1.1.0",
|
||||
"happypack": "3.0.0"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"module": "./index.js",
|
||||
|
Reference in New Issue
Block a user