Revert custom file drop implementation

This commit is contained in:
shamoon 2023-09-25 23:57:23 -07:00
parent 209c621a0b
commit 01f57af215
14 changed files with 214 additions and 258 deletions

View File

@ -29,7 +29,6 @@ test('dashboard saved view show all', async ({ page }) => {
.locator('pngx-widget-frame')
.filter({ hasText: 'Inbox' })
.getByRole('link', { name: 'Show all' })
.first()
.click()
await expect(page).toHaveURL(/view\/7/)
await expect(page.locator('pngx-document-list')).toHaveText(/8 documents/)

View File

@ -28,6 +28,7 @@
"ngx-color": "^9.0.0",
"ngx-cookie-service": "^16.0.1",
"ngx-drag-drop": "^16.1.0",
"ngx-file-drop": "^16.0.0",
"ngx-ui-tour-ng-bootstrap": "^13.0.4",
"rxjs": "^7.8.1",
"tslib": "^2.6.2",
@ -14073,6 +14074,22 @@
"@angular/core": "^16.0.0"
}
},
"node_modules/ngx-file-drop": {
"version": "16.0.0",
"resolved": "https://registry.npmjs.org/ngx-file-drop/-/ngx-file-drop-16.0.0.tgz",
"integrity": "sha512-33RPoZBAiMkV110Rzu3iOrzGcG5M20S4sAiwLzNylfJobu9qVw5XR83FhUelSeqJRoaDxXBRKAozYCSnUf2CNw==",
"dependencies": {
"tslib": "^2.3.0"
},
"engines": {
"node": ">= 14.5.0",
"npm": ">= 6.9.0"
},
"peerDependencies": {
"@angular/common": ">=14.0.0",
"@angular/core": ">=14.0.0"
}
},
"node_modules/ngx-ui-tour-core": {
"version": "11.0.4",
"resolved": "https://registry.npmjs.org/ngx-ui-tour-core/-/ngx-ui-tour-core-11.0.4.tgz",

View File

@ -30,6 +30,7 @@
"ngx-color": "^9.0.0",
"ngx-cookie-service": "^16.0.1",
"ngx-drag-drop": "^16.1.0",
"ngx-file-drop": "^16.0.0",
"ngx-ui-tour-ng-bootstrap": "^13.0.4",
"rxjs": "^7.8.1",
"tslib": "^2.6.2",

View File

@ -1,10 +1,16 @@
<pngx-toasts></pngx-toasts>
<pngx-file-drop>
<ng-container content>
<ngx-file-drop dropZoneClassName="main-dropzone" contentClassName="main-content" [disabled]="!dragDropEnabled"
(onFileDrop)="dropped($event)" (onFileOver)="fileOver()" (onFileLeave)="fileLeave()">
<ng-template ngx-file-drop-content-tmp>
<div class="global-dropzone-overlay fade" [class.show]="fileIsOver" [class.hide]="hidden">
<h2 i18n>Drop files to begin upload</h2>
</div>
<div [class.inert]="fileIsOver">
<router-outlet></router-outlet>
</ng-container>
</pngx-file-drop>
</div>
</ng-template>
</ngx-file-drop>
<tour-step-template>
<ng-template #tourStep let-step="step">

View File

@ -5,8 +5,10 @@ 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'
@ -18,8 +20,8 @@ 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'
describe('AppComponent', () => {
let component: AppComponent
@ -30,15 +32,17 @@ describe('AppComponent', () => {
let toastService: ToastService
let router: Router
let settingsService: SettingsService
let uploadDocumentsService: UploadDocumentsService
beforeEach(async () => {
TestBed.configureTestingModule({
declarations: [AppComponent, ToastsComponent, FileDropComponent],
declarations: [AppComponent, ToastsComponent],
providers: [],
imports: [
HttpClientTestingModule,
TourNgBootstrapModule,
RouterTestingModule.withRoutes(routes),
NgxFileDropModule,
],
}).compileComponents()
@ -48,6 +52,7 @@ 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
})
@ -132,4 +137,44 @@ 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(component.fileIsOver).toBeFalsy()
component.fileOver()
tick(1)
fixture.detectChanges()
expect(component.fileIsOver).toBeTruthy()
const dropzone = fixture.debugElement.query(
By.css('.global-dropzone-overlay')
)
expect(dropzone).not.toBeNull()
component.fileLeave()
tick(700)
fixture.detectChanges()
expect(dropzone.classes['hide']).toBeTruthy()
// drop
const toastSpy = jest.spyOn(toastService, 'show')
const uploadSpy = jest.spyOn(uploadDocumentsService, 'onNgxFileDrop')
component.dropped([
{
fileEntry: {
isFile: true,
file: () => {},
},
} as unknown as NgxFileDropEntry,
])
tick(3000)
expect(toastSpy).toHaveBeenCalled()
expect(uploadSpy).toHaveBeenCalled()
}))
})

