Merge pull request #83 in TEA/mina-sidor-fa-web from feature/TV-396 to develop

Squashed commit of the following:

commit f5029b04d2117df86eaf6692c88bdc692059d8d6
Merge: 3d776b1 45f2fb5
Author: arbetsformedlingen_garcn <christian.gardebrink@arbetsformedlingen.se>
Date:   Sat Sep 11 06:17:07 2021 +0200

    Merge branch 'develop-remote' into feature/TV-396

commit 3d776b10824be6b54e186104a5bcd351e5b2fb42
Author: arbetsformedlingen_garcn <christian.gardebrink@arbetsformedlingen.se>
Date:   Fri Sep 10 23:26:47 2021 +0200

    TV-396 fixed some tests and so on

commit bd57fce383ba409ae8de1869c242b5a8f51071d2
Author: arbetsformedlingen_garcn <christian.gardebrink@arbetsformedlingen.se>
Date:   Fri Sep 10 23:20:26 2021 +0200

    TV-396 made some adjustments to the validation logic after feedback in PR

commit 942cddb263d0965e772f7f34305e85737da76df4
Merge: 174dfe9 ceee702
Author: arbetsformedlingen_garcn <christian.gardebrink@arbetsformedlingen.se>
Date:   Fri Sep 10 14:10:23 2021 +0200

    Merge branch 'develop-remote' into feature/TV-396

commit 174dfe924f2eac979992275ddd55ed0758144efb
Author: arbetsformedlingen_garcn <christian.gardebrink@arbetsformedlingen.se>
Date:   Fri Sep 10 14:05:39 2021 +0200

    TV-396 fixed issue with general info after restructuring..

commit 7e0d4bdf9e76e0fb58fe30358c3e729cce1f9260
Merge: da02f6c 5b00453
Author: arbetsformedlingen_garcn <christian.gardebrink@arbetsformedlingen.se>
Date:   Fri Sep 10 12:00:29 2021 +0200

    Merge branch 'develop-remote' into feature/TV-396

commit da02f6cc7f4a9405ad1d8167ef18729b18973d61
Author: arbetsformedlingen_garcn <christian.gardebrink@arbetsformedlingen.se>
Date:   Fri Sep 10 10:43:42 2021 +0200

    TV-396 added aria-expanded attribute

commit 48eb24ca6e354b44ae4d4b62ce2ffa496743d0b5
Author: arbetsformedlingen_garcn <christian.gardebrink@arbetsformedlingen.se>
Date:   Fri Sep 10 09:46:39 2021 +0200

    TV-396 moved some logic into seperate template

commit 0ef787d6c3700677ae793c486930b07748365412
Merge: 6dfd7b0 5f81d6f
Author: arbetsformedlingen_garcn <christian.gardebrink@arbetsformedlingen.se>
Date:   Thu Sep 9 17:34:40 2021 +0200

    Merge branch 'develop-remote' into feature/TV-396

commit 6dfd7b00caa45d335f3fe8619b92c282038ac5cb
Author: arbetsformedlingen_garcn <christian.gardebrink@arbetsformedlingen.se>
Date:   Thu Sep 9 17:33:54 2021 +0200

    TV-396 used digi internal link instead of basic a-tag

commit 46c17011b7f6b1f628b14ccf020c06cdc95627c8
Merge: 4338e15 1938b94
Author: arbetsformedlingen_garcn <christian.gardebrink@arbetsformedlingen.se>
Date:   Thu Sep 9 15:07:16 2021 +0200

    Merge branch 'develop-remote' into feature/TV-396

commit 4338e153f7e4ebdf8ab65a64a6194dbd4d9fa9c7
Author: arbetsformedlingen_garcn <christian.gardebrink@arbetsformedlingen.se>
Date:   Thu Sep 9 14:56:26 2021 +0200

    TV-396 added error summary an validation handling for the edit form etc.

commit ebb2e76993b99756d5a641ab8ca7d137be8a982f
Author: arbetsformedlingen_garcn <christian.gardebrink@arbetsformedlingen.se>
Date:   Wed Sep 8 22:58:44 2021 +0200

    TV-396 making sure utforande verksamheter and addresses are populated as they should be when editing an employee.

commit 01f4c9bf7ad8fc4ad44b0e8945182492b864d0cf
Merge: 1c2aa92 b06436a
Author: arbetsformedlingen_garcn <christian.gardebrink@arbetsformedlingen.se>
Date:   Wed Sep 8 22:07:25 2021 +0200

    Merge branch 'develop-remote' into feature/TV-396

commit 1c2aa92f21aac57036ed05d5ebeab0f0e6a45c2c
Author: arbetsformedlingen_garcn <christian.gardebrink@arbetsformedlingen.se>
Date:   Wed Sep 8 22:07:00 2021 +0200

    Merge branch 'develop-remote' into bugs/TV-520

    # Conflicts:
    #	apps/mina-sidor-fa/src/app/pages/administration/pages/employee-card/employee-card.component.html
This commit is contained in:
Christian Gårdebrink
2021-09-11 06:19:16 +02:00
parent 45f2fb577a
commit 723ad02092
26 changed files with 724 additions and 723 deletions

View File

@@ -1,12 +1,17 @@
<digi-typography>
<digi-typography #editEmployeeFormContainer>
<form
class="edit-employee-form"
*ngIf="editEmployeeFormGroup"
[formGroup]="editEmployeeFormGroup"
(ngSubmit)="onFormSubmitted()"
>
<msfa-error-list
[hidden]="!displayErrorSummary"
[headingText]="'Åtgärda följande fel för att spara dina ändringar:'"
[validationErrorLinks]="getValidationErrorLinks()"
></msfa-error-list>
<digi-ng-form-input
afId="edit-employee-form-email"
[afId]="emailElementId"
afLabel="E-post adress"
afType="email"
[formControlName]="emailFormControlName"
@@ -20,7 +25,6 @@
<h2>Behörigheter</h2>
<p>Här kan du ändra personalkontots behörigheter.</p>
</div>
<div class="edit-employee-form__block">
<h3>Tjänster</h3>
<p>Välj de tjänster du vill ge personalen tillgång till.</p>
@@ -32,6 +36,7 @@
[afSelectItems]="selectableTjansterFormItems"
[afDisableValidStyle]="true"
[afInvalid]="tjansterFormControl.invalid && tjansterFormControl.touched"
[afId]="tjansterElementId"
(afOnChange)="toggleTjanst()"
></digi-ng-form-select>
<digi-form-validation-message
@@ -48,22 +53,30 @@
<p *ngIf="!availableUtforandeVerksamheter || availableUtforandeVerksamheter.length === 0">
<strong>Du måste välja en eller flera tjänster för att kunna välja utförande verksamheter.</strong>
</p>
<digi-ng-form-checkbox
class="edit-employee-form__choose-all-utforande-verksamheter"
[formControl]="toggleAllUtforandeVerksamhetFormControl"
[afLabel]="'Välj alla utförande verksamheter och alla utförande adresser'"
(afOnChange)="toggleAllUtforandeVerksamheter($event)"
></digi-ng-form-checkbox>
<ng-container *ngIf="!isLoadingUtforandeVerksamheter else loadingUtforandeVerksamheterTemplate">
<digi-ng-form-checkbox
class="edit-employee-form__choose-all-utforande-verksamheter"
[formControl]="selectAllUtforandeVerksamheterFormControl"
[afLabel]="'Välj alla utförande verksamheter och alla utförande adresser'"
></digi-ng-form-checkbox>
<msfa-tree-nodes-selector
*ngIf="!toggleAllUtforandeVerksamhetFormControl.value"
[headingText]="'Välj utförande verksamheter och adresser'"
[formControlName]="utforandeVerksamhetFormControlName"
[isInvalid]="utforandeVerksamhetFormControl?.invalid"
[showValidation]="utforandeVerksamhetFormControl?.touched"
[validationMessages]="utforandeVerksamhetFormControl.errors?.required ? ['Välj minst en utförande verksamhet'] : []"
(selectedTreeNodesChanged)="updateToggleAllUtforandeVerksamheter()"
></msfa-tree-nodes-selector>
<msfa-tree-nodes-selector
*ngIf="!selectAllUtforandeVerksamheterFormControl.value"
[buttonElementId]="utforandeVerksamhetElementId"
[headingText]="'Välj utförande verksamheter och adresser'"
[formControlName]="utforandeVerksamheterFormControlName"
[isInvalid]="editEmployeeFormGroup.invalid && editEmployeeFormGroup.errors?.noUtforandeVerksamhetSelected"
[showValidation]="utforandeVerksamheterFormControl?.touched"
[validationMessages]="editEmployeeFormGroup.errors?.noUtforandeVerksamhetSelected ? [utforandeVerksamhetRequiredMessage] : []"
(selectedTreeNodesChanged)="updateToggleAllUtforandeVerksamheter()"
></msfa-tree-nodes-selector>
</ng-container>
<ng-template #loadingUtforandeVerksamheterTemplate>
<digi-ng-loader-spinner
*ngIf="isLoadingUtforandeVerksamheter"
[afLabel]="'Läser in utförande verksamheter för vald/a tjänster...'"
></digi-ng-loader-spinner>
</ng-template>
</div>
<div class="edit-employee-form__block" *ngIf="rolesFormGroup && availableRoles" [formGroup]="rolesFormGroup">

View File

@@ -3,12 +3,14 @@ import { FormSelectItem } from '@af/digi-ng/_form/form-select';
import {
ChangeDetectionStrategy,
Component,
ElementRef,
EventEmitter,
Input,
OnChanges,
OnInit,
Output,
SimpleChanges,
ViewChild,
} from '@angular/core';
import { AbstractControl, FormControl, FormGroup } from '@angular/forms';
import { RoleEnum } from '@msfa-enums/role.enum';
@@ -19,13 +21,12 @@ import { Role } from '@msfa-models/role.model';
import { Tjanst } from '@msfa-models/tjanst.model';
import { UtforandeVerksamhet } from '@msfa-models/utforande-verksamhet.model';
import { UtforandeVerksamheterService } from '@msfa-services/utforande-verksamheter/utforande-verksamheter.service';
import {
TreeNode,
TreeNodesSelectorService,
} from '@msfa-shared/components/tree-nodes-selector/services/tree-nodes-selector.service';
import { ValidationErrorLink } from '@msfa-shared/components/error-list/error-list.component';
import { TreeNodesSelectorService } from '@msfa-shared/components/tree-nodes-selector/services/tree-nodes-selector.service';
import { EmailValidator } from '@msfa-utils/validators/email.validator';
import { EmployeeValidator } from '@msfa-utils/validators/employee.validator';
import { RequiredValidator } from '@msfa-utils/validators/required.validator';
import { TreeNodeValidator } from '@msfa-utils/validators/tree-node.validator';
import { UUID } from 'angular2-uuid';
import { EmployeeFormService } from '../services/employee-form.service';
@Component({
@@ -35,9 +36,12 @@ import { EmployeeFormService } from '../services/employee-form.service';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class EditEmployeeFormComponent implements OnInit, OnChanges {
@ViewChild('editEmployeeFormContainer') editEmployeeFormContainer: ElementRef;
@Input() employee: Employee;
@Input() availableRoles: Role[];
@Input() availableTjanster: Tjanst[];
@Input() isLoadingUtforandeVerksamheter = false;
@Input() availableUtforandeVerksamheter: UtforandeVerksamhet[];
@Input() errorWhileUpdating: CustomError;
@@ -51,14 +55,21 @@ export class EditEmployeeFormComponent implements OnInit, OnChanges {
readonly emailFormControlName = 'email';
readonly tjansterFormControlName = 'tjanster';
readonly utforandeVerksamhetFormControlName = 'utforandeVerksamheter';
readonly toggleAllUtforandeVerksamhetFormControlName = 'allaUtforandeVerksamheter';
readonly utforandeVerksamheterFormControlName = 'utforandeVerksamheter';
readonly selectAllUtforandeVerksamheterFormControlName = 'allaUtforandeVerksamheter';
readonly utforandeVerksamhetRequiredMessage = 'Välj minst en utförande verksamhet';
readonly formUuid = UUID.UUID();
readonly emailElementId = `email-control-${this.formUuid}`;
readonly tjansterElementId = `tjanster-control-${this.formUuid}`;
readonly utforandeVerksamhetElementId = `utforande-verksamhet-control-${this.formUuid}`;
readonly firstValidationErrorLinkId = `validation-error-link-${this.formUuid}`;
editEmployeeFormGroup: FormGroup | null = null;
displayEditWithoutRolesDialog = false;
displayPristineWarning = false;
displayRolesDialog = false;
displayErrorSummary = false;
selectableTjansterFormItems: Array<FormSelectItem> | null = null;
@@ -88,9 +99,11 @@ export class EditEmployeeFormComponent implements OnInit, OnChanges {
this.editEmployeeFormGroup.patchValue(
Object.fromEntries([
[
this.utforandeVerksamhetFormControlName,
this.utforandeVerksamheterFormControlName,
this.utforandeVerksamheterService.getTreeNodeDataFromUtforandeVerksamheter(
this.availableUtforandeVerksamheter
this.availableUtforandeVerksamheter,
this.employee?.utforandeVerksamheter,
this.employee?.allaUtforandeVerksamheter
),
],
])
@@ -112,23 +125,23 @@ export class EditEmployeeFormComponent implements OnInit, OnChanges {
return this.editEmployeeFormGroup.get('roles');
}
get utforandeVerksamhetFormControl(): AbstractControl | undefined {
return this.editEmployeeFormGroup?.get(this.utforandeVerksamhetFormControlName);
get utforandeVerksamheterFormControl(): AbstractControl | undefined {
return this.editEmployeeFormGroup?.get(this.utforandeVerksamheterFormControlName);
}
get toggleAllUtforandeVerksamhetFormControl(): AbstractControl | undefined {
return this.editEmployeeFormGroup?.get(this.toggleAllUtforandeVerksamhetFormControlName);
get selectAllUtforandeVerksamheterFormControl(): AbstractControl | undefined {
return this.editEmployeeFormGroup?.get(this.selectAllUtforandeVerksamheterFormControlName);
}
private updateUtforandeVerksamhetStatus(): void {
if (this.availableUtforandeVerksamheter && this.availableUtforandeVerksamheter.length > 0) {
this.utforandeVerksamhetFormControl.enable();
this.toggleAllUtforandeVerksamhetFormControl.enable();
this.utforandeVerksamheterFormControl.enable();
this.selectAllUtforandeVerksamheterFormControl.enable();
return;
}
this.utforandeVerksamhetFormControl.disable();
this.toggleAllUtforandeVerksamhetFormControl.disable();
this.utforandeVerksamheterFormControl.disable();
this.selectAllUtforandeVerksamheterFormControl.disable();
}
private updateSelectableTjansterFormItems(): void {
@@ -143,22 +156,32 @@ export class EditEmployeeFormComponent implements OnInit, OnChanges {
? this.availableTjanster.find(tjanst => tjanst.code === currentTjanst.tjansteKod).tjanstId
: null;
this.editEmployeeFormGroup = new FormGroup({
email: new FormControl(this.employee.email, [RequiredValidator('E-postadress'), EmailValidator()]),
tjanster: new FormControl(tjanstId, [RequiredValidator('Tjänst')]),
roles: this.employeeFormService.getRolesFormGroup(this.availableRoles, this.employee.roles),
utforandeVerksamheter: new FormControl(
this.utforandeVerksamheterService.getTreeNodeDataFromUtforandeVerksamheter(this.availableUtforandeVerksamheter),
[
TreeNodeValidator.IsValidTreeNode(
this.utforandeVerksamheterService.hasSelectedUtforandeVerksamhet,
'required',
this.toggleAllUtforandeVerksamhetFormControl
this.editEmployeeFormGroup = new FormGroup(
{
email: new FormControl(this.employee.email, [
RequiredValidator('E-postadress'),
EmailValidator('e-postadress'),
]),
tjanster: new FormControl(tjanstId, [RequiredValidator('Tjänst')]),
roles: this.employeeFormService.getRolesFormGroup(this.availableRoles, this.employee.roles),
utforandeVerksamheter: new FormControl(
this.utforandeVerksamheterService.getTreeNodeDataFromUtforandeVerksamheter(
this.availableUtforandeVerksamheter,
this.employee?.utforandeVerksamheter,
this.employee?.allaUtforandeVerksamheter
),
]
),
allaUtforandeVerksamheter: new FormControl(this.employee.allaUtforandeVerksamheter),
});
[]
),
allaUtforandeVerksamheter: new FormControl(this.employee.allaUtforandeVerksamheter),
},
{
validators: EmployeeValidator.HasSelectedAtLeastOneUtforandeVerksamhet(
this.utforandeVerksamheterFormControlName,
this.selectAllUtforandeVerksamheterFormControlName,
this.utforandeVerksamheterService.hasSelectedUtforandeVerksamhet
),
}
);
this.updateUtforandeVerksamhetStatus();
}
@@ -172,22 +195,27 @@ export class EditEmployeeFormComponent implements OnInit, OnChanges {
}
onFormSubmitted(saveWithoutRoles = false): void {
if (!this.editEmployeeFormGroup) {
if (!this.editEmployeeFormGroup || this.isLoadingUtforandeVerksamheter) {
return;
}
this.displayPristineWarning = this.editEmployeeFormGroup.pristine;
this.editEmployeeFormGroup.markAllAsTouched();
if (this.editEmployeeFormGroup.invalid) {
if (this.editEmployeeFormGroup.invalid || this.editEmployeeFormGroup.pristine) {
this.displayErrorSummary = true;
setTimeout(() => {
this.focusLinkElement('.error-list__validation-error-link a');
});
return;
}
if (this.editEmployeeFormGroup.pristine) {
this.displayPristineWarning = true;
return;
}
this.displayErrorSummary = false;
const roles = this.employeeFormService.getRolesFromFormGroup(this.rolesFormGroup, this.availableRoles);
if (!roles.length && !saveWithoutRoles) {
this.displayEditWithoutRolesDialog = true;
return;
@@ -204,21 +232,67 @@ export class EditEmployeeFormComponent implements OnInit, OnChanges {
RoleEnum.MSFA_Standard,
]),
],
adressIds: this.toggleAllUtforandeVerksamhetFormControl.value
adressIds: this.selectAllUtforandeVerksamheterFormControl.value
? []
: this.utforandeVerksamheterService.getSelectedAdressIdsFromTreeNode(
this.utforandeVerksamhetFormControl?.value
this.utforandeVerksamheterFormControl?.value
),
allaUtforandeVerksamheter: !!this.toggleAllUtforandeVerksamhetFormControl.value,
allaUtforandeVerksamheter: !!this.selectAllUtforandeVerksamheterFormControl.value,
});
}
toggleTjanst(): void {
if (this.tjansterFormControl.value) {
this.tjansterSelected.emit(
this.employeeFormService.getSelectedTjanster(this.availableTjanster, +this.tjansterFormControl.value)
);
getValidationErrorLinks(): ValidationErrorLink[] {
let validationErrorLinks: ValidationErrorLink[] = [];
if (!this.editEmployeeFormGroup) {
return;
}
if (this.emailFormControl?.errors) {
validationErrorLinks = validationErrorLinks.concat({
elementId: this.emailElementId,
text: this.emailFormControl?.errors?.message as string,
});
}
if (this.tjansterFormControl?.errors) {
validationErrorLinks = validationErrorLinks.concat({
elementId: this.tjansterElementId,
text: this.tjansterFormControl?.errors?.message as string,
});
}
if (this.editEmployeeFormGroup.errors?.noUtforandeVerksamhetSelected) {
validationErrorLinks = validationErrorLinks.concat({
elementId: this.utforandeVerksamhetElementId,
text: this.utforandeVerksamhetRequiredMessage,
});
}
return validationErrorLinks;
}
focusLinkElement(selector: string): void {
let errorListElement: HTMLElement = null;
let linkElement: HTMLLinkElement = null;
if (!this.editEmployeeFormContainer || !this.editEmployeeFormContainer.nativeElement) {
return;
}
errorListElement = this.editEmployeeFormContainer.nativeElement as HTMLElement;
linkElement = errorListElement?.querySelector(selector);
linkElement?.focus();
}
toggleTjanst(): void {
if (!this.tjansterFormControl.value) {
return;
}
this.tjansterSelected.emit(
this.employeeFormService.getSelectedTjanster(this.availableTjanster, +this.tjansterFormControl.value)
);
}
openRolesDialog(): void {
@@ -229,23 +303,13 @@ export class EditEmployeeFormComponent implements OnInit, OnChanges {
this.displayRolesDialog = false;
}
toggleAllUtforandeVerksamheter(selectAll: boolean): void {
let treeNode: TreeNode = this.utforandeVerksamhetFormControl.value as TreeNode;
treeNode = selectAll
? this.treeNodesSelectorService.getTreeNodeWithAllNodesSelected(treeNode)
: this.treeNodesSelectorService.getTreeNodeWithNoNodesSelected(treeNode);
this.editEmployeeFormGroup.patchValue(Object.fromEntries([[this.utforandeVerksamhetFormControlName, treeNode]]));
}
updateToggleAllUtforandeVerksamheter(): void {
const hasSelectedAllLeafNodes = this.treeNodesSelectorService.hasSelectedAllLeafNodes(
this.utforandeVerksamhetFormControl.value
this.utforandeVerksamheterFormControl.value
);
this.editEmployeeFormGroup.patchValue(
Object.fromEntries([[this.toggleAllUtforandeVerksamhetFormControlName, hasSelectedAllLeafNodes]])
Object.fromEntries([[this.selectAllUtforandeVerksamheterFormControlName, hasSelectedAllLeafNodes]])
);
}

View File

@@ -1,6 +1,6 @@
<msfa-layout>
<digi-typography>
<section class="employee-form" *ngIf="employee$ | async as employee">
<section class="employee-form">
<header class="employee-form__header">
<h1>Redigera personalkonto</h1>
<msfa-employee-delete [returnToEmployeeList]="true"></msfa-employee-delete>
@@ -13,41 +13,46 @@
Ta bort konto
</digi-button> -->
</header>
<div class="employee-form__block">
<h2>Personuppgifter</h2>
<dl>
<dt>Förnamn</dt>
<dd>{{employee.firstName}}</dd>
</dl>
<dl>
<dt>Efternamn</dt>
<dd>{{employee.lastName}}</dd>
</dl>
<dl>
<dt>Personnummer</dt>
<dd>
<msfa-hide-text
symbols="********-****"
[changingText]="employee.ssn"
ariaLabelType="personnummer"
></msfa-hide-text>
</dd>
</dl>
</div>
<div class="employee-form__block">
<msfa-edit-employee-form
*ngIf="employee && (tjanster$ | async)"
[employee]="employee"
[availableRoles]="availableRoles"
[availableTjanster]="tjanster$ | async"
[availableUtforandeVerksamheter]="availableUtforandeVerksamheter$ | async"
[errorWhileUpdating]="errorWhileUpdating$ | async"
(tjansterSelected)="setupAvailableUtforandeVerksamheter($event)"
(formSubmitted)="updateEmployee($event)"
(closeError)="closeError()"
></msfa-edit-employee-form>
</div>
<ng-container *ngIf="employee$ | async as employee; else isLoadingEmployeeTemplate">
<div class="employee-form__block">
<h2>Personuppgifter</h2>
<dl>
<dt>Förnamn</dt>
<dd>{{employee.firstName}}</dd>
</dl>
<dl>
<dt>Efternamn</dt>
<dd>{{employee.lastName}}</dd>
</dl>
<dl>
<dt>Personnummer</dt>
<dd>
<msfa-hide-text
symbols="********-****"
[changingText]="employee.ssn"
ariaLabelType="personnummer"
></msfa-hide-text>
</dd>
</dl>
</div>
<div class="employee-form__block">
<msfa-edit-employee-form
*ngIf="employee && (tjanster$ | async)"
[employee]="employee"
[availableRoles]="availableRoles"
[availableTjanster]="tjanster$ | async"
[isLoadingUtforandeVerksamheter]="isLoadingUtforandeVerksamheter$ | async"
[availableUtforandeVerksamheter]="availableUtforandeVerksamheter$ | async"
[errorWhileUpdating]="errorWhileUpdating$ | async"
(tjansterSelected)="setupAvailableUtforandeVerksamheter($event)"
(formSubmitted)="updateEmployee($event)"
(closeError)="closeError()"
></msfa-edit-employee-form>
</div>
</ng-container>
<ng-template #isLoadingEmployeeTemplate>
<digi-ng-skeleton-base [afCount]="3" afText="Laddar personalkonto..."></digi-ng-skeleton-base>
</ng-template>
</section>
</digi-typography>
</msfa-layout>

View File

@@ -10,8 +10,8 @@ import { EmployeeService } from '@msfa-services/api/employee.service';
import { TjanstService } from '@msfa-services/api/tjanst.service';
import { RoleService } from '@msfa-services/role.service';
import { UtforandeVerksamheterService } from '@msfa-services/utforande-verksamheter/utforande-verksamheter.service';
import { BehaviorSubject, Observable } from 'rxjs';
import { filter, switchMap } from 'rxjs/operators';
import { BehaviorSubject, Observable, of } from 'rxjs';
import { catchError, mapTo, startWith, switchMap } from 'rxjs/operators';
@Component({
selector: 'msfa-employee-form',
@@ -23,16 +23,18 @@ export class EmployeeFormComponent implements OnInit {
private _employeeId$ = new BehaviorSubject<string>(this.activatedRoute.snapshot.params['employeeId']);
private _selectedTjanstIds$ = new BehaviorSubject<number[]>(null);
private _errorWhileUpdating$ = new BehaviorSubject<CustomError>(null);
errorWhileUpdating$: Observable<CustomError> = this._errorWhileUpdating$.asObservable();
employee$ = this.employeeService.employee$;
tjanster$: Observable<Tjanst[]> = this.tjanstService.tjanster$;
availableUtforandeVerksamheter$: Observable<UtforandeVerksamhet[]> = this._selectedTjanstIds$.pipe(
filter(selectedTjanstIds => !!selectedTjanstIds?.length),
switchMap(selectedTjanstIds => this.utforandeVerksamheterService.fetchUtforandeVerksamheter$(selectedTjanstIds))
);
errorWhileUpdating$: Observable<CustomError> = this._errorWhileUpdating$.asObservable();
employee$ = this.employeeService.employee$;
tjanster$: Observable<Tjanst[]> = this.tjanstService.tjanster$;
availableRoles: Role[] = this.roleService.allRoles;
isLoadingUtforandeVerksamheter$: Observable<boolean>;
constructor(
private employeeService: EmployeeService,
private roleService: RoleService,
@@ -68,6 +70,11 @@ export class EmployeeFormComponent implements OnInit {
setupAvailableUtforandeVerksamheter(selectedTjanster: Tjanst[]): void {
this._selectedTjanstIds$.next(selectedTjanster.map(tjanst => tjanst.tjanstId));
this.isLoadingUtforandeVerksamheter$ = this.availableUtforandeVerksamheter$.pipe(
mapTo(false),
catchError(() => of(false)),
startWith(true)
);
}
setEmployeeToDelete(employee: Employee): void {
this.employeeService.setEmployeeToDelete(employee);

View File

@@ -6,6 +6,7 @@ import { DigiNgFormInputModule } from '@af/digi-ng/_form/form-input';
import { DigiNgFormRadiobuttonGroupModule } from '@af/digi-ng/_form/form-radiobutton-group';
import { DigiNgFormSelectModule } from '@af/digi-ng/_form/form-select';
import { DigiNgPopoverModule } from '@af/digi-ng/_popover/popover';
import { DigiNgLoaderSpinnerModule } from '@af/digi-ng/_loader/loader-spinner';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
@@ -18,6 +19,8 @@ import { LocalDatePipeModule } from '@msfa-shared/pipes/local-date/local-date.mo
import { EmployeeDeleteModule } from '../../components/employee-delete/employee-delete.module';
import { EditEmployeeFormComponent } from './edit-employee-form/edit-employee-form.component';
import { EmployeeFormComponent } from './employee-form.component';
import { DigiNgSkeletonBaseModule } from '@af/digi-ng/_skeleton/skeleton-base';
import { ErrorListModule } from '@msfa-shared/components/error-list/error-list.module';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
@@ -34,11 +37,14 @@ import { EmployeeFormComponent } from './employee-form.component';
DigiNgPopoverModule,
DigiNgFormCheckboxModule,
DigiNgButtonModule,
DigiNgLoaderSpinnerModule,
DigiNgSkeletonBaseModule,
LayoutModule,
EmployeeDeleteModule,
DigiNgDialogModule,
HideTextModule,
TreeNodesSelectorModule,
ErrorListModule,
RolesDialogModule,
],
})

View File

@@ -0,0 +1,34 @@
<div class="error-list-wrapper" [hidden]="!validationErrorLinks || validationErrorLinks.length === 0">
<digi-notification-alert
class="error-list"
af-variation="danger"
[attr.af-heading]="headingText"
[af-heading-level]="headingLevel"
[afCloseable]="false"
>
<ul class="error-list__validation-error-links">
<li *ngFor="let validationErrorLink of validationErrorLinks;">
<digi-ng-link-internal
msfaAnchorLink
class="error-list__validation-error-link"
[afHref]="'#' + validationErrorLink.elementId"
[afText]="validationErrorLink.text"
></digi-ng-link-internal>
</li>
</ul>
</digi-notification-alert>
</div>
<!-- <digi-form-error-list
class="edit-employee-form__validation-error-summary"
af-heading="Åtgärda följande fel för att spara dina ändringar:"
*ngIf="validationErrorLinks && validationErrorLinks.length !== 0"
>
Behöver hantera ankarlänkar kopplat till den här komponenten om det ska fungera att använda den...
<a
[attr.href]="'#' + validationErrorLink.elementId"
[attr.id]="first ? firstValidationErrorLinkId : 'validation-error-link-' + index"
*ngFor="let validationErrorLink of validationErrorLinks; let first = first; let index = index"
>
{{ validationErrorLink.text }}
</a>
</digi-form-error-list> -->

View File

@@ -0,0 +1,15 @@
@import 'mixins/list';
@import 'variables/gutters';
.error-list {
display: block;
margin: 1.5rem 0;
&__validation-error-links {
@include msfa__reset-list;
display: flex;
flex-direction: column;
gap: $digi--layout--gutter;
margin: 1.5rem 0;
}
}

View File

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ErrorListComponent } from './error-list.component';
describe('ErrorListComponent', () => {
let component: ErrorListComponent;
let fixture: ComponentFixture<ErrorListComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ ErrorListComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ErrorListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,19 @@
import { TypographyDynamicHeadingLevel } from '@af/digi-ng/_typography/typography-dynamic-heading';
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
export interface ValidationErrorLink {
elementId: string;
text: string;
}
@Component({
selector: 'msfa-error-list',
templateUrl: './error-list.component.html',
styleUrls: ['./error-list.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ErrorListComponent {
@Input() validationErrorLinks: ValidationErrorLink[] = [];
@Input() headingText: string;
@Input() headingLevel: TypographyDynamicHeadingLevel = TypographyDynamicHeadingLevel.H3;
}

View File

@@ -0,0 +1,13 @@
import { DigiNgLinkInternalModule } from '@af/digi-ng/_link/link-internal';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { AnchorLinkModule } from '@msfa-directives/anchor-link.module';
import { ErrorListComponent } from './error-list.component';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [ErrorListComponent],
imports: [CommonModule, AnchorLinkModule, DigiNgLinkInternalModule],
exports: [ErrorListComponent],
})
export class ErrorListModule {}

View File

@@ -1,88 +1,115 @@
<div class="expanded-tree-node" *ngIf="treeNodeModel">
<digi-ng-typography-dynamic-heading
class="expanded-tree-node__heading"
[afText]="getTreeNodeHeadingText(treeNodeModel)"
[afLevel]="headingLevel"
></digi-ng-typography-dynamic-heading>
<p class="expanded-tree-node__info" *ngIf="treeNodeModel.showGeneralInfoAboutGrandChildren">
{{treeNodeModel.grandChildrenInfo}}
</p>
<div
*ngIf="treeNodeModel.children && !treeNodeModel.showGeneralInfoAboutGrandChildren"
class="expanded-tree-node__filter"
>
<digi-form-input-search
[attr.af-variation]="FormInputSearchVariation.S"
[attr.af-button-text]="getFilterButtonAriaLabelText(treeNodeModel)"
[attr.af-button-type]="ButtonType.BUTTON"
[attr.af-label]="' '"
[attr.af-aria-labelledby]="getFilterDescriptionId(treeNodeModel)"
(afOnFocusOutside)="onFocusOutsideFilter($event)"
(afOnChange)="onFilterTextChanged($event, treeNodeModel)"
(afOnKeyup)="onFilterTextChanged($event, treeNodeModel)"
></digi-form-input-search>
<div class="msfa__a11y-sr-only" [attr.id]="getFilterDescriptionId(treeNodeModel)">
Filtrera valbara alternativ där deras namn måste innehålla den angivna texten.
</div>
<div
class="expanded-tree-node"
[ngClass]="{
'expanded-tree-node--is-root-node' : treeNodeModel?.isRoot
}"
>
<div class="expanded-tree-node__content">
<digi-ng-typography-dynamic-heading
class="expanded-tree-node__heading"
[afText]="headingText"
[afLevel]="headingLevel"
></digi-ng-typography-dynamic-heading>
<ng-container *ngIf="showGeneralInfo else treeNodeChildrenTemplate">
<p class="expanded-tree-node__info">{{generalInfo}}</p>
</ng-container>
</div>
<div
*ngIf="hasChildLeafNodes(treeNodeModel)"
class="expanded-tree-node__node-checkbox-presentation expanded-tree-node__node-checkbox-presentation--toggle-all"
[ngClass]="{
'expanded-tree-node__node-checkbox-presentation--focus': treeNodeModel.toggleAllHasFocus,
'expanded-tree-node__node-checkbox-presentation--checked': allChildLeafNodesAreSelected(treeNodeModel)
}"
[attr.id]="getPresentationToggleAllId(treeNodeModel)"
(click)="nodePresentationToggleAllClicked(treeNodeModel)"
>
<span class="expanded-tree-node__node-checkbox-presentation__box"></span>
<span class="expanded-tree-node__node-checkbox-presentation__text">{{treeNodeModel.toggleAllChildrenLabel}}</span>
</div>
<ng-container *ngIf="visibleChildren">
<ul *ngIf="!treeNodeModel.showGeneralInfoAboutGrandChildren" class="expanded-tree-node__nodes">
<li
*ngFor="let childNode of visibleChildren"
[attr.id]="getPresentationItemId(childNode)"
[ngClass]="{'expanded-tree-node__node--leaf' : isLeafNode(childNode)}"
class="expanded-tree-node__node"
>
<ng-container
[ngTemplateOutlet]="childNode.children ? nodeExpansionPresentationTemplate : nodeCheckboxPresentationTemplate"
[ngTemplateOutletContext]="{node: childNode, parentNode: treeNodeModel}"
></ng-container>
</li>
</ul>
</ng-container>
<ng-template #nodeCheckboxPresentationTemplate let-node="node">
<div
class="expanded-tree-node__node-checkbox-presentation"
[ngClass]="{
'expanded-tree-node__node-checkbox-presentation--focus': node.hasFocus,
'expanded-tree-node__node-checkbox-presentation--checked': node.isSelected
}"
(click)="nodePresentationItemClicked(node)"
>
<span class="expanded-tree-node__node-checkbox-presentation__box"></span>
<span class="expanded-tree-node__node-checkbox-presentation__text">{{node.label}}</span>
</div>
<ng-template #treeNodeChildrenTemplate>
<ng-container *ngIf="treeNodeModel">
<div *ngIf="treeNodeModel.children" class="expanded-tree-node__filter">
<digi-form-input-search
[attr.af-variation]="FormInputSearchVariation.S"
[attr.af-button-text]="getFilterButtonAriaLabelText(treeNodeModel)"
[attr.af-button-type]="ButtonType.BUTTON"
[attr.af-label]="' '"
[attr.af-aria-labelledby]="'filter-description-'+treeNodeModel.uuid"
[attr.af-value]="treeNodeModel.filterText"
(afOnFocusOutside)="onFocusOutsideFilter($event)"
(afOnChange)="onFilterTextChanged($event, treeNodeModel)"
(afOnKeyup)="onFilterTextChanged($event, treeNodeModel)"
></digi-form-input-search>
<div class="msfa__a11y-sr-only" [attr.id]="'filter-description-'+treeNodeModel.uuid">
Filtrera valbara alternativ där deras namn måste innehålla den angivna texten.
</div>
</div>
<digi-form-checkbox
*ngIf="hasChildLeafNodes(treeNodeModel)"
class="expanded-tree-node__child-node-checkbox expanded-tree-node__child-node-checkbox--toggle-all"
[attr.af-label]="treeNodeModel.toggleAllChildrenLabel"
[attr.af-aria-labelledby]="'toggle-all-description-'+treeNodeModel.uuid"
[attr.af-checked]="allChildLeafNodesAreSelected(treeNodeModel)"
(afOnChange)="onToggleAllChildLeafNodes(treeNodeModel)"
></digi-form-checkbox>
<div class="msfa__a11y-sr-only" [attr.af-id]="'toggle-all-description-'+treeNodeModel.uuid">
{{getAriaLabelForToggleAllButton(treeNodeModel)}}
</div>
<ul *ngIf="visibleChildren" class="expanded-tree-node__nodes">
<li *ngFor="let childNode of visibleChildren" class="expanded-tree-node__node">
<ng-container
[ngTemplateOutlet]="childNode.children ? nodeExpansionTemplate : nodeCheckboxTemplate"
[ngTemplateOutletContext]="{node: childNode, parentNode: treeNodeModel, isExpanded: isExpandedNode(treeNodeModel, childNode)}"
>
</ng-container>
</li>
</ul>
<ng-container
*ngIf="showGeneralInfoTemplate(treeNodeModel)"
[ngTemplateOutlet]="generalInfoPanelTemplate"
></ng-container>
</ng-container>
</ng-template>
<ng-template #nodeExpansionPresentationTemplate let-node="node" let-parentNode="parentNode">
<div
class="expanded-tree-node__node-expansion-presentation"
<ng-template #nodeCheckboxTemplate let-node="node">
<digi-form-checkbox
class="expanded-tree-node__child-node-checkbox"
[attr.af-id]="'item-'+node.uuid"
[attr.af-label]="node.label"
[attr.af-checked]="node.isSelected"
[attr.af-value]="node.value"
(afOnChange)="onToggleSelected(node, $event)"
></digi-form-checkbox>
</ng-template>
<ng-template #nodeExpansionTemplate let-node="node" let-parentNode="parentNode" let-isExpanded="isExpanded">
<button
type="button"
class="expanded-tree-node__child-node-expansion-btn"
[ngClass]="{
'expanded-tree-node__node-expansion-presentation--focus': node.hasFocus,
'expanded-tree-node__node-expansion-presentation--active' : isExpandedNode(parentNode, node)
}"
(click)="nodePresentationItemClicked(node)"
'expanded-tree-node__child-node-expansion-btn--active' : isExpanded
}"
[attr.aria-expanded]="isExpanded"
[attr.aria-label]="getAriaLabelTextForExpansionButton(parentNode, node)"
[attr.aria-controls]="'expansion-panel-'+node.uuid"
(click)="onSetExpandedChild(parentNode, node)"
>
<span class="expanded-tree-node__node-expansion-presentation__text">{{node.label}}</span>
<span class="expanded-tree-node__child-node-expansion-btn__text">{{node.label}}</span>
<span
class="expanded-tree-node__node-expansion-presentation__has-selection-dot"
class="expanded-tree-node__child-node-expansion-btn__has-selection-dot"
*ngIf="hasSelectedDescendant(node)"
aria-hidden="true"
></span>
<digi-icon-arrow-right class="expanded-tree-node__node-expansion-presentation__icon"></digi-icon-arrow-right>
<digi-icon-arrow-right class="expanded-tree-node__child-node-expansion-btn__icon"></digi-icon-arrow-right>
</button>
<div
[attr.id]="'expansion-panel-'+node.uuid"
class="expanded-tree-node__child-node-expansion-panel"
[ngClass]="{'expanded-tree-node__child-node-expansion-panel--active': isExpanded}"
>
<msfa-expanded-tree-node
*ngIf="isExpanded"
[treeNodeModel]="node"
[headingText]="node.label"
[visibleChildren]="childNodeVisibleChildren"
(filterVisibleChildrenRequested)="updateChildNodeVisibleChildren($event)"
(selectedNodesChanged)="updateExpandedChildPanel($event)"
></msfa-expanded-tree-node>
</div>
</ng-template>
<ng-template #generalInfoPanelTemplate>
<div class="expanded-tree-node__child-node-expansion-panel expanded-tree-node__child-node-expansion-panel--active">
<msfa-expanded-tree-node
[headingText]="treeNodeModel.grandChildrenItemType"
[showGeneralInfo]="true"
[generalInfo]="treeNodeModel.grandChildrenInfo"
></msfa-expanded-tree-node>
</div>
</ng-template>
</div>

View File

@@ -4,11 +4,24 @@
@import 'variables/gutters';
.expanded-tree-node {
display: flex;
flex-direction: column;
min-height: 100%;
max-height: 300px;
overflow: auto;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
box-sizing: border-box;
padding-top: $digi--layout--gutter--m;
&--is-root-node {
position: static;
}
&__content {
display: flex;
flex-direction: column;
height: 100%;
overflow: auto;
}
&__filter {
padding: 0 0.9375rem;
@@ -60,53 +73,7 @@
}
}
&__node-checkbox-presentation {
font-size: var(--digi--typography--font-size--s);
font-weight: 400;
display: block;
position: relative;
padding-left: 1.875rem;
cursor: pointer;
line-height: normal;
&__box {
position: absolute;
top: 0;
left: 0;
height: 1.25rem;
width: 1.25rem;
border-radius: 0.1875rem;
background-color: var(--digi--ui--color--background);
border: 1px solid var(--digi--ui--color--background--dark);
}
&--toggle-all {
margin: 1.25rem $digi--layout--gutter;
}
&--checked &__box {
background-color: var(--digi--ui--color--success);
border-color: var(--digi--ui--color--success);
&::after {
position: absolute;
left: 0.375rem;
top: 0.0625rem;
width: 0.375rem;
height: 0.6875rem;
border: solid #fff;
border-width: 0 0.125rem 0.125rem 0;
transform: rotate(35deg);
content: '';
}
}
&--focus &__box {
box-shadow: var(--digi--ui--outline--focus--m);
}
}
&__node-expansion-presentation {
&__child-node-expansion-btn {
display: inline-flex;
padding: 0.3125rem 0.9375rem;
justify-content: space-between;
@@ -122,7 +89,7 @@
position: relative;
&:hover,
&--focus {
&:focus {
background-color: var(--digi--ui--color--background--secondary);
}
@@ -131,7 +98,7 @@
color: var(--digi--ui--color--background);
&:hover,
&--focus {
&:focus {
background-color: lighten($digi--ui--color--primary, 10%);
}
}
@@ -139,7 +106,6 @@
&__text {
text-align: left;
flex-grow: 1;
max-width: rem(450);
margin-right: $digi--layout--gutter--s;
display: block;
overflow: hidden;
@@ -163,4 +129,33 @@
max-height: 1em;
}
}
&__child-node-checkbox {
display: block;
margin: 0.3125rem $digi--layout--gutter;
&--toggle-all {
margin: 1.25rem $digi--layout--gutter;
}
::ng-deep {
.digi-form-checkbox__label {
display: block;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
&__child-node-expansion-panel {
&--active {
position: absolute;
top: 0;
left: 100%;
border-left: 1px solid var(--digi--typography--color--text--disabled);
height: 100%;
width: 100%;
}
}
}

View File

@@ -1,13 +1,12 @@
import { TypographyDynamicHeadingLevel } from '@af/digi-ng/_typography/typography-dynamic-heading';
import {
Component,
OnInit,
ChangeDetectionStrategy,
Input,
OnChanges,
SimpleChanges,
Output,
EventEmitter,
OnChanges,
SimpleChanges,
} from '@angular/core';
import { UnsubscribeDirective } from '@msfa-directives/unsubscribe.directive';
import { ButtonType } from '../../../../../pages/avrop/enums/button-type.enum';
@@ -26,42 +25,44 @@ import {
styleUrls: ['./expanded-tree-node.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ExpandedTreeNodeComponent extends UnsubscribeDirective implements OnInit, OnChanges {
@Input() treeNodeModel: TreeNodeModel | null = null;
export class ExpandedTreeNodeComponent extends UnsubscribeDirective implements OnChanges {
@Input() treeNodeModel: TreeNodeModel;
@Input() headingLevel: TypographyDynamicHeadingLevel = TypographyDynamicHeadingLevel.H3;
@Input() latestParentActionKey: string;
@Output() filterTreeNodeRequested = new EventEmitter<FilterTreeNodeData>();
@Output() clickAndFocusElementRequested = new EventEmitter<string>();
@Input() headingText: string;
@Input() showGeneralInfo: boolean;
@Input() generalInfo: string;
@Input() visibleChildren: Array<TreeNodeModel>;
@Output() filterVisibleChildrenRequested = new EventEmitter<FilterTreeNodeData>();
@Output() selectedNodesChanged = new EventEmitter<TreeNodeModel>();
readonly ButtonType = ButtonType;
readonly FormInputSearchVariation = FormInputSearchVariation;
visibleChildren: Array<TreeNodeModel> | null = null;
childNodeVisibleChildren: Array<TreeNodeModel>;
private readonly filterTreeNodeDebouncer: Subject<FilterTreeNodeData> = new Subject<FilterTreeNodeData>();
private readonly filterTreeNodeDebouncer: Subject<string> = new Subject<string>();
constructor(private treeNodesSelectorService: TreeNodesSelectorService) {
super();
super.unsubscribeOnDestroy(
this.filterTreeNodeDebouncer.pipe(debounceTime(200)).subscribe(filterTreeNodeData => {
this.filterTreeNodeRequested.emit(filterTreeNodeData);
this.filterTreeNodeDebouncer.pipe(debounceTime(200)).subscribe(text => {
this.filterVisibleChildrenRequested.emit({ text, treeNode: this.treeNodeModel });
})
);
}
ngOnInit(): void {
this.refreshComponentData();
}
ngOnChanges(changes: SimpleChanges): void {
if (changes.latestParentActionKey || changes.treeNodeModel) {
this.refreshComponentData();
}
}
let expandedChildNode: TreeNodeModel | null = null;
private refreshComponentData(): void {
this.visibleChildren = this.treeNodesSelectorService.getVisibleChildren(this.treeNodeModel);
if (changes.visibleChildren) {
expandedChildNode = this.visibleChildren?.find(childNode => this.isExpandedNode(this.treeNodeModel, childNode));
this.updateChildNodeVisibleChildren({
text: expandedChildNode?.filterText,
treeNode: expandedChildNode,
});
}
}
onFilterTextChanged(event: CustomEvent<{ target: { value: string } }>, treeNode: TreeNodeModel): void {
@@ -69,10 +70,7 @@ export class ExpandedTreeNodeComponent extends UnsubscribeDirective implements O
return;
}
this.filterTreeNodeDebouncer.next({
text: event.detail.target.value,
treeNode,
});
this.filterTreeNodeDebouncer.next(event.detail.target.value);
}
onFocusOutsideFilter(event: Event): void {
@@ -80,42 +78,68 @@ export class ExpandedTreeNodeComponent extends UnsubscribeDirective implements O
event.stopPropagation();
}
nodePresentationToggleAllClicked(treeNode: TreeNodeModel): void {
if (!treeNode) {
onToggleSelected(treeNode: TreeNodeModel, event: CustomEvent<{ target: { checked: boolean } }>): void {
if (!treeNode || !event?.detail?.target) {
return;
}
this.clickAndFocusElementRequested.emit(`#${this.treeNodesSelectorService.getToggleAllButtonId(treeNode)}`);
treeNode.isSelected = event?.detail?.target?.checked;
this.selectedNodesChanged.emit(this.treeNodeModel);
}
nodePresentationItemClicked(treeNode: TreeNodeModel): void {
if (!treeNode) {
onToggleAllChildLeafNodes(treeNode: TreeNodeModel): void {
let allChildLeafNodesAreSelected = false;
if (!treeNode || !treeNode.children) {
return;
}
this.clickAndFocusElementRequested.emit(`#${this.treeNodesSelectorService.getItemButtonId(treeNode)}`);
allChildLeafNodesAreSelected = this.allChildLeafNodesAreSelected(treeNode);
treeNode.children = treeNode.children.map(child => {
return { ...child, isSelected: !allChildLeafNodesAreSelected };
});
this.selectedNodesChanged.emit(this.treeNodeModel);
}
getPresentationItemId(treeNode: TreeNodeModel): string {
return this.treeNodesSelectorService.getPresentationItemId(treeNode);
onSetExpandedChild(parentTreeNode: TreeNodeModel, treeNode: TreeNodeModel): void {
if (!parentTreeNode || !treeNode) {
return;
}
parentTreeNode.expandedChildUuid = parentTreeNode.expandedChildUuid === treeNode.uuid ? null : treeNode.uuid;
this.updateChildNodeVisibleChildren({ text: treeNode.filterText, treeNode });
}
getPresentationToggleAllId(treeNode: TreeNodeModel): string {
return this.treeNodesSelectorService.getPresentationToggleAllId(treeNode);
updateExpandedChildPanel(treeNode: TreeNodeModel): void {
if (!this.childNodeVisibleChildren) {
return;
}
this.childNodeVisibleChildren = this.treeNodesSelectorService.getVisibleChildren(treeNode);
}
getFilterDescriptionId(treeNode: TreeNodeModel): string {
return this.treeNodesSelectorService.getFilterDescriptionId(treeNode);
updateChildNodeVisibleChildren(filterTreeNodeData: FilterTreeNodeData): void {
if (!filterTreeNodeData || !filterTreeNodeData.treeNode) {
return;
}
filterTreeNodeData.treeNode.children = this.treeNodesSelectorService.getFilteredTreeNodeChildren(
filterTreeNodeData
);
filterTreeNodeData.treeNode.filterText = filterTreeNodeData.text;
this.childNodeVisibleChildren = this.treeNodesSelectorService.getVisibleChildren(filterTreeNodeData.treeNode);
}
getFilterButtonAriaLabelText(treeNode: TreeNodeModel): string {
return this.treeNodesSelectorService.getFilterButtonAriaLabelText(treeNode);
}
getTreeNodeHeadingText(treeNode: TreeNodeModel): string {
return this.treeNodesSelectorService.getTreeNodeHeadingText(treeNode);
}
hasChildLeafNodes(node: TreeNodeModel): boolean {
return this.treeNodesSelectorService.hasChildLeafNodes(node);
}
@@ -135,4 +159,16 @@ export class ExpandedTreeNodeComponent extends UnsubscribeDirective implements O
hasSelectedDescendant(treeNode: TreeNodeModel): boolean {
return this.treeNodesSelectorService.hasSelectedLeafNodeDescendant(treeNode);
}
getAriaLabelTextForExpansionButton(parentTreeNode: TreeNodeModel, treeNode: TreeNodeModel): string {
return this.treeNodesSelectorService.getAriaLabelTextForExpansionButton(parentTreeNode, treeNode);
}
getAriaLabelForToggleAllButton(treeNode: TreeNodeModel): string {
return this.treeNodesSelectorService.getAriaLabelForToggleAllButton(treeNode);
}
showGeneralInfoTemplate(treeNode: TreeNodeModel): boolean {
return this.treeNodesSelectorService.showGeneralInfoTemplate(treeNode);
}
}

View File

@@ -1,6 +1,6 @@
<digi-util-detect-focus-outside (afOnFocusOutside)="closePanel()">
<digi-util-keydown-handler (afOnEsc)="closePanel()">
<section class="tree-nodes-selector-panel" #treeNodesSelectorPanel>
<section class="tree-nodes-selector-panel">
<header>
<h2 class="tree-nodes-selector-panel__heading">{{headingText}}</h2>
<p class="msfa__a11y-sr-only">
@@ -18,21 +18,13 @@
</button>
</header>
<div class="tree-nodes-selector-panel__content" *ngIf="pendingRootNode">
<ul class="tree-nodes-selector-panel__expanded-nodes" aria-hidden="true" *ngIf="expandedTreeNodes">
<li class="tree-nodes-selector-panel__expanded-node" *ngFor="let node of expandedTreeNodes">
<msfa-expanded-tree-node
[treeNodeModel]="node"
[latestParentActionKey]="latestParentActionKey"
(filterTreeNodeRequested)="filterTreeNode($event)"
(clickAndFocusElementRequested)="clickAndFocusElementByQuerySelector($event)"
></msfa-expanded-tree-node>
</li>
</ul>
<div class="msfa__a11y-sr-only">
<ng-container
[ngTemplateOutlet]="nodeTemplate"
[ngTemplateOutletContext]="{node: pendingRootNode}"
></ng-container>
<div class="tree-nodes-selector-panel__tree">
<msfa-expanded-tree-node
[treeNodeModel]="pendingRootNode"
[visibleChildren]="visibleChildren"
(selectedNodesChanged)="updateExpandedChildPanel($event)"
(filterVisibleChildrenRequested)="updateVisibleChildren($event)"
></msfa-expanded-tree-node>
</div>
</div>
<footer>
@@ -45,64 +37,6 @@
{{confirmationButtonText}}
</digi-button>
</footer>
<ng-template #nodeTemplate let-node="node">
<div class="tree-nodes-selector-panel__level">
<h3 class="tree-nodes-selector-panel__heading">{{node.label}}</h3>
<digi-form-checkbox
*ngIf="hasChildLeafNodes(node)"
[attr.af-id]="getToggleAllButtonId(node)"
[attr.af-label]="node.selectAllChildrenLabel"
[attr.af-aria-labelledby]="'toggle-all-description-'+node.uuid"
[attr.af-checked]="allChildLeafNodesAreSelected(node)"
(afOnChange)="toggleAllChildLeafNodes(node)"
(afOnFocus)="setFocusOnToggleAll(node, true)"
(afOnBlur)="setFocusOnToggleAll(node, false)"
></digi-form-checkbox>
<div [attr.af-id]="'toggle-all-description-'+node.uuid">{{getAriaLabelForToggleAllButton(node)}}</div>
<ul *ngIf="getVisibleChildren(node) as visibleChildren" class="tree-nodes-selector-panel__nodes">
<li
*ngFor="let childNode of visibleChildren"
class="tree-nodes-selector-panel__node"
[ngClass]="{'tree-nodes-selector-panel__node--expanded': isExpandedNode(node, childNode)}"
>
<ng-container
[ngTemplateOutlet]="childNode.children ? nodeExpansionTemplate : nodeCheckboxTemplate"
[ngTemplateOutletContext]="{node: childNode, parentNode: node}"
>
</ng-container>
</li>
</ul>
</div>
</ng-template>
<ng-template #nodeCheckboxTemplate let-node="node">
<digi-form-checkbox
[attr.af-id]="'item-'+node.uuid"
[attr.af-label]="node.label"
[attr.af-checked]="node.isSelected"
[attr.af-value]="node.value"
(afOnChange)="onToggleSelected(node, $event)"
(afOnFocus)="setFocusOnNodeItem(node, true)"
(afOnBlur)="setFocusOnNodeItem(node, false)"
></digi-form-checkbox>
</ng-template>
<ng-template #nodeExpansionTemplate let-node="node" let-parentNode="parentNode">
<button
type="button"
[attr.id]="getItemButtonId(node)"
[attr.aria-label]="getAriaLabelTextForExpansionButton(parentNode, node)"
(click)="setExpandedChild(parentNode, node)"
(focus)="setFocusOnNodeItem(node, true)"
(blur)="setFocusOnNodeItem(node, false)"
>
{{node.label}}
</button>
<ng-container
*ngIf="isExpandedNode(parentNode, node)"
[ngTemplateOutlet]="nodeTemplate"
[ngTemplateOutletContext]="{node: node}"
>
</ng-container>
</ng-template>
</section>
</digi-util-keydown-handler>
</digi-util-detect-focus-outside>

View File

@@ -41,28 +41,15 @@
font-size: var(--digi--typography--font-size--xs);
}
&__expanded-nodes {
@include msfa__reset-list;
display: flex;
flex-direction: row;
&__content {
overflow: auto;
max-width: 100%;
}
&__expanded-node {
position: relative;
display: flex;
flex-direction: column;
flex: 0 0 50%;
&__tree {
max-width: 50%;
overflow: auto;
padding-top: $digi--layout--gutter--m;
padding-bottom: $digi--layout--gutter--l;
border-left: 1px solid var(--digi--typography--color--text--disabled);
&:first-child {
border-left-width: 0;
}
height: 300px;
position: relative;
}
footer {

View File

@@ -1,14 +1,5 @@
import { ButtonSize } from '@af/digi-ng/_button/button';
import {
ChangeDetectionStrategy,
Component,
ElementRef,
EventEmitter,
Input,
OnInit,
Output,
ViewChild,
} from '@angular/core';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import {
FilterTreeNodeData,
TreeNodeModel,
@@ -28,13 +19,10 @@ export class TreeNodesSelectorPanelComponent implements OnInit {
@Output() selectedChangesConfirmed = new EventEmitter<TreeNodeModel>();
@Output() closePanelRequested = new EventEmitter<void>();
@ViewChild('treeNodesSelectorPanel') treeNodesSelector: ElementRef;
readonly ButtonSize = ButtonSize;
pendingRootNode: TreeNodeModel | null = null;
expandedTreeNodes: Array<TreeNodeModel> | null = null;
latestParentActionKey: string = null;
visibleChildren: Array<TreeNodeModel> | null = null;
constructor(private treeNodesSelectorService: TreeNodesSelectorService) {}
@@ -44,123 +32,18 @@ export class TreeNodesSelectorPanelComponent implements OnInit {
private setupPendingRootNode(): void {
this.pendingRootNode = this.treeNodesSelectorService.getClonedTreeNode(this.rootNode);
this.expandedTreeNodes = this.treeNodesSelectorService.getExpandedTreeNodes(this.pendingRootNode);
}
onToggleSelected(treeNode: TreeNodeModel, event: CustomEvent<{ target: { checked: boolean } }>): void {
if (!treeNode || !event?.detail?.target) {
if (!this.pendingRootNode) {
return;
}
treeNode.isSelected = event?.detail?.target?.checked;
// eslint-disable-next-line
this.latestParentActionKey = `${treeNode.uuid}-selected-${treeNode.isSelected}`;
}
setExpandedChild(parentTreeNode: TreeNodeModel, treeNode: TreeNodeModel): void {
if (!parentTreeNode || !treeNode) {
return;
}
parentTreeNode.expandedChildUuid = parentTreeNode.expandedChildUuid === treeNode.uuid ? null : treeNode.uuid;
this.expandedTreeNodes = this.treeNodesSelectorService.getExpandedTreeNodes(this.pendingRootNode);
this.latestParentActionKey = `${parentTreeNode.uuid}-expanded-child-${parentTreeNode.expandedChildUuid}`;
}
setFocusOnToggleAll(treeNode: TreeNodeModel, hasFocus: boolean): void {
let presentationToggleAllElement: HTMLElement = null;
let expansionColumnElement: HTMLElement = null;
if (!treeNode) {
return;
}
treeNode.toggleAllHasFocus = hasFocus;
if (!hasFocus) {
return;
}
presentationToggleAllElement = this.treeNodesSelectorService.getPresentationToggleAllElement(
this.treeNodesSelector,
treeNode
);
expansionColumnElement = this.treeNodesSelectorService.getExpansionColumnElement(presentationToggleAllElement);
this.scrollElementInContainer(expansionColumnElement);
}
private scrollElementInContainer(element: HTMLElement): void {
let elementRect: DOMRect = null;
let parentElementRect: DOMRect = null;
if (!element || !element.parentElement) {
return;
}
if (this.treeNodesSelectorService.isFullyVisibleElement(element, element.parentElement)) {
return;
}
elementRect = element.getBoundingClientRect();
parentElementRect = element.parentElement.getBoundingClientRect();
element.parentElement.scrollTop = elementRect.top + element.parentElement.scrollTop - parentElementRect.top;
element.parentElement.scrollLeft = elementRect.left + element.parentElement.scrollLeft - parentElementRect.left;
}
setFocusOnNodeItem(treeNode: TreeNodeModel, hasFocus: boolean): void {
let presentationItemElement: HTMLElement = null;
let expansionColumnElement: HTMLElement = null;
if (!treeNode) {
return;
}
treeNode.hasFocus = hasFocus;
if (!hasFocus) {
return;
}
this.latestParentActionKey = `${treeNode.uuid}-focused-item`;
presentationItemElement = this.treeNodesSelectorService.getPresentationItemElement(
this.treeNodesSelector,
treeNode
);
expansionColumnElement = this.treeNodesSelectorService.getExpansionColumnElement(presentationItemElement);
this.scrollElementInContainer(presentationItemElement);
this.scrollElementInContainer(expansionColumnElement);
}
toggleAllChildLeafNodes(treeNode: TreeNodeModel): void {
let allChildLeafNodesAreSelected = false;
if (!treeNode || !treeNode.children) {
return;
}
allChildLeafNodesAreSelected = this.allChildLeafNodesAreSelected(treeNode);
treeNode.children = treeNode.children.map(child => {
return { ...child, isSelected: !allChildLeafNodesAreSelected };
});
this.latestParentActionKey = `${treeNode.uuid}-selected-${allChildLeafNodesAreSelected.toString()}`;
this.pendingRootNode.isRoot = true;
this.updateExpandedChildPanel(this.pendingRootNode);
}
clearSelections(): void {
this.pendingRootNode = this.treeNodesSelectorService.getTreeNodeWithNoNodesSelected(this.pendingRootNode);
this.latestParentActionKey = `cleared-selections`;
this.expandedTreeNodes = this.treeNodesSelectorService.getExpandedTreeNodes(this.pendingRootNode);
this.updateExpandedChildPanel(this.pendingRootNode);
}
closePanel(): void {
@@ -168,65 +51,21 @@ export class TreeNodesSelectorPanelComponent implements OnInit {
this.closePanelRequested.emit();
}
filterTreeNode(filterTreeNodeData: FilterTreeNodeData): void {
if (!filterTreeNodeData || !filterTreeNodeData.treeNode) {
return;
}
filterTreeNodeData.treeNode.children = this.treeNodesSelectorService.getFilteredTreeNodeChildren(
filterTreeNodeData
);
this.latestParentActionKey = `${filterTreeNodeData.treeNode.uuid}-filtered-${filterTreeNodeData.text}`;
this.expandedTreeNodes = this.treeNodesSelectorService.getExpandedTreeNodes(this.pendingRootNode);
}
clickAndFocusElementByQuerySelector(selector: string): void {
const selectedItem = this.treeNodesSelector
? this.treeNodesSelectorService.getElementByQuerySelector(this.treeNodesSelector, selector)
: null;
if (!selectedItem) {
return;
}
selectedItem.focus();
selectedItem.click();
}
getVisibleChildren(treeNode: TreeNodeModel): Array<TreeNodeModel> | null {
return this.treeNodesSelectorService.getVisibleChildren(treeNode);
}
hasChildLeafNodes(node: TreeNodeModel): boolean {
return this.treeNodesSelectorService.hasChildLeafNodes(node);
}
allChildLeafNodesAreSelected(node: TreeNodeModel): boolean {
return this.treeNodesSelectorService.allChildLeafNodesAreSelected(node);
}
hasSelectedDescendant(treeNode: TreeNodeModel): boolean {
return this.treeNodesSelectorService.hasSelectedLeafNodeDescendant(treeNode);
}
getAriaLabelTextForExpansionButton(parentTreeNode: TreeNodeModel, treeNode: TreeNodeModel): string {
return this.treeNodesSelectorService.getAriaLabelTextForExpansionButton(parentTreeNode, treeNode);
updateExpandedChildPanel(treeNode: TreeNodeModel): void {
this.visibleChildren = this.treeNodesSelectorService.getVisibleChildren(treeNode);
}
getAriaLabelForToggleAllButton(treeNode: TreeNodeModel): string {
return this.treeNodesSelectorService.getAriaLabelForToggleAllButton(treeNode);
}
updateVisibleChildren(filterTreeNodeData: FilterTreeNodeData): void {
filterTreeNodeData.treeNode.children = this.treeNodesSelectorService.getFilteredTreeNodeChildren(
filterTreeNodeData
);
isExpandedNode(parentTreeNode: TreeNodeModel, treeNode: TreeNodeModel): boolean {
return this.treeNodesSelectorService.isExpandedNode(parentTreeNode, treeNode);
}
filterTreeNodeData.treeNode.filterText = filterTreeNodeData.text;
getToggleAllButtonId(treeNode: TreeNodeModel): string {
return this.treeNodesSelectorService.getToggleAllButtonId(treeNode);
}
getItemButtonId(treeNode: TreeNodeModel): string {
return this.treeNodesSelectorService.getItemButtonId(treeNode);
this.visibleChildren = this.treeNodesSelectorService.getVisibleChildren(filterTreeNodeData.treeNode);
}
}

View File

@@ -3,6 +3,7 @@
<button
#togglePanelBtn
(click)="togglePanel()"
[attr.id]="buttonElementId"
[attr.aria-controls]="panelId"
[ngClass]="{'tree-nodes-selector__toggle-panel-btn--invalid': isInvalid && showValidation}"
type="button"

View File

@@ -36,6 +36,7 @@ interface PropagateTouchedFn {
],
})
export class TreeNodesSelectorComponent implements ControlValueAccessor {
@Input() buttonElementId: string = UUID.UUID();
@Input() headingText: string;
@Input() confirmationButtonText = 'Spara';
@Input() isInvalid = false;

View File

@@ -59,71 +59,6 @@ describe('TreeNodesSelectorService', () => {
});
});
describe('getExpandedTreeNodes', () => {
it('should return a list of all tree nodes that are in an expanded chain starting with the provided tree node.', () => {
const treeNode: TreeNodeModel = {
label: 'root',
isSelected: false,
value: null,
uuid: '1',
expandedChildUuid: '1_1',
children: [
{
label: 'level 1',
isSelected: false,
value: null,
uuid: '1_1',
expandedChildUuid: '1_1_2',
children: [
{
label: 'level 2',
isSelected: false,
value: null,
uuid: '1_1_1',
},
{
label: 'level 2',
isSelected: true,
value: null,
uuid: '1_1_2',
},
],
},
{
label: 'level 1',
isSelected: true,
value: null,
uuid: '1_2',
expandedChildUuid: '1_2_2',
children: [
{
label: 'level 2',
isSelected: false,
value: null,
uuid: '1_2_1',
},
{
label: 'level 2',
isSelected: true,
value: null,
uuid: '1_2_2',
},
],
},
],
};
const result = service.getExpandedTreeNodes(treeNode);
expect(result).not.toBeUndefined();
expect(result).not.toBeNull();
expect(result.length).toEqual(3);
expect(result[0].uuid).toEqual('1');
expect(result[1].uuid).toEqual('1_1');
expect(result[2].uuid).toEqual('1_1_2');
});
});
describe('getFilteredTreeNodeChildren', () => {
it('should set the propert hideTreeNode to true for all children with a label that contains the provided text when compared case insensitive, trimming white space and return them.', () => {
const treeNode: TreeNodeModel = {

View File

@@ -1,4 +1,4 @@
import { ElementRef, Injectable } from '@angular/core';
import { Injectable } from '@angular/core';
import { UUID } from 'angular2-uuid';
export interface TreeNode {
@@ -18,8 +18,9 @@ export interface TreeNodeModel extends TreeNode {
children?: Array<TreeNodeModel>;
hasFocus?: boolean;
toggleAllHasFocus?: boolean;
showGeneralInfoAboutGrandChildren?: boolean;
hideTreeNode?: boolean;
isRoot?: boolean;
filterText?: string;
}
export interface FilterTreeNodeData {
@@ -82,30 +83,7 @@ export class TreeNodesSelectorService {
return `${isActiveNode ? 'Döljer' : 'Visar'} ${treeNode.grandChildrenItemType} för ${treeNode.label}`;
}
isFullyVisibleElement(element: HTMLElement, container: HTMLElement): boolean {
const { bottom, top, left, right } = element.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
if (top <= containerRect.top && containerRect.top - top > 0) {
return false;
}
if (bottom - containerRect.bottom > 0) {
return false;
}
if (left <= containerRect.left && containerRect.left - left > 0) {
return false;
}
if (right - containerRect.right > 0) {
return false;
}
return true;
}
hasChildLeafNodes(node: TreeNodeModel): boolean {
hasChildLeafNodes(node: TreeNode): boolean {
if (!node || !node.children || node.children.length === 0) {
return false;
}
@@ -125,42 +103,6 @@ export class TreeNodesSelectorService {
return !node.children.some(child => this.isLeafNode(child) && !child.isSelected);
}
getElementByQuerySelector(elementRef: ElementRef, selector: string): HTMLElement {
const element = elementRef?.nativeElement as HTMLElement;
return element?.querySelector(selector);
}
getPresentationItemElement(elementRef: ElementRef, treeNode: TreeNodeModel): HTMLElement {
return this.getElementByQuerySelector(elementRef, `#${this.getPresentationItemId(treeNode)}`);
}
getPresentationToggleAllElement(elementRef: ElementRef, treeNode: TreeNodeModel): HTMLElement {
return this.getElementByQuerySelector(elementRef, `#${this.getPresentationToggleAllId(treeNode)}`);
}
getExpansionColumnElement(element: HTMLElement): HTMLElement {
return element?.closest('.tree-nodes-selector-panel__expanded-node');
}
getExpandedTreeNodes(treeNode: TreeNodeModel): Array<TreeNodeModel> {
let expandedChildNode: TreeNodeModel | null = null;
if (!treeNode) {
return [];
}
expandedChildNode = treeNode.children?.find(childNode => this.isExpandedNode(treeNode, childNode));
if (expandedChildNode) {
return [treeNode].concat(this.getExpandedTreeNodes(expandedChildNode));
}
return treeNode.children && treeNode.children.some(child => child.children && child.children.length > 0)
? [treeNode, { ...treeNode, children: undefined, showGeneralInfoAboutGrandChildren: true }]
: [treeNode];
}
isExpandedNode(parentTreeNode: TreeNodeModel, treeNode: TreeNodeModel): boolean {
if (!parentTreeNode || !treeNode) {
return false;
@@ -210,35 +152,7 @@ export class TreeNodesSelectorService {
return '';
}
return `Filtrera ${treeNode.showGeneralInfoAboutGrandChildren ? treeNode.grandChildrenItemType : treeNode.label}`;
}
getTreeNodeHeadingText(treeNode: TreeNodeModel): string {
if (!treeNode) {
return '';
}
return treeNode.showGeneralInfoAboutGrandChildren ? treeNode.grandChildrenItemType : treeNode.label;
}
getToggleAllButtonId(treeNode: TreeNodeModel): string {
return `toggle-all-${treeNode.uuid}`;
}
getItemButtonId(treeNode: TreeNodeModel): string {
return `item-${treeNode.uuid}`;
}
getPresentationItemId(treeNode: TreeNodeModel): string {
return `presentation-item-${treeNode?.uuid}`;
}
getPresentationToggleAllId(treeNode: TreeNodeModel): string {
return `presentation-toggle-all-${treeNode?.uuid}`;
}
getFilterDescriptionId(treeNode: TreeNodeModel): string {
return `filter-description-${treeNode?.uuid}`;
return `Filtrera ${treeNode.label}`;
}
getCleanedText(text: string): string {
@@ -360,4 +274,20 @@ export class TreeNodesSelectorService {
? treeNode.isSelected
: !treeNode.children.some(childNode => !this.hasSelectedAllLeafNodes(childNode));
}
hasExpandedChild(treeNode: TreeNode): boolean {
if (!treeNode || !treeNode.children) {
return false;
}
return treeNode.children.some(childNode => this.isExpandedNode(treeNode, childNode));
}
showGeneralInfoTemplate(treeNode: TreeNode): boolean {
if (!treeNode || !treeNode.grandChildrenInfo || this.hasExpandedChild(treeNode)) {
return false;
}
return this.getCleanedText(treeNode.grandChildrenInfo).length > 0;
}
}

View File

@@ -0,0 +1,8 @@
import { AnchorLinkDirective } from './anchor-link.directive';
describe('AnchorLinkDirective', () => {
it('should create an instance', () => {
const directive = new AnchorLinkDirective();
expect(directive).toBeTruthy();
});
});

View File

@@ -0,0 +1,21 @@
import { Directive, HostListener } from '@angular/core';
@Directive({
selector: '[msfaAnchorLink]',
})
export class AnchorLinkDirective {
@HostListener('click', ['$event'])
onClick(event: MouseEvent): void {
const target = event.target as HTMLElement;
const link = target.tagName === 'a' ? target : target.closest('a');
const href = link?.getAttribute('href');
const element = document.getElementById(href?.trim().replace('#', ''));
if (element && element.focus) {
element.focus();
}
event.stopPropagation();
event.preventDefault();
}
}

View File

@@ -0,0 +1,8 @@
import { NgModule } from '@angular/core';
import { AnchorLinkDirective } from './anchor-link.directive';
@NgModule({
declarations: [AnchorLinkDirective],
exports: [AnchorLinkDirective],
})
export class AnchorLinkModule {}

View File

@@ -3,6 +3,7 @@ import { Injectable } from '@angular/core';
import { environment } from '@msfa-environment';
import { Params } from '@msfa-models/api/params.model';
import { UtforandeVerksamhetResponse } from '@msfa-models/api/utforande-verksamhet.response.model';
import { EmployeeUtforandeVerksamhet } from '@msfa-models/employee-utforande-verksamhet.model';
import { UtforandeAdress } from '@msfa-models/utforande-adress.model';
import { mapResponseToUtforandeVerksamhet, UtforandeVerksamhet } from '@msfa-models/utforande-verksamhet.model';
import {
@@ -21,7 +22,7 @@ export class UtforandeVerksamheterService {
constructor(private treeNodesSelectorService: TreeNodesSelectorService, private httpClient: HttpClient) {}
fetchUtforandeVerksamheter$(tjanstIds: number[]): Observable<UtforandeVerksamhet[]> {
if (!tjanstIds.length) {
if (!tjanstIds?.length) {
return of<UtforandeVerksamhet[]>([]);
}
@@ -38,10 +39,18 @@ export class UtforandeVerksamheterService {
return selectedUtforandeVerksamheter.map(uv => uv.adresser.map(adress => adress.id)).flat();
}
getTreeNodeDataFromUtforandeVerksamheter(utforandeVerksamhetList: UtforandeVerksamhet[]): TreeNode | null {
getTreeNodeDataFromUtforandeVerksamheter(
availableUtforandeVerksamhetList: UtforandeVerksamhet[],
selectedUtforandeVerksamhetList: EmployeeUtforandeVerksamhet[],
selectAll = false
): TreeNode | null {
let treeNode: TreeNode | null = null;
if (!utforandeVerksamhetList || utforandeVerksamhetList.length === 0 || !Array.isArray(utforandeVerksamhetList)) {
if (
!availableUtforandeVerksamhetList ||
availableUtforandeVerksamhetList.length === 0 ||
!Array.isArray(availableUtforandeVerksamhetList)
) {
return treeNode;
}
@@ -52,17 +61,24 @@ export class UtforandeVerksamheterService {
isSelected: false,
value: null,
childItemType: 'Utförande verksamheter',
children: utforandeVerksamhetList.map(
children: availableUtforandeVerksamhetList.map(
(utforandeVerksamhet: UtforandeVerksamhet): TreeNode => {
const utforandeVerksahmetTreeNode: TreeNode = {
label: utforandeVerksamhet.name,
toggleAllChildrenLabel: 'Välj alla adresser',
isSelected: false,
isSelected:
selectAll || this.isSelectedUtforandeVerksamhet(utforandeVerksamhet, selectedUtforandeVerksamhetList),
value: utforandeVerksamhet,
childItemType: 'Adresser',
children: utforandeVerksamhet.adresser
? utforandeVerksamhet.adresser.map(adress => {
return { label: adress.name, isSelected: false, value: adress };
return {
label: adress.name,
isSelected:
selectAll ||
this.isSelectedUtforandeAdress(utforandeVerksamhet.id, adress, selectedUtforandeVerksamhetList),
value: adress,
};
})
: [],
};
@@ -75,6 +91,52 @@ export class UtforandeVerksamheterService {
return treeNode;
}
isSelectedUtforandeVerksamhet(
utforandeVerksamhet: UtforandeVerksamhet,
selectedUtforandeVerksamhetList: EmployeeUtforandeVerksamhet[]
): boolean {
if (
!utforandeVerksamhet ||
!selectedUtforandeVerksamhetList ||
selectedUtforandeVerksamhetList.length === 0 ||
!Array.isArray(selectedUtforandeVerksamhetList)
) {
return false;
}
return selectedUtforandeVerksamhetList.some(
selectedUtforandeVerksamhet => selectedUtforandeVerksamhet.id === utforandeVerksamhet.id
);
}
isSelectedUtforandeAdress(
utforandeVerksamhetId: number,
utforandeAdress: UtforandeAdress,
selectedUtforandeVerksamhetList: EmployeeUtforandeVerksamhet[]
): boolean {
let selectedUtforandeVerksamhet: EmployeeUtforandeVerksamhet | null = null;
if (
!utforandeAdress ||
!selectedUtforandeVerksamhetList ||
selectedUtforandeVerksamhetList.length === 0 ||
!Array.isArray(selectedUtforandeVerksamhetList)
) {
return false;
}
selectedUtforandeVerksamhet = selectedUtforandeVerksamhetList.find(
selectedUtforandeVerksamhet => selectedUtforandeVerksamhet.id === utforandeVerksamhetId
);
return selectedUtforandeVerksamhet
? selectedUtforandeVerksamhet.allaAdresser ||
selectedUtforandeVerksamhet.adresser.some(
selectedUtforandeAdress => selectedUtforandeAdress.id === utforandeAdress.id
)
: false;
}
getSelectedUtforandeVerksamheterFromTreeNode(treeNode: TreeNode): UtforandeVerksamhet[] {
let utforandeVerksamhetList: UtforandeVerksamhet[] = [];

View File

@@ -1,4 +1,5 @@
import { FormGroup, ValidatorFn } from '@angular/forms';
import { TreeNode } from '@msfa-shared/components/tree-nodes-selector/services/tree-nodes-selector.service';
export class EmployeeValidator {
static HasSelectedAtLeastOneRole(roleFormControlNames: Array<string>): ValidatorFn {
@@ -12,4 +13,20 @@ export class EmployeeValidator {
: { noRoleSelected: true };
};
}
static HasSelectedAtLeastOneUtforandeVerksamhet(
utforandeVerksamheterFormControlName: string,
selectAllUtforandeVerksamheterFormControlName: string,
validationFn: (treeNode: TreeNode | null | undefined) => boolean
): ValidatorFn {
return (fg: FormGroup): { [key: string]: unknown } => {
if (fg?.get(selectAllUtforandeVerksamheterFormControlName)?.value) {
return null;
}
return validationFn(fg?.get(utforandeVerksamheterFormControlName)?.value)
? null
: { noUtforandeVerksamhetSelected: true };
};
}
}

View File

@@ -4,15 +4,14 @@ import { TreeNode } from '@msfa-shared/components/tree-nodes-selector/services/t
export class TreeNodeValidator {
static IsValidTreeNode(
validationFn: (treeNode: TreeNode | null | undefined) => boolean,
nameOfError: string,
allUtforandeVerksamheterFormControl: AbstractControl
nameOfError: string
): ValidatorFn {
return (control: AbstractControl): { [key: string]: unknown } => {
const isValid = validationFn(control.value);
const validationObj = {};
if (isValid || allUtforandeVerksamheterFormControl?.value) {
if (isValid) {
return null;
}