@linid-dm/directory-manager-client-core
Version:
Core package by providing a set of angular components for the Directory Manager app.
768 lines • 240 kB
JavaScript
import { __decorate } from "tslib";
/**
* Copyright (C) 2020-2024 Linagora
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version, provided you comply with the Additional Terms applicable for
* LinID Directory Manager software by LINAGORA pursuant to Section 7 of the GNU
* Affero General Public License, subsections (b), (c), and (e), pursuant to
* which these Appropriate Legal Notices must notably (i) retain the display of
* the "LinID™" trademark/logo at the top of the interface window, the display
* of the “You are using the Open Source and free version of LinID™, powered by
* Linagora © 2009–2013. Contribute to LinID R&D by subscribing to an Enterprise
* offer!” infobox and in the e-mails sent with the Program, notice appended to
* any type of outbound messages (e.g. e-mail and meeting requests) as well as
* in the LinID Directory Manager user interface, (ii) retain all hypertext
* links between LinID Directory Manager and https://linid.org/, as well as
* between LINAGORA and LINAGORA.com, and (iii) refrain from infringing LINAGORA
* intellectual property rights over its trademarks and commercial brands. Other
* Additional Terms apply, see <http://www.linagora.com/licenses/> for more
* details.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License and
* its applicable Additional Terms for LinID Directory Manager along with this
* program. If not, see <http://www.gnu.org/licenses/> for the GNU Affero
* General Public License version 3 and <http://www.linagora.com/licenses/> for
* the Additional Terms applicable to the LinID Directory Manager software.
*/
import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { DOCUMENT } from '@angular/common';
import { Component, EventEmitter, Inject, Input, Output, ViewChild, ViewChildren, } from '@angular/core';
import { FormControl, FormGroup, Validators, } from '@angular/forms';
import { MatChipGrid } from '@angular/material/chips';
import { ErrorStateMatcher } from '@angular/material/core';
import { Select } from '@ngxs/store';
import _ from 'lodash';
import moment from 'moment';
import { BehaviorSubject, Subject, catchError, debounceTime, distinctUntilChanged, filter, map, of, pairwise, scan, switchMap, takeUntil, tap, } from 'rxjs';
import { CustomValidators, DataState, EComponentName, EUserAction, Error as ErrorActions, FormErrorStateMatcher, UiState, User, UserState, concatOldValuesToNewValue, convertAutocompleteInputValueForRequestBody, convertBooleanToString, convertInputValueToAutocompleteOption, convertToScimObject, formatDatepickerForm, getAuthorWithDate, getFilteredOptionsOnInitialState, getFormAutocompleteFieldInitialValue, getFormErrors, getLinkedFieldsValues, getOptionsFilteringCondition, getResponseAttributesIds, inputValueIsNotAnOption, inputValueIsNotAnOptionSimpleAutocomplete, isAutocompleteInitialState, isFormFieldDisabled, isFormFieldDisplayed, keepFieldInRequestBody, resizeImage, saveCheckoutFormState, setFormControlValue, trimFormFieldsValues, } from '../../shared';
import * as i0 from "@angular/core";
import * as i1 from "@ngxs/store";
import * as i2 from "../../shared";
import * as i3 from "@angular/router";
import * as i4 from "@angular/common";
import * as i5 from "@angular/forms";
import * as i6 from "@angular/flex-layout/flex";
import * as i7 from "@angular/flex-layout/extended";
import * as i8 from "@angular/material/autocomplete";
import * as i9 from "@angular/material/core";
import * as i10 from "@angular/material/button";
import * as i11 from "@angular/material/chips";
import * as i12 from "@angular/material/datepicker";
import * as i13 from "@angular/material/form-field";
import * as i14 from "@angular/material/icon";
import * as i15 from "@angular/material/input";
import * as i16 from "@angular/cdk/text-field";
import * as i17 from "@angular/material/progress-spinner";
import * as i18 from "@angular/material/select";
import * as i19 from "@angular/material/slide-toggle";
import * as i20 from "ngx-material-file-input";
import * as i21 from "../../shared/directives/infinite-scroll.directive";
import * as i22 from "../../shared/directives/unfocusable.directive";
import * as i23 from "../../shared/directives/omit-chars.directive";
/**
* This class is a generic and configurable table to be used for every data table.
*/
export class GenericFormComponent {
set enableCheckoutForm(isEnable) {
this._enableCheckoutForm = isEnable;
if (!!this.checkoutForm) {
if (isEnable) {
this.checkoutForm.enable();
}
else {
this.checkoutForm.disable();
}
}
}
get enableCheckoutForm() {
return this._enableCheckoutForm;
}
constructor(_document, _store$, _dataService, _interactionsService, _errorsHandlerService, _envService, _router, _formService) {
this._document = _document;
this._store$ = _store$;
this._dataService = _dataService;
this._interactionsService = _interactionsService;
this._errorsHandlerService = _errorsHandlerService;
this._envService = _envService;
this._router = _router;
this._formService = _formService;
this._onDestroy$ = new Subject();
this._requiredUndisplayedFields = [];
this.autocompleteOff = false;
this.isDisplayingDefaultFormBtns = false;
this.authorizeDisableOnRequest = true;
this.isInModal = false;
this.clickOnCancelBtn = new EventEmitter();
this.clickOnSubmitBtn = new EventEmitter();
this.addSucceeded = new EventEmitter();
this.addFailed = new EventEmitter();
this.updateSucceeded = new EventEmitter();
this.updateFailed = new EventEmitter();
this.submitForm = new EventEmitter();
this.templateFormFields = [];
this.formPreviousState = {};
this.separatorKeysCodes = [ENTER, COMMA];
this.formInitialState = {};
this.limit = {};
this.offset = {};
this.options = {};
this.options$ = {};
this.autoCompleteOptions = {};
this.filteredOptions = {};
this.autocompleteOptionsLoaded = {};
this.imageBase64 = {};
this.imgAspectRatio = {};
this.componentNameType = EComponentName;
this.areAutocompletesPanelsOpened = {};
}
/**
* ngOnInit generate FormControl for every attribute in the form
*/
ngOnInit() {
this._selectedEntryExternalId = this._store$.selectSnapshot(DataState.getSelectedEntryExternalId);
this._userDisplayName = this._store$.selectSnapshot(UserState.getDisplayName);
this.processingRequest$ = this._interactionsService.processingRequest$;
this.templateFormFields = this.formData.reduce((acc, field) => {
if (isFormFieldDisplayed(field, this.crudAction)) {
acc.push(field);
}
else if (field.props.isSentOnUpdate) {
this._requiredUndisplayedFields.push(field);
}
return acc;
}, []);
this._formDataMap = this.templateFormFields.reduce((acc, curr) => ({
...acc,
[curr.modelRef]: curr.value.field.componentName,
}), {});
this.nbFormFields = this.templateFormFields.filter((field) => isFormFieldDisplayed(field, this.crudAction)).length;
const imgFiles = this.templateFormFields.filter((formElt) => formElt.value.field.componentName === EComponentName.imgInput);
if (imgFiles.length > 0) {
imgFiles.forEach((imgFile) => {
this.setImgAspectRatio(imgFile.dataValue, imgFile.modelRef);
this.imageBase64 = {
...this.imageBase64,
[imgFile.modelRef]: imgFile.dataValue !== null ? imgFile.dataValue : null,
};
});
}
this.datepickerPlaceholder =
'Ex: ' +
moment().endOf('year').format(this._envService.ui.displayDateFormat);
this.templateFormFields
.filter((formElt) => formElt.value.field.componentName === EComponentName.slideToggle)
.forEach((formField) => (formField.dataValue = formField.dataValue ?? false));
const formContainsAutocomplete = this.templateFormFields.some((formElt) => formElt.value.field.componentName.includes(EComponentName.autocomplete));
if (formContainsAutocomplete) {
const allAutocompleteFields = this.templateFormFields.filter((formElt) => formElt.value.field.componentName.includes(EComponentName.autocomplete));
this.initAutocompleteProperties(allAutocompleteFields);
allAutocompleteFields.forEach((autocompleteField) => {
const resourceTypeId = autocompleteField.getValuesParams.requestResourceTypeId;
const responseAttributesIds = getResponseAttributesIds(null, resourceTypeId, autocompleteField);
return this._dataService
.getDataList(autocompleteField.getValuesParams.requestTarget, {
attributes: responseAttributesIds,
}, true)
.pipe(takeUntil(this._onDestroy$), map((dataList) => dataList.Resources), tap((dataResources) => this.setAutocompleteOptions(autocompleteField, dataResources, resourceTypeId)))
.subscribe();
});
}
this.initCheckoutForm();
}
ngOnDestroy() {
this._onDestroy$.next();
this._onDestroy$.complete();
}
ngAfterViewInit() {
this.customFormContentTemplate =
this._formService.getCustomFormContentTemplate(this);
}
initCheckoutForm() {
this.checkoutForm = new FormGroup(this.templateFormFields.reduce((acc, data) => {
const currentFormState = {
value: setFormControlValue(data),
disabled: data.value.field.componentName.includes(EComponentName.autocomplete)
? !this.autocompleteOptionsLoaded[data.modelRef]
: isFormFieldDisabled(data, this.crudAction),
};
const validators = [];
let ctrlOptions = {};
if (this.crudAction !== 'search') {
if (data.value.field.props.required &&
this.crudAction !== 'search') {
validators.push(Validators.required);
}
if (data.value.field.props.type === 'email') {
ctrlOptions = { updateOn: 'blur' };
validators.push(Validators.email);
}
if (!!data.value.field.props.format &&
data.value.field.props.format.length > 0) {
validators.push(CustomValidators.validFormat(data.value.field.props.format));
}
if (data.value.field.props.isInteger) {
validators.push(Validators.compose([
Validators.min(data.value.field.props.min),
CustomValidators.validIntegerCharacters(),
]));
}
if (data.value.field.componentName === EComponentName.datepicker) {
ctrlOptions = { updateOn: 'blur' };
validators.push(CustomValidators.validDate());
}
if (data.value.field.componentName ===
EComponentName.inputChipsAutocomplete) {
validators.push(inputValueIsNotAnOption(this.filteredOptions, data.modelRef));
}
if (data.value.field.props.regexValidator) {
validators.push(Validators.pattern(new RegExp(data.value.field.props.regexValidator)));
}
}
if (data.value.field.componentName === EComponentName.autocomplete) {
validators.push(inputValueIsNotAnOptionSimpleAutocomplete(this.filteredOptions, data.modelRef));
}
ctrlOptions = { ...ctrlOptions, validators };
return {
...acc,
[data.modelRef]: new FormControl({ ...currentFormState }, ctrlOptions),
};
}, {}));
this.formInitialState = {
...saveCheckoutFormState(this.checkoutForm.getRawValue(), this.templateFormFields, this.crudAction),
};
this.formPreviousState = {
...Object.keys(this.formInitialState).reduce((acc, curr) => ({
...acc,
[curr]: {
value: this.formInitialState[curr].value,
enabled: this.formInitialState[curr].enabled,
},
}), {}),
};
this.checkoutForm.valueChanges
.pipe(tap((changes) => {
this.setDisableSubmitBtn(changes);
this.formErrors = getFormErrors(this.checkoutForm, this.templateFormFields, changes);
}), pairwise(), switchMap(([prevChanges, currChanges]) => of([
currChanges,
this.getCurrentEditedField(prevChanges, currChanges),
])), tap(([changes, currentEditedField]) => this.updateFormPreviousState(changes, currentEditedField)), filter(([_changes, currentEditedField]) => !!currentEditedField &&
currentEditedField.value.field.componentName.includes(EComponentName.autocomplete)), debounceTime(600), distinctUntilChanged(([prevChanges, _], [currChanges, currCurrentEditedField]) => prevChanges[currCurrentEditedField.modelRef] ===
currChanges[currCurrentEditedField.modelRef]), map(([changes, currentEditedField]) => {
const currentModelRef = currentEditedField.modelRef;
this.onAutocompleteInputValueChange(currentModelRef, changes[currentModelRef]);
this.checkoutForm.controls[currentModelRef].updateValueAndValidity();
if (currentEditedField.value.field.componentName ===
EComponentName.inputChipsAutocomplete) {
this.chipLists.find((chip) => chip.id === currentModelRef).errorState = this.checkoutForm.controls[currentModelRef].invalid;
}
}))
.subscribe();
}
onAutocompleteInputValueChange(modelRef, changesEditedField) {
this.offset[modelRef] = 0;
this.getNextBatch(modelRef, changesEditedField);
}
getCurrentEditedField(prevChanges, currChanges) {
const prevChangesKeys = Object.keys(prevChanges);
const currChangesKeys = Object.keys(currChanges);
const changesKeys = prevChangesKeys.filter((value) => currChangesKeys.includes(value));
const editedFieldKey = changesKeys.find((key) => prevChanges[key] !== currChanges[key]);
return this.templateFormFields.find((element) => element.modelRef === editedFieldKey);
}
setDisableSubmitBtn(changes) {
for (const key in changes) {
switch (this._formDataMap[key]) {
case EComponentName.input:
if ((this.checkoutForm.controls[key].value === '' &&
this.formInitialState[key].value == null &&
this.checkoutForm.controls[key].valid) ||
changes[key] === this.formInitialState[key].value) {
this.checkoutForm.get(key).markAsPristine();
}
break;
case EComponentName.imgInput:
if (this.imageBase64[key] !== this.formInitialState[key].value) {
this.checkoutForm.get(key).markAsDirty();
}
else {
this.checkoutForm.get(key).markAsPristine();
}
break;
case EComponentName.inputChipsAutocomplete:
const initialStateArray = this.formInitialState[key]
.value;
const previousStateArray = this.formPreviousState[key]
.value;
if (_.isEqual(initialStateArray, previousStateArray) &&
(changes[key] === null ||
changes[key] === '' ||
(!!changes[key] && this.filteredOptions[key].length > 0))) {
this.checkoutForm.get(key).markAsPristine();
}
else {
this.checkoutForm.get(key).markAsDirty();
}
break;
case EComponentName.autocomplete:
if (typeof changes[key] === 'string') {
const options = this.filteredOptions[key].map((element) => element.option);
if (changes[key] ===
this.formInitialState[key].value
?.option ||
(options.length > 0 &&
!options.includes(changes[key]) &&
this.areAutocompletesPanelsOpened[key])) {
this.checkoutForm.get(key).markAsPristine();
}
else {
this.checkoutForm.get(key).markAsDirty();
}
}
else {
if (_.isEqual(changes[key], this.formInitialState[key].value)) {
this.checkoutForm.get(key).markAsPristine();
}
}
break;
case EComponentName.datepicker:
let dateAsString = moment.isMoment(changes[key])
? changes[key].format('YYYY-MM-DD')
: changes[key];
if (dateAsString !== this.formInitialState[key].value ||
this.checkoutForm.get(key).invalid) {
this.checkoutForm.get(key).markAsDirty();
}
else {
this.checkoutForm.get(key).markAsPristine();
}
break;
default:
if (changes[key] === this.formInitialState[key].value) {
this.checkoutForm.get(key).markAsPristine();
}
break;
}
}
}
onBlur(event, trigger) {
if (event.relatedTarget != null) {
trigger.closePanel();
}
}
onDatepickerInputValueChange(event, modelRef) {
const controlMoment = moment(event.targetElement.value, this._envService.ui.displayDateFormat);
if (controlMoment.isValid()) {
this.checkoutForm.controls[modelRef].setValue(controlMoment);
}
else if (event.targetElement.value != null &&
event.targetElement.value !== '') {
this.checkoutForm.controls[modelRef].setErrors({
isNotAValidDate: true,
});
}
else if (event.targetElement.value == null ||
event.targetElement.value === '') {
this.checkoutForm.controls[modelRef].setErrors(null);
}
}
add(event, modelRef) {
const input = event.chipInput.inputElement;
const value = event.value;
if ((value || '').trim()) {
const option = convertInputValueToAutocompleteOption(value.trim(), this.autoCompleteOptions[modelRef]);
if (!!option) {
this.formPreviousState[modelRef].value.push(option);
}
}
if (input) {
input.value = '';
}
this.checkoutForm.controls[modelRef].setValue(null);
this.onAutocompleteInputValueChange(modelRef, null);
}
remove(value, modelRef) {
const index = this.formPreviousState[modelRef].value.indexOf(value);
if (index >= 0) {
this.formPreviousState[modelRef].value.splice(index, 1);
}
this.setDisableSubmitBtn(Object.keys(this.formPreviousState).reduce((acc, curr) => ({
...acc,
[curr]: this.formPreviousState[curr].value,
}), {}));
this.onAutocompleteInputValueChange(modelRef, null);
}
selected(modelRef, input, event) {
this.formPreviousState[modelRef].value.push(event.option.value);
input.value = '';
this.checkoutForm.controls[modelRef].setValue(null);
}
onCloseAutocompletePanel(modelRef, value, componentType) {
this.areAutocompletesPanelsOpened[modelRef] = false;
this.resetOffset(modelRef);
if (componentType === this.componentNameType.autocomplete &&
value !==
this.formInitialState[modelRef].value?.option) {
this.checkoutForm.get(modelRef).markAsDirty();
this.checkoutForm.controls[modelRef].updateValueAndValidity();
}
}
onClickFocusAutocompleteInput(modelRef, value, autocompleteTrigger) {
if (!this.areAutocompletesPanelsOpened[modelRef]) {
this.getNextBatch(modelRef, value);
autocompleteTrigger.openPanel();
this.areAutocompletesPanelsOpened[modelRef] = true;
}
}
onSubmit(newData) {
const convertedRequestBodyForm = convertAutocompleteInputValueForRequestBody(newData, this.autoCompleteOptions, this.formPreviousState, Object.keys(this.autocompleteOptionsLoaded).filter((key) => this.autocompleteOptionsLoaded[key]));
const trimmedRequestedBodyForm = trimFormFieldsValues(convertedRequestBodyForm);
for (const key in trimmedRequestedBodyForm) {
if (Object.prototype.hasOwnProperty.call(trimmedRequestedBodyForm, key)) {
const isFieldValueStillNull = trimmedRequestedBodyForm[key] == null &&
this.formInitialState[key].value == null;
const isSearchFormFieldEmpty = this.crudAction === 'search' && trimmedRequestedBodyForm[key] === '';
const isFieldStillDisabled = this.checkoutForm.controls[key].disabled &&
!this.formInitialState[key].enabled;
const isInputValueStillEmpty = this._formDataMap[key] === EComponentName.input &&
this.checkoutForm.controls[key].value === '' &&
this.formInitialState[key].value == null;
if (this.checkoutForm.controls[key].disabled &&
this.formInitialState[key].enabled) {
trimmedRequestedBodyForm[key] = null;
}
else if (isFieldValueStillNull ||
isSearchFormFieldEmpty ||
isFieldStillDisabled ||
isInputValueStillEmpty ||
!keepFieldInRequestBody(this._formDataMap[key], trimmedRequestedBodyForm[key], this.formInitialState[key], this.autocompleteOptionsLoaded[key], this.crudAction)) {
delete trimmedRequestedBodyForm[key];
}
}
}
const formattedDatePickerForm = formatDatepickerForm(trimmedRequestedBodyForm, this.templateFormFields, this._envService.ui.displayDateFormat);
if (this.crudAction === 'update') {
this.clickOnSubmitBtn.emit(true);
const convertedScimObjectForm = convertToScimObject(formattedDatePickerForm, this.templateFormFields, this.entryId, this._selectedEntryExternalId, this.scimProperties.schemas);
const convertedBooleanToStringForm = convertBooleanToString(convertedScimObjectForm, this.templateFormFields);
const concatenatedFields = concatOldValuesToNewValue(convertedBooleanToStringForm, this.templateFormFields, getAuthorWithDate(this._userDisplayName, this._document.documentElement.lang) + '\n');
const fullRequestBody = {
...concatenatedFields,
...this._requiredUndisplayedFields.reduce((acc, field) => ({ ...acc, [field.modelRef]: field.dataValue }), {}),
};
this._interactionsService.isProcessingRequest(true);
this.checkoutForm.disable();
this._dataService
.updateData(this.endpoint, this.entryId, fullRequestBody)
.pipe(catchError((err) => {
this._interactionsService.isProcessingRequest(false);
this._store$.dispatch([
new ErrorActions.SetErrorDetail({
error: err,
userActionOnError: EUserAction.DETAIL_EDIT,
}),
new ErrorActions.IsHandlingError(true),
]);
if (err.status === 404) {
this._router.navigate([
this._router.url.split('/').slice(0, -1).join('/'),
'not-found',
], { replaceUrl: true });
}
else {
this.templateFormFields.forEach((field) => {
if (!isFormFieldDisabled(field, this.crudAction)) {
this.checkoutForm.controls[field.modelRef].enable();
}
else {
this.checkoutForm.controls[field.modelRef].disable();
}
});
this.clickOnSubmitBtn.emit(false);
}
this.updateFailed.emit();
return this._errorsHandlerService.handleError(err);
}))
.subscribe((response) => {
if (response.status === 200 || response.status === 201) {
this._interactionsService.isProcessingRequest(false);
this.updateSucceeded.emit(response.body);
this.userId$
.pipe(takeUntil(this._onDestroy$), tap((id) => (this._userId = id)))
.subscribe();
if (this.entryId === this._userId) {
const value = {
displayName: response.body.displayName,
photo: response.body.photo,
};
this._store$.dispatch(new User.UpdateCurrentUserConfig({
userInfo: value,
}));
}
}
});
}
if (this.crudAction === 'create') {
this.clickOnSubmitBtn.emit(true);
const convertedScimObjectForm = convertToScimObject(formattedDatePickerForm, this.templateFormFields, this.entryId, this._selectedEntryExternalId, this.scimProperties.schemas);
const convertedBooleanToStringForm = convertBooleanToString(convertedScimObjectForm, this.templateFormFields);
this._interactionsService.isProcessingRequest(true);
if (this.authorizeDisableOnRequest) {
this.checkoutForm.disable();
}
let fullRequestBody = convertedBooleanToStringForm;
if (this.entryId) {
fullRequestBody = {
...fullRequestBody,
parentId: this.entryId,
};
}
this._dataService
.createData(this.endpoint, fullRequestBody)
.pipe(catchError((err) => {
this._interactionsService.isProcessingRequest(false);
this.templateFormFields.forEach((field) => {
if (!isFormFieldDisabled(field, this.crudAction)) {
this.checkoutForm.controls[field.modelRef].enable();
}
else {
this.checkoutForm.controls[field.modelRef].disable();
}
});
this.clickOnSubmitBtn.emit(false);
this.addFailed.emit(err);
return this._errorsHandlerService.handleError(err);
}))
.subscribe((response) => {
if (response.status === 200 || response.status === 201) {
this._interactionsService.isProcessingRequest(false);
this.addSucceeded.emit(response.body);
}
});
}
if (this.crudAction === 'search') {
this.formData.forEach((formField) => (formField.dataValue = formattedDatePickerForm[formField.modelRef]));
this.submitForm.emit();
}
}
closeSubmitFormDialog() {
this.clickOnCancelBtn.emit(true);
}
getLabel(labels) {
return Object.prototype.hasOwnProperty.call(labels, 'onCreate') &&
this.crudAction === 'create'
? labels.onCreate
: labels.long;
}
getNextBatch(modelRef, inputValue) {
const initialValue = this.formInitialState[modelRef].value;
const previousValue = this.formPreviousState[modelRef].value;
const formField = this.templateFormFields.find((field) => field.modelRef === modelRef);
const isInputChipsAutocomplete = formField.value.field.componentName ===
EComponentName.inputChipsAutocomplete;
const isInitialState = isAutocompleteInitialState(inputValue, isInputChipsAutocomplete, previousValue, initialValue);
if (isInitialState) {
this.filteredOptions[modelRef] = getFilteredOptionsOnInitialState(isInputChipsAutocomplete, this.autoCompleteOptions[modelRef], previousValue);
}
else {
const filteringCondition = getOptionsFilteringCondition(inputValue, isInputChipsAutocomplete, previousValue);
this.filteredOptions[modelRef] = this.autoCompleteOptions[modelRef].filter((option) => filteringCondition(option.option));
}
if (!!formField.value.field.props.linkedToFields) {
const allLinkedFieldsValues = getLinkedFieldsValues(formField.value.field.props.linkedToFields, this.formPreviousState);
this.filteredOptions[modelRef] = this.filteredOptions[modelRef].filter((option) => !allLinkedFieldsValues.includes(option.value?.id?.toLocaleUpperCase()));
}
const result = this.filteredOptions[modelRef].slice(this.offset[modelRef], this.offset[modelRef] + this.limit[modelRef]);
this.options[modelRef].next(result);
this.offset[modelRef] += this.limit[modelRef];
}
initAutocompleteProperties(allAutocompleteFields) {
allAutocompleteFields.forEach((field) => {
this.options = {
...this.options,
[field.modelRef]: new BehaviorSubject([]),
};
this.options$ = {
...this.options$,
[field.modelRef]: this.options[field.modelRef].asObservable().pipe(scan((acc, curr) => {
if (this.offset[field.modelRef] !== 0) {
return [...acc, ...curr];
}
else {
return [...curr];
}
}, [])),
};
this.autoCompleteOptions = {
...this.autoCompleteOptions,
[field.modelRef]: [],
};
this.filteredOptions = {
...this.filteredOptions,
[field.modelRef]: [],
};
this.autocompleteOptionsLoaded = {
...this.autocompleteOptionsLoaded,
[field.modelRef]: false,
};
this.offset = {
...this.offset,
[field.modelRef]: field.value.field.nestedComponent.props.offset,
};
this.limit = {
...this.limit,
[field.modelRef]: field.value.field.nestedComponent.props.limit,
};
this.areAutocompletesPanelsOpened = {
...this.areAutocompletesPanelsOpened,
[field.modelRef]: false,
};
});
}
setAutocompleteOptions(autocompleteField, dataResources, resourceTypeId) {
let checkoutFormFieldValue = null;
if (!!this.checkoutForm.controls[autocompleteField.modelRef].value) {
checkoutFormFieldValue =
this.checkoutForm.controls[autocompleteField.modelRef].value;
}
if (autocompleteField.value.field.componentName ===
EComponentName.inputChipsAutocomplete) {
this.checkoutForm.controls[autocompleteField.modelRef].setValue(null);
this.formInitialState[autocompleteField.modelRef].value = [];
this.formPreviousState[autocompleteField.modelRef].value = [];
}
const optionNestedComponent = autocompleteField.value.field.nestedComponent;
const optionsField = optionNestedComponent.props.options[resourceTypeId].optionsField;
dataResources.forEach((data) => {
const currentValue = {
id: data.id,
label: data[optionNestedComponent.props.options[resourceTypeId].valuesField],
};
let currentOption = '';
if (!optionsField.some((optionField) => data[optionField] === undefined)) {
optionsField.forEach((optionField, index) => (currentOption =
index > 0
? currentOption.concat(' (', data[optionField], ')')
: currentOption.concat(data[optionField])));
const currentObject = {
value: currentValue,
option: currentOption,
};
this.autoCompleteOptions[autocompleteField.modelRef].push(currentObject);
if (!!checkoutFormFieldValue) {
const initialValue = getFormAutocompleteFieldInitialValue(checkoutFormFieldValue, currentObject, this.formInitialState[autocompleteField.modelRef].value, this.formPreviousState[autocompleteField.modelRef].value);
if (!!initialValue) {
this.checkoutForm.controls[autocompleteField.modelRef].setValue(initialValue.controlValue);
this.formInitialState[autocompleteField.modelRef] = {
value: initialValue.initialStateValue,
enabled: this.formInitialState[autocompleteField.modelRef].enabled,
};
this.formPreviousState[autocompleteField.modelRef] = {
value: initialValue.previousStateValue,
enabled: this.formInitialState[autocompleteField.modelRef].enabled,
};
}
}
}
});
this.autocompleteOptionsLoaded[autocompleteField.modelRef] = true;
if (!!this.checkoutForm &&
this.checkoutForm.controls[autocompleteField.modelRef]) {
this.checkoutForm.controls[autocompleteField.modelRef].enable();
}
}
updateFormPreviousState(changes, editedField) {
for (const modelRef in changes) {
if (Object.prototype.hasOwnProperty.call(changes, modelRef)) {
const element = changes[modelRef];
if (editedField !== undefined &&
editedField.modelRef === modelRef &&
editedField.value.field.componentName !==
EComponentName.inputChipsAutocomplete) {
this.formPreviousState[modelRef].value = element;
}
}
}
}
resetOffset(modelRef) {
this.offset[modelRef] = 0;
}
displayOption(option) {
return option && option.option ? option.option : '';
}
convertImgToBase64(event, modelRef, label) {
if (event.target.files && event.target.files[0]) {
const reader = new FileReader();
reader.onload = (e) => {
const image = new Image();
image.src = e.target.result;
image.onload = (_) => {
const canvas = document.createElement('canvas');
const newImgSizes = resizeImage(image.width, image.height, 256, 256);
canvas.width = newImgSizes.width;
canvas.height = newImgSizes.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(image, 0, 0, newImgSizes.width, newImgSizes.height);
const dataUrl = canvas.toDataURL('image/jpeg');
this.imageBase64 = {
...this.imageBase64,
[modelRef]: dataUrl.replace(/data:.*;base64,/, ''),
};
this.setImgAspectRatio(this.imageBase64[modelRef], modelRef);
};
};
reader.readAsDataURL(event.target.files[0]);
}
label.focus();
}
setImgAspectRatio(imgDataValue, modelRef) {
const image = new Image();
image.src = 'data:image/jpeg;base64,' + imgDataValue;
image.onload = () => {
this.imgAspectRatio = {
...this.imgAspectRatio,
[modelRef]: image.width / image.height,
};
};
}
getFxFlex(componentName, screenSize = null) {
const isInputChipsAutcomplete = componentName === EComponentName.inputChipsAutocomplete;
const isTextArea = componentName === EComponentName.textarea;
let fxFlexPropertyValue;
if (isInputChipsAutcomplete || isTextArea) {
fxFlexPropertyValue = '1 1 100%';
}
else {
switch (screenSize) {
case 'sm':
fxFlexPropertyValue = '1 1 calc(50% - 4px)';
break;
case 'xs':
fxFlexPropertyValue = '1 1 calc(100%)';
break;
default:
fxFlexPropertyValue = '1 1 calc(33.3% - 4px)';
break;
}
}
return fxFlexPropertyValue;
}
removeImg(modelRef, inputFile, event, label) {
this.imageBase64[modelRef] = null;
inputFile.clear();
label.focus();
event.stopPropagation();
}
getAutocomplete(data) {
return this.autocompleteOff ? 'off' : data.value.field.props.autocomplete;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.4", ngImport: i0, type: GenericFormComponent, deps: [{ token: DOCUMENT }, { token: i1.Store }, { token: i2.DataService }, { token: i2.InteractionsService }, { token: i2.ErrorsHandlerService }, { token: i2.EnvService }, { token: i3.Router }, { token: i2.FormService }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.4", type: GenericFormComponent, selector: "dm-generic-form", inputs: { autocompleteOff: "autocompleteOff", isDisplayingDefaultFormBtns: "isDisplayingDefaultFormBtns", crudAction: "crudAction", endpoint: "endpoint", entryId: "entryId", formData: "formData", scimProperties: "scimProperties", authorizeDisableOnRequest: "authorizeDisableOnRequest", isInModal: "isInModal", enableCheckoutForm: "enableCheckoutForm" }, outputs: { clickOnCancelBtn: "clickOnCancelBtn", clickOnSubmitBtn: "clickOnSubmitBtn", addSucceeded: "addSucceeded", addFailed: "addFailed", updateSucceeded: "updateSucceeded", updateFailed: "updateFailed", submitForm: "submitForm" }, providers: [{ provide: ErrorStateMatcher, useClass: FormErrorStateMatcher }], viewQueries: [{ propertyName: "formFieldsList", first: true, predicate: ["formFieldsList"], descendants: true }, { propertyName: "buttonsActions", first: true, predicate: ["buttonsActions"], descendants: true }, { propertyName: "chipLists", predicate: MatChipGrid, descendants: true }], ngImport: i0, template: "<!-- Copyright (C) 2020-2024 Linagora\n\nThis program is free software: you can redistribute it and/or modify it under\nthe terms of the GNU Affero General Public License as published by the Free\nSoftware Foundation, either version 3 of the License, or (at your option) any\nlater version, provided you comply with the Additional Terms applicable for\nLinID Directory Manager software by LINAGORA pursuant to Section 7 of the GNU\nAffero General Public License, subsections (b), (c), and (e), pursuant to\nwhich these Appropriate Legal Notices must notably (i) retain the display of\nthe \"LinID\u2122\" trademark/logo at the top of the interface window, the display\nof the \u201CYou are using the Open Source and free version of LinID\u2122, powered by\nLinagora \u00A9 2009\u20132013. Contribute to LinID R&D by subscribing to an Enterprise\noffer!\u201D infobox and in the e-mails sent with the Program, notice appended to\nany type of outbound messages (e.g. e-mail and meeting requests) as well as\nin the LinID Directory Manager user interface, (ii) retain all hypertext\nlinks between LinID Directory Manager and https://linid.org/, as well as\nbetween LINAGORA and LINAGORA.com, and (iii) refrain from infringing LINAGORA\nintellectual property rights over its trademarks and commercial brands. Other\nAdditional Terms apply, see <http://www.linagora.com/licenses/> for more\ndetails.\n\nThis program is distributed in the hope that it will be useful, but WITHOUT\nANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS\nFOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more\ndetails.\n\nYou should have received a copy of the GNU Affero General Public License and\nits applicable Additional Terms for LinID Directory Manager along with this\nprogram. If not, see <http://www.gnu.org/licenses/> for the GNU Affero\nGeneral Public License version 3 and <http://www.linagora.com/licenses/> for\nthe Additional Terms applicable to the LinID Directory Manager software. -->\n\n<form\n *ngIf=\"formAccessibility$ | async as formAccessibility\"\n #f=\"ngForm\"\n id=\"genericForm\"\n [formGroup]=\"checkoutForm\"\n (keydown.tab)=\"$event.stopPropagation()\"\n (ngSubmit)=\"onSubmit(checkoutForm.getRawValue())\"\n>\n <ng-container\n *ngIf=\"customFormContentTemplate != null; else defaultTemplate\"\n [ngTemplateOutlet]=\"customFormContentTemplate\"\n ></ng-container>\n\n <ng-template #defaultTemplate>\n <ng-container\n [ngTemplateOutlet]=\"formFieldsList\"\n [ngTemplateOutletContext]=\"{\n formFields: templateFormFields\n }\"\n ></ng-container>\n </ng-template>\n\n <ng-template let-formFields=\"formFields\" #formFieldsList>\n <div\n #divFormFields\n fxLayout=\"{{ nbFormFields > 3 ? 'row wrap' : 'column' }}\"\n [ngClass]=\"{ 'div-form-search-content': crudAction === 'search' }\"\n class=\"div-form-content\"\n >\n <div\n [fxFlex]=\"getFxFlex(data.value.field.componentName)\"\n [fxFlex.sm]=\"getFxFlex(data.value.field.componentName, 'sm')\"\n [fxFlex.xs]=\"getFxFlex(data.value.field.componentName, 'xs')\"\n *ngFor=\"let data of formFields; let i = index\"\n class=\"div-form-field\"\n [ngClass]=\"data.modelRef\"\n >\n <div [ngSwitch]=\"data.value.field.componentName\">\n <mat-form-field\n fxFlex\n *ngSwitchCase=\"componentNameType.input\"\n color=\"accent\"\n class=\"custom-form-field\"\n >\n <mat-label fxLayout=\"row\">\n <mat-icon aria-hidden=\"true\">{{\n data.value.icon.value\n }}</mat-icon>\n {{ getLabel(data.value.label.value) }}\n </mat-label>\n <input\n matInput\n appOmitChars\n [formControlName]=\"data.modelRef\"\n [type]=\"data.value.field.props.type\"\n [maxlength]=\"data.value.field.props.maxlength\"\n [min]=\"data.value.field.props.min ?? null\"\n [placeholder]=\"data.value.field.props.placeholder\"\n [autocomplete]=\"getAutocomplete(data)\"\n [ngModel]=\"data.dataValue\"\n [invalidChars]=\"data.value.field.props.invalidCharacters ?? []\"\n [regexString]=\"data.value.field.props.regexInput ?? null\"\n class=\"ellipsis\"\n />\n <mat-hint>{{ data.value.hint.value }}</mat-hint>\n <mat-error>{{ formErrors[data.modelRef] }}</mat-error>\n </mat-form-field>\n <mat-form-field\n *ngSwitchCase=\"componentNameType.inputChipsAutocomplete\"\n fxFlex\n color=\"accent\"\n #formField\n class=\"form-field-input-chips\"\n >\n <mat-label>\n <mat-icon aria-hidden=\"true\">{{\n data.value.icon.value\n }}</mat-icon>\n {{ getLabel(data.value.label.value) }}\n </mat-label>\n <mat-chip-grid\n #chipList\n [disabled]=\"!autocompleteOptionsLoaded[data?.modelRef]\"\n >\n <mat-chip-row\n *ngFor=\"let value of formPreviousState[data.modelRef].value\"\n [removable]=\"data.value.field.props.removable\"\n (removed)=\"remove(value, data.modelRef)\"\n >\n {{ value.option }}\n <mat-icon\n *ngIf=\"data?.value.field.props.removable\"\n aria-hidden=\"true\"\n matChipRemove\n tabindex=\"0\"\n (keydown.enter)=\"remove(value, data.modelRef)\"\n >cancel</mat-icon\n >\n </mat-chip-row>\n <input\n [formControlName]=\"data.modelRef\"\n [id]=\"data.modelRef\"\n [maxLength]=\"data.value.field.props.maxlength\"\n [placeholder]=\"data.value.field.props.placeholder\"\n [matAutocomplete]=\"autoInputChips\"\n [matChipInputFor]=\"chipList\"\n [matChipInputSeparatorKeyCodes]=\"separatorKeysCodes\"\n [matChipInputAddOnBlur]=\"data.value.field.props.addOnBlur\"\n [autocomplete]=\"getAutocomplete(data)\"\n (matChipInputTokenEnd)=\"add($event, data.modelRef)\"\n (click)=\"\n onClickFocusAutocompleteInput(\n data.modelRef,\n inputChipsAutocomplete.value,\n autocompleteTrigger\n )\n \"\n (focus)=\"\n onClickFocusAutocompleteInput(\n data.modelRef,\n inputChipsAutocomplete.value,\n autocompleteTrigger\n )\n \"\n class=\"ellipsis\"\n #inputChipsAutocomplete\n #autocompleteTrigger=\"matAutocompleteTrigger\"\n />\n </mat-chip-grid>\n <mat-autocomplete\n appInfiniteScroll\n [displayWith]=\"displayOption\"\n [isOpened]=\"autoInputChips.isOpen\"\n [inputWidth]=\"formField._elementRef.nativeElement.offsetWidth\"\n [complete]=\"\n offset[data.modelRef] === filteredOptions[data.modelRef].length\n \"\n [panelWidth]=\"'fit-content'\"\n (closed)=\"\n onCloseAutocompletePanel(\n data.modelRef,\n inputChipsAutocomplete.value,\n data.value.field.componentName\n )\n \"\n (infiniteScroll)=\"\n getNextBatch(data.modelRef, inputChipsAutocomplete.value)\n \"\n (optionSelected)=\"\n selected(data.modelRef, inputChipsAutocomplete, $event)\n \"\n #autoInputChips=\"matAutocomplete\"\n >\n <mat-option\n *ngFor=\"\n let currentOption of options$[data.modelRef] | async;\n let i = index\n \"\n [value]=\"currentOption\"\n [title]=\"currentOption.option\"\n >\n {{ currentOption.option }}\n </mat-option>\n </mat-autocomplete>\n <mat-hint *ngIf=\"!autocompleteOptionsLoaded[data?.modelRef]\">\n {{ data.value.hint.value }}\n </mat-hint>\n <mat-error *ngIf=\"chipList?.errorState\">\n {{ formErrors[data.modelRef] }}\n </mat-error>\n </mat-form-field>\n <mat-form-field\n *ngSwitchCase=\"componentNameType.textarea\"\n fxFlex\n color=\"accent\"\n appearance=\"fill\"\n class=\"form-field-textarea\"\n >\n <mat-label fxLayout=\"row\">\n <mat-icon aria-hidden=\"true\">{{\n data.value.icon.value\n }}</mat-icon>\n {{ getLabel(data.value.label.value) }}\n </mat-label>\n <textarea\n matInput\n cdkTextareaAutosize\n [formControlName]=\"data.modelRef\"\n [placeholder]=\"data.value.field.props.placeholder\"\n [autocomplete]=\"getAutocompl