Feature/bulk deletion for asset profiles (#3531)

* Add support for bulk deletion of asset profiles

* Update changelog
This commit is contained in:
Thomas Kaul 2024-06-30 09:21:04 +02:00 committed by GitHub
parent 83b5cfff1f
commit 554136cdcd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 82 additions and 6 deletions

View File

@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
### Added
- Added support for bulk deletion of asset profiles from the market data table in the admin control panel
### Changed ### Changed
- Added support for derived currencies in the currency validation - Added support for derived currencies in the currency validation

View File

@ -10,6 +10,7 @@ import { Filter, UniqueAsset, User } from '@ghostfolio/common/interfaces';
import { AdminMarketDataItem } from '@ghostfolio/common/interfaces/admin-market-data.interface'; import { AdminMarketDataItem } from '@ghostfolio/common/interfaces/admin-market-data.interface';
import { translate } from '@ghostfolio/ui/i18n'; import { translate } from '@ghostfolio/ui/i18n';
import { SelectionModel } from '@angular/cdk/collections';
import { import {
AfterViewInit, AfterViewInit,
ChangeDetectionStrategy, ChangeDetectionStrategy,
@ -97,6 +98,7 @@ export class AdminMarketDataComponent
public defaultDateFormat: string; public defaultDateFormat: string;
public deviceType: string; public deviceType: string;
public displayedColumns = [ public displayedColumns = [
'select',
'nameWithSymbol', 'nameWithSymbol',
'dataSource', 'dataSource',
'assetClass', 'assetClass',
@ -115,6 +117,7 @@ export class AdminMarketDataComponent
public isUUID = isUUID; public isUUID = isUUID;
public placeholder = ''; public placeholder = '';
public pageSize = DEFAULT_PAGE_SIZE; public pageSize = DEFAULT_PAGE_SIZE;
public selection: SelectionModel<Partial<SymbolProfile>>;
public totalItems = 0; public totalItems = 0;
public user: User; public user: User;
@ -188,6 +191,8 @@ export class AdminMarketDataComponent
this.benchmarks = benchmarks; this.benchmarks = benchmarks;
this.deviceType = this.deviceService.getDeviceInfo().deviceType; this.deviceType = this.deviceService.getDeviceInfo().deviceType;
this.selection = new SelectionModel(true);
} }
public onChangePage(page: PageEvent) { public onChangePage(page: PageEvent) {
@ -198,8 +203,16 @@ export class AdminMarketDataComponent
}); });
} }
public onDeleteProfileData({ dataSource, symbol }: UniqueAsset) { public onDeleteAssetProfile({ dataSource, symbol }: UniqueAsset) {
this.adminMarketDataService.deleteProfileData({ dataSource, symbol }); this.adminMarketDataService.deleteAssetProfile({ dataSource, symbol });
}
public onDeleteAssetProfiles() {
this.adminMarketDataService.deleteAssetProfiles(
this.selection.selected.map(({ dataSource, symbol }) => {
return { dataSource, symbol };
})
);
} }
public onGather7Days() { public onGather7Days() {
@ -286,6 +299,8 @@ export class AdminMarketDataComponent
this.placeholder = this.placeholder =
this.activeFilters.length <= 0 ? $localize`Filter by...` : ''; this.activeFilters.length <= 0 ? $localize`Filter by...` : '';
this.selection.clear();
this.adminService this.adminService
.fetchAdminMarketData({ .fetchAdminMarketData({
sortColumn, sortColumn,

View File

@ -20,6 +20,27 @@
matSortDirection="asc" matSortDirection="asc"
[dataSource]="dataSource" [dataSource]="dataSource"
> >
<ng-container matColumnDef="select">
<th *matHeaderCellDef class="px-1" mat-header-cell></th>
<td *matCellDef="let element" class="px-1" mat-cell>
@if (
!(
element.activitiesCount !== 0 ||
element.isBenchmark ||
element.symbol.startsWith(ghostfolioScraperApiSymbolPrefix)
)
) {
<mat-checkbox
color="primary"
[checked]="selection.isSelected(element)"
(change)="$event ? selection.toggle(element) : null"
(click)="$event.stopPropagation()"
>
</mat-checkbox>
}
</td>
</ng-container>
<ng-container matColumnDef="symbol"> <ng-container matColumnDef="symbol">
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header> <th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
<ng-container i18n>Symbol</ng-container> <ng-container i18n>Symbol</ng-container>
@ -152,6 +173,13 @@
<button mat-menu-item (click)="onGatherProfileData()"> <button mat-menu-item (click)="onGatherProfileData()">
<ng-container i18n>Gather Profile Data</ng-container> <ng-container i18n>Gather Profile Data</ng-container>
</button> </button>
<button
mat-menu-item
[disabled]="!selection.hasValue()"
(click)="onDeleteAssetProfiles()"
>
<ng-container i18n>Delete Asset Profiles</ng-container>
</button>
</mat-menu> </mat-menu>
</th> </th>
<td *matCellDef="let element" class="px-1 text-center" mat-cell> <td *matCellDef="let element" class="px-1 text-center" mat-cell>
@ -186,7 +214,7 @@
element.symbol.startsWith(ghostfolioScraperApiSymbolPrefix) element.symbol.startsWith(ghostfolioScraperApiSymbolPrefix)
" "
(click)=" (click)="
onDeleteProfileData({ onDeleteAssetProfile({
dataSource: element.dataSource, dataSource: element.dataSource,
symbol: element.symbol symbol: element.symbol
}) })

View File

@ -4,6 +4,7 @@ import { GfActivitiesFilterComponent } from '@ghostfolio/ui/activities-filter';
import { CommonModule } from '@angular/common'; 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 { MatCheckboxModule } from '@angular/material/checkbox';
import { MatMenuModule } from '@angular/material/menu'; import { MatMenuModule } from '@angular/material/menu';
import { MatPaginatorModule } from '@angular/material/paginator'; import { MatPaginatorModule } from '@angular/material/paginator';
import { MatSortModule } from '@angular/material/sort'; import { MatSortModule } from '@angular/material/sort';
@ -25,6 +26,7 @@ import { GfCreateAssetProfileDialogModule } from './create-asset-profile-dialog/
GfCreateAssetProfileDialogModule, GfCreateAssetProfileDialogModule,
GfSymbolModule, GfSymbolModule,
MatButtonModule, MatButtonModule,
MatCheckboxModule,
MatMenuModule, MatMenuModule,
MatPaginatorModule, MatPaginatorModule,
MatSortModule, MatSortModule,

View File

@ -2,13 +2,13 @@ import { AdminService } from '@ghostfolio/client/services/admin.service';
import { UniqueAsset } from '@ghostfolio/common/interfaces'; import { UniqueAsset } from '@ghostfolio/common/interfaces';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { takeUntil } from 'rxjs'; import { EMPTY, catchError, finalize, forkJoin, takeUntil } from 'rxjs';
@Injectable() @Injectable()
export class AdminMarketDataService { export class AdminMarketDataService {
public constructor(private adminService: AdminService) {} public constructor(private adminService: AdminService) {}
public deleteProfileData({ dataSource, symbol }: UniqueAsset) { public deleteAssetProfile({ dataSource, symbol }: UniqueAsset) {
const confirmation = confirm( const confirmation = confirm(
$localize`Do you really want to delete this asset profile?` $localize`Do you really want to delete this asset profile?`
); );
@ -23,4 +23,31 @@ export class AdminMarketDataService {
}); });
} }
} }
public deleteAssetProfiles(uniqueAssets: UniqueAsset[]) {
const confirmation = confirm(
$localize`Do you really want to delete these asset profiles?`
);
if (confirmation) {
const deleteRequests = uniqueAssets.map(({ dataSource, symbol }) => {
return this.adminService.deleteProfileData({ dataSource, symbol });
});
forkJoin(deleteRequests)
.pipe(
catchError(() => {
alert($localize`Oops! Could not delete asset profiles.`);
return EMPTY;
}),
finalize(() => {
setTimeout(() => {
window.location.reload();
}, 300);
})
)
.subscribe(() => {});
}
}
} }

View File

@ -176,7 +176,7 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
} }
public onDeleteProfileData({ dataSource, symbol }: UniqueAsset) { public onDeleteProfileData({ dataSource, symbol }: UniqueAsset) {
this.adminMarketDataService.deleteProfileData({ dataSource, symbol }); this.adminMarketDataService.deleteAssetProfile({ dataSource, symbol });
this.dialogRef.close(); this.dialogRef.close();
} }