UNPKG

@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
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