mirror of
https://github.com/SuiteCRM/SuiteCRM-Core.git
synced 2025-09-02 08:09:19 +08:00
Add Feature Query List View Filter
Now you can use legacy query filter in list view pages
This commit is contained in:
parent
a76dfef14f
commit
7cbf033208
19 changed files with 843 additions and 35 deletions
|
@ -35,6 +35,7 @@ services:
|
|||
$listViewTableActions: '%module.listview.table_action%'
|
||||
$listViewLineActionsLimits: '%module.listview.line_actions_limits%'
|
||||
$listViewSidebarWidgets: '%module.listview.sidebar_widgets%'
|
||||
$listViewUrlQueryFilterMapping: '%module.listview.url_query_filter_mapping%'
|
||||
$listViewColumnLimits: '%module.listview.column_limits%'
|
||||
$listViewSettingsLimits: '%module.listview.settings_limits%'
|
||||
$listViewActionsLimits: '%module.listview.actions_limits%'
|
||||
|
|
|
@ -32,6 +32,7 @@ parameters:
|
|||
listview_settings_limits: true
|
||||
listview_actions_limits: true
|
||||
listview_line_actions_limits: true
|
||||
listview_url_query_filter_mapping: true
|
||||
module_routing: true
|
||||
recordview_actions_limits: true
|
||||
subpanelview_actions_limits: true
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
parameters:
|
||||
module.listview.url_query_filter_mapping:
|
||||
'{field}_range_choice':
|
||||
not_equal:
|
||||
'range_{field}': 'target'
|
||||
between:
|
||||
'start_range_{field}': 'start'
|
||||
'end_range_{field}': 'end'
|
||||
'greater_than':
|
||||
'range_{field}': 'target'
|
||||
'less_than':
|
||||
'range_{field}': 'target'
|
||||
'last_7_days':
|
||||
'{field}_range_choice': 'operator'
|
||||
'next_7_days':
|
||||
'{field}_range_choice': 'operator'
|
||||
'last_30_days':
|
||||
'{field}_range_choice': 'operator'
|
||||
'next_30_days':
|
||||
'{field}_range_choice': 'operator'
|
||||
'last_month':
|
||||
'{field}_range_choice': 'operator'
|
||||
'this_month':
|
||||
'{field}_range_choice': 'operator'
|
||||
'next_month':
|
||||
'{field}_range_choice': 'operator'
|
||||
'last_year':
|
||||
'{field}_range_choice': 'operator'
|
||||
'this_year':
|
||||
'{field}_range_choice': 'operator'
|
||||
'next_year':
|
||||
'{field}_range_choice': 'operator'
|
||||
'{field}_filter_type':
|
||||
equal:
|
||||
'{field}_filter_value': 'target'
|
||||
not_equal:
|
||||
'{field}_filter_value': 'target'
|
||||
between:
|
||||
'{field}_filter_start': 'start'
|
||||
'{field}_filter_end': 'end'
|
||||
'greater_than':
|
||||
'{field}_filter_start': 'target'
|
||||
'less_than':
|
||||
'{field}_filter_end': 'target'
|
||||
'last_7_days':
|
||||
'{field}_filter_type': 'operator'
|
||||
'next_7_days':
|
||||
'{field}_filter_type': 'operator'
|
||||
'last_30_days':
|
||||
'{field}_filter_type': 'operator'
|
||||
'next_30_days':
|
||||
'{field}_filter_type': 'operator'
|
||||
'last_month':
|
||||
'{field}_filter_type': 'operator'
|
||||
'this_month':
|
||||
'{field}_filter_type': 'operator'
|
||||
'next_month':
|
||||
'{field}_filter_type': 'operator'
|
||||
'last_year':
|
||||
'{field}_filter_type': 'operator'
|
||||
'this_year':
|
||||
'{field}_filter_type': 'operator'
|
||||
'next_year':
|
||||
'{field}_filter_type': 'operator'
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
import {ViewFieldDefinition} from './metadata.model';
|
||||
import {WidgetMetadata} from './widget.metadata';
|
||||
import {FieldDefinition} from '../record/field.model';
|
||||
import {DisplayType,FieldDefinition} from '../record/field.model';
|
||||
import {BulkActionsMap} from '../actions/bulk-action.model';
|
||||
import {ChartTypesMap} from '../containers/chart/chart.model';
|
||||
import {Action} from '../actions/action.model';
|
||||
|
@ -65,6 +65,9 @@ export interface ColumnDefinition extends ViewFieldDefinition {
|
|||
|
||||
export interface SearchMetaField {
|
||||
name?: string;
|
||||
vardefBased?: boolean;
|
||||
readonly?: boolean;
|
||||
display?: DisplayType;
|
||||
type?: string;
|
||||
label?: string;
|
||||
default?: boolean;
|
||||
|
|
|
@ -157,6 +157,8 @@ export interface AttributeDependency {
|
|||
types: string[];
|
||||
}
|
||||
|
||||
export type FieldSource = 'field' | 'attribute' | 'item' | 'groupField';
|
||||
|
||||
export interface Field {
|
||||
type: string;
|
||||
value?: string;
|
||||
|
@ -164,6 +166,7 @@ export interface Field {
|
|||
valueObject?: any;
|
||||
valueObjectArray?: ObjectMap[];
|
||||
name?: string;
|
||||
vardefBased?: boolean;
|
||||
label?: string;
|
||||
labelKey?: string;
|
||||
loading?: boolean;
|
||||
|
@ -174,7 +177,7 @@ export interface Field {
|
|||
readonly?: boolean;
|
||||
display?: DisplayType;
|
||||
defaultDisplay?: string;
|
||||
source?: 'field' | 'attribute' | 'item';
|
||||
source?: FieldSource;
|
||||
valueSource?: 'value' | 'valueList' | 'valueObject' | 'criteria';
|
||||
metadata?: FieldMetadata;
|
||||
definition?: FieldDefinition;
|
||||
|
@ -195,13 +198,14 @@ export interface Field {
|
|||
export class BaseField implements Field {
|
||||
type: string;
|
||||
name?: string;
|
||||
vardefBased?: boolean;
|
||||
label?: string;
|
||||
labelKey?: string;
|
||||
dynamicLabelKey?: string;
|
||||
readonly?: boolean;
|
||||
display?: DisplayType;
|
||||
defaultDisplay?: string;
|
||||
source?: 'field' | 'attribute';
|
||||
source?: FieldSource;
|
||||
metadata?: FieldMetadata;
|
||||
definition?: FieldDefinition;
|
||||
criteria?: SearchCriteriaFieldFilter;
|
||||
|
|
|
@ -31,6 +31,7 @@ export interface SearchCriteriaFieldFilter {
|
|||
fieldType?: string;
|
||||
operator: string;
|
||||
values?: string[];
|
||||
target?: string;
|
||||
start?: string;
|
||||
end?: string;
|
||||
valueObjectArray?: ObjectMap[];
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
* the words "Supercharged by SuiteCRM".
|
||||
*/
|
||||
|
||||
import {isEmpty} from 'lodash-es';
|
||||
import {Injectable} from '@angular/core';
|
||||
import {StateStore} from '../../../../store/state';
|
||||
import {
|
||||
|
@ -33,7 +34,8 @@ import {
|
|||
Field,
|
||||
isVoid,
|
||||
SearchMetaFieldMap,
|
||||
ViewMode
|
||||
ViewMode,
|
||||
SearchCriteriaFieldFilter
|
||||
} from 'common';
|
||||
import {map, take, tap} from 'rxjs/operators';
|
||||
import {BehaviorSubject, combineLatestWith, Observable, Subscription} from 'rxjs';
|
||||
|
@ -219,15 +221,29 @@ export class ListFilterStore implements StateStore {
|
|||
return;
|
||||
}
|
||||
|
||||
Object.keys(savedFilter.criteriaFields).forEach(key => {
|
||||
const field = savedFilter.criteriaFields[key];
|
||||
Object.entries(savedFilter.criteriaFields).forEach(([key, field]) => {
|
||||
const name = field.name || key;
|
||||
|
||||
if (name.includes('_only')) {
|
||||
this.special.push(field);
|
||||
} else {
|
||||
this.fields.push(field);
|
||||
return;
|
||||
}
|
||||
|
||||
if (field.vardefBased) {
|
||||
const filters = savedFilter?.criteria?.filters ?? {};
|
||||
const fieldFilter = (
|
||||
filters[key] ?? {}
|
||||
) as SearchCriteriaFieldFilter;
|
||||
|
||||
if (
|
||||
!isEmpty(fieldFilter?.operator)
|
||||
&& field.display === 'none'
|
||||
) {
|
||||
field.display = 'default';
|
||||
}
|
||||
}
|
||||
|
||||
this.fields.push(field);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -246,9 +262,12 @@ export class ListFilterStore implements StateStore {
|
|||
const fields = [];
|
||||
this.fields.forEach(field => {
|
||||
const name = field.name;
|
||||
if (!this.searchFields[name]) {
|
||||
if (field.display === 'none' || field.source === 'groupField') {
|
||||
return;
|
||||
}
|
||||
if (!this.searchFields[name]) {
|
||||
field.readonly = true;
|
||||
}
|
||||
fields.push(field);
|
||||
});
|
||||
|
||||
|
|
|
@ -290,6 +290,9 @@ export class SavedFilterRecordStore extends RecordStore {
|
|||
const definition = {
|
||||
name: fieldMeta.name,
|
||||
label: fieldMeta.label,
|
||||
vardefBased: fieldMeta?.vardefBased ?? false,
|
||||
readonly: fieldMeta?.readonly ?? false,
|
||||
display: fieldMeta?.display ?? '',
|
||||
type,
|
||||
fieldDefinition: {}
|
||||
} as ViewFieldDefinition;
|
||||
|
|
|
@ -165,6 +165,11 @@ export class BaseEnumComponent extends BaseFieldComponent implements OnInit, OnD
|
|||
|
||||
this.selectedValues = [];
|
||||
|
||||
if (this.field.criteria){
|
||||
this.initValueLabel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.field.value) {
|
||||
this.initEnumDefault();
|
||||
return;
|
||||
|
@ -182,10 +187,15 @@ export class BaseEnumComponent extends BaseFieldComponent implements OnInit, OnD
|
|||
return;
|
||||
}
|
||||
|
||||
if (this.field.value) {
|
||||
this.valueLabel = this.optionsMap[this.field.value];
|
||||
this.initValueLabel();
|
||||
}
|
||||
|
||||
protected initValueLabel () {
|
||||
const fieldValue = this.field.value || this.field.criteria?.target || undefined;
|
||||
if (fieldValue !== undefined) {
|
||||
this.valueLabel = this.optionsMap[fieldValue];
|
||||
this.selectedValues.push({
|
||||
value: this.field.value,
|
||||
value: fieldValue,
|
||||
label: this.valueLabel
|
||||
});
|
||||
}
|
||||
|
|
|
@ -59,7 +59,7 @@ export class FieldComponent implements OnInit {
|
|||
mode = 'edit';
|
||||
}
|
||||
|
||||
if (mode === 'edit' && this.field.readonly) {
|
||||
if (['edit', 'filter'].includes(mode) && this.field.readonly) {
|
||||
mode = 'detail';
|
||||
}
|
||||
|
||||
|
|
|
@ -177,6 +177,7 @@ export class FieldBuilder {
|
|||
|
||||
field.type = viewField.type || definition.type;
|
||||
field.name = viewField.name || definition.name || '';
|
||||
field.vardefBased = viewField?.vardefBased ?? definition?.vardefBased ?? false;
|
||||
field.readonly = isTrue(viewField.readonly) || isTrue(definition.readonly) || false;
|
||||
field.display = (viewField.display || definition.display || 'default') as DisplayType;
|
||||
field.defaultDisplay = field.display;
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
* the words "Supercharged by SuiteCRM".
|
||||
*/
|
||||
|
||||
import {isEmpty} from 'lodash-es';
|
||||
import {Field, FieldDefinition, Record, ViewFieldDefinition} from 'common';
|
||||
import {LanguageStore} from '../../../store/language/language.store';
|
||||
import {Injectable} from '@angular/core';
|
||||
|
@ -121,6 +122,9 @@ export class FieldManager {
|
|||
* @returns {object}Field
|
||||
*/
|
||||
public addFilterField(record: SavedFilter, viewField: ViewFieldDefinition, language: LanguageStore = null): Field {
|
||||
if (viewField.vardefBased && !isEmpty(record.criteriaFields[viewField.name])) {
|
||||
return record.criteriaFields[viewField.name];
|
||||
}
|
||||
|
||||
const field = this.filterFieldBuilder.buildFilterField(record, viewField, language);
|
||||
|
||||
|
|
|
@ -132,7 +132,8 @@ export class FilterFieldBuilder extends FieldBuilder {
|
|||
* @returns {boolean} isInitialized
|
||||
*/
|
||||
public isCriteriaFieldInitialized(record: SavedFilter, fieldName: string): boolean {
|
||||
return !!record.criteriaFields[fieldName];
|
||||
const criteriaField = record.criteriaFields[fieldName];
|
||||
return !!criteriaField && !criteriaField.vardefBased;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -86,6 +86,7 @@ export class GroupFieldBuilder extends FieldBuilder {
|
|||
};
|
||||
|
||||
const groupField = buildFieldFunction(record, groupViewField, language);
|
||||
groupField.source = 'groupField';
|
||||
addRecordFunction(record, fieldDefinition.name, groupField);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,571 @@
|
|||
/**
|
||||
* SuiteCRM is a customer relationship management program developed by SalesAgility Ltd.
|
||||
* Copyright (C) 2024 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".
|
||||
*/
|
||||
|
||||
import { isArray, isEmpty } from 'lodash-es';
|
||||
import { DateTime } from 'luxon';
|
||||
import {
|
||||
FieldDefinitionMap,
|
||||
isEmptyString,
|
||||
SearchCriteria,
|
||||
SearchCriteriaFieldFilter
|
||||
} from 'common';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Params } from '@angular/router';
|
||||
import { SavedFilter } from '../../../store/saved-filters/saved-filter.model';
|
||||
import { MetadataStore } from '../../../store/metadata/metadata.store.service';
|
||||
import { SystemConfigStore } from '../../../store/system-config/system-config.store';
|
||||
import { DataTypeFormatter } from '../../../services/formatters/data-type.formatter.service';
|
||||
|
||||
type GenericMap<T> = { [key: string]: T };
|
||||
type NestedGenericMap<T> = GenericMap<GenericMap<T>>;
|
||||
type DoubleNestedGenericMap<T> = GenericMap<NestedGenericMap<T>>;
|
||||
|
||||
const MONTH_YEAR_REGEX = new RegExp('^(\\d{4})-(0[1-9]|1[0-2])$');
|
||||
const MONTH_REGEX = new RegExp('^(\\d{4})$');
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ListViewUrlQueryService {
|
||||
|
||||
/**
|
||||
* Array of allowed properties to be set to the searchCriteriaFieldFilter from url_query_filter_mapping
|
||||
*/
|
||||
private allowedProperties = [
|
||||
'operator',
|
||||
'target',
|
||||
'values',
|
||||
'start',
|
||||
'end'
|
||||
];
|
||||
|
||||
/**
|
||||
* An array containing properties that can be converted into dbFormat.
|
||||
*/
|
||||
private convertableProperties = [
|
||||
'target',
|
||||
'values',
|
||||
'start',
|
||||
'end'
|
||||
];
|
||||
|
||||
constructor (
|
||||
protected systemConfig: SystemConfigStore,
|
||||
protected metadataStore: MetadataStore,
|
||||
protected dataTypeFormatter: DataTypeFormatter
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a URL query-based filter.
|
||||
*
|
||||
* @param {string} module - The module name.
|
||||
* @param {SavedFilter} defaultFilter - The default filter.
|
||||
* @param {Params} rawQueryParams - The raw query parameters.
|
||||
* @returns {SavedFilter|null} - The built URL query-based filter, or null if no filter criteria are found.
|
||||
*/
|
||||
public buildUrlQueryBasedFilter (
|
||||
module: string,
|
||||
defaultFilter: SavedFilter,
|
||||
rawQueryParams: Params
|
||||
): SavedFilter | null {
|
||||
const filterFieldDefinitions = this.metadataStore.get().recordView.vardefs;
|
||||
|
||||
const queryParams = Object.entries(rawQueryParams)
|
||||
.reduce((acc, [queryParamKey, queryParamVal]) => {
|
||||
const [cleanQueryParamKey, cleanQueryParamVal] = this.cleanQueryParam([
|
||||
queryParamKey,
|
||||
queryParamVal]);
|
||||
acc[cleanQueryParamKey] = cleanQueryParamVal;
|
||||
return acc;
|
||||
}, {} as Params);
|
||||
|
||||
const filterCriteria: SearchCriteria = this.getQueryFilterCriteria(
|
||||
filterFieldDefinitions,
|
||||
module,
|
||||
queryParams
|
||||
);
|
||||
|
||||
if (isEmpty(filterCriteria.filters)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
key: 'default',
|
||||
searchModule: module,
|
||||
module: 'saved-search',
|
||||
criteria: filterCriteria
|
||||
} as SavedFilter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the query filter criteria based on the provided field definitions map, module, and query parameters.
|
||||
*
|
||||
* @param {FieldDefinitionMap} fieldDefinitionMap - The field definition map.
|
||||
* @param {string} module - The module name.
|
||||
* @param {Params} queryParams - The query parameters.
|
||||
* @returns {SearchCriteria} - The generated search criteria.
|
||||
* @protected
|
||||
*/
|
||||
protected getQueryFilterCriteria (
|
||||
fieldDefinitionMap: FieldDefinitionMap,
|
||||
module: string,
|
||||
queryParams: Params
|
||||
): SearchCriteria {
|
||||
const criteria: SearchCriteria = {
|
||||
name: 'default',
|
||||
filters: {}
|
||||
} as SearchCriteria;
|
||||
|
||||
const queryParamsKeys = Object.keys(queryParams);
|
||||
const fieldDefinitions = Object.values(fieldDefinitionMap)
|
||||
.filter(({ name }) => queryParamsKeys.some(qPKey => qPKey.includes(name)));
|
||||
|
||||
const listviewUrlQueryFilterMapping = this.systemConfig.getConfigValue(
|
||||
'listview_url_query_filter_mapping'
|
||||
) as DoubleNestedGenericMap<string>;
|
||||
const listviewUrlQueryFilterMappingEntries = Object.entries(listviewUrlQueryFilterMapping);
|
||||
listviewUrlQueryFilterMappingEntries.push(['', {}]);
|
||||
|
||||
let searchType;
|
||||
switch (queryParams['searchFormTab']) {
|
||||
case 'basic_search':
|
||||
searchType = 'basic';
|
||||
break;
|
||||
case 'advanced_search':
|
||||
searchType = 'advanced';
|
||||
break;
|
||||
default:
|
||||
searchType = 'advanced';
|
||||
}
|
||||
|
||||
for (const fieldDefinition of fieldDefinitions) {
|
||||
const fieldFilterName = fieldDefinition.name;
|
||||
const fieldFilterKeys = [
|
||||
fieldFilterName,
|
||||
`${fieldFilterName}_${searchType}`
|
||||
];
|
||||
|
||||
for (const [queryFilterOperatorKeyTemplate, queryFilterOperatorParamsMap] of listviewUrlQueryFilterMappingEntries) {
|
||||
if (!isEmpty(criteria.filters[fieldFilterName])) {
|
||||
break;
|
||||
}
|
||||
|
||||
for (const fieldFilterKey of fieldFilterKeys) {
|
||||
if (!isEmpty(criteria.filters[fieldFilterName])) {
|
||||
break;
|
||||
}
|
||||
|
||||
const searchCriteriaFieldFilter = this.buildSearchCriteriaFieldFilter(
|
||||
fieldFilterName,
|
||||
fieldDefinition.type,
|
||||
queryParams,
|
||||
fieldFilterKey,
|
||||
queryFilterOperatorKeyTemplate,
|
||||
queryFilterOperatorParamsMap
|
||||
);
|
||||
|
||||
if (isEmpty(searchCriteriaFieldFilter)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
this.convertableProperties.forEach((convertableProperty) => {
|
||||
if (!searchCriteriaFieldFilter[convertableProperty]) {
|
||||
return;
|
||||
}
|
||||
|
||||
let internalFormatValue;
|
||||
if (isArray(searchCriteriaFieldFilter[convertableProperty])) {
|
||||
internalFormatValue = searchCriteriaFieldFilter[convertableProperty].map(
|
||||
prop => this.toInternalFormat(
|
||||
fieldDefinition.type,
|
||||
prop
|
||||
));
|
||||
} else {
|
||||
internalFormatValue = this.toInternalFormat(
|
||||
fieldDefinition.type,
|
||||
searchCriteriaFieldFilter[convertableProperty]
|
||||
);
|
||||
}
|
||||
|
||||
searchCriteriaFieldFilter[convertableProperty] = internalFormatValue;
|
||||
});
|
||||
} catch (e) {
|
||||
continue;
|
||||
}
|
||||
|
||||
criteria.filters[fieldFilterName] = searchCriteriaFieldFilter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return criteria;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a search criteria field filter object based on the provided parameters.
|
||||
*
|
||||
* @param {string} fieldFilterName - The name of the field filter.
|
||||
* @param {string} fieldFilterFieldType - The type of the field filter.
|
||||
* @param {Params} queryParams - The query parameters.
|
||||
* @param {string} fieldFilterKey - The key of the field filter in the query parameters.
|
||||
* @param {string} queryFilterOperatorKeyTemplate - The template for the query filter operator key.
|
||||
* @param {NestedGenericMap<string>} queryFilterOperatorParamsMap - The map of query filter operator keys to their respective parameter maps.
|
||||
* @returns {SearchCriteriaFieldFilter | null} The built search criteria field filter object.
|
||||
* @protected
|
||||
*/
|
||||
protected buildSearchCriteriaFieldFilter (
|
||||
fieldFilterName: string,
|
||||
fieldFilterFieldType: string,
|
||||
queryParams: Params,
|
||||
fieldFilterKey: string,
|
||||
queryFilterOperatorKeyTemplate: string,
|
||||
queryFilterOperatorParamsMap: NestedGenericMap<string>
|
||||
): SearchCriteriaFieldFilter | null {
|
||||
const searchCriteriaFieldFilter = {
|
||||
field: fieldFilterName,
|
||||
fieldType: fieldFilterFieldType,
|
||||
operator: '=',
|
||||
values: []
|
||||
} as SearchCriteriaFieldFilter;
|
||||
|
||||
if (isEmpty(queryFilterOperatorKeyTemplate) || isEmpty(queryFilterOperatorParamsMap)) {
|
||||
const fieldFilterValue = this.getQueryParamValue(
|
||||
fieldFilterKey,
|
||||
fieldFilterKey,
|
||||
queryParams
|
||||
);
|
||||
if (isEmpty(fieldFilterValue) && !isEmptyString(fieldFilterValue)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const values = isArray(fieldFilterValue)
|
||||
? fieldFilterValue
|
||||
: [fieldFilterValue];
|
||||
|
||||
searchCriteriaFieldFilter.values = values;
|
||||
searchCriteriaFieldFilter.target = values[0];
|
||||
|
||||
return this.checkDateSpecialsOrReturn(
|
||||
searchCriteriaFieldFilter,
|
||||
searchCriteriaFieldFilter.target
|
||||
);
|
||||
}
|
||||
|
||||
const queryFilterOperatorKey = this.getQueryParamValue(
|
||||
queryFilterOperatorKeyTemplate,
|
||||
fieldFilterKey,
|
||||
queryParams,
|
||||
{ forceSingleString: true }
|
||||
) as string;
|
||||
const queryFilterOperatorParams = (
|
||||
queryFilterOperatorParamsMap[queryFilterOperatorKey] ??
|
||||
Object
|
||||
.values(queryFilterOperatorParamsMap)
|
||||
.reduce((prev, curr) => (
|
||||
{ ...prev, ...curr }
|
||||
), {})
|
||||
?? {}
|
||||
) as GenericMap<string>;
|
||||
if (isEmpty(queryFilterOperatorParams)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let returnEmpty = true;
|
||||
searchCriteriaFieldFilter.operator = queryFilterOperatorKey;
|
||||
Object.entries(queryFilterOperatorParams)
|
||||
.filter(([_, searchCriteriaPropertyKey]) => (
|
||||
typeof searchCriteriaPropertyKey === 'string'
|
||||
&& this.allowedProperties.includes(searchCriteriaPropertyKey)
|
||||
))
|
||||
.forEach(([searchCriteriaPropertyValueTemplate, searchCriteriaPropertyKey]) => {
|
||||
const rawSearchCriteriaPropertyValue = this.getQueryParamValue(
|
||||
searchCriteriaPropertyValueTemplate,
|
||||
fieldFilterKey,
|
||||
queryParams
|
||||
);
|
||||
|
||||
if (isEmpty(rawSearchCriteriaPropertyValue)) {
|
||||
return;
|
||||
}
|
||||
returnEmpty = false;
|
||||
|
||||
let searchCriteriaPropertyValue = rawSearchCriteriaPropertyValue;
|
||||
|
||||
if (searchCriteriaPropertyKey === 'values') {
|
||||
if (!isArray(searchCriteriaPropertyValue)) {
|
||||
searchCriteriaPropertyValue = [searchCriteriaPropertyValue];
|
||||
}
|
||||
|
||||
searchCriteriaFieldFilter['target'] = searchCriteriaPropertyValue[0];
|
||||
} else if (searchCriteriaPropertyKey === 'target') {
|
||||
if (isArray(searchCriteriaPropertyValue)) {
|
||||
searchCriteriaPropertyValue = searchCriteriaPropertyValue[0];
|
||||
}
|
||||
|
||||
searchCriteriaFieldFilter['values'] = [searchCriteriaPropertyValue] as string[];
|
||||
}
|
||||
|
||||
searchCriteriaFieldFilter[searchCriteriaPropertyKey] = searchCriteriaPropertyValue;
|
||||
|
||||
if (!isArray(searchCriteriaPropertyValue)) {
|
||||
this.checkDateSpecialsOrReturn(
|
||||
searchCriteriaFieldFilter,
|
||||
searchCriteriaPropertyValue,
|
||||
{
|
||||
operator: queryFilterOperatorKey,
|
||||
key: searchCriteriaPropertyKey
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return !returnEmpty ? this.checkForMissingOperator(searchCriteriaFieldFilter) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the value of a query parameter based on the provided queryParamKeyTemplate,
|
||||
* fieldFilterKey, and queryParams.
|
||||
*
|
||||
* @param {string} queryParamKeyTemplate - The template for the query parameter key, with "{field}" as a placeholder for fieldFilterKey.
|
||||
* @param {string} fieldFilterKey - The field filter key used to replace the "{field}" placeholder in queryParamKeyTemplate.
|
||||
* @param {Params} queryParams - The object containing the query parameters.
|
||||
* @param {object} options - Optional parameters to customize the behavior of the method.
|
||||
* @param {boolean} options.forceSingleString - Flag indicating whether the result should always be a single string value.
|
||||
* @returns {string|string[]} - The value of the query parameter. If forceSingleString is false, it will be either a string or an array of strings.
|
||||
* @protected
|
||||
*/
|
||||
protected getQueryParamValue (
|
||||
queryParamKeyTemplate: string,
|
||||
fieldFilterKey: string,
|
||||
queryParams: Params,
|
||||
{ forceSingleString = false } = {}
|
||||
): string | string[] | null {
|
||||
const queryParamKey = queryParamKeyTemplate.replace(
|
||||
'{field}',
|
||||
fieldFilterKey
|
||||
) ?? '';
|
||||
|
||||
let queryParamValue = queryParams[queryParamKey];
|
||||
|
||||
if (!queryParamValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isArray(queryParamValue)) {
|
||||
queryParamValue = queryParamValue.map(this.transform);
|
||||
} else {
|
||||
queryParamValue = this.transform(queryParamValue);
|
||||
}
|
||||
|
||||
if (forceSingleString && isArray(queryParamValue)) {
|
||||
return queryParamValue[0] ?? '';
|
||||
}
|
||||
|
||||
return queryParamValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans the query parameter key by removing the '[]' brackets if present.
|
||||
*
|
||||
* @returns {string} - The cleaned query parameter key.
|
||||
* @protected
|
||||
* @param queryParam
|
||||
*/
|
||||
protected cleanQueryParam (queryParam: [string, string | string[]]): [string, string | string[]] {
|
||||
let [queryParamKey, queryParamVal] = queryParam;
|
||||
|
||||
const queryParamKeyReversed = queryParamKey.split('').reverse().join('');
|
||||
if (queryParamKeyReversed.indexOf('][') === 0 && typeof queryParamVal === 'string') {
|
||||
queryParamKey = queryParamKey.replace('[]', '');
|
||||
queryParamVal = queryParamVal.split(',');
|
||||
}
|
||||
|
||||
return [queryParamKey, queryParamVal];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if given fieldFilterValue matches MONTH_YEAR_REGEX or yearRegex and returns
|
||||
* overridesSearchCriteriaFieldFilter if true, else returns searchCriteriaFieldFilter.
|
||||
*
|
||||
* @param {SearchCriteriaFieldFilter} searchCriteriaFieldFilter - The search criteria field filter.
|
||||
* @param {string} fieldFilterValue - The field filter value.
|
||||
* @param {Object} options - The options object.
|
||||
* @param {string} [options.operator='='] - The range option.
|
||||
* @param {string} [options.key='target'] - The key option.
|
||||
* @returns {SearchCriteriaFieldFilter} - The updated search criteria field filter.
|
||||
* @protected
|
||||
*/
|
||||
protected checkDateSpecialsOrReturn (
|
||||
searchCriteriaFieldFilter: SearchCriteriaFieldFilter,
|
||||
fieldFilterValue: string,
|
||||
{ operator = '=', key = 'target' }: { operator?: string, key?: string } = {}
|
||||
): SearchCriteriaFieldFilter {
|
||||
if (fieldFilterValue.match(MONTH_YEAR_REGEX)) {
|
||||
return this.overridesSearchCriteriaFieldFilter(
|
||||
searchCriteriaFieldFilter,
|
||||
fieldFilterValue,
|
||||
{ type: 'month', operator, key }
|
||||
);
|
||||
}
|
||||
|
||||
if (fieldFilterValue.match(MONTH_REGEX)) {
|
||||
return this.overridesSearchCriteriaFieldFilter(
|
||||
searchCriteriaFieldFilter,
|
||||
fieldFilterValue,
|
||||
{ type: 'year', operator, key }
|
||||
);
|
||||
}
|
||||
|
||||
return searchCriteriaFieldFilter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the search criteria field filter based on the provided parameters.
|
||||
*
|
||||
* @param {SearchCriteriaFieldFilter} searchCriteriaFieldFilter - The original search criteria field filter.
|
||||
* @param {string} fieldFilterValue - The value of the field filter.
|
||||
* @param {Object} options - The options for overriding the field filter.
|
||||
* @param {string} options.type - The type of the field filter.
|
||||
* @param {string} [options.operator='equal'] - The operator for the field filter.
|
||||
* @param {string} [options.key='target'] - The key for the field filter.
|
||||
* @protected
|
||||
* @returns {SearchCriteriaFieldFilter} - The overridden search criteria field filter.
|
||||
*/
|
||||
protected overridesSearchCriteriaFieldFilter (
|
||||
searchCriteriaFieldFilter: SearchCriteriaFieldFilter,
|
||||
fieldFilterValue: string,
|
||||
{ type = '', operator = 'equal', key = 'target' }: {
|
||||
type: string,
|
||||
operator?: string,
|
||||
key?: string
|
||||
}
|
||||
): SearchCriteriaFieldFilter {
|
||||
let plusObject;
|
||||
let fmt;
|
||||
switch (type) {
|
||||
case 'year':
|
||||
plusObject = { year: 1 };
|
||||
fmt = 'yyyy';
|
||||
break;
|
||||
case 'month':
|
||||
plusObject = { month: 1 };
|
||||
fmt = 'yyyy-MM';
|
||||
break;
|
||||
default:
|
||||
return searchCriteriaFieldFilter;
|
||||
}
|
||||
|
||||
const start = DateTime.fromFormat(fieldFilterValue, fmt);
|
||||
const end = start.plus(plusObject).minus({ day: 1 });
|
||||
|
||||
if (key !== 'target') {
|
||||
switch (key) {
|
||||
case 'start':
|
||||
searchCriteriaFieldFilter.start = start.toFormat('yyyy-MM-dd');
|
||||
break;
|
||||
case 'end':
|
||||
searchCriteriaFieldFilter.end = end.toFormat('yyyy-MM-dd');
|
||||
break;
|
||||
}
|
||||
return searchCriteriaFieldFilter;
|
||||
}
|
||||
|
||||
searchCriteriaFieldFilter.operator = operator;
|
||||
switch (operator) {
|
||||
case 'greater_than':
|
||||
case 'greater_than_equals':
|
||||
searchCriteriaFieldFilter.start = start.toFormat('yyyy-MM-dd');
|
||||
searchCriteriaFieldFilter.target = searchCriteriaFieldFilter.start;
|
||||
searchCriteriaFieldFilter.values = [searchCriteriaFieldFilter.target];
|
||||
break;
|
||||
case 'less_than':
|
||||
case 'less_than_equals':
|
||||
searchCriteriaFieldFilter.end = end.toFormat('yyyy-MM-dd');
|
||||
searchCriteriaFieldFilter.target = searchCriteriaFieldFilter.end;
|
||||
searchCriteriaFieldFilter.values = [searchCriteriaFieldFilter.target];
|
||||
break;
|
||||
case 'not_equal':
|
||||
searchCriteriaFieldFilter.start = start.toFormat('yyyy-MM-dd');
|
||||
searchCriteriaFieldFilter.end = end.toFormat('yyyy-MM-dd');
|
||||
searchCriteriaFieldFilter.target = fieldFilterValue;
|
||||
searchCriteriaFieldFilter.values = [fieldFilterValue];
|
||||
break;
|
||||
case 'equal':
|
||||
case 'between':
|
||||
default:
|
||||
searchCriteriaFieldFilter.operator = 'between';
|
||||
searchCriteriaFieldFilter.start = start.toFormat('yyyy-MM-dd');
|
||||
searchCriteriaFieldFilter.end = end.toFormat('yyyy-MM-dd');
|
||||
searchCriteriaFieldFilter.target = '';
|
||||
searchCriteriaFieldFilter.values = [];
|
||||
break;
|
||||
}
|
||||
|
||||
return searchCriteriaFieldFilter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given value to the internal format based on the specified type.
|
||||
*
|
||||
* @param {string} type - The type of value to convert to.
|
||||
* @param {string} value - The value to convert.
|
||||
* @return {string} - The converted value in the internal format.
|
||||
* @protected
|
||||
*/
|
||||
protected toInternalFormat (type: string, value: string): string {
|
||||
if (value.match(MONTH_REGEX) || value.match(MONTH_YEAR_REGEX)) {
|
||||
return value;
|
||||
}
|
||||
return this.dataTypeFormatter.toInternalFormat(type, value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Transforms the given value from url to a value understandable by backend.
|
||||
*
|
||||
* @param {any} value - The value to be transformed.
|
||||
* @protected
|
||||
* @return {string} The transformed value.
|
||||
*/
|
||||
protected transform (value: any): string {
|
||||
switch (value) {
|
||||
case '':
|
||||
return '__SuiteCRMEmptyString__';
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
protected checkForMissingOperator (searchCriteriaFieldFilter: SearchCriteriaFieldFilter): SearchCriteriaFieldFilter {
|
||||
if (
|
||||
!isEmpty(searchCriteriaFieldFilter.start)
|
||||
&& !isEmpty(searchCriteriaFieldFilter.end)
|
||||
) {
|
||||
searchCriteriaFieldFilter.operator = 'between';
|
||||
}
|
||||
|
||||
return searchCriteriaFieldFilter;
|
||||
}
|
||||
}
|
|
@ -24,6 +24,7 @@
|
|||
* the words "Supercharged by SuiteCRM".
|
||||
*/
|
||||
|
||||
import { isArray, isEmpty, union } from 'lodash-es';
|
||||
import {
|
||||
Action,
|
||||
ColumnDefinition,
|
||||
|
@ -37,11 +38,13 @@ import {
|
|||
SelectionStatus,
|
||||
SortDirection,
|
||||
SortingSelection,
|
||||
ViewContext
|
||||
ViewContext,
|
||||
isTrue
|
||||
} from 'common';
|
||||
import {BehaviorSubject, combineLatestWith, Observable, Subscription} from 'rxjs';
|
||||
import {distinctUntilChanged, map, take, tap} from 'rxjs/operators';
|
||||
import {Injectable} from '@angular/core';
|
||||
import { ActivatedRoute, Params } from '@angular/router';
|
||||
import {NavigationStore} from '../../../../store/navigation/navigation.store';
|
||||
import {RecordList, RecordListStore} from '../../../../store/record-list/record-list.store';
|
||||
import {Metadata, MetadataStore} from '../../../../store/metadata/metadata.store.service';
|
||||
|
@ -61,7 +64,7 @@ import {FilterListStoreFactory} from '../../../../store/saved-filters/filter-lis
|
|||
import {ConfirmationModalService} from '../../../../services/modals/confirmation-modal.service';
|
||||
import {RecordPanelMetadata} from '../../../../containers/record-panel/store/record-panel/record-panel.store.model';
|
||||
import {UserPreferenceStore} from '../../../../store/user-preference/user-preference.store';
|
||||
import {isArray, union} from 'lodash-es';
|
||||
import {ListViewUrlQueryService} from '../../services/list-view-url-query.service';
|
||||
|
||||
export interface ListViewData {
|
||||
records: Record[];
|
||||
|
@ -173,6 +176,8 @@ export class ListViewStore extends ViewStore implements StateStore {
|
|||
protected filterListStoreFactory: FilterListStoreFactory,
|
||||
protected confirmation: ConfirmationModalService,
|
||||
protected preferences: UserPreferenceStore,
|
||||
protected route: ActivatedRoute,
|
||||
protected listViewUrlQueryService: ListViewUrlQueryService
|
||||
) {
|
||||
|
||||
super(appStateStore, languageStore, navigationStore, moduleNavigation, metadataStore);
|
||||
|
@ -356,8 +361,19 @@ export class ListViewStore extends ViewStore implements StateStore {
|
|||
sortOrder: this?.metadata?.listView?.sortOrder ?? 'NONE' as SortDirection
|
||||
} as SortingSelection;
|
||||
|
||||
this.loadCurrentFilter(module);
|
||||
this.loadCurrentSort(module);
|
||||
const queryParams = this.route?.snapshot?.queryParams ?? {};
|
||||
let filterType = '';
|
||||
if (isTrue(queryParams['query'])) {
|
||||
filterType = 'query';
|
||||
}
|
||||
switch (filterType) {
|
||||
case 'query':
|
||||
this.loadQueryFilter(module, queryParams);
|
||||
break
|
||||
default:
|
||||
this.loadCurrentFilter(module);
|
||||
this.loadCurrentSort(module);
|
||||
}
|
||||
this.loadCurrentDisplayedColumns();
|
||||
|
||||
return this.load();
|
||||
|
@ -778,6 +794,41 @@ export class ListViewStore extends ViewStore implements StateStore {
|
|||
this.setFilters(activeFiltersPref, false, currentSort);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load current filter
|
||||
* @param module
|
||||
* @param queryParams
|
||||
* @protected
|
||||
*/
|
||||
protected loadQueryFilter (
|
||||
module:string,
|
||||
queryParams: Params
|
||||
): void {
|
||||
const orderBy: string = queryParams['orderBy'] ?? '';
|
||||
const sortOrder: string = queryParams['sortOrder'] ?? '';
|
||||
const direction = this.recordList.mapSortOrder(sortOrder);
|
||||
|
||||
const filter = this.listViewUrlQueryService.buildUrlQueryBasedFilter(
|
||||
module,
|
||||
this.internalState.activeFilters.default,
|
||||
queryParams
|
||||
);
|
||||
if (isEmpty(filter)){
|
||||
return;
|
||||
}
|
||||
|
||||
const filters = { 'default': filter };
|
||||
|
||||
this.updateState({
|
||||
...this.internalState,
|
||||
activeFilters: deepClone(filters),
|
||||
openFilter: deepClone(filter)
|
||||
});
|
||||
|
||||
this.recordList.updateSorting(orderBy, direction, false);
|
||||
this.recordList.updateSearchCriteria(filter.criteria, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load current sorting
|
||||
* @param module
|
||||
|
|
|
@ -130,6 +130,7 @@ class SystemConfigHandler extends LegacyHandler implements SystemConfigProviderI
|
|||
array $recordViewActionLimits,
|
||||
array $subpanelViewActionLimits,
|
||||
array $listViewLineActionsLimits,
|
||||
array $listViewUrlQueryFilterMapping,
|
||||
array $uiConfigs,
|
||||
array $notificationsConfigs,
|
||||
array $notificationsReloadActions,
|
||||
|
@ -161,6 +162,7 @@ class SystemConfigHandler extends LegacyHandler implements SystemConfigProviderI
|
|||
$this->injectedSystemConfigs['recordview_actions_limits'] = $recordViewActionLimits;
|
||||
$this->injectedSystemConfigs['subpanelview_actions_limits'] = $subpanelViewActionLimits;
|
||||
$this->injectedSystemConfigs['listview_line_actions_limits'] = $listViewLineActionsLimits;
|
||||
$this->injectedSystemConfigs['listview_url_query_filter_mapping'] = $listViewUrlQueryFilterMapping;
|
||||
$this->injectedSystemConfigs['ui'] = $uiConfigs ?? [];
|
||||
$this->injectedSystemConfigs['ui']['notifications'] = $notificationsConfigs ?? [];
|
||||
$this->injectedSystemConfigs['ui']['notifications_reload_actions'] = $notificationsReloadActions ?? [];
|
||||
|
|
|
@ -338,8 +338,8 @@ class ViewDefinitionsHandler extends LegacyHandler implements ViewDefinitionsPro
|
|||
unset($definition['templateMeta']);
|
||||
}
|
||||
|
||||
$this->mergeSearchInfo($module, $definition, $searchDefs, 'basic_search');
|
||||
$this->mergeSearchInfo($module, $definition, $searchDefs, 'advanced_search');
|
||||
$this->mergeSearchInfo($module, $definition, $fieldDefinition, $searchDefs, 'basic_search');
|
||||
$this->mergeSearchInfo($module, $definition, $fieldDefinition, $searchDefs, 'advanced_search');
|
||||
|
||||
$this->mergeFieldDefinition($definition, $fieldDefinition, 'basic_search');
|
||||
$this->mergeFieldDefinition($definition, $fieldDefinition, 'advanced_search');
|
||||
|
@ -359,15 +359,24 @@ class ViewDefinitionsHandler extends LegacyHandler implements ViewDefinitionsPro
|
|||
protected function mergeFieldDefinition(array &$definition, FieldDefinition $fieldDefinition, string $type): void
|
||||
{
|
||||
$vardefs = $fieldDefinition->getVardef();
|
||||
if (isset($definition['layout'][$type])) {
|
||||
foreach ($definition['layout'][$type] as $key => $field) {
|
||||
$fieldName = $this->getFieldName($key, $field);
|
||||
if (!isset($definition['layout'][$type])) {
|
||||
return;
|
||||
}
|
||||
foreach ($definition['layout'][$type] as $key => $field) {
|
||||
$fieldName = $this->getFieldName($key, $field);
|
||||
|
||||
if (!empty($vardefs[$fieldName])) {
|
||||
$merged = $this->addFieldDefinition($vardefs, $fieldName, $field);
|
||||
$aliasKey = $merged['name'] ?? $key;
|
||||
$definition['layout'][$type][$aliasKey] = $merged;
|
||||
}
|
||||
if (empty($vardefs[$fieldName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$merged = $this->addFieldDefinition($vardefs, $fieldName, $field);
|
||||
$merged = $this->injectEmptyOption($merged);
|
||||
$aliasKey = $merged['name'] ?? $key;
|
||||
$definition['layout'][$type][$aliasKey] = $merged;
|
||||
|
||||
if (empty($definition['layout'][$type][$aliasKey]['vardefBased'])) {
|
||||
$definition['layout'][$type][$aliasKey]['vardefBased'] = false;
|
||||
$definition['layout'][$type][$aliasKey]['readonly'] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -407,17 +416,39 @@ class ViewDefinitionsHandler extends LegacyHandler implements ViewDefinitionsPro
|
|||
* @param array $searchDefs
|
||||
* @param string $type
|
||||
*/
|
||||
protected function mergeSearchInfo(string $module, array &$definition, array $searchDefs, string $type): void
|
||||
protected function mergeSearchInfo(string $module, array &$definition, FieldDefinition $fieldDefinition, array $searchDefs, string $type): void
|
||||
{
|
||||
if (isset($definition['layout'][$type])) {
|
||||
foreach ($definition['layout'][$type] as $key => $field) {
|
||||
$name = $field['name'] ?? '';
|
||||
if (!isset($definition['layout'][$type])) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->useRangeSearch($module, $searchDefs, $name)) {
|
||||
$definition['layout'][$type][$key]['enable_range_search'] = true;
|
||||
}
|
||||
foreach ($definition['layout'][$type] as $key => $field) {
|
||||
$name = $field['name'] ?? '';
|
||||
|
||||
if ($this->useRangeSearch($module, $searchDefs, $name)) {
|
||||
$definition['layout'][$type][$key]['enable_range_search'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$layoutKeys = array_keys($definition['layout'][$type]);
|
||||
$layoutValues = array_values($definition['layout'][$type]);
|
||||
|
||||
$vardefs = $fieldDefinition->getVardef();
|
||||
foreach ($vardefs as $fieldVardefKey => $fieldVardefDefinition) {
|
||||
if (
|
||||
in_array($fieldVardefKey, $layoutKeys)
|
||||
|| in_array($fieldVardefKey, $layoutValues)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$definition['layout'][$type][$fieldVardefKey] = [
|
||||
'name' => $fieldVardefKey,
|
||||
'vardefBased' => true,
|
||||
'display' => 'none',
|
||||
'readonly' => true,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -519,4 +550,29 @@ class ViewDefinitionsHandler extends LegacyHandler implements ViewDefinitionsPro
|
|||
|
||||
return $field;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects an empty option into the field options if it's not already present.
|
||||
*
|
||||
* @param array $merged The merged array containing field definition and metadata
|
||||
* @return array The updated merged array with the empty option injected
|
||||
*/
|
||||
protected function injectEmptyOption(array $merged): array
|
||||
{
|
||||
if (empty($merged['fieldDefinition']['options'])) {
|
||||
return $merged;
|
||||
}
|
||||
|
||||
$metadata = $merged['fieldDefinition']['metadata'] ?? [];
|
||||
$extraOptions = $metadata['extraOptions'] ?? [];
|
||||
$extraOptions[] = [
|
||||
'value' => '__SuiteCRMEmptyString__',
|
||||
'labelKey' => 'LBL_EMPTY',
|
||||
];
|
||||
|
||||
$metadata['extraOptions'] = $extraOptions;
|
||||
$merged['fieldDefinition']['metadata'] = $metadata;
|
||||
|
||||
return $merged;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,6 +50,21 @@ class DefaultFilterMapper implements FilterMapperInterface
|
|||
}
|
||||
|
||||
$legacyValue = $values;
|
||||
|
||||
$mapEmptyString = false;
|
||||
foreach ($legacyValue as $legacyValueKey => $legacyValueValue){
|
||||
switch ($legacyValueValue) {
|
||||
case "__SuiteCRMEmptyString__":
|
||||
$mapEmptyString = true;
|
||||
$legacyValue[$legacyValueKey] = '';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($mapEmptyString){
|
||||
return $legacyValue;
|
||||
}
|
||||
|
||||
if (count($values) === 1) {
|
||||
$legacyValue = $values[0];
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue