Browse Source

Task/improve Angular template type safety in client (#7762)

* feat(client): resolve angular type errors

* feat(libs): resolve angular type errors

* feat(client): enable strict input access modifiers

* fix(client): set strict templates to false

* fix(client): resolve type errors

* feat(client): enable strict literal types

* feat(client): lock down other strict rules to prevent regression

* feat(client): enable strict null input types
pull/1685/merge
Kenrick Tandrian 1 week ago
committed by GitHub
parent
commit
f89a9f28fb
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
  2. 2
      apps/client/src/app/components/header/header.component.ts
  3. 5
      apps/client/src/app/components/home-holdings/home-holdings.component.ts
  4. 20
      apps/client/src/app/components/investment-chart/investment-chart.component.ts
  5. 3
      apps/client/src/app/components/markets/markets.component.ts
  6. 1
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html
  7. 17
      apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts
  8. 2
      apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
  9. 2
      apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.html
  10. 12
      apps/client/tsconfig.json
  11. 6
      libs/common/src/lib/types/toggle-option.type.ts
  12. 1
      libs/ui/src/lib/portfolio-proportion-chart/index.ts
  13. 2
      libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts
  14. 11
      libs/ui/src/lib/toggle/toggle.component.ts

1
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html

@ -660,7 +660,6 @@
formControlName="headers" formControlName="headers"
matInput matInput
type="text" type="text"
[matAutocomplete]="auto"
></textarea> ></textarea>
</mat-form-field> </mat-form-field>
</div> </div>

2
apps/client/src/app/components/header/header.component.ts

@ -220,7 +220,7 @@ export class GfHeaderComponent implements OnChanges {
this.assistentMenuTriggerElement().closeMenu(); this.assistentMenuTriggerElement().closeMenu();
} }
protected impersonateAccount(aId: string) { protected impersonateAccount(aId: string | null) {
if (aId) { if (aId) {
this.impersonationStorageService.setId(aId); this.impersonationStorageService.setId(aId);
} else { } else {

5
apps/client/src/app/components/home-holdings/home-holdings.component.ts

@ -56,7 +56,8 @@ export class GfHomeHoldingsComponent implements OnInit {
protected holdings: PortfolioPosition[] | undefined; protected holdings: PortfolioPosition[] | undefined;
protected holdingsViewMode: HoldingsViewMode = protected holdingsViewMode: HoldingsViewMode =
GfHomeHoldingsComponent.DEFAULT_HOLDINGS_VIEW_MODE; GfHomeHoldingsComponent.DEFAULT_HOLDINGS_VIEW_MODE;
protected readonly holdingsViewModeOptions: ToggleOption[] = [ protected readonly holdingsViewModeOptions: ToggleOption<HoldingsViewMode>[] =
[
{ {
iconName: 'reorder-four-outline', iconName: 'reorder-four-outline',
title: $localize`Table`, title: $localize`Table`,
@ -69,7 +70,7 @@ export class GfHomeHoldingsComponent implements OnInit {
} }
]; ];
protected holdingType: HoldingType = 'ACTIVE'; protected holdingType: HoldingType = 'ACTIVE';
protected readonly holdingTypeOptions: ToggleOption[] = [ protected readonly holdingTypeOptions: ToggleOption<HoldingType>[] = [
{ label: $localize`Active`, value: 'ACTIVE' }, { label: $localize`Active`, value: 'ACTIVE' },
{ label: $localize`Closed`, value: 'CLOSED' } { label: $localize`Closed`, value: 'CLOSED' }
]; ];

20
apps/client/src/app/components/investment-chart/investment-chart.component.ts

@ -53,16 +53,16 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
templateUrl: './investment-chart.component.html' templateUrl: './investment-chart.component.html'
}) })
export class GfInvestmentChartComponent implements OnChanges, OnDestroy { export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
@Input() public readonly benchmarkDataItems: InvestmentItem[] = []; @Input() public benchmarkDataItems: InvestmentItem[] = [];
@Input() public readonly benchmarkDataLabel = ''; @Input() public benchmarkDataLabel = '';
@Input() public readonly colorScheme: ColorScheme; @Input() public colorScheme: ColorScheme;
@Input() public readonly currency: string; @Input() public currency: string;
@Input() public readonly groupBy: GroupBy; @Input() public groupBy: GroupBy;
@Input() public readonly historicalDataItems: LineChartItem[] = []; @Input() public historicalDataItems: LineChartItem[] = [];
@Input() public readonly isInPercentage = false; @Input() public isInPercentage = false;
@Input() public readonly isLoading = false; @Input() public isLoading = false;
@Input() public readonly locale = getLocale(); @Input() public locale = getLocale();
@Input() public readonly savingsRate = 0; @Input() public savingsRate = 0;
private readonly chartCanvas = private readonly chartCanvas =
viewChild.required<ElementRef<HTMLCanvasElement>>('chartCanvas'); viewChild.required<ElementRef<HTMLCanvasElement>>('chartCanvas');

3
apps/client/src/app/components/markets/markets.component.ts

@ -49,7 +49,8 @@ export class GfMarketsComponent implements OnInit {
() => this.deviceDetectorService.deviceInfo().deviceType () => this.deviceDetectorService.deviceInfo().deviceType
); );
protected readonly fearAndGreedIndexModeOptions: ToggleOption[] = [ protected readonly fearAndGreedIndexModeOptions: ToggleOption<FearAndGreedIndexMode>[] =
[
{ label: $localize`Stocks`, value: 'STOCKS' }, { label: $localize`Stocks`, value: 'STOCKS' },
{ label: $localize`Cryptocurrencies`, value: 'CRYPTOCURRENCIES' } { label: $localize`Cryptocurrencies`, value: 'CRYPTOCURRENCIES' }
]; ];

1
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html

@ -210,6 +210,7 @@
currencyOfAssetProfile === currencyOfAssetProfile ===
activityForm.get('currencyOfUnitPrice')?.value && activityForm.get('currencyOfUnitPrice')?.value &&
currentMarketPrice && currentMarketPrice &&
data.activity.type &&
['BUY', 'SELL'].includes(data.activity.type) && ['BUY', 'SELL'].includes(data.activity.type) &&
isToday(activityForm.get('date')?.value) isToday(activityForm.get('date')?.value)
) { ) {

17
apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts

@ -12,7 +12,6 @@ import {
getCountryName getCountryName
} from '@ghostfolio/common/helper'; } from '@ghostfolio/common/helper';
import { import {
AssetProfileIdentifier,
HoldingWithParents, HoldingWithParents,
PortfolioDetails, PortfolioDetails,
PortfolioPosition, PortfolioPosition,
@ -22,7 +21,10 @@ import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { hasScope, scopes } from '@ghostfolio/common/scopes'; import { hasScope, scopes } from '@ghostfolio/common/scopes';
import { MarketAdvanced } from '@ghostfolio/common/types'; import { MarketAdvanced } from '@ghostfolio/common/types';
import { translate } from '@ghostfolio/ui/i18n'; import { translate } from '@ghostfolio/ui/i18n';
import { GfPortfolioProportionChartComponent } from '@ghostfolio/ui/portfolio-proportion-chart'; import {
GfPortfolioProportionChartComponent,
PortfolioProportionChartClickEvent
} from '@ghostfolio/ui/portfolio-proportion-chart';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
import { DataService } from '@ghostfolio/ui/services'; import { DataService } from '@ghostfolio/ui/services';
import { GfTopHoldingsComponent } from '@ghostfolio/ui/top-holdings'; import { GfTopHoldingsComponent } from '@ghostfolio/ui/top-holdings';
@ -206,7 +208,9 @@ export class GfAllocationsPageComponent implements OnInit {
this.initialize(); this.initialize();
} }
protected onAccountChartClicked({ accountId }: { accountId: string }) { protected onAccountChartClicked(event: PortfolioProportionChartClickEvent) {
const accountId = 'accountId' in event ? event.accountId : undefined;
if (accountId && accountId !== UNKNOWN_KEY) { if (accountId && accountId !== UNKNOWN_KEY) {
void this.router.navigate([], { void this.router.navigate([], {
queryParams: { accountId, accountDetailDialog: true } queryParams: { accountId, accountDetailDialog: true }
@ -214,10 +218,9 @@ export class GfAllocationsPageComponent implements OnInit {
} }
} }
protected onSymbolChartClicked({ protected onSymbolChartClicked(event: PortfolioProportionChartClickEvent) {
dataSource, const { dataSource, symbol } = 'symbol' in event ? event : {};
symbol
}: AssetProfileIdentifier) {
if (dataSource && symbol) { if (dataSource && symbol) {
void this.router.navigate([], { void this.router.navigate([], {
queryParams: { dataSource, symbol, holdingDetailDialog: true } queryParams: { dataSource, symbol, holdingDetailDialog: true }

2
apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts

@ -94,7 +94,7 @@ export class GfAnalysisPageComponent implements OnInit {
protected isLoadingInvestmentTimelineChart: boolean; protected isLoadingInvestmentTimelineChart: boolean;
protected isLoadingPortfolioPrompt: boolean; protected isLoadingPortfolioPrompt: boolean;
protected readonly mode = signal<GroupBy>('month'); protected readonly mode = signal<GroupBy>('month');
protected readonly modeOptions: ToggleOption[] = [ protected readonly modeOptions: ToggleOption<GroupBy>[] = [
{ label: $localize`Monthly`, value: 'month' }, { label: $localize`Monthly`, value: 'month' },
{ label: $localize`Yearly`, value: 'year' } { label: $localize`Yearly`, value: 'year' }
]; ];

2
apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.html

@ -75,7 +75,7 @@
matInput matInput
readonly readonly
type="text" type="text"
[(value)]="accessToken" [value]="accessToken"
></textarea> ></textarea>
<div class="float-right mt-1"> <div class="float-right mt-1">
<button <button

12
apps/client/tsconfig.json

@ -15,8 +15,18 @@
], ],
"angularCompilerOptions": { "angularCompilerOptions": {
"strictInjectionParameters": true, "strictInjectionParameters": true,
// TODO: Remove these subsets once strictTemplates is enabled
"strictAttributeTypes": true,
"strictContextGenerics": true,
"strictDomEventTypes": true,
"strictDomLocalRefTypes": true,
"strictInputAccessModifiers": true,
"strictInputTypes": false,
"strictLiteralTypes": true,
"strictNullInputTypes": true,
"strictOutputEventTypes": true,
"strictSafeNavigationTypes": false,
// TODO: Enable stricter rules for this project // TODO: Enable stricter rules for this project
"strictInputAccessModifiers": false,
"strictTemplates": false "strictTemplates": false
}, },
"compilerOptions": { "compilerOptions": {

6
libs/common/src/lib/types/toggle-option.type.ts

@ -1,8 +1,8 @@
interface BaseToggleOption { interface BaseToggleOption<T extends string = string> {
value: string; value: T;
} }
export type ToggleOption = BaseToggleOption & export type ToggleOption<T extends string = string> = BaseToggleOption<T> &
( (
| { iconName: string; label?: never; title: string } | { iconName: string; label?: never; title: string }
| { iconName?: never; label: string; title?: never } | { iconName?: never; label: string; title?: never }

1
libs/ui/src/lib/portfolio-proportion-chart/index.ts

@ -1 +1,2 @@
export * from './interfaces/interfaces';
export * from './portfolio-proportion-chart.component'; export * from './portfolio-proportion-chart.component';

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

@ -82,7 +82,7 @@ export class GfSymbolAutocompleteComponent
@Input() public defaultLookupItems: LookupItem[] = []; @Input() public defaultLookupItems: LookupItem[] = [];
@Input() public isLoading = false; @Input() public isLoading = false;
@Input() private includeIndices = false; @Input() public includeIndices = false;
public readonly control = new FormControl(); public readonly control = new FormControl();
public lookupItems: (LookupItem & { assetSubClassString: string })[] = []; public lookupItems: (LookupItem & { assetSubClassString: string })[] = [];

11
libs/ui/src/lib/toggle/toggle.component.ts

@ -18,14 +18,15 @@ import { IonIcon } from '@ionic/angular/standalone';
styleUrls: ['./toggle.component.scss'], styleUrls: ['./toggle.component.scss'],
templateUrl: './toggle.component.html' templateUrl: './toggle.component.html'
}) })
export class GfToggleComponent { export class GfToggleComponent<T extends string = string> {
public readonly defaultValue = input.required<string>(); public readonly defaultValue = input.required<T>();
public readonly isDisabled = input<boolean>(false); public readonly isDisabled = input<boolean>(false);
public readonly isLoading = input<boolean>(false); public readonly isLoading = input<boolean>(false);
public readonly options = input<ToggleOption[]>([]); public readonly options = input<ToggleOption<T>[]>([]);
protected readonly optionFormControl = new FormControl<string | null>(null); public readonly valueChange = output<Pick<ToggleOption<T>, 'value'>>();
protected readonly valueChange = output<Pick<ToggleOption, 'value'>>();
protected readonly optionFormControl = new FormControl<T | null>(null);
public constructor() { public constructor() {
effect(() => { effect(() => {

Loading…
Cancel
Save