feat(avrop): Implemented custom filters for avrop. (TV-344)
Squashed commit of the following: commit 4afa2445d552cb82ccc40a0e3ad706173ebc541f Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se> Date: Thu Sep 16 16:55:46 2021 +0200 Fixade bugg som gjorde att inte selekteringar nollställdes ifall man stängde utan att spara commit ddda68558f3ba253e67a66b773f99ba781a3b46d Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se> Date: Thu Sep 16 14:18:48 2021 +0200 Fixed issue with unavailable options commit 58bc4aa4f34ff7e2b13e8ea85182c31735df08a0 Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se> Date: Thu Sep 16 12:56:55 2021 +0200 Added margin to unavailable selected items commit 93e1b61bbbe1ec7a6e785ed468ba8648668f30c8 Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se> Date: Thu Sep 16 12:53:52 2021 +0200 Fixed issue where selected filters are not selected commit 582a89be65adb17811c6e14cedad327b6960b20c Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se> Date: Thu Sep 16 11:54:17 2021 +0200 refactor commit ddc300d942b069d7440c0f24da8bcb917df65df7 Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se> Date: Thu Sep 16 11:52:58 2021 +0200 replace ng-uuid package with util function commit 976a78aa22a5abe587e0f33e4255f44521b5f5f6 Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se> Date: Thu Sep 16 11:47:53 2021 +0200 fix propagate commit dabe345243e0e5454ff9ef5af0ccff84b1fc4a09 Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se> Date: Thu Sep 16 11:22:58 2021 +0200 . commit e42c6e5588cd3cabd2cec3c10f363b193536ad7e Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se> Date: Thu Sep 16 09:17:43 2021 +0200 Implemented ControlValueAccessor commit 045492202866de21dd0ed34803ab2f3bfc3bb8e3 Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se> Date: Tue Sep 14 13:26:24 2021 +0200 refactor commit 574a3bc505b455bdb100a2e064ad235176545536 Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se> Date: Tue Sep 14 13:21:53 2021 +0200 refactor commit 6ff07030584bb4d904a370f08817e246705af035 Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se> Date: Tue Sep 14 13:17:06 2021 +0200 refactoring commit 365fc080a49dc487423048ed0dea3744be8dd0c5 Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se> Date: Tue Sep 14 12:10:12 2021 +0200 styling och tillgänglighet commit f7e7f3ffa78c5328029513784114083a2a06cd9d Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se> Date: Tue Sep 14 10:54:14 2021 +0200 refactor multiselect commit 91d0e832f6f79991b8577f5aaabd657a5f8bbba2 Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se> Date: Mon Sep 13 16:57:25 2021 +0200 replace temporary filter with new module commit 90829e451eebf98a63a5934066563028b80fbadb Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se> Date: Mon Sep 13 16:33:14 2021 +0200 Add styling from tree-node-selector commit df84e3e75105ffe0c6459606c58684fab1411561 Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se> Date: Mon Sep 13 15:58:37 2021 +0200 feature(General): Added a generic dropdown component (TV-344) commit 9022427d59290dfd2b08c4a076d6b0690d44d926 Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se> Date: Mon Sep 13 13:25:05 2021 +0200 Update mock-apis
This commit is contained in:
@@ -23,10 +23,10 @@ import { UtforandeVerksamhet } from '@msfa-models/utforande-verksamhet.model';
|
|||||||
import { UtforandeVerksamheterService } from '@msfa-services/utforande-verksamheter/utforande-verksamheter.service';
|
import { UtforandeVerksamheterService } from '@msfa-services/utforande-verksamheter/utforande-verksamheter.service';
|
||||||
import { ValidationErrorLink } from '@msfa-shared/components/error-list/error-list.component';
|
import { ValidationErrorLink } from '@msfa-shared/components/error-list/error-list.component';
|
||||||
import { TreeNodesSelectorService } from '@msfa-shared/components/tree-nodes-selector/services/tree-nodes-selector.service';
|
import { TreeNodesSelectorService } from '@msfa-shared/components/tree-nodes-selector/services/tree-nodes-selector.service';
|
||||||
|
import { uuid } from '@msfa-utils/uuid';
|
||||||
import { EmailValidator } from '@msfa-utils/validators/email.validator';
|
import { EmailValidator } from '@msfa-utils/validators/email.validator';
|
||||||
import { EmployeeValidator } from '@msfa-utils/validators/employee.validator';
|
import { EmployeeValidator } from '@msfa-utils/validators/employee.validator';
|
||||||
import { RequiredValidator } from '@msfa-utils/validators/required.validator';
|
import { RequiredValidator } from '@msfa-utils/validators/required.validator';
|
||||||
import { UUID } from 'angular2-uuid';
|
|
||||||
import { EmployeeFormService } from '../services/employee-form.service';
|
import { EmployeeFormService } from '../services/employee-form.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -58,7 +58,7 @@ export class EditEmployeeFormComponent implements OnInit, OnChanges {
|
|||||||
readonly utforandeVerksamheterFormControlName = 'utforandeVerksamheter';
|
readonly utforandeVerksamheterFormControlName = 'utforandeVerksamheter';
|
||||||
readonly selectAllUtforandeVerksamheterFormControlName = 'allaUtforandeVerksamheter';
|
readonly selectAllUtforandeVerksamheterFormControlName = 'allaUtforandeVerksamheter';
|
||||||
readonly utforandeVerksamhetRequiredMessage = 'Välj minst en utförande verksamhet';
|
readonly utforandeVerksamhetRequiredMessage = 'Välj minst en utförande verksamhet';
|
||||||
readonly formUuid = UUID.UUID();
|
readonly formUuid = uuid();
|
||||||
readonly emailElementId = `email-control-${this.formUuid}`;
|
readonly emailElementId = `email-control-${this.formUuid}`;
|
||||||
readonly tjansterElementId = `tjanster-control-${this.formUuid}`;
|
readonly tjansterElementId = `tjanster-control-${this.formUuid}`;
|
||||||
readonly utforandeVerksamhetElementId = `utforande-verksamhet-control-${this.formUuid}`;
|
readonly utforandeVerksamhetElementId = `utforande-verksamhet-control-${this.formUuid}`;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||||
import { Avrop, AvropCompactData } from '@msfa-models/avrop.model';
|
import { Avrop, AvropCompactData } from '@msfa-models/avrop.model';
|
||||||
import { Handledare } from '@msfa-models/handledare.model';
|
import { Handledare } from '@msfa-models/handledare.model';
|
||||||
import { MultiselectFilterOption } from '@msfa-models/multiselect-filter-option';
|
import { MultiselectFilterOption } from '@msfa-shared/components/multiselect/multiselect-filter-option';
|
||||||
import { AvropService } from '@msfa-services/avrop.service';
|
import { AvropService } from '@msfa-services/avrop.service';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +1,32 @@
|
|||||||
<div style="display: flex">
|
<div class="avrop-filters">
|
||||||
<ng-container *ngIf="availableTjanster$ | async; let availableTjanster; else loadingRef">
|
<div class="avrop-filters__filter-wrapper" *ngIf="availableTjanster$ | async; let availableTjanster; else loadingRef">
|
||||||
<msfa-temporary-filter
|
<msfa-multiselect
|
||||||
[filterLabel]="'Tjänster'"
|
[multiselectTitle]="'Tjänster'"
|
||||||
[filterOptions]="availableTjanster"
|
[availableOptions]="availableTjanster"
|
||||||
|
confirmButtonText="Uppdatera filter"
|
||||||
[selectedOptions]="filteredTjanster$ | async"
|
[selectedOptions]="filteredTjanster$ | async"
|
||||||
(selectedOptionsChange)="updateSelectedTjanster($event)"
|
(selectedOptionsChange)="updateSelectedTjanster($event)"
|
||||||
></msfa-temporary-filter>
|
></msfa-multiselect>
|
||||||
</ng-container>
|
</div>
|
||||||
<ng-container *ngIf="availableUtforandeVerksamheter$ | async; let availableUtforandeVerksamheter; else loadingRef">
|
<div class="avrop-filters__filter-wrapper" *ngIf="availableUtforandeVerksamheter$ | async; let availableUtforandeVerksamheter; else loadingRef">
|
||||||
<msfa-temporary-filter
|
<msfa-multiselect
|
||||||
[filterLabel]="'Utförande verksamheter'"
|
[multiselectTitle]="'Utförande verksamheter'"
|
||||||
[filterOptions]="availableUtforandeVerksamheter"
|
confirmButtonText="Uppdatera filter"
|
||||||
|
[availableOptions]="availableUtforandeVerksamheter"
|
||||||
[selectedOptions]="filteredUtforandeVerksamheter$ | async"
|
[selectedOptions]="filteredUtforandeVerksamheter$ | async"
|
||||||
(selectedOptionsChange)="updateSelectedUtforandeVerksamheter($event)"
|
(selectedOptionsChange)="updateSelectedUtforandeVerksamheter($event)"
|
||||||
></msfa-temporary-filter>
|
></msfa-multiselect>
|
||||||
</ng-container>
|
</div>
|
||||||
|
|
||||||
<ng-container *ngIf="availableKommuner$ | async; let availableKommuner; else loadingRef">
|
<div class="avrop-filters__filter-wrapper" *ngIf="availableKommuner$ | async; let availableKommuner; else loadingRef">
|
||||||
<msfa-temporary-filter
|
<msfa-multiselect
|
||||||
[filterLabel]="'Kommuner'"
|
[multiselectTitle]="'Kommuner'"
|
||||||
[filterOptions]="availableKommuner"
|
[availableOptions]="availableKommuner"
|
||||||
[selectedOptions]="filteredKommuner$ | async"
|
[selectedOptions]="filteredKommuner$ | async"
|
||||||
|
confirmButtonText="Uppdatera filter"
|
||||||
(selectedOptionsChange)="updateSelectedKommuner($event)"
|
(selectedOptionsChange)="updateSelectedKommuner($event)"
|
||||||
></msfa-temporary-filter>
|
></msfa-multiselect>
|
||||||
</ng-container>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<br /><br />
|
<br /><br />
|
||||||
<div class="avrop-filters__tags">
|
<div class="avrop-filters__tags">
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
@import 'variables/gutters';
|
@import 'variables/gutters';
|
||||||
|
|
||||||
.avrop-filters {
|
.avrop-filters {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
&__tags {
|
&__tags {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -9,9 +11,13 @@
|
|||||||
|
|
||||||
&__tag {
|
&__tag {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-right: $digi--layout--gutter--s;
|
margin-right: var(--digi--layout--gutter--s);
|
||||||
}
|
}
|
||||||
&__loading-spinner {
|
&__loading-spinner {
|
||||||
margin-right: $digi--layout--gutter;
|
margin-right: var(--digi--layout--gutter);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__filter-wrapper {
|
||||||
|
margin-right: var(--digi--layout--gutter--s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { ComponentFixture, discardPeriodicTasks, fakeAsync, TestBed, tick } from
|
|||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { AvropService } from '@msfa-services/avrop.service';
|
import { AvropService } from '@msfa-services/avrop.service';
|
||||||
import { of } from 'rxjs';
|
import { of } from 'rxjs';
|
||||||
import { TemporaryFilterComponent } from '../temporary-filter/temporary-filter.component';
|
|
||||||
import { AvropFiltersComponent } from './avrop-filters.component';
|
import { AvropFiltersComponent } from './avrop-filters.component';
|
||||||
|
|
||||||
describe('AvropFiltersComponent', () => {
|
describe('AvropFiltersComponent', () => {
|
||||||
@@ -15,7 +14,7 @@ describe('AvropFiltersComponent', () => {
|
|||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||||
imports: [HttpClientTestingModule],
|
imports: [HttpClientTestingModule],
|
||||||
declarations: [AvropFiltersComponent, TemporaryFilterComponent],
|
declarations: [AvropFiltersComponent],
|
||||||
providers: [AvropService],
|
providers: [AvropService],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||||
import { MultiselectFilterOption } from '@msfa-models/multiselect-filter-option';
|
import { MultiselectFilterOption } from '@msfa-shared/components/multiselect/multiselect-filter-option';
|
||||||
import { AvropService } from '@msfa-services/avrop.service';
|
import { AvropService } from '@msfa-services/avrop.service';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { TemporaryFilterModule } from '../temporary-filter/temporary-filter.module';
|
|
||||||
import { AvropFiltersComponent } from './avrop-filters.component';
|
import { AvropFiltersComponent } from './avrop-filters.component';
|
||||||
|
import { MultiselectModule } from '@msfa-shared/components/multiselect/multiselect.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||||
declarations: [AvropFiltersComponent],
|
declarations: [AvropFiltersComponent],
|
||||||
imports: [CommonModule, TemporaryFilterModule],
|
imports: [CommonModule, MultiselectModule],
|
||||||
exports: [AvropFiltersComponent],
|
exports: [AvropFiltersComponent],
|
||||||
})
|
})
|
||||||
export class AvropFiltersModule {}
|
export class AvropFiltersModule {}
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
<div style="border: 2px solid #CCC; background: #EFEFEF; margin-right: 2rem; padding: 1rem;">
|
|
||||||
<strong>{{filterLabel}}</strong>
|
|
||||||
<digi-form-checkbox
|
|
||||||
*ngFor="let filterOption of filterOptions"
|
|
||||||
[afLabel]="filterOption.label + ' (' + (filterOption.count || 0) + ')'"
|
|
||||||
(change)="setOptionState(filterOption, $event.target.checked)"
|
|
||||||
[afChecked]="isSelected(filterOption)"
|
|
||||||
>
|
|
||||||
</digi-form-checkbox>
|
|
||||||
|
|
||||||
<digi-button (click)="emitSelectedOptions()" af-size="s">Spara</digi-button>
|
|
||||||
</div>
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import {
|
|
||||||
ChangeDetectionStrategy,
|
|
||||||
Component,
|
|
||||||
EventEmitter,
|
|
||||||
Input,
|
|
||||||
OnChanges,
|
|
||||||
OnInit,
|
|
||||||
Output,
|
|
||||||
SimpleChanges,
|
|
||||||
} from '@angular/core';
|
|
||||||
import { MultiselectFilterOption } from '@msfa-models/multiselect-filter-option';
|
|
||||||
import { BehaviorSubject, Observable } from 'rxjs';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'msfa-temporary-filter',
|
|
||||||
templateUrl: './temporary-filter.component.html',
|
|
||||||
styleUrls: ['./temporary-filter.component.scss'],
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
||||||
})
|
|
||||||
export class TemporaryFilterComponent implements OnInit, OnChanges {
|
|
||||||
private _selectedAvropFilterOption$ = new BehaviorSubject<MultiselectFilterOption[]>(null);
|
|
||||||
selectedAvropFilterOptionState$: Observable<MultiselectFilterOption[]>;
|
|
||||||
|
|
||||||
@Input() filterLabel: string;
|
|
||||||
@Input() filterOptions: MultiselectFilterOption[];
|
|
||||||
@Input() selectedOptions: MultiselectFilterOption[];
|
|
||||||
@Output() selectedOptionsChange = new EventEmitter<MultiselectFilterOption[]>();
|
|
||||||
|
|
||||||
// THIS SHOULD BE REPLACED BY DIGI COMPONENT
|
|
||||||
ngOnInit(): void {
|
|
||||||
this._selectedAvropFilterOption$ = new BehaviorSubject<MultiselectFilterOption[]>(this.selectedOptions);
|
|
||||||
this.selectedAvropFilterOptionState$ = this._selectedAvropFilterOption$.asObservable();
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
|
||||||
if (changes.selectedOptions?.currentValue) {
|
|
||||||
this._selectedAvropFilterOption$.next(changes.selectedOptions.currentValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isSelected(filterOption: MultiselectFilterOption): boolean {
|
|
||||||
return this.selectedOptions?.includes(filterOption) ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
setOptionState(filterOption: MultiselectFilterOption, isSelected: boolean): void {
|
|
||||||
if (isSelected) {
|
|
||||||
return this._selectedAvropFilterOption$.next([...(this._selectedAvropFilterOption$.value ?? []), filterOption]);
|
|
||||||
}
|
|
||||||
return this._selectedAvropFilterOption$.next(
|
|
||||||
this._selectedAvropFilterOption$.value?.filter(item => item != filterOption) ?? []
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
emitSelectedOptions(): void {
|
|
||||||
this.selectedOptionsChange.emit(this._selectedAvropFilterOption$.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
|
||||||
import { TemporaryFilterComponent } from './temporary-filter.component';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
||||||
declarations: [TemporaryFilterComponent],
|
|
||||||
imports: [CommonModule],
|
|
||||||
exports: [TemporaryFilterComponent],
|
|
||||||
})
|
|
||||||
export class TemporaryFilterModule {}
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { EventEmitter, TemplateRef } from '@angular/core';
|
||||||
|
|
||||||
|
export interface DropdownPanel {
|
||||||
|
templateRef: TemplateRef<never>;
|
||||||
|
readonly closed: EventEmitter<void>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { Overlay, OverlayRef } from '@angular/cdk/overlay';
|
||||||
|
import { TemplatePortal } from '@angular/cdk/portal';
|
||||||
|
import { Directive, ElementRef, HostListener, Input, OnDestroy, ViewContainerRef } from '@angular/core';
|
||||||
|
import { merge, Observable, Subscription } from 'rxjs';
|
||||||
|
import { DropdownPanel } from './dropdown-panel';
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: '[msfaDropdownTriggerFor]',
|
||||||
|
})
|
||||||
|
export class DropdownTriggerForDirective implements OnDestroy {
|
||||||
|
@Input('msfaDropdownTriggerFor') public dropdownPanel: DropdownPanel;
|
||||||
|
|
||||||
|
private isDropdownOpen = false;
|
||||||
|
public overlayRef: OverlayRef;
|
||||||
|
private dropdownClosingActionsSub = Subscription.EMPTY;
|
||||||
|
|
||||||
|
private onClosedCallback: () => void = () => {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
private onOpenCallback: () => void = () => {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
@HostListener('click') onClick(): void {
|
||||||
|
return this.toggleDropdown();
|
||||||
|
}
|
||||||
|
@HostListener('document:keyup.escape', ['$event']) onKeydownHandler(): void {
|
||||||
|
this.closeDropdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private overlay: Overlay,
|
||||||
|
private elementRef: ElementRef<HTMLElement>,
|
||||||
|
private viewContainerRef: ViewContainerRef
|
||||||
|
) {}
|
||||||
|
|
||||||
|
toggleDropdown(): void {
|
||||||
|
this.isDropdownOpen ? this.closeDropdown() : this.openDropdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
openDropdown(): void {
|
||||||
|
this.isDropdownOpen = true;
|
||||||
|
this.overlayRef = this.overlay.create({
|
||||||
|
hasBackdrop: true,
|
||||||
|
backdropClass: 'cdk-overlay-transparent-backdrop',
|
||||||
|
scrollStrategy: this.overlay.scrollStrategies.close(),
|
||||||
|
positionStrategy: this.overlay
|
||||||
|
.position()
|
||||||
|
.flexibleConnectedTo(this.elementRef)
|
||||||
|
.withPositions([
|
||||||
|
{
|
||||||
|
originX: 'start',
|
||||||
|
originY: 'bottom',
|
||||||
|
overlayX: 'start',
|
||||||
|
overlayY: 'top',
|
||||||
|
offsetY: 3,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|
||||||
|
const templatePortal = new TemplatePortal(this.dropdownPanel.templateRef, this.viewContainerRef);
|
||||||
|
|
||||||
|
this.overlayRef.attach(templatePortal);
|
||||||
|
this.onOpenCallback();
|
||||||
|
|
||||||
|
this.dropdownClosingActionsSub = this.dropdownClosingActions().subscribe(() => this.closeDropdown());
|
||||||
|
}
|
||||||
|
|
||||||
|
private dropdownClosingActions(): Observable<MouseEvent | void> {
|
||||||
|
const backdropClick$ = this.overlayRef.backdropClick();
|
||||||
|
const detachment$ = this.overlayRef.detachments();
|
||||||
|
const dropdownClosed = this.dropdownPanel.closed;
|
||||||
|
|
||||||
|
return merge(backdropClick$, detachment$, dropdownClosed);
|
||||||
|
}
|
||||||
|
|
||||||
|
closeDropdown(): void {
|
||||||
|
if (!this.overlayRef || !this.isDropdownOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.onClosedCallback();
|
||||||
|
this.dropdownClosingActionsSub.unsubscribe();
|
||||||
|
this.isDropdownOpen = false;
|
||||||
|
this.overlayRef.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
if (this.overlayRef) {
|
||||||
|
this.overlayRef.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClosed(fn: () => void): void {
|
||||||
|
this.onClosedCallback = fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpen(fn: () => void): void {
|
||||||
|
this.onOpenCallback = fn;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<ng-template>
|
||||||
|
<div class="dropdown-content" cdkTrapFocus cdkTrapFocusAutoCapture>
|
||||||
|
<ng-content></ng-content>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
@import "variables/shadows";
|
||||||
|
|
||||||
|
.dropdown-content {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: var(--digi--ui--border--radius);
|
||||||
|
box-shadow: $msfa__shadow;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { DropdownComponent } from './dropdown.component';
|
||||||
|
|
||||||
|
describe('DropdownComponent', () => {
|
||||||
|
let component: DropdownComponent;
|
||||||
|
let fixture: ComponentFixture<DropdownComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ DropdownComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(DropdownComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, EventEmitter, Output, TemplateRef, ViewChild } from '@angular/core';
|
||||||
|
import { DropdownPanel } from './dropdown-panel';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'msfa-dropdown',
|
||||||
|
templateUrl: './dropdown.component.html',
|
||||||
|
styleUrls: ['./dropdown.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class DropdownComponent implements DropdownPanel {
|
||||||
|
@ViewChild(TemplateRef) templateRef: TemplateRef<never>;
|
||||||
|
@Output() closed = new EventEmitter<void>();
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { DropdownTriggerForDirective } from './dropdown-trigger-for.directive';
|
||||||
|
import { DropdownComponent } from './dropdown.component';
|
||||||
|
import { OverlayModule } from '@angular/cdk/overlay';
|
||||||
|
import { A11yModule } from '@angular/cdk/a11y';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [DropdownTriggerForDirective, DropdownComponent],
|
||||||
|
imports: [CommonModule, OverlayModule, A11yModule],
|
||||||
|
exports: [DropdownTriggerForDirective, DropdownComponent],
|
||||||
|
})
|
||||||
|
export class DropdownModule {}
|
||||||
@@ -2,4 +2,5 @@ export interface MultiselectFilterOption {
|
|||||||
label?: string;
|
label?: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
count?: number;
|
count?: number;
|
||||||
|
secondary?: boolean;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<form autocomplete="off">
|
||||||
|
<div class="multiselect-panel">
|
||||||
|
<div class="multiselect-panel__content">
|
||||||
|
<fieldset
|
||||||
|
class="multiselect-panel__fieldset"
|
||||||
|
*ngIf="availableOptions?.length > 0 else noOptionsWithCurrentFilters"
|
||||||
|
>
|
||||||
|
<legend class="multiselect-panel__legend">{{ multiselectTitle }}</legend>
|
||||||
|
<p *ngIf="description" class="multiselect-panel__description">{{ description }}</p>
|
||||||
|
|
||||||
|
<div class="multiselect-panel__checkboxes">
|
||||||
|
<ul class="multiselect-panel__checkboxes-list">
|
||||||
|
<li
|
||||||
|
class="multiselect-panel__checkboxes-item"
|
||||||
|
*ngFor="let multiselectOption of availableOptions; let i = index"
|
||||||
|
>
|
||||||
|
<digi-ng-form-checkbox
|
||||||
|
[afLabel]="multiselectOption.label + ' (' + (multiselectOption.count || 0) + ')'"
|
||||||
|
(change)="setOptionState(multiselectOption, $event.target.checked)"
|
||||||
|
[ngModel]="isSelected(multiselectOption)"
|
||||||
|
[name]="multiselectOption.id"
|
||||||
|
></digi-ng-form-checkbox>
|
||||||
|
</li>
|
||||||
|
<ng-container *ngIf="selectedUnavailableOptions.length">
|
||||||
|
<li
|
||||||
|
class="multiselect-panel__checkboxes-item"
|
||||||
|
[ngClass]="{'multiselect-panel__checkboxes-item--unavailable': i === 0}"
|
||||||
|
*ngFor="let multiselectOption of selectedUnavailableOptions; let i = index"
|
||||||
|
>
|
||||||
|
<digi-ng-form-checkbox
|
||||||
|
[afLabel]="multiselectOption.label + ' (0)'"
|
||||||
|
(change)="setOptionState(multiselectOption, $event.target.checked)"
|
||||||
|
[ngModel]="isSelected(multiselectOption)"
|
||||||
|
[name]="multiselectOption.id"
|
||||||
|
[afSecondary]="true"
|
||||||
|
></digi-ng-form-checkbox>
|
||||||
|
</li>
|
||||||
|
</ng-container>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="multiselect-panel__footer">
|
||||||
|
<digi-button (click)="emitSelectedOptions()" af-size="m">
|
||||||
|
<span>{{confirmButtonText}}</span>
|
||||||
|
<span class="msfa__a11y-sr-only"> och stäng dialogen för filter {{ multiselectTitle }}</span>
|
||||||
|
</digi-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<ng-template #noOptionsWithCurrentFilters>
|
||||||
|
<div class="multiselect-panel__no-options-available">
|
||||||
|
<p>
|
||||||
|
I kombination med dina nuvarande filterval, finns det inga nya deltagare med {{ multiselectTitlePlural }} att
|
||||||
|
filtrera på.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
@import 'mixins/form';
|
||||||
|
|
||||||
|
@import 'mixins/a11y';
|
||||||
|
.multiselect-panel {
|
||||||
|
&__checkboxes {
|
||||||
|
max-width: var(--digi--typography--text--max-width);
|
||||||
|
padding: var(--digi--layout--padding--15);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__fieldset {
|
||||||
|
@include msfa__fieldset;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__checkboxes-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__checkboxes-item {
|
||||||
|
margin-top: var(--digi--layout--gutter--s);
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--unavailable {
|
||||||
|
margin-top: var(--digi--layout--gutter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__search-wrapper {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: var(--digi--layout--gutter--l);
|
||||||
|
|
||||||
|
&--searched {
|
||||||
|
margin-bottom: var(--digi--layout--gutter--s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__select-all {
|
||||||
|
margin-bottom: var(--digi--layout--gutter--l);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__search-input ::ng-deep .digi-ng-form-input__label {
|
||||||
|
@include msfa__a11y-sr-only;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__input-reset-button {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-panel {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
::ng-deep &__backdrop {
|
||||||
|
background-color: rgba(var(--digi--ui--color--background--dark), 40%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__fieldset {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__legend {
|
||||||
|
position: relative;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__heading,
|
||||||
|
&__legend {
|
||||||
|
width: 100%;
|
||||||
|
height: 45px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: var(--digi--typography--font-weight--bold);
|
||||||
|
font-size: var(--digi--typography--font-size);
|
||||||
|
padding: var(--digi--layout--gutter--s) var(--digi--layout--gutter);
|
||||||
|
border-bottom: 1px solid var(--digi--ui--color--background--tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
position: relative;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__inner-content {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 300px;
|
||||||
|
max-width: 800px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: var(--digi--ui--color--background--tertiary);
|
||||||
|
height: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__no-options-available {
|
||||||
|
text-align: center;
|
||||||
|
padding: var(--digi--layout--gutter--s);
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { MultiselectPanelComponent } from './multiselect-panel.component';
|
||||||
|
|
||||||
|
describe('MultiselectPanelComponent', () => {
|
||||||
|
let component: MultiselectPanelComponent;
|
||||||
|
let fixture: ComponentFixture<MultiselectPanelComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [MultiselectPanelComponent],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||||
|
}).compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(MultiselectPanelComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
EventEmitter,
|
||||||
|
Input,
|
||||||
|
OnChanges,
|
||||||
|
Output,
|
||||||
|
SimpleChanges,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { DropdownTriggerForDirective } from '@msfa-shared/components/dropdown/dropdown-trigger-for.directive';
|
||||||
|
import { MultiselectFilterOption } from '@msfa-shared/components/multiselect/multiselect-filter-option';
|
||||||
|
import { BehaviorSubject } from 'rxjs';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'msfa-multiselect-panel',
|
||||||
|
templateUrl: './multiselect-panel.component.html',
|
||||||
|
styleUrls: ['./multiselect-panel.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class MultiselectPanelComponent implements OnChanges {
|
||||||
|
@Input() dropdownRef: DropdownTriggerForDirective;
|
||||||
|
@Input() confirmButtonText = 'Spara';
|
||||||
|
@Input() multiselectTitle: string;
|
||||||
|
@Input() multiselectTitlePlural: string;
|
||||||
|
@Input() description: string;
|
||||||
|
@Input() availableOptions: MultiselectFilterOption[];
|
||||||
|
@Input() selectedOptions: MultiselectFilterOption[];
|
||||||
|
@Output() selectedOptionsChange = new EventEmitter<MultiselectFilterOption[]>();
|
||||||
|
|
||||||
|
private _pendingSelectedOptions$ = new BehaviorSubject<MultiselectFilterOption[]>([]);
|
||||||
|
private _selectedUnavailableOptions$ = new BehaviorSubject<MultiselectFilterOption[]>([]);
|
||||||
|
|
||||||
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
|
if (changes.availableOptions || changes.selectedOptions) {
|
||||||
|
this._setPendingSelectedOptions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
get selectedUnavailableOptions(): MultiselectFilterOption[] {
|
||||||
|
return this._selectedUnavailableOptions$.getValue();
|
||||||
|
}
|
||||||
|
private get _pendingSelectedOptions(): MultiselectFilterOption[] {
|
||||||
|
return this._pendingSelectedOptions$.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setPendingSelectedOptions(): void {
|
||||||
|
this._pendingSelectedOptions$.next(this.selectedOptions || []);
|
||||||
|
this._selectedUnavailableOptions$.next(
|
||||||
|
this.selectedOptions?.filter(
|
||||||
|
selectedOption => !this.availableOptions.some(availableOption => availableOption.id === selectedOption.id)
|
||||||
|
) || []
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
isSelected(filterOption: MultiselectFilterOption): boolean {
|
||||||
|
return this._pendingSelectedOptions$.value?.some(selectedOption => selectedOption.id === filterOption.id) ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
emitSelectedOptions(): void {
|
||||||
|
this.selectedOptionsChange.emit(this._pendingSelectedOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
setOptionState(filterOption: MultiselectFilterOption, isSelected: boolean): void {
|
||||||
|
if (isSelected) {
|
||||||
|
this._pendingSelectedOptions$.next([...this._pendingSelectedOptions, filterOption]);
|
||||||
|
} else {
|
||||||
|
this._pendingSelectedOptions$.next(
|
||||||
|
this._pendingSelectedOptions.filter(item => item.id !== filterOption.id) ?? []
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<button
|
||||||
|
#multiselectButton
|
||||||
|
[msfaDropdownTriggerFor]="dropdown"
|
||||||
|
[attr.id]="buttonElementId"
|
||||||
|
[attr.aria-controls]="panelId"
|
||||||
|
[ngClass]="{'multiselect__toggle-btn--invalid': isInvalid && showValidation}"
|
||||||
|
type="button"
|
||||||
|
class="multiselect__toggle-btn"
|
||||||
|
>
|
||||||
|
<span class="multiselect__toggle-btn__text">{{multiselectTitle}}</span>
|
||||||
|
<digi-icon-arrow-down
|
||||||
|
aria-hidden="true"
|
||||||
|
class="multiselect__toggle-btn__arrow-down"
|
||||||
|
></digi-icon-arrow-down>
|
||||||
|
<span class="msfa__a11y-sr-only">
|
||||||
|
Filtrera på {{multiselectTitle}}.
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<msfa-dropdown #dropdown [attr.id]="panelId">
|
||||||
|
<msfa-multiselect-panel
|
||||||
|
[availableOptions]="availableOptions"
|
||||||
|
[selectedOptions]="selectedOptions"
|
||||||
|
[dropdownRef]="dropdown"
|
||||||
|
[multiselectTitle]="multiselectTitle"
|
||||||
|
[confirmButtonText]="confirmButtonText"
|
||||||
|
(selectedOptionsChange)="emitSelectedOptions($event)"
|
||||||
|
></msfa-multiselect-panel>
|
||||||
|
</msfa-dropdown>
|
||||||
|
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
@import 'variables/colors';
|
||||||
|
@import 'mixins/list';
|
||||||
|
@import 'variables/gutters';
|
||||||
|
|
||||||
|
|
||||||
|
.multiselect {
|
||||||
|
|
||||||
|
&__toggle-btn {
|
||||||
|
margin-right: var(--digi--layout--gutter);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
appearance: none;
|
||||||
|
width: 100%;
|
||||||
|
height: var(--digi--ui--input--height);
|
||||||
|
padding: var(--digi--ui--input--padding);
|
||||||
|
border: 1px solid var(--digi--ui--input--border--color);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: var(--digi--typography--font-size--m);
|
||||||
|
color: var(--digi--ui--color--background--overlay--opaque);
|
||||||
|
background-color: var(--digi--ui--color--background);
|
||||||
|
border-color: var(--digi-form-select--border-color);
|
||||||
|
|
||||||
|
&: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 {
|
||||||
|
cursor: default;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__toggle-btn__arrow-down {
|
||||||
|
padding: var(--digi--layout--padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,21 +1,20 @@
|
|||||||
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 { MultiselectComponent } from './multiselect.component';
|
||||||
|
|
||||||
import { TemporaryFilterComponent } from './temporary-filter.component';
|
describe('MultiselectComponent', () => {
|
||||||
|
let component: MultiselectComponent;
|
||||||
describe('TemporaryFilterComponent', () => {
|
let fixture: ComponentFixture<MultiselectComponent>;
|
||||||
let component: TemporaryFilterComponent;
|
|
||||||
let fixture: ComponentFixture<TemporaryFilterComponent>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
declarations: [TemporaryFilterComponent],
|
declarations: [MultiselectComponent],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(TemporaryFilterComponent);
|
fixture = TestBed.createComponent(MultiselectComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
ElementRef,
|
||||||
|
EventEmitter,
|
||||||
|
forwardRef,
|
||||||
|
Input,
|
||||||
|
AfterViewInit,
|
||||||
|
Output,
|
||||||
|
Renderer2,
|
||||||
|
ViewChild,
|
||||||
|
ChangeDetectorRef,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||||
|
import { DropdownTriggerForDirective } from '@msfa-shared/components/dropdown/dropdown-trigger-for.directive';
|
||||||
|
import { MultiselectFilterOption } from '@msfa-shared/components/multiselect/multiselect-filter-option';
|
||||||
|
import { uuid } from '@msfa-utils/uuid';
|
||||||
|
|
||||||
|
interface PropagateChangeFn {
|
||||||
|
(_: unknown): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PropagateTouchedFn {
|
||||||
|
(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'msfa-multiselect',
|
||||||
|
templateUrl: './multiselect.component.html',
|
||||||
|
styleUrls: ['./multiselect.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: NG_VALUE_ACCESSOR,
|
||||||
|
useExisting: forwardRef(() => MultiselectComponent),
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class MultiselectComponent implements AfterViewInit, ControlValueAccessor {
|
||||||
|
@ViewChild(DropdownTriggerForDirective) dropdownRef: DropdownTriggerForDirective;
|
||||||
|
panelId = `panel-${uuid()}`;
|
||||||
|
@Input() buttonElementId: string = uuid();
|
||||||
|
@Input() isInvalid = false;
|
||||||
|
@Input() showValidation = false;
|
||||||
|
@Input() confirmButtonText = 'Spara';
|
||||||
|
@Input() description: string;
|
||||||
|
@Input() multiselectTitle: string;
|
||||||
|
@Input() availableOptions: MultiselectFilterOption[];
|
||||||
|
@Input() selectedOptions: MultiselectFilterOption[];
|
||||||
|
@Output() selectedOptionsChange = new EventEmitter<MultiselectFilterOption[]>();
|
||||||
|
@ViewChild('multiselectButton') multiselectButton: ElementRef;
|
||||||
|
isDisabled = false;
|
||||||
|
|
||||||
|
private propagateChange: PropagateChangeFn;
|
||||||
|
private propagateTouched: PropagateTouchedFn;
|
||||||
|
|
||||||
|
constructor(private renderer: Renderer2, private changeDetectionRef: ChangeDetectorRef) {}
|
||||||
|
|
||||||
|
// Allows Angular to update the model.
|
||||||
|
// Update the model and changes needed for the view here.
|
||||||
|
writeValue(selectedOptions: MultiselectFilterOption[]): void {
|
||||||
|
this.selectedOptions = selectedOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allows Angular to register a function to call when the model changes.
|
||||||
|
// Save the function as a property to call later here.
|
||||||
|
registerOnChange(fn: PropagateChangeFn): void {
|
||||||
|
this.propagateChange = fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allows Angular to register a function to call when the input has been touched.
|
||||||
|
// Save the function as a property to call later here.
|
||||||
|
registerOnTouched(fn: PropagateTouchedFn): void {
|
||||||
|
this.propagateTouched = fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allows Angular to disable the input.
|
||||||
|
setDisabledState?(isDisabled: boolean): void {
|
||||||
|
this.renderer.setProperty(this.multiselectButton.nativeElement, 'disabled', isDisabled);
|
||||||
|
this.dropdownRef.closeDropdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
emitSelectedOptions(selectedOptions: MultiselectFilterOption[]): void {
|
||||||
|
// If used with FormsModule/ReactiveFormsModule
|
||||||
|
if (this.propagateChange && this.propagateTouched) {
|
||||||
|
this.propagateChange(selectedOptions);
|
||||||
|
this.propagateTouched();
|
||||||
|
} else {
|
||||||
|
this.selectedOptionsChange.emit(selectedOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dropdownRef.closeDropdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterViewInit(): void {
|
||||||
|
this.dropdownRef.onOpen(() => {
|
||||||
|
// force refresh of selected options if it's reopened
|
||||||
|
this.selectedOptions = [...(this.selectedOptions || [])];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { DigiNgFormCheckboxModule } from '@af/digi-ng/_form/form-checkbox';
|
||||||
|
import { A11yModule } from '@angular/cdk/a11y';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { DropdownModule } from '@msfa-shared/components/dropdown/dropdown.module';
|
||||||
|
import { MultiselectComponent } from '@msfa-shared/components/multiselect/multiselect.component';
|
||||||
|
import { MultiselectPanelComponent } from './multiselect-panel/multiselect-panel.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [MultiselectComponent, MultiselectPanelComponent],
|
||||||
|
imports: [CommonModule, DropdownModule, A11yModule, DigiNgFormCheckboxModule, FormsModule],
|
||||||
|
exports: [MultiselectComponent],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||||
|
})
|
||||||
|
export class MultiselectModule {}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { ExpandedTreeNodeComponent } from './expanded-tree-node.component';
|
import { ExpandedTreeNodeComponent } from './expanded-tree-node.component';
|
||||||
|
|
||||||
describe('ExpandedTreeNodeComponent', () => {
|
describe('ExpandedTreeNodeComponent', () => {
|
||||||
@@ -8,9 +8,9 @@ describe('ExpandedTreeNodeComponent', () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
declarations: [ ExpandedTreeNodeComponent ]
|
declarations: [ExpandedTreeNodeComponent],
|
||||||
})
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||||
.compileComponents();
|
}).compileComponents();
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
ViewChild,
|
ViewChild,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||||
import { UUID } from 'angular2-uuid';
|
import { uuid } from '@msfa-utils/uuid';
|
||||||
import { TreeNode, TreeNodeModel, TreeNodesSelectorService } from '../../services/tree-nodes-selector.service';
|
import { TreeNode, TreeNodeModel, TreeNodesSelectorService } from '../../services/tree-nodes-selector.service';
|
||||||
|
|
||||||
interface PropagateChangeFn {
|
interface PropagateChangeFn {
|
||||||
@@ -36,7 +36,7 @@ interface PropagateTouchedFn {
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class TreeNodesSelectorComponent implements ControlValueAccessor {
|
export class TreeNodesSelectorComponent implements ControlValueAccessor {
|
||||||
@Input() buttonElementId: string = UUID.UUID();
|
@Input() buttonElementId: string = uuid();
|
||||||
@Input() headingText: string;
|
@Input() headingText: string;
|
||||||
@Input() confirmationButtonText = 'Spara';
|
@Input() confirmationButtonText = 'Spara';
|
||||||
@Input() isInvalid = false;
|
@Input() isInvalid = false;
|
||||||
@@ -51,7 +51,7 @@ export class TreeNodesSelectorComponent implements ControlValueAccessor {
|
|||||||
selectionLabel: string | null = null;
|
selectionLabel: string | null = null;
|
||||||
isDisabled = false;
|
isDisabled = false;
|
||||||
|
|
||||||
panelId = `panel-${UUID.UUID()}`;
|
panelId = `panel-${uuid()}`;
|
||||||
|
|
||||||
private propagateChange: PropagateChangeFn;
|
private propagateChange: PropagateChangeFn;
|
||||||
private propagateTouched: PropagateTouchedFn;
|
private propagateTouched: PropagateTouchedFn;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { UUID } from 'angular2-uuid';
|
import { uuid } from '@msfa-utils/uuid';
|
||||||
|
|
||||||
export interface TreeNode {
|
export interface TreeNode {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -43,7 +43,7 @@ export class TreeNodesSelectorService {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...treeNode,
|
...treeNode,
|
||||||
uuid: UUID.UUID(),
|
uuid: uuid(),
|
||||||
hasFocus: false,
|
hasFocus: false,
|
||||||
children,
|
children,
|
||||||
hideTreeNode: false,
|
hideTreeNode: false,
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { AfterViewInit, Directive, ElementRef, Input } from '@angular/core';
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: '[msfaAutofocus]',
|
||||||
|
})
|
||||||
|
export class AutofocusDirective implements AfterViewInit {
|
||||||
|
@Input() msfaAutofocus: boolean;
|
||||||
|
|
||||||
|
constructor(private elementRef: ElementRef) {}
|
||||||
|
|
||||||
|
ngAfterViewInit(): void {
|
||||||
|
if (this.msfaAutofocus) {
|
||||||
|
(this.elementRef.nativeElement as HTMLElement).focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { AutofocusDirective } from './autofocus.directive';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [AutofocusDirective],
|
||||||
|
imports: [CommonModule],
|
||||||
|
exports: [AutofocusDirective],
|
||||||
|
})
|
||||||
|
export class AutofocusModule {}
|
||||||
@@ -2,8 +2,8 @@ import { Injectable } from '@angular/core';
|
|||||||
import { Params } from '@msfa-models/api/params.model';
|
import { Params } from '@msfa-models/api/params.model';
|
||||||
import { Avrop, AvropCompactData } from '@msfa-models/avrop.model';
|
import { Avrop, AvropCompactData } from '@msfa-models/avrop.model';
|
||||||
import { Handledare } from '@msfa-models/handledare.model';
|
import { Handledare } from '@msfa-models/handledare.model';
|
||||||
import { MultiselectFilterOption } from '@msfa-models/multiselect-filter-option';
|
|
||||||
import { AvropApiService } from '@msfa-services/api/avrop-api.service';
|
import { AvropApiService } from '@msfa-services/api/avrop-api.service';
|
||||||
|
import { MultiselectFilterOption } from '@msfa-shared/components/multiselect/multiselect-filter-option';
|
||||||
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
|
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
|
||||||
import { filter, map, switchMap } from 'rxjs/operators';
|
import { filter, map, switchMap } from 'rxjs/operators';
|
||||||
|
|
||||||
@@ -50,24 +50,24 @@ export class AvropService {
|
|||||||
this._filteredKommuner$,
|
this._filteredKommuner$,
|
||||||
this._page$,
|
this._page$,
|
||||||
this._limit$,
|
this._limit$,
|
||||||
]).pipe(switchMap(() => this.avropApiService.fetchAvrop$(this._getParams('avrop'))));
|
]).pipe(switchMap(() => this.avropApiService.fetchAvrop$(this.filtersForAvrop)));
|
||||||
|
|
||||||
public availableTjanster$: Observable<MultiselectFilterOption[]> = combineLatest([
|
public availableTjanster$: Observable<MultiselectFilterOption[]> = combineLatest([
|
||||||
this._filteredUtforandeVerksamheter$,
|
this._filteredUtforandeVerksamheter$,
|
||||||
this._filteredKommuner$,
|
this._filteredKommuner$,
|
||||||
]).pipe(switchMap(() => this.avropApiService.fetchAvailableTjanster$(this._getParams('tjanster'))));
|
]).pipe(switchMap(() => this.avropApiService.fetchAvailableTjanster$(this.filtersForTjanster)));
|
||||||
|
|
||||||
public availableUtforandeVerksamheter$: Observable<MultiselectFilterOption[]> = combineLatest([
|
public availableUtforandeVerksamheter$: Observable<MultiselectFilterOption[]> = combineLatest([
|
||||||
this._filteredTjanster$,
|
this._filteredTjanster$,
|
||||||
this._filteredKommuner$,
|
this._filteredKommuner$,
|
||||||
]).pipe(
|
]).pipe(
|
||||||
switchMap(() => this.avropApiService.fetchAvailableUtforandeVerksamheter$(this._getParams('utforandeVerksamheter')))
|
switchMap(() => this.avropApiService.fetchAvailableUtforandeVerksamheter$(this.filtersForUtforandeVerksamheter))
|
||||||
);
|
);
|
||||||
|
|
||||||
public availableKommuner$: Observable<MultiselectFilterOption[]> = combineLatest([
|
public availableKommuner$: Observable<MultiselectFilterOption[]> = combineLatest([
|
||||||
this._filteredTjanster$,
|
this._filteredTjanster$,
|
||||||
this._filteredUtforandeVerksamheter$,
|
this._filteredUtforandeVerksamheter$,
|
||||||
]).pipe(switchMap(() => this.avropApiService.fetchAvailableKommuner$(this._getParams('kommuner'))));
|
]).pipe(switchMap(() => this.avropApiService.fetchAvailableKommuner$(this.filtersForKommuner)));
|
||||||
|
|
||||||
private _lockedAvrop$: Observable<Avrop[]> = combineLatest([this.selectedAvrop$, this.avropIsLocked$]).pipe(
|
private _lockedAvrop$: Observable<Avrop[]> = combineLatest([this.selectedAvrop$, this.avropIsLocked$]).pipe(
|
||||||
map(([selectedAvrop, isLocked]) => (isLocked ? selectedAvrop : null))
|
map(([selectedAvrop, isLocked]) => (isLocked ? selectedAvrop : null))
|
||||||
@@ -102,48 +102,57 @@ export class AvropService {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getParams(type: 'avrop' | 'kommuner' | 'tjanster' | 'utforandeVerksamheter'): Params {
|
private get tjanstKoder(): { tjanstKoder: string[] } | null {
|
||||||
const tjanstKoder = this._filteredTjanster$.getValue()?.length
|
return this._filteredTjanster$.value?.length
|
||||||
? { tjanstKoder: this._filteredTjanster$.getValue().map(tjanst => tjanst.id) }
|
? { tjanstKoder: this._filteredTjanster$.value.map(tjanst => tjanst.id) }
|
||||||
: null;
|
: null;
|
||||||
const kommunKoder = this._filteredKommuner$.getValue()?.length
|
}
|
||||||
? { kommunKoder: this._filteredKommuner$.getValue().map(kommun => kommun.id) }
|
|
||||||
|
private get kommunKoder(): { kommunKoder: string[] } | null {
|
||||||
|
return this._filteredKommuner$.value?.length
|
||||||
|
? { kommunKoder: this._filteredKommuner$.value.map(kommun => kommun.id) }
|
||||||
: null;
|
: null;
|
||||||
const utforandeverksamhetIds = this._filteredUtforandeVerksamheter$.getValue()?.length
|
}
|
||||||
|
|
||||||
|
private get utforandeverksamhetIds(): { utforandeverksamhetIds: string[] } | null {
|
||||||
|
return this._filteredUtforandeVerksamheter$.value?.length
|
||||||
? {
|
? {
|
||||||
utforandeverksamhetIds: this._filteredUtforandeVerksamheter$
|
utforandeverksamhetIds: this._filteredUtforandeVerksamheter$.value.map(
|
||||||
.getValue()
|
utforandeVerksamhet => utforandeVerksamhet.id
|
||||||
.map(utforandeVerksamhet => utforandeVerksamhet.id),
|
),
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
switch (type) {
|
private get filtersForAvrop(): Params {
|
||||||
case 'avrop':
|
return {
|
||||||
return {
|
...this.tjanstKoder,
|
||||||
...tjanstKoder,
|
...this.kommunKoder,
|
||||||
...kommunKoder,
|
...this.utforandeverksamhetIds,
|
||||||
...utforandeverksamhetIds,
|
page: this._page$.value.toString(),
|
||||||
page: this._page$.getValue().toString(),
|
limit: this._limit$.value.toString(),
|
||||||
limit: this._limit$.getValue().toString(),
|
};
|
||||||
};
|
}
|
||||||
case 'kommuner':
|
|
||||||
return {
|
private get filtersForTjanster(): Params {
|
||||||
...tjanstKoder,
|
return {
|
||||||
...utforandeverksamhetIds,
|
...this.kommunKoder,
|
||||||
};
|
...this.utforandeverksamhetIds,
|
||||||
case 'tjanster':
|
};
|
||||||
return {
|
}
|
||||||
...kommunKoder,
|
|
||||||
...utforandeverksamhetIds,
|
private get filtersForUtforandeVerksamheter(): Params {
|
||||||
};
|
return {
|
||||||
case 'utforandeVerksamheter':
|
...this.tjanstKoder,
|
||||||
return {
|
...this.kommunKoder,
|
||||||
...tjanstKoder,
|
};
|
||||||
...kommunKoder,
|
}
|
||||||
};
|
|
||||||
default:
|
private get filtersForKommuner(): Params {
|
||||||
return {};
|
return {
|
||||||
}
|
...this.tjanstKoder,
|
||||||
|
...this.utforandeverksamhetIds,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public setSelectedAvrop(deltagare: Avrop[]): void {
|
public setSelectedAvrop(deltagare: Avrop[]): void {
|
||||||
|
|||||||
8
apps/mina-sidor-fa/src/app/shared/utils/uuid.ts
Normal file
8
apps/mina-sidor-fa/src/app/shared/utils/uuid.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// From https://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid
|
||||||
|
export function uuid(): string {
|
||||||
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||||
|
const r = (Math.random() * 16) | 0,
|
||||||
|
v = c == 'x' ? r : (r & 0x3) | 0x8;
|
||||||
|
return v.toString(16);
|
||||||
|
});
|
||||||
|
}
|
||||||
25
apps/mina-sidor-fa/src/styles/mixins/_form.scss
Normal file
25
apps/mina-sidor-fa/src/styles/mixins/_form.scss
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
@import 'variables/colors';
|
||||||
|
@import 'variables/gutters';
|
||||||
|
|
||||||
|
|
||||||
|
@mixin msfa__fieldset($legendAsHeading: true) {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border-width: 0;
|
||||||
|
|
||||||
|
legend {
|
||||||
|
position: relative;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
@if $legendAsHeading {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-bottom: var(--digi--layout--gutter--xs);
|
||||||
|
margin: 0;
|
||||||
|
font-weight: var(--digi--typography--font-weight--bold);
|
||||||
|
font-size: var(--digi--typography--font-size--s);
|
||||||
|
border-bottom: 1px solid var(--digi--ui--color--background--off);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
@import '@digi/core/dist/digi/digi.css';
|
@import '@digi/core/dist/digi/digi.css';
|
||||||
|
@import '~@angular/cdk/overlay-prebuilt.css';
|
||||||
@import 'mixins/a11y';
|
@import 'mixins/a11y';
|
||||||
@import 'mixins/backdrop';
|
@import 'mixins/backdrop';
|
||||||
@import 'mixins/icon';
|
@import 'mixins/icon';
|
||||||
|
|||||||
17
package-lock.json
generated
17
package-lock.json
generated
@@ -1,12 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "mina-sidor-fa-web",
|
"name": "mina-sidor-fa-web",
|
||||||
"version": "1.5.0",
|
"version": "1.5.1",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mina-sidor-fa-web",
|
"version": "1.5.1",
|
||||||
"version": "1.5.0",
|
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -25,7 +24,6 @@
|
|||||||
"@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",
|
||||||
@@ -4952,12 +4950,6 @@
|
|||||||
"integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=",
|
"integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/angular2-uuid": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "http://nexus.arbetsformedlingen.se/repository/npm/angular2-uuid/-/angular2-uuid-1.1.1.tgz",
|
|
||||||
"integrity": "sha1-cvA81TK39AAy6x7PufhFc4S+lW4=",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/ansi-colors": {
|
"node_modules/ansi-colors": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "http://nexus.arbetsformedlingen.se/repository/npm/ansi-colors/-/ansi-colors-4.1.1.tgz",
|
"resolved": "http://nexus.arbetsformedlingen.se/repository/npm/ansi-colors/-/ansi-colors-4.1.1.tgz",
|
||||||
@@ -36731,11 +36723,6 @@
|
|||||||
"integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=",
|
"integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"angular2-uuid": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "http://nexus.arbetsformedlingen.se/repository/npm/angular2-uuid/-/angular2-uuid-1.1.1.tgz",
|
|
||||||
"integrity": "sha1-cvA81TK39AAy6x7PufhFc4S+lW4="
|
|
||||||
},
|
|
||||||
"ansi-colors": {
|
"ansi-colors": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "http://nexus.arbetsformedlingen.se/repository/npm/ansi-colors/-/ansi-colors-4.1.1.tgz",
|
"resolved": "http://nexus.arbetsformedlingen.se/repository/npm/ansi-colors/-/ansi-colors-4.1.1.tgz",
|
||||||
|
|||||||
@@ -59,7 +59,6 @@
|
|||||||
"@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