diff --git a/CHANGELOG.md b/CHANGELOG.md index d2424f991..b96fb62f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added support to drop a file in the import activities dialog + ### Changed - Harmonized the logger output: () diff --git a/apps/client/src/app/directives/file-drop/file-drop.directive.ts b/apps/client/src/app/directives/file-drop/file-drop.directive.ts new file mode 100644 index 000000000..65f3ed23a --- /dev/null +++ b/apps/client/src/app/directives/file-drop/file-drop.directive.ts @@ -0,0 +1,28 @@ +import { Directive, HostListener, Output, EventEmitter } from '@angular/core'; + +@Directive({ + selector: '[gfFileDrop]' +}) +export class FileDropDirective { + @Output() filesDropped = new EventEmitter(); + + @HostListener('dragenter', ['$event']) onDragEnter(event: DragEvent) { + event.preventDefault(); + event.stopPropagation(); + } + + @HostListener('dragover', ['$event']) onDragOver(event: DragEvent) { + event.preventDefault(); + event.stopPropagation(); + } + + @HostListener('drop', ['$event']) onDrop(event: DragEvent) { + event.preventDefault(); + event.stopPropagation(); + + // Prevent the browser's default behavior for handling the file drop + event.dataTransfer.dropEffect = 'copy'; + + this.filesDropped.emit(event.dataTransfer.files); + } +} diff --git a/apps/client/src/app/directives/file-drop/file-drop.module.ts b/apps/client/src/app/directives/file-drop/file-drop.module.ts new file mode 100644 index 000000000..a0148516e --- /dev/null +++ b/apps/client/src/app/directives/file-drop/file-drop.module.ts @@ -0,0 +1,9 @@ +import { NgModule } from '@angular/core'; + +import { FileDropDirective } from './file-drop.directive'; + +@NgModule({ + declarations: [FileDropDirective], + exports: [FileDropDirective] +}) +export class GfFileDropModule {} diff --git a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts index 46a307318..d11cafdb9 100644 --- a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts @@ -137,6 +137,20 @@ export class ImportActivitiesDialog implements OnDestroy { } } + public onFilesDropped({ + files, + stepper + }: { + files: FileList; + stepper: MatStepper; + }): void { + if (files.length === 0) { + return; + } + + this.handleFile({ stepper, file: files[0] }); + } + public onImportStepChange(event: StepperSelectionEvent) { if (event.selectedIndex === ImportStep.UPLOAD_FILE) { this.importStep = ImportStep.UPLOAD_FILE; @@ -175,111 +189,120 @@ export class ImportActivitiesDialog implements OnDestroy { aStepper.reset(); } - public onSelectFile(aStepper: MatStepper) { + public onSelectFile(stepper: MatStepper) { const input = document.createElement('input'); input.accept = 'application/JSON, .csv'; input.type = 'file'; input.onchange = (event) => { - this.snackBar.open('⏳ ' + $localize`Validating data...`); - // Getting the file reference const file = (event.target as HTMLInputElement).files[0]; + this.handleFile({ file, stepper }); + }; - // Setting up the reader - const reader = new FileReader(); - reader.readAsText(file, 'UTF-8'); + input.click(); + } + + public updateSelection(activities: Activity[]) { + this.selectedActivities = activities.filter(({ error }) => { + return !error; + }); + } - reader.onload = async (readerEvent) => { - const fileContent = readerEvent.target.result as string; + public ngOnDestroy() { + this.unsubscribeSubject.next(); + this.unsubscribeSubject.complete(); + } - try { - if (file.name.endsWith('.json')) { - const content = JSON.parse(fileContent); + private async handleFile({ + file, + stepper + }: { + file: File; + stepper: MatStepper; + }): Promise { + this.snackBar.open('⏳ ' + $localize`Validating data...`); - this.accounts = content.accounts; + // Setting up the reader + const reader = new FileReader(); + reader.readAsText(file, 'UTF-8'); - if (!isArray(content.activities)) { - if (isArray(content.orders)) { - this.handleImportError({ - activities: [], - error: { - error: { - message: [`orders needs to be renamed to activities`] - } - } - }); - return; - } else { - throw new Error(); - } - } + reader.onload = async (readerEvent) => { + const fileContent = readerEvent.target.result as string; - try { - const { activities } = - await this.importActivitiesService.importJson({ - accounts: content.accounts, - activities: content.activities, - isDryRun: true - }); - this.activities = activities; - } catch (error) { - console.error(error); - this.handleImportError({ error, activities: content.activities }); - } + try { + if (file.name.endsWith('.json')) { + const content = JSON.parse(fileContent); - return; - } else if (file.name.endsWith('.csv')) { - try { - const data = await this.importActivitiesService.importCsv({ - fileContent, - isDryRun: true, - userAccounts: this.data.user.accounts - }); - this.activities = data.activities; - } catch (error) { - console.error(error); + this.accounts = content.accounts; + + if (!isArray(content.activities)) { + if (isArray(content.orders)) { this.handleImportError({ - activities: error?.activities ?? [], + activities: [], error: { - error: { message: error?.error?.message ?? [error?.message] } + error: { + message: [`orders needs to be renamed to activities`] + } } }); + return; + } else { + throw new Error(); } - - return; } - throw new Error(); - } catch (error) { - console.error(error); - this.handleImportError({ - activities: [], - error: { error: { message: ['Unexpected format'] } } - }); - } finally { - this.importStep = ImportStep.SELECT_ACTIVITIES; - this.snackBar.dismiss(); + try { + const { activities } = + await this.importActivitiesService.importJson({ + accounts: content.accounts, + activities: content.activities, + isDryRun: true + }); + this.activities = activities; + } catch (error) { + console.error(error); + this.handleImportError({ error, activities: content.activities }); + } - aStepper.next(); + return; + } else if (file.name.endsWith('.csv')) { + try { + const data = await this.importActivitiesService.importCsv({ + fileContent, + isDryRun: true, + userAccounts: this.data.user.accounts + }); + this.activities = data.activities; + } catch (error) { + console.error(error); + this.handleImportError({ + activities: error?.activities ?? [], + error: { + error: { message: error?.error?.message ?? [error?.message] } + } + }); + } - this.changeDetectorRef.markForCheck(); + return; } - }; - }; - input.click(); - } + throw new Error(); + } catch (error) { + console.error(error); + this.handleImportError({ + activities: [], + error: { error: { message: ['Unexpected format'] } } + }); + } finally { + this.importStep = ImportStep.SELECT_ACTIVITIES; + this.snackBar.dismiss(); - public updateSelection(activities: Activity[]) { - this.selectedActivities = activities.filter(({ error }) => { - return !error; - }); - } + stepper.next(); - public ngOnDestroy() { - this.unsubscribeSubject.next(); - this.unsubscribeSubject.complete(); + this.changeDetectorRef.markForCheck(); + } + }; } private handleImportError({ diff --git a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html index 02071c137..06cadad8e 100644 --- a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html +++ b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html @@ -70,29 +70,38 @@
-

- The following file formats are supported: - CSV - or - JSON + + Choose or drop a file here +

+ +

+ + The following file formats are supported: + CSV + or + JSON +

@@ -109,7 +118,7 @@ >
- +