Skip to content

Commit 2db48c0

Browse files
authored
Issue 53118: Filter modal change to multiValue type to account for numeric value when parsing (#1819)
### version 6.52.4 *Released*: 30 June 2025 - Issue 53118: Filter modal change to multiValue type to account for numeric value when parsing
1 parent 41c6243 commit 2db48c0

6 files changed

Lines changed: 63 additions & 48 deletions

File tree

packages/components/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/components/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@labkey/components",
3-
"version": "6.52.3",
3+
"version": "6.52.4",
44
"description": "Components, models, actions, and utility functions for LabKey applications and pages",
55
"sideEffects": false,
66
"files": [

packages/components/releaseNotes/components.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# @labkey/components
22
Components, models, actions, and utility functions for LabKey applications and pages
33

4+
### version 6.52.4
5+
*Released*: 30 June 2025
6+
- Issue 53118: Filter modal change to multiValue type to account for numeric value when parsing
7+
48
### version 6.52.3
59
*Released*: 27 June 2025
610
- GitHub Issue 787: DomainField JSON file import should respect required boolean for SampleId field

packages/components/src/internal/components/search/FilterExpressionView.tsx

Lines changed: 41 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ import { DatePickerInput } from '../forms/input/DatePickerInput';
1313
import { OntologyBrowserFilterPanel } from '../ontology/OntologyBrowserFilterPanel';
1414

1515
import {
16+
getFilterOptionsForType,
1617
getFilterSelections,
1718
getFilterTypePlaceHolder,
18-
getFilterOptionsForType,
1919
getUpdatedFilters,
2020
getUpdatedFilterSelection,
2121
} from './utils';
@@ -229,16 +229,12 @@ export const FilterExpressionView: FC<Props> = memo(props => {
229229
return (
230230
<DatePickerInput
231231
allowRelativeInput={allowRelativeDateFilter}
232+
disabled={disabled}
232233
formsy={false}
234+
hideTime={!isTimeOnly} // filter date and datetime by date only, without timepicker
233235
inputClassName="form-control filter-expression__input"
234-
wrapperClassName="form-group col-sm-12 filter-expression__input-wrapper"
235-
queryColumn={field}
236-
name={'field-value-date' + suffix}
237-
value={valueRaw}
238-
showLabel={false}
239236
isClearable
240-
hideTime={!isTimeOnly} // filter date and datetime by date only, without timepicker
241-
disabled={disabled}
237+
name={'field-value-date' + suffix}
242238
onChange={newDate => {
243239
let dateStr = newDate;
244240
if (typeof newDate !== 'string')
@@ -247,6 +243,10 @@ export const FilterExpressionView: FC<Props> = memo(props => {
247243
: getJsonDateFormatString(newDate);
248244
updateDateFilterFieldValue(filterIndex, dateStr, isSecondInput);
249245
}}
246+
queryColumn={field}
247+
showLabel={false}
248+
value={valueRaw}
249+
wrapperClassName="form-group col-sm-12 filter-expression__input-wrapper"
250250
/>
251251
);
252252
} else if (jsonType === 'boolean') {
@@ -257,11 +257,11 @@ export const FilterExpressionView: FC<Props> = memo(props => {
257257
<input
258258
checked={valueRaw == 'true'}
259259
className=""
260-
type="radio"
261-
name={'field-value-bool' + suffix}
262-
value="true"
263260
disabled={disabled}
261+
name={'field-value-bool' + suffix}
264262
onChange={event => updateBooleanFilterFieldValue(filterIndex, event)}
263+
type="radio"
264+
value="true"
265265
/>{' '}
266266
TRUE
267267
</label>
@@ -271,11 +271,11 @@ export const FilterExpressionView: FC<Props> = memo(props => {
271271
<input
272272
checked={valueRaw && valueRaw != 'true'}
273273
className=""
274-
type="radio"
275-
name={'field-value-bool' + suffix}
276-
value="false"
277274
disabled={disabled}
275+
name={'field-value-bool' + suffix}
278276
onChange={event => updateBooleanFilterFieldValue(filterIndex, event)}
277+
type="radio"
278+
value="false"
279279
/>{' '}
280280
FALSE
281281
</label>
@@ -285,18 +285,20 @@ export const FilterExpressionView: FC<Props> = memo(props => {
285285
}
286286

287287
if (filterType.multiValue && !filterType.betweenOperator) {
288-
// Issue 52068: if the valueRaw is an array, just join it by new lines (not replacing semicolons)
289-
const value = Array.isArray(valueRaw) ? valueRaw.join('\n') : (valueRaw?.replaceAll(';', '\n') ?? '');
288+
// Issue 52068/53118: if the valueRaw is an array, just join it by new lines (not replacing semicolons)
289+
const value = Array.isArray(valueRaw)
290+
? valueRaw.join('\n')
291+
: (valueRaw?.toString().replaceAll(';', '\n') ?? '');
290292

291293
return (
292294
<textarea
293295
className="form-control filter-expression__textarea"
296+
defaultValue={value}
294297
name={'field-value-text' + suffix}
295298
onChange={event => updateTextFilterFieldValue(filterIndex, event)}
296-
defaultValue={value}
297-
rows={3}
298-
required
299299
placeholder={placeholder}
300+
required
301+
rows={3}
300302
/>
301303
);
302304
}
@@ -305,29 +307,29 @@ export const FilterExpressionView: FC<Props> = memo(props => {
305307
return (
306308
<input
307309
className="form-control filter-expression__input"
308-
step={jsonType === 'int' ? 1 : undefined}
310+
disabled={disabled}
309311
name={'field-value-text' + suffix}
310312
onChange={event => updateTextFilterFieldValue(filterIndex, event, true)}
311313
pattern={jsonType === 'int' ? '-?[0-9]*' : undefined}
312-
type="number"
313-
value={valueRaw ?? ''}
314314
placeholder={placeholder}
315315
required
316-
disabled={disabled}
316+
step={jsonType === 'int' ? 1 : undefined}
317+
type="number"
318+
value={valueRaw ?? ''}
317319
/>
318320
);
319321
}
320322

321323
const textInput = (
322324
<input
323325
className="form-control filter-expression__input"
326+
disabled={disabled}
324327
name={'field-value-text' + suffix}
325-
type="text"
326-
value={valueRaw ?? ''}
327328
onChange={event => updateTextFilterFieldValue(filterIndex, event)}
328329
placeholder={placeholder}
329330
required
330-
disabled={disabled}
331+
type="text"
332+
value={valueRaw ?? ''}
331333
/>
332334
);
333335

@@ -349,13 +351,13 @@ export const FilterExpressionView: FC<Props> = memo(props => {
349351
</a>
350352
{expanded && (
351353
<OntologyBrowserFilterPanel
352-
ontologyId={field.sourceOntology}
353354
conceptSubtree={field.conceptSubtree}
354-
filterValue={valueRaw}
355355
filterType={Filter.getFilterTypeForURLSuffix(filterType.value)}
356+
filterValue={valueRaw}
356357
onFilterChange={filterValue =>
357358
updateOntologyFieldValue(filterIndex, filterValue, isSecondInput)
358359
}
360+
ontologyId={field.sourceOntology}
359361
/>
360362
)}
361363
</div>
@@ -409,31 +411,31 @@ export const FilterExpressionView: FC<Props> = memo(props => {
409411
return (
410412
<div className="filter-expression__panel">
411413
<SelectInput
412-
key={'filter-expression-field-filter-type-' + removeFilterCount} // we need to recreate this component when a filter is removed
413-
name="filter-expression-field-filter-type"
414414
containerClass="form-group filter-expression__input-wrapper"
415+
disabled={disabled}
415416
inputClass="filter-expression__input-select"
416-
placeholder="Select a filter type..."
417-
value={activeFilters[0]?.filterType?.value}
417+
key={'filter-expression-field-filter-type-' + removeFilterCount} // we need to recreate this component when a filter is removed
418+
name="filter-expression-field-filter-type"
418419
onChange={onUpdateFirstField}
419420
options={unusedFilterOptions(0)}
420-
disabled={disabled}
421+
placeholder="Select a filter type..."
422+
value={activeFilters[0]?.filterType?.value}
421423
/>
422424
{renderFilterTypeInputs(0)}
423425
{shouldShowSecondFilter && (
424426
<>
425427
<div className="field-modal__col-sub-title">and</div>
426428
<SelectInput
427-
key="filter-expression-field-filter-type"
428-
name="filter-expression-field-filter-type"
429429
containerClass="form-group filter-expression__input-wrapper"
430+
disabled={disabled}
430431
inputClass="filter-expression__input-select"
431-
placeholder="Select a filter type..."
432-
value={activeFilters[1]?.filterType?.value}
432+
key="filter-expression-field-filter-type"
433+
menuPosition="fixed"
434+
name="filter-expression-field-filter-type"
433435
onChange={onUpdateSecondField}
434436
options={unusedFilterOptions(1)}
435-
menuPosition="fixed"
436-
disabled={disabled}
437+
placeholder="Select a filter type..."
438+
value={activeFilters[1]?.filterType?.value}
437439
/>
438440
{renderFilterTypeInputs(1)}
439441
</>

packages/components/src/internal/components/search/utils.test.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
ALL_VALUE_DISPLAY,
1717
decodeErrorMessage,
1818
EMPTY_VALUE_DISPLAY,
19+
escapeSearchQuery,
1920
getCheckedFilterValues,
2021
getFieldFiltersValidationResult,
2122
getFilterSelections,
@@ -27,7 +28,6 @@ import {
2728
getUpdatedFilterSelection,
2829
getUpdateFilterExpressionFilter,
2930
isValidFilterField,
30-
escapeSearchQuery,
3131
} from './utils';
3232
import { SearchCategory } from './constants';
3333
import { FieldFilter } from './models';
@@ -467,6 +467,12 @@ describe('getUpdateFilterExpressionFilter', () => {
467467
});
468468

469469
test('in filter type with value string', () => {
470+
expect(getUpdateFilterExpressionFilter(oneOfOption, stringField, null, null, '123')).toStrictEqual(
471+
Filter.create(fieldKey, ['123'], Filter.Types.IN)
472+
);
473+
expect(getUpdateFilterExpressionFilter(oneOfOption, stringField, null, null, 123)).toStrictEqual(
474+
Filter.create(fieldKey, ['123'], Filter.Types.IN)
475+
);
470476
expect(getUpdateFilterExpressionFilter(oneOfOption, stringField, null, null, 'a;b;c')).toStrictEqual(
471477
Filter.create(fieldKey, ['a', 'b', 'c'], Filter.Types.IN)
472478
);

packages/components/src/internal/components/search/utils.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,8 @@ export function getFilterValuesAsArray(filter: Filter.IFilter, blankValue?: stri
127127
}
128128

129129
export function getFieldFiltersValidationResult(
130-
dataTypeFilters: { [key: string]: FieldFilter[] },
131-
queryLabels?: { [key: string]: string },
130+
dataTypeFilters: Record<string, FieldFilter[]>,
131+
queryLabels?: Record<string, string>,
132132
maxMultiValuedValues: number = MAX_MULTI_VALUE_FILTER_VALUES
133133
): string {
134134
const missingValueFields = {};
@@ -265,6 +265,9 @@ export function getUpdateFilterExpressionFilter(
265265
} else if (!value && field.getDisplayFieldJsonType() === 'boolean') {
266266
value = 'false';
267267
} else if (value && filterType.isMultiValued()) {
268+
// Issue 53118: if the value is not an array, convert it to a string
269+
if (!Array.isArray(value) && !Utils.isString(value)) value = value.toString();
270+
268271
// Issue 52068: for multivalued filter types, split on new line to get an array of values
269272
value = value.indexOf('\n') > -1 ? value.split('\n') : filterType.parseValue(value);
270273
}
@@ -299,6 +302,9 @@ export function getCheckedFilterValues(filter: Filter.IFilter, allValues: string
299302
case '':
300303
case 'any':
301304
return allValues;
305+
case 'eq':
306+
case 'in':
307+
return filterValues;
302308
case 'isblank':
303309
return [EMPTY_VALUE_DISPLAY];
304310
case 'isnonblank':
@@ -308,9 +314,6 @@ export function getCheckedFilterValues(filter: Filter.IFilter, allValues: string
308314
case 'neq':
309315
case 'neqornull':
310316
return allValues.filter(value => value !== filterValues[0] && value !== ALL_VALUE_DISPLAY);
311-
case 'eq':
312-
case 'in':
313-
return filterValues;
314317
case 'notin':
315318
return allValues?.filter(value => filterValues.indexOf(value) === -1 && value !== ALL_VALUE_DISPLAY);
316319
default:

0 commit comments

Comments
 (0)