Mykyta Maliarchuk e62c0587b6
[ACS-9215] Update license headers (#10625)
* [ACS-9215] Update license headers

* [ACS-9215] Update license headers
2025-02-06 13:18:56 +01:00

502 lines
16 KiB
TypeScript

/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
Component,
DestroyRef,
ElementRef,
EventEmitter,
inject,
Input,
OnChanges,
OnInit,
Output,
SimpleChanges,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import { ReactiveFormsModule, UntypedFormControl } from '@angular/forms';
import { animate, state, style, transition, trigger } from '@angular/animations';
import { BehaviorSubject, Observable } from 'rxjs';
import { debounceTime, distinctUntilChanged, filter, mergeMap, switchMap, tap } from 'rxjs/operators';
import { ComponentSelectionMode } from '../../types';
import { IdentityGroupModel } from '../models/identity-group.model';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { CommonModule } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { MatIconModule } from '@angular/material/icon';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatSelectModule } from '@angular/material/select';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button';
import { MatInputModule } from '@angular/material/input';
import { MatChipsModule } from '@angular/material/chips';
import { IdentityGroupService } from '../services/identity-group.service';
@Component({
selector: 'adf-cloud-group',
standalone: true,
imports: [
CommonModule,
TranslateModule,
MatIconModule,
MatFormFieldModule,
MatProgressBarModule,
MatSelectModule,
MatAutocompleteModule,
MatButtonModule,
ReactiveFormsModule,
MatInputModule,
MatChipsModule
],
templateUrl: './group-cloud.component.html',
styleUrls: ['./group-cloud.component.scss'],
animations: [
trigger('transitionMessages', [
state('enter', style({ opacity: 1, transform: 'translateY(0%)' })),
transition('void => enter', [style({ opacity: 0, transform: 'translateY(-100%)' }), animate('300ms cubic-bezier(0.55, 0, 0.55, 0.2)')])
])
],
encapsulation: ViewEncapsulation.None
})
export class GroupCloudComponent implements OnInit, OnChanges {
/** Name of the application. If specified this shows the groups who have access to the app. */
@Input()
appName: string;
/** Title of the field */
@Input()
title: string;
/** Group selection mode (single/multiple). */
@Input()
mode: ComponentSelectionMode = 'single';
/** Array of groups to be pre-selected. This pre-selects all groups in multi selection mode and only the first group of the array in single selection mode. */
@Input()
preSelectGroups: IdentityGroupModel[] = [];
/**
* This flag enables the validation on the preSelectGroups passed as input.
* In case the flag is true the components call the identity service to verify the validity of the information passed as input.
* Otherwise, no check will be done.
*/
@Input()
validate = false;
/**
* Show the info in readonly mode
*/
@Input()
readOnly = false;
/**
* Mark this field as required
*/
@Input()
required = false;
/** FormControl to list of group */
@Input()
groupChipsCtrl = new UntypedFormControl({ value: '', disabled: false });
/** FormControl to search the group */
@Input()
searchGroupsControl = new UntypedFormControl({ value: '', disabled: false });
/** Role names of the groups to be listed. */
@Input()
roles: string[] = [];
/** Emitted when a group is selected. */
@Output()
selectGroup = new EventEmitter<IdentityGroupModel>();
/** Emitted when a group is removed. */
@Output()
removeGroup = new EventEmitter<IdentityGroupModel>();
/** Emitted when a group selection change. */
@Output()
changedGroups = new EventEmitter<IdentityGroupModel[]>();
/** Emitted when an warning occurs. */
@Output()
warning = new EventEmitter<any>();
@ViewChild('groupInput')
private groupInput: ElementRef<HTMLInputElement>;
private searchGroups: IdentityGroupModel[] = [];
selectedGroups: IdentityGroupModel[] = [];
invalidGroups: IdentityGroupModel[] = [];
searchGroups$ = new BehaviorSubject<IdentityGroupModel[]>(this.searchGroups);
subscriptAnimationState: string = 'enter';
isFocused: boolean;
touched: boolean = false;
validateGroupsMessage: string;
searchedValue = '';
validationLoading = false;
searchLoading = false;
typingUniqueValueNotEmpty$: Observable<any>;
private readonly destroyRef = inject(DestroyRef);
constructor(private identityGroupService: IdentityGroupService) {}
ngOnInit(): void {
this.initSearch();
}
ngOnChanges(changes: SimpleChanges): void {
if (this.hasPreselectedGroupsChanged(changes) || this.hasModeChanged(changes) || this.isValidationChanged(changes)) {
if (this.hasPreSelectGroups()) {
this.loadPreSelectGroups();
} else if (this.hasPreselectedGroupsCleared(changes)) {
this.selectedGroups = [];
this.invalidGroups = [];
}
if (!this.isValidationEnabled()) {
this.invalidGroups = [];
}
}
}
private initSearch(): void {
this.initializeStream();
this.typingUniqueValueNotEmpty$
.pipe(
switchMap((name: string) => this.identityGroupService.search(name, { roles: this.roles, withinApplication: this.appName })),
mergeMap((groups: IdentityGroupModel[]) => {
this.resetSearchGroups();
this.searchLoading = false;
return groups;
}),
filter((group) => !this.isGroupAlreadySelected(group)),
takeUntilDestroyed(this.destroyRef)
)
.subscribe((searchedGroup: IdentityGroupModel) => {
this.searchGroups.push(searchedGroup);
this.searchGroups$.next(this.searchGroups);
});
}
private initializeStream() {
const typingValueFromControl$ = this.searchGroupsControl.valueChanges;
const typingValueTypeSting$ = typingValueFromControl$.pipe(
filter((value) => {
this.searchLoading = true;
return typeof value === 'string';
})
);
const typingValueHandleErrorMessage$ = typingValueTypeSting$.pipe(
tap((value: string) => {
if (value) {
this.setTypingError();
}
})
);
const typingValueDebouncedUnique$ = typingValueHandleErrorMessage$.pipe(debounceTime(500), distinctUntilChanged());
this.typingUniqueValueNotEmpty$ = typingValueDebouncedUnique$.pipe(
tap((value: string) => {
if (value.trim()) {
this.searchedValue = value;
} else {
this.searchGroupsControl.markAsPristine();
this.searchGroupsControl.markAsUntouched();
}
}),
tap(() => this.resetSearchGroups())
);
}
private isGroupAlreadySelected(group: IdentityGroupModel): boolean {
if (this.selectedGroups && this.selectedGroups.length > 0) {
const result = this.selectedGroups.find((selectedGroup: IdentityGroupModel) => selectedGroup.name === group.name);
return !!result;
}
return false;
}
private async searchGroup(name: string): Promise<IdentityGroupModel> {
return (await this.identityGroupService.search(name).toPromise())[0];
}
private getPreselectedGroups(): IdentityGroupModel[] {
if (this.isSingleMode()) {
return [this.preSelectGroups[0]];
} else {
return this.removeDuplicatedGroups(this.preSelectGroups);
}
}
private async validatePreselectGroups(): Promise<any> {
this.invalidGroups = [];
for (const group of this.getPreselectedGroups()) {
try {
const validationResult = await this.searchGroup(group.name);
if (this.isPreselectedGroupInvalid(group, validationResult)) {
this.invalidGroups.push(group);
}
} catch (error) {
this.invalidGroups.push(group);
}
}
this.checkPreselectValidationErrors();
}
private checkPreselectValidationErrors(): void {
this.invalidGroups = this.removeDuplicatedGroups(this.invalidGroups);
if (this.invalidGroups.length > 0) {
this.generateInvalidGroupsMessage();
}
this.warning.emit({
message: 'INVALID_PRESELECTED_GROUPS',
groups: this.invalidGroups
});
}
private generateInvalidGroupsMessage(): void {
this.validateGroupsMessage = '';
this.invalidGroups.forEach((invalidGroup: IdentityGroupModel, index) => {
if (index === this.invalidGroups.length - 1) {
this.validateGroupsMessage += `${invalidGroup.name} `;
} else {
this.validateGroupsMessage += `${invalidGroup.name}, `;
}
});
}
private async loadPreSelectGroups(): Promise<void> {
this.selectedGroups = [];
if (this.isSingleMode()) {
this.selectedGroups = [this.preSelectGroups[0]];
} else {
this.selectedGroups = this.removeDuplicatedGroups(this.preSelectGroups);
}
this.groupChipsCtrl.setValue(this.selectedGroups[0].name);
if (this.isValidationEnabled()) {
this.validationLoading = true;
await this.validatePreselectGroups();
this.validationLoading = false;
}
}
onSelect(group: IdentityGroupModel): void {
if (group) {
this.selectGroup.emit(group);
if (this.isMultipleMode()) {
if (!this.isGroupAlreadySelected(group)) {
this.selectedGroups.push(group);
}
} else {
this.invalidGroups = [];
this.selectedGroups = [group];
}
this.groupInput.nativeElement.value = '';
this.searchGroupsControl.setValue('');
this.groupChipsCtrlValue(this.selectedGroups[0].name);
this.changedGroups.emit(this.selectedGroups);
this.resetSearchGroups();
}
}
onRemove(groupToRemove: IdentityGroupModel): void {
this.removeGroup.emit(groupToRemove);
this.removeGroupFromSelected(groupToRemove);
this.changedGroups.emit(this.selectedGroups);
if (this.selectedGroups.length === 0) {
this.groupChipsCtrlValue('');
} else {
this.groupChipsCtrlValue(this.selectedGroups[0].name);
}
this.searchGroupsControl.markAsDirty();
this.searchGroupsControl.markAsTouched();
if (this.isValidationEnabled()) {
this.removeGroupFromValidation(groupToRemove);
this.checkPreselectValidationErrors();
}
}
private isPreselectedGroupInvalid(preselectedGroup: IdentityGroupModel, validatedGroup: IdentityGroupModel): boolean {
if (validatedGroup?.name !== undefined) {
return preselectedGroup.name !== validatedGroup.name;
} else {
return true;
}
}
removeDuplicatedGroups(groups: IdentityGroupModel[]): IdentityGroupModel[] {
return groups.filter(
(group, index, self) => index === self.findIndex((auxGroup) => group.id === auxGroup.id && group.name === auxGroup.name)
);
}
private groupChipsCtrlValue(value: string) {
this.groupChipsCtrl.setValue(value);
this.groupChipsCtrl.markAsDirty();
this.groupChipsCtrl.markAsTouched();
}
private removeGroupFromSelected({ id, name }: IdentityGroupModel): void {
const indexToRemove = this.selectedGroups.findIndex((group) => group.id === id && group.name === name);
if (indexToRemove !== -1) {
this.selectedGroups.splice(indexToRemove, 1);
}
}
private removeGroupFromValidation({ id, name }: IdentityGroupModel): void {
const indexToRemove = this.invalidGroups.findIndex((group) => group.id === id && group.name === name);
if (indexToRemove !== -1) {
this.invalidGroups.splice(indexToRemove, 1);
}
}
private resetSearchGroups(): void {
this.searchGroups = [];
this.searchGroups$.next(this.searchGroups);
}
private isSingleSelectionReadonly(): boolean {
return this.isSingleMode() && this.selectedGroups.length === 1 && this.selectedGroups[0].readonly === true;
}
private isSingleMode(): boolean {
return this.mode === 'single';
}
private isMultipleMode(): boolean {
return this.mode === 'multiple';
}
private hasPreSelectGroups(): boolean {
return this.preSelectGroups && this.preSelectGroups.length > 0;
}
private hasModeChanged(changes: SimpleChanges): boolean {
return changes?.mode && changes.mode.currentValue !== changes.mode.previousValue;
}
private isValidationChanged(changes: SimpleChanges): boolean {
return changes?.validate && changes.validate.currentValue !== changes.validate.previousValue;
}
private hasPreselectedGroupsChanged(changes: SimpleChanges): boolean {
return changes?.preSelectGroups && changes.preSelectGroups.currentValue !== changes.preSelectGroups.previousValue;
}
private hasPreselectedGroupsCleared(changes: SimpleChanges): boolean {
return changes?.preSelectGroups?.currentValue.length === 0;
}
private setTypingError(): void {
this.searchGroupsControl.setErrors({
searchTypingError: true,
...this.searchGroupsControl.errors
});
}
hasPreselectError(): boolean {
return this.invalidGroups && this.invalidGroups.length > 0;
}
isReadonly(): boolean {
return this.readOnly || this.isSingleSelectionReadonly();
}
getDisplayName(group: IdentityGroupModel): string {
return group?.name || '';
}
hasError(): boolean {
return !!this.searchGroupsControl.errors;
}
isValidationLoading(): boolean {
return this.isValidationEnabled() && this.validationLoading;
}
markAsTouched(): void {
this.touched = true;
}
isTouched(): boolean {
return this.touched;
}
isSelected(): boolean {
return this.selectedGroups && !!this.selectedGroups.length;
}
isDirty(): boolean {
return this.isTouched() && !this.isSelected();
}
setFocus(isFocused: boolean) {
this.isFocused = isFocused;
}
isValidationEnabled(): boolean {
return this.validate === true;
}
getValidationPattern(): string {
return this.searchGroupsControl.errors.pattern.requiredPattern;
}
getValidationMaxLength(): string {
return this.searchGroupsControl.errors.maxlength.requiredLength;
}
getValidationMinLength(): string {
return this.searchGroupsControl.errors.minlength.requiredLength;
}
getGroupNameInitials(group: IdentityGroupModel): string {
let result = '';
if (group) {
const groupName = group.name;
result = (groupName ? groupName[0] : '').toUpperCase();
}
return result;
}
}