Introduced stepper to import activities (#1990)

* Introduced stepper to import activities

* Update changelog
pull/2014/head
Visrut 2 years ago committed by GitHub
parent b24ddc30c9
commit 16a5ace4be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Added a stepper to the activities import dialog
- Added a link to manage the benchmarks to the benchmark comparator - Added a link to manage the benchmarks to the benchmark comparator
- Added support for localized routes - Added support for localized routes

@ -0,0 +1,4 @@
export enum ImportStep {
UPLOAD_FILE = 0,
SELECT_ACTIVITIES = 1
}

@ -1,3 +1,7 @@
import {
StepperOrientation,
StepperSelectionEvent
} from '@angular/cdk/stepper';
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
@ -8,6 +12,7 @@ import {
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { MatStepper } from '@angular/material/stepper';
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto'; import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
@ -15,8 +20,10 @@ import { ImportActivitiesService } from '@ghostfolio/client/services/import-acti
import { Position } from '@ghostfolio/common/interfaces'; import { Position } from '@ghostfolio/common/interfaces';
import { AssetClass } from '@prisma/client'; import { AssetClass } from '@prisma/client';
import { isArray, sortBy } from 'lodash'; import { isArray, sortBy } from 'lodash';
import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject, takeUntil } from 'rxjs'; import { Subject, takeUntil } from 'rxjs';
import { ImportStep } from './enums/import-step';
import { ImportActivitiesDialogParams } from './interfaces/interfaces'; import { ImportActivitiesDialogParams } from './interfaces/interfaces';
@Component({ @Component({
@ -29,12 +36,14 @@ export class ImportActivitiesDialog implements OnDestroy {
public accounts: CreateAccountDto[] = []; public accounts: CreateAccountDto[] = [];
public activities: Activity[] = []; public activities: Activity[] = [];
public details: any[] = []; public details: any[] = [];
public deviceType: string;
public errorMessages: string[] = []; public errorMessages: string[] = [];
public holdings: Position[] = []; public holdings: Position[] = [];
public isFileSelected = false; public importStep: ImportStep = ImportStep.UPLOAD_FILE;
public maxSafeInteger = Number.MAX_SAFE_INTEGER; public maxSafeInteger = Number.MAX_SAFE_INTEGER;
public mode: 'DIVIDEND'; public mode: 'DIVIDEND';
public selectedActivities: Activity[] = []; public selectedActivities: Activity[] = [];
public stepperOrientation: StepperOrientation;
public uniqueAssetForm: FormGroup; public uniqueAssetForm: FormGroup;
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
@ -43,6 +52,7 @@ export class ImportActivitiesDialog implements OnDestroy {
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
@Inject(MAT_DIALOG_DATA) public data: ImportActivitiesDialogParams, @Inject(MAT_DIALOG_DATA) public data: ImportActivitiesDialogParams,
private dataService: DataService, private dataService: DataService,
private deviceService: DeviceDetectorService,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
public dialogRef: MatDialogRef<ImportActivitiesDialog>, public dialogRef: MatDialogRef<ImportActivitiesDialog>,
private importActivitiesService: ImportActivitiesService, private importActivitiesService: ImportActivitiesService,
@ -50,6 +60,10 @@ export class ImportActivitiesDialog implements OnDestroy {
) {} ) {}
public ngOnInit() { public ngOnInit() {
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
this.stepperOrientation =
this.deviceType === 'mobile' ? 'vertical' : 'horizontal';
this.uniqueAssetForm = this.formBuilder.group({ this.uniqueAssetForm = this.formBuilder.group({
uniqueAsset: [undefined, Validators.required] uniqueAsset: [undefined, Validators.required]
}); });
@ -116,7 +130,15 @@ export class ImportActivitiesDialog implements OnDestroy {
} }
} }
public onLoadDividends() { public onImportStepChange(event: StepperSelectionEvent) {
if (event.selectedIndex === ImportStep.UPLOAD_FILE) {
this.importStep = ImportStep.UPLOAD_FILE;
} else if (event.selectedIndex === ImportStep.SELECT_ACTIVITIES) {
this.importStep = ImportStep.SELECT_ACTIVITIES;
}
}
public onLoadDividends(aStepper: MatStepper) {
this.uniqueAssetForm.controls['uniqueAsset'].disable(); this.uniqueAssetForm.controls['uniqueAsset'].disable();
const { dataSource, symbol } = const { dataSource, symbol } =
@ -130,19 +152,23 @@ export class ImportActivitiesDialog implements OnDestroy {
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ activities }) => { .subscribe(({ activities }) => {
this.activities = activities; this.activities = activities;
this.isFileSelected = true;
aStepper.next();
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
} }
public onReset() { public onReset(aStepper: MatStepper) {
this.details = []; this.details = [];
this.errorMessages = []; this.errorMessages = [];
this.isFileSelected = false; this.importStep = ImportStep.SELECT_ACTIVITIES;
this.uniqueAssetForm.controls['uniqueAsset'].enable();
aStepper.reset();
} }
public onSelectFile() { public onSelectFile(aStepper: MatStepper) {
const input = document.createElement('input'); const input = document.createElement('input');
input.accept = 'application/JSON, .csv'; input.accept = 'application/JSON, .csv';
input.type = 'file'; input.type = 'file';
@ -225,8 +251,11 @@ export class ImportActivitiesDialog implements OnDestroy {
error: { error: { message: ['Unexpected format'] } } error: { error: { message: ['Unexpected format'] } }
}); });
} finally { } finally {
this.isFileSelected = true; this.importStep = ImportStep.SELECT_ACTIVITIES;
this.snackBar.dismiss(); this.snackBar.dismiss();
aStepper.next();
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
} }
}; };

@ -5,123 +5,152 @@
(closeButtonClicked)="onCancel()" (closeButtonClicked)="onCancel()"
></gf-dialog-header> ></gf-dialog-header>
<div class="flex-grow-1 py-3" mat-dialog-content> <div class="flex-grow-1" mat-dialog-content>
<ng-container *ngIf="!isFileSelected"> <mat-stepper
<ng-container *ngIf="mode === 'DIVIDEND'; else selectFile"> #stepper
<form [formGroup]="uniqueAssetForm" (ngSubmit)="onLoadDividends()"> [animationDuration]="0"
<mat-form-field appearance="outline" class="w-100"> [linear]="true"
<mat-label i18n>Holding</mat-label> [orientation]="stepperOrientation"
<mat-select formControlName="uniqueAsset"> [selectedIndex]="importStep"
<mat-option (selectionChange)="onImportStepChange($event)"
*ngFor="let holding of holdings" >
[value]="{dataSource: holding.dataSource, symbol: holding.symbol}" <mat-step [completed]="importStep === 0" [selected]="importStep === 0">
>{{ holding.name }}</mat-option <ng-template i18n matStepLabel>Select File</ng-template>
> <div class="pt-3">
</mat-select> <ng-container *ngIf="mode === 'DIVIDEND'; else selectFile">
</mat-form-field> <form
<div class="d-flex justify-content-center flex-column"> [formGroup]="uniqueAssetForm"
<button (ngSubmit)="onLoadDividends(stepper)"
color="primary"
mat-flat-button
type="submit"
[disabled]="!uniqueAssetForm.valid"
>
<span i18n>Load Dividends</span>
</button>
</div>
</form>
</ng-container>
<ng-template #selectFile>
<div class="d-flex justify-content-center flex-column">
<button
class="py-4"
color="primary"
mat-stroked-button
(click)="onSelectFile()"
>
<ion-icon class="mr-2" name="cloud-upload-outline"></ion-icon>
<span i18n>Choose File</span>
</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> <mat-form-field appearance="outline" class="w-100">
</div> <mat-label i18n>Holding</mat-label>
</ng-template> <mat-select formControlName="uniqueAsset">
</ng-container> <mat-option
<ng-container *ngIf="isFileSelected"> *ngFor="let holding of holdings"
<ng-container *ngIf="errorMessages.length === 0; else errorMessage"> [value]="{dataSource: holding.dataSource, symbol: holding.symbol}"
<gf-activities-table >{{ holding.name }}</mat-option
[activities]="activities" >
[baseCurrency]="data?.user?.settings?.baseCurrency" </mat-select>
[deviceType]="data?.deviceType" </mat-form-field>
[hasPermissionToCreateActivity]="false" <div class="d-flex flex-column justify-content-center">
[hasPermissionToExportActivities]="false" <button
[hasPermissionToFilter]="false" color="primary"
[hasPermissionToOpenDetails]="false" mat-flat-button
[locale]="data?.user?.settings?.locale" type="submit"
[pageSize]="maxSafeInteger" [disabled]="!uniqueAssetForm.valid"
[showActions]="false" >
[showCheckbox]="true" <span i18n>Load Dividends</span>
[showFooter]="false" </button>
[showSymbolColumn]="false" </div>
(selectedActivities)="updateSelection($event)" </form>
></gf-activities-table> </ng-container>
</ng-container> <ng-template #selectFile>
<ng-template #errorMessage> <div class="d-flex flex-column justify-content-center">
<mat-accordion displayMode="flat"> <button
<mat-expansion-panel class="py-4"
*ngFor="let message of errorMessages; let i = index" color="primary"
[disabled]="!details[i]" mat-stroked-button
> (click)="onSelectFile(stepper)"
<mat-expansion-panel-header class="pl-1"> >
<mat-panel-title> <ion-icon class="mr-2" name="cloud-upload-outline"></ion-icon>
<div class="d-flex"> <span i18n>Choose File</span>
<div class="align-items-center d-flex mr-2"> </button>
<ion-icon name="warning-outline"></ion-icon> <p class="mb-0 mt-4 text-center">
</div> <span class="mr-1" i18n
<div>{{ message }}</div> >The following file formats are supported:</span
</div> >
</mat-panel-title> <a
</mat-expansion-panel-header> href="https://github.com/ghostfolio/ghostfolio/blob/main/test/import/ok.csv"
<pre target="_blank"
*ngIf="details[i]" >CSV</a
class="m-0" >
><code>{{ details[i] | json }}</code></pre> <span class="mx-1" i18n>or</span>
</mat-expansion-panel> <a
</mat-accordion> href="https://github.com/ghostfolio/ghostfolio/blob/main/test/import/ok.json"
<div class="mt-2"> target="_blank"
<button mat-button (click)="onReset()"> >JSON</a
<ion-icon class="mr-2" name="arrow-back-outline"></ion-icon> >
<span i18n>Back</span> </p>
</button> </div>
</ng-template>
</div> </div>
</ng-template> </mat-step>
</ng-container>
</div>
<div *ngIf="isFileSelected" class="justify-content-end" mat-dialog-actions> <mat-step [completed]="importStep === 1" [selected]="importStep === 1">
<button i18n mat-button (click)="onCancel()">Cancel</button> <ng-template i18n matStepLabel>Select Activities</ng-template>
<button <div class="pt-3">
color="primary" <ng-container *ngIf="errorMessages.length === 0; else errorMessage">
mat-flat-button <gf-activities-table
[disabled]="!selectedActivities?.length" *ngIf="importStep === 1"
(click)="onImportActivities()" [activities]="activities"
> [baseCurrency]="data?.user?.settings?.baseCurrency"
<ng-container i18n>Import</ng-container> [deviceType]="data?.deviceType"
</button> [hasPermissionToCreateActivity]="false"
[hasPermissionToExportActivities]="false"
[hasPermissionToFilter]="false"
[hasPermissionToOpenDetails]="false"
[locale]="data?.user?.settings?.locale"
[pageSize]="maxSafeInteger"
[showActions]="false"
[showCheckbox]="true"
[showFooter]="false"
[showSymbolColumn]="false"
(selectedActivities)="updateSelection($event)"
></gf-activities-table>
<div class="d-flex justify-content-end mt-3">
<button mat-button (click)="onReset(stepper)">
<ng-container i18n>Back</ng-container>
</button>
<button
class="ml-1"
color="primary"
mat-flat-button
[disabled]="!selectedActivities?.length"
(click)="onImportActivities()"
>
<ng-container i18n>Import</ng-container>
</button>
</div>
</ng-container>
<ng-template #errorMessage>
<mat-accordion displayMode="flat">
<mat-expansion-panel
*ngFor="let message of errorMessages; let i = index"
[disabled]="!details[i]"
>
<mat-expansion-panel-header class="pl-1">
<mat-panel-title>
<div class="d-flex">
<div class="align-items-center d-flex mr-2">
<ion-icon name="warning-outline"></ion-icon>
</div>
<div>{{ message }}</div>
</div>
</mat-panel-title>
</mat-expansion-panel-header>
<pre
*ngIf="details[i]"
class="m-0"
><code>{{ details[i] | json }}</code></pre>
</mat-expansion-panel>
</mat-accordion>
<div class="d-flex justify-content-end mt-3">
<button mat-button (click)="onReset(stepper)">
<ng-container i18n>Back</ng-container>
</button>
<button
class="ml-1"
color="primary"
mat-flat-button
[disabled]="true"
>
<ng-container i18n>Import</ng-container>
</button>
</div>
</ng-template>
</div>
</mat-step>
</mat-stepper>
</div> </div>
<gf-dialog-footer <gf-dialog-footer

@ -6,6 +6,7 @@ import { MatDialogModule } from '@angular/material/dialog';
import { MatExpansionModule } from '@angular/material/expansion'; import { MatExpansionModule } from '@angular/material/expansion';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { MatStepperModule } from '@angular/material/stepper';
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module'; import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module'; import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module'; import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
@ -25,6 +26,7 @@ import { ImportActivitiesDialog } from './import-activities-dialog.component';
MatExpansionModule, MatExpansionModule,
MatFormFieldModule, MatFormFieldModule,
MatSelectModule, MatSelectModule,
MatStepperModule,
ReactiveFormsModule ReactiveFormsModule
], ],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]

@ -5,6 +5,16 @@
color: rgba(var(--palette-primary-500), 1); color: rgba(var(--palette-primary-500), 1);
} }
mat-stepper {
::ng-deep {
.mat-step-header {
&[aria-selected='false'] {
pointer-events: none;
}
}
}
}
.mat-expansion-panel { .mat-expansion-panel {
background: none; background: none;
box-shadow: none; box-shadow: none;

Loading…
Cancel
Save