Browse Source

Refactor to FormGroup

pull/666/head
Thomas 3 years ago
parent
commit
8e0d53ebf1
  1. 4
      apps/api/src/app/order/order.service.ts
  2. 3
      apps/api/src/app/order/update-order.dto.ts
  3. 214
      apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.component.ts
  4. 76
      apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.html

4
apps/api/src/app/order/order.service.ts

@ -203,6 +203,10 @@ export class OrderService {
}): Promise<Order> { }): Promise<Order> {
const { data, where } = params; const { data, where } = params;
if (data.Account.connect.id_userId.id === null) {
delete data.Account;
}
if (data.type === 'ITEM') { if (data.type === 'ITEM') {
const name = data.symbol; const name = data.symbol;

3
apps/api/src/app/order/update-order.dto.ts

@ -1,7 +1,8 @@
import { DataSource, Type } from '@prisma/client'; import { DataSource, Type } from '@prisma/client';
import { IsISO8601, IsNumber, IsString } from 'class-validator'; import { IsISO8601, IsNumber, IsOptional, IsString } from 'class-validator';
export class UpdateOrderDto { export class UpdateOrderDto {
@IsOptional()
@IsString() @IsString()
accountId: string; accountId: string;

214
apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.component.ts

@ -6,7 +6,7 @@ import {
OnDestroy, OnDestroy,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { FormControl, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
@ -14,6 +14,7 @@ import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { Type } from '@prisma/client'; import { Type } from '@prisma/client';
import { isUUID } from 'class-validator';
import { isString } from 'lodash'; import { isString } from 'lodash';
import { EMPTY, Observable, Subject } from 'rxjs'; import { EMPTY, Observable, Subject } from 'rxjs';
import { import {
@ -37,25 +38,15 @@ import { CreateOrUpdateTransactionDialogParams } from './interfaces/interfaces';
export class CreateOrUpdateTransactionDialog implements OnDestroy { export class CreateOrUpdateTransactionDialog implements OnDestroy {
@ViewChild('autocomplete') autocomplete; @ViewChild('autocomplete') autocomplete;
public accountIdCtrl = new FormControl({}, Validators.required); public activityForm: FormGroup;
public currencyCtrl = new FormControl({}, Validators.required);
public currencies: string[] = []; public currencies: string[] = [];
public currentMarketPrice = null; public currentMarketPrice = null;
public dataSourceCtrl = new FormControl({}, Validators.required);
public dateCtrl = new FormControl({}, Validators.required);
public feeCtrl = new FormControl({}, Validators.required);
public filteredLookupItems: LookupItem[]; public filteredLookupItems: LookupItem[];
public filteredLookupItemsObservable: Observable<LookupItem[]>; public filteredLookupItemsObservable: Observable<LookupItem[]>;
public isLoading = false; public isLoading = false;
public nameCtrl = new FormControl({}, Validators.required);
public platforms: { id: string; name: string }[]; public platforms: { id: string; name: string }[];
public quantityCtrl = new FormControl({}, Validators.required); public Validators = Validators;
public searchSymbolCtrl = new FormControl({}, Validators.required);
public showAccountIdCtrl = true;
public showNameCtrl = true;
public showSearchSymbolCtrl = true;
public typeCtrl = new FormControl({}, Validators.required);
public unitPriceCtrl = new FormControl({}, Validators.required);
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
@ -63,6 +54,7 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
public dialogRef: MatDialogRef<CreateOrUpdateTransactionDialog>, public dialogRef: MatDialogRef<CreateOrUpdateTransactionDialog>,
private formBuilder: FormBuilder,
@Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateTransactionDialogParams @Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateTransactionDialogParams
) {} ) {}
@ -72,78 +64,98 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
this.currencies = currencies; this.currencies = currencies;
this.platforms = platforms; this.platforms = platforms;
this.filteredLookupItemsObservable = this.activityForm = this.formBuilder.group({
this.searchSymbolCtrl.valueChanges.pipe( accountId: [this.data.activity?.accountId, Validators.required],
startWith(''), currency: [
debounceTime(400), this.data.activity?.SymbolProfile?.currency,
distinctUntilChanged(), Validators.required
switchMap((query: string) => { ],
if (isString(query)) { dataSource: [
const filteredLookupItemsObservable = this.data.activity?.SymbolProfile?.dataSource,
this.dataService.fetchSymbols(query); Validators.required
],
filteredLookupItemsObservable.subscribe((filteredLookupItems) => { date: [this.data.activity?.date, Validators.required],
this.filteredLookupItems = filteredLookupItems; fee: [this.data.activity?.fee, Validators.required],
}); name: [this.data.activity?.SymbolProfile?.name, Validators.required],
quantity: [this.data.activity?.quantity, Validators.required],
return filteredLookupItemsObservable; searchSymbol: [
} {
dataSource: this.data.activity?.SymbolProfile?.dataSource,
symbol: this.data.activity?.SymbolProfile?.symbol
},
Validators.required
],
type: [undefined, Validators.required], // Set after value changes subscription
unitPrice: [this.data.activity?.unitPrice, Validators.required]
});
return []; this.filteredLookupItemsObservable = this.activityForm.controls[
}) 'searchSymbol'
); ].valueChanges.pipe(
startWith(''),
debounceTime(400),
distinctUntilChanged(),
switchMap((query: string) => {
if (isString(query)) {
const filteredLookupItemsObservable =
this.dataService.fetchSymbols(query);
filteredLookupItemsObservable.subscribe((filteredLookupItems) => {
this.filteredLookupItems = filteredLookupItems;
});
return filteredLookupItemsObservable;
}
return [];
})
);
this.typeCtrl.valueChanges.subscribe((type: Type) => { this.activityForm.controls['type'].valueChanges.subscribe((type: Type) => {
if (type === 'ITEM') { if (type === 'ITEM') {
this.accountIdCtrl.removeValidators(Validators.required); this.activityForm.controls['accountId'].removeValidators(
this.accountIdCtrl.updateValueAndValidity(); Validators.required
this.currencyCtrl.setValue(this.data.user.settings.baseCurrency); );
this.dataSourceCtrl.removeValidators(Validators.required); this.activityForm.controls['accountId'].updateValueAndValidity();
this.dataSourceCtrl.updateValueAndValidity(); this.activityForm.controls['currency'].setValue(
this.nameCtrl.setValidators(Validators.required); this.data.user.settings.baseCurrency
this.nameCtrl.updateValueAndValidity(); );
this.quantityCtrl.setValue(1); this.activityForm.controls['dataSource'].removeValidators(
this.searchSymbolCtrl.removeValidators(Validators.required); Validators.required
this.searchSymbolCtrl.updateValueAndValidity(); );
this.showAccountIdCtrl = false; this.activityForm.controls['dataSource'].updateValueAndValidity();
this.showNameCtrl = true; this.activityForm.controls['name'].setValidators(Validators.required);
this.showSearchSymbolCtrl = false; this.activityForm.controls['name'].updateValueAndValidity();
this.activityForm.controls['quantity'].setValue(1);
this.activityForm.controls['searchSymbol'].removeValidators(
Validators.required
);
this.activityForm.controls['searchSymbol'].updateValueAndValidity();
} else { } else {
this.accountIdCtrl.setValidators(Validators.required); this.activityForm.controls['accountId'].setValidators(
this.accountIdCtrl.updateValueAndValidity(); Validators.required
this.currencyCtrl.setValue(undefined); );
this.dataSourceCtrl.setValidators(Validators.required); this.activityForm.controls['accountId'].updateValueAndValidity();
this.dataSourceCtrl.updateValueAndValidity(); this.activityForm.controls['dataSource'].setValidators(
this.nameCtrl.removeValidators(Validators.required); Validators.required
this.nameCtrl.updateValueAndValidity(); );
this.quantityCtrl.setValue(undefined); this.activityForm.controls['dataSource'].updateValueAndValidity();
this.searchSymbolCtrl.setValidators(Validators.required); this.activityForm.controls['name'].removeValidators(
this.searchSymbolCtrl.updateValueAndValidity(); Validators.required
this.showAccountIdCtrl = true; );
this.showNameCtrl = false; this.activityForm.controls['name'].updateValueAndValidity();
this.showSearchSymbolCtrl = true; this.activityForm.controls['searchSymbol'].setValidators(
Validators.required
);
this.activityForm.controls['searchSymbol'].updateValueAndValidity();
} }
this.changeDetectorRef.markForCheck();
}); });
this.accountIdCtrl.setValue(this.data.activity?.accountId); this.activityForm.controls['type'].setValue(this.data.activity?.type);
this.currencyCtrl.setValue(this.data.activity?.currency);
this.dataSourceCtrl.setValue(this.data.activity?.dataSource);
this.dateCtrl.setValue(this.data.activity?.date);
this.feeCtrl.setValue(this.data.activity?.fee);
this.nameCtrl.setValue(this.data.activity?.SymbolProfile?.name);
this.quantityCtrl.setValue(this.data.activity?.quantity);
this.searchSymbolCtrl.setValue({
dataSource: this.data.activity?.dataSource,
symbol: this.data.activity?.symbol
});
this.typeCtrl.setValue(this.data.activity?.type);
this.unitPriceCtrl.setValue(this.data.activity?.unitPrice);
if (this.data.activity?.id) { if (this.data.activity?.id) {
this.searchSymbolCtrl.disable(); this.activityForm.controls['searchSymbol'].disable();
this.typeCtrl.disable(); this.activityForm.controls['type'].disable();
} }
if (this.data.activity?.symbol) { if (this.data.activity?.symbol) {
@ -162,7 +174,9 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
} }
public applyCurrentMarketPrice() { public applyCurrentMarketPrice() {
this.unitPriceCtrl.setValue(this.currentMarketPrice); this.activityForm.patchValue({
unitPrice: this.currentMarketPrice
});
} }
public displayFn(aLookupItem: LookupItem) { public displayFn(aLookupItem: LookupItem) {
@ -171,13 +185,16 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
public onBlurSymbol() { public onBlurSymbol() {
const currentLookupItem = this.filteredLookupItems.find((lookupItem) => { const currentLookupItem = this.filteredLookupItems.find((lookupItem) => {
return lookupItem.symbol === this.data.activity.symbol; return (
lookupItem.symbol ===
this.activityForm.controls['searchSymbol'].value.symbol
);
}); });
if (currentLookupItem) { if (currentLookupItem) {
this.updateSymbol(currentLookupItem.symbol); this.updateSymbol(currentLookupItem.symbol);
} else { } else {
this.searchSymbolCtrl.setErrors({ incorrect: true }); this.activityForm.controls['searchSymbol'].setErrors({ incorrect: true });
this.data.activity.currency = null; this.data.activity.currency = null;
this.data.activity.dataSource = null; this.data.activity.dataSource = null;
@ -193,15 +210,17 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
public onSubmit() { public onSubmit() {
const activity: CreateOrderDto | UpdateOrderDto = { const activity: CreateOrderDto | UpdateOrderDto = {
accountId: this.accountIdCtrl.value, accountId: this.activityForm.controls['accountId'].value,
currency: this.currencyCtrl.value, currency: this.activityForm.controls['currency'].value,
date: this.dateCtrl.value, date: this.activityForm.controls['date'].value,
dataSource: this.dataSourceCtrl.value, dataSource: this.activityForm.controls['dataSource'].value,
fee: this.feeCtrl.value, fee: this.activityForm.controls['fee'].value,
quantity: this.quantityCtrl.value, quantity: this.activityForm.controls['quantity'].value,
symbol: this.searchSymbolCtrl.value.symbol ?? this.nameCtrl.value, symbol: isUUID(this.activityForm.controls['searchSymbol'].value.symbol)
type: this.typeCtrl.value, ? this.activityForm.controls['name'].value
unitPrice: this.unitPriceCtrl.value : this.activityForm.controls['searchSymbol'].value.symbol,
type: this.activityForm.controls['type'].value,
unitPrice: this.activityForm.controls['unitPrice'].value
}; };
if (this.data.activity.id) { if (this.data.activity.id) {
@ -212,7 +231,9 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
} }
public onUpdateSymbol(event: MatAutocompleteSelectedEvent) { public onUpdateSymbol(event: MatAutocompleteSelectedEvent) {
this.dataSourceCtrl.setValue(event.option.value.dataSource); this.activityForm.controls['dataSource'].setValue(
event.option.value.dataSource
);
this.updateSymbol(event.option.value.symbol); this.updateSymbol(event.option.value.symbol);
} }
@ -224,16 +245,15 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
private updateSymbol(symbol: string) { private updateSymbol(symbol: string) {
this.isLoading = true; this.isLoading = true;
this.searchSymbolCtrl.setErrors(null); this.activityForm.controls['searchSymbol'].setErrors(null);
this.activityForm.controls['searchSymbol'].setValue({ symbol });
this.searchSymbolCtrl.setValue({ symbol });
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
this.dataService this.dataService
.fetchSymbolItem({ .fetchSymbolItem({
dataSource: this.dataSourceCtrl.value, dataSource: this.activityForm.controls['dataSource'].value,
symbol: this.searchSymbolCtrl.value.symbol symbol: this.activityForm.controls['searchSymbol'].value.symbol
}) })
.pipe( .pipe(
catchError(() => { catchError(() => {
@ -250,8 +270,8 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
takeUntil(this.unsubscribeSubject) takeUntil(this.unsubscribeSubject)
) )
.subscribe(({ currency, dataSource, marketPrice }) => { .subscribe(({ currency, dataSource, marketPrice }) => {
this.currencyCtrl.setValue(currency); this.activityForm.controls['currency'].setValue(currency);
this.dataSourceCtrl.setValue(dataSource); this.activityForm.controls['dataSource'].setValue(dataSource);
this.currentMarketPrice = marketPrice; this.currentMarketPrice = marketPrice;

76
apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.html

@ -1,11 +1,15 @@
<form #addTransactionForm="ngForm" class="d-flex flex-column h-100"> <form
class="d-flex flex-column h-100"
[formGroup]="activityForm"
(ngSubmit)="onSubmit()"
>
<h1 *ngIf="data.activity.id" mat-dialog-title i18n>Update activity</h1> <h1 *ngIf="data.activity.id" mat-dialog-title i18n>Update activity</h1>
<h1 *ngIf="!data.activity.id" mat-dialog-title i18n>Add activity</h1> <h1 *ngIf="!data.activity.id" mat-dialog-title i18n>Add activity</h1>
<div class="flex-grow-1" mat-dialog-content> <div class="flex-grow-1" mat-dialog-content>
<div> <div>
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Type</mat-label> <mat-label i18n>Type</mat-label>
<mat-select name="type" [formControl]="typeCtrl"> <mat-select formControlName="type">
<mat-option value="BUY" i18n>BUY</mat-option> <mat-option value="BUY" i18n>BUY</mat-option>
<mat-option value="DIVIDEND" i18n>DIVIDEND</mat-option> <mat-option value="DIVIDEND" i18n>DIVIDEND</mat-option>
<mat-option value="ITEM" i18n>ITEM</mat-option> <mat-option value="ITEM" i18n>ITEM</mat-option>
@ -13,25 +17,29 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
<div [ngClass]="{ 'd-none': !showAccountIdCtrl }"> <div
[ngClass]="{ 'd-none': !activityForm.controls['accountId'].hasValidator(Validators.required) }"
>
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Account</mat-label> <mat-label i18n>Account</mat-label>
<mat-select name="accountId" [formControl]="accountIdCtrl"> <mat-select formControlName="accountId">
<mat-option *ngFor="let account of data.accounts" [value]="account.id" <mat-option *ngFor="let account of data.accounts" [value]="account.id"
>{{ account.name }}</mat-option >{{ account.name }}</mat-option
> >
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
<div [ngClass]="{ 'd-none': !showSearchSymbolCtrl }"> <div
[ngClass]="{ 'd-none': !activityForm.controls['searchSymbol'].hasValidator(Validators.required) }"
>
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Symbol or ISIN</mat-label> <mat-label i18n>Symbol or ISIN</mat-label>
<input <input
autocapitalize="off" autocapitalize="off"
autocomplete="off" autocomplete="off"
autocorrect="off" autocorrect="off"
formControlName="searchSymbol"
matInput matInput
[formControl]="searchSymbolCtrl"
[matAutocomplete]="autocomplete" [matAutocomplete]="autocomplete"
(blur)="onBlurSymbol()" (blur)="onBlurSymbol()"
/> />
@ -54,20 +62,18 @@
<mat-spinner *ngIf="isLoading" matSuffix [diameter]="20"></mat-spinner> <mat-spinner *ngIf="isLoading" matSuffix [diameter]="20"></mat-spinner>
</mat-form-field> </mat-form-field>
</div> </div>
<div [ngClass]="{ 'd-none': !showNameCtrl }"> <div
[ngClass]="{ 'd-none': !activityForm.controls['name'].hasValidator(Validators.required) }"
>
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Name</mat-label> <mat-label i18n>Name</mat-label>
<input matInput name="name" [formControl]="nameCtrl" /> <input formControlName="name" matInput />
</mat-form-field> </mat-form-field>
</div> </div>
<div class="d-none"> <div class="d-none">
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Currency</mat-label> <mat-label i18n>Currency</mat-label>
<mat-select <mat-select class="no-arrow" formControlName="currency">
class="no-arrow"
name="currency"
[formControl]="currencyCtrl"
>
<mat-option *ngFor="let currency of currencies" [value]="currency" <mat-option *ngFor="let currency of currencies" [value]="currency"
>{{ currency }}</mat-option >{{ currency }}</mat-option
> >
@ -77,18 +83,13 @@
<div class="d-none"> <div class="d-none">
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Data Source</mat-label> <mat-label i18n>Data Source</mat-label>
<input matInput name="dataSource" [formControl]="dataSourceCtrl" /> <input formControlName="dataSource" matInput />
</mat-form-field> </mat-form-field>
</div> </div>
<div> <div>
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Date</mat-label> <mat-label i18n>Date</mat-label>
<input <input formControlName="date" matInput [matDatepicker]="date" />
matInput
name="date"
[formControl]="dateCtrl"
[matDatepicker]="date"
/>
<mat-datepicker-toggle matSuffix [for]="date"> <mat-datepicker-toggle matSuffix [for]="date">
<ion-icon <ion-icon
class="text-muted" class="text-muted"
@ -102,29 +103,22 @@
<div> <div>
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Quantity</mat-label> <mat-label i18n>Quantity</mat-label>
<input <input formControlName="quantity" matInput type="number" />
matInput
name="quantity"
type="number"
[formControl]="quantityCtrl"
/>
</mat-form-field> </mat-form-field>
</div> </div>
<div> <div>
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Unit Price</mat-label> <mat-label i18n>Unit Price</mat-label>
<input <input formControlName="unitPrice" matInput type="number" />
matInput <span class="ml-2" matSuffix
name="unitPrice" >{{ activityForm.controls['currency'].value }}</span
type="number" >
[formControl]="unitPriceCtrl"
/>
<span class="ml-2" matSuffix>{{ currencyCtrl.value }}</span>
<button <button
*ngIf="currentMarketPrice && (data.activity.type === 'BUY' || data.activity.type === 'SELL')" *ngIf="currentMarketPrice && (data.activity.type === 'BUY' || data.activity.type === 'SELL')"
mat-icon-button mat-icon-button
matSuffix matSuffix
title="Apply current market price" title="Apply current market price"
type="button"
(click)="applyCurrentMarketPrice()" (click)="applyCurrentMarketPrice()"
> >
<ion-icon class="text-muted" name="refresh-outline"></ion-icon> <ion-icon class="text-muted" name="refresh-outline"></ion-icon>
@ -134,26 +128,28 @@
<div> <div>
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Fee</mat-label> <mat-label i18n>Fee</mat-label>
<input matInput name="fee" type="number" [formControl]="feeCtrl" /> <input formControlName="fee" matInput type="number" />
<span class="ml-2" matSuffix>{{ currencyCtrl.value }}</span> <span class="ml-2" matSuffix
>{{ activityForm.controls['currency'].value }}</span
>
</mat-form-field> </mat-form-field>
</div> </div>
</div> </div>
<div class="d-flex" mat-dialog-actions> <div class="d-flex" mat-dialog-actions>
<gf-value <gf-value
class="flex-grow-1" class="flex-grow-1"
[currency]="currencyCtrl.value" [currency]="activityForm.controls['currency'].value"
[locale]="data.user?.settings?.locale" [locale]="data.user?.settings?.locale"
[value]="feeCtrl.value + (quantityCtrl.value * unitPriceCtrl.value) ?? 0" [value]="activityForm.controls['fee'].value + (activityForm.controls['quantity'].value * activityForm.controls['unitPrice'].value) ?? 0"
></gf-value> ></gf-value>
<div> <div>
<button i18n mat-button (click)="onCancel()">Cancel</button> <button i18n mat-button type="button" (click)="onCancel()">Cancel</button>
<button <button
color="primary" color="primary"
i18n i18n
mat-flat-button mat-flat-button
[disabled]="!(addTransactionForm.form.valid)" type="submit"
(click)="onSubmit()" [disabled]="!activityForm.valid"
> >
Save Save
</button> </button>

Loading…
Cancel
Save