Browse Source

Merge remote-tracking branch 'origin/main' into task/migrate-deprecated-webpack-executor

pull/7198/head
KenTandrian 1 week ago
parent
commit
ff568222a7
  1. 5
      CHANGELOG.md
  2. 8
      apps/client/src/app/components/admin-overview/admin-overview.component.ts
  3. 22
      apps/client/src/app/components/admin-overview/admin-overview.html
  4. 6
      apps/client/src/app/components/user-account-membership/user-account-membership.component.ts
  5. 6
      apps/client/src/app/pages/resources/personal-finance-tools/interfaces/interfaces.ts
  6. 38
      apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
  7. 8
      apps/client/src/app/pages/resources/personal-finance-tools/product-page.html
  8. 330
      apps/client/src/locales/messages.fr.xlf
  9. 3
      libs/common/src/lib/interfaces/coupon.interface.ts
  10. 29
      libs/ui/src/lib/notifications/alert-dialog/alert-dialog.component.ts
  11. 16
      libs/ui/src/lib/notifications/alert-dialog/alert-dialog.html
  12. 1
      libs/ui/src/lib/notifications/alert-dialog/interfaces/interfaces.ts
  13. 1
      libs/ui/src/lib/notifications/interfaces/interfaces.ts
  14. 1
      libs/ui/src/lib/notifications/notification.service.ts

5
CHANGELOG.md

@ -7,9 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
### Added
- Added support for a copy-to-clipboard action in the alert dialog component
### Changed ### Changed
- Set the change detection strategy to `OnPush` in the _FIRE_ page - Set the change detection strategy to `OnPush` in the _FIRE_ page
- Improved the language localization for French (`fr`)
- Improved the language localization for German (`de`) - Improved the language localization for German (`de`)
## 3.21.0 - 2026-07-05 ## 3.21.0 - 2026-07-05

8
apps/client/src/app/components/admin-overview/admin-overview.component.ts