View File

@ -5,6 +5,7 @@ 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'
@ -24,11 +25,16 @@ export class AppComponent implements OnInit, OnDestroy {
successSubscription: Subscription
failedSubscription: Subscription
private fileLeaveTimeoutID: any
fileIsOver: boolean = false
hidden: boolean = true
constructor(
private settings: SettingsService,
private consumerStatusService: ConsumerStatusService,
private toastService: ToastService,
private router: Router,
private uploadDocumentsService: UploadDocumentsService,
private tasksService: TasksService,
public tourService: TourService,
private renderer: Renderer2,
@ -244,4 +250,46 @@ 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
// allows transition
setTimeout(() => {
this.fileIsOver = true
}, 1)
this.hidden = false
// stop fileLeave timeout
clearTimeout(this.fileLeaveTimeoutID)
}
public fileLeave(immediate: boolean = false) {
this.settings.globalDropzoneActive = false
const ms = immediate ? 0 : 500
this.fileLeaveTimeoutID = setTimeout(() => {
this.fileIsOver = false
// await transition completed
setTimeout(() => {
this.hidden = true
}, 150)
}, ms)
}
public dropped(files: NgxFileDropEntry[]) {
this.settings.globalDropzoneActive = false
this.fileLeave(true)
this.uploadDocumentsService.onNgxFileDrop(files)
if (files.length > 0)
this.toastService.showInfo($localize`Initiating upload...`, 3000)
}
}

View File

