diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d520963..80c3fea3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +### 1.49.1 - 2026-03-?? +- Fix Filter parseValue method incorrectly handling invalid JSON values + - GH Issue 948: Multi value text choice filters don't work for JSON values, crash the app + ### 1.49.0 - 2026-03-10 - Update TypeScript compiler `lib` option to `"ES2023"' - Add `declarationMap` and `outDir` to TypeScript compiler options diff --git a/package-lock.json b/package-lock.json index 22a20a4a..a5989e00 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/api", - "version": "1.49.0", + "version": "1.49.1-fb-gh-948.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/api", - "version": "1.49.0", + "version": "1.49.1-fb-gh-948.0", "license": "Apache-2.0", "devDependencies": { "@babel/core": "7.29.0", diff --git a/package.json b/package.json index 527cffa0..10505d51 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/api", - "version": "1.49.0", + "version": "1.49.1-fb-gh-948.0", "description": "JavaScript client API for LabKey Server", "scripts": { "build": "npm run build:dist && npm run build:docs", diff --git a/src/labkey/filter/Types.spec.ts b/src/labkey/filter/Types.spec.ts index 248440ee..f1c7ac69 100644 --- a/src/labkey/filter/Types.spec.ts +++ b/src/labkey/filter/Types.spec.ts @@ -51,3 +51,45 @@ describe('Types', () => { expect(generateFilterTypesSnapshot(Types)).toStrictEqual(typesSnapshot); }); }); + +describe('parseValue', () => { + describe('multi value types', () => { + it('should parse JSON formatted values', () => { + const type = Types.IN; + const value = '{json:["value1","value2","value;3"]}'; + expect(type.parseValue(value)).toEqual(['value1', 'value2', 'value;3']); + }); + + it('should split values by the type multi-value separator', () => { + const semicolonType = Types.IN; // Uses ';' as separator + const semicolonValue = 'value1;value2;value3'; + expect(semicolonType.parseValue(semicolonValue)).toEqual(['value1', 'value2', 'value3']); + + const commaType = Types.BETWEEN; // Uses ',' as separator + const commaValue = 'value1,value2'; + expect(commaType.parseValue(commaValue)).toEqual(['value1', 'value2']); + }); + + it('should split values by newline separator', () => { + const type = Types.IN; + const value = 'value1\nvalue2\nvalue3'; + expect(type.parseValue(value)).toEqual(['value1', 'value2', 'value3']); + }); + + it('should split values by both type separator and newline', () => { + const type = Types.IN; + const value = 'value1;value2\nvalue3'; + expect(type.parseValue(value)).toEqual(['value1', 'value2', 'value3']); + }); + + it('should fall back to regex parsing if JSON is invalid', () => { + const type = Types.IN; + // Invalid JSON: missing closing quote for value2 + const singleValue = '{json:["value1","value2]}'; + expect(type.parseValue(singleValue)).toEqual([singleValue]); + + const multiValue = '{json:aaa;bb}'; + expect(type.parseValue(multiValue)).toEqual(['{json:aaa', 'bb}']); + }); + }) +}); \ No newline at end of file diff --git a/src/labkey/filter/Types.ts b/src/labkey/filter/Types.ts index fa78dd15..e81225e2 100644 --- a/src/labkey/filter/Types.ts +++ b/src/labkey/filter/Types.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { isArray, isString } from '../Utils'; +import { isString } from '../Utils'; import { FilterValue, multiValueToSingleMap, oppositeMap, singleValueToMultiMap } from './constants'; @@ -557,6 +557,22 @@ export function getFilterTypesForType(jsonType: JsonType, mvEnabled?: boolean): return types; } +// Note that while ';' and ',' are both used as primary separators, '\n' is the only secondary separator +const NEW_LINE_SEP = '\n'; + +export function parseMultiValueFilterString(type: IFilterType, value: string) { + if (value.indexOf('{json:') === 0 && value.indexOf('}') === value.length - 1) { + try { + return JSON.parse(value.substring('{json:'.length, value.length - 1)); + } catch { + // GH Issue #948 Purposely do nothing, revert to parsing with regex + } + } + + const regexPattern = new RegExp(`[${NEW_LINE_SEP}${type.getMultiValueSeparator()}]`); + return value.split(regexPattern); +} + /** * Creates a FilterType object and stores it in the global URL Map used by Filter.getFilterTypeForURLSuffix * @param displayText The text to display in a filter menu @@ -585,8 +601,6 @@ export function registerFilterType( const isDataValueRequired = () => dataValueRequired === true; const isMultiValued = () => multiValueSeparator != null; const isTableWise = () => tableWise === true; - // Note that while ';' and ',' are both used as primary separators, '\n' is the only secondary separator - const NEW_LINE_SEP = '\n'; const type: IFilterType = { getDisplaySymbol: () => displaySymbol ?? null, @@ -606,15 +620,10 @@ export function registerFilterType( parseValue: value => { if (type.isMultiValued()) { if (isString(value)) { - if (value.indexOf('{json:') === 0 && value.indexOf('}') === value.length - 1) { - value = JSON.parse(value.substring('{json:'.length, value.length - 1)); - } else { - const regexPattern = new RegExp(`[${NEW_LINE_SEP}${type.getMultiValueSeparator()}]`); - value = value.split(regexPattern); - } + value = parseMultiValueFilterString(type, value); } - if (!isArray(value)) + if (!Array.isArray(value)) throw new Error( "Filter '" + type.getDisplayText() + @@ -625,7 +634,7 @@ export function registerFilterType( ); } - if (!type.isMultiValued() && isArray(value)) + if (!type.isMultiValued() && Array.isArray(value)) throw new Error("Array of values not supported for '" + type.getDisplayText() + "' filter: " + value); return value; @@ -636,7 +645,7 @@ export function registerFilterType( return ''; } - if (type.isMultiValued() && isArray(value)) { + if (type.isMultiValued() && Array.isArray(value)) { // 35265: Create alternate syntax to handle semicolons const sep = type.getMultiValueSeparator(); const found = value.some((v: string) => {