@ -90,7 +90,12 @@ export class GfAdminOverviewComponent implements OnInit {
protected activitiesCount: number; protected activitiesCount: number;
protected couponDuration: StringValue = '14 days'; protected couponDuration: StringValue = '14 days';
protected readonly couponsDataSource = new MatTableDataSource<Coupon>(); protected readonly couponsDataSource = new MatTableDataSource<Coupon>();
protected readonly couponsDisplayedColumns = ['code', 'duration', 'actions']; protected readonly couponsDisplayedColumns = [
'code',
'duration',
'createdAt',
'actions'
];
protected hasPermissionForSubscription: boolean; protected hasPermissionForSubscription: boolean;
protected hasPermissionForSystemMessage: boolean; protected hasPermissionForSystemMessage: boolean;
protected hasPermissionToSyncDemoUserAccount: boolean; protected hasPermissionToSyncDemoUserAccount: boolean;
@ -201,6 +206,7 @@ export class GfAdminOverviewComponent implements OnInit {
protected onAddCoupon() { protected onAddCoupon() {
const newCoupon: Coupon = { const newCoupon: Coupon = {
code: `${ghostfolioPrefix}${this.generateCouponCode(14)}`, code: `${ghostfolioPrefix}${this.generateCouponCode(14)}`,
createdAt: new Date().toISOString(),
duration: this.couponDuration duration: this.couponDuration
}; };

22
apps/client/src/app/components/admin-overview/admin-overview.html

@ -180,6 +180,28 @@
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="createdAt">
<th *matHeaderCellDef class="px-1 text-right" mat-header-cell>
<ng-container i18n>Creation</ng-container>
</th>
<td
*matCellDef="let element"
class="px-1 text-right"
mat-cell
>
@if (element.createdAt) {
<gf-value
class="d-inline-block justify-content-end"
[isDate]="true"
[locale]="user?.settings?.locale"
[value]="element.createdAt"
/>
} @else {
<span>-</span>
}
</td>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd> <ng-container matColumnDef="actions" stickyEnd>
<th <th
*matHeaderCellDef *matHeaderCellDef

6
apps/client/src/app/components/user-account-membership/user-account-membership.component.ts

@ -146,11 +146,9 @@ export class GfUserAccountMembershipComponent {
) )
.subscribe(({ apiKey }) => { .subscribe(({ apiKey }) => {
this.notificationService.alert({ this.notificationService.alert({
copyValue: apiKey,
discardLabel: $localize`Okay`, discardLabel: $localize`Okay`,
message: message: $localize`Set this API key in your self-hosted environment:`,
$localize`Set this API key in your self-hosted environment:` +
'<br />' +
apiKey,
title: $localize`Ghostfolio Premium Data Provider API Key` title: $localize`Ghostfolio Premium Data Provider API Key`
}); });
}); });

6
apps/client/src/app/pages/resources/personal-finance-tools/interfaces/interfaces.ts

@ -0,0 +1,6 @@
import type { Product } from '@ghostfolio/common/interfaces';
export type ResolvedProduct = Omit<Product, 'categories' | 'platforms'> & {
categories?: string[];
platforms?: string[];
};

38
apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts

@ -1,5 +1,4 @@
import { getCountryName } from '@ghostfolio/common/helper'; import { getCountryName } from '@ghostfolio/common/helper';
import { Product } from '@ghostfolio/common/interfaces';
import { personalFinanceTools } from '@ghostfolio/common/personal-finance-tools'; import { personalFinanceTools } from '@ghostfolio/common/personal-finance-tools';
import { publicRoutes } from '@ghostfolio/common/routes/routes'; import { publicRoutes } from '@ghostfolio/common/routes/routes';
import { translate } from '@ghostfolio/ui/i18n'; import { translate } from '@ghostfolio/ui/i18n';
@ -14,6 +13,8 @@ import {
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { ActivatedRoute, RouterModule } from '@angular/router'; import { ActivatedRoute, RouterModule } from '@angular/router';
import { ResolvedProduct } from './interfaces/interfaces';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'page' }, host: { class: 'page' },
@ -28,8 +29,12 @@ export class GfProductPageComponent {
return subscriptionOffer?.price; return subscriptionOffer?.price;
}); });
protected readonly product1 = computed<Product>(() => ({ protected readonly product1 = computed<ResolvedProduct>(() => ({
categories: ['FINANCIAL_PLANNING', 'NET_WORTH_TRACKING', 'STOCK_TRACKING'], categories: this.getSortedTranslations([
'FINANCIAL_PLANNING',
'NET_WORTH_TRACKING',
'STOCK_TRACKING'
]),
founded: 2021, founded: 2021,
hasFreePlan: true, hasFreePlan: true,
hasSelfHostingAbility: true, hasSelfHostingAbility: true,
@ -50,21 +55,23 @@ export class GfProductPageComponent {
], ],
name: 'Ghostfolio', name: 'Ghostfolio',
origin: getCountryName({ code: 'CH' }), origin: getCountryName({ code: 'CH' }),
platforms: ['ANDROID', 'WEB'], platforms: this.getSortedTranslations(['ANDROID', 'WEB']),
regions: [$localize`Global`], regions: [$localize`Global`],
slogan: 'Open Source Wealth Management', slogan: 'Open Source Wealth Management',
useAnonymously: true useAnonymously: true
})); }));
protected readonly product2 = computed<Product>(() => { protected readonly product2 = computed<ResolvedProduct>(() => {
const product = personalFinanceTools.find(({ key }) => { const product = personalFinanceTools.find(({ key }) => {
return key === this.route.snapshot.data['key']; return key === this.route.snapshot.data['key'];
}); });
const mappedProduct = { const mappedProduct: ResolvedProduct = {
key: product?.key ?? '', key: product?.key ?? '',
name: product?.name ?? '', name: product?.name ?? '',
...product ...product,
categories: this.getSortedTranslations(product?.categories),
platforms: this.getSortedTranslations(product?.platforms)
}; };
if (mappedProduct.origin) { if (mappedProduct.origin) {
@ -97,9 +104,8 @@ export class GfProductPageComponent {
...[product1, product2].flatMap( ...[product1, product2].flatMap(
({ categories, name, origin, platforms }) => { ({ categories, name, origin, platforms }) => {
return [ return [
...[...(categories ?? []), ...(platforms ?? [])].map((key) => { ...(categories ?? []),
return translate(key); ...(platforms ?? []),
}),
name, name,
origin origin
]; ];
@ -130,8 +136,16 @@ export class GfProductPageComponent {
}); });
}); });
protected readonly translate = translate;
private readonly dataService = inject(DataService); private readonly dataService = inject(DataService);
private readonly route = inject(ActivatedRoute); private readonly route = inject(ActivatedRoute);
private getSortedTranslations(values?: string[]) {
return values
?.map((value) => {
return translate(value);
})
.sort((a, b) => {
return a.localeCompare(b, undefined, { sensitivity: 'base' });
});
}
} }

8
apps/client/src/app/pages/resources/personal-finance-tools/product-page.html

@ -81,7 +81,7 @@
track category; track category;
let isLast = $last let isLast = $last
) { ) {
{{ translate(category) }}{{ isLast ? '' : ', ' }} {{ category }}{{ isLast ? '' : ', ' }}
} }
</td> </td>
<td class="mat-mdc-cell px-1 py-2"> <td class="mat-mdc-cell px-1 py-2">
@ -90,7 +90,7 @@
track category; track category;
let isLast = $last let isLast = $last
) { ) {
{{ translate(category) }}{{ isLast ? '' : ', ' }} {{ category }}{{ isLast ? '' : ', ' }}
} }
</td> </td>
</tr> </tr>
@ -135,7 +135,7 @@
track platform; track platform;
let isLast = $last let isLast = $last
) { ) {
{{ translate(platform) }}{{ isLast ? '' : ', ' }} {{ platform }}{{ isLast ? '' : ', ' }}
} }
</td> </td>
<td class="mat-mdc-cell px-1 py-2"> <td class="mat-mdc-cell px-1 py-2">
@ -144,7 +144,7 @@
track platform; track platform;
let isLast = $last let isLast = $last
) { ) {
{{ translate(platform) }}{{ isLast ? '' : ', ' }} {{ platform }}{{ isLast ? '' : ', ' }}
} }
</td> </td>
</tr> </tr>