@ -35,6 +35,7 @@ import { DateDropdownComponent } from './components/common/date-dropdown/date-dr
import { DocumentCardLargeComponent } from './components/document-list/document-card-large/document-card-large.component'
import { DocumentCardSmallComponent } from './components/document-list/document-card-small/document-card-small.component'
import { BulkEditorComponent } from './components/document-list/bulk-editor/bulk-editor.component'
import { NgxFileDropModule } from 'ngx-file-drop'
import { TextComponent } from './components/common/input/text/text.component'
import { SelectComponent } from './components/common/input/select/select.component'
import { CheckComponent } from './components/common/input/check/check.component'
@ -99,7 +100,6 @@ 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,7 +242,6 @@ function initializeApp(settings: SettingsService) {
ConsumptionTemplateEditDialogComponent,
MailComponent,
UsersAndGroupsComponent,
FileDropComponent,
],
imports: [
BrowserModule,
@ -251,6 +250,7 @@ function initializeApp(settings: SettingsService) {
HttpClientModule,
FormsModule,
ReactiveFormsModule,
NgxFileDropModule,
PdfViewerModule,
NgSelectModule,
ColorSliderModule,

View File

@ -3,7 +3,7 @@
<form class="justify-content-center d-flex flex-column align-items-center py-3 px-2">
<span class="text-muted" i18n>Drop documents anywhere or</span>
<button class="btn btn-sm btn-outline-primary mt-3" (click)="fileUpload.click()" i18n>Browse files</button>
<input type="file" class="visually-hidden" (change)="onFileSelected($event)" #fileUpload>
<input type="file" class="visually-hidden" (change)="onFileSelected($event)" multiple #fileUpload>
</form>
<div class="fixed-bottom p-2 p-md-4" [ngClass]="slimSidebarEnabled ? 'col-slim' : 'offset-md-3 offset-lg-2'">
<div class="row d-flex justify-content-end">

View File

@ -1,7 +0,0 @@
<div [class.inert]="fileIsOver">
<ng-content select="[content]"></ng-content>
</div>
<div class="global-dropzone-overlay fade" [class.show]="fileIsOver" [class.hide]="hidden">
<h2 i18n>Drop files to begin upload</h2>
</div>

View File

@ -1,115 +0,0 @@
import { HttpClientTestingModule } from '@angular/common/http/testing'
import {
ComponentFixture,
TestBed,
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'
describe('FileDropComponent', () => {
let component: FileDropComponent
let fixture: ComponentFixture<FileDropComponent>
let permissionsService: PermissionsService
let toastService: ToastService
let settingsService: SettingsService
let uploadDocumentsService: UploadDocumentsService
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [FileDropComponent, ToastsComponent],
providers: [],
imports: [HttpClientTestingModule],
}).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, 'uploadFiles')
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)
tick(3000)
expect(toastSpy).toHaveBeenCalled()
expect(uploadSpy).toHaveBeenCalled()
}))
})

View File

@ -1,81 +0,0 @@
import { Component, HostListener } from '@angular/core'
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
)
)
}
@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)
this.uploadDocumentsService.uploadFiles(event.dataTransfer.files)
if (event.dataTransfer.files.length)
this.toastService.showInfo($localize`Initiating upload...`, 3000)
}
}

View File

@ -134,4 +134,35 @@ describe('UploadDocumentsService', () => {
consumerStatusService.getConsumerStatus(FileStatusPhase.FAILED)
).toHaveLength(2)
})
it('accepts files via drag and drop', () => {
const uploadSpy = jest.spyOn(
UploadDocumentsService.prototype as any,
'uploadFile'
)
const fileEntry = {
name: 'file.pdf',
isDirectory: false,
isFile: true,
file: (callback) => {
return callback(
new File(
[new Blob(['testing'], { type: 'application/pdf' })],
'file.pdf'
)
)
},
}
uploadDocumentsService.onNgxFileDrop([
{
relativePath: 'path/to/file.pdf',
fileEntry,
},
])
expect(uploadSpy).toHaveBeenCalled()
let req = httpTestingController.match(
`${environment.apiBaseUrl}documents/post_document/`
)
})
})

View File

@ -1,5 +1,6 @@
import { Injectable } from '@angular/core'
import { HttpEventType } from '@angular/common/http'
import { FileSystemFileEntry, NgxFileDropEntry } from 'ngx-file-drop'
import {
ConsumerStatusService,
FileStatusPhase,
@ -18,10 +19,22 @@ export class UploadDocumentsService {
private consumerStatusService: ConsumerStatusService
) {}
onNgxFileDrop(files: NgxFileDropEntry[]) {
for (const droppedFile of files) {
if (droppedFile.fileEntry.isFile) {
const fileEntry = droppedFile.fileEntry as FileSystemFileEntry
fileEntry.file((file: File) => this.uploadFile(file))
}
}
}
uploadFiles(files: FileList) {
for (let index = 0; index < files.length; index++) {
const file = files.item(index)
this.uploadFile(files.item(index))
}
}
private uploadFile(file: File) {
let formData = new FormData()
formData.append('document', file, file.name)
let status = this.consumerStatusService.newFileUpload(file.name)
@ -63,5 +76,4 @@ export class UploadDocumentsService {
},
})
}
}
}