diff --git a/src-ui/src/app/app.component.html b/src-ui/src/app/app.component.html index f1cc630fb..ef2addf2c 100644 --- a/src-ui/src/app/app.component.html +++ b/src-ui/src/app/app.component.html @@ -1,16 +1,10 @@ - - -
-

Drop files to begin upload

-
-
- -
-
-
+ + + + + diff --git a/src-ui/src/app/app.component.spec.ts b/src-ui/src/app/app.component.spec.ts index 10016fa64..80fbdfa5f 100644 --- a/src-ui/src/app/app.component.spec.ts +++ b/src-ui/src/app/app.component.spec.ts @@ -5,10 +5,8 @@ import { fakeAsync, tick, } from '@angular/core/testing' -import { By } from '@angular/platform-browser' import { Router } from '@angular/router' import { RouterTestingModule } from '@angular/router/testing' -import { NgxFileDropEntry, NgxFileDropModule } from 'ngx-file-drop' import { TourService, TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap' import { Subject } from 'rxjs' import { routes } from './app-routing.module' @@ -20,8 +18,9 @@ import { } from './services/consumer-status.service' import { PermissionsService } from './services/permissions.service' import { ToastService, Toast } from './services/toast.service' -import { UploadDocumentsService } from './services/upload-documents.service' import { SettingsService } from './services/settings.service' +import { FileDropComponent } from './components/file-drop/file-drop.component' +import { NgxFileDropModule } from 'ngx-file-drop' describe('AppComponent', () => { let component: AppComponent @@ -32,11 +31,10 @@ describe('AppComponent', () => { let toastService: ToastService let router: Router let settingsService: SettingsService - let uploadDocumentsService: UploadDocumentsService beforeEach(async () => { TestBed.configureTestingModule({ - declarations: [AppComponent, ToastsComponent], + declarations: [AppComponent, ToastsComponent, FileDropComponent], providers: [], imports: [ HttpClientTestingModule, @@ -52,7 +50,6 @@ describe('AppComponent', () => { settingsService = TestBed.inject(SettingsService) toastService = TestBed.inject(ToastService) router = TestBed.inject(Router) - uploadDocumentsService = TestBed.inject(UploadDocumentsService) fixture = TestBed.createComponent(AppComponent) component = fixture.componentInstance }) @@ -142,43 +139,4 @@ describe('AppComponent', () => { fileStatusSubject.next(new FileStatus()) expect(toastSpy).toHaveBeenCalled() }) - - it('should enable drag-drop if user has permissions', () => { - jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true) - expect(component.dragDropEnabled).toBeTruthy() - }) - - it('should disable drag-drop if user does not have permissions', () => { - jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(false) - expect(component.dragDropEnabled).toBeFalsy() - }) - - it('should support drag drop', fakeAsync(() => { - expect(settingsService.globalDropzoneActive).toBeFalsy() - component.fileOver() - tick(1) - fixture.detectChanges() - expect(settingsService.globalDropzoneActive).toBeTruthy() - const dropzone = fixture.debugElement.query( - By.css('.global-dropzone-overlay') - ) - expect(dropzone).not.toBeNull() - tick(700) - fixture.detectChanges() - // drop - const toastSpy = jest.spyOn(toastService, 'show') - const uploadSpy = jest.spyOn(uploadDocumentsService, 'onNgxFileDrop') - component.dropped([ - { - fileEntry: { - isFile: true, - file: () => {}, - }, - } as unknown as NgxFileDropEntry, - ]) - expect(settingsService.globalDropzoneActive).toBeFalsy() - tick(3000) - expect(toastSpy).toHaveBeenCalled() - expect(uploadSpy).toHaveBeenCalled() - })) }) diff --git a/src-ui/src/app/app.component.ts b/src-ui/src/app/app.component.ts index 3baaa63d2..fb54665f0 100644 --- a/src-ui/src/app/app.component.ts +++ b/src-ui/src/app/app.component.ts @@ -5,8 +5,6 @@ import { Router } from '@angular/router' import { Subscription, first } from 'rxjs' import { ConsumerStatusService } from './services/consumer-status.service' import { ToastService } from './services/toast.service' -import { NgxFileDropEntry } from 'ngx-file-drop' -import { UploadDocumentsService } from './services/upload-documents.service' import { TasksService } from './services/tasks.service' import { TourService } from 'ngx-ui-tour-ng-bootstrap' import { @@ -30,7 +28,6 @@ export class AppComponent implements OnInit, OnDestroy { private consumerStatusService: ConsumerStatusService, private toastService: ToastService, private router: Router, - private uploadDocumentsService: UploadDocumentsService, private tasksService: TasksService, public tourService: TourService, private renderer: Renderer2, @@ -246,29 +243,4 @@ export class AppComponent implements OnInit, OnDestroy { }) }) } - - public get dragDropEnabled(): boolean { - return ( - this.settings.globalDropzoneEnabled && - this.permissionsService.currentUserCan( - PermissionAction.Add, - PermissionType.Document - ) - ) - } - - public fileOver() { - this.settings.globalDropzoneActive = true - } - - public fileLeave() { - this.settings.globalDropzoneActive = false - } - - public dropped(files: NgxFileDropEntry[]) { - this.fileLeave() - this.uploadDocumentsService.onNgxFileDrop(files) - if (files.length > 0) - this.toastService.showInfo($localize`Initiating upload...`, 3000) - } } diff --git a/src-ui/src/app/app.module.ts b/src-ui/src/app/app.module.ts index 945dfe515..42a226015 100644 --- a/src-ui/src/app/app.module.ts +++ b/src-ui/src/app/app.module.ts @@ -100,6 +100,7 @@ import { ConsumptionTemplateEditDialogComponent } from './components/common/edit import { MailComponent } from './components/manage/mail/mail.component' import { UsersAndGroupsComponent } from './components/admin/users-groups/users-groups.component' import { DndModule } from 'ngx-drag-drop' +import { FileDropComponent } from './components/file-drop/file-drop.component' import localeAf from '@angular/common/locales/af' import localeAr from '@angular/common/locales/ar' @@ -242,6 +243,7 @@ function initializeApp(settings: SettingsService) { ConsumptionTemplateEditDialogComponent, MailComponent, UsersAndGroupsComponent, + FileDropComponent, ], imports: [ BrowserModule, diff --git a/src-ui/src/app/components/file-drop/file-drop.component.html b/src-ui/src/app/components/file-drop/file-drop.component.html new file mode 100644 index 000000000..1d4b9e0e6 --- /dev/null +++ b/src-ui/src/app/components/file-drop/file-drop.component.html @@ -0,0 +1,14 @@ +
+ +
+ +
+

Drop files to begin upload

+
+ + + diff --git a/src-ui/src/app/components/file-drop/file-drop.component.scss b/src-ui/src/app/components/file-drop/file-drop.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src-ui/src/app/components/file-drop/file-drop.component.spec.ts b/src-ui/src/app/components/file-drop/file-drop.component.spec.ts new file mode 100644 index 000000000..e945d31b8 --- /dev/null +++ b/src-ui/src/app/components/file-drop/file-drop.component.spec.ts @@ -0,0 +1,148 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing' +import { + ComponentFixture, + TestBed, + discardPeriodicTasks, + fakeAsync, + tick, +} from '@angular/core/testing' +import { By } from '@angular/platform-browser' +import { PermissionsService } from 'src/app/services/permissions.service' +import { SettingsService } from 'src/app/services/settings.service' +import { ToastService } from 'src/app/services/toast.service' +import { UploadDocumentsService } from 'src/app/services/upload-documents.service' +import { ToastsComponent } from '../common/toasts/toasts.component' +import { FileDropComponent } from './file-drop.component' +import { NgxFileDropEntry, NgxFileDropModule } from 'ngx-file-drop' + +describe('FileDropComponent', () => { + let component: FileDropComponent + let fixture: ComponentFixture + let permissionsService: PermissionsService + let toastService: ToastService + let settingsService: SettingsService + let uploadDocumentsService: UploadDocumentsService + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [FileDropComponent, ToastsComponent], + providers: [], + imports: [HttpClientTestingModule, NgxFileDropModule], + }).compileComponents() + + permissionsService = TestBed.inject(PermissionsService) + settingsService = TestBed.inject(SettingsService) + toastService = TestBed.inject(ToastService) + uploadDocumentsService = TestBed.inject(UploadDocumentsService) + + fixture = TestBed.createComponent(FileDropComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should enable drag-drop if user has permissions', () => { + jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true) + expect(component.dragDropEnabled).toBeTruthy() + }) + + it('should disable drag-drop if user does not have permissions', () => { + jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(false) + expect(component.dragDropEnabled).toBeFalsy() + }) + + it('should disable drag-drop if disabled in settings', fakeAsync(() => { + jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true) + settingsService.globalDropzoneEnabled = false + expect(component.dragDropEnabled).toBeFalsy() + + component.onDragOver(new Event('dragover') as DragEvent) + tick(1) + fixture.detectChanges() + expect(component.fileIsOver).toBeFalsy() + const dropzone = fixture.debugElement.query( + By.css('.global-dropzone-overlay') + ) + expect(dropzone.classes['hide']).toBeTruthy() + component.onDragLeave(new Event('dragleave') as DragEvent) + tick(700) + fixture.detectChanges() + // drop + const uploadSpy = jest.spyOn(uploadDocumentsService, 'uploadFiles') + const dragEvent = new Event('drop') + dragEvent['dataTransfer'] = { + files: { + item: () => {}, + length: 0, + }, + } + component.onDrop(dragEvent as DragEvent) + tick(3000) + expect(uploadSpy).not.toHaveBeenCalled() + })) + + it('should support drag drop, initiate upload', fakeAsync(() => { + jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true) + expect(component.fileIsOver).toBeFalsy() + component.onDragOver(new Event('dragover') as DragEvent) + tick(1) + fixture.detectChanges() + expect(component.fileIsOver).toBeTruthy() + const dropzone = fixture.debugElement.query( + By.css('.global-dropzone-overlay') + ) + component.onDragLeave(new Event('dragleave') as DragEvent) + tick(700) + fixture.detectChanges() + expect(dropzone.classes['hide']).toBeTruthy() + // drop + const toastSpy = jest.spyOn(toastService, 'show') + const uploadSpy = jest.spyOn( + UploadDocumentsService.prototype as any, + 'uploadFile' + ) + const dragEvent = new Event('drop') + dragEvent['dataTransfer'] = { + files: { + item: () => { + return new File( + [new Blob(['testing'], { type: 'application/pdf' })], + 'file.pdf' + ) + }, + length: 1, + } as unknown as FileList, + } + component.onDrop(dragEvent as DragEvent) + component.dropped([ + { + fileEntry: { + isFile: true, + file: (callback) => { + callback( + new File( + [new Blob(['testing'], { type: 'application/pdf' })], + 'file.pdf' + ) + ) + }, + }, + } as unknown as NgxFileDropEntry, + ]) + tick(3000) + expect(toastSpy).toHaveBeenCalled() + expect(uploadSpy).toHaveBeenCalled() + discardPeriodicTasks() + })) + + it('should ignore events if disabled', fakeAsync(() => { + settingsService.globalDropzoneEnabled = false + expect(settingsService.globalDropzoneActive).toBeFalsy() + component.onDragOver(new Event('dragover') as DragEvent) + expect(settingsService.globalDropzoneActive).toBeFalsy() + settingsService.globalDropzoneActive = true + component.onDragLeave(new Event('dragleave') as DragEvent) + expect(settingsService.globalDropzoneActive).toBeTruthy() + component.onDrop(new Event('drop') as DragEvent) + expect(settingsService.globalDropzoneActive).toBeTruthy() + })) +}) diff --git a/src-ui/src/app/components/file-drop/file-drop.component.ts b/src-ui/src/app/components/file-drop/file-drop.component.ts new file mode 100644 index 000000000..63d26bb26 --- /dev/null +++ b/src-ui/src/app/components/file-drop/file-drop.component.ts @@ -0,0 +1,89 @@ +import { Component, HostListener, ViewChild } from '@angular/core' +import { NgxFileDropComponent, NgxFileDropEntry } from 'ngx-file-drop' +import { + PermissionsService, + PermissionAction, + PermissionType, +} from 'src/app/services/permissions.service' +import { SettingsService } from 'src/app/services/settings.service' +import { ToastService } from 'src/app/services/toast.service' +import { UploadDocumentsService } from 'src/app/services/upload-documents.service' + +@Component({ + selector: 'pngx-file-drop', + templateUrl: './file-drop.component.html', + styleUrls: ['./file-drop.component.scss'], +}) +export class FileDropComponent { + private fileLeaveTimeoutID: any + fileIsOver: boolean = false + hidden: boolean = true + + constructor( + private settings: SettingsService, + private toastService: ToastService, + private uploadDocumentsService: UploadDocumentsService, + private permissionsService: PermissionsService + ) {} + + public get dragDropEnabled(): boolean { + return ( + this.settings.globalDropzoneEnabled && + this.permissionsService.currentUserCan( + PermissionAction.Add, + PermissionType.Document + ) + ) + } + + @ViewChild('ngxFileDrop') ngxFileDrop: NgxFileDropComponent + + @HostListener('dragover', ['$event ']) onDragOver(event: DragEvent) { + if (!this.dragDropEnabled) return + event.preventDefault() + event.stopImmediatePropagation() + this.settings.globalDropzoneActive = true + // allows transition + setTimeout(() => { + this.fileIsOver = true + }, 1) + this.hidden = false + // stop fileLeave timeout + clearTimeout(this.fileLeaveTimeoutID) + } + + @HostListener('dragleave', ['$event']) public onDragLeave( + event: DragEvent, + immediate: boolean = false + ) { + if (!this.dragDropEnabled) return + event.preventDefault() + event.stopImmediatePropagation() + this.settings.globalDropzoneActive = false + + const ms = immediate ? 0 : 500 + + this.fileLeaveTimeoutID = setTimeout(() => { + this.fileIsOver = false + // await transition completed + setTimeout(() => { + this.hidden = true + }, 150) + }, ms) + } + + @HostListener('drop', ['$event']) public onDrop(event: DragEvent) { + if (!this.dragDropEnabled) return + event.preventDefault() + event.stopImmediatePropagation() + this.onDragLeave(event, true) + // pass event onto ngx-file-drop to handle files + this.ngxFileDrop.dropFiles(event) + } + + public dropped(files: NgxFileDropEntry[]) { + this.uploadDocumentsService.onNgxFileDrop(files) + if (files.length > 0) + this.toastService.showInfo($localize`Initiating upload...`, 3000) + } +} diff --git a/src-ui/src/styles.scss b/src-ui/src/styles.scss index beaf258ef..6065224ce 100644 --- a/src-ui/src/styles.scss +++ b/src-ui/src/styles.scss @@ -505,8 +505,6 @@ table.table { user-select: none !important; text-align: center; padding-top: 25%; - opacity: 0; - transition: opacity 0.05s linear; h2 { color: var(--pngx-primary-text-contrast)