330
apps/client/src/locales/messages.fr.xlf

File diff suppressed because it is too large

3
libs/common/src/lib/interfaces/coupon.interface.ts

@ -2,5 +2,6 @@ import { StringValue } from 'ms';
export interface Coupon { export interface Coupon {
code: string; code: string;
duration?: StringValue; createdAt: string;
duration: StringValue;
} }

29
libs/ui/src/lib/notifications/alert-dialog/alert-dialog.component.ts

@ -1,6 +1,9 @@
import { Clipboard } from '@angular/cdk/clipboard';
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import ms from 'ms';
import { AlertDialogParams } from './interfaces/interfaces'; import { AlertDialogParams } from './interfaces/interfaces';
@ -12,6 +15,7 @@ import { AlertDialogParams } from './interfaces/interfaces';
templateUrl: './alert-dialog.html' templateUrl: './alert-dialog.html'
}) })
export class GfAlertDialogComponent { export class GfAlertDialogComponent {
public copyValue?: string;
public discardLabel: string; public discardLabel: string;
public message?: string; public message?: string;
public title: string; public title: string;
@ -19,9 +23,32 @@ export class GfAlertDialogComponent {
protected readonly dialogRef = protected readonly dialogRef =
inject<MatDialogRef<GfAlertDialogComponent>>(MatDialogRef); inject<MatDialogRef<GfAlertDialogComponent>>(MatDialogRef);
public initialize({ discardLabel, message, title }: AlertDialogParams) { private readonly clipboard = inject(Clipboard);
private readonly snackBar = inject(MatSnackBar);
public initialize({
copyValue,
discardLabel,
message,
title
}: AlertDialogParams) {
this.copyValue = copyValue;
this.discardLabel = discardLabel; this.discardLabel = discardLabel;
this.message = message; this.message = message;
this.title = title; this.title = title;
} }
public onCopyToClipboard() {
if (this.copyValue) {
this.clipboard.copy(this.copyValue);
this.snackBar.open(
'✅ ' + $localize`The value has been copied to the clipboard`,
undefined,
{
duration: ms('3 seconds')
}
);
}
}
} }

16
libs/ui/src/lib/notifications/alert-dialog/alert-dialog.html

@ -2,10 +2,22 @@
<div mat-dialog-title [innerHTML]="title"></div> <div mat-dialog-title [innerHTML]="title"></div>
} }
@if (message) { @if (message || copyValue) {
<div mat-dialog-content [innerHTML]="message"></div> <div mat-dialog-content>
@if (message) {
<div [innerHTML]="message"></div>
}
@if (copyValue) {
<div class="mt-2">{{ copyValue }}</div>
}
</div>
} }
<div align="end" mat-dialog-actions> <div align="end" mat-dialog-actions>
<button mat-button (click)="dialogRef.close()">{{ discardLabel }}</button> <button mat-button (click)="dialogRef.close()">{{ discardLabel }}</button>
@if (copyValue) {
<button color="primary" mat-flat-button (click)="onCopyToClipboard()">
<ng-container i18n>Copy</ng-container>
</button>
}
</div> </div>

1
libs/ui/src/lib/notifications/alert-dialog/interfaces/interfaces.ts

@ -1,4 +1,5 @@
export interface AlertDialogParams { export interface AlertDialogParams {
copyValue?: string;
discardLabel: string; discardLabel: string;
message?: string; message?: string;
title: string; title: string;

1
libs/ui/src/lib/notifications/interfaces/interfaces.ts

@ -1,6 +1,7 @@
import { ConfirmationDialogType } from '@ghostfolio/common/enums'; import { ConfirmationDialogType } from '@ghostfolio/common/enums';
export interface AlertParams { export interface AlertParams {
copyValue?: string;
discardFn?: () => void; discardFn?: () => void;
discardLabel?: string; discardLabel?: string;
message?: string; message?: string;

1
libs/ui/src/lib/notifications/notification.service.ts

@ -31,6 +31,7 @@ export class NotificationService {
}); });
dialog.componentInstance.initialize({ dialog.componentInstance.initialize({
copyValue: aParams.copyValue,
discardLabel: aParams.discardLabel, discardLabel: aParams.discardLabel,
message: aParams.message, message: aParams.message,
title: aParams.title title: aParams.title

Loading…
Cancel
Save