diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3a327edf2..cd1ecc0c1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
+- Extracted the floating action buttons (FAB) to a reusable component
+- Configured the queues to keep the last `5000` completed jobs with a maximum age of one week
- Removed the deprecated attributes (`assetClass`, `countries`, `currency`, `dataSource`, `name`, `sectors`, `symbol` and `url`) from the holdings of the public portfolio endpoint response
- Removed the deprecated `api/v1/order` endpoints
- Upgraded `@keyv/redis` from version `4.4.0` to `5.1.6`
diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts b/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts
index 28b7297d2..805adf89d 100644
--- a/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts
+++ b/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts
@@ -18,6 +18,7 @@ import { AdminMarketDataItem } from '@ghostfolio/common/interfaces/admin-market-
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { GfSymbolPipe } from '@ghostfolio/common/pipes';
import { GfActivitiesFilterComponent } from '@ghostfolio/ui/activities-filter';
+import { GfFabComponent } from '@ghostfolio/ui/fab';
import { translate } from '@ghostfolio/ui/i18n';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
import { AdminService, DataService } from '@ghostfolio/ui/services';
@@ -80,10 +81,10 @@ import { CreateAssetProfileDialogParams } from './create-asset-profile-dialog/in
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
- host: { class: 'has-fab' },
imports: [
CommonModule,
GfActivitiesFilterComponent,
+ GfFabComponent,
GfPremiumIndicatorComponent,
GfSymbolPipe,
GfValueComponent,
diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.html b/apps/client/src/app/components/admin-market-data/admin-market-data.html
index 14d12627d..63d425513 100644
--- a/apps/client/src/app/components/admin-market-data/admin-market-data.html
+++ b/apps/client/src/app/components/admin-market-data/admin-market-data.html
@@ -332,15 +332,5 @@
-
+
diff --git a/apps/client/src/app/components/home-watchlist/home-watchlist.component.ts b/apps/client/src/app/components/home-watchlist/home-watchlist.component.ts
index 7deace7de..22d829daa 100644
--- a/apps/client/src/app/components/home-watchlist/home-watchlist.component.ts
+++ b/apps/client/src/app/components/home-watchlist/home-watchlist.component.ts
@@ -8,6 +8,7 @@ import {
} from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { GfBenchmarkComponent } from '@ghostfolio/ui/benchmark';
+import { GfFabComponent } from '@ghostfolio/ui/fab';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
import { DataService } from '@ghostfolio/ui/services';
@@ -22,12 +23,8 @@ import {
OnInit
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
-import { MatButtonModule } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
-import { IonIcon } from '@ionic/angular/standalone';
-import { addIcons } from 'ionicons';
-import { addOutline } from 'ionicons/icons';
import { DeviceDetectorService } from 'ngx-device-detector';
import { GfCreateWatchlistItemDialogComponent } from './create-watchlist-item-dialog/create-watchlist-item-dialog.component';
@@ -37,9 +34,8 @@ import { CreateWatchlistItemDialogParams } from './create-watchlist-item-dialog/
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
GfBenchmarkComponent,
+ GfFabComponent,
GfPremiumIndicatorComponent,
- IonIcon,
- MatButtonModule,
RouterModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
@@ -108,8 +104,6 @@ export class GfHomeWatchlistComponent implements OnInit {
this.changeDetectorRef.markForCheck();
}
});
-
- addIcons({ addOutline });
}
public ngOnInit() {
diff --git a/apps/client/src/app/components/home-watchlist/home-watchlist.html b/apps/client/src/app/components/home-watchlist/home-watchlist.html
index c7c9a9c4b..e2865b9cf 100644
--- a/apps/client/src/app/components/home-watchlist/home-watchlist.html
+++ b/apps/client/src/app/components/home-watchlist/home-watchlist.html
@@ -22,15 +22,5 @@
@if (!hasImpersonationId && hasPermissionToCreateWatchlistItem) {
-
+
}
diff --git a/apps/client/src/app/components/user-account-access/user-account-access.component.ts b/apps/client/src/app/components/user-account-access/user-account-access.component.ts
index 985dba2cb..eef50cee3 100644
--- a/apps/client/src/app/components/user-account-access/user-account-access.component.ts
+++ b/apps/client/src/app/components/user-account-access/user-account-access.component.ts
@@ -4,6 +4,7 @@ import { CreateAccessDto } from '@ghostfolio/common/dtos';
import { ConfirmationDialogType } from '@ghostfolio/common/enums';
import { Access, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
+import { GfFabComponent } from '@ghostfolio/ui/fab';
import { NotificationService } from '@ghostfolio/ui/notifications';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
import { DataService } from '@ghostfolio/ui/services';
@@ -42,9 +43,9 @@ import { CreateOrUpdateAccessDialogParams } from './create-or-update-access-dial
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
- host: { class: 'has-fab' },
imports: [
GfAccessTableComponent,
+ GfFabComponent,
GfPremiumIndicatorComponent,
IonIcon,
MatButtonModule,
diff --git a/apps/client/src/app/components/user-account-access/user-account-access.html b/apps/client/src/app/components/user-account-access/user-account-access.html
index 412a2f8d2..62b1648bb 100644
--- a/apps/client/src/app/components/user-account-access/user-account-access.html
+++ b/apps/client/src/app/components/user-account-access/user-account-access.html
@@ -69,16 +69,6 @@
(accessToUpdate)="onUpdateAccess($event)"
/>
@if (hasPermissionToCreateAccess) {
-
+
}
diff --git a/apps/client/src/app/pages/accounts/accounts-page.component.ts b/apps/client/src/app/pages/accounts/accounts-page.component.ts
index 08513ef3e..cca1eda03 100644
--- a/apps/client/src/app/pages/accounts/accounts-page.component.ts
+++ b/apps/client/src/app/pages/accounts/accounts-page.component.ts
@@ -10,6 +10,7 @@ import {
import { User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { GfAccountsTableComponent } from '@ghostfolio/ui/accounts-table';
+import { GfFabComponent } from '@ghostfolio/ui/fab';
import { NotificationService } from '@ghostfolio/ui/notifications';
import { DataService } from '@ghostfolio/ui/services';
@@ -20,12 +21,9 @@ import {
OnInit
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
-import { MatButtonModule } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { Account as AccountModel } from '@prisma/client';
-import { addIcons } from 'ionicons';
-import { addOutline } from 'ionicons/icons';
import { DeviceDetectorService } from 'ngx-device-detector';
import { EMPTY, Subscription } from 'rxjs';
import { catchError } from 'rxjs/operators';
@@ -36,8 +34,8 @@ import { TransferBalanceDialogParams } from './transfer-balance/interfaces/inter
import { GfTransferBalanceDialogComponent } from './transfer-balance/transfer-balance-dialog.component';
@Component({
- host: { class: 'has-fab page' },
- imports: [GfAccountsTableComponent, MatButtonModule, RouterModule],
+ host: { class: 'page' },
+ imports: [GfAccountsTableComponent, GfFabComponent, RouterModule],
selector: 'gf-accounts-page',
styleUrls: ['./accounts-page.scss'],
templateUrl: './accounts-page.html'
@@ -90,8 +88,6 @@ export class GfAccountsPageComponent implements OnInit {
this.openTransferBalanceDialog();
}
});
-
- addIcons({ addOutline });
}
public ngOnInit() {
diff --git a/apps/client/src/app/pages/accounts/accounts-page.html b/apps/client/src/app/pages/accounts/accounts-page.html
index 3d9d7ee5c..1bdedbbb9 100644
--- a/apps/client/src/app/pages/accounts/accounts-page.html
+++ b/apps/client/src/app/pages/accounts/accounts-page.html
@@ -26,16 +26,6 @@
hasPermissionToCreateAccount &&
!user.settings.isRestrictedView
) {
-
+
}
diff --git a/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts b/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts
index e43af52c9..41ff570c2 100644
--- a/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts
+++ b/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts
@@ -12,6 +12,7 @@ import {
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { DateRange } from '@ghostfolio/common/types';
import { GfActivitiesTableComponent } from '@ghostfolio/ui/activities-table';
+import { GfFabComponent } from '@ghostfolio/ui/fab';
import { DataService } from '@ghostfolio/ui/services';
import {
@@ -21,17 +22,13 @@ import {
OnInit
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
-import { MatButtonModule } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog';
import { PageEvent } from '@angular/material/paginator';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { Sort, SortDirection } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
-import { IonIcon } from '@ionic/angular/standalone';
import { format, parseISO } from 'date-fns';
-import { addIcons } from 'ionicons';
-import { addOutline } from 'ionicons/icons';
import { DeviceDetectorService } from 'ngx-device-detector';
import { Subscription } from 'rxjs';
@@ -41,11 +38,9 @@ import { GfImportActivitiesDialogComponent } from './import-activities-dialog/im
import { ImportActivitiesDialogParams } from './import-activities-dialog/interfaces/interfaces';
@Component({
- host: { class: 'has-fab' },
imports: [
GfActivitiesTableComponent,
- IonIcon,
- MatButtonModule,
+ GfFabComponent,
MatSnackBarModule,
RouterModule
],
@@ -107,8 +102,6 @@ export class GfActivitiesPageComponent implements OnInit {
}
}
});
-
- addIcons({ addOutline });
}
public ngOnInit() {
diff --git a/apps/client/src/app/pages/portfolio/activities/activities-page.html b/apps/client/src/app/pages/portfolio/activities/activities-page.html
index 2a72dcfd2..f06947988 100644
--- a/apps/client/src/app/pages/portfolio/activities/activities-page.html
+++ b/apps/client/src/app/pages/portfolio/activities/activities-page.html
@@ -43,16 +43,6 @@
hasPermissionToCreateActivity &&
!user.settings.isRestrictedView
) {
-
+
}
diff --git a/apps/client/src/styles.scss b/apps/client/src/styles.scss
index 1eb5bd2dd..4321622a1 100644
--- a/apps/client/src/styles.scss
+++ b/apps/client/src/styles.scss
@@ -360,10 +360,6 @@ ngx-skeleton-loader {
text-wrap: balance;
}
-.has-fab {
- padding-bottom: 3rem !important;
-}
-
.has-info-message {
// Restrict viewport height of tabbed views when the Live Demo or system announcements banner are displayed
.page:has(gf-page-tabs) {
@@ -484,13 +480,6 @@ ngx-skeleton-loader {
padding-bottom: env(safe-area-inset-bottom);
padding-bottom: constant(safe-area-inset-bottom);
- .fab-container {
- bottom: 2rem;
- position: fixed;
- right: 2rem;
- z-index: 999;
- }
-
// Restrict viewport height and layout boundaries only when the page hosts tab navigation
&:has(gf-page-tabs) {
height: calc(100svh - var(--mat-toolbar-standard-height));
diff --git a/libs/ui/src/lib/fab/fab.component.html b/libs/ui/src/lib/fab/fab.component.html
new file mode 100644
index 000000000..8fb49de1c
--- /dev/null
+++ b/libs/ui/src/lib/fab/fab.component.html
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libs/ui/src/lib/fab/fab.component.scss b/libs/ui/src/lib/fab/fab.component.scss
new file mode 100644
index 000000000..ab6353981
--- /dev/null
+++ b/libs/ui/src/lib/fab/fab.component.scss
@@ -0,0 +1,14 @@
+:host {
+ bottom: calc(constant(safe-area-inset-bottom) + 2rem);
+ bottom: calc(env(safe-area-inset-bottom) + 2rem);
+ position: fixed;
+ right: 2rem;
+ z-index: 999;
+}
+
+:host-context(gf-page-tabs) {
+ @media (max-width: 575.98px) {
+ bottom: calc(constant(safe-area-inset-bottom) + 5rem);
+ bottom: calc(env(safe-area-inset-bottom) + 5rem);
+ }
+}
diff --git a/libs/ui/src/lib/fab/fab.component.ts b/libs/ui/src/lib/fab/fab.component.ts
new file mode 100644
index 000000000..7a4a4c169
--- /dev/null
+++ b/libs/ui/src/lib/fab/fab.component.ts
@@ -0,0 +1,16 @@
+import { ChangeDetectionStrategy, Component, input } from '@angular/core';
+import { MatButtonModule } from '@angular/material/button';
+import { Params, RouterModule } from '@angular/router';
+import { IonIcon } from '@ionic/angular/standalone';
+
+@Component({
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [IonIcon, MatButtonModule, RouterModule],
+ selector: 'gf-fab',
+ styleUrls: ['./fab.component.scss'],
+ templateUrl: './fab.component.html'
+})
+export class GfFabComponent {
+ public readonly icon = input('add-outline');
+ public readonly queryParams = input.required();
+}
diff --git a/libs/ui/src/lib/fab/index.ts b/libs/ui/src/lib/fab/index.ts
new file mode 100644
index 000000000..d03295245
--- /dev/null
+++ b/libs/ui/src/lib/fab/index.ts
@@ -0,0 +1 @@
+export * from './fab.component';
diff --git a/libs/ui/src/lib/page-tabs/page-tabs.component.scss b/libs/ui/src/lib/page-tabs/page-tabs.component.scss
index 920b00ae9..0b377e57a 100644
--- a/libs/ui/src/lib/page-tabs/page-tabs.component.scss
+++ b/libs/ui/src/lib/page-tabs/page-tabs.component.scss
@@ -15,12 +15,6 @@
);
::ng-deep {
- .fab-container {
- @media (max-width: 575.98px) {
- bottom: 5rem;
- }
- }
-
.mat-mdc-tab-nav-panel {
padding: 2rem 0;