Browse Source

Merge remote-tracking branch 'origin/main' into feature/enable-strict-null-checks-in-ui

pull/6264/head
Kenrick Tandrian 3 weeks ago
parent
commit
16a3e23a76
  1. 6
      CHANGELOG.md
  2. 2
      apps/client/src/styles.scss
  3. 98
      libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts
  4. 57
      libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts
  5. 8
      package-lock.json
  6. 2
      package.json

6
CHANGELOG.md

@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Changed
- Upgraded `svgmap` from version `2.14.0` to `2.19.2`
## 2.249.0 - 2026-03-10 ## 2.249.0 - 2026-03-10
### Added ### Added

2
apps/client/src/styles.scss

@ -2,7 +2,7 @@
@import './styles/table'; @import './styles/table';
@import './styles/variables'; @import './styles/variables';
@import 'svgmap/dist/svgMap'; @import 'svgmap/style.min';
:root { :root {
--dark-background: rgb(25, 25, 25); --dark-background: rgb(25, 25, 25);

98
libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts

@ -13,12 +13,11 @@ import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
ElementRef, ElementRef,
EventEmitter,
Input, Input,
OnChanges, OnChanges,
OnDestroy, OnDestroy,
Output, output,
ViewChild viewChild
} from '@angular/core'; } from '@angular/core';
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
import { Big } from 'big.js'; import { Big } from 'big.js';
@ -81,15 +80,16 @@ export class GfPortfolioProportionChartComponent
@Input() maxItems?: number; @Input() maxItems?: number;
@Input() showLabels = false; @Input() showLabels = false;
@Output() proportionChartClicked = new EventEmitter<AssetProfileIdentifier>();
@ViewChild('chartCanvas') chartCanvas: ElementRef<HTMLCanvasElement>;
public chart: Chart<'doughnut'>; public chart: Chart<'doughnut'>;
public isLoading = true; public isLoading = true;
protected readonly proportionChartClicked = output<AssetProfileIdentifier>();
private readonly OTHER_KEY = 'OTHER'; private readonly OTHER_KEY = 'OTHER';
private readonly chartCanvas =
viewChild.required<ElementRef<HTMLCanvasElement>>('chartCanvas');
private colorMap: { private colorMap: {
[symbol: string]: string; [symbol: string]: string;
} = {}; } = {};
@ -130,47 +130,45 @@ export class GfPortfolioProportionChartComponent
}; };
if (this.keys.length > 0) { if (this.keys.length > 0) {
const primaryKey = this.keys[0];
const secondaryKey = this.keys[1];
Object.keys(this.data).forEach((symbol) => { Object.keys(this.data).forEach((symbol) => {
if (this.data[symbol][this.keys[0]]?.toUpperCase()) { const asset = this.data[symbol];
if (chartData[this.data[symbol][this.keys[0]].toUpperCase()]) { const assetValue = asset.value || 0;
chartData[this.data[symbol][this.keys[0]].toUpperCase()].value = const primaryKeyValue = (asset[primaryKey] as string)?.toUpperCase();
chartData[ const secondaryKeyValue = asset[secondaryKey] as string;
this.data[symbol][this.keys[0]].toUpperCase()
].value.plus(this.data[symbol].value || 0); if (primaryKeyValue) {
if (chartData[primaryKeyValue]) {
if ( chartData[primaryKeyValue].value =
chartData[this.data[symbol][this.keys[0]].toUpperCase()] chartData[primaryKeyValue].value.plus(assetValue);
?.subCategory?.[this.data[symbol][this.keys[1]]]
) { const targetSubCategory =
// @ts-ignore chartData[primaryKeyValue].subCategory?.[secondaryKeyValue];
chartData[ if (targetSubCategory) {
this.data[symbol][this.keys[0]].toUpperCase() targetSubCategory.value =
].subCategory[this.data[symbol][this.keys[1]]].value = chartData[ targetSubCategory.value.plus(assetValue);
this.data[symbol][this.keys[0]].toUpperCase()
].subCategory?.[this.data[symbol][this.keys[1]]].value.plus(
this.data[symbol].value || 0
);
} else { } else {
// @ts-ignore if (chartData[primaryKeyValue].subCategory) {
chartData[ chartData[primaryKeyValue].subCategory[
this.data[symbol][this.keys[0]].toUpperCase() secondaryKeyValue ?? UNKNOWN_KEY
].subCategory[this.data[symbol][this.keys[1]] ?? UNKNOWN_KEY] = { ] = {
value: new Big(this.data[symbol].value || 0) value: new Big(assetValue)
}; };
}
} }
} else { } else {
chartData[this.data[symbol][this.keys[0]].toUpperCase()] = { chartData[primaryKeyValue] = {
name: this.data[symbol][this.keys[0]], name: asset[primaryKey] as string,
subCategory: {}, subCategory: {},
value: new Big(this.data[symbol].value || 0) value: new Big(assetValue)
}; };
if (this.data[symbol][this.keys[1]]) { if (secondaryKeyValue) {
chartData[ chartData[primaryKeyValue].subCategory = {
this.data[symbol][this.keys[0]].toUpperCase() [secondaryKeyValue]: {
].subCategory = { value: new Big(assetValue)
[this.data[symbol][this.keys[1]]]: {
value: new Big(this.data[symbol].value || 0)
} }
}; };
} }
@ -183,10 +181,10 @@ export class GfPortfolioProportionChartComponent
} else { } else {
chartData[UNKNOWN_KEY] = { chartData[UNKNOWN_KEY] = {
name: this.data[symbol].name, name: this.data[symbol].name,
subCategory: this.keys[1] subCategory: secondaryKey
? { [this.keys[1]]: { value: new Big(0) } } ? { [secondaryKey]: { value: new Big(0) } }
: undefined, : undefined,
value: new Big(this.data[symbol].value || 0) value: new Big(assetValue)
}; };
} }
} }
@ -280,15 +278,15 @@ export class GfPortfolioProportionChartComponent
Object.keys(item.subCategory ?? {}).forEach((subCategory) => { Object.keys(item.subCategory ?? {}).forEach((subCategory) => {
if (item.name === UNKNOWN_KEY) { if (item.name === UNKNOWN_KEY) {
// @ts-ignore backgroundColorSubCategory.push(item.color ?? '');
backgroundColorSubCategory.push(item.color);
} else { } else {
backgroundColorSubCategory.push( backgroundColorSubCategory.push(
Color(item.color).lighten(lightnessRatio).hex() Color(item.color).lighten(lightnessRatio).hex()
); );
} }
// @ts-ignore dataSubCategory.push(
dataSubCategory.push(item.subCategory[subCategory].value.toNumber()); item.subCategory?.[subCategory].value.toNumber() ?? 0
);
labelSubCategory.push(subCategory); labelSubCategory.push(subCategory);
lightnessRatio += 0.1; lightnessRatio += 0.1;
@ -338,7 +336,7 @@ export class GfPortfolioProportionChartComponent
labels labels
}; };
if (this.chartCanvas) { if (this.chartCanvas()) {
if (this.chart) { if (this.chart) {
this.chart.data = data; this.chart.data = data;
this.chart.options.plugins ??= {}; this.chart.options.plugins ??= {};
@ -347,7 +345,7 @@ export class GfPortfolioProportionChartComponent
this.chart.update(); this.chart.update();
} else { } else {
this.chart = new Chart<'doughnut'>(this.chartCanvas.nativeElement, { this.chart = new Chart<'doughnut'>(this.chartCanvas().nativeElement, {
data, data,
options: { options: {
animation: false, animation: false,

57
libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts

@ -8,15 +8,17 @@ import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
DestroyRef,
DoCheck, DoCheck,
ElementRef, ElementRef,
Input, Input,
OnChanges, OnChanges,
OnDestroy,
OnInit, OnInit,
SimpleChanges, SimpleChanges,
ViewChild inject,
viewChild
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { import {
FormControl, FormControl,
FormsModule, FormsModule,
@ -35,13 +37,12 @@ import {
import { MatInput, MatInputModule } from '@angular/material/input'; import { MatInput, MatInputModule } from '@angular/material/input';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { isString } from 'lodash'; import { isString } from 'lodash';
import { Subject, tap } from 'rxjs'; import { tap } from 'rxjs';
import { import {
debounceTime, debounceTime,
distinctUntilChanged, distinctUntilChanged,
filter, filter,
switchMap, switchMap
takeUntil
} from 'rxjs/operators'; } from 'rxjs/operators';
import { translate } from '../i18n'; import { translate } from '../i18n';
@ -77,21 +78,21 @@ import { AbstractMatFormField } from '../shared/abstract-mat-form-field';
}) })
export class GfSymbolAutocompleteComponent export class GfSymbolAutocompleteComponent
extends AbstractMatFormField<LookupItem> extends AbstractMatFormField<LookupItem>
implements DoCheck, OnChanges, OnDestroy, OnInit implements DoCheck, OnChanges, OnInit
{ {
@Input() public defaultLookupItems: LookupItem[] = []; @Input() public defaultLookupItems: LookupItem[] = [];
@Input() public isLoading = false; @Input() public isLoading = false;
@ViewChild('symbolAutocomplete') public symbolAutocomplete: MatAutocomplete;
@Input() private includeIndices = false; @Input() private includeIndices = false;
@ViewChild(MatInput) private input: MatInput; public readonly control = new FormControl();
public control = new FormControl();
public lookupItems: (LookupItem & { assetSubClassString: string })[] = []; public lookupItems: (LookupItem & { assetSubClassString: string })[] = [];
private unsubscribeSubject = new Subject<void>(); protected readonly symbolAutocomplete =
viewChild.required<MatAutocomplete>('symbolAutocomplete');
private readonly destroyRef = inject(DestroyRef);
private readonly input = viewChild.required(MatInput);
public constructor( public constructor(
public readonly _elementRef: ElementRef, public readonly _elementRef: ElementRef,
@ -105,13 +106,22 @@ export class GfSymbolAutocompleteComponent
this.controlType = 'symbol-autocomplete'; this.controlType = 'symbol-autocomplete';
} }
public get empty() {
return this.input().empty;
}
public set value(value: LookupItem) {
this.control.setValue(value);
super.value = value;
}
public ngOnInit() { public ngOnInit() {
if (this.disabled) { if (this.disabled) {
this.control.disable(); this.control.disable();
} }
this.control.valueChanges this.control.valueChanges
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
if (super.value) { if (super.value) {
super.value.dataSource = null; super.value.dataSource = null;
@ -136,7 +146,7 @@ export class GfSymbolAutocompleteComponent
}), }),
debounceTime(400), debounceTime(400),
distinctUntilChanged(), distinctUntilChanged(),
takeUntil(this.unsubscribeSubject), takeUntilDestroyed(this.destroyRef),
switchMap((query: string) => { switchMap((query: string) => {
return this.dataService.fetchSymbols({ return this.dataService.fetchSymbols({
query, query,
@ -168,12 +178,8 @@ export class GfSymbolAutocompleteComponent
return aLookupItem?.symbol ?? ''; return aLookupItem?.symbol ?? '';
} }
public get empty() {
return this.input?.empty;
}
public focus() { public focus() {
this.input.focus(); this.input().focus();
} }
public isValueInOptions(value: string) { public isValueInOptions(value: string) {
@ -197,18 +203,6 @@ export class GfSymbolAutocompleteComponent
} as LookupItem; } as LookupItem;
} }
public set value(value: LookupItem) {
this.control.setValue(value);
super.value = value;
}
public ngOnDestroy() {
super.ngOnDestroy();
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private showDefaultOptions() { private showDefaultOptions() {
this.lookupItems = this.defaultLookupItems.map((lookupItem) => { this.lookupItems = this.defaultLookupItems.map((lookupItem) => {
return { return {
@ -224,6 +218,7 @@ export class GfSymbolAutocompleteComponent
const requiredCheck = super.required const requiredCheck = super.required
? !super.value?.dataSource || !super.value?.symbol ? !super.value?.dataSource || !super.value?.symbol
: false; : false;
if (requiredCheck) { if (requiredCheck) {
this.ngControl.control?.setErrors({ invalidData: true }); this.ngControl.control?.setErrors({ invalidData: true });
} }

8
package-lock.json

@ -90,7 +90,7 @@
"reflect-metadata": "0.2.2", "reflect-metadata": "0.2.2",
"rxjs": "7.8.1", "rxjs": "7.8.1",
"stripe": "20.3.0", "stripe": "20.3.0",
"svgmap": "2.14.0", "svgmap": "2.19.2",
"tablemark": "4.1.0", "tablemark": "4.1.0",
"twitter-api-v2": "1.29.0", "twitter-api-v2": "1.29.0",
"yahoo-finance2": "3.13.2", "yahoo-finance2": "3.13.2",
@ -32339,9 +32339,9 @@
"license": "BSD-2-Clause" "license": "BSD-2-Clause"
}, },
"node_modules/svgmap": { "node_modules/svgmap": {
"version": "2.14.0", "version": "2.19.2",
"resolved": "https://registry.npmjs.org/svgmap/-/svgmap-2.14.0.tgz", "resolved": "https://registry.npmjs.org/svgmap/-/svgmap-2.19.2.tgz",
"integrity": "sha512-+Vklx4DO1uv1SFq6wnJWl/dRjX4uRT9CcsIHuADxAcZ+h5X1OSyDVbNdIu837fx5TtYYuaGRhWuFCXIioN/1ww==", "integrity": "sha512-mRqRcQiwwSTh9kTOPhjTmd3ywxA9aTfybBHGAoyuGn9CI9PnAQsuZ7H/2/VEIvgJhi1xM5IGBfk8i4/Ke4iTCQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"svg-pan-zoom": "^3.6.2" "svg-pan-zoom": "^3.6.2"

2
package.json

@ -135,7 +135,7 @@
"reflect-metadata": "0.2.2", "reflect-metadata": "0.2.2",
"rxjs": "7.8.1", "rxjs": "7.8.1",
"stripe": "20.3.0", "stripe": "20.3.0",
"svgmap": "2.14.0", "svgmap": "2.19.2",
"tablemark": "4.1.0", "tablemark": "4.1.0",
"twitter-api-v2": "1.29.0", "twitter-api-v2": "1.29.0",
"yahoo-finance2": "3.13.2", "yahoo-finance2": "3.13.2",

Loading…
Cancel
Save