mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Merge pull request #283 from paperless-ngx/feature-global-dragdrop
Feature: global drag'n'drop
This commit is contained in:
		| @@ -1,3 +1,13 @@ | ||||
| <app-toasts></app-toasts> | ||||
|  | ||||
| <router-outlet></router-outlet> | ||||
| <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> | ||||
|         </div> | ||||
|     </ng-template> | ||||
| </ngx-file-drop> | ||||
|   | ||||
| @@ -4,6 +4,8 @@ import { Router } from '@angular/router' | ||||
| import { Subscription } 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' | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-root', | ||||
| @@ -15,11 +17,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 router: Router, | ||||
|     private uploadDocumentsService: UploadDocumentsService | ||||
|   ) { | ||||
|     let anyWindow = window as any | ||||
|     anyWindow.pdfWorkerSrc = 'assets/js/pdf.worker.min.js' | ||||
| @@ -100,4 +107,36 @@ export class AppComponent implements OnInit, OnDestroy { | ||||
|         } | ||||
|       }) | ||||
|   } | ||||
|  | ||||
|   public get dragDropEnabled(): boolean { | ||||
|     return !this.router.url.includes('dashboard') | ||||
|   } | ||||
|  | ||||
|   public fileOver() { | ||||
|     // allows transition | ||||
|     setTimeout(() => { | ||||
|       this.fileIsOver = true | ||||
|     }, 1) | ||||
|     this.hidden = false | ||||
|     // stop fileLeave timeout | ||||
|     clearTimeout(this.fileLeaveTimeoutID) | ||||
|   } | ||||
|  | ||||
|   public fileLeave(immediate: boolean = 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.fileLeave(true) | ||||
|     this.uploadDocumentsService.uploadFiles(files) | ||||
|     this.toastService.showInfo($localize`Initiating upload...`, 3000) | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
|   *ngFor="let toast of toasts" | ||||
|   [header]="toast.title" [autohide]="true" [delay]="toast.delay" | ||||
|   [class]="toast.classname" | ||||
|   (hide)="toastService.closeToast(toast)"> | ||||
|   (hidden)="toastService.closeToast(toast)"> | ||||
|   <p>{{toast.content}}</p> | ||||
|   <p *ngIf="toast.action"><button class="btn btn-sm btn-outline-secondary" (click)="toastService.closeToast(toast); toast.action()">{{toast.actionName}}</button></p> | ||||
| </ngb-toast> | ||||
|   | ||||
| @@ -5,3 +5,7 @@ | ||||
|   margin: 0.5em; | ||||
|   z-index: 1200; | ||||
| } | ||||
|  | ||||
| .toast:not(.show) { | ||||
|   display: block; // this corrects an ng-bootstrap bug that prevented animations | ||||
| } | ||||
| @@ -33,3 +33,7 @@ form { | ||||
|   mix-blend-mode: soft-light; | ||||
|   pointer-events: none; | ||||
| } | ||||
|  | ||||
| ::ng-deep .ngx-file-drop__drop-zone--over { | ||||
|   background-color: var(--ngx-primary-faded) !important; | ||||
| } | ||||
|   | ||||
| @@ -6,7 +6,7 @@ import { | ||||
|   FileStatus, | ||||
|   FileStatusPhase, | ||||
| } from 'src/app/services/consumer-status.service' | ||||
| import { DocumentService } from 'src/app/services/rest/document.service' | ||||
| import { UploadDocumentsService } from 'src/app/services/upload-documents.service' | ||||
|  | ||||
| const MAX_ALERTS = 5 | ||||
|  | ||||
| @@ -19,8 +19,8 @@ export class UploadFileWidgetComponent implements OnInit { | ||||
|   alertsExpanded = false | ||||
|  | ||||
|   constructor( | ||||
|     private documentService: DocumentService, | ||||
|     private consumerStatusService: ConsumerStatusService | ||||
|     private consumerStatusService: ConsumerStatusService, | ||||
|     private uploadDocumentsService: UploadDocumentsService | ||||
|   ) {} | ||||
|  | ||||
|   getStatus() { | ||||
| @@ -116,48 +116,6 @@ export class UploadFileWidgetComponent implements OnInit { | ||||
|   public fileLeave(event) {} | ||||
|  | ||||
|   public dropped(files: NgxFileDropEntry[]) { | ||||
|     for (const droppedFile of files) { | ||||
|       if (droppedFile.fileEntry.isFile) { | ||||
|         const fileEntry = droppedFile.fileEntry as FileSystemFileEntry | ||||
|         fileEntry.file((file: File) => { | ||||
|           let formData = new FormData() | ||||
|           formData.append('document', file, file.name) | ||||
|           let status = this.consumerStatusService.newFileUpload(file.name) | ||||
|  | ||||
|           status.message = $localize`Connecting...` | ||||
|  | ||||
|           this.documentService.uploadDocument(formData).subscribe( | ||||
|             (event) => { | ||||
|               if (event.type == HttpEventType.UploadProgress) { | ||||
|                 status.updateProgress( | ||||
|                   FileStatusPhase.UPLOADING, | ||||
|                   event.loaded, | ||||
|                   event.total | ||||
|                 ) | ||||
|                 status.message = $localize`Uploading...` | ||||
|               } else if (event.type == HttpEventType.Response) { | ||||
|                 status.taskId = event.body['task_id'] | ||||
|                 status.message = $localize`Upload complete, waiting...` | ||||
|               } | ||||
|             }, | ||||
|             (error) => { | ||||
|               switch (error.status) { | ||||
|                 case 400: { | ||||
|                   this.consumerStatusService.fail(status, error.error.document) | ||||
|                   break | ||||
|                 } | ||||
|                 default: { | ||||
|                   this.consumerStatusService.fail( | ||||
|                     status, | ||||
|                     $localize`HTTP error: ${error.status} ${error.statusText}` | ||||
|                   ) | ||||
|                   break | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           ) | ||||
|         }) | ||||
|       } | ||||
|     } | ||||
|     this.uploadDocumentsService.uploadFiles(files) | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -75,7 +75,7 @@ | ||||
|  | ||||
| </app-page-header> | ||||
|  | ||||
| <div class="sticky-top py-2 mt-n2 mt-sm-n3 py-sm-4 bg-body mx-n3 px-3"> | ||||
| <div class="row sticky-top pt-4 pb-2 pb-lg-4 bg-body"> | ||||
|   <app-filter-editor [hidden]="isBulkEditing" [(filterRules)]="list.filterRules" [unmodifiedFilterRules]="unmodifiedFilterRules" #filterEditor></app-filter-editor> | ||||
|   <app-bulk-editor [hidden]="!isBulkEditing"></app-bulk-editor> | ||||
| </div> | ||||
| @@ -185,7 +185,7 @@ | ||||
|     </tbody> | ||||
|   </table> | ||||
|  | ||||
|   <div class="m-n2 row row-cols-paperless-cards" *ngIf="displayMode == 'smallCards'"> | ||||
|   <div class="row row-cols-paperless-cards" *ngIf="displayMode == 'smallCards'"> | ||||
|     <app-document-card-small class="p-0" [selected]="list.isSelected(d)" (toggleSelected)="toggleSelected(d, $event)" [document]="d" *ngFor="let d of list.documents; trackBy: trackByDocumentId" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)" (clickDocumentType)="clickDocumentType($event)"></app-document-card-small> | ||||
|   </div> | ||||
|   <div *ngIf="list.documents?.length > 15" class="mt-3"> | ||||
|   | ||||
| @@ -1,5 +1,9 @@ | ||||
| @import "/src/theme"; | ||||
|  | ||||
| ::ng-deep app-document-list app-page-header > div.mb-3 { | ||||
|   margin-bottom: 0 !important; | ||||
| } | ||||
|  | ||||
| tr { | ||||
|   user-select: none; | ||||
| } | ||||
|   | ||||
| @@ -50,7 +50,7 @@ | ||||
|      </div> | ||||
|    </div> | ||||
|    <div class="w-100 d-xl-none"></div> | ||||
|    <div class="col col-xl-auto mb-2 mb-xl-0"> | ||||
|    <div class="col col-xl-auto"> | ||||
|      <button class="btn btn-link btn-sm px-0 mx-0 ms-xl-n3" [disabled]="!rulesModified" (click)="resetSelected()"> | ||||
|        <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-x me-1" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> | ||||
|          <path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/> | ||||
|   | ||||
							
								
								
									
										74
									
								
								src-ui/src/app/services/upload-documents.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								src-ui/src/app/services/upload-documents.service.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | ||||
| import { Injectable } from '@angular/core' | ||||
| import { HttpEventType } from '@angular/common/http' | ||||
| import { FileSystemFileEntry, NgxFileDropEntry } from 'ngx-file-drop' | ||||
| import { | ||||
|   ConsumerStatusService, | ||||
|   FileStatusPhase, | ||||
| } from './consumer-status.service' | ||||
| import { DocumentService } from './rest/document.service' | ||||
| import { Subscription } from 'rxjs' | ||||
|  | ||||
| @Injectable({ | ||||
|   providedIn: 'root', | ||||
| }) | ||||
| export class UploadDocumentsService { | ||||
|   private uploadSubscriptions: Array<Subscription> = [] | ||||
|  | ||||
|   constructor( | ||||
|     private documentService: DocumentService, | ||||
|     private consumerStatusService: ConsumerStatusService | ||||
|   ) {} | ||||
|  | ||||
|   uploadFiles(files: NgxFileDropEntry[]) { | ||||
|     for (const droppedFile of files) { | ||||
|       if (droppedFile.fileEntry.isFile) { | ||||
|         const fileEntry = droppedFile.fileEntry as FileSystemFileEntry | ||||
|         fileEntry.file((file: File) => { | ||||
|           let formData = new FormData() | ||||
|           formData.append('document', file, file.name) | ||||
|           let status = this.consumerStatusService.newFileUpload(file.name) | ||||
|  | ||||
|           status.message = $localize`Connecting...` | ||||
|  | ||||
|           this.uploadSubscriptions[file.name] = this.documentService | ||||
|             .uploadDocument(formData) | ||||
|             .subscribe({ | ||||
|               next: (event) => { | ||||
|                 if (event.type == HttpEventType.UploadProgress) { | ||||
|                   status.updateProgress( | ||||
|                     FileStatusPhase.UPLOADING, | ||||
|                     event.loaded, | ||||
|                     event.total | ||||
|                   ) | ||||
|                   status.message = $localize`Uploading...` | ||||
|                 } else if (event.type == HttpEventType.Response) { | ||||
|                   status.taskId = event.body['task_id'] | ||||
|                   status.message = $localize`Upload complete, waiting...` | ||||
|                   this.uploadSubscriptions[file.name]?.complete() | ||||
|                 } | ||||
|               }, | ||||
|               error: (error) => { | ||||
|                 switch (error.status) { | ||||
|                   case 400: { | ||||
|                     this.consumerStatusService.fail( | ||||
|                       status, | ||||
|                       error.error.document | ||||
|                     ) | ||||
|                     break | ||||
|                   } | ||||
|                   default: { | ||||
|                     this.consumerStatusService.fail( | ||||
|                       status, | ||||
|                       $localize`HTTP error: ${error.status} ${error.statusText}` | ||||
|                     ) | ||||
|                     break | ||||
|                   } | ||||
|                 } | ||||
|                 this.uploadSubscriptions[file.name]?.complete() | ||||
|               }, | ||||
|             }) | ||||
|         }) | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -15,6 +15,10 @@ | ||||
|   --ngx-focus-alpha: 0.3; | ||||
| } | ||||
|  | ||||
| body { | ||||
|   height: 100vh; | ||||
| } | ||||
|  | ||||
| svg.logo { | ||||
|   .leaf { | ||||
|     fill: var(--bs-primary) !important; | ||||
| @@ -244,8 +248,44 @@ table.table { | ||||
|   color: var(--bs-body-color); | ||||
| } | ||||
|  | ||||
| .ngx-file-drop__drop-zone--over { | ||||
|   background-color: var(--ngx-primary-faded) !important; | ||||
| .main-dropzone { | ||||
|   height: 100%; | ||||
|   width: 100%; | ||||
|  | ||||
|   &.ngx-file-drop__drop-zone--over { | ||||
|     background-color: transparent !important; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .global-dropzone-overlay { | ||||
|   position: absolute; | ||||
|   top: 0; | ||||
|   right: 0; | ||||
|   bottom: 0; | ||||
|   left: 0; | ||||
|   background-color: rgba(23, 84, 31, .8); | ||||
|   z-index: 1055; // $zindex-modal | ||||
|   pointer-events: none !important; | ||||
|   user-select: none !important; | ||||
|   text-align: center; | ||||
|   padding-top: 25%; | ||||
|  | ||||
|   &.show { | ||||
|     opacity: 1 !important; | ||||
|   } | ||||
|  | ||||
|   &.hide { | ||||
|     display: none; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .ngx-file-drop__drop-zone--over .global-dropzone-overlay { | ||||
|   opacity: 0; | ||||
| } | ||||
|  | ||||
| .inert { | ||||
|   pointer-events: none !important; | ||||
|   user-select: none !important; | ||||
| } | ||||
|  | ||||
| .alert-danger { | ||||
|   | ||||
| @@ -141,11 +141,11 @@ $border-color-dark-mode: #47494f; | ||||
|     color: $text-color-dark-mode-accent; | ||||
|   } | ||||
|  | ||||
|   .close, .modal .btn-close { | ||||
|   .close, .modal .btn-close, .alert .btn-close { | ||||
|     text-shadow: 0 1px 0 #666; | ||||
|   } | ||||
|  | ||||
|   .modal .btn-close { | ||||
|   .modal .btn-close, .alert .btn-close { | ||||
|     filter: invert(1) grayscale(100%) brightness(200%); | ||||
|   } | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 shamoon
					shamoon