Browse Source

Task/improve type filter of activities table component to only list activity types in use (#7602)

* Limit type filter in activities table component to used types

* Update changelog
pull/7695/head
Thomas Kaul 6 days ago
committed by GitHub
parent
commit
4da8658eec
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      CHANGELOG.md
  2. 2
      apps/api/src/app/portfolio/portfolio.service.spec.ts
  3. 32
      apps/api/src/app/user/user.service.ts
  4. 14
      apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts
  5. 1
      apps/client/src/app/pages/portfolio/activities/activities-page.html
  6. 3
      libs/common/src/lib/interfaces/user.interface.ts
  7. 2
      libs/common/src/lib/types/user-with-settings.type.ts
  8. 7
      libs/ui/src/lib/activities-table/activities-table.component.html
  9. 1
      libs/ui/src/lib/activities-table/activities-table.component.stories.ts
  10. 26
      libs/ui/src/lib/activities-table/activities-table.component.ts
  11. 17
      prisma/migrations/20260822000000_updated_indexes_of_order/migration.sql
  12. 6
      prisma/schema.prisma

2
CHANGELOG.md

@ -9,9 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Improved the type filter of the activities table on the activities page to only list the activity types in use (experimental)
- Improved the permission selector with icons in the create or update access dialog - Improved the permission selector with icons in the create or update access dialog
- Extracted the access level icon to a reusable component - Extracted the access level icon to a reusable component
- Disabled the telemetry in the _Storybook_ setup - Disabled the telemetry in the _Storybook_ setup
- Improved the indexes of the order database table
- Upgraded the `Node.js` engine from version `>=22.18.0` to `>=22.22.3` (`package.json`) - Upgraded the `Node.js` engine from version `>=22.18.0` to `>=22.22.3` (`package.json`)
### Fixed ### Fixed

2
apps/api/src/app/portfolio/portfolio.service.spec.ts

@ -247,7 +247,7 @@ describe('PortfolioService', () => {
jest.spyOn(userService, 'user').mockResolvedValue({ jest.spyOn(userService, 'user').mockResolvedValue({
accounts: [], accounts: [],
activityCount: 0, activitiesCount: 0,
dataProviderGhostfolioDailyRequests: 0, dataProviderGhostfolioDailyRequests: 0,
id: userDummyData.id, id: userDummyData.id,
settings: { settings: {

32
apps/api/src/app/user/user.service.ts

@ -129,8 +129,7 @@ export class UserService {
const [ const [
access, access,
accounts, accounts,
activitiesCount, activitiesGroupedByType,
firstActivity,
impersonationUser, impersonationUser,
tagsForUser tagsForUser
] = await Promise.all([ ] = await Promise.all([
@ -150,13 +149,10 @@ export class UserService {
userId: impersonationUserId || user.id userId: impersonationUserId || user.id
} }
}), }),
this.prismaService.order.count({ this.prismaService.order.groupBy({
where: { userId: impersonationUserId || user.id } _min: { date: true },
}), by: ['type'],
this.prismaService.order.findFirst({ orderBy: { _min: { date: 'asc' } },
orderBy: {
date: 'asc'
},
where: { userId: impersonationUserId || user.id } where: { userId: impersonationUserId || user.id }
}), }),
impersonationUserId impersonationUserId
@ -165,6 +161,19 @@ export class UserService {
this.tagService.getTagsForUser(impersonationUserId || user.id) this.tagService.getTagsForUser(impersonationUserId || user.id)
]); ]);
const activitiesCount = impersonationUserId
? (impersonationUser?.activitiesCount ?? 0)
: (user.activitiesCount ?? 0);
const activityTypes = activitiesGroupedByType.map(({ type }) => {
return type;
});
// The groupBy is ordered by the minimum date, thus the first group
// carries the date of the first activity
const dateOfFirstActivity =
activitiesGroupedByType[0]?._min.date ?? new Date();
const resolvedUserSettings = resolveUserSettings({ const resolvedUserSettings = resolveUserSettings({
impersonationUserSettings: impersonationUser?.settings impersonationUserSettings: impersonationUser?.settings
?.settings as UserSettings, ?.settings as UserSettings,
@ -209,6 +218,8 @@ export class UserService {
return { return {
activitiesCount, activitiesCount,
activityTypes,
dateOfFirstActivity,
id, id,
permissions, permissions,
referralPartners, referralPartners,
@ -226,7 +237,6 @@ export class UserService {
accounts: accounts.sort((a, b) => { accounts: accounts.sort((a, b) => {
return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
}), }),
dateOfFirstActivity: firstActivity?.date ?? new Date(),
settings: { settings: {
...resolvedUserSettings, ...resolvedUserSettings,
baseCurrency: resolvedUserSettings.baseCurrency ?? DEFAULT_CURRENCY, baseCurrency: resolvedUserSettings.baseCurrency ?? DEFAULT_CURRENCY,
@ -324,6 +334,7 @@ export class UserService {
const user: UserWithSettings = { const user: UserWithSettings = {
accessToken, accessToken,
accounts, accounts,
activitiesCount,
authChallenge, authChallenge,
createdAt, createdAt,
id, id,
@ -332,7 +343,6 @@ export class UserService {
settings: settings as UserWithSettings['settings'], settings: settings as UserWithSettings['settings'],
thirdPartyId, thirdPartyId,
updatedAt, updatedAt,
activityCount: analytics?.activityCount,
dataProviderGhostfolioDailyRequests: dataProviderGhostfolioDailyRequests:
analytics?.dataProviderGhostfolioDailyRequests ?? 0 analytics?.dataProviderGhostfolioDailyRequests ?? 0
}; };

14
apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts

@ -649,11 +649,9 @@ export class GfHoldingDetailDialogComponent implements OnInit {
.postActivity(activity) .postActivity(activity)
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
this.router.navigate( this.userService.get(true).subscribe();
internalRoutes.portfolio.subRoutes.activities.routerLink
);
this.dialogRef.close(); this.navigateToActivities();
}); });
} }
@ -729,4 +727,12 @@ export class GfHoldingDetailDialogComponent implements OnInit {
{ id: this.data.symbol, type: 'SYMBOL' } { id: this.data.symbol, type: 'SYMBOL' }
]; ];
} }
private navigateToActivities() {
this.router.navigate(
internalRoutes.portfolio.subRoutes.activities.routerLink
);
this.dialogRef.close();
}
} }

1
apps/client/src/app/pages/portfolio/activities/activities-page.html

@ -3,6 +3,7 @@
<div class="col"> <div class="col">
<h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Activities</h1> <h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Activities</h1>
<gf-activities-table <gf-activities-table
[activityTypes]="user?.activityTypes"
[baseCurrency]="user?.settings?.baseCurrency" [baseCurrency]="user?.settings?.baseCurrency"
[dataSource]="dataSource" [dataSource]="dataSource"
[deviceType]="deviceType" [deviceType]="deviceType"

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

@ -1,7 +1,7 @@
import { SubscriptionType } from '@ghostfolio/common/enums'; import { SubscriptionType } from '@ghostfolio/common/enums';
import { AccountWithPlatform } from '@ghostfolio/common/types'; import { AccountWithPlatform } from '@ghostfolio/common/types';
import { Access, Tag } from '@prisma/client'; import { Access, Tag, Type as ActivityType } from '@prisma/client';
import { ReferralPartner } from './referral-partner.interface'; import { ReferralPartner } from './referral-partner.interface';
import { SubscriptionOffer } from './subscription-offer.interface'; import { SubscriptionOffer } from './subscription-offer.interface';
@ -13,6 +13,7 @@ export interface User {
access: Pick<Access, 'alias' | 'id' | 'scopes'>[]; access: Pick<Access, 'alias' | 'id' | 'scopes'>[];
accounts: AccountWithPlatform[]; accounts: AccountWithPlatform[];
activitiesCount: number; activitiesCount: number;
activityTypes: ActivityType[];
dateOfFirstActivity: Date; dateOfFirstActivity: Date;
id: string; id: string;
permissions: string[]; permissions: string[];

2
libs/common/src/lib/types/user-with-settings.type.ts

@ -6,7 +6,7 @@ import { Account, Settings, User } from '@prisma/client';
// TODO: Compare with User interface // TODO: Compare with User interface
export type UserWithSettings = User & { export type UserWithSettings = User & {
accounts: Account[]; accounts: Account[];
activityCount: number; activitiesCount: number;
dataProviderGhostfolioDailyRequests: number; dataProviderGhostfolioDailyRequests: number;
permissions?: string[]; permissions?: string[];
settings: Settings & { settings: UserSettings }; settings: Settings & { settings: UserSettings };

7
libs/ui/src/lib/activities-table/activities-table.component.html

@ -2,14 +2,11 @@
class="align-items-center d-flex justify-content-end justify-content-lg-between" class="align-items-center d-flex justify-content-end justify-content-lg-between"
> >
<div class="d-none d-lg-flex"> <div class="d-none d-lg-flex">
@if (hasPermissionToFilterByType) { @if (hasPermissionToFilterByType && activityTypeOptions().length > 1) {
<mat-form-field appearance="outline" class="without-hint"> <mat-form-field appearance="outline" class="without-hint">
<mat-label i18n>Type</mat-label> <mat-label i18n>Type</mat-label>
<mat-select multiple [formControl]="typesFilter"> <mat-select multiple [formControl]="typesFilter">
@for ( @for (activityType of activityTypeOptions(); track activityType.key) {
activityType of activityTypesTranslationMap | keyvalue: sortByValue;
track activityType.key
) {
<mat-option [value]="activityType.key"> <mat-option [value]="activityType.key">
{{ activityType.value }} {{ activityType.value }}
</mat-option> </mat-option>

1
libs/ui/src/lib/activities-table/activities-table.component.stories.ts

@ -483,6 +483,7 @@ export const Actions: Story = {
export const Toolbar: Story = { export const Toolbar: Story = {
args: { args: {
dataSource, dataSource,
activityTypes: ['BUY', 'DIVIDEND', 'SELL'],
baseCurrency: 'USD', baseCurrency: 'USD',
deviceType: 'desktop', deviceType: 'desktop',
hasActivities: true, hasActivities: true,

26
libs/ui/src/lib/activities-table/activities-table.component.ts

@ -141,7 +141,6 @@ export class GfActivitiesTableComponent implements AfterViewInit, OnInit {
@ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort; @ViewChild(MatSort) sort: MatSort;
public activityTypesTranslationMap = new Map<ActivityType, string>();
public hasDrafts = false; public hasDrafts = false;
public hasErrors = false; public hasErrors = false;
public isDraftActivity = isDraftActivity; public isDraftActivity = isDraftActivity;
@ -149,6 +148,7 @@ export class GfActivitiesTableComponent implements AfterViewInit, OnInit {
public selectedRows = new SelectionModel<Activity>(true, []); public selectedRows = new SelectionModel<Activity>(true, []);
public typesFilter = new FormControl<string[]>([]); public typesFilter = new FormControl<string[]>([]);
public readonly activityTypes = input<ActivityType[]>([]);
public readonly dataSource = input.required< public readonly dataSource = input.required<
MatTableDataSource<Activity> | undefined MatTableDataSource<Activity> | undefined
>(); >();
@ -175,6 +175,16 @@ export class GfActivitiesTableComponent implements AfterViewInit, OnInit {
return routerLinks; return routerLinks;
}); });
protected readonly activityTypeOptions = computed(() => {
return (this.activityTypes() ?? [])
.map((activityType) => {
return { key: activityType, value: translate(activityType) };
})
.sort((a, b) => {
return a.value.localeCompare(b.value);
});
});
protected readonly displayedColumns = computed(() => { protected readonly displayedColumns = computed(() => {
let columns = [ let columns = [
'select', 'select',
@ -222,13 +232,6 @@ export class GfActivitiesTableComponent implements AfterViewInit, OnInit {
private readonly notificationService = inject(NotificationService); private readonly notificationService = inject(NotificationService);
public constructor(private destroyRef: DestroyRef) { public constructor(private destroyRef: DestroyRef) {
for (const type of Object.keys(ActivityType) as ActivityType[]) {
this.activityTypesTranslationMap.set(
ActivityType[type],
translate(ActivityType[type])
);
}
addIcons({ addIcons({
alertCircleOutline, alertCircleOutline,
calendarClearOutline, calendarClearOutline,
@ -386,13 +389,6 @@ export class GfActivitiesTableComponent implements AfterViewInit, OnInit {
}); });
} }
public sortByValue(
a: { key: ActivityType; value: string },
b: { key: ActivityType; value: string }
) {
return a.value.localeCompare(b.value);
}
public toggleAllRows() { public toggleAllRows() {
if (this.areAllRowsSelected()) { if (this.areAllRowsSelected()) {
this.selectedRows.clear(); this.selectedRows.clear();

17
prisma/migrations/20260822000000_updated_indexes_of_order/migration.sql

@ -0,0 +1,17 @@
-- DropIndex
DROP INDEX "Order_symbolProfileId_idx";
-- DropIndex
DROP INDEX "Order_type_idx";
-- DropIndex
DROP INDEX "Order_userId_idx";
-- CreateIndex
CREATE INDEX "Order_symbolProfileId_date_idx" ON "Order"("symbolProfileId", "date");
-- CreateIndex
CREATE INDEX "Order_userId_date_idx" ON "Order"("userId", "date");
-- CreateIndex
CREATE INDEX "Order_userId_type_date_idx" ON "Order"("userId", "type", "date");

6
prisma/schema.prisma

@ -195,9 +195,9 @@ model Order {
@@index([accountId]) @@index([accountId])
@@index([date]) @@index([date])
@@index([symbolProfileId]) @@index([symbolProfileId, date])
@@index([type]) @@index([userId, date])
@@index([userId]) @@index([userId, type, date])
} }
model Platform { model Platform {

Loading…
Cancel
Save