Browse Source

Feature/extend import validation message (#421)

* Extend import validation message

* Update changelog
pull/422/head
Thomas Kaul 3 years ago
committed by GitHub
parent
commit
b57301ef50
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      CHANGELOG.md
  2. 4
      apps/api/src/app/import/import.service.ts
  3. 16
      apps/client/src/app/pages/portfolio/transactions/import-transaction-dialog/import-transaction-dialog.component.ts
  4. 23
      apps/client/src/app/pages/portfolio/transactions/import-transaction-dialog/import-transaction-dialog.html
  5. 4
      apps/client/src/app/pages/portfolio/transactions/import-transaction-dialog/import-transaction-dialog.module.ts
  6. 9
      apps/client/src/app/pages/portfolio/transactions/import-transaction-dialog/import-transaction-dialog.scss
  7. 1
      apps/client/src/app/pages/portfolio/transactions/import-transaction-dialog/interfaces/interfaces.ts
  8. 15
      apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts
  9. 117
      apps/client/src/app/services/import-transactions.service.ts
  10. 1
      libs/common/src/lib/interfaces/info-item.interface.ts

6
CHANGELOG.md

@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Added
- Extended the validation message of the import functionality for transactions
## 1.61.0 - 15.10.2021 ## 1.61.0 - 15.10.2021
### Added ### Added

4
apps/api/src/app/import/import.service.ts

@ -94,7 +94,9 @@ export class ImportService {
]); ]);
if (result[symbol] === undefined) { if (result[symbol] === undefined) {
throw new Error(`${symbol} is not a valid symbol for ${dataSource}`); throw new Error(
`orders.${index}.symbol ("${symbol}") is not valid for the specified data source ("${dataSource}")`
);
} }
} }
} }

16
apps/client/src/app/pages/portfolio/transactions/import-transaction-dialog/import-transaction-dialog.component.ts

@ -16,6 +16,8 @@ import { ImportTransactionDialogParams } from './interfaces/interfaces';
templateUrl: 'import-transaction-dialog.html' templateUrl: 'import-transaction-dialog.html'
}) })
export class ImportTransactionDialog implements OnDestroy { export class ImportTransactionDialog implements OnDestroy {
public details: any[] = [];
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
@ -23,7 +25,19 @@ export class ImportTransactionDialog implements OnDestroy {
public dialogRef: MatDialogRef<ImportTransactionDialog> public dialogRef: MatDialogRef<ImportTransactionDialog>
) {} ) {}
public ngOnInit() {} public ngOnInit() {
for (const message of this.data.messages) {
if (message.includes('orders.')) {
let [index] = message.split(' ');
index = index.replace('orders.', '');
[index] = index.split('.');
this.details.push(this.data.orders[index]);
} else {
this.details.push('');
}
}
}
public onCancel(): void { public onCancel(): void {
this.dialogRef.close(); this.dialogRef.close();

23
apps/client/src/app/pages/portfolio/transactions/import-transaction-dialog/import-transaction-dialog.html

@ -6,14 +6,27 @@
></gf-dialog-header> ></gf-dialog-header>
<div class="flex-grow-1" mat-dialog-content> <div class="flex-grow-1" mat-dialog-content>
<ul class="list-unstyled"> <mat-accordion displayMode="flat">
<li *ngFor="let message of data.messages" class="d-flex"> <mat-expansion-panel
<div class="align-items-center d-flex px-2"> *ngFor="let message of data.messages; 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> <ion-icon name="warning-outline"></ion-icon>
</div> </div>
<div>{{ message }}</div> <div>{{ message }}</div>
</li> </div>
</ul> </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> </div>
<gf-dialog-footer <gf-dialog-footer

4
apps/client/src/app/pages/portfolio/transactions/import-transaction-dialog/import-transaction-dialog.module.ts

@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog'; import { MatDialogModule } from '@angular/material/dialog';
import { MatExpansionModule } from '@angular/material/expansion';
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';
@ -15,7 +16,8 @@ import { ImportTransactionDialog } from './import-transaction-dialog.component';
GfDialogFooterModule, GfDialogFooterModule,
GfDialogHeaderModule, GfDialogHeaderModule,
MatButtonModule, MatButtonModule,
MatDialogModule MatDialogModule,
MatExpansionModule
], ],
providers: [], providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]

9
apps/client/src/app/pages/portfolio/transactions/import-transaction-dialog/import-transaction-dialog.scss

@ -1,3 +1,12 @@
:host { :host {
display: block; display: block;
.mat-expansion-panel {
background: none;
box-shadow: none;
.mat-expansion-panel-header {
color: inherit;
}
}
} }

1
apps/client/src/app/pages/portfolio/transactions/import-transaction-dialog/interfaces/interfaces.ts

@ -1,4 +1,5 @@
export interface ImportTransactionDialogParams { export interface ImportTransactionDialogParams {
deviceType: string; deviceType: string;
messages: string[]; messages: string[];
orders: any[];
} }

15
apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts

@ -197,7 +197,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
this.handleImportSuccess(); this.handleImportSuccess();
} catch (error) { } catch (error) {
this.handleImportError(error); this.handleImportError({ error, orders: content.orders });
} }
return; return;
@ -212,7 +212,10 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
this.handleImportSuccess(); this.handleImportSuccess();
} catch (error) { } catch (error) {
this.handleImportError({ this.handleImportError({
error: {
error: { message: error?.error?.message ?? [error?.message] } error: { message: error?.error?.message ?? [error?.message] }
},
orders: error?.orders ?? []
}); });
} }
@ -221,7 +224,10 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
throw new Error(); throw new Error();
} catch (error) { } catch (error) {
this.handleImportError({ error: { message: ['Unexpected format'] } }); this.handleImportError({
error: { error: { message: ['Unexpected format'] } },
orders: []
});
} }
}; };
}; };
@ -307,13 +313,14 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
a.click(); a.click();
} }
private handleImportError(aError: any) { private handleImportError({ error, orders }: { error: any; orders: any[] }) {
this.snackBar.dismiss(); this.snackBar.dismiss();
this.dialog.open(ImportTransactionDialog, { this.dialog.open(ImportTransactionDialog, {
data: { data: {
orders,
deviceType: this.deviceType, deviceType: this.deviceType,
messages: aError?.error?.message messages: error?.error?.message
}, },
width: this.deviceType === 'mobile' ? '100vw' : '50rem' width: this.deviceType === 'mobile' ? '100vw' : '50rem'
}); });

117
apps/client/src/app/services/import-transactions.service.ts

@ -39,17 +39,17 @@ export class ImportTransactionsService {
const orders: CreateOrderDto[] = []; const orders: CreateOrderDto[] = [];
for (const item of content) { for (const [index, item] of content.entries()) {
orders.push({ orders.push({
accountId: defaultAccountId, accountId: defaultAccountId,
currency: this.parseCurrency(item), currency: this.parseCurrency({ content, index, item }),
dataSource: primaryDataSource, dataSource: primaryDataSource,
date: this.parseDate(item), date: this.parseDate({ content, index, item }),
fee: this.parseFee(item), fee: this.parseFee({ content, index, item }),
quantity: this.parseQuantity(item), quantity: this.parseQuantity({ content, index, item }),
symbol: this.parseSymbol(item), symbol: this.parseSymbol({ content, index, item }),
type: this.parseType(item), type: this.parseType({ content, index, item }),
unitPrice: this.parseUnitPrice(item) unitPrice: this.parseUnitPrice({ content, index, item })
}); });
} }
@ -90,8 +90,16 @@ export class ImportTransactionsService {
}, {}); }, {});
} }
private parseCurrency(aItem: any) { private parseCurrency({
const item = this.lowercaseKeys(aItem); content,
index,
item
}: {
content: any[];
index: number;
item: any;
}) {
item = this.lowercaseKeys(item);
for (const key of ImportTransactionsService.CURRENCY_KEYS) { for (const key of ImportTransactionsService.CURRENCY_KEYS) {
if (item[key]) { if (item[key]) {
@ -99,11 +107,19 @@ export class ImportTransactionsService {
} }
} }
throw new Error('Could not parse currency'); throw { message: `orders.${index}.currency is not valid`, orders: content };
} }
private parseDate(aItem: any) { private parseDate({
const item = this.lowercaseKeys(aItem); content,
index,
item
}: {
content: any[];
index: number;
item: any;
}) {
item = this.lowercaseKeys(item);
let date: string; let date: string;
for (const key of ImportTransactionsService.DATE_KEYS) { for (const key of ImportTransactionsService.DATE_KEYS) {
@ -122,11 +138,19 @@ export class ImportTransactionsService {
} }
} }
throw new Error('Could not parse date'); throw { message: `orders.${index}.date is not valid`, orders: content };
} }
private parseFee(aItem: any) { private parseFee({
const item = this.lowercaseKeys(aItem); content,
index,
item
}: {
content: any[];
index: number;
item: any;
}) {
item = this.lowercaseKeys(item);
for (const key of ImportTransactionsService.FEE_KEYS) { for (const key of ImportTransactionsService.FEE_KEYS) {
if ((item[key] || item[key] === 0) && isNumber(item[key])) { if ((item[key] || item[key] === 0) && isNumber(item[key])) {
@ -134,11 +158,19 @@ export class ImportTransactionsService {
} }
} }
throw new Error('Could not parse fee'); throw { message: `orders.${index}.fee is not valid`, orders: content };
} }
private parseQuantity(aItem: any) { private parseQuantity({
const item = this.lowercaseKeys(aItem); content,
index,
item
}: {
content: any[];
index: number;
item: any;
}) {
item = this.lowercaseKeys(item);
for (const key of ImportTransactionsService.QUANTITY_KEYS) { for (const key of ImportTransactionsService.QUANTITY_KEYS) {
if (item[key] && isNumber(item[key])) { if (item[key] && isNumber(item[key])) {
@ -146,11 +178,19 @@ export class ImportTransactionsService {
} }
} }
throw new Error('Could not parse quantity'); throw { message: `orders.${index}.quantity is not valid`, orders: content };
} }
private parseSymbol(aItem: any) { private parseSymbol({
const item = this.lowercaseKeys(aItem); content,
index,
item
}: {
content: any[];
index: number;
item: any;
}) {
item = this.lowercaseKeys(item);
for (const key of ImportTransactionsService.SYMBOL_KEYS) { for (const key of ImportTransactionsService.SYMBOL_KEYS) {
if (item[key]) { if (item[key]) {
@ -158,11 +198,19 @@ export class ImportTransactionsService {
} }
} }
throw new Error('Could not parse symbol'); throw { message: `orders.${index}.symbol is not valid`, orders: content };
} }
private parseType(aItem: any) { private parseType({
const item = this.lowercaseKeys(aItem); content,
index,
item
}: {
content: any[];
index: number;
item: any;
}) {
item = this.lowercaseKeys(item);
for (const key of ImportTransactionsService.TYPE_KEYS) { for (const key of ImportTransactionsService.TYPE_KEYS) {
if (item[key]) { if (item[key]) {
@ -174,11 +222,19 @@ export class ImportTransactionsService {
} }
} }
throw new Error('Could not parse type'); throw { message: `orders.${index}.type is not valid`, orders: content };
} }
private parseUnitPrice(aItem: any) { private parseUnitPrice({
const item = this.lowercaseKeys(aItem); content,
index,
item
}: {
content: any[];
index: number;
item: any;
}) {
item = this.lowercaseKeys(item);
for (const key of ImportTransactionsService.UNIT_PRICE_KEYS) { for (const key of ImportTransactionsService.UNIT_PRICE_KEYS) {
if (item[key] && isNumber(item[key])) { if (item[key] && isNumber(item[key])) {
@ -186,7 +242,10 @@ export class ImportTransactionsService {
} }
} }
throw new Error('Could not parse unit price (unitPrice)'); throw {
message: `orders.${index}.unitPrice is not valid`,
orders: content
};
} }
private postImport(aImportData: { orders: CreateOrderDto[] }) { private postImport(aImportData: { orders: CreateOrderDto[] }) {

1
libs/common/src/lib/interfaces/info-item.interface.ts

@ -1,4 +1,5 @@
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
import { Statistics } from './statistics.interface'; import { Statistics } from './statistics.interface';
import { Subscription } from './subscription.interface'; import { Subscription } from './subscription.interface';

Loading…
Cancel
Save