Merge pull request #74 in TEA/mina-sidor-fa-web from feature/TV-389 to develop
Squashed commit of the following: commit a865f5452ae9cb5eab0b55080dd7e7ec43d9ed61 Merge: b4e5a9ed9938ccAuthor: arbetsformedlingen_garcn <christian.gardebrink@arbetsformedlingen.se> Date: Fri Sep 3 10:25:38 2021 +0200 Merge branch 'develop-remote' into feature/TV-389 # Conflicts: # apps/mina-sidor-fa/src/app/pages/administration/pages/employee-form/employee-form.component.ts # apps/mina-sidor-fa/src/app/pages/administration/pages/employee-form/employee-form.module.ts commit b4e5a9ef26f99d0e0e8b2f8104f5e432da4bc82e Author: arbetsformedlingen_garcn <christian.gardebrink@arbetsformedlingen.se> Date: Fri Sep 3 02:43:51 2021 +0200 TV-389 removed some references to inputs that are no longer existing. commit 04c1527a994d9c5479ebcd523261dd331beb093e Author: arbetsformedlingen_garcn <christian.gardebrink@arbetsformedlingen.se> Date: Thu Sep 2 15:17:24 2021 +0200 TV-389 adjusted spelling error commit 3ea3faf1b13fafc16d4a97a6fc748dc790d1bc41 Author: arbetsformedlingen_garcn <christian.gardebrink@arbetsformedlingen.se> Date: Thu Sep 2 15:13:57 2021 +0200 TV-389 have adjusted a bunch of issues after feedback in PR commit 9ced585dd830c19006ead3bfe5a52ae1467189ef Author: arbetsformedlingen_garcn <christian.gardebrink@arbetsformedlingen.se> Date: Thu Sep 2 10:55:55 2021 +0200 Merge branch 'develop-remote' into feature/TV-389 # Conflicts: # apps/mina-sidor-fa/src/app/pages/administration/pages/employee-form/edit-employee-form/edit-employee-form.component.html # apps/mina-sidor-fa/src/app/pages/administration/pages/employee-form/employee-form.component.ts commit eb873ecb2125574c624523818f0441acd0a1bb61 Merge: 8f896cbb80bf22Author: arbetsformedlingen_garcn <christian.gardebrink@arbetsformedlingen.se> Date: Thu Sep 2 10:35:44 2021 +0200 Merge branch 'develop-remote' into feature/TV-389 commit 8f896cbf156ea65fed95a19d17ef485d06046ed0 Author: arbetsformedlingen_garcn <christian.gardebrink@arbetsformedlingen.se> Date: Wed Sep 1 18:19:16 2021 +0200 TV-389 making sure we're getting data of the right format commit 801e0298781815c9b4ca78f900cda17fbf33ffb5 Author: arbetsformedlingen_garcn <christian.gardebrink@arbetsformedlingen.se> Date: Wed Sep 1 09:09:51 2021 +0200 TV-389 fixed old function name commit 145e312d68e9a067377b228a718386dcf419ef49 Author: arbetsformedlingen_garcn <christian.gardebrink@arbetsformedlingen.se> Date: Wed Sep 1 08:52:54 2021 +0200 TV-389 restored file commit b1cf3b44bae548979fd090fca4e2194ae9c586c1 Author: arbetsformedlingen_garcn <christian.gardebrink@arbetsformedlingen.se> Date: Wed Sep 1 08:47:30 2021 +0200 TV-389 cleaned up some console logs for testing and renamed a function commit e9d79205902771eafeaf0fbe3bdb63e7cceeb0d5 Author: arbetsformedlingen_garcn <christian.gardebrink@arbetsformedlingen.se> Date: Wed Sep 1 08:30:08 2021 +0200 TV-389 have added a bunch of tests and refactored some stuff on the edit employee forms into a service. commit 185b4597c303ff20ae079efdf9247a53615b627e Author: arbetsformedlingen_garcn <christian.gardebrink@arbetsformedlingen.se> Date: Tue Aug 31 15:00:50 2021 +0200 TV-389 made a first working version of the tree node selector commit ddff1ed3a05434a42a81d4dabfd8b2f2ff3c468e Author: arbetsformedlingen_garcn <christian.gardebrink@arbetsformedlingen.se> Date: Tue Aug 31 13:01:49 2021 +0200 TV-389 adjustments after checking out integration against API. commit 92117d54b248f00a8b0619c3200d20a06510d9ba Author: arbetsformedlingen_garcn <christian.gardebrink@arbetsformedlingen.se> Date: Tue Aug 31 12:21:05 2021 +0200 TV-389 made various changes in prepertion for integration against the api.. commit 2f15741eb47335cfe4e8c47dc779642a8ab9893b Merge: 062f42b02cf0f6Author: arbetsformedlingen_garcn <christian.gardebrink@arbetsformedlingen.se> Date: Tue Aug 31 08:13:21 2021 +0200 Merge branch 'develop-remote' into feature/TV-389 commit 062f42b4d89976685fce463eec4f8deff399fd75 Author: arbetsformedlingen_garcn <christian.gardebrink@arbetsformedlingen.se> Date: Tue Aug 31 08:12:37 2021 +0200 TV-389 preparing for integration with api.. commit 674b636e4b32aa391e1e14763c5781fa25bc31fb Author: arbetsformedlingen_garcn <christian.gardebrink@arbetsformedlingen.se> Date: Tue Aug 31 07:51:12 2021 +0200 TV-389 fixed some custom validators for utforandeverksamheter.. commit 07256654273e499b41cbb6b06e26b9ca4f7627c5 Author: arbetsformedlingen_garcn <christian.gardebrink@arbetsformedlingen.se> Date: Tue Aug 31 05:39:27 2021 +0200 TV-389 removed useless z-index commit 36b6ac2f6f846f5e88b393650c6d66821b600933 Author: arbetsformedlingen_garcn <christian.gardebrink@arbetsformedlingen.se> Date: Tue Aug 31 05:32:16 2021 +0200 TV-389 added styling to button for opening the panel. commit 75ea6b7196e6ab69b0ec4ce103214dc742ea5252 Author: arbetsformedlingen_garcn <christian.gardebrink@arbetsformedlingen.se> Date: Tue Aug 31 03:23:10 2021 +0200 TV-389 minor adjustments of panel. ... and 13 more commits
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
@@ -11,7 +12,7 @@ describe('EmployeeDeleteComponent', () => {
|
|||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||||
declarations: [EmployeeDeleteComponent],
|
declarations: [EmployeeDeleteComponent],
|
||||||
imports: [RouterTestingModule],
|
imports: [RouterTestingModule, HttpClientTestingModule],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|
||||||
fixture = TestBed.createComponent(EmployeeDeleteComponent);
|
fixture = TestBed.createComponent(EmployeeDeleteComponent);
|
||||||
|
|||||||
@@ -16,54 +16,50 @@
|
|||||||
[afInvalid]="emailFormControl.invalid && emailFormControl.touched"
|
[afInvalid]="emailFormControl.invalid && emailFormControl.touched"
|
||||||
></digi-ng-form-input>
|
></digi-ng-form-input>
|
||||||
|
|
||||||
<fieldset *ngIf="rolesFormGroup && availableRoles" [formGroup]="rolesFormGroup">
|
<fieldset>
|
||||||
<legend>Tjänster</legend>
|
<legend>Tjänster</legend>
|
||||||
<p>Välj de tjänster du vill ge personalen tillgång till.</p>
|
<p>Välj de tjänster du vill ge personalen tillgång till.</p>
|
||||||
<digi-ng-form-select
|
<digi-ng-form-select
|
||||||
[formControl]="tjansterFormControl"
|
[formControl]="tjansterFormControl"
|
||||||
afLabel="Välj tjänster"
|
afLabel="Välj tjänster"
|
||||||
[afPlaceholder]="'Välj tjänst'"
|
[afPlaceholder]="'Välj tjänst'"
|
||||||
[afSelectItems]="availableTjanster"
|
[afSelectItems]="selectableTjansterFormItems"
|
||||||
[afDescription]="description"
|
|
||||||
[afDisableValidStyle]="true"
|
[afDisableValidStyle]="true"
|
||||||
[afDisableValidation]="disableValidation"
|
[afInvalid]="tjansterFormControl.invalid && tjansterFormControl.touched"
|
||||||
[afValidMessage]="validMessage"
|
|
||||||
[afDisabled]="disabled"
|
|
||||||
[afInvalidMessage]="invalidMessage"
|
|
||||||
[afInvalid]="invalid"
|
|
||||||
(afOnChange)="toggleTjanst()"
|
(afOnChange)="toggleTjanst()"
|
||||||
></digi-ng-form-select>
|
></digi-ng-form-select>
|
||||||
<div class="edit-employee-form__service-tag">
|
<digi-form-validation-message
|
||||||
<ng-container *ngFor="let employeeTjanst of selectedTjanster">
|
*ngIf="tjansterFormControl.invalid && tjansterFormControl.touched"
|
||||||
<digi-tag
|
af-variation="error"
|
||||||
class="edit-employee-form__service-tag--item"
|
>
|
||||||
[attr.af-text]="employeeTjanst?.name"
|
Du måste välja minst en tjänst
|
||||||
af-no-icon="false"
|
</digi-form-validation-message>
|
||||||
af-size="s"
|
<!-- Vi får se till att bygga en kontrol för att kunna välja flera tjänster här istället, en digi-ng-select får vara en temporär lösning.. -->
|
||||||
(click)="unselectTjanstTag(employeeTjanst)"
|
|
||||||
>
|
|
||||||
</digi-tag>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset *ngIf="rolesFormGroup && availableRoles" [formGroup]="rolesFormGroup">
|
<fieldset>
|
||||||
<legend>Utförande verksamheter och adresser</legend>
|
<legend>Utförande verksamheter och adresser</legend>
|
||||||
<p>Välj de utförandeverksamheter och utförande adresser du vill ge personalen behörighet till.</p>
|
<p>Välj de utförandeverksamheter och utförande adresser du vill ge personalen behörighet till.</p>
|
||||||
|
<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>
|
||||||
<div class="edit-employee-form__choose_all-utforande-verksamh">
|
<div class="edit-employee-form__choose_all-utforande-verksamh">
|
||||||
<digi-form-checkbox
|
<digi-ng-form-checkbox
|
||||||
af-variation="primary"
|
[formControlName]="toggleAllUtforandeVerksamhetFormControlName"
|
||||||
[afLabel]="'Välj alla utförade verksamheter och alla utförande adresser'"
|
[afLabel]="'Välj alla utförade verksamheter och alla utförande adresser'"
|
||||||
[afValue]=""
|
(afOnChange)="toggleAllUtforandeVerksamheter($event)"
|
||||||
(afOnChange)="selectAllUtforandeVerksamheter($event.detail.target.checked)"
|
>
|
||||||
></digi-form-checkbox>
|
</digi-ng-form-checkbox>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<msfa-tree-nodes-selector
|
||||||
style="display: flex; border: 1px solid; background-color: #eee; padding: 5px; justify-content: space-between"
|
[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()"
|
||||||
>
|
>
|
||||||
Plats för digi-select-form-item för utförande verksamheter
|
</msfa-tree-nodes-selector>
|
||||||
<digi-icon-arrow-down></digi-icon-arrow-down>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset *ngIf="rolesFormGroup && availableRoles" [formGroup]="rolesFormGroup">
|
<fieldset *ngIf="rolesFormGroup && availableRoles" [formGroup]="rolesFormGroup">
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import { DigiNgFormInputModule } from '@af/digi-ng/_form/form-input';
|
|||||||
import { DigiNgFormRadiobuttonGroupModule } from '@af/digi-ng/_form/form-radiobutton-group';
|
import { DigiNgFormRadiobuttonGroupModule } from '@af/digi-ng/_form/form-radiobutton-group';
|
||||||
import { DigiNgFormSelectModule } from '@af/digi-ng/_form/form-select';
|
import { DigiNgFormSelectModule } from '@af/digi-ng/_form/form-select';
|
||||||
import { DigiNgPopoverModule } from '@af/digi-ng/_popover/popover';
|
import { DigiNgPopoverModule } from '@af/digi-ng/_popover/popover';
|
||||||
|
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { ReactiveFormsModule } from '@angular/forms';
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { TreeNodesSelectorModule } from '@msfa-shared/components/tree-nodes-selector/tree-nodes-selector.module';
|
||||||
|
|
||||||
import { EditEmployeeFormComponent } from './edit-employee-form.component';
|
import { EditEmployeeFormComponent } from './edit-employee-form.component';
|
||||||
|
|
||||||
@@ -25,6 +27,8 @@ describe('EditEmployeeFormComponent', () => {
|
|||||||
DigiNgFormSelectModule,
|
DigiNgFormSelectModule,
|
||||||
DigiNgPopoverModule,
|
DigiNgPopoverModule,
|
||||||
DigiNgFormCheckboxModule,
|
DigiNgFormCheckboxModule,
|
||||||
|
HttpClientTestingModule,
|
||||||
|
TreeNodesSelectorModule,
|
||||||
],
|
],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { ButtonSize, ButtonType, ButtonVariation } from '@af/digi-ng/_button/button';
|
import { ButtonSize, ButtonType, ButtonVariation } from '@af/digi-ng/_button/button';
|
||||||
import { FormSelectItem } from '@af/digi-ng/_form/form-select';
|
|
||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
OnInit,
|
OnInit,
|
||||||
@@ -12,14 +11,24 @@ import {
|
|||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { AbstractControl, FormControl, FormGroup, Validators } from '@angular/forms';
|
import { AbstractControl, FormControl, FormGroup, Validators } from '@angular/forms';
|
||||||
import { Role } from '@msfa-models/role.model';
|
import { Role } from '@msfa-models/role.model';
|
||||||
import { RoleEnum } from '@msfa-enums/role.enum';
|
|
||||||
import { Tjanst } from '@msfa-models/tjanst.model';
|
import { Tjanst } from '@msfa-models/tjanst.model';
|
||||||
import { FormTagData } from '@msfa-models/form-tag.model';
|
import { FormSelectItem } from '@af/digi-ng/_form/form-select';
|
||||||
|
import { TreeNodeValidator } from '@msfa-utils/validators/tree-node.validator';
|
||||||
|
import {
|
||||||
|
UtforandeVerksamhet,
|
||||||
|
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 { EmployeeFormService } from '../services/employee-form.service';
|
||||||
|
|
||||||
export interface EditEmployeeFormData {
|
export interface EditEmployeeFormData {
|
||||||
email: string;
|
email: string;
|
||||||
tjanster: FormTagData[],
|
tjanster: Tjanst[];
|
||||||
roles: Array<Role>;
|
roles: Role[];
|
||||||
|
utforandeVerksamheter: UtforandeVerksamhet[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -29,14 +38,14 @@ export interface EditEmployeeFormData {
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class EditEmployeeFormComponent implements OnInit, OnChanges {
|
export class EditEmployeeFormComponent implements OnInit, OnChanges {
|
||||||
@Input() currentEmail: string | null = null;
|
@Input() currentEmail: string;
|
||||||
@Input() availableRoles: Array<Role> | null = null;
|
@Input() availableRoles: Role[];
|
||||||
@Input() currentEmployeeRoles: Array<string> | null = null;
|
@Input() currentEmployeeRoles: string[];
|
||||||
|
@Input() availableTjanster: Tjanst[];
|
||||||
@Input() availableTjanster: Array<FormSelectItem> | null = null;
|
@Input() currentEmployeeTjanster: Tjanst[];
|
||||||
@Input() currentEmployeeTjanster: Array<Tjanst> | null = null;
|
@Input() availableUtforandeVerksamheter: UtforandeVerksamhet[];
|
||||||
selectedTjanster: Array<FormTagData> | null = null;
|
|
||||||
|
|
||||||
|
@Output() tjansterSelected = new EventEmitter<Tjanst[]>();
|
||||||
@Output() formSubmitted = new EventEmitter<EditEmployeeFormData>();
|
@Output() formSubmitted = new EventEmitter<EditEmployeeFormData>();
|
||||||
|
|
||||||
readonly ButtonVariation = ButtonVariation;
|
readonly ButtonVariation = ButtonVariation;
|
||||||
@@ -45,28 +54,55 @@ export class EditEmployeeFormComponent implements OnInit, OnChanges {
|
|||||||
|
|
||||||
readonly emailFormControlName = 'email';
|
readonly emailFormControlName = 'email';
|
||||||
readonly tjansterFormControlName = 'tjanster';
|
readonly tjansterFormControlName = 'tjanster';
|
||||||
|
readonly utforandeVerksamhetFormControlName = 'utforandeVerksamheter';
|
||||||
|
readonly toggleAllUtforandeVerksamhetFormControlName = 'toggleAllUtforandeVerksamhet';
|
||||||
|
|
||||||
editEmployeeFormGroup: FormGroup | null = null;
|
editEmployeeFormGroup: FormGroup | null = null;
|
||||||
rolesFormGroup: FormGroup | null = null;
|
rolesFormGroup: FormGroup | null = null;
|
||||||
|
|
||||||
displayRolesDialog = false;
|
displayRolesDialog = false;
|
||||||
|
|
||||||
constructor() {}
|
selectableTjansterFormItems: Array<FormSelectItem> | null = null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private employeeFormService: EmployeeFormService,
|
||||||
|
private utforandeVerksamheterService: UtforandeVerksamheterService,
|
||||||
|
private treeNodesSelectorService: TreeNodesSelectorService
|
||||||
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.initializeEditEmployeeFormGroup();
|
this.initializeEditEmployeeFormGroup();
|
||||||
|
this.updateSelectableTjansterFormItems();
|
||||||
if(this.currentEmployeeTjanster) {
|
|
||||||
this.selectedTjanster = this.currentEmployeeTjanster
|
|
||||||
.map(tjanst => ({tjanstekod: tjanst.code, name: tjanst.name} as FormTagData));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
if (changes.availableRoles || changes.availableTjanster) {
|
if (changes.availableRoles) {
|
||||||
this.initializeEditEmployeeFormGroup();
|
this.initializeEditEmployeeFormGroup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (changes.availableTjanster) {
|
||||||
|
this.updateSelectableTjansterFormItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changes.availableUtforandeVerksamheter) {
|
||||||
|
this.editEmployeeFormGroup.patchValue(
|
||||||
|
Object.fromEntries([
|
||||||
|
[
|
||||||
|
this.utforandeVerksamhetFormControlName,
|
||||||
|
this.utforandeVerksamheterService.getTreeNodeDataFromUtforandeVerksamheter(
|
||||||
|
this.availableUtforandeVerksamheter
|
||||||
|
),
|
||||||
|
],
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
this.editEmployeeFormGroup.patchValue(
|
||||||
|
Object.fromEntries([[this.toggleAllUtforandeVerksamhetFormControlName, false]])
|
||||||
|
);
|
||||||
|
|
||||||
|
this.updateUtforandeVerksamhetStatus();
|
||||||
|
}
|
||||||
|
|
||||||
if (changes.currentEmployeeRoles) {
|
if (changes.currentEmployeeRoles) {
|
||||||
this.updateRolesFormGroup();
|
this.updateRolesFormGroup();
|
||||||
}
|
}
|
||||||
@@ -75,19 +111,88 @@ export class EditEmployeeFormComponent implements OnInit, OnChanges {
|
|||||||
this.editEmployeeFormGroup.patchValue(Object.fromEntries([[this.emailFormControlName, this.currentEmail]]));
|
this.editEmployeeFormGroup.patchValue(Object.fromEntries([[this.emailFormControlName, this.currentEmail]]));
|
||||||
}
|
}
|
||||||
|
|
||||||
if(changes.currentEmployeeTjanster) {
|
if (changes.currentEmployeeTjanster) {
|
||||||
this.editEmployeeFormGroup.patchValue(Object.fromEntries([[this.tjansterFormControlName, '']]));
|
this.editEmployeeFormGroup.patchValue(Object.fromEntries([[this.tjansterFormControlName, '']]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get emailFormControl(): AbstractControl | null {
|
get emailFormControl(): AbstractControl | undefined {
|
||||||
return this.editEmployeeFormGroup?.get(this.emailFormControlName);
|
return this.editEmployeeFormGroup?.get(this.emailFormControlName);
|
||||||
}
|
}
|
||||||
|
|
||||||
get tjansterFormControl(): AbstractControl | null {
|
get tjansterFormControl(): AbstractControl | undefined {
|
||||||
return this.editEmployeeFormGroup?.get(this.tjansterFormControlName);
|
return this.editEmployeeFormGroup?.get(this.tjansterFormControlName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get utforandeVerksamhetFormControl(): AbstractControl | undefined {
|
||||||
|
return this.editEmployeeFormGroup?.get(this.utforandeVerksamhetFormControlName);
|
||||||
|
}
|
||||||
|
|
||||||
|
get toggleAllUtforandeVerksamhetFormControl(): AbstractControl | undefined {
|
||||||
|
return this.editEmployeeFormGroup?.get(this.toggleAllUtforandeVerksamhetFormControlName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateUtforandeVerksamhetStatus(): void {
|
||||||
|
if (this.availableUtforandeVerksamheter && this.availableUtforandeVerksamheter.length > 0) {
|
||||||
|
this.utforandeVerksamhetFormControl.enable();
|
||||||
|
this.toggleAllUtforandeVerksamhetFormControl.enable();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.utforandeVerksamhetFormControl.disable();
|
||||||
|
this.toggleAllUtforandeVerksamhetFormControl.disable();
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateSelectableTjansterFormItems(): void {
|
||||||
|
this.selectableTjansterFormItems = this.availableTjanster?.map(tjanst => {
|
||||||
|
return { name: tjanst.name, value: `${tjanst.tjanstId}` };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializeEditEmployeeFormGroup(): void {
|
||||||
|
this.rolesFormGroup = this.employeeFormService.getRolesFormGroup(this.availableRoles, this.currentEmployeeRoles);
|
||||||
|
|
||||||
|
this.editEmployeeFormGroup = new FormGroup({
|
||||||
|
// eslint-disable-next-line
|
||||||
|
email: new FormControl(this.currentEmail, [Validators.required, Validators.email]),
|
||||||
|
// eslint-disable-next-line
|
||||||
|
tjanster: new FormControl('', [Validators.required]),
|
||||||
|
roles: this.rolesFormGroup,
|
||||||
|
utforandeVerksamheter: new FormControl(
|
||||||
|
this.utforandeVerksamheterService.getTreeNodeDataFromUtforandeVerksamheter(this.availableUtforandeVerksamheter),
|
||||||
|
[
|
||||||
|
// eslint-disable-next-line
|
||||||
|
TreeNodeValidator.IsValidTreeNode(
|
||||||
|
this.utforandeVerksamheterService.hasSelectedUtforandeVerksamhet,
|
||||||
|
'required'
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
toggleAllUtforandeVerksamhet: new FormControl(false, []),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updateUtforandeVerksamhetStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateRolesFormGroup(): void {
|
||||||
|
if (!this.rolesFormGroup || !this.availableRoles) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.rolesFormGroup.patchValue(
|
||||||
|
Object.fromEntries(
|
||||||
|
this.availableRoles.map(role => [
|
||||||
|
this.employeeFormService.getFormControlName(role),
|
||||||
|
this.employeeFormService.isSelectedRole(role, this.currentEmployeeRoles),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getFormControlName(role: Role): string {
|
||||||
|
return this.employeeFormService.getFormControlName(role);
|
||||||
|
}
|
||||||
|
|
||||||
onFormSubmitted(): void {
|
onFormSubmitted(): void {
|
||||||
if (!this.editEmployeeFormGroup) {
|
if (!this.editEmployeeFormGroup) {
|
||||||
return;
|
return;
|
||||||
@@ -98,14 +203,22 @@ export class EditEmployeeFormComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.formSubmitted.emit({
|
this.formSubmitted.emit({
|
||||||
email: this.emailFormControl?.value,
|
email: this.emailFormControl?.value as string,
|
||||||
tjanster: this.selectedTjanster,
|
tjanster: this.employeeFormService.getSelectedTjanster(
|
||||||
roles: this.getRolesFromFormGroup(this.rolesFormGroup, this.availableRoles),
|
this.availableTjanster,
|
||||||
|
parseInt(this.tjansterFormControl?.value, 10)
|
||||||
|
),
|
||||||
|
roles: this.employeeFormService.getRolesFromFormGroup(this.rolesFormGroup, this.availableRoles),
|
||||||
|
utforandeVerksamheter: this.utforandeVerksamheterService.getSelectedUtforandeVerksamheterFromTreeNode(
|
||||||
|
this.utforandeVerksamhetFormControl?.value
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getFormControlName(role: Role): string {
|
toggleTjanst(): void {
|
||||||
return RoleEnum[role?.type];
|
this.tjansterSelected.emit(
|
||||||
|
this.employeeFormService.getSelectedTjanster(this.availableTjanster, parseInt(this.tjansterFormControl.value, 10))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
openRolesDialog(): void {
|
openRolesDialog(): void {
|
||||||
@@ -116,99 +229,23 @@ export class EditEmployeeFormComponent implements OnInit, OnChanges {
|
|||||||
this.displayRolesDialog = false;
|
this.displayRolesDialog = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private initializeEditEmployeeFormGroup(): void {
|
toggleAllUtforandeVerksamheter(selectAll: boolean): void {
|
||||||
this.rolesFormGroup = this.getRolesFormGroup(
|
let treeNode: TreeNode = this.utforandeVerksamhetFormControl.value as TreeNode;
|
||||||
this.availableRoles,
|
|
||||||
this.currentEmployeeRoles
|
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.editEmployeeFormGroup = new FormGroup({
|
this.editEmployeeFormGroup.patchValue(
|
||||||
email: new FormControl(this.currentEmail, [Validators.required, Validators.email]),
|
Object.fromEntries([[this.toggleAllUtforandeVerksamhetFormControlName, hasSelectedAllLeafNodes]])
|
||||||
tjanster: new FormControl('', []),
|
|
||||||
roles: this.rolesFormGroup,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateRolesFormGroup(): void {
|
|
||||||
if (!this.rolesFormGroup || !this.availableRoles) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.rolesFormGroup.patchValue(
|
|
||||||
Object.fromEntries(
|
|
||||||
this.availableRoles.map(role => [
|
|
||||||
this.getFormControlName(role),
|
|
||||||
this.isSelectedRole(role, this.currentEmployeeRoles),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getRolesFromFormGroup(
|
|
||||||
formGroup: FormGroup | null,
|
|
||||||
roles: Array<Role> | null
|
|
||||||
): Array<Role> {
|
|
||||||
if (!formGroup || !roles) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return roles.filter(
|
|
||||||
role => formGroup.get(this.getFormControlName(role))?.value === true
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getRolesFormGroup(
|
|
||||||
roles: Array<Role> | null,
|
|
||||||
selectedRoles: Array<string> | null
|
|
||||||
): FormGroup {
|
|
||||||
if (!roles) {
|
|
||||||
return new FormGroup({});
|
|
||||||
}
|
|
||||||
|
|
||||||
return new FormGroup(
|
|
||||||
Object.fromEntries(
|
|
||||||
roles.map(role => [
|
|
||||||
this.getFormControlName(role),
|
|
||||||
new FormControl(this.isSelectedRole(role, selectedRoles), []),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private isSelectedRole(
|
|
||||||
role: Role,
|
|
||||||
selectedRoles: Array<string> | null
|
|
||||||
): boolean {
|
|
||||||
if (!selectedRoles || !role) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return selectedRoles.some(selectedRole => selectedRole === role.type);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tjanster helper methods
|
|
||||||
toggleTjanst(): void {
|
|
||||||
const tjanstExistsInList: boolean = this.selectedTjanster
|
|
||||||
.some(tag => tag.tjanstekod === this.tjansterFormControl.value);
|
|
||||||
|
|
||||||
const selectedTjanst: FormTagData[] = this.availableTjanster
|
|
||||||
.filter(tjanst => tjanst.value === this.tjansterFormControl.value)
|
|
||||||
.map(tjanst => ({tjanstekod: tjanst.value, name: tjanst.name}));
|
|
||||||
if(this.tjansterFormControl.value && !tjanstExistsInList) {
|
|
||||||
this.selectedTjanster.push(...selectedTjanst);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
unselectTjanstTag(tjanst: FormTagData): void {
|
|
||||||
const tagExistsInList = this.selectedTjanster.some(tag => tag.tjanstekod === tjanst.tjanstekod);
|
|
||||||
if(tjanst.tjanstekod && tagExistsInList) {
|
|
||||||
this.selectedTjanster.splice(this.selectedTjanster.indexOf(tjanst), 1);
|
|
||||||
}
|
|
||||||
this.tjansterFormControl.setValue('');
|
|
||||||
}
|
|
||||||
|
|
||||||
selectAllUtforandeVerksamheter(checked: boolean):void {
|
|
||||||
console.log('selectAllUtforandeVerksamheter', checked);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,14 +41,18 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
<!-- Component för att hantera formuläret -->
|
<!-- Component för att hantera formuläret -->
|
||||||
<msfa-edit-employee-form
|
<ng-container>
|
||||||
[currentEmail]="employee?.email"
|
<msfa-edit-employee-form
|
||||||
[availableRoles]="selectableRoles"
|
[currentEmail]="employee?.email"
|
||||||
[currentEmployeeRoles]="employee?.roles"
|
[availableRoles]="selectableRoles"
|
||||||
[availableTjanster]="availableTjanster"
|
[currentEmployeeRoles]="currentEmployeeRoles$ | async"
|
||||||
[currentEmployeeTjanster]="employee.tjanster"
|
[availableTjanster]="tjanster$ | async"
|
||||||
[selectedTjanster]="selectedServices$ | async"
|
[currentEmployeeTjanster]="employee.tjanster"
|
||||||
(formSubmitted)="updateEmployee($event)"
|
[selectedTjanster]="selectedServices$ | async"
|
||||||
></msfa-edit-employee-form>
|
[availableUtforandeVerksamheter]="availableUtforandeVerksamheter$ | async"
|
||||||
|
(tjansterSelected)="setupAvailableUtforandeVerksamheter($event)"
|
||||||
|
(formSubmitted)="updateEmployee($event)"
|
||||||
|
></msfa-edit-employee-form>
|
||||||
|
</ng-container>
|
||||||
</section>
|
</section>
|
||||||
</msfa-layout>
|
</msfa-layout>
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import { FormSelectItem } from '@af/digi-ng/_form/form-select';
|
|
||||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { UnsubscribeDirective } from '@msfa-directives/unsubscribe.directive';
|
|
||||||
import { Employee } from '@msfa-models/employee.model';
|
import { Employee } from '@msfa-models/employee.model';
|
||||||
import { mapRoleResponseToRoleObject, Role } from '@msfa-models/role.model';
|
import { mapRoleResponseToRoleObject, Role } from '@msfa-models/role.model';
|
||||||
import { Tjanst } from '@msfa-models/tjanst.model';
|
import { Tjanst } from '@msfa-models/tjanst.model';
|
||||||
import { EmployeeService } from '@msfa-services/api/employee.service';
|
import { EmployeeService } from '@msfa-services/api/employee.service';
|
||||||
import { TjanstService } from '@msfa-services/api/tjanst.service';
|
import { TjanstService } from '@msfa-services/api/tjanst.service';
|
||||||
|
import {
|
||||||
|
UtforandeVerksamhet,
|
||||||
|
UtforandeVerksamheterService,
|
||||||
|
} from '@msfa-services/utforande-verksamheter/utforande-verksamheter.service';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
import { EditEmployeeFormData } from './edit-employee-form/edit-employee-form.component';
|
import { EditEmployeeFormData } from './edit-employee-form/edit-employee-form.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -16,48 +19,42 @@ import { EditEmployeeFormData } from './edit-employee-form/edit-employee-form.co
|
|||||||
styleUrls: ['./employee-form.component.scss'],
|
styleUrls: ['./employee-form.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class EmployeeFormComponent extends UnsubscribeDirective implements OnInit {
|
export class EmployeeFormComponent implements OnInit {
|
||||||
subscriptionsList = [];
|
|
||||||
employee$ = this.employeeService.employee$;
|
employee$ = this.employeeService.employee$;
|
||||||
employee: Employee;
|
|
||||||
|
|
||||||
tjanster$: Observable<Tjanst[]> = this.tjanstService.tjanster$;
|
tjanster$: Observable<Tjanst[]> = this.tjanstService.tjanster$;
|
||||||
availableTjanster: FormSelectItem[] | null = null;
|
currentEmployeeRoles$: Observable<Role[] | undefined | null> = null;
|
||||||
|
availableUtforandeVerksamheter$: Observable<Array<UtforandeVerksamhet>> | null = null;
|
||||||
|
|
||||||
selectableRoles: Role[] = this.employeeService.allRoles;
|
selectableRoles: Role[] = this.employeeService.allRoles;
|
||||||
currentEmployeeRoles: Role[] | undefined | null = null;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private employeeService: EmployeeService,
|
private employeeService: EmployeeService,
|
||||||
private tjanstService: TjanstService,
|
private tjanstService: TjanstService,
|
||||||
|
private utforandeVerksamheterService: UtforandeVerksamheterService,
|
||||||
private activatedRoute: ActivatedRoute
|
private activatedRoute: ActivatedRoute
|
||||||
) {
|
) {}
|
||||||
super();
|
|
||||||
super.unsubscribeOnDestroy(...this.subscriptionsList);
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.employeeService.setCurrentEmployeeId(this.activatedRoute.snapshot.params['employeeId']);
|
this.employeeService.setCurrentEmployeeId(this.activatedRoute.snapshot.params['employeeId']);
|
||||||
const employeeDataSub = this.employee$.subscribe(employee => {
|
|
||||||
this.employee = employee;
|
|
||||||
this.currentEmployeeRoles = employee?.roles.map(role => mapRoleResponseToRoleObject(role));
|
|
||||||
});
|
|
||||||
|
|
||||||
const tjanstRelatedDataSub = this.tjanster$.subscribe(tjanster => {
|
this.currentEmployeeRoles$ = this.employee$.pipe(
|
||||||
const tjanstOptions: FormSelectItem[] = [];
|
map(employee => employee?.roles?.map(role => mapRoleResponseToRoleObject(role)))
|
||||||
tjanster?.forEach(tjanst => {
|
);
|
||||||
tjanstOptions.push({ name: tjanst?.name, value: tjanst?.code });
|
|
||||||
});
|
|
||||||
this.availableTjanster = tjanstOptions;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.subscriptionsList.push(employeeDataSub, tjanstRelatedDataSub);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateEmployee(editEmployeeFormData: EditEmployeeFormData): void {
|
updateEmployee(editEmployeeFormData: EditEmployeeFormData): void {
|
||||||
console.log(editEmployeeFormData);
|
console.log(editEmployeeFormData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setupAvailableUtforandeVerksamheter(selectedTjanster: Array<Tjanst>): void {
|
||||||
|
if (!selectedTjanster) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.availableUtforandeVerksamheter$ = this.utforandeVerksamheterService.getUtforandeVerksamheter(
|
||||||
|
selectedTjanster.map(tjanst => tjanst.tjanstId)
|
||||||
|
);
|
||||||
|
}
|
||||||
setEmployeeToDelete(employee: Employee): void {
|
setEmployeeToDelete(employee: Employee): void {
|
||||||
this.employeeService.setEmployeeToDelete(employee);
|
this.employeeService.setEmployeeToDelete(employee);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,12 +9,13 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { ReactiveFormsModule } from '@angular/forms';
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { HideTextModule } from '@msfa-shared/components/hide-text/hide-text.module';
|
|
||||||
import { LayoutModule } from '@msfa-shared/components/layout/layout.module';
|
import { LayoutModule } from '@msfa-shared/components/layout/layout.module';
|
||||||
import { LocalDatePipeModule } from '@msfa-shared/pipes/local-date/local-date.module';
|
import { LocalDatePipeModule } from '@msfa-shared/pipes/local-date/local-date.module';
|
||||||
import { EmployeeDeleteModule } from '../../components/employee-delete/employee-delete.module';
|
import { EmployeeDeleteModule } from '../../components/employee-delete/employee-delete.module';
|
||||||
import { EditEmployeeFormComponent } from './edit-employee-form/edit-employee-form.component';
|
import { EditEmployeeFormComponent } from './edit-employee-form/edit-employee-form.component';
|
||||||
|
import { HideTextModule } from '@msfa-shared/components/hide-text/hide-text.module';
|
||||||
import { EmployeeFormComponent } from './employee-form.component';
|
import { EmployeeFormComponent } from './employee-form.component';
|
||||||
|
import { TreeNodesSelectorModule } from '@msfa-shared/components/tree-nodes-selector/tree-nodes-selector.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||||
@@ -34,6 +35,7 @@ import { EmployeeFormComponent } from './employee-form.component';
|
|||||||
LayoutModule,
|
LayoutModule,
|
||||||
EmployeeDeleteModule,
|
EmployeeDeleteModule,
|
||||||
HideTextModule,
|
HideTextModule,
|
||||||
|
TreeNodesSelectorModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class EmployeeFormModule {}
|
export class EmployeeFormModule {}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { EmployeeFormService } from './employee-form.service';
|
||||||
|
|
||||||
|
describe('EmployeeFormService', () => {
|
||||||
|
let service: EmployeeFormService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(EmployeeFormService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { FormControl, FormGroup } from '@angular/forms';
|
||||||
|
import { RoleEnum } from '@msfa-enums/role.enum';
|
||||||
|
import { Role } from '@msfa-models/role.model';
|
||||||
|
import { Tjanst } from '@msfa-models/tjanst.model';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class EmployeeFormService {
|
||||||
|
isSelectedRole(role: Role, selectedRoles: Array<string> | null): boolean {
|
||||||
|
if (!selectedRoles || !role) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedRoles.some(selectedRole => selectedRole === role.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSelectedTjanster(tjanster: Array<Tjanst>, tjanstId: number): Array<Tjanst> {
|
||||||
|
let selectedTjanst: Tjanst | null = null;
|
||||||
|
|
||||||
|
if (!tjanster) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedTjanst = tjanster.find(tjanst => tjanst.tjanstId === tjanstId);
|
||||||
|
|
||||||
|
return selectedTjanst ? [selectedTjanst] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
getRolesFromFormGroup(formGroup: FormGroup | null, roles: Array<Role> | null): Array<Role> {
|
||||||
|
if (!formGroup || !roles) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return roles.filter(role => formGroup.get(this.getFormControlName(role))?.value === true);
|
||||||
|
}
|
||||||
|
|
||||||
|
getRolesFormGroup(roles: Array<Role> | null, selectedRoles: Array<string> | null): FormGroup {
|
||||||
|
if (!roles) {
|
||||||
|
return new FormGroup({});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new FormGroup(
|
||||||
|
Object.fromEntries(
|
||||||
|
roles.map(role => [
|
||||||
|
this.getFormControlName(role),
|
||||||
|
new FormControl(this.isSelectedRole(role, selectedRoles), []),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getFormControlName(role: Role): string {
|
||||||
|
return RoleEnum[role?.type];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { DigiNgSkeletonBaseModule } from '@af/digi-ng/_skeleton/skeleton-base';
|
import { DigiNgSkeletonBaseModule } from '@af/digi-ng/_skeleton/skeleton-base';
|
||||||
|
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
@@ -18,7 +19,7 @@ describe('EmployeesListComponent', () => {
|
|||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||||
declarations: [EmployeesListComponent, LayoutComponent],
|
declarations: [EmployeesListComponent, LayoutComponent],
|
||||||
imports: [RouterTestingModule, DigiNgSkeletonBaseModule],
|
imports: [RouterTestingModule, DigiNgSkeletonBaseModule, HttpClientTestingModule],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|
||||||
fixture = TestBed.createComponent(EmployeesListComponent);
|
fixture = TestBed.createComponent(EmployeesListComponent);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { Service } from '@msfa-enums/service.enum';
|
|
||||||
import { EmployeeCompact } from '@msfa-models/employee.model';
|
import { EmployeeCompact } from '@msfa-models/employee.model';
|
||||||
|
|
||||||
export const employeesMock: EmployeeCompact[] = [
|
export const employeesMock: EmployeeCompact[] = [
|
||||||
@@ -7,5 +6,6 @@ export const employeesMock: EmployeeCompact[] = [
|
|||||||
fullName: 'Jayson Karlsson',
|
fullName: 'Jayson Karlsson',
|
||||||
tjanster: ['KROM'],
|
tjanster: ['KROM'],
|
||||||
utforandeVerksamheter: [],
|
utforandeVerksamheter: [],
|
||||||
|
allaUtforandeVerksamheter: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import { HttpClientTestingModule } from '@angular/common/http/testing';
|
|||||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
import { ComponentFixture, discardPeriodicTasks, fakeAsync, TestBed, tick } from '@angular/core/testing';
|
import { ComponentFixture, discardPeriodicTasks, fakeAsync, TestBed, tick } from '@angular/core/testing';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { AvropService } from '@msfa-services/avrop.service';
|
||||||
import { of } from 'rxjs';
|
import { of } from 'rxjs';
|
||||||
import { AvropService } from '../../avrop.service';
|
import { TemporaryFilterComponent } from '../temporary-filter/temporary-filter.component';
|
||||||
import { AvropFiltersComponent } from './avrop-filters.component';
|
import { AvropFiltersComponent } from './avrop-filters.component';
|
||||||
import { TemporaryFilterComponent } from './temporary-filter/temporary-filter.component';
|
|
||||||
|
|
||||||
describe('AvropFiltersComponent', () => {
|
describe('AvropFiltersComponent', () => {
|
||||||
let component: AvropFiltersComponent;
|
let component: AvropFiltersComponent;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { AvropListComponent } from './avrop-list.component';
|
import { AvropListComponent } from './avrop-list.component';
|
||||||
|
|
||||||
@@ -8,6 +9,7 @@ describe('AvropListComponent', () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
declarations: [AvropListComponent],
|
declarations: [AvropListComponent],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -12,42 +12,45 @@
|
|||||||
</div>
|
</div>
|
||||||
<dl class="avrop-row__name">
|
<dl class="avrop-row__name">
|
||||||
<dt class="avrop-table__label">Namn:</dt>
|
<dt class="avrop-table__label">Namn:</dt>
|
||||||
<dd *ngIf="avrop.fullName; else emptyText">{{avrop.fullName}}</dd>
|
<dd *ngIf="avrop?.fullName; else emptyText">{{avrop?.fullName}}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
<dl class="avrop-row__tjanst">
|
<dl class="avrop-row__tjanst">
|
||||||
<dt class="avrop-table__label">Tjänst:</dt>
|
<dt class="avrop-table__label">Tjänst:</dt>
|
||||||
<dd *ngIf="avrop.tjanst; else emptyText">{{avrop.tjanst}}</dd>
|
<dd *ngIf="avrop?.tjanst; else emptyText">{{avrop?.tjanst}}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
<dl class="avrop-row__start">
|
<dl class="avrop-row__start">
|
||||||
<dt class="avrop-table__label">Startdatum:</dt>
|
<dt class="avrop-table__label">Startdatum:</dt>
|
||||||
<dd>
|
<dd>
|
||||||
<digi-typography-time
|
<digi-typography-time
|
||||||
*ngIf="avrop.startDate; else emptyText"
|
*ngIf="avrop?.startDate; else emptyText"
|
||||||
[afDateTime]="avrop.startDate"
|
[afDateTime]="avrop?.startDate"
|
||||||
></digi-typography-time>
|
></digi-typography-time>
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
<dl class="avrop-row__end">
|
<dl class="avrop-row__end">
|
||||||
<dt class="avrop-table__label">Slutdatum:</dt>
|
<dt class="avrop-table__label">Slutdatum:</dt>
|
||||||
<dd>
|
<dd>
|
||||||
<digi-typography-time *ngIf="avrop.endDate; else emptyText" [afDateTime]="avrop.endDate"></digi-typography-time>
|
<digi-typography-time
|
||||||
|
*ngIf="avrop?.endDate; else emptyText"
|
||||||
|
[afDateTime]="avrop?.endDate"
|
||||||
|
></digi-typography-time>
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
<dl class="avrop-row__translator">
|
<dl class="avrop-row__translator">
|
||||||
<dt class="avrop-table__label">Språkstöd/Tolk:</dt>
|
<dt class="avrop-table__label">Språkstöd/Tolk:</dt>
|
||||||
<dd>{{avrop.sprakstod || '- '}}/{{avrop.tolkbehov || ' -'}}</dd>
|
<dd>{{avrop?.sprakstod || '- '}}/{{avrop?.tolkbehov || ' -'}}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
<dl class="avrop-row__address">
|
<dl class="avrop-row__address">
|
||||||
<dt class="avrop-table__label">Utförande adress:</dt>
|
<dt class="avrop-table__label">Utförande adress:</dt>
|
||||||
<dd *ngIf="avrop.utforandeAdress; else emptyText">{{avrop.utforandeAdress}}</dd>
|
<dd *ngIf="avrop?.utforandeAdress; else emptyText">{{avrop?.utforandeAdress}}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
<dl class="avrop-row__level">
|
<dl class="avrop-row__level">
|
||||||
<dt class="avrop-table__label">Spår/nivå:</dt>
|
<dt class="avrop-table__label">Spår/nivå:</dt>
|
||||||
<dd *ngIf="avrop.trackCode; else emptyText">{{avrop.trackCode}}</dd>
|
<dd *ngIf="avrop?.trackCode; else emptyText">{{avrop?.trackCode}}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
<dl class="avrop-row__handledare" *ngIf="isLocked">
|
<dl class="avrop-row__handledare" *ngIf="isLocked">
|
||||||
<dt class="avrop-table__label">Vald handledare:</dt>
|
<dt class="avrop-table__label">Vald handledare:</dt>
|
||||||
<dd *ngIf="handledare?.fullName; else emptyText">{{handledare.fullName}}</dd>
|
<dd *ngIf="handledare?.fullName; else emptyText">{{handledare?.fullName}}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
<div *ngIf="isLocked" class="avrop-row__delete">
|
<div *ngIf="isLocked" class="avrop-row__delete">
|
||||||
<digi-button
|
<digi-button
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export enum ButtonType {
|
||||||
|
BUTTON = 'button',
|
||||||
|
SUBMIT = 'submit',
|
||||||
|
RESET = 'reset',
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export enum FormInputSearchVariation {
|
||||||
|
S = 's',
|
||||||
|
M = 'm',
|
||||||
|
L = 'l',
|
||||||
|
}
|
||||||
@@ -27,15 +27,15 @@ export class OrganizationPickerFormComponent implements OnInit, OnChanges {
|
|||||||
organizationPickerFormGroup: FormGroup | null = null;
|
organizationPickerFormGroup: FormGroup | null = null;
|
||||||
selectableOrganizations: Array<FormSelectItem> = [];
|
selectableOrganizations: Array<FormSelectItem> = [];
|
||||||
|
|
||||||
constructor() {}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.setupOrganizationPickerFormGroup();
|
this.setupOrganizationPickerFormGroup();
|
||||||
this.setupSelectableOrganizations(this.organizations);
|
this.setupSelectableOrganizations(this.organizations);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
this.setupSelectableOrganizations(this.organizations);
|
if (changes.organizations) {
|
||||||
|
this.setupSelectableOrganizations(this.organizations);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get organizationFormControl(): AbstractControl | null {
|
get organizationFormControl(): AbstractControl | null {
|
||||||
@@ -44,6 +44,7 @@ export class OrganizationPickerFormComponent implements OnInit, OnChanges {
|
|||||||
|
|
||||||
private setupOrganizationPickerFormGroup(): void {
|
private setupOrganizationPickerFormGroup(): void {
|
||||||
this.organizationPickerFormGroup = new FormGroup({
|
this.organizationPickerFormGroup = new FormGroup({
|
||||||
|
// eslint-disable-next-line
|
||||||
organization: new FormControl(null, [Validators.required]),
|
organization: new FormControl(null, [Validators.required]),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<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()"
|
||||||
|
[attr.af-button-type]="ButtonType.BUTTON"
|
||||||
|
[attr.af-label]="' '"
|
||||||
|
[attr.af-aria-labelledby]="filterDescriptionId"
|
||||||
|
(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>
|
||||||
|
<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>
|
||||||
|
<ng-template #nodeExpansionPresentationTemplate let-node="node" let-parentNode="parentNode">
|
||||||
|
<div
|
||||||
|
class="expanded-tree-node__node-expansion-presentation"
|
||||||
|
[ngClass]="{
|
||||||
|
'expanded-tree-node__node-expansion-presentation--focus': node.hasFocus,
|
||||||
|
'expanded-tree-node__node-expansion-presentation--active' : isExpandedNode(parentNode, node)
|
||||||
|
}"
|
||||||
|
(click)="nodePresentationItemClicked(node)"
|
||||||
|
>
|
||||||
|
<span class="expanded-tree-node__node-expansion-presentation__text">{{node.label}}</span>
|
||||||
|
<span
|
||||||
|
class="expanded-tree-node__node-expansion-presentation__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>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
@import 'variables/colors';
|
||||||
|
@import 'mixins/list';
|
||||||
|
@import 'variables/gutters';
|
||||||
|
|
||||||
|
.expanded-tree-node {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100%;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
&__filter {
|
||||||
|
padding: 0 0.9375rem;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__heading {
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: 0 0.9375rem;
|
||||||
|
display: block;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
::ng-deep {
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__nodes {
|
||||||
|
overflow: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
|
@include msfa__reset-list;
|
||||||
|
padding: 0.3125rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__info {
|
||||||
|
padding: 0.9375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__node {
|
||||||
|
&--leaf {
|
||||||
|
margin-top: 0.625rem;
|
||||||
|
padding-left: 0.625rem;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__node-checkbox-presentation {
|
||||||
|
font-size: 1rem;
|
||||||
|
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 0.625rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--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 {
|
||||||
|
display: inline-flex;
|
||||||
|
padding: 0.3125rem 0.9375rem;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
border-width: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
align-items: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&--focus {
|
||||||
|
background-color: var(--digi--ui--color--background--secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--focus {
|
||||||
|
border-top: 1px solid var(--digi--typography--color--text--disabled);
|
||||||
|
border-bottom: 1px solid var(--digi--typography--color--text--disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
background-color: var(--digi--ui--color--primary);
|
||||||
|
color: var(--digi--ui--color--background);
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&--focus {
|
||||||
|
background-color: lighten($digi--ui--color--primary, 10%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
text-align: left;
|
||||||
|
flex-grow: 1;
|
||||||
|
max-width: 250px;
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__has-selection-dot {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--digi--ui--color--success);
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 1em;
|
||||||
|
max-height: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ExpandedTreeNodeComponent } from './expanded-tree-node.component';
|
||||||
|
|
||||||
|
describe('ExpandedTreeNodeComponent', () => {
|
||||||
|
let component: ExpandedTreeNodeComponent;
|
||||||
|
let fixture: ComponentFixture<ExpandedTreeNodeComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ ExpandedTreeNodeComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ExpandedTreeNodeComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import { TypographyDynamicHeadingLevel } from '@af/digi-ng/_typography/typography-dynamic-heading';
|
||||||
|
import {
|
||||||
|
Component,
|
||||||
|
OnInit,
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Input,
|
||||||
|
OnChanges,
|
||||||
|
SimpleChanges,
|
||||||
|
Output,
|
||||||
|
EventEmitter,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { UnsubscribeDirective } from '@msfa-directives/unsubscribe.directive';
|
||||||
|
import { ButtonType } from '../../../../../pages/avrop/enums/button-type.enum';
|
||||||
|
import { FormInputSearchVariation } from '../../../../../pages/avrop/enums/form-input-search-variation';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
import { debounceTime } from 'rxjs/operators';
|
||||||
|
import {
|
||||||
|
FilterTreeNodeData,
|
||||||
|
TreeNodeModel,
|
||||||
|
TreeNodesSelectorService,
|
||||||
|
} from '../../services/tree-nodes-selector.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'msfa-expanded-tree-node',
|
||||||
|
templateUrl: './expanded-tree-node.component.html',
|
||||||
|
styleUrls: ['./expanded-tree-node.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class ExpandedTreeNodeComponent extends UnsubscribeDirective implements OnInit, OnChanges {
|
||||||
|
@Input() treeNodeModel: TreeNodeModel | null = null;
|
||||||
|
@Input() headingLevel: TypographyDynamicHeadingLevel = TypographyDynamicHeadingLevel.H3;
|
||||||
|
@Input() latestParentActionKey: string;
|
||||||
|
@Output() filterTreeNodeRequested = new EventEmitter<FilterTreeNodeData>();
|
||||||
|
@Output() clickAndFocusElementRequested = new EventEmitter<string>();
|
||||||
|
|
||||||
|
readonly ButtonType = ButtonType;
|
||||||
|
readonly FormInputSearchVariation = FormInputSearchVariation;
|
||||||
|
|
||||||
|
visibleChildren: Array<TreeNodeModel> | null = null;
|
||||||
|
|
||||||
|
private readonly filterTreeNodeDebouncer: Subject<FilterTreeNodeData> = new Subject<FilterTreeNodeData>();
|
||||||
|
|
||||||
|
constructor(private treeNodesSelectorService: TreeNodesSelectorService) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
super.unsubscribeOnDestroy(
|
||||||
|
this.filterTreeNodeDebouncer.pipe(debounceTime(200)).subscribe(filterTreeNodeData => {
|
||||||
|
this.filterTreeNodeRequested.emit(filterTreeNodeData);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.refreshComponentData();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
|
if (changes.latestParentActionKey || changes.treeNodeModel) {
|
||||||
|
this.refreshComponentData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private refreshComponentData(): void {
|
||||||
|
this.visibleChildren = this.treeNodesSelectorService.getVisibleChildren(this.treeNodeModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
onFilterTextChanged(event: CustomEvent<{ target: { value: string } }>, treeNode: TreeNodeModel): void {
|
||||||
|
if (!treeNode || !event?.detail?.target) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.filterTreeNodeDebouncer.next({
|
||||||
|
text: event.detail.target.value,
|
||||||
|
treeNode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onFocusOutsideFilter(event: Event): void {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
nodePresentationToggleAllClicked(treeNode: TreeNodeModel): void {
|
||||||
|
if (!treeNode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.clickAndFocusElementRequested.emit(`#${this.treeNodesSelectorService.getToggleAllButtonId(treeNode)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
nodePresentationItemClicked(treeNode: TreeNodeModel): void {
|
||||||
|
if (!treeNode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.clickAndFocusElementRequested.emit(`#${this.treeNodesSelectorService.getItemButtonId(treeNode)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPresentationItemId(treeNode: TreeNodeModel): string {
|
||||||
|
return this.treeNodesSelectorService.getPresentationItemId(treeNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPresentationToggleAllId(treeNode: TreeNodeModel): string {
|
||||||
|
return this.treeNodesSelectorService.getPresentationToggleAllId(treeNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
getFilterDescriptionId(treeNode: TreeNodeModel): string {
|
||||||
|
return this.treeNodesSelectorService.getFilterDescriptionId(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);
|
||||||
|
}
|
||||||
|
|
||||||
|
allChildLeafNodesAreSelected(node: TreeNodeModel): boolean {
|
||||||
|
return this.treeNodesSelectorService.allChildLeafNodesAreSelected(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
isLeafNode(node: TreeNodeModel): boolean {
|
||||||
|
return this.treeNodesSelectorService.isLeafNode(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
isExpandedNode(parentTreeNode: TreeNodeModel, treeNode: TreeNodeModel): boolean {
|
||||||
|
return this.treeNodesSelectorService.isExpandedNode(parentTreeNode, treeNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
hasSelectedDescendant(treeNode: TreeNodeModel): boolean {
|
||||||
|
return this.treeNodesSelectorService.hasSelectedLeafNodeDescendant(treeNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
<digi-util-detect-focus-outside (afOnFocusOutside)="closePanel()">
|
||||||
|
<digi-util-keydown-handler (afOnEsc)="closePanel()">
|
||||||
|
<section class="tree-nodes-selector-panel" #treeNodesSelectorPanel>
|
||||||
|
<header>
|
||||||
|
<h2 class="tree-nodes-selector-panel__heading">{{headingText}}</h2>
|
||||||
|
<p class="msfa__a11y-sr-only">
|
||||||
|
I den här kontrollen kan du välja flera olika alternativ genom att fylla i kryssrutor som listas hierarkiskt
|
||||||
|
nedan. Du kan stänga kontrollen genom att trycka Escape.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
*ngIf="hasSelectedDescendant(pendingRootNode)"
|
||||||
|
class="tree-nodes-selector-panel__clear-btn"
|
||||||
|
type="button"
|
||||||
|
aria-label="ta bort samtliga val bland alternativen nedan"
|
||||||
|
(click)="clearSelections()"
|
||||||
|
>
|
||||||
|
Rensa
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
<digi-button
|
||||||
|
[attr.af-aria-label]="'Bekräftar dina val och stänger kontrollen'"
|
||||||
|
[attr.af-type]="'button'"
|
||||||
|
[attr.af-size]="ButtonSize.S"
|
||||||
|
(afOnClick)="closePanel()"
|
||||||
|
>
|
||||||
|
{{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>
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
@import 'variables/colors';
|
||||||
|
@import 'mixins/list';
|
||||||
|
@import 'variables/gutters';
|
||||||
|
|
||||||
|
.tree-nodes-selector-panel {
|
||||||
|
background-color: var(--digi--ui--color--background);
|
||||||
|
min-width: 710px;
|
||||||
|
min-height: 144px;
|
||||||
|
box-shadow: 0 0.2rem 0.6rem 0 var(--digi--ui--color--shadow);
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
header {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border-width: 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__heading {
|
||||||
|
width: 100%;
|
||||||
|
height: 45px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: 0.625rem 0.9375rem;
|
||||||
|
border-bottom: 1px solid var(--digi--typography--color--text--disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__clear-btn {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 20;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
display: block;
|
||||||
|
background-image: none;
|
||||||
|
background-color: transparent;
|
||||||
|
border: 0 none transparent;
|
||||||
|
color: var(--digi--ui--color--danger);
|
||||||
|
padding: 0.625rem 0.9375rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__expanded-nodes {
|
||||||
|
@include msfa__reset-list;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
overflow: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__expanded-node {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 0 0 50%;
|
||||||
|
max-width: 50%;
|
||||||
|
overflow: auto;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-left: 1px solid var(--digi--typography--color--text--disabled);
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
border-left: 0 none transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: var(--digi--ui--color--background--tertiary);
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { TreeNodesSelectorPanelComponent } from './tree-nodes-selector-panel.component';
|
||||||
|
|
||||||
|
describe('TreeNodesSelectorPanelComponent', () => {
|
||||||
|
let component: TreeNodesSelectorPanelComponent;
|
||||||
|
let fixture: ComponentFixture<TreeNodesSelectorPanelComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [TreeNodesSelectorPanelComponent],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||||
|
}).compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(TreeNodesSelectorPanelComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
import { ButtonSize } from '@af/digi-ng/_button/button';
|
||||||
|
import {
|
||||||
|
Component,
|
||||||
|
OnInit,
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Input,
|
||||||
|
ElementRef,
|
||||||
|
ViewChild,
|
||||||
|
Output,
|
||||||
|
EventEmitter,
|
||||||
|
} from '@angular/core';
|
||||||
|
import {
|
||||||
|
FilterTreeNodeData,
|
||||||
|
TreeNodeModel,
|
||||||
|
TreeNodesSelectorService,
|
||||||
|
} from '../../services/tree-nodes-selector.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'msfa-tree-nodes-selector-panel',
|
||||||
|
templateUrl: './tree-nodes-selector-panel.component.html',
|
||||||
|
styleUrls: ['./tree-nodes-selector-panel.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class TreeNodesSelectorPanelComponent implements OnInit {
|
||||||
|
@Input() rootNode: TreeNodeModel | null = null;
|
||||||
|
@Input() headingText: string;
|
||||||
|
@Input() confirmationButtonText = 'Stäng';
|
||||||
|
@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;
|
||||||
|
|
||||||
|
constructor(private treeNodesSelectorService: TreeNodesSelectorService) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.setupPendingRootNode();
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearSelections(): void {
|
||||||
|
this.pendingRootNode = this.treeNodesSelectorService.getTreeNodeWithNoNodesSelected(this.pendingRootNode);
|
||||||
|
this.latestParentActionKey = `cleared-selections`;
|
||||||
|
this.expandedTreeNodes = this.treeNodesSelectorService.getExpandedTreeNodes(this.pendingRootNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
closePanel(): void {
|
||||||
|
this.selectedChangesConfirmed.emit(this.pendingRootNode);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAriaLabelForToggleAllButton(treeNode: TreeNodeModel): string {
|
||||||
|
return this.treeNodesSelectorService.getAriaLabelForToggleAllButton(treeNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
isExpandedNode(parentTreeNode: TreeNodeModel, treeNode: TreeNodeModel): boolean {
|
||||||
|
return this.treeNodesSelectorService.isExpandedNode(parentTreeNode, treeNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
getToggleAllButtonId(treeNode: TreeNodeModel): string {
|
||||||
|
return this.treeNodesSelectorService.getToggleAllButtonId(treeNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
getItemButtonId(treeNode: TreeNodeModel): string {
|
||||||
|
return this.treeNodesSelectorService.getItemButtonId(treeNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<div class="tree-nodes-selector">
|
||||||
|
<div class="tree-nodes-selector__panel-wrapper">
|
||||||
|
<button
|
||||||
|
#togglePanelBtn
|
||||||
|
(click)="togglePanel()"
|
||||||
|
[attr.aria-controls]="panelId"
|
||||||
|
[ngClass]="{'tree-nodes-selector__toggle-panel-btn--invalid': isInvalid && showValidation}"
|
||||||
|
type="button"
|
||||||
|
class="tree-nodes-selector__toggle-panel-btn"
|
||||||
|
>
|
||||||
|
<span class="tree-nodes-selector__toggle-panel-btn__text">{{selectionLabel}}</span>
|
||||||
|
<digi-icon-arrow-down
|
||||||
|
aria-hidden="true"
|
||||||
|
class="tree-nodes-selector__toggle-panel-btn__arrow-down"
|
||||||
|
></digi-icon-arrow-down>
|
||||||
|
<span class="msfa__a11y-sr-only">
|
||||||
|
Klicka här för att öppna en kontroll där du kan välja mellan ett flertal alternativ i en hierarkisk struktur.
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<div [attr.id]="panelId">
|
||||||
|
<msfa-tree-nodes-selector-panel
|
||||||
|
class="tree-nodes-selector__panel"
|
||||||
|
*ngIf="displayPanel && rootNode"
|
||||||
|
[rootNode]="rootNode"
|
||||||
|
[headingText]="headingText"
|
||||||
|
[confirmationButtonText]="confirmationButtonText"
|
||||||
|
(selectedChangesConfirmed)="updateTreeNodeSelectorWithSelectedChanges($event)"
|
||||||
|
(closePanelRequested)="closePanel()"
|
||||||
|
>
|
||||||
|
</msfa-tree-nodes-selector-panel>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul class="tree-nodes-selector__validation-messages" *ngIf="showValidation && validationMessages">
|
||||||
|
<li *ngFor="let validationMessage of validationMessages">
|
||||||
|
<digi-form-validation-message af-variation="error"> {{validationMessage}} </digi-form-validation-message>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
@import 'variables/colors';
|
||||||
|
@import 'mixins/list';
|
||||||
|
@import 'variables/gutters';
|
||||||
|
|
||||||
|
.tree-nodes-selector {
|
||||||
|
&__panel-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__toggle-panel-btn {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
border: 1px solid var(--digi--ui--input--border--color);
|
||||||
|
background-color: var(--digi--ui--color--background);
|
||||||
|
color: var(--digi--ui--color--primary);
|
||||||
|
padding: 6px 12px;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: var(--digi--typography--font-size--xs);
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: var(--digi--ui--color--focus--light);
|
||||||
|
box-shadow: 0 0 0.1rem 0.05rem var(--digi--ui--color--focus);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
background-color: var(--digi--ui--color--background--disabled);
|
||||||
|
border-color: var(--digi--ui--color--border--disabled);
|
||||||
|
color: var(--digi--typography--color--text--disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--invalid {
|
||||||
|
background-color: var(--digi--ui--color--background--error);
|
||||||
|
border-color: var(--digi--ui--color--border--error);
|
||||||
|
box-shadow: 0 0 0 1px var(--digi--ui--color--border--error);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__validation-messages {
|
||||||
|
@include msfa__reset-list;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: var(--digi--layout--gutter--s) 0;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--digi--layout--gutter--s);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__panel {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { TreeNodesSelectorComponent } from './tree-nodes-selector.component';
|
||||||
|
|
||||||
|
describe('TreeNodesSelectorComponent', () => {
|
||||||
|
let component: TreeNodesSelectorComponent;
|
||||||
|
let fixture: ComponentFixture<TreeNodesSelectorComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [TreeNodesSelectorComponent],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||||
|
}).compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(TreeNodesSelectorComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import {
|
||||||
|
Component,
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
forwardRef,
|
||||||
|
Input,
|
||||||
|
Renderer2,
|
||||||
|
ViewChild,
|
||||||
|
ElementRef,
|
||||||
|
Output,
|
||||||
|
EventEmitter,
|
||||||
|
ChangeDetectorRef,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||||
|
import { UUID } from 'angular2-uuid';
|
||||||
|
import { TreeNode, TreeNodeModel, TreeNodesSelectorService } from '../../services/tree-nodes-selector.service';
|
||||||
|
|
||||||
|
interface PropagateChangeFn {
|
||||||
|
(_: unknown): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PropagateTouchedFn {
|
||||||
|
(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'msfa-tree-nodes-selector',
|
||||||
|
templateUrl: './tree-nodes-selector.component.html',
|
||||||
|
styleUrls: ['./tree-nodes-selector.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: NG_VALUE_ACCESSOR,
|
||||||
|
useExisting: forwardRef(() => TreeNodesSelectorComponent),
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class TreeNodesSelectorComponent implements ControlValueAccessor {
|
||||||
|
@Input() headingText: string;
|
||||||
|
@Input() confirmationButtonText = 'Stäng';
|
||||||
|
@Input() isInvalid = false;
|
||||||
|
@Input() showValidation = false;
|
||||||
|
@Input() validationMessages: Array<string>;
|
||||||
|
@Output() selectedTreeNodesChanged = new EventEmitter<void>();
|
||||||
|
|
||||||
|
@ViewChild('togglePanelBtn') togglePanelBtn: ElementRef;
|
||||||
|
|
||||||
|
rootNode: TreeNodeModel | null = null;
|
||||||
|
displayPanel = false;
|
||||||
|
selectionLabel: string | null = null;
|
||||||
|
isDisabled = false;
|
||||||
|
|
||||||
|
panelId = `panel-${UUID.UUID()}`;
|
||||||
|
|
||||||
|
private propagateChange: PropagateChangeFn;
|
||||||
|
private propagateTouched: PropagateTouchedFn;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private treeNodesSelectorService: TreeNodesSelectorService,
|
||||||
|
private renderer: Renderer2,
|
||||||
|
private changeDetectorRef: ChangeDetectorRef
|
||||||
|
) {}
|
||||||
|
|
||||||
|
writeValue(rootNode: TreeNode | null): void {
|
||||||
|
this.rootNode = this.treeNodesSelectorService.getTreeNodeModelFromTreeNode(rootNode);
|
||||||
|
this.selectionLabel = this.treeNodesSelectorService.getTreeNodeSelectorLabel(this.rootNode);
|
||||||
|
this.changeDetectorRef.detectChanges();
|
||||||
|
}
|
||||||
|
registerOnChange(fn: PropagateChangeFn): void {
|
||||||
|
this.propagateChange = fn;
|
||||||
|
}
|
||||||
|
registerOnTouched(fn: PropagateTouchedFn): void {
|
||||||
|
this.propagateTouched = fn;
|
||||||
|
}
|
||||||
|
setDisabledState?(isDisabled: boolean): void {
|
||||||
|
this.renderer.setProperty(this.togglePanelBtn.nativeElement, 'disabled', isDisabled);
|
||||||
|
|
||||||
|
this.closePanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTreeNodeSelectorWithSelectedChanges(treeNode: TreeNodeModel): void {
|
||||||
|
this.rootNode = treeNode;
|
||||||
|
this.selectionLabel = this.treeNodesSelectorService.getTreeNodeSelectorLabel(this.rootNode);
|
||||||
|
this.propagateTouched();
|
||||||
|
this.propagateChange(this.treeNodesSelectorService.getBasicTreeNodeFromModel(this.rootNode));
|
||||||
|
this.selectedTreeNodesChanged.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
closePanel(): void {
|
||||||
|
this.displayPanel = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
togglePanel(): void {
|
||||||
|
this.displayPanel = !this.displayPanel;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,724 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { TreeNode, TreeNodeModel, TreeNodesSelectorService } from './tree-nodes-selector.service';
|
||||||
|
|
||||||
|
describe('TreeNodesSelectorService', () => {
|
||||||
|
let service: TreeNodesSelectorService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(TreeNodesSelectorService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('allChildLeafNodesAreSelected', () => {
|
||||||
|
it('should return false if not all leaf nodes among the given tree nodes direct children are selectd', () => {
|
||||||
|
const treeNode: TreeNode = {
|
||||||
|
label: 'root',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: 'level 1',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'level 1',
|
||||||
|
isSelected: true,
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
expect(service.allChildLeafNodesAreSelected(treeNode)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true if all leaf nodes among the given tree nodes direct children are selectd', () => {
|
||||||
|
const treeNode = {
|
||||||
|
label: 'root',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: 'level 1',
|
||||||
|
isSelected: true,
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'level 1',
|
||||||
|
isSelected: true,
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(service.allChildLeafNodesAreSelected(treeNode)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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 = {
|
||||||
|
label: 'root',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: 'a B',
|
||||||
|
isSelected: true,
|
||||||
|
value: null,
|
||||||
|
uuid: '1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'a b c',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
uuid: '2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'h b C f',
|
||||||
|
isSelected: true,
|
||||||
|
value: null,
|
||||||
|
uuid: '3',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'd f',
|
||||||
|
isSelected: true,
|
||||||
|
value: null,
|
||||||
|
uuid: '4',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = service.getFilteredTreeNodeChildren({ text: 'B C', treeNode });
|
||||||
|
|
||||||
|
expect(result).not.toBeUndefined();
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result.length).toEqual(4);
|
||||||
|
expect(result[0].hideTreeNode).toBe(true);
|
||||||
|
expect(result[0].uuid).toEqual('1');
|
||||||
|
expect(result[1].hideTreeNode).toBe(false);
|
||||||
|
expect(result[1].uuid).toEqual('2');
|
||||||
|
expect(result[2].hideTreeNode).toBe(false);
|
||||||
|
expect(result[2].uuid).toEqual('3');
|
||||||
|
expect(result[3].hideTreeNode).toBe(true);
|
||||||
|
expect(result[3].uuid).toEqual('4');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getTreeNodeFilteredBySelectedLeafOrDecsendants', () => {
|
||||||
|
it('should return a tree node that either is a selected leaf node or has at least one descendant tree node that is a selected leaf node', () => {
|
||||||
|
const treeNode: TreeNodeModel = {
|
||||||
|
label: 'root',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
uuid: '0',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: '1evel 1',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
uuid: '1',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: '1evel 2',
|
||||||
|
isSelected: true,
|
||||||
|
value: null,
|
||||||
|
uuid: '1_1',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: '1evel 3',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
uuid: '1_1_1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'level 3',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
uuid: '1_1_2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'level 2',
|
||||||
|
isSelected: true,
|
||||||
|
value: null,
|
||||||
|
uuid: '1_2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'level 1',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
uuid: '2',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: 'level 2',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
uuid: '2_1',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: '1evel 3',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
uuid: '2_1_1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'level 3',
|
||||||
|
isSelected: true,
|
||||||
|
value: null,
|
||||||
|
uuid: '2_1_2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'level 2',
|
||||||
|
isSelected: true,
|
||||||
|
value: null,
|
||||||
|
uuid: '2_2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result: TreeNodeModel = service.getTreeNodeFilteredBySelectedLeafOrDecsendants(treeNode);
|
||||||
|
|
||||||
|
expect(result).not.toBeUndefined();
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result.uuid).toEqual('0');
|
||||||
|
expect(result.children).not.toBeUndefined();
|
||||||
|
expect(result.children).not.toBeNull();
|
||||||
|
expect(result.children.length).toEqual(2);
|
||||||
|
expect(result.children[0].uuid).toEqual('1');
|
||||||
|
expect(result.children[0].children).not.toBeUndefined();
|
||||||
|
expect(result.children[0].children).not.toBeNull();
|
||||||
|
expect(result.children[0].children.length).toEqual(1);
|
||||||
|
expect(result.children[0].children[0].uuid).toEqual('1_2');
|
||||||
|
expect(result.children[1].uuid).toEqual('2');
|
||||||
|
expect(result.children[1].children).not.toBeUndefined();
|
||||||
|
expect(result.children[1].children).not.toBeNull();
|
||||||
|
expect(result.children[1].children.length).toEqual(2);
|
||||||
|
expect(result.children[1].children[0].uuid).toEqual('2_1');
|
||||||
|
expect(result.children[1].children[0].children).not.toBeUndefined();
|
||||||
|
expect(result.children[1].children[0].children).not.toBeNull();
|
||||||
|
expect(result.children[1].children[0].children.length).toEqual(1);
|
||||||
|
expect(result.children[1].children[0].children[0].uuid).toEqual('2_1_2');
|
||||||
|
expect(result.children[1].children[1].uuid).toEqual('2_2');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getTreeNodeWithAllNodesSelected', () => {
|
||||||
|
it('should return a tree node with all sub nodes having the property isSelected set to true', () => {
|
||||||
|
const treeNode: TreeNodeModel = {
|
||||||
|
label: 'root',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
uuid: '0',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: '1evel 1',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
uuid: '1',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: '1evel 2',
|
||||||
|
isSelected: true,
|
||||||
|
value: null,
|
||||||
|
uuid: '1_1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'level 2',
|
||||||
|
isSelected: true,
|
||||||
|
value: null,
|
||||||
|
uuid: '1_2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'level 1',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
uuid: '2',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: 'level 2',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
uuid: '2_1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'level 2',
|
||||||
|
isSelected: true,
|
||||||
|
value: null,
|
||||||
|
uuid: '2_2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result: TreeNodeModel = service.getTreeNodeWithAllNodesSelected(treeNode);
|
||||||
|
|
||||||
|
expect(result).not.toBeUndefined();
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result.isSelected).toBe(true);
|
||||||
|
expect(result.children[0].isSelected).toBe(true);
|
||||||
|
expect(result.children[0].children[0].isSelected).toBe(true);
|
||||||
|
expect(result.children[0].children[1].isSelected).toBe(true);
|
||||||
|
expect(result.children[1].isSelected).toBe(true);
|
||||||
|
expect(result.children[1].children[0].isSelected).toBe(true);
|
||||||
|
expect(result.children[1].children[1].isSelected).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getTreeNodeWithNoNodesSelected', () => {
|
||||||
|
it('should return a tree node with all sub nodes having the property isSelected set to false', () => {
|
||||||
|
const treeNode: TreeNodeModel = {
|
||||||
|
label: 'root',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
uuid: '0',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: '1evel 1',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
uuid: '1',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: '1evel 2',
|
||||||
|
isSelected: true,
|
||||||
|
value: null,
|
||||||
|
uuid: '1_1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'level 2',
|
||||||
|
isSelected: true,
|
||||||
|
value: null,
|
||||||
|
uuid: '1_2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'level 1',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
uuid: '2',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: 'level 2',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
uuid: '2_1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'level 2',
|
||||||
|
isSelected: true,
|
||||||
|
value: null,
|
||||||
|
uuid: '2_2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result: TreeNodeModel = service.getTreeNodeWithNoNodesSelected(treeNode);
|
||||||
|
|
||||||
|
expect(result).not.toBeUndefined();
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result.isSelected).toBe(false);
|
||||||
|
expect(result.children[0].isSelected).toBe(false);
|
||||||
|
expect(result.children[0].children[0].isSelected).toBe(false);
|
||||||
|
expect(result.children[0].children[1].isSelected).toBe(false);
|
||||||
|
expect(result.children[1].isSelected).toBe(false);
|
||||||
|
expect(result.children[1].children[0].isSelected).toBe(false);
|
||||||
|
expect(result.children[1].children[1].isSelected).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getVisibleChildren', () => {
|
||||||
|
it('should return a list with all the child nodes having property hideTreeNode set to false', () => {
|
||||||
|
const treeNode: TreeNodeModel = {
|
||||||
|
label: 'root',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
uuid: '0',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: '1evel 1',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
uuid: '1',
|
||||||
|
hideTreeNode: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'level 1',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
uuid: '2',
|
||||||
|
hideTreeNode: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '1evel 1',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
uuid: '3',
|
||||||
|
hideTreeNode: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'level 1',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
uuid: '4',
|
||||||
|
hideTreeNode: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = service.getVisibleChildren(treeNode);
|
||||||
|
|
||||||
|
expect(result).not.toBeUndefined();
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result.length).toEqual(2);
|
||||||
|
expect(result[0].uuid).toEqual('2');
|
||||||
|
expect(result[1].uuid).toEqual('4');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('hasChildLeafNodes', () => {
|
||||||
|
it('should return true when the tree node has a leaf node amongst its children', () => {
|
||||||
|
const treeNode: TreeNodeModel = {
|
||||||
|
label: 'root',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
uuid: '0',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: '1evel 1',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'level 1',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: '1evel 2',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = service.hasChildLeafNodes(treeNode);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when the tree node has no leaf node amongst its children', () => {
|
||||||
|
const treeNode: TreeNodeModel = {
|
||||||
|
label: 'root',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
uuid: '0',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: '1evel 1',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: '1evel 2',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'level 1',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: '1evel 2',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = service.hasChildLeafNodes(treeNode);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('hasSelectedAllLeafNodes', () => {
|
||||||
|
it('should return true when all leaf nodes are selected', () => {
|
||||||
|
const treeNode: TreeNodeModel = {
|
||||||
|
label: 'root',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
uuid: '0',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: '1evel 1',
|
||||||
|
isSelected: true,
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'level 1',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: '1evel 2',
|
||||||
|
isSelected: true,
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '1evel 2',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: '1evel 3',
|
||||||
|
isSelected: true,
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '1evel 3',
|
||||||
|
isSelected: true,
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = service.hasChildLeafNodes(treeNode);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when at least one leaf node is not selected', () => {
|
||||||
|
const treeNode: TreeNodeModel = {
|
||||||
|
label: 'root',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
uuid: '0',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: '1evel 1',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: '1evel 2',
|
||||||
|
isSelected: true,
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'level 1',
|
||||||
|
isSelected: true,
|
||||||
|
value: null,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: '1evel 2',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = service.hasSelectedAllLeafNodes(treeNode);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('hasSelectedLeafNodeDescendant', () => {
|
||||||
|
it('should return true when there is at least one selected leaf node descendant.', () => {
|
||||||
|
const treeNode: TreeNodeModel = {
|
||||||
|
label: 'root',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
uuid: '0',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: '1evel 1',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'level 1',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: '1evel 2',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '1evel 2',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: '1evel 3',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '1evel 3',
|
||||||
|
isSelected: true,
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = service.hasSelectedLeafNodeDescendant(treeNode);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when no leaf node is selected', () => {
|
||||||
|
const treeNode: TreeNodeModel = {
|
||||||
|
label: 'root',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
uuid: '0',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: '1evel 1',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'level 1',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: '1evel 2',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '1evel 2',
|
||||||
|
isSelected: true,
|
||||||
|
value: null,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: '1evel 3',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '1evel 3',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = service.hasSelectedLeafNodeDescendant(treeNode);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,363 @@
|
|||||||
|
import { ElementRef, Injectable } from '@angular/core';
|
||||||
|
import { UUID } from 'angular2-uuid';
|
||||||
|
|
||||||
|
export interface TreeNode {
|
||||||
|
label: string;
|
||||||
|
isSelected: boolean;
|
||||||
|
value: unknown;
|
||||||
|
childItemType?: string;
|
||||||
|
grandChildrenItemType?: string;
|
||||||
|
grandChildrenInfo?: string;
|
||||||
|
toggleAllChildrenLabel?: string;
|
||||||
|
children?: Array<TreeNode>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TreeNodeModel extends TreeNode {
|
||||||
|
uuid?: string;
|
||||||
|
expandedChildUuid?: string;
|
||||||
|
children?: Array<TreeNodeModel>;
|
||||||
|
hasFocus?: boolean;
|
||||||
|
toggleAllHasFocus?: boolean;
|
||||||
|
showGeneralInfoAboutGrandChildren?: boolean;
|
||||||
|
hideTreeNode?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FilterTreeNodeData {
|
||||||
|
text: string;
|
||||||
|
treeNode: TreeNodeModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class TreeNodesSelectorService {
|
||||||
|
getTreeNodeModelFromTreeNode(treeNode: TreeNode): TreeNodeModel | null {
|
||||||
|
let children: Array<TreeNode> = [];
|
||||||
|
|
||||||
|
if (!treeNode) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
children = treeNode.children?.map(childNode => this.getTreeNodeModelFromTreeNode(childNode));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...treeNode,
|
||||||
|
uuid: UUID.UUID(),
|
||||||
|
hasFocus: false,
|
||||||
|
children,
|
||||||
|
hideTreeNode: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getBasicTreeNodeFromModel(treeNodeModel: TreeNodeModel): TreeNode {
|
||||||
|
const children = treeNodeModel?.children?.map(childNode => this.getBasicTreeNodeFromModel(childNode));
|
||||||
|
|
||||||
|
const treeNode: TreeNode = {
|
||||||
|
isSelected: treeNodeModel.isSelected,
|
||||||
|
value: treeNodeModel.value,
|
||||||
|
label: treeNodeModel.label,
|
||||||
|
grandChildrenItemType: treeNodeModel.grandChildrenItemType,
|
||||||
|
grandChildrenInfo: treeNodeModel.grandChildrenInfo,
|
||||||
|
childItemType: treeNodeModel.childItemType,
|
||||||
|
toggleAllChildrenLabel: treeNodeModel.toggleAllChildrenLabel,
|
||||||
|
children,
|
||||||
|
};
|
||||||
|
|
||||||
|
return treeNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasSelectedLeafNodeDescendant(treeNode: TreeNodeModel): boolean {
|
||||||
|
if (!treeNode || !treeNode.children) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return treeNode.children.some(
|
||||||
|
childNode => (this.isLeafNode(childNode) && childNode.isSelected) || this.hasSelectedLeafNodeDescendant(childNode)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAriaLabelTextForExpansionButton(parentTreeNode: TreeNodeModel, treeNode: TreeNodeModel): string {
|
||||||
|
const isActiveNode = parentTreeNode && treeNode ? parentTreeNode.expandedChildUuid === treeNode.uuid : false;
|
||||||
|
|
||||||
|
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 {
|
||||||
|
if (!node || !node.children || node.children.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return node.children.some(child => this.isLeafNode(child));
|
||||||
|
}
|
||||||
|
|
||||||
|
isLeafNode(node: TreeNodeModel): boolean {
|
||||||
|
return node && (!node.children || node.children.length === 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
allChildLeafNodesAreSelected(node: TreeNodeModel): boolean {
|
||||||
|
if (!node || !node.children || node.children.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !parentTreeNode.hideTreeNode && !treeNode.hideTreeNode && parentTreeNode.expandedChildUuid === treeNode.uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
getVisibleChildren(treeNode: TreeNodeModel): Array<TreeNodeModel> | null {
|
||||||
|
if (!treeNode || !treeNode.children) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return treeNode.children.filter(childNode => !childNode.hideTreeNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTreeNodeWithNoNodesSelected(treeNode: TreeNodeModel): TreeNodeModel {
|
||||||
|
return this.getTreeNodeWithAllOrNoNodesSelected(treeNode, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTreeNodeWithAllNodesSelected(treeNode: TreeNodeModel): TreeNodeModel {
|
||||||
|
return this.getTreeNodeWithAllOrNoNodesSelected(treeNode, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTreeNodeWithAllOrNoNodesSelected(treeNode: TreeNodeModel, selectingAll: boolean): TreeNodeModel {
|
||||||
|
if (!treeNode) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...treeNode,
|
||||||
|
isSelected: selectingAll,
|
||||||
|
children: treeNode.children
|
||||||
|
? treeNode.children.map(child => this.getTreeNodeWithAllOrNoNodesSelected(child, selectingAll))
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getAriaLabelForToggleAllButton(treeNode: TreeNodeModel): string {
|
||||||
|
const allChildNodesAreSelected = this.allChildLeafNodesAreSelected(treeNode);
|
||||||
|
|
||||||
|
return `${allChildNodesAreSelected ? 'Tar bort' : 'Väljer'} samtliga alternativ under: ${treeNode.label}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getFilterButtonAriaLabelText(treeNode: TreeNodeModel): string {
|
||||||
|
if (!treeNode) {
|
||||||
|
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}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCleanedText(text: string): string {
|
||||||
|
return text?.trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
getFilteredTreeNodeChildren(filterTreeNodeData: FilterTreeNodeData): Array<TreeNodeModel> {
|
||||||
|
let cleanedFilterText: string = null;
|
||||||
|
|
||||||
|
if (!filterTreeNodeData || !filterTreeNodeData.treeNode || !filterTreeNodeData.treeNode.children) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanedFilterText = this.getCleanedText(filterTreeNodeData.text);
|
||||||
|
|
||||||
|
if (!cleanedFilterText || cleanedFilterText.length === 0) {
|
||||||
|
return filterTreeNodeData.treeNode.children.map(childNode => {
|
||||||
|
return { ...childNode, hideTreeNode: false };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return filterTreeNodeData.treeNode.children.map(childNode => {
|
||||||
|
return { ...childNode, hideTreeNode: this.getCleanedText(childNode?.label).indexOf(cleanedFilterText) === -1 };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getTreeNodeFilteredBySelectedLeafOrDecsendants(treeNode: TreeNode): TreeNode {
|
||||||
|
const filteredTreeNode: TreeNode = {
|
||||||
|
...treeNode,
|
||||||
|
children: treeNode.children
|
||||||
|
? treeNode.children
|
||||||
|
.filter(
|
||||||
|
childNode =>
|
||||||
|
this.hasSelectedLeafNodeDescendant(childNode) || (this.isLeafNode(childNode) && childNode.isSelected)
|
||||||
|
)
|
||||||
|
.map(childNode => this.getTreeNodeFilteredBySelectedLeafOrDecsendants(childNode))
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
return filteredTreeNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTreeNodeSelectorLabel(treeNode: TreeNode): string {
|
||||||
|
const levelInfoList: Array<{ levelItemType: string; sumOfItems: number }> = [];
|
||||||
|
let selectorLabel = '';
|
||||||
|
|
||||||
|
if (!treeNode) {
|
||||||
|
return selectorLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectorLabel = `Välj ${this.getCleanedText(treeNode.label)}`;
|
||||||
|
|
||||||
|
if (!treeNode.children || treeNode.children.length === 0) {
|
||||||
|
return selectorLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateLevelInfoList(treeNode, 0, levelInfoList);
|
||||||
|
|
||||||
|
if (!levelInfoList || levelInfoList.length === 0 || levelInfoList[0].sumOfItems === 0) {
|
||||||
|
return selectorLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectorLabel = levelInfoList.map(levelInfo => `${levelInfo.sumOfItems} ${levelInfo.levelItemType}`).join(', ');
|
||||||
|
|
||||||
|
return selectorLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateLevelInfoList(
|
||||||
|
treeNode: TreeNode,
|
||||||
|
level: number,
|
||||||
|
levelInfoList: Array<{ levelItemType: string; sumOfItems: number }>
|
||||||
|
): void {
|
||||||
|
let selectedDescendants: Array<TreeNode> = [];
|
||||||
|
let nodeSum = 0;
|
||||||
|
let i: number;
|
||||||
|
|
||||||
|
if (!treeNode || !treeNode.children || treeNode.children.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedDescendants = treeNode.children.filter(
|
||||||
|
childNode => this.hasSelectedLeafNodeDescendant(childNode) || (this.isLeafNode(childNode) && childNode.isSelected)
|
||||||
|
);
|
||||||
|
nodeSum = selectedDescendants.length;
|
||||||
|
|
||||||
|
if (levelInfoList.length > level && levelInfoList[level]) {
|
||||||
|
levelInfoList[level].sumOfItems += nodeSum;
|
||||||
|
} else {
|
||||||
|
levelInfoList[level] = { levelItemType: treeNode.childItemType, sumOfItems: nodeSum };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isLeafNode(treeNode)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i = 0; i < selectedDescendants.length; i++) {
|
||||||
|
this.updateLevelInfoList(selectedDescendants[i], level + 1, levelInfoList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getClonedTreeNode(treeNode: TreeNodeModel): TreeNodeModel | null {
|
||||||
|
let clonedTreeNode: TreeNodeModel | null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
clonedTreeNode = JSON.parse(JSON.stringify(treeNode)) as TreeNode;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error, 'Failed to clone tree node');
|
||||||
|
}
|
||||||
|
|
||||||
|
return clonedTreeNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasSelectedAllLeafNodes(treeNode: TreeNode): boolean {
|
||||||
|
if (!treeNode) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.isLeafNode(treeNode)
|
||||||
|
? treeNode.isSelected
|
||||||
|
: !treeNode.children.some(childNode => !this.hasSelectedAllLeafNodes(childNode));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { TreeNodesSelectorPanelComponent } from './components/tree-nodes-selector-panel/tree-nodes-selector-panel.component';
|
||||||
|
import { TreeNodesSelectorComponent } from './components/tree-nodes-selector/tree-nodes-selector.component';
|
||||||
|
import { ExpandedTreeNodeComponent } from './components/expanded-tree-node/expanded-tree-node.component';
|
||||||
|
import { DigiNgTypographyDynamicHeadingModule } from '@af/digi-ng/_typography/typography-dynamic-heading';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [TreeNodesSelectorComponent, TreeNodesSelectorPanelComponent, ExpandedTreeNodeComponent],
|
||||||
|
imports: [CommonModule, DigiNgTypographyDynamicHeadingModule],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||||
|
exports: [TreeNodesSelectorComponent],
|
||||||
|
})
|
||||||
|
export class TreeNodesSelectorModule {}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { UtforandeVerksamheterService } from './utforande-verksamheter.service';
|
||||||
|
|
||||||
|
describe('UtforandeVerksamheterService', () => {
|
||||||
|
let service: UtforandeVerksamheterService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [HttpClientTestingModule],
|
||||||
|
providers: [UtforandeVerksamheterService],
|
||||||
|
});
|
||||||
|
service = TestBed.inject(UtforandeVerksamheterService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { environment } from '@msfa-environment';
|
||||||
|
import {
|
||||||
|
TreeNode,
|
||||||
|
TreeNodesSelectorService,
|
||||||
|
} from '@msfa-shared/components/tree-nodes-selector/services/tree-nodes-selector.service';
|
||||||
|
import { Observable, of } from 'rxjs';
|
||||||
|
|
||||||
|
export interface UtforandeVerksamhetAdress {
|
||||||
|
id: number;
|
||||||
|
namn: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UtforandeVerksamhet {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
adresser: Array<UtforandeVerksamhetAdress>;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class UtforandeVerksamheterService {
|
||||||
|
private readonly apiBaseUrl = `${environment.api.url}/utforandeverksamheter`;
|
||||||
|
|
||||||
|
constructor(private treeNodesSelectorService: TreeNodesSelectorService, private httpClient: HttpClient) {}
|
||||||
|
|
||||||
|
getUtforandeVerksamheter(tjanstIds: Array<number>): Observable<Array<UtforandeVerksamhet>> {
|
||||||
|
let params = new HttpParams();
|
||||||
|
let i: number;
|
||||||
|
|
||||||
|
if (!tjanstIds) {
|
||||||
|
return of<Array<UtforandeVerksamhet>>([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i = 0; i < tjanstIds.length; i++) {
|
||||||
|
params = params.append('tjansteIds', tjanstIds[i].toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.httpClient.get<Array<UtforandeVerksamhet>>(`${this.apiBaseUrl}`, { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
getTreeNodeDataFromUtforandeVerksamheter(utforandeVerksamhetList: Array<UtforandeVerksamhet>): TreeNode | null {
|
||||||
|
let treeNode: TreeNode | null = null;
|
||||||
|
|
||||||
|
if (!utforandeVerksamhetList || utforandeVerksamhetList.length === 0 || !Array.isArray(utforandeVerksamhetList)) {
|
||||||
|
return treeNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
treeNode = {
|
||||||
|
label: 'Utförande Verksamheter',
|
||||||
|
grandChildrenItemType: 'Adresser',
|
||||||
|
grandChildrenInfo: 'Här kan du välja adresser när du valt en verksamhet till vänster.',
|
||||||
|
isSelected: false,
|
||||||
|
value: null,
|
||||||
|
childItemType: 'Utförande verksamheter',
|
||||||
|
children: utforandeVerksamhetList.map(
|
||||||
|
(utforandeVerksamhet: UtforandeVerksamhet): TreeNode => {
|
||||||
|
const utforandeVerksahmetTreeNode: TreeNode = {
|
||||||
|
label: utforandeVerksamhet.name,
|
||||||
|
toggleAllChildrenLabel: 'Välj alla adresser',
|
||||||
|
isSelected: false,
|
||||||
|
value: utforandeVerksamhet,
|
||||||
|
childItemType: 'Adresser',
|
||||||
|
children: utforandeVerksamhet.adresser
|
||||||
|
? utforandeVerksamhet.adresser.map(adress => {
|
||||||
|
return { label: adress.namn, isSelected: false, value: adress };
|
||||||
|
})
|
||||||
|
: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
return utforandeVerksahmetTreeNode;
|
||||||
|
}
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
return treeNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSelectedUtforandeVerksamheterFromTreeNode(treeNode: TreeNode): Array<UtforandeVerksamhet> {
|
||||||
|
let utforandeVerksamhetList: Array<UtforandeVerksamhet> = [];
|
||||||
|
|
||||||
|
if (!treeNode || !treeNode.children) {
|
||||||
|
return utforandeVerksamhetList;
|
||||||
|
}
|
||||||
|
|
||||||
|
utforandeVerksamhetList = this.treeNodesSelectorService
|
||||||
|
.getTreeNodeFilteredBySelectedLeafOrDecsendants(treeNode)
|
||||||
|
?.children?.map(utforandeVerksamhetNode => {
|
||||||
|
const originalUtforandeVerksamhet = utforandeVerksamhetNode.value as UtforandeVerksamhet;
|
||||||
|
const utforandeVerksamhet: UtforandeVerksamhet = {
|
||||||
|
name: originalUtforandeVerksamhet?.name,
|
||||||
|
id: originalUtforandeVerksamhet?.id,
|
||||||
|
adresser: utforandeVerksamhetNode.children.map(adressNode => {
|
||||||
|
return adressNode.value as UtforandeVerksamhetAdress;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
return utforandeVerksamhet;
|
||||||
|
});
|
||||||
|
|
||||||
|
return utforandeVerksamhetList ? utforandeVerksamhetList : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
hasSelectedUtforandeVerksamhet = (treeNode: TreeNode): boolean => {
|
||||||
|
if (!treeNode || !treeNode.children || treeNode.children.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return treeNode.children.some(childNode => this.treeNodesSelectorService.hasSelectedLeafNodeDescendant(childNode));
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { AbstractControl, ValidatorFn } from '@angular/forms';
|
||||||
|
import { TreeNode } from '@msfa-shared/components/tree-nodes-selector/services/tree-nodes-selector.service';
|
||||||
|
|
||||||
|
export class TreeNodeValidator {
|
||||||
|
static IsValidTreeNode(
|
||||||
|
validationFn: (treeNode: TreeNode | null | undefined) => boolean,
|
||||||
|
nameOfError: string
|
||||||
|
): ValidatorFn {
|
||||||
|
return (control: AbstractControl): { [key: string]: unknown } => {
|
||||||
|
const isValid = validationFn(control.value);
|
||||||
|
|
||||||
|
const validationObj = {};
|
||||||
|
|
||||||
|
if (isValid) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
validationObj[nameOfError] = { value: control.value as TreeNode };
|
||||||
|
|
||||||
|
return validationObj;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static Required(): ValidatorFn {
|
||||||
|
return (control: AbstractControl): { [key: string]: unknown } => {
|
||||||
|
const hasSelectedDescendantsOrIsSelectedLeaf = (treeNode: TreeNode): boolean => {
|
||||||
|
if (!treeNode) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!treeNode.children) {
|
||||||
|
return treeNode.isSelected;
|
||||||
|
}
|
||||||
|
|
||||||
|
return treeNode.children.some(childNode => hasSelectedDescendantsOrIsSelectedLeaf(childNode));
|
||||||
|
};
|
||||||
|
const isValid = hasSelectedDescendantsOrIsSelectedLeaf(control.value);
|
||||||
|
|
||||||
|
const validationObj = {};
|
||||||
|
|
||||||
|
if (isValid) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
validationObj['required'] = { value: control.value as TreeNode };
|
||||||
|
|
||||||
|
return validationObj;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,6 +55,7 @@
|
|||||||
"@digi/core": "^9.4.0",
|
"@digi/core": "^9.4.0",
|
||||||
"@digi/styles": "^6.0.2",
|
"@digi/styles": "^6.0.2",
|
||||||
"@nrwl/angular": "11.5.1",
|
"@nrwl/angular": "11.5.1",
|
||||||
|
"angular2-uuid": "^1.1.1",
|
||||||
"date-fns": "^2.22.1",
|
"date-fns": "^2.22.1",
|
||||||
"ngx-markdown": "^11.1.3",
|
"ngx-markdown": "^11.1.3",
|
||||||
"rxjs": "~6.6.3",
|
"rxjs": "~6.6.3",
|
||||||
|
|||||||
Reference in New Issue
Block a user