From 5f551bb7c4046a3697b967d746dc7d38a0015e4c Mon Sep 17 00:00:00 2001 From: XingY Date: Mon, 12 Jan 2026 10:54:19 -0800 Subject: [PATCH 1/6] Multi value text choices --- CHANGELOG.md | 4 +++ package-lock.json | 4 +-- package.json | 2 +- src/labkey/filter/Types.ts | 60 +++++++++++++++++++++++++++++++++++++- 4 files changed, 66 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7edd6caa..b108827d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +### 1.X - 2026-01-X +- Add Array filter types + - ARRAY_CONTAINS_ALL, ARRAY_CONTAINS_ANY, ARRAY_CONTAINS_EXACT, ARRAY_ISEMPTY, ARRAY_ISNOTEMPTY + ### 1.44.1 - 2026-01-07 - Lineage: add "restricted" property diff --git a/package-lock.json b/package-lock.json index 45b738ad..927d82de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/api", - "version": "1.44.1", + "version": "1.45.0-fb-mvtc.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/api", - "version": "1.44.1", + "version": "1.45.0-fb-mvtc.2", "license": "Apache-2.0", "devDependencies": { "@babel/core": "7.28.5", diff --git a/package.json b/package.json index 17a36fca..7aab6ad2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/api", - "version": "1.44.1", + "version": "1.45.0-fb-mvtc.2", "description": "JavaScript client API for LabKey Server", "scripts": { "build": "npm run build:dist && npm run build:docs", diff --git a/src/labkey/filter/Types.ts b/src/labkey/filter/Types.ts index 60f14e12..4d3d73c1 100644 --- a/src/labkey/filter/Types.ts +++ b/src/labkey/filter/Types.ts @@ -128,6 +128,39 @@ export const Types: Record = { // These operators require a data value // + ARRAY_CONTAINS_ALL: registerFilterType( + 'Contains All', + null, + 'arraycontainsall', + true, + ';', + 'Contains All Of' + ), + ARRAY_CONTAINS_ANY: registerFilterType( + 'Contains Any', + null, + 'arraycontainsany', + true, + ';', + 'Contains At Least One Of' + ), + ARRAY_CONTAINS_EXACT: registerFilterType( + 'Contains Exactly', + null, + 'arraymatches', + true, + ';', + 'Contains Exactly the Selected Values' + ), + ARRAY_CONTAINS_NONE: registerFilterType( + 'Contains None', + null, + 'arraycontainsnone', + true, + ';', + 'Contains None Of' + ), + EQUAL, DATE_EQUAL: registerFilterType( EQUAL.getDisplayText(), @@ -271,6 +304,29 @@ export const Types: Record = { // These are the 'no data value' operators // + ARRAY_ISEMPTY: registerFilterType( + 'Is Empty', + null, + 'arrayisempty', + false, + undefined, + undefined, + undefined, + undefined, + false, + ), + ARRAY_ISNOTEMPTY: registerFilterType( + 'Is Not Empty', + null, + 'arrayisnotempty', + false, + undefined, + undefined, + undefined, + undefined, + false, + ), + // NOTE: This type, for better or worse, uses empty string as it's urlSuffix. // The result is a filter that is encoded as ".~=". HAS_ANY_VALUE: registerFilterType('Has Any Value', null, ''), @@ -359,9 +415,10 @@ export const Types: Record = { EXP_LINEAGE_OF: registerFilterType('In The Lineage Of', null, 'exp:lineageof', true, ',', ' in the lineage of'), }; -export type JsonType = 'boolean' | 'date' | 'float' | 'int' | 'string' | 'time'; +export type JsonType = 'array' | 'boolean' | 'date' | 'float' | 'int' | 'string' | 'time'; export const TYPES_BY_JSON_TYPE: Record = { + array: [Types.ARRAY_ISEMPTY, Types.ARRAY_ISNOTEMPTY, Types.ARRAY_CONTAINS_ALL, Types.ARRAY_CONTAINS_NONE, Types.ARRAY_CONTAINS_ANY, Types.ARRAY_CONTAINS_EXACT], boolean: [Types.HAS_ANY_VALUE, Types.EQUAL, Types.NEQ_OR_NULL, Types.ISBLANK, Types.NONBLANK], date: [ Types.DATE_EQUAL, @@ -440,6 +497,7 @@ export const TYPES_BY_JSON_TYPE: Record = { // TODO: Update to Record export const TYPES_BY_JSON_TYPE_DEFAULT: Record = { + array: Types.ARRAY_CONTAINS_ALL, boolean: Types.EQUAL, date: Types.DATE_EQUAL, float: Types.EQUAL, From 4a7592ffcae8b40276648aff9d33d6e0101e95c9 Mon Sep 17 00:00:00 2001 From: XingY Date: Mon, 12 Jan 2026 12:10:30 -0800 Subject: [PATCH 2/6] fix test --- src/test/data/filter_types_snapshot.json | 102 +++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/src/test/data/filter_types_snapshot.json b/src/test/data/filter_types_snapshot.json index cd3e6334..4ee2bb8f 100644 --- a/src/test/data/filter_types_snapshot.json +++ b/src/test/data/filter_types_snapshot.json @@ -1,4 +1,106 @@ { + "ARRAY_CONTAINS_ALL": { + "getDisplaySymbol": null, + "getDisplayText": "Contains All", + "getLabKeySqlOperator": "undefined", + "getLongDisplayText": "Contains All Of", + "getMultiValueFilter": null, + "getMultiValueMaxOccurs": "undefined", + "getMultiValueMinOccurs": "undefined", + "getMultiValueSeparator": ";", + "getOpposite": null, + "getSingleValueFilter": "undefined", + "getURLParameterValue": "undefined", + "getURLSuffix": "arraycontainsall", + "isDataValueRequired": true, + "isMultiValued": true, + "isTableWise": false + }, + "ARRAY_CONTAINS_ANY": { + "getDisplaySymbol": null, + "getDisplayText": "Contains Any", + "getLabKeySqlOperator": "undefined", + "getLongDisplayText": "Contains At Least One Of", + "getMultiValueFilter": null, + "getMultiValueMaxOccurs": "undefined", + "getMultiValueMinOccurs": "undefined", + "getMultiValueSeparator": ";", + "getOpposite": null, + "getSingleValueFilter": "undefined", + "getURLParameterValue": "undefined", + "getURLSuffix": "arraycontainsany", + "isDataValueRequired": true, + "isMultiValued": true, + "isTableWise": false + }, + "ARRAY_CONTAINS_EXACT": { + "getDisplaySymbol": null, + "getDisplayText": "Contains Exactly", + "getLabKeySqlOperator": "undefined", + "getLongDisplayText": "Contains Exactly the Selected Values", + "getMultiValueFilter": null, + "getMultiValueMaxOccurs": "undefined", + "getMultiValueMinOccurs": "undefined", + "getMultiValueSeparator": ";", + "getOpposite": null, + "getSingleValueFilter": "undefined", + "getURLParameterValue": "undefined", + "getURLSuffix": "arraymatches", + "isDataValueRequired": true, + "isMultiValued": true, + "isTableWise": false + }, + "ARRAY_CONTAINS_NONE": { + "getDisplaySymbol": null, + "getDisplayText": "Contains None", + "getLabKeySqlOperator": "undefined", + "getLongDisplayText": "Contains None Of", + "getMultiValueFilter": null, + "getMultiValueMaxOccurs": "undefined", + "getMultiValueMinOccurs": "undefined", + "getMultiValueSeparator": ";", + "getOpposite": null, + "getSingleValueFilter": "undefined", + "getURLParameterValue": "undefined", + "getURLSuffix": "arraycontainsnone", + "isDataValueRequired": true, + "isMultiValued": true, + "isTableWise": false + }, + "ARRAY_ISEMPTY": { + "getDisplaySymbol": null, + "getDisplayText": "Is Empty", + "getLabKeySqlOperator": "undefined", + "getLongDisplayText": "Is Empty", + "getMultiValueFilter": "undefined", + "getMultiValueMaxOccurs": "undefined", + "getMultiValueMinOccurs": "undefined", + "getMultiValueSeparator": null, + "getOpposite": null, + "getSingleValueFilter": "arrayisempty", + "getURLParameterValue": "", + "getURLSuffix": "arrayisempty", + "isDataValueRequired": false, + "isMultiValued": false, + "isTableWise": false + }, + "ARRAY_ISNOTEMPTY": { + "getDisplaySymbol": null, + "getDisplayText": "Is Not Empty", + "getLabKeySqlOperator": "undefined", + "getLongDisplayText": "Is Not Empty", + "getMultiValueFilter": "undefined", + "getMultiValueMaxOccurs": "undefined", + "getMultiValueMinOccurs": "undefined", + "getMultiValueSeparator": null, + "getOpposite": null, + "getSingleValueFilter": "arrayisnotempty", + "getURLParameterValue": "", + "getURLSuffix": "arrayisnotempty", + "isDataValueRequired": false, + "isMultiValued": false, + "isTableWise": false + }, "BETWEEN": { "getDisplaySymbol": null, "getDisplayText": "Between", From e3b94d3a3b1c89b9e571c3147e37d730196ab81b Mon Sep 17 00:00:00 2001 From: XingY Date: Mon, 12 Jan 2026 19:09:35 -0800 Subject: [PATCH 3/6] support filters --- package-lock.json | 4 ++-- package.json | 2 +- src/labkey/filter/Types.ts | 18 +++++++++++++----- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 927d82de..1f03519f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/api", - "version": "1.45.0-fb-mvtc.2", + "version": "1.45.0-fb-mvtc.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/api", - "version": "1.45.0-fb-mvtc.2", + "version": "1.45.0-fb-mvtc.4", "license": "Apache-2.0", "devDependencies": { "@babel/core": "7.28.5", diff --git a/package.json b/package.json index 7aab6ad2..07055c26 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/api", - "version": "1.45.0-fb-mvtc.2", + "version": "1.45.0-fb-mvtc.4", "description": "JavaScript client API for LabKey Server", "scripts": { "build": "npm run build:dist && npm run build:docs", diff --git a/src/labkey/filter/Types.ts b/src/labkey/filter/Types.ts index 4d3d73c1..a992ea64 100644 --- a/src/labkey/filter/Types.ts +++ b/src/labkey/filter/Types.ts @@ -133,7 +133,7 @@ export const Types: Record = { null, 'arraycontainsall', true, - ';', + ',', 'Contains All Of' ), ARRAY_CONTAINS_ANY: registerFilterType( @@ -141,7 +141,7 @@ export const Types: Record = { null, 'arraycontainsany', true, - ';', + ',', 'Contains At Least One Of' ), ARRAY_CONTAINS_EXACT: registerFilterType( @@ -149,15 +149,23 @@ export const Types: Record = { null, 'arraymatches', true, - ';', + ',', 'Contains Exactly the Selected Values' ), + ARRAY_CONTAINS_NOT_EXACT: registerFilterType( + 'Does Not Contain Exactly', + null, + 'arraynotmatches', + true, + ',', + 'Does Not Contains Exactly the Selected Values' + ), ARRAY_CONTAINS_NONE: registerFilterType( 'Contains None', null, 'arraycontainsnone', true, - ';', + ',', 'Contains None Of' ), @@ -418,7 +426,7 @@ export const Types: Record = { export type JsonType = 'array' | 'boolean' | 'date' | 'float' | 'int' | 'string' | 'time'; export const TYPES_BY_JSON_TYPE: Record = { - array: [Types.ARRAY_ISEMPTY, Types.ARRAY_ISNOTEMPTY, Types.ARRAY_CONTAINS_ALL, Types.ARRAY_CONTAINS_NONE, Types.ARRAY_CONTAINS_ANY, Types.ARRAY_CONTAINS_EXACT], + array: [Types.ARRAY_ISEMPTY, Types.ARRAY_ISNOTEMPTY, Types.ARRAY_CONTAINS_ALL, Types.ARRAY_CONTAINS_NONE, Types.ARRAY_CONTAINS_ANY, Types.ARRAY_CONTAINS_EXACT, Types.ARRAY_CONTAINS_NOT_EXACT], boolean: [Types.HAS_ANY_VALUE, Types.EQUAL, Types.NEQ_OR_NULL, Types.ISBLANK, Types.NONBLANK], date: [ Types.DATE_EQUAL, From 6be2fbe48bb7fa5242b62de405542e6c844728ee Mon Sep 17 00:00:00 2001 From: XingY Date: Tue, 13 Jan 2026 10:51:40 -0800 Subject: [PATCH 4/6] update snapshot --- src/test/data/filter_types_snapshot.json | 25 ++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/test/data/filter_types_snapshot.json b/src/test/data/filter_types_snapshot.json index 4ee2bb8f..103b7a3d 100644 --- a/src/test/data/filter_types_snapshot.json +++ b/src/test/data/filter_types_snapshot.json @@ -7,7 +7,7 @@ "getMultiValueFilter": null, "getMultiValueMaxOccurs": "undefined", "getMultiValueMinOccurs": "undefined", - "getMultiValueSeparator": ";", + "getMultiValueSeparator": ",", "getOpposite": null, "getSingleValueFilter": "undefined", "getURLParameterValue": "undefined", @@ -24,7 +24,7 @@ "getMultiValueFilter": null, "getMultiValueMaxOccurs": "undefined", "getMultiValueMinOccurs": "undefined", - "getMultiValueSeparator": ";", + "getMultiValueSeparator": ",", "getOpposite": null, "getSingleValueFilter": "undefined", "getURLParameterValue": "undefined", @@ -41,7 +41,7 @@ "getMultiValueFilter": null, "getMultiValueMaxOccurs": "undefined", "getMultiValueMinOccurs": "undefined", - "getMultiValueSeparator": ";", + "getMultiValueSeparator": ",", "getOpposite": null, "getSingleValueFilter": "undefined", "getURLParameterValue": "undefined", @@ -58,7 +58,7 @@ "getMultiValueFilter": null, "getMultiValueMaxOccurs": "undefined", "getMultiValueMinOccurs": "undefined", - "getMultiValueSeparator": ";", + "getMultiValueSeparator": ",", "getOpposite": null, "getSingleValueFilter": "undefined", "getURLParameterValue": "undefined", @@ -67,6 +67,23 @@ "isMultiValued": true, "isTableWise": false }, + "ARRAY_CONTAINS_NOT_EXACT": { + "getDisplaySymbol": null, + "getDisplayText": "Does Not Contain Exactly", + "getLabKeySqlOperator": "undefined", + "getLongDisplayText": "Does Not Contains Exactly the Selected Values", + "getMultiValueFilter": null, + "getMultiValueMaxOccurs": "undefined", + "getMultiValueMinOccurs": "undefined", + "getMultiValueSeparator": ",", + "getOpposite": null, + "getSingleValueFilter": "undefined", + "getURLParameterValue": "undefined", + "getURLSuffix": "arraynotmatches", + "isDataValueRequired": true, + "isMultiValued": true, + "isTableWise": false + }, "ARRAY_ISEMPTY": { "getDisplaySymbol": null, "getDisplayText": "Is Empty", From fa863c4d43defd3ada1fd51f2e32d122c9955157 Mon Sep 17 00:00:00 2001 From: XingY Date: Wed, 14 Jan 2026 13:27:48 -0800 Subject: [PATCH 5/6] update log --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b108827d..5253ef59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ ### 1.X - 2026-01-X - Add Array filter types - - ARRAY_CONTAINS_ALL, ARRAY_CONTAINS_ANY, ARRAY_CONTAINS_EXACT, ARRAY_ISEMPTY, ARRAY_ISNOTEMPTY + - ARRAY_CONTAINS_ALL, ARRAY_CONTAINS_ANY, ARRAY_CONTAINS_EXACT, ARRAY_CONTAINS_NOT_EXACT, ARRAY_ISEMPTY, ARRAY_ISNOTEMPTY ### 1.44.1 - 2026-01-07 - Lineage: add "restricted" property From 6db5e67096386b4095425ba2bb08d751792ac33c Mon Sep 17 00:00:00 2001 From: XingY Date: Thu, 15 Jan 2026 14:45:31 -0800 Subject: [PATCH 6/6] lint --- src/labkey/filter/Types.ts | 38 ++++++++++++++++---------------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/src/labkey/filter/Types.ts b/src/labkey/filter/Types.ts index a992ea64..e66e45b5 100644 --- a/src/labkey/filter/Types.ts +++ b/src/labkey/filter/Types.ts @@ -45,7 +45,7 @@ export interface IFilterType { * Split a filter String or Array value appropriately for this filter type. * @return For multi-valued filter types, an Array of values, otherwise the original filter value. */ - parseValue: (value: string | FilterValue[]) => FilterValue | FilterValue[]; + parseValue: (value: FilterValue[] | string) => FilterValue | FilterValue[]; validate: (value: FilterValue, jsonType: string, columnName: string) => any; } @@ -128,14 +128,7 @@ export const Types: Record = { // These operators require a data value // - ARRAY_CONTAINS_ALL: registerFilterType( - 'Contains All', - null, - 'arraycontainsall', - true, - ',', - 'Contains All Of' - ), + ARRAY_CONTAINS_ALL: registerFilterType('Contains All', null, 'arraycontainsall', true, ',', 'Contains All Of'), ARRAY_CONTAINS_ANY: registerFilterType( 'Contains Any', null, @@ -160,14 +153,7 @@ export const Types: Record = { ',', 'Does Not Contains Exactly the Selected Values' ), - ARRAY_CONTAINS_NONE: registerFilterType( - 'Contains None', - null, - 'arraycontainsnone', - true, - ',', - 'Contains None Of' - ), + ARRAY_CONTAINS_NONE: registerFilterType('Contains None', null, 'arraycontainsnone', true, ',', 'Contains None Of'), EQUAL, DATE_EQUAL: registerFilterType( @@ -321,7 +307,7 @@ export const Types: Record = { undefined, undefined, undefined, - false, + false ), ARRAY_ISNOTEMPTY: registerFilterType( 'Is Not Empty', @@ -332,7 +318,7 @@ export const Types: Record = { undefined, undefined, undefined, - false, + false ), // NOTE: This type, for better or worse, uses empty string as it's urlSuffix. @@ -426,7 +412,15 @@ export const Types: Record = { export type JsonType = 'array' | 'boolean' | 'date' | 'float' | 'int' | 'string' | 'time'; export const TYPES_BY_JSON_TYPE: Record = { - array: [Types.ARRAY_ISEMPTY, Types.ARRAY_ISNOTEMPTY, Types.ARRAY_CONTAINS_ALL, Types.ARRAY_CONTAINS_NONE, Types.ARRAY_CONTAINS_ANY, Types.ARRAY_CONTAINS_EXACT, Types.ARRAY_CONTAINS_NOT_EXACT], + array: [ + Types.ARRAY_ISEMPTY, + Types.ARRAY_ISNOTEMPTY, + Types.ARRAY_CONTAINS_ALL, + Types.ARRAY_CONTAINS_NONE, + Types.ARRAY_CONTAINS_ANY, + Types.ARRAY_CONTAINS_EXACT, + Types.ARRAY_CONTAINS_NOT_EXACT, + ], boolean: [Types.HAS_ANY_VALUE, Types.EQUAL, Types.NEQ_OR_NULL, Types.ISBLANK, Types.NONBLANK], date: [ Types.DATE_EQUAL, @@ -659,7 +653,7 @@ export function registerFilterType( return value; }, - validate: (value: FilterValue, jsonType: JsonType, columnName: string): string | boolean | undefined => { + validate: (value: FilterValue, jsonType: JsonType, columnName: string): boolean | string | undefined => { if (!isDataValueRequired()) { return true; // TODO: This method is all over the place with it's return type. WTB sanity... } @@ -746,7 +740,7 @@ function validate(jsonType: JsonType, value: FilterValue, columnName: string): s return undefined; } } else if (jsonType === 'date') { - let year: number, month: number, day: number, hour: number, minute: number; + let day: number, hour: number, minute: number, month: number, year: number; hour = 0; minute = 0;