diff --git a/CHANGELOG.md b/CHANGELOG.md index 297677b966..5de317da97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Improved the holdings table of the home overview by adding the sorting by name and performance +- Changed the default sorting of the holdings table to the performance in descending order +- Persisted the sorting of the holdings and accounts tables on the device - Improved the market price in the benchmark detail dialog by adding the currency and reducing the precision on mobile - Improved the style of the close button in the dialog footer component - Improved the style of the close button in the dialog header component diff --git a/libs/ui/src/lib/accounts-table/accounts-table.component.html b/libs/ui/src/lib/accounts-table/accounts-table.component.html index 43a6a429b4..00b544bf4e 100644 --- a/libs/ui/src/lib/accounts-table/accounts-table.component.html +++ b/libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -20,6 +20,7 @@ matSortActive="name" matSortDirection="asc" [dataSource]="dataSource" + [gfPersistSort]="'accounts'" > @@ -210,11 +211,7 @@ - + { - return this.mode() === 'default' - ? 'allocationInPercentage' - : 'assetProfile.name'; - }); - - protected readonly sortDirection = computed(() => { - return this.mode() === 'default' ? 'desc' : 'asc'; - }); + protected readonly sortActive = 'netPerformancePercentWithCurrencyEffect'; + protected readonly sortDirection: SortDirection = 'desc'; public constructor() { this.dataSource.sortingDataAccessor = (holding, path) => { diff --git a/libs/ui/src/lib/persist-sort/index.ts b/libs/ui/src/lib/persist-sort/index.ts new file mode 100644 index 0000000000..22d5db3391 --- /dev/null +++ b/libs/ui/src/lib/persist-sort/index.ts @@ -0,0 +1 @@ +export * from './persist-sort.directive'; diff --git a/libs/ui/src/lib/persist-sort/persist-sort.directive.spec.ts b/libs/ui/src/lib/persist-sort/persist-sort.directive.spec.ts new file mode 100644 index 0000000000..7fa7a7f811 --- /dev/null +++ b/libs/ui/src/lib/persist-sort/persist-sort.directive.spec.ts @@ -0,0 +1,70 @@ +import { Component } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatSort, MatSortModule } from '@angular/material/sort'; + +import { GfPersistSortDirective } from './persist-sort.directive'; + +@Component({ + imports: [GfPersistSortDirective, MatSortModule], + template: `
` +}) +class TestHostComponent {} + +describe('GfPersistSortDirective', () => { + let fixture: ComponentFixture; + + const create = () => { + fixture = TestBed.createComponent(TestHostComponent); + fixture.detectChanges(); + + return fixture.debugElement.children[0].injector.get(MatSort); + }; + + beforeEach(() => { + localStorage.clear(); + TestBed.configureTestingModule({ imports: [TestHostComponent] }); + }); + + it('keeps the default sorting without a stored value', () => { + const sort = create(); + + expect(sort.active).toBe('name'); + expect(sort.direction).toBe('asc'); + }); + + it('stores the sorting on change', () => { + const sort = create(); + + sort.sort({ id: 'value', start: 'desc', disableClear: false }); + + expect(JSON.parse(localStorage.getItem('gf-sort-test') ?? '{}')).toEqual({ + active: 'value', + direction: 'desc' + }); + }); + + it('restores a stored sorting', () => { + localStorage.setItem( + 'gf-sort-test', + JSON.stringify({ active: 'value', direction: 'desc' }) + ); + + const sort = create(); + + expect(sort.active).toBe('value'); + expect(sort.direction).toBe('desc'); + }); + + it('ignores an invalid stored sorting', () => { + localStorage.setItem('gf-sort-test', '{"active":"value","direction":"x"}'); + + const sort = create(); + + expect(sort.active).toBe('name'); + }); +}); diff --git a/libs/ui/src/lib/persist-sort/persist-sort.directive.ts b/libs/ui/src/lib/persist-sort/persist-sort.directive.ts new file mode 100644 index 0000000000..d7e70f81ae --- /dev/null +++ b/libs/ui/src/lib/persist-sort/persist-sort.directive.ts @@ -0,0 +1,57 @@ +import { DestroyRef, Directive, inject, input, OnInit } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { MatSort, SortDirection } from '@angular/material/sort'; + +const STORAGE_KEY_PREFIX = 'gf-sort-'; + +/** + * Remembers the sorting of a `matSort` table on the device and restores it + * on the next visit. + */ +@Directive({ + selector: '[gfPersistSort]' +}) +export class GfPersistSortDirective implements OnInit { + public readonly gfPersistSort = input.required(); + + private readonly destroyRef = inject(DestroyRef); + private readonly matSort = inject(MatSort, { self: true }); + + public ngOnInit() { + const storageKey = `${STORAGE_KEY_PREFIX}${this.gfPersistSort()}`; + const storedSort = this.readStoredSort(storageKey); + + if (storedSort) { + this.matSort.active = storedSort.active; + this.matSort.direction = storedSort.direction; + } + + this.matSort.sortChange + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(({ active, direction }) => { + try { + globalThis.localStorage?.setItem( + storageKey, + JSON.stringify({ active, direction }) + ); + } catch {} + }); + } + + private readStoredSort(aStorageKey: string) { + try { + const item = globalThis.localStorage?.getItem(aStorageKey); + const { active, direction } = JSON.parse(item ?? 'null') ?? {}; + + if ( + typeof active === 'string' && + active.length > 0 && + (direction === 'asc' || direction === 'desc') + ) { + return { active, direction: direction as SortDirection }; + } + } catch {} + + return undefined; + } +}