import {
Component,
inject,
OnInit,
ChangeDetectionStrategy,
} from "@angular/core";
import { MAT_DIALOG_DATA, MatDialogModule } from "@angular/material/dialog";
import {
ProgressDashboardConfig,
ProgressDashboardPart,
} from "../progress-dashboard/progress-dashboard-config";
import {
FormArray,
FormBuilder,
FormControl,
FormGroup,
ReactiveFormsModule,
ValidationErrors,
Validators,
} from "@angular/forms";
import { ErrorStateMatcher } from "@angular/material/core";
import { MatFormFieldModule } from "@angular/material/form-field";
import { MatInputModule } from "@angular/material/input";
import { DialogCloseComponent } from "../../../../core/common-components/dialog-close/dialog-close.component";
import { MatButtonModule } from "@angular/material/button";
import { FontAwesomeModule } from "@fortawesome/angular-fontawesome";
import { MatTooltipModule } from "@angular/material/tooltip";
import { TypedFormGroup } from "#src/app/core/common-components/entity-form/entity-form";
export interface EditProgressDashboardComponentData {
title: string;
parts: ProgressDashboardPart[];
}
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: "app-edit-progress-dashboard",
templateUrl: "./edit-progress-dashboard.component.html",
styleUrls: ["./edit-progress-dashboard.component.scss"],
imports: [
MatDialogModule,
MatFormFieldModule,
ReactiveFormsModule,
MatInputModule,
DialogCloseComponent,
MatButtonModule,
FontAwesomeModule,
MatTooltipModule,
],
})
export class EditProgressDashboardComponent implements OnInit {
private data = inject<ProgressDashboardConfig>(MAT_DIALOG_DATA);
private fb = inject(FormBuilder);
/**
* This marks the control as invalid when the whole form has an error
*/
readonly currentErrorStateMatcher: ErrorStateMatcher = {
isErrorState: (control: FormControl | null) => !control?.parent?.valid,
};
title: FormControl;
parts: FormArray;
outputData: FormGroup;
ngOnInit(): void {
this.title = new FormControl(this.data.title, [Validators.required]);
this.parts = this.fb.array(
this.data.parts.map((part) => this.createPartForm(part)),
);
this.outputData = new FormGroup({
title: this.title,
parts: this.parts,
});
}
createPartForm(part: ProgressDashboardPart) {
return this.fb.group(
{
label: this.fb.control(part.label, [Validators.required]),
currentValue: this.fb.control(part.currentValue, [
Validators.required,
Validators.min(0),
]),
targetValue: this.fb.control(part.targetValue, [
Validators.required,
Validators.min(0),
]),
},
{
validators: [this.currentLessThanTarget],
},
);
}
currentLessThanTarget(
control: TypedFormGroup<ProgressDashboardPart>,
): ValidationErrors | null {
const current = control.get("currentValue");
const target = control.get("targetValue");
if (current.value > target.value) {
return {
currentGtTarget: true,
};
} else {
return null;
}
}
addPart() {
const newPart: ProgressDashboardPart = {
label: $localize`:Part of a whole:Part`,
currentValue: 1,
targetValue: 10,
};
this.parts.push(this.createPartForm(newPart));
}
removePart(index: number) {
this.parts.removeAt(index);
}
}
<div mat-dialog-title style="display: flex">
<mat-form-field class="title-field margin-top-small">
<input
[formControl]="title"
matInput
type="text"
i18n-placeholder="Edit the progress of one or multiple tasks"
placeholder="Title"
/>
@if (title.hasError("required")) {
<mat-error i18n> This field is required </mat-error>
}
</mat-form-field>
<app-dialog-close mat-dialog-close></app-dialog-close>
</div>
<mat-dialog-content class="dialog-content">
@for (control of parts.controls; track $index; let index = $index) {
<div class="entry-wrapper mat-elevation-z1" [formGroup]="$any(control)">
<mat-form-field class="header-field">
<input
formControlName="label"
matInput
type="text"
i18n-placeholder="The label of a process or task"
placeholder="Task"
/>
@if (control.hasError("required", "label")) {
<mat-error i18n> This field is required </mat-error>
}
</mat-form-field>
<button mat-icon-button color="accent" (click)="removePart(index)">
<fa-icon
class="button-icon"
i18n-aria-label
aria-label="remove element"
icon="trash"
></fa-icon>
</button>
<mat-form-field class="current-field">
<input
formControlName="currentValue"
matInput
type="number"
i18n-placeholder="
Current process|The Current value of a process or task
"
placeholder="Current"
[errorStateMatcher]="currentErrorStateMatcher"
/>
@if (control.hasError("required", "currentValue")) {
<mat-error i18n> This field is required </mat-error>
}
@if (control.hasError("min", "currentValue")) {
<mat-error i18n> Must be greater than 0 </mat-error>
}
@if (control.hasError("currentGtTarget")) {
<mat-error
i18n="
The number entered in this form is less than another field but
should not be
"
>
Must not be greater than target
</mat-error>
}
</mat-form-field>
<mat-form-field class="target-field">
<input
formControlName="targetValue"
matInput
type="number"
i18n-placeholder="Target process|The target amount of a process"
placeholder="Target"
/>
@if (control.hasError("required", "targetValue")) {
<mat-error
i18n="A field entered in a form is required and wasn't provided"
>
This field is required
</mat-error>
}
@if (control.hasError("min", "targetValue")) {
<mat-error i18n="A field entered in a form must be greater than 0">
Must be greater than 0
</mat-error>
}
</mat-form-field>
</div>
}
<div>
<button mat-button color="accent" (click)="addPart()">
<fa-icon
class="button-icon margin-sides-small"
aria-hidden="true"
icon="plus-circle"
></fa-icon>
<span i18n="Add a task to the progress dashboard list">
Add New Task
</span>
</button>
</div>
</mat-dialog-content>
<mat-dialog-actions>
<button
mat-raised-button
color="accent"
[mat-dialog-close]="outputData.value"
[disabled]="outputData.invalid"
>
<span
matTooltip="Fix the errors to save the form"
[matTooltipDisabled]="outputData.valid"
i18n-matTooltip="Shown when there are errors that prevent saving"
i18n
>Save</span
>
</button>
<button mat-stroked-button mat-dialog-close i18n>Cancel</button>
</mat-dialog-actions>
@use "variables/sizes";
.entry-wrapper {
display: grid;
grid-template-columns: repeat(2, 1fr) 64px;
grid-template-rows: repeat(2, 1fr);
grid-gap: sizes.$small;
grid-template-areas:
"header header trash"
"current target trash";
padding: 12px;
}
.entry-wrapper:not(:last-child) {
margin-bottom: sizes.$large;
}
.entry-wrapper:last-child {
margin-bottom: sizes.$regular;
}
.title-field {
width: 90%;
min-width: 0;
input {
font-size: 32px;
}
}
.header-field {
grid-area: header;
min-width: 0;
}
.current-field {
grid-area: current;
min-width: 0;
}
.target-field {
grid-area: target;
min-width: 0;
}
.dialog-content {
@media (max-height: 800px) {
height: 40vh;
}
}