Add drop file functionality on import (#2323)
* Add drop file functionality on import * Update changelog --------- Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
This commit is contained in:
parent
41437636b1
commit
369386f976
@ -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: <symbol> (<dataSource>)
|
||||
|
@ -0,0 +1,28 @@
|
||||
import { Directive, HostListener, Output, EventEmitter } from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: '[gfFileDrop]'
|
||||
})
|
||||
export class FileDropDirective {
|
||||
@Output() filesDropped = new EventEmitter<FileList>();
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { FileDropDirective } from './file-drop.directive';
|
||||
|
||||
@NgModule({
|
||||
declarations: [FileDropDirective],
|
||||
exports: [FileDropDirective]
|
||||
})
|
||||
export class GfFileDropModule {}
|
@ -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,97 +189,15 @@ 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];
|
||||
|
||||
// Setting up the reader
|
||||
const reader = new FileReader();
|
||||
reader.readAsText(file, 'UTF-8');
|
||||
|
||||
reader.onload = async (readerEvent) => {
|
||||
const fileContent = readerEvent.target.result as string;
|
||||
|
||||
try {
|
||||
if (file.name.endsWith('.json')) {
|
||||
const content = JSON.parse(fileContent);
|
||||
|
||||
this.accounts = content.accounts;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
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] }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
aStepper.next();
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
};
|
||||
this.handleFile({ file, stepper });
|
||||
};
|
||||
|
||||
input.click();
|
||||
@ -282,6 +214,97 @@ export class ImportActivitiesDialog implements OnDestroy {
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
|
||||
private async handleFile({
|
||||
file,
|
||||
stepper
|
||||
}: {
|
||||
file: File;
|
||||
stepper: MatStepper;
|
||||
}): Promise<void> {
|
||||
this.snackBar.open('⏳ ' + $localize`Validating data...`);
|
||||
|
||||
// Setting up the reader
|
||||
const reader = new FileReader();
|
||||
reader.readAsText(file, 'UTF-8');
|
||||
|
||||
reader.onload = async (readerEvent) => {
|
||||
const fileContent = readerEvent.target.result as string;
|
||||
|
||||
try {
|
||||
if (file.name.endsWith('.json')) {
|
||||
const content = JSON.parse(fileContent);
|
||||
|
||||
this.accounts = content.accounts;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
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] }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
stepper.next();
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private handleImportError({
|
||||
activities,
|
||||
error
|
||||
|
@ -70,29 +70,38 @@
|
||||
<ng-template #selectFile>
|
||||
<div class="d-flex flex-column justify-content-center">
|
||||
<button
|
||||
class="py-4"
|
||||
color="primary"
|
||||
mat-stroked-button
|
||||
class="drop-area p-4 text-center text-muted"
|
||||
gfFileDrop
|
||||
(click)="onSelectFile(stepper)"
|
||||
(filesDropped)="onFilesDropped({stepper, files: $event})"
|
||||
>
|
||||
<ion-icon class="mr-2" name="cloud-upload-outline"></ion-icon>
|
||||
<span i18n>Choose File</span>
|
||||
<div
|
||||
class="align-items-center d-flex flex-column justify-content-center"
|
||||
>
|
||||
<ion-icon
|
||||
class="cloud-icon"
|
||||
name="cloud-upload-outline"
|
||||
></ion-icon>
|
||||
<span i18n>Choose or drop a file here</span>
|
||||
</div>
|
||||
</button>
|
||||
<p class="mb-0 mt-4 text-center">
|
||||
<span class="mr-1" i18n
|
||||
>The following file formats are supported:</span
|
||||
>
|
||||
<a
|
||||
href="https://github.com/ghostfolio/ghostfolio/blob/main/test/import/ok.csv"
|
||||
target="_blank"
|
||||
>CSV</a
|
||||
>
|
||||
<span class="mx-1" i18n>or</span>
|
||||
<a
|
||||
href="https://github.com/ghostfolio/ghostfolio/blob/main/test/import/ok.json"
|
||||
target="_blank"
|
||||
>JSON</a
|
||||
>
|
||||
<p class="mb-0 mt-3 text-center">
|
||||
<small>
|
||||
<span class="mr-1" i18n
|
||||
>The following file formats are supported:</span
|
||||
>
|
||||
<a
|
||||
href="https://github.com/ghostfolio/ghostfolio/blob/main/test/import/ok.csv"
|
||||
target="_blank"
|
||||
>CSV</a
|
||||
>
|
||||
<span class="mx-1" i18n>or</span>
|
||||
<a
|
||||
href="https://github.com/ghostfolio/ghostfolio/blob/main/test/import/ok.json"
|
||||
target="_blank"
|
||||
>JSON</a
|
||||
>
|
||||
</small>
|
||||
</p>
|
||||
</div>
|
||||
</ng-template>
|
||||
@ -109,7 +118,7 @@
|
||||
>
|
||||
</ng-template>
|
||||
<div class="pt-3">
|
||||
<ng-container *ngIf="errorMessages.length === 0; else errorMessage">
|
||||
<ng-container *ngIf="errorMessages?.length === 0; else errorMessage">
|
||||
<gf-activities-table
|
||||
*ngIf="importStep === 1"
|
||||
[activities]="activities"
|
||||
|
@ -10,6 +10,7 @@ import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatStepperModule } from '@angular/material/stepper';
|
||||
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
|
||||
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
|
||||
import { GfFileDropModule } from '@ghostfolio/client/directives/file-drop/file-drop.module';
|
||||
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
||||
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
|
||||
|
||||
@ -23,6 +24,7 @@ import { ImportActivitiesDialog } from './import-activities-dialog.component';
|
||||
GfActivitiesTableModule,
|
||||
GfDialogFooterModule,
|
||||
GfDialogHeaderModule,
|
||||
GfFileDropModule,
|
||||
GfSymbolModule,
|
||||
MatButtonModule,
|
||||
MatDialogModule,
|
||||
|
@ -32,4 +32,32 @@
|
||||
right: 1.5rem;
|
||||
top: calc(50% - 10px);
|
||||
}
|
||||
|
||||
.drop-area {
|
||||
background-color: rgba(var(--palette-foreground-base), 0.02);
|
||||
border: 1px dashed
|
||||
rgba(
|
||||
var(--palette-foreground-divider),
|
||||
var(--palette-foreground-divider-alpha)
|
||||
);
|
||||
border-radius: 0.25rem;
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(var(--palette-primary-500), 1) !important;
|
||||
color: rgba(var(--palette-primary-500), 1);
|
||||
}
|
||||
|
||||
.cloud-icon {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.is-dark-theme) {
|
||||
.drop-area {
|
||||
border-color: rgba(
|
||||
var(--palette-foreground-divider-dark),
|
||||
var(--palette-foreground-divider-alpha-dark)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -430,6 +430,11 @@ ngx-skeleton-loader {
|
||||
}
|
||||
}
|
||||
|
||||
.mat-stepper-vertical,
|
||||
.mat-stepper-horizontal {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.mdc-button {
|
||||
&.mat-accent,
|
||||
&.mat-primary {
|
||||
|
Loading…
x
Reference in New Issue
Block a user