Feature/improve activities import (#1531)
This commit is contained in:
parent
c22733db56
commit
a08610b603
apps/client/src/app
pages/portfolio/activities/import-activities-dialog
import-activities-dialog.component.tsimport-activities-dialog.htmlimport-activities-dialog.module.ts
services
libs/ui/src/lib/activities-table
@ -7,6 +7,7 @@ import {
|
||||
} from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||
import { ImportActivitiesService } from '@ghostfolio/client/services/import-activities.service';
|
||||
import { isArray } from 'lodash';
|
||||
import { Subject } from 'rxjs';
|
||||
@ -20,8 +21,11 @@ import { ImportActivitiesDialogParams } from './interfaces/interfaces';
|
||||
templateUrl: 'import-activities-dialog.html'
|
||||
})
|
||||
export class ImportActivitiesDialog implements OnDestroy {
|
||||
public activities: Activity[] = [];
|
||||
public details: any[] = [];
|
||||
public errorMessages: string[] = [];
|
||||
public isFileSelected = false;
|
||||
public selectedActivities: Activity[] = [];
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
@ -39,13 +43,47 @@ export class ImportActivitiesDialog implements OnDestroy {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
public onImport() {
|
||||
public async onImportActivities() {
|
||||
try {
|
||||
this.snackBar.open('⏳ ' + $localize`Importing data...`);
|
||||
|
||||
await this.importActivitiesService.importSelectedActivities(
|
||||
this.selectedActivities
|
||||
);
|
||||
|
||||
this.snackBar.open(
|
||||
'✅ ' + $localize`Import has been completed`,
|
||||
undefined,
|
||||
{
|
||||
duration: 3000
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
this.snackBar.open(
|
||||
$localize`Oops! Something went wrong.` +
|
||||
' ' +
|
||||
$localize`Please try again later.`,
|
||||
$localize`Okay`,
|
||||
{ duration: 3000 }
|
||||
);
|
||||
} finally {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
||||
|
||||
public onReset() {
|
||||
this.details = [];
|
||||
this.errorMessages = [];
|
||||
this.isFileSelected = false;
|
||||
}
|
||||
|
||||
public onSelectFile() {
|
||||
const input = document.createElement('input');
|
||||
input.accept = 'application/JSON, .csv';
|
||||
input.type = 'file';
|
||||
|
||||
input.onchange = (event) => {
|
||||
this.snackBar.open('⏳ ' + $localize`Importing data...`);
|
||||
this.snackBar.open('⏳ ' + $localize`Validating data...`);
|
||||
|
||||
// Getting the file reference
|
||||
const file = (event.target as HTMLInputElement).files[0];
|
||||
@ -80,11 +118,10 @@ export class ImportActivitiesDialog implements OnDestroy {
|
||||
}
|
||||
|
||||
try {
|
||||
await this.importActivitiesService.importJson({
|
||||
content: content.activities
|
||||
this.activities = await this.importActivitiesService.importJson({
|
||||
content: content.activities,
|
||||
dryRun: true
|
||||
});
|
||||
|
||||
this.handleImportSuccess();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.handleImportError({ error, activities: content.activities });
|
||||
@ -93,12 +130,11 @@ export class ImportActivitiesDialog implements OnDestroy {
|
||||
return;
|
||||
} else if (file.name.endsWith('.csv')) {
|
||||
try {
|
||||
await this.importActivitiesService.importCsv({
|
||||
this.activities = await this.importActivitiesService.importCsv({
|
||||
dryRun: true,
|
||||
fileContent,
|
||||
userAccounts: this.data.user.accounts
|
||||
});
|
||||
|
||||
this.handleImportSuccess();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.handleImportError({
|
||||
@ -119,6 +155,10 @@ export class ImportActivitiesDialog implements OnDestroy {
|
||||
activities: [],
|
||||
error: { error: { message: ['Unexpected format'] } }
|
||||
});
|
||||
} finally {
|
||||
this.isFileSelected = true;
|
||||
this.snackBar.dismiss();
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
};
|
||||
};
|
||||
@ -126,9 +166,8 @@ export class ImportActivitiesDialog implements OnDestroy {
|
||||
input.click();
|
||||
}
|
||||
|
||||
public onReset() {
|
||||
this.details = [];
|
||||
this.errorMessages = [];
|
||||
public updateSelection(data: Activity[]) {
|
||||
this.selectedActivities = data;
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
@ -143,8 +182,6 @@ export class ImportActivitiesDialog implements OnDestroy {
|
||||
activities: any[];
|
||||
error: any;
|
||||
}) {
|
||||
this.snackBar.dismiss();
|
||||
|
||||
this.errorMessages = error?.error?.message;
|
||||
|
||||
for (const message of this.errorMessages) {
|
||||
@ -161,16 +198,4 @@ export class ImportActivitiesDialog implements OnDestroy {
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
private handleImportSuccess() {
|
||||
this.snackBar.open(
|
||||
'✅ ' + $localize`Import has been completed`,
|
||||
undefined,
|
||||
{
|
||||
duration: 3000
|
||||
}
|
||||
);
|
||||
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
||||
|
@ -6,13 +6,13 @@
|
||||
></gf-dialog-header>
|
||||
|
||||
<div class="flex-grow-1" mat-dialog-content>
|
||||
<ng-container *ngIf="errorMessages.length === 0">
|
||||
<ng-container *ngIf="!isFileSelected">
|
||||
<div class="d-flex justify-content-center flex-column">
|
||||
<button
|
||||
class="py-3"
|
||||
color="primary"
|
||||
mat-stroked-button
|
||||
(click)="onImport()"
|
||||
(click)="onSelectFile()"
|
||||
>
|
||||
<ion-icon class="mr-2" name="cloud-upload-outline"></ion-icon>
|
||||
<span i18n>Choose File</span>
|
||||
@ -33,37 +33,68 @@
|
||||
</p>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="errorMessages.length > 0">
|
||||
<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>
|
||||
<ng-container *ngIf="isFileSelected">
|
||||
<ng-container *ngIf="errorMessages.length === 0; else errorMessage">
|
||||
<gf-activities-table
|
||||
[activities]="activities"
|
||||
[baseCurrency]="data?.user?.settings?.baseCurrency"
|
||||
[deviceType]="data?.deviceType"
|
||||
[hasPermissionToCreateActivity]="false"
|
||||
[hasPermissionToExportActivities]="false"
|
||||
[hasPermissionToFilter]="false"
|
||||
[hasPermissionToImportActivities]="false"
|
||||
[hasPermissionToOpenDetails]="false"
|
||||
[locale]="data?.user?.settings?.locale"
|
||||
[showActions]="false"
|
||||
[showCheckbox]="true"
|
||||
[showSymbolColumn]="false"
|
||||
(selectedActivities)="updateSelection($event)"
|
||||
></gf-activities-table>
|
||||
</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>
|
||||
<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="mt-2">
|
||||
<button mat-button (click)="onReset()">
|
||||
<ion-icon class="mr-2" name="arrow-back-outline"></ion-icon>
|
||||
<span i18n>Back</span>
|
||||
</button>
|
||||
</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="mt-2">
|
||||
<button mat-button (click)="onReset()">
|
||||
<ion-icon class="mr-2" name="arrow-back-outline"></ion-icon>
|
||||
<span i18n>Back</span>
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div *ngIf="isFileSelected" class="justify-content-end" mat-dialog-actions>
|
||||
<button i18n mat-button (click)="onCancel()">Cancel</button>
|
||||
<button
|
||||
color="primary"
|
||||
mat-flat-button
|
||||
[disabled]="!selectedActivities?.length"
|
||||
(click)="onImportActivities()"
|
||||
>
|
||||
<ng-container i18n>Import</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<gf-dialog-footer
|
||||
mat-dialog-actions
|
||||
[deviceType]="data.deviceType"
|
||||
|
@ -3,6 +3,7 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatExpansionModule } from '@angular/material/expansion';
|
||||
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
|
||||
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
|
||||
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
|
||||
|
||||
@ -12,6 +13,7 @@ import { ImportActivitiesDialog } from './import-activities-dialog.component';
|
||||
declarations: [ImportActivitiesDialog],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfActivitiesTableModule,
|
||||
GfDialogFooterModule,
|
||||
GfDialogHeaderModule,
|
||||
MatButtonModule,
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||
import { Account, DataSource, Type } from '@prisma/client';
|
||||
import { isMatch, parse, parseISO } from 'date-fns';
|
||||
import { isFinite } from 'lodash';
|
||||
@ -25,12 +26,14 @@ export class ImportActivitiesService {
|
||||
public constructor(private http: HttpClient) {}
|
||||
|
||||
public async importCsv({
|
||||
dryRun = false,
|
||||
fileContent,
|
||||
userAccounts
|
||||
}: {
|
||||
dryRun?: boolean;
|
||||
fileContent: string;
|
||||
userAccounts: Account[];
|
||||
}) {
|
||||
}): Promise<Activity[]> {
|
||||
const content = csvToJson(fileContent, {
|
||||
dynamicTyping: true,
|
||||
header: true,
|
||||
@ -52,14 +55,23 @@ export class ImportActivitiesService {
|
||||
});
|
||||
}
|
||||
|
||||
await this.importJson({ content: activities });
|
||||
return await this.importJson({ content: activities, dryRun });
|
||||
}
|
||||
|
||||
public importJson({ content }: { content: CreateOrderDto[] }): Promise<void> {
|
||||
public importJson({
|
||||
content,
|
||||
dryRun = false
|
||||
}: {
|
||||
content: CreateOrderDto[];
|
||||
dryRun?: boolean;
|
||||
}): Promise<Activity[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.postImport({
|
||||
activities: content
|
||||
})
|
||||
this.postImport(
|
||||
{
|
||||
activities: content
|
||||
},
|
||||
dryRun
|
||||
)
|
||||
.pipe(
|
||||
catchError((error) => {
|
||||
reject(error);
|
||||
@ -67,13 +79,35 @@ export class ImportActivitiesService {
|
||||
})
|
||||
)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
resolve();
|
||||
next: (data) => {
|
||||
resolve(data.activities);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public importSelectedActivities(
|
||||
selectedActivities: Activity[]
|
||||
): Promise<Activity[]> {
|
||||
const importData: CreateOrderDto[] = [];
|
||||
for (const activity of selectedActivities) {
|
||||
importData.push(this.convertToCreateOrderDto(activity));
|
||||
}
|
||||
return this.importJson({ content: importData });
|
||||
}
|
||||
|
||||
private convertToCreateOrderDto(aActivity: Activity): CreateOrderDto {
|
||||
return {
|
||||
currency: aActivity.SymbolProfile.currency,
|
||||
date: aActivity.date.toString(),
|
||||
fee: aActivity.fee,
|
||||
quantity: aActivity.quantity,
|
||||
symbol: aActivity.SymbolProfile.symbol,
|
||||
type: aActivity.type,
|
||||
unitPrice: aActivity.unitPrice
|
||||
};
|
||||
}
|
||||
|
||||
private lowercaseKeys(aObject: any) {
|
||||
return Object.keys(aObject).reduce((acc, key) => {
|
||||
acc[key.toLowerCase()] = aObject[key];
|
||||
@ -301,7 +335,13 @@ export class ImportActivitiesService {
|
||||
};
|
||||
}
|
||||
|
||||
private postImport(aImportData: { activities: CreateOrderDto[] }) {
|
||||
return this.http.post<void>('/api/v1/import', aImportData);
|
||||
private postImport(
|
||||
aImportData: { activities: CreateOrderDto[] },
|
||||
dryRun = false
|
||||
) {
|
||||
return this.http.post<{ activities: Activity[] }>(
|
||||
`/api/v1/import?dryRun=${dryRun}`,
|
||||
aImportData
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,26 @@
|
||||
matSortDirection="desc"
|
||||
[dataSource]="dataSource"
|
||||
>
|
||||
<ng-container matColumnDef="select">
|
||||
<th *matHeaderCellDef class="px-1" mat-header-cell>
|
||||
<mat-checkbox
|
||||
class="mt-2"
|
||||
[checked]="selectedRows.hasValue() && areAllRowsSelected()"
|
||||
[indeterminate]="selectedRows.hasValue() && !areAllRowsSelected()"
|
||||
(change)="$event ? toggleAllRows() : null"
|
||||
></mat-checkbox>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||
<mat-checkbox
|
||||
class="mt-2"
|
||||
[checked]="selectedRows.isSelected(element)"
|
||||
(change)="$event ? selectedRows.toggle(element) : null"
|
||||
(click)="$event.stopPropagation()"
|
||||
></mat-checkbox>
|
||||
</td>
|
||||
<td *matFooterCellDef class="px-1" mat-footer-cell></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="count">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
@ -45,7 +65,7 @@
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||
<div class="d-flex">
|
||||
{{ element.date | date: defaultDateFormat }}
|
||||
{{ element.date | date : defaultDateFormat }}
|
||||
</div>
|
||||
</td>
|
||||
<td *matFooterCellDef class="px-1" i18n mat-footer-cell>Total</td>
|
||||
@ -432,15 +452,7 @@
|
||||
'cursor-pointer':
|
||||
hasPermissionToOpenDetails && !row.isDraft && row.type !== 'ITEM'
|
||||
}"
|
||||
(click)="
|
||||
hasPermissionToOpenDetails &&
|
||||
!row.isDraft &&
|
||||
row.type !== 'ITEM' &&
|
||||
onOpenPositionDialog({
|
||||
dataSource: row.SymbolProfile.dataSource,
|
||||
symbol: row.SymbolProfile.symbol
|
||||
})
|
||||
"
|
||||
(click)="onClickActivity(row)"
|
||||
></tr>
|
||||
<tr
|
||||
*matFooterRowDef="displayedColumns"
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
Output,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { SelectionModel } from '@angular/cdk/collections';
|
||||
import { MatPaginator, PageEvent } from '@angular/material/paginator';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
@ -41,6 +42,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
||||
@Input() locale: string;
|
||||
@Input() pageSize = DEFAULT_PAGE_SIZE;
|
||||
@Input() showActions: boolean;
|
||||
@Input() showCheckbox = false;
|
||||
@Input() showNameColumn = true;
|
||||
|
||||
@Output() activityDeleted = new EventEmitter<string>();
|
||||
@ -49,6 +51,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
||||
@Output() export = new EventEmitter<string[]>();
|
||||
@Output() exportDrafts = new EventEmitter<string[]>();
|
||||
@Output() import = new EventEmitter<void>();
|
||||
@Output() selectedActivities = new EventEmitter<Activity[]>();
|
||||
|
||||
@ViewChild(MatPaginator) paginator: MatPaginator;
|
||||
@ViewChild(MatSort) sort: MatSort;
|
||||
@ -67,6 +70,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
||||
public placeholder = '';
|
||||
public routeQueryParams: Subscription;
|
||||
public searchKeywords: string[] = [];
|
||||
public selectedRows = new SelectionModel<Activity>(true, []);
|
||||
public totalFees: number;
|
||||
public totalValue: number;
|
||||
|
||||
@ -81,8 +85,15 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
public areAllRowsSelected() {
|
||||
const numSelectedRows = this.selectedRows.selected.length;
|
||||
const numTotalRows = this.dataSource.data.length;
|
||||
return numSelectedRows === numTotalRows;
|
||||
}
|
||||
|
||||
public ngOnChanges() {
|
||||
this.displayedColumns = [
|
||||
'select',
|
||||
'count',
|
||||
'date',
|
||||
'type',
|
||||
@ -98,6 +109,16 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
||||
'actions'
|
||||
];
|
||||
|
||||
if (this.showCheckbox) {
|
||||
this.displayedColumns = this.displayedColumns.filter((column) => {
|
||||
return column !== 'count';
|
||||
});
|
||||
} else {
|
||||
this.displayedColumns = this.displayedColumns.filter((column) => {
|
||||
return column !== 'select';
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.showNameColumn) {
|
||||
this.displayedColumns = this.displayedColumns.filter((column) => {
|
||||
return column !== 'nameWithSymbol';
|
||||
@ -133,6 +154,17 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if (this.showCheckbox) {
|
||||
this.toggleAllRows();
|
||||
this.selectedRows.changed
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((selectedRows) => {
|
||||
this.selectedActivities.emit(selectedRows.source.selected);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public onChangePage(page: PageEvent) {
|
||||
this.pageIndex = page.pageIndex;
|
||||
|
||||
@ -140,6 +172,21 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
||||
this.totalValue = this.getTotalValue();
|
||||
}
|
||||
|
||||
public onClickActivity(activity: Activity) {
|
||||
if (this.showCheckbox) {
|
||||
this.selectedRows.toggle(activity);
|
||||
} else if (
|
||||
this.hasPermissionToOpenDetails &&
|
||||
!activity.isDraft &&
|
||||
activity.type !== 'ITEM'
|
||||
) {
|
||||
this.onOpenPositionDialog({
|
||||
dataSource: activity.SymbolProfile.dataSource,
|
||||
symbol: activity.SymbolProfile.symbol
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public onCloneActivity(aActivity: OrderWithAccount) {
|
||||
this.activityToClone.emit(aActivity);
|
||||
}
|
||||
@ -200,6 +247,14 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
||||
this.activityToUpdate.emit(aActivity);
|
||||
}
|
||||
|
||||
public toggleAllRows() {
|
||||
this.areAllRowsSelected()
|
||||
? this.selectedRows.clear()
|
||||
: this.dataSource.data.forEach((row) => this.selectedRows.select(row));
|
||||
|
||||
this.selectedActivities.emit(this.selectedRows.selected);
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
@ -237,9 +292,9 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
||||
type: 'TAG'
|
||||
};
|
||||
|
||||
fieldValueMap[format(activity.date, 'yyyy')] = {
|
||||
id: format(activity.date, 'yyyy'),
|
||||
label: format(activity.date, 'yyyy'),
|
||||
fieldValueMap[format(new Date(activity.date), 'yyyy')] = {
|
||||
id: format(new Date(activity.date), 'yyyy'),
|
||||
label: format(new Date(activity.date), 'yyyy'),
|
||||
type: 'TAG'
|
||||
};
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatPaginatorModule } from '@angular/material/paginator';
|
||||
import { MatSortModule } from '@angular/material/sort';
|
||||
@ -26,6 +27,7 @@ import { ActivitiesTableComponent } from './activities-table.component';
|
||||
GfSymbolModule,
|
||||
GfValueModule,
|
||||
MatButtonModule,
|
||||
MatCheckboxModule,
|
||||
MatMenuModule,
|
||||
MatPaginatorModule,
|
||||
MatSortModule,
|
||||
|
Loading…
x
Reference in New Issue
Block a user