Change ngx-chips with primeng multiselect

This commit is contained in:
y.yerli 2024-02-07 18:15:24 +03:00 committed by Jack Anderson
parent 0855cb18a2
commit 4795d38da5
19 changed files with 251 additions and 119 deletions

View file

@ -26,7 +26,9 @@
],
"styles": [
"node_modules/bootstrap-css-only/css/bootstrap.min.css",
"core/app/shell/src/themes/suite8/css/style.scss"
"core/app/shell/src/themes/suite8/css/style.scss",
"node_modules/primeng/resources/themes/lara-light-blue/theme.css",
"node_modules/primeng/resources/primeng.min.css"
],
"stylePreprocessorOptions": {
"includePaths": [
@ -359,6 +361,7 @@
}
},
"cli": {
"packageManager": "yarn"
"packageManager": "yarn",
"analytics": false
}
}

View file

@ -38,6 +38,12 @@ parameters:
listview_max_height: 0
record_modal_max_height: 620
inline_confirmation_loading_delay: 300
multiselect_max_number:
XSmall: 2
Small: 2
Medium: 4
Large: 20
XLarge: 20
displayed_quick_filters:
XSmall: 0
Small: 4

View file

@ -205,7 +205,7 @@ export class SavedFilterStore implements StateStore {
}
initValidators(record: Record): void {
if(!record || !Object.keys(record?.fields).length) {
if(!record || !record?.fields || !Object.keys(record?.fields).length) {
return;
}

View file

@ -37,6 +37,7 @@ import {
} from '../../store/language/language.store';
import {FieldLogicManager} from '../field-logic/field-logic.manager';
import {FieldLogicDisplayManager} from '../field-logic-display/field-logic-display.manager';
import {isNull, isObject} from "lodash-es";
@Component({template: ''})
export class BaseEnumComponent extends BaseFieldComponent implements OnInit, OnDestroy {
@ -150,6 +151,10 @@ export class BaseEnumComponent extends BaseFieldComponent implements OnInit, OnD
this.options = [];
Object.keys(this.optionsMap).forEach(key => {
if(isEmptyString(this.optionsMap[key]) && this.field.type === 'multienum') {
return;
}
this.options.push({
value: key,
label: this.optionsMap[key]
@ -209,8 +214,7 @@ export class BaseEnumComponent extends BaseFieldComponent implements OnInit, OnD
* */
protected initEnumDefault(): void {
if (!isEmptyString(this.record?.id)) {
if (!isEmptyString(this.record?.id)) {
this.field?.formControl.setValue('');
return;
@ -224,12 +228,10 @@ export class BaseEnumComponent extends BaseFieldComponent implements OnInit, OnD
this.field.formControl.setValue('');
return;
}
this.selectedValues.push({
value: defaultVal,
label: this.optionsMap[defaultVal]
});
this.initEnumDefaultFieldValues(defaultVal);
}
@ -335,4 +337,22 @@ export class BaseEnumComponent extends BaseFieldComponent implements OnInit, OnD
}
}
protected buildOptionFromValue(value: string): Option {
const option: Option = {value: '', label: ''};
if (isNull(value)) {
return option;
}
option.value = (typeof value !== 'string' ? JSON.stringify(value) : value).trim();
option.label = option.value;
const valueLabel = this.optionsMap[option.value] ?? option.label;
if (isObject(valueLabel)) {
return option;
}
option.label = (typeof valueLabel !== 'string' ? JSON.stringify(valueLabel) : valueLabel).trim();
return option;
}
}

View file

@ -32,6 +32,7 @@ import {DataTypeFormatter} from '../../services/formatters/data-type.formatter.s
import {debounceTime} from 'rxjs/operators';
import {FieldLogicManager} from '../field-logic/field-logic.manager';
import {FieldLogicDisplayManager} from '../field-logic-display/field-logic-display.manager';
import {isEqual} from "lodash-es";
@Component({template: ''})
export class BaseFieldComponent implements FieldComponentInterface, OnInit, OnDestroy {
@ -278,6 +279,15 @@ export class BaseFieldComponent implements FieldComponentInterface, OnInit, OnDe
this.field.value = newValue;
}
protected setFormControlValue(newValue: string | string[]): void {
this.field.formControl.markAsDirty();
if (isEqual(this.field.formControl.value, newValue)) {
return;
}
this.field.formControl.setValue(newValue);
}
protected unsubscribeAll(): void {
this.subs.forEach(sub => sub.unsubscribe());
}

View file

@ -30,6 +30,8 @@ import {BaseEnumComponent} from './base-enum.component';
import {LanguageStore} from '../../store/language/language.store';
import {FieldLogicManager} from '../field-logic/field-logic.manager';
import {FieldLogicDisplayManager} from '../field-logic-display/field-logic-display.manager';
import { isArray, isEmpty, uniqBy } from 'lodash-es';
import { isVoid } from 'common';
@Component({template: ''})
export class BaseMultiEnumComponent extends BaseEnumComponent {
@ -43,22 +45,42 @@ export class BaseMultiEnumComponent extends BaseEnumComponent {
super(languages, typeFormatter, logic, logicDisplay);
}
protected initValue(): void {
this.selectedValues = [];
protected subscribeValueChanges(): void {
if (!this.field?.formControl) {
return;
}
if (!this.field.valueList || this.field.valueList.length < 1) {
const formValueChangesSubscription = this.field.formControl.valueChanges.subscribe(
(value: string[]) => this.field.valueList = value);
this.subs.push(formValueChangesSubscription);
}
protected initValue(): void {
const fieldValueList = this.field.valueList;
if (isVoid(fieldValueList) || isEmpty(fieldValueList)) {
this.initEnumDefault();
return;
}
this.field.valueList.forEach(value => {
if (typeof this.optionsMap[value] !== 'string') {
return;
}
this.selectedValues.push({
value,
label: this.optionsMap[value]
});
});
this.updateInternalState(fieldValueList);
}
protected updateInternalState(value: string | string[] = []): void {
const valueArray = isArray(value) ? value : [value];
this.selectedValues = valueArray.map(valueElement=>this.buildOptionFromValue(valueElement));
this.selectedValues = uniqBy(this.selectedValues, 'value');
this.syncSelectedValuesWithForm();
}
protected syncSelectedValuesWithForm(): string[] {
const selectedValuesValueMap = this.selectedValues.map(selectedValue => selectedValue.value);
this.setFormControlValue(selectedValuesValueMap);
return selectedValuesValueMap;
}
}

View file

@ -39,6 +39,7 @@ import {FieldLogicDisplayManager} from '../field-logic-display/field-logic-displ
@Component({template: ''})
export class BaseRelateComponent extends BaseFieldComponent implements OnInit, OnDestroy {
selectedValues: AttributeMap[] = [];
options: AttributeMap[] = [];
status: '' | 'searching' | 'not-found' | 'error' | 'found' | 'no-module' = '';
initModule = '';
@ -79,7 +80,6 @@ export class BaseRelateComponent extends BaseFieldComponent implements OnInit, O
}
onModuleChange(): void {
const currentModule = this.initModule;
const newModule = this?.field?.definition?.module ?? '';
@ -96,12 +96,20 @@ export class BaseRelateComponent extends BaseFieldComponent implements OnInit, O
if (newModule === '') {
this.status = 'no-module';
} else {
this.init();
this.status = '';
this.selectedValues = [];
this.options = [];
}
}
search = (text: string): Observable<any> => {
if(text === '') {
return of([]);
}
this.status = 'searching';
return this.relateService.search(text, this.getRelateFieldName()).pipe(

View file

@ -26,13 +26,13 @@
*/
-->
<div class="dropdownenum">
<select [formControl]="field.formControl" class="custom-select custom-select-sm">
<ng-container *ngIf="this.options && this.options.length">
<ng-container *ngIf="this.options && this.options.length">
<select [formControl]="field.formControl" class="custom-select custom-select-sm">
<option *ngFor="let item of this.options;"
class="{{getId(item)}}"
[ngValue]="item.value">
{{item.label}}
</option>
</ng-container>
</select>
</select>
</ng-container>
</div>

View file

@ -27,7 +27,7 @@
import {Injectable} from '@angular/core';
import {BaseActionManager} from '../../services/actions/base-action-manager.service';
import {FieldLogicActionData, FieldLogicActionHandlerMap} from './field-logic.action';
import {Action, ActionContext, ActionHandlerMap, Field, ModeActions, Record, ViewMode} from 'common';
import {Action, ActionContext, Field, ModeActions, Record, ViewMode} from 'common';
import {FieldLogicDisplayTypeAction} from './display-type/field-logic-display-type.action';
import {EmailPrimarySelectAction} from './email-primary-select/email-primary-select.action';
import {RequiredAction} from './required/required.action';
@ -37,7 +37,6 @@ import {UpdateFlexRelateModuleAction} from './update-flex-relate-module/update-f
import {UpdateValueAction} from './update-value/update-value.action';
import {UpdateValueBackendAction} from './update-value-backend/update-value-backend.action';
import {DisplayTypeBackendAction} from './display-type-backend/display-type-backend.action';
import {RecordActionData} from '../../views/record/actions/record.action';
@Injectable({
providedIn: 'root'

View file

@ -25,22 +25,15 @@
* the words "Supercharged by SuiteCRM".
*/
-->
<tag-input [(ngModel)]="selectedValues"
[onlyFromAutocomplete]="true"
[clearOnBlur]="true"
[displayBy]="'label'"
[identifyBy]="'value'"
[placeholder]="getPlaceholderLabel()"
[secondaryPlaceholder]="getPlaceholderLabel()"
[inputClass]="getInvalidClass()"
#tag
(onAdd)="onAdd()"
(onRemove)="onRemove()"
(keyup.enter)="selectFirstElement()">
<tag-input-dropdown [displayBy]="'label'"
[identifyBy]="'value'"
[showDropdownIfEmpty]="true"
[keepOpen]="false"
[autocompleteItems]="this.options">
</tag-input-dropdown>
</tag-input>
<p-multiSelect
[options]="this.options"
[(ngModel)]="selectedValues"
[optionLabel]="'label'"
(onChange)="onAdd($event)"
(onRemove)="onRemove()"
[placeholder]="placeholderLabel"
[selectedItemsLabel]="'{0} ' + selectedItemsLabel"
[emptyFilterMessage]="emptyFilterLabel"
[maxSelectedLabels]="maxSelectedLabels"
[styleClass]="getInvalidClass()"
></p-multiSelect>

View file

@ -24,14 +24,15 @@
* the words "Supercharged by SuiteCRM".
*/
import {Component, ViewChild} from '@angular/core';
import {TagInputComponent} from 'ngx-chips';
import {Component} from '@angular/core';
import {DataTypeFormatter} from '../../../../services/formatters/data-type.formatter.service';
import {BaseMultiEnumComponent} from '../../../base/base-multienum.component';
import {LanguageStore} from '../../../../store/language/language.store';
import {TagModel} from 'ngx-chips/core/tag-model';
import {FieldLogicManager} from '../../../field-logic/field-logic.manager';
import {FieldLogicDisplayManager} from '../../../field-logic-display/field-logic-display.manager';
import {ScreenSizeObserverService} from "../../../../services/ui/screen-size-observer/screen-size-observer.service";
import {take} from "rxjs/operators";
import {SystemConfigStore} from "../../../../store/system-config/system-config.store";
@Component({
selector: 'scrm-multienum-edit',
@ -40,29 +41,40 @@ import {FieldLogicDisplayManager} from '../../../field-logic-display/field-logic
})
export class MultiEnumEditFieldComponent extends BaseMultiEnumComponent {
@ViewChild('tag') tag: TagInputComponent;
placeholderLabel: string = '';
selectedItemsLabel: string = '';
emptyFilterLabel: string = '';
maxSelectedLabels: number = 20;
constructor(
protected languages: LanguageStore,
protected typeFormatter: DataTypeFormatter,
protected logic: FieldLogicManager,
protected logicDisplay: FieldLogicDisplayManager
protected logicDisplay: FieldLogicDisplayManager,
protected screenSize: ScreenSizeObserverService,
protected systemConfigStore: SystemConfigStore,
) {
super(languages, typeFormatter, logic, logicDisplay);
}
ngOnInit(): void {
this.checkAndInitAsDynamicEnum();
this.getTranslatedLabels();
super.ngOnInit();
const maxSelectedLabelsForDisplay = this.systemConfigStore.getUi('multiselect_max_number');
this.screenSize.screenSize$
.pipe(take(1))
.subscribe((screenSize: any) => {
this.maxSelectedLabels = maxSelectedLabelsForDisplay[screenSize] || this.maxSelectedLabels;
})
}
public onAdd(): void {
public onAdd(event): void {
const value = this.selectedValues.map(option => option.value);
this.field.valueList = value;
this.field.formControl.setValue(value);
this.field.formControl.markAsDirty();
return;
}
public onRemove(): void {
@ -70,25 +82,12 @@ export class MultiEnumEditFieldComponent extends BaseMultiEnumComponent {
this.field.valueList = value;
this.field.formControl.setValue(value);
this.field.formControl.markAsDirty();
setTimeout(() => {
this.tag.focus(true, true);
this.tag.dropdown.show();
}, 200);
}
public getPlaceholderLabel(): string {
return this.languages.getAppString('LBL_SELECT_ITEM') || '';
}
public selectFirstElement(): void {
const filteredElements: TagModel = this.tag.dropdown.items;
if (filteredElements.length !== 0) {
const firstElement = filteredElements[0];
this.selectedValues.push(firstElement);
this.onAdd();
this.tag.dropdown.hide();
}
public getTranslatedLabels(): void {
this.placeholderLabel = this.languages.getAppString('LBL_SELECT_ITEM') || '';
this.selectedItemsLabel = this.languages.getAppString('LBL_ITEMS_SELECTED') || '';
this.emptyFilterLabel = this.languages.getAppString('ERR_SEARCH_NO_RESULTS') || '';
}
}

View file

@ -28,16 +28,16 @@ import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {FormsModule} from '@angular/forms';
import {TagInputModule} from 'ngx-chips';
import {MultiEnumEditFieldComponent} from './multienum.component';
import {MultiSelectModule} from "primeng/multiselect";
@NgModule({
declarations: [MultiEnumEditFieldComponent],
exports: [MultiEnumEditFieldComponent],
imports: [
CommonModule,
TagInputModule,
FormsModule
FormsModule,
MultiSelectModule
]
})
export class MultiEnumEditFieldModule {

View file

@ -28,27 +28,23 @@
<div class="d-flex">
<ng-container *ngIf="initModule">
<div class="flex-grow-1">
<tag-input #tag
(keyup.enter)="selectFirstElement()"
(onAdd)="onAdd($event)"
(onBlur)="resetStatus()"
(onRemove)="onRemove()"
[(ngModel)]="selectedValues"
[class]="getInvalidClass()"
[clearOnBlur]="true"
[displayBy]="getRelateFieldName()"
[inputClass]="getInvalidClass()"
[onTextChangeDebounce]="500"
[onlyFromAutocomplete]="true"
[placeholder]="getPlaceholderLabel()"
[secondaryPlaceholder]="getPlaceholderLabel()"
maxItems="1">
<tag-input-dropdown [autocompleteObservable]="search"
[displayBy]="getRelateFieldName()"
[keepOpen]="false"
[showDropdownIfEmpty]="true">
</tag-input-dropdown>
</tag-input>
<p-multiSelect
#tag
[options]="options"
[(ngModel)]="selectedValues"
[optionLabel]="getRelateFieldName()"
(onChange)="onAdd($event.value[0])"
(onBlur)="resetStatus()"
(onRemove)="onRemove()"
(onFilter)="onFilter($event)"
[virtualScroll]="true"
[virtualScrollItemSize]="40"
[placeholder]="placeholderLabel"
[emptyFilterMessage]="emptyFilterLabel"
[selectionLimit]="1"
[styleClass]="getInvalidClass()"
>
</p-multiSelect>
</div>
<div class="relate-btn">
<scrm-button [config]="selectButton">

View file

@ -25,7 +25,6 @@
*/
import {Component, ViewChild} from '@angular/core';
import {TagInputComponent} from 'ngx-chips';
import {ButtonInterface, Field, Record} from 'common';
import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
import {ModuleNameMapper} from '../../../../services/navigation/module-name-mapper/module-name-mapper.service';
@ -35,9 +34,10 @@ import {BaseRelateComponent} from '../../../base/base-relate.component';
import {LanguageStore} from '../../../../store/language/language.store';
import {RelateService} from '../../../../services/record/relate/relate.service';
import {RecordListModalResult} from '../../../../containers/record-list-modal/components/record-list-modal/record-list-modal.model';
import {TagModel} from 'ngx-chips/core/tag-model';
import {FieldLogicManager} from '../../../field-logic/field-logic.manager';
import {FieldLogicDisplayManager} from '../../../field-logic-display/field-logic-display.manager';
import {map, take} from "rxjs/operators";
import {MultiSelect} from "primeng/multiselect";
@Component({
selector: 'scrm-relate-edit',
@ -46,10 +46,13 @@ import {FieldLogicDisplayManager} from '../../../field-logic-display/field-logic
providers: [RelateService]
})
export class RelateEditFieldComponent extends BaseRelateComponent {
@ViewChild('tag') tag: TagInputComponent;
@ViewChild('tag') tag: MultiSelect;
selectButton: ButtonInterface;
idField: Field;
placeholderLabel: string = '';
emptyFilterLabel: string = '';
/**
* Constructor
*
@ -113,9 +116,8 @@ export class RelateEditFieldComponent extends BaseRelateComponent {
this.field.formControl.setValue('');
return;
}
this.selectedValues = [];
this.selectedValues.push(this.field.valueObject);
this.selectedValues = [this.field.valueObject];
this.options = [this.field.valueObject];
}
/**
@ -124,8 +126,7 @@ export class RelateEditFieldComponent extends BaseRelateComponent {
* @param {object} item added
*/
onAdd(item): void {
if (item) {
if(item) {
const relateName = this.getRelateFieldName();
this.setValue(item.id, item[relateName]);
return;
@ -143,10 +144,23 @@ export class RelateEditFieldComponent extends BaseRelateComponent {
onRemove(): void {
this.setValue('', '');
this.selectedValues = [];
this.options = [];
}
setTimeout(() => {
this.tag.focus(true, true);
}, 200);
onFilter($event): void {
this.onRemove();
const relateName = this.getRelateFieldName();
let term = $event.filter;
this.search(term).pipe(
take(1),
map(data => data.filter(item => item[relateName] !== '')),
map(filteredData => filteredData.map(item => ({
id: item.id,
[relateName]: item[relateName]
})))
).subscribe(filteredOptions => {
this.options = filteredOptions;
})
}
/**
@ -167,6 +181,13 @@ export class RelateEditFieldComponent extends BaseRelateComponent {
this.idField.formControl.setValue(id);
this.idField.formControl.markAsDirty();
}
if(relateValue) {
const relateName = this.getRelateFieldName();
this.selectedValues = [{ id: id, [relateName]: relateValue }];
}
this.options = this.selectedValues;
}
/**
@ -223,14 +244,8 @@ export class RelateEditFieldComponent extends BaseRelateComponent {
this.onAdd(record.attributes);
}
public selectFirstElement(): void {
const filteredElements: TagModel = this.tag.dropdown.items;
if (filteredElements.length !== 0) {
const firstElement = filteredElements[0];
this.selectedValues.push(firstElement);
this.onAdd(firstElement);
this.tag.dropdown.hide();
}
public getTranslatedLabels(): void {
this.placeholderLabel = this.languages.getAppString('LBL_SELECT_ITEM') || '';
this.emptyFilterLabel = this.languages.getAppString('ERR_SEARCH_NO_RESULTS') || '';
}
}

View file

@ -27,22 +27,20 @@
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {RelateEditFieldComponent} from './relate.component';
import {TagInputModule} from 'ngx-chips';
import {FormsModule} from '@angular/forms';
import {InlineLoadingSpinnerModule} from '../../../../components/inline-loading-spinner/inline-loading-spinner.module';
import {ButtonModule} from '../../../../components/button/button.module';
import {LabelModule} from '../../../../components/label/label.module';
import {MultiSelectModule} from "primeng/multiselect";
@NgModule({
declarations: [RelateEditFieldComponent],
exports: [RelateEditFieldComponent],
imports: [
CommonModule,
TagInputModule,
LabelModule,
FormsModule,
InlineLoadingSpinnerModule,
ButtonModule
ButtonModule,
MultiSelectModule
]
})
export class RelateEditFieldModule {

View file

@ -0,0 +1,54 @@
/**
* SuiteCRM is a customer relationship management program developed by SalesAgility Ltd.
* Copyright (C) 2023 SalesAgility Ltd.
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License version 3 as published by the
* Free Software Foundation with the addition of the following permission added
* to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
* IN WHICH THE COPYRIGHT IS OWNED BY SALESAGILITY, SALESAGILITY DISCLAIMS THE
* WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
*
* 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
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* In accordance with Section 7(b) of the GNU Affero General Public License
* version 3, these Appropriate Legal Notices must retain the display of the
* "Supercharged by SuiteCRM" logo. If the display of the logos is not reasonably
* feasible for technical reasons, the Appropriate Legal Notices must display
* the words "Supercharged by SuiteCRM".
*/
/* --------- MESSAGE SECTION ---------- */
.p-multiselect {
width: 100%;
height: calc(1.5em + 0.5rem + 1px);
.p-multiselect-label {
font-size: 0.85rem;
padding: 0.35rem 0.48rem;
max-width: 400px;
}
.p-multiselect-panel {
.p-inputtext {
padding: 0.3rem 0.6rem;
}
.p-multiselect-close {
padding: 0.6em;
}
ul.p-multiselect-items {
padding-inline-start: 0em;
}
}
}

View file

@ -80,6 +80,7 @@
@import 'components/action-group-menu';
@import 'components/popover';
@import 'components/popover-mobile';
@import 'components/multiselect';
@import 'layout/panel';
@import 'layout/app';

View file

@ -68,6 +68,7 @@
"ngx-chips": "^3.0.0",
"nyc": "~15.1.0",
"object-hash": "^3.0.0",
"primeng": "^16.9.1",
"rxjs": "^7.8.1",
"tinymce": "^6.4.2",
"tslib": "^2.5.2",

View file

@ -7911,6 +7911,13 @@ pretty-format@^29.7.0:
ansi-styles "^5.0.0"
react-is "^18.0.0"
primeng@^16.9.1:
version "16.9.1"
resolved "https://registry.yarnpkg.com/primeng/-/primeng-16.9.1.tgz#50ee3ed304633c1c5a8ea155f6c0c8be9de7b85c"
integrity sha512-dCkvKoV62xDEyeGaEm8B0qncdKWNz4mDU/Camnr27AafaDNGV0lCA29imgJvZPEmJTmC2wDhtedg7Syfwz4UqA==
dependencies:
tslib "^2.3.0"
proc-log@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/proc-log/-/proc-log-3.0.0.tgz#fb05ef83ccd64fd7b20bbe9c8c1070fc08338dd8"