diff --git a/packages/frontend-core/src/constants.js b/packages/frontend-core/src/constants.js index 9ca47de23f..f69590f720 100644 --- a/packages/frontend-core/src/constants.js +++ b/packages/frontend-core/src/constants.js @@ -42,6 +42,10 @@ export const OperatorOptions = { value: "notEqual", label: "Does Not Contain", }, + In: { + value: "oneOf", + label: "Is in", + }, } // Cookie names diff --git a/packages/frontend-core/src/utils/lucene.js b/packages/frontend-core/src/utils/lucene.js index 8f59a2bd9d..1001ec26a8 100644 --- a/packages/frontend-core/src/utils/lucene.js +++ b/packages/frontend-core/src/utils/lucene.js @@ -14,6 +14,7 @@ export const getValidOperatorsForType = type => { Op.Like, Op.Empty, Op.NotEmpty, + Op.In, ] const numOps = [ Op.Equals, @@ -22,6 +23,7 @@ export const getValidOperatorsForType = type => { Op.LessThan, Op.Empty, Op.NotEmpty, + Op.In, ] if (type === "string") { return stringOps @@ -91,6 +93,7 @@ export const buildLuceneQuery = filter => { notEmpty: {}, contains: {}, notContains: {}, + oneOf: {}, } if (Array.isArray(filter)) { filter.forEach(expression => { @@ -99,8 +102,12 @@ export const buildLuceneQuery = filter => { if (type === "datetime" && value) { value = new Date(value).toISOString() } - if (type === "number") { - value = parseFloat(value) + if (type === "number" && !Array.isArray(value)) { + if (operator === "oneOf") { + value = value.split(",").map(item => parseFloat(item)) + } else { + value = parseFloat(value) + } } if (type === "boolean") { value = `${value}`?.toLowerCase() === "true" @@ -139,7 +146,6 @@ export const buildLuceneQuery = filter => { } }) } - return query } @@ -211,6 +217,17 @@ export const runLuceneQuery = (docs, query) => { return docValue == null || docValue === "" }) + // Process an includes match (fails if the value is not included) + const oneOf = match("oneOf", (docValue, testValue) => { + if (typeof testValue === "string") { + testValue = testValue.split(",") + if (typeof docValue === "number") { + testValue = testValue.map(item => parseFloat(item)) + } + } + return !testValue?.includes(docValue) + }) + // Match a document against all criteria const docMatch = doc => { return ( @@ -220,7 +237,8 @@ export const runLuceneQuery = (docs, query) => { equalMatch(doc) && notEqualMatch(doc) && emptyMatch(doc) && - notEmptyMatch(doc) + notEmptyMatch(doc) && + oneOf(doc) ) } diff --git a/packages/server/src/api/controllers/row/internalSearch.js b/packages/server/src/api/controllers/row/internalSearch.js index d6c6860332..d262592d3b 100644 --- a/packages/server/src/api/controllers/row/internalSearch.js +++ b/packages/server/src/api/controllers/row/internalSearch.js @@ -17,6 +17,7 @@ class QueryBuilder { notEqual: {}, empty: {}, notEmpty: {}, + oneOf: {}, ...base, } this.limit = 50 @@ -112,6 +113,11 @@ class QueryBuilder { return this } + addOneOf(key, value) { + this.query.oneOf[key] = value + return this + } + /** * Preprocesses a value before going into a lucene search. * Transforms strings to lowercase and wraps strings and bools in quotes. @@ -220,6 +226,28 @@ class QueryBuilder { if (this.query.notEmpty) { build(this.query.notEmpty, key => `${key}:["" TO *]`) } + if (this.query.oneOf) { + build(this.query.oneOf, (key, value) => { + if (!Array.isArray(value)) { + if (typeof value === "string") { + value = value.split(",") + } else { + return "" + } + } + let orStatement = `${builder.preprocess( + value[0], + allPreProcessingOpts + )}` + for (let i = 1; i < value.length; i++) { + orStatement += ` OR ${builder.preprocess( + value[i], + allPreProcessingOpts + )}` + } + return `${key}:(${orStatement})` + }) + } return query }