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. 27
      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. 9
      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"
matInput
type="text"
[matAutocomplete]="auto"
></textarea>
</mat-form-field>
</div>

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

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

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

@ -56,20 +56,21 @@ export class GfHomeHoldingsComponent implements OnInit {
protected holdings: PortfolioPosition[] | undefined;
protected holdingsViewMode: HoldingsViewMode =
GfHomeHoldingsComponent.DEFAULT_HOLDINGS_VIEW_MODE;
protected readonly holdingsViewModeOptions: ToggleOption[] = [
{
iconName: 'reorder-four-outline',
title: $localize`Table`,
value: 'TABLE'
},
{
iconName: 'grid-outline',
title: $localize`Chart`,
value: 'CHART'
}
];
protected readonly holdingsViewModeOptions: ToggleOption<HoldingsViewMode>[] =
[
{
iconName: 'reorder-four-outline',
title: $localize`Table`,
value: 'TABLE'
},
{
iconName: 'grid-outline',
title: $localize`Chart`,
value: 'CHART'
}
];
protected holdingType: HoldingType = 'ACTIVE';
protected readonly holdingTypeOptions: ToggleOption[] = [
protected readonly holdingTypeOptions: ToggleOption<HoldingType>[] = [
{ label: $localize`Active`, value: 'ACTIVE' },
{ 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'
})
export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
@Input() public readonly benchmarkDataItems: InvestmentItem[] = [];
@Input() public readonly benchmarkDataLabel = '';
@Input() public readonly colorScheme: ColorScheme;
@Input() public readonly currency: string;
@Input() public readonly groupBy: GroupBy;
@Input() public readonly historicalDataItems: LineChartItem[] = [];
@Input() public readonly isInPercentage = false;
@Input() public readonly isLoading = false;
@Input() public readonly locale = getLocale();
@Input() public readonly savingsRate = 0;
@Input() public benchmarkDataItems: InvestmentItem[] = [];
@Input() public benchmarkDataLabel = '';
@Input() public colorScheme: ColorScheme;
@Input() public currency: string;
@Input() public groupBy: GroupBy;
@Input() public historicalDataItems: LineChartItem[] = [];
@Input() public isInPercentage = false;
@Input() public isLoading = false;
@Input() public locale = getLocale();
@Input() public savingsRate = 0;
private readonly chartCanvas =
viewChild.required<ElementRef<HTMLCanvasElement>>('chartCanvas');

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

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

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

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

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

@ -12,7 +12,6 @@ import {
getCountryName
} from '@ghostfolio/common/helper';
import {
AssetProfileIdentifier,
HoldingWithParents,
PortfolioDetails,
PortfolioPosition,
@ -22,7 +21,10 @@ import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { hasScope, scopes } from '@ghostfolio/common/scopes';
import { MarketAdvanced } from '@ghostfolio/common/types';
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 { DataService } from '@ghostfolio/ui/services';
import { GfTopHoldingsComponent } from '@ghostfolio/ui/top-holdings';
@ -206,7 +208,9 @@ export class GfAllocationsPageComponent implements OnInit {
this.initialize();
}
protected onAccountChartClicked({ accountId }: { accountId: string }) {
protected onAccountChartClicked(event: PortfolioProportionChartClickEvent) {
const accountId = 'accountId' in event ? event.accountId : undefined;
if (accountId && accountId !== UNKNOWN_KEY) {
void this.router.navigate([], {
queryParams: { accountId, accountDetailDialog: true }
@ -214,10 +218,9 @@ export class GfAllocationsPageComponent implements OnInit {
}
}
protected onSymbolChartClicked({
dataSource,
symbol
}: AssetProfileIdentifier) {
protected onSymbolChartClicked(event: PortfolioProportionChartClickEvent) {
const { dataSource, symbol } = 'symbol' in event ? event : {};
if (dataSource && symbol) {
void this.router.navigate([], {
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 isLoadingPortfolioPrompt: boolean;
protected readonly mode = signal<GroupBy>('month');
protected readonly modeOptions: ToggleOption[] = [
protected readonly modeOptions: ToggleOption<GroupBy>[] = [
{ label: $localize`Monthly`, value: 'month' },
{ 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
readonly
type="text"
[(value)]="accessToken"
[value]="accessToken"
></textarea>
<div class="float-right mt-1">
<button

12
apps/client/tsconfig.json

@ -15,8 +15,18 @@
],
"angularCompilerOptions": {
"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
"strictInputAccessModifiers": false,
"strictTemplates": false
},
"compilerOptions": {

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

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

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

@ -82,7 +82,7 @@ export class GfSymbolAutocompleteComponent
@Input() public defaultLookupItems: LookupItem[] = [];
@Input() public isLoading = false;
@Input() private includeIndices = false;
@Input() public includeIndices = false;
public readonly control = new FormControl();
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'],
templateUrl: './toggle.component.html'
})
export class GfToggleComponent {
public readonly defaultValue = input.required<string>();
export class GfToggleComponent<T extends string = string> {
public readonly defaultValue = input.required<T>();
public readonly isDisabled = 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);
protected readonly valueChange = output<Pick<ToggleOption, 'value'>>();
public readonly valueChange = output<Pick<ToggleOption<T>, 'value'>>();
protected readonly optionFormControl = new FormControl<T | null>(null);
public constructor() {
effect(() => {

Loading…
Cancel
Save