feat(ui): support custom operator for array object contains (#23248)

* feat(ui): support custom operator for array object contains

* add contains logicOps

* added playwright test and supported contains in tags and glossary as well

* fix sonar issue

---------

Co-authored-by: Pere Miquel Brull <peremiquelbrull@gmail.com>
Co-authored-by: Ashish Gupta <ashish@getcollate.io>
This commit is contained in:
Chirag Madlani 2025-09-08 14:56:37 +05:30 committed by GitHub
parent 4483e183cb
commit 5b7c569ebf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 382 additions and 22 deletions

View File

@ -28,6 +28,7 @@ public class LogicOps {
public enum CustomLogicOps {
LENGTH("length"),
CONTAINS("contains"),
IS_REVIEWER("isReviewer"),
IS_OWNER("isOwner");
@ -55,6 +56,31 @@ public class LogicOps {
return args.length;
});
jsonLogic.addOperation(
"contains",
(args) -> {
if (nullOrEmpty(args) || args.length < 2) {
return false;
}
Object value = args[0];
Object container = args[1];
// If either value or container is null/empty, the rule is broken
if (CommonUtil.nullOrEmpty(value) || CommonUtil.nullOrEmpty(container)) {
return false;
}
if (container instanceof List<?> list) {
return list.contains(value);
} else if (container.getClass().isArray()) {
Object[] array = (Object[]) container;
return Arrays.asList(array).contains(value);
} else {
return container.toString().contains(value.toString());
}
});
// {"isReviewer": { var: "updatedBy"} }
jsonLogic.addOperation(
new JsonLogicExpression() {

View File

@ -476,6 +476,108 @@ public class RuleEngineTests extends OpenMetadataApplicationTest {
RuleEngine.getInstance().evaluate(table, List.of(piiRule), false, false);
}
@Test
@Execution(ExecutionMode.CONCURRENT)
void testContainsOperator(TestInfo test) {
Table table = getMockTable(test);
// Test case 1: Empty tags - rule should fail (no tags to check)
table.withTags(List.of());
SemanticsRule containsRule =
new SemanticsRule()
.withName("Tag FQN must be in allowed list")
.withDescription("Validates that tagFQN is contained in the allowed list")
.withRule(
"{\"and\":[{\"some\":[{\"var\":\"tags\"},{\"contains\":[{\"var\":\"tagFQN\"},[\"Tier.Tier3\",\"Tier.Tier2\"]]}]}]}");
assertThrows(
RuleValidationException.class,
() -> RuleEngine.getInstance().evaluate(table, List.of(containsRule), false, false));
// Test case 2: Tags with matching FQN - rule should pass
TagLabel allowedTag =
new TagLabel().withTagFQN("Tier.Tier2").withSource(TagLabel.TagSource.CLASSIFICATION);
table.withTags(List.of(allowedTag));
RuleEngine.getInstance().evaluate(table, List.of(containsRule), false, false);
// Test case 3: Tags with non-matching FQN - rule should fail
TagLabel disallowedTag =
new TagLabel().withTagFQN("Tier.Tier1").withSource(TagLabel.TagSource.CLASSIFICATION);
table.withTags(List.of(disallowedTag));
assertThrows(
RuleValidationException.class,
() -> RuleEngine.getInstance().evaluate(table, List.of(containsRule), false, false));
// Test case 4: Multiple tags with at least one matching FQN - rule should pass
table.withTags(List.of(disallowedTag, allowedTag));
RuleEngine.getInstance().evaluate(table, List.of(containsRule), false, false);
// Test case 5: Multiple allowed tags - rule should pass
TagLabel anotherAllowedTag =
new TagLabel().withTagFQN("Tier.Tier3").withSource(TagLabel.TagSource.CLASSIFICATION);
table.withTags(List.of(allowedTag, anotherAllowedTag));
RuleEngine.getInstance().evaluate(table, List.of(containsRule), false, false);
}
@Test
@Execution(ExecutionMode.CONCURRENT)
void testContainsOperatorEdgeCases(TestInfo test) {
Table table = getMockTable(test);
// Test case 1: Rule with single allowed value
SemanticsRule singleValueRule =
new SemanticsRule()
.withName("Tag FQN must be specific value")
.withDescription("Validates that tagFQN equals specific value using contains")
.withRule(
"{\"and\":[{\"some\":[{\"var\":\"tags\"},{\"contains\":[{\"var\":\"tagFQN\"},[\"Tier.Tier1\"]]}]}]}");
TagLabel exactMatch =
new TagLabel().withTagFQN("Tier.Tier1").withSource(TagLabel.TagSource.CLASSIFICATION);
table.withTags(List.of(exactMatch));
RuleEngine.getInstance().evaluate(table, List.of(singleValueRule), false, false);
TagLabel noMatch =
new TagLabel().withTagFQN("Tier.Tier2").withSource(TagLabel.TagSource.CLASSIFICATION);
table.withTags(List.of(noMatch));
assertThrows(
RuleValidationException.class,
() -> RuleEngine.getInstance().evaluate(table, List.of(singleValueRule), false, false));
// Test case 2: Mixed tag sources (Glossary and Classification)
SemanticsRule mixedSourceRule =
new SemanticsRule()
.withName("Allow mixed tag sources")
.withDescription("Validates that either glossary or classification tags are allowed")
.withRule(
"{\"and\":[{\"some\":[{\"var\":\"tags\"},{\"contains\":[{\"var\":\"tagFQN\"},[\"Glossary.Term1\",\"Tier.Tier1\"]]}]}]}");
TagLabel glossaryTag =
new TagLabel().withTagFQN("Glossary.Term1").withSource(TagLabel.TagSource.GLOSSARY);
TagLabel classificationTag =
new TagLabel().withTagFQN("Tier.Tier1").withSource(TagLabel.TagSource.CLASSIFICATION);
// Test with glossary tag
table.withTags(List.of(glossaryTag));
RuleEngine.getInstance().evaluate(table, List.of(mixedSourceRule), false, false);
// Test with classification tag
table.withTags(List.of(classificationTag));
RuleEngine.getInstance().evaluate(table, List.of(mixedSourceRule), false, false);
// Test with both
table.withTags(List.of(glossaryTag, classificationTag));
RuleEngine.getInstance().evaluate(table, List.of(mixedSourceRule), false, false);
// Test with neither allowed
TagLabel disallowedTag =
new TagLabel().withTagFQN("NotAllowed.Tag").withSource(TagLabel.TagSource.CLASSIFICATION);
table.withTags(List.of(disallowedTag));
assertThrows(
RuleValidationException.class,
() -> RuleEngine.getInstance().evaluate(table, List.of(mixedSourceRule), false, false));
}
/**
* Helper method to create a real Domain entity for testing
*/

View File

@ -52,3 +52,22 @@ export const NEW_TABLE_TEST_CASE = {
value: '1000',
description: 'New table test case for TableColumnCountToEqual',
};
export const DATA_CONTRACT_CONTAIN_SEMANTICS = {
name: `data_contract_container_semantic_${uuid()}`,
description: 'new data contract semantic contains description ',
rules: [
{
field: 'Tier',
operator: 'Contains',
},
{
field: 'Tags',
operator: 'Contains',
},
{
field: 'Glossary Term',
operator: 'Contains',
},
],
};

View File

@ -12,6 +12,7 @@
*/
import { test as base, expect, Page } from '@playwright/test';
import {
DATA_CONTRACT_CONTAIN_SEMANTICS,
DATA_CONTRACT_DETAILS,
DATA_CONTRACT_SEMANTICS1,
DATA_CONTRACT_SEMANTICS2,
@ -39,7 +40,13 @@ import {
validateDataContractInsideBundleTestSuites,
waitForDataContractExecution,
} from '../../utils/dataContracts';
import { addOwner, addOwnerWithoutValidation } from '../../utils/entity';
import {
addOwner,
addOwnerWithoutValidation,
assignGlossaryTerm,
assignTag,
assignTier,
} from '../../utils/entity';
import { settingClick } from '../../utils/sidebar';
const adminUser = new UserClass();
@ -1124,6 +1131,164 @@ test.describe('Data Contracts', () => {
}
});
test('Semantic with Contains Operator should work for Tier, Tag and Glossary', async ({
page,
}) => {
await redirectToHomePage(page);
await table.visitEntityPage(page);
await page.click('[data-testid="contract"]');
await page.getByTestId('add-contract-button').click();
await expect(page.getByTestId('add-contract-card')).toBeVisible();
await expect(page.getByTestId('add-contract-card')).toBeVisible();
await page.getByTestId('contract-name').fill(DATA_CONTRACT_DETAILS.name);
await page.getByRole('tab', { name: 'Semantics' }).click();
await expect(page.getByTestId('add-semantic-button')).toBeDisabled();
await page.fill('#semantics_0_name', DATA_CONTRACT_CONTAIN_SEMANTICS.name);
await page.fill(
'#semantics_0_description',
DATA_CONTRACT_CONTAIN_SEMANTICS.description
);
const ruleLocator = page.locator('.group').nth(0);
await selectOption(
page,
ruleLocator.locator('.group--field .ant-select'),
DATA_CONTRACT_CONTAIN_SEMANTICS.rules[0].field,
true
);
await selectOption(
page,
ruleLocator.locator('.rule--operator .ant-select'),
DATA_CONTRACT_CONTAIN_SEMANTICS.rules[0].operator
);
await selectOption(
page,
ruleLocator.locator('.rule--value .ant-select'),
'Tier.Tier1',
true
);
await page.getByRole('button', { name: 'Add New Rule' }).click();
await expect(page.locator('.group--conjunctions')).toBeVisible();
const ruleLocator2 = page.locator('.rule').nth(1);
await selectOption(
page,
ruleLocator2.locator('.rule--field .ant-select'),
DATA_CONTRACT_CONTAIN_SEMANTICS.rules[1].field,
true
);
await selectOption(
page,
ruleLocator2.locator('.rule--operator .ant-select'),
DATA_CONTRACT_CONTAIN_SEMANTICS.rules[1].operator
);
await selectOption(
page,
ruleLocator2.locator('.rule--value .ant-select'),
testTag.responseData.name,
true
);
await page.getByRole('button', { name: 'Add New Rule' }).click();
await expect(page.locator('.group--conjunctions')).toBeVisible();
const ruleLocator3 = page.locator('.rule').nth(2);
await selectOption(
page,
ruleLocator3.locator('.rule--field .ant-select'),
DATA_CONTRACT_CONTAIN_SEMANTICS.rules[2].field,
true
);
await selectOption(
page,
ruleLocator3.locator('.rule--operator .ant-select'),
DATA_CONTRACT_CONTAIN_SEMANTICS.rules[2].operator
);
await selectOption(
page,
ruleLocator3.locator('.rule--value .ant-select'),
testGlossaryTerm.responseData.name,
true
);
await page.getByTestId('save-semantic-button').click();
await expect(
page
.getByTestId('contract-semantics-card-0')
.locator('.semantic-form-item-title')
).toContainText(DATA_CONTRACT_CONTAIN_SEMANTICS.name);
await expect(
page
.getByTestId('contract-semantics-card-0')
.locator('.semantic-form-item-description')
).toContainText(DATA_CONTRACT_CONTAIN_SEMANTICS.description);
await page.locator('.expand-collapse-icon').click();
await expect(page.locator('.semantic-rule-editor-view-only')).toBeVisible();
// save and trigger contract validation
await saveAndTriggerDataContractValidation(page, true);
await expect(
page.getByTestId('contract-card-title-container').filter({
hasText: 'Contract Status',
})
).toBeVisible();
await expect(
page.getByTestId('contract-status-card-item-Semantics-status')
).toContainText('Failed');
await expect(
page.getByTestId('data-contract-latest-result-btn')
).toContainText('Contract Failed');
await page.getByTestId('schema').click();
// Add the data in the Table Entity which Semantic Required
await assignTier(page, 'Tier1', EntityTypeEndpoint.Table);
await assignTag(
page,
testTag.responseData.displayName,
'Add',
EntityTypeEndpoint.Table,
'KnowledgePanel.Tags',
testTag.responseData.fullyQualifiedName
);
await assignGlossaryTerm(page, testGlossaryTerm.responseData);
await page.click('[data-testid="contract"]');
const runNowResponse = page.waitForResponse(
'/api/v1/dataContracts/*/validate'
);
await page.getByTestId('contract-run-now-button').click();
await runNowResponse;
await toastNotification(page, 'Contract validation trigger successfully.');
await page.reload();
await page.waitForLoadState('networkidle');
await page.waitForSelector('[data-testid="loader"]', {
state: 'detached',
});
await expect(
page.getByTestId('contract-status-card-item-Semantics-status')
).toContainText('Passed');
});
test('Nested Column should not be selectable', async ({ page }) => {
const entityFQN = table.entityResponseData.fullyQualifiedName;
await redirectToHomePage(page);
@ -1133,6 +1298,8 @@ test.describe('Data Contracts', () => {
await expect(page.getByTestId('add-contract-card')).toBeVisible();
await page.getByTestId('contract-name').fill(DATA_CONTRACT_DETAILS.name);
await page.getByRole('button', { name: 'Schema' }).click();
await page.waitForSelector('[data-testid="loader"]', {

View File

@ -12,6 +12,7 @@
*/
import { EntityReferenceFields } from '../enums/AdvancedSearch.enum';
import jsonLogicSearchClassBase from '../utils/JSONLogicSearchClassBase';
export enum DataContractMode {
YAML,
@ -41,3 +42,8 @@ export const DATA_ASSET_RULE_FIELDS_NOT_TO_RENDER = [
EntityReferenceFields.DISPLAY_NAME,
EntityReferenceFields.DELETED,
];
export const SEMANTIC_OPERATORS = [
...(jsonLogicSearchClassBase.defaultSelectOperators ?? []),
'array_contains',
];

View File

@ -282,6 +282,7 @@
"constraint-plural": "Einschränkungen",
"constraint-type": "Einschränkungstyp",
"consumer-aligned": "Consumer-aligned",
"contain-plural": "Enthält",
"container": "Container",
"container-column": "Container Column",
"container-plural": "Container",

View File

@ -282,6 +282,7 @@
"constraint-plural": "Constraints",
"constraint-type": "Constraint Type",
"consumer-aligned": "Consumer-aligned",
"contain-plural": "Contains",
"container": "Container",
"container-column": "Container Column",
"container-plural": "Containers",

View File

@ -282,6 +282,7 @@
"constraint-plural": "Restricciones",
"constraint-type": "Tipo de restricción",
"consumer-aligned": "Alineado con el Consumidor",
"contain-plural": "Contiene",
"container": "Contenedor",
"container-column": "Contenedor Columna",
"container-plural": "Contenedores",
@ -2501,13 +2502,6 @@
"welcome-screen-message": "Descubre todos tus datos en un solo lugar y colabora sin problemas con tu equipo en datos en los que puedas confiar.",
"welcome-to-om": "¡Bienvenido a OpenMetadata!",
"welcome-to-open-metadata": "¡Bienvenido a OpenMetadata!",
"workflow-status-exception": "AutoPilot aplicación encontró una excepción.",
"workflow-status-failure": "AutoPilot aplicación falló.",
"workflow-status-failure-description": "Por favor, revise los registros de ejecución de la aplicación para obtener más detalles. También puede volver a activar la aplicación desde la página de la aplicación.",
"workflow-status-finished": "AutoPilot aplicación completada correctamente.",
"workflow-status-finished-description": "La aplicación ha finalizado la ejecución correctamente. Ahora puede ver los insights para su servicio.",
"workflow-status-running": "AutoPilot aplicación está activada y en ejecución.",
"workflow-status-running-description": "La aplicación está actualmente en ejecución. Por favor, espere mientras obtenemos los insights para su servicio.",
"would-like-to-start-adding-some": "¿Te gustaría comenzar a agregar algunos?",
"write-your-announcement-lowercase": "escribe tu anuncio",
"write-your-description": "Escribe tu descripción",

View File

@ -282,6 +282,7 @@
"constraint-plural": "Constraints",
"constraint-type": "Type de contrainte",
"consumer-aligned": "Consumer-aligned",
"contain-plural": "Contient",
"container": "Conteneur",
"container-column": "Container Column",
"container-plural": "Conteneurs",

View File

@ -282,6 +282,7 @@
"constraint-plural": "Restrición",
"constraint-type": "Tipo de Restrición",
"consumer-aligned": "Aliñado co consumidor",
"contain-plural": "Contains",
"container": "Contedor",
"container-column": "Columna do contedor",
"container-plural": "Contedores",

View File

@ -282,6 +282,7 @@
"constraint-plural": "Constraints",
"constraint-type": "סוג מגבלה",
"consumer-aligned": "מכוון לצרכן",
"contain-plural": "Contains",
"container": "אחסון",
"container-column": "Container Column",
"container-plural": "אחסון (Storage)",
@ -2037,7 +2038,7 @@
"error-self-signup-disabled": "Self-signup is currently disabled. To proceed, please reach out to your administrator for further assistance or to request access.",
"error-team-transfer-message": "You cannot move to this team as Team Type {{dragTeam}} can't be {{dropTeam}} children",
"error-while-fetching-access-token": "שגיאה בעת קבלת טוקן גישה.",
"expected-schema-structure-of-this-asset": "מבנה סכמה צפוי של נכס זה",
"expected-schema-structure-of-this-asset": "מבנה סchema צפוי של נכס זה",
"explore-our-guide-here": "explore our guide here.",
"export-entity-help": "הורד את כל הישויות שלך בפורמט קובץ CSV ושתף עם הצוות שלך.",
"external-destination-selection": "Only external destinations can be tested.",
@ -2347,8 +2348,8 @@
"redirect-message": "אנא המתן בזמן שאתה מועבר.",
"redirecting-to-home-page": "מועבר לדף הבית",
"refer-to-our-doc": "זקוק.ה לעזרה נוספת? בקר.י ב-<0>{{doc}}</0> לקבלת מידע נוסף.",
"remove-default-persona-confirmation": "האם אתה בטוח שברצונך להסיר את {{persona}} כפרסונה ברירת מחדל? לא תוגדר פרסונה ברירת מחדל למשתמשים.",
"remove-default-persona-description": "לא תוגדר פרסונה ברירת מחדל למשתמשים",
"remove-default-persona-confirmation": "האם אתה בטוח שברצונך להסיר את {{persona}} כפרסונה ברירת מחדל? לא תוגדר פרסונה ברירת המחדל למשתמשים.",
"remove-default-persona-description": "לא תוגדר פרסונה ברירת המחדל למשתמשים",
"remove-edge-between-source-and-target": "האם אתה בטוח שברצונך להסיר את הקשת בין \"{{sourceDisplayName}} ו-{{targetDisplayName}}\"?",
"remove-lineage-edge": "הסר קישוריות",
"rename-entity": "שנה את השם והשם לתצוגה של {{entity}}.",

View File

@ -282,6 +282,7 @@
"constraint-plural": "制約",
"constraint-type": "制約タイプ",
"consumer-aligned": "消費者に合わせる",
"contain-plural": "含む",
"container": "コンテナ",
"container-column": "コンテナカラム",
"container-plural": "コンテナ",

View File

@ -282,6 +282,7 @@
"constraint-plural": "제약 조건",
"constraint-type": "제약 조건 유형",
"consumer-aligned": "소비자 중심",
"contain-plural": "포함",
"container": "컨테이너",
"container-column": "컨테이너 열",
"container-plural": "컨테이너들",

View File

@ -282,6 +282,7 @@
"constraint-plural": "Constraints",
"constraint-type": "बंधन प्रकार",
"consumer-aligned": "ग्राहक-संरेखित",
"contain-plural": "अंतर्भूत आहे",
"container": "कंटेनर",
"container-column": "कंटेनर स्तंभ",
"container-plural": "कंटेनर",

View File

@ -282,6 +282,7 @@
"constraint-plural": "Constraints",
"constraint-type": "Beperkings type",
"consumer-aligned": "Consumentafgestemd",
"contain-plural": "Bevat",
"container": "Container",
"container-column": "Container Column",
"container-plural": "Containers",

View File

@ -282,6 +282,7 @@
"constraint-plural": "Constraints",
"constraint-type": "نوع محدودیت",
"consumer-aligned": "هماهنگ با مصرف‌کننده",
"contain-plural": "Contém",
"container": "ظرف",
"container-column": "ستون ظرف",
"container-plural": "ظرف‌ها",

View File

@ -282,6 +282,7 @@
"constraint-plural": "Restrições",
"constraint-type": "Tipo de restrição",
"consumer-aligned": "Alinhado ao Consumidor",
"contain-plural": "Contém",
"container": "Contêiner",
"container-column": "Coluna de contêiner",
"container-plural": "Contêineres",

View File

@ -282,6 +282,7 @@
"constraint-plural": "Constraints",
"constraint-type": "Tipo de restrição",
"consumer-aligned": "Alinhado ao Consumidor",
"contain-plural": "Contém",
"container": "Contentor",
"container-column": "Container Column",
"container-plural": "Contentores",

View File

@ -282,6 +282,7 @@
"constraint-plural": "Ограничения",
"constraint-type": "Тип ограничения",
"consumer-aligned": "Ориентированный на потребителя",
"contain-plural": "Содержит",
"container": "Контейнер",
"container-column": "Container Column",
"container-plural": "Контейнеры",

View File

@ -282,6 +282,7 @@
"constraint-plural": "ข้อจำกัดหลายรายการ",
"constraint-type": "ประเภทข้อจำกัด",
"consumer-aligned": "ตรงตามความต้องการของผู้ใช้",
"contain-plural": "มีอยู่",
"container": "คอนเทนเนอร์",
"container-column": "คอลัมน์คอนเทนเนอร์",
"container-plural": "คอนเทนเนอร์",

View File

@ -282,6 +282,7 @@
"constraint-plural": "Kısıtlamalar",
"constraint-type": "Kısıtlama Türü",
"consumer-aligned": "Tüketici Odaklı",
"contain-plural": "İçerir",
"container": "Konteyner",
"container-column": "Konteyner Sütunu",
"container-plural": "Konteynerler",

View File

@ -282,6 +282,7 @@
"constraint-plural": "Constraints",
"constraint-type": "约束类型",
"consumer-aligned": "使用者对齐",
"contain-plural": "包含",
"container": "存储容器",
"container-column": "存储容器列",
"container-plural": "存储容器",

View File

@ -282,6 +282,7 @@
"constraint-plural": "約束",
"constraint-type": "約束類型",
"consumer-aligned": "消費者導向",
"contain-plural": "包含",
"container": "容器",
"container-column": "容器欄位",
"container-plural": "容器",

View File

@ -26,6 +26,7 @@ import {
RED_3,
YELLOW_2,
} from '../../constants/Color.constants';
import { SEMANTIC_OPERATORS } from '../../constants/DataContract.constants';
import { EntityReferenceFields } from '../../enums/AdvancedSearch.enum';
import { SearchIndex } from '../../enums/search.enum';
import { TestCaseType } from '../../enums/TestSuite.enum';
@ -239,7 +240,7 @@ export const getSematicRuleFields = () => {
label: 'Tags',
type: 'select',
mainWidgetProps: jsonLogicSearchClassBase.mainWidgetProps,
operators: jsonLogicSearchClassBase.defaultSelectOperators,
operators: SEMANTIC_OPERATORS,
fieldSettings: {
asyncFetch: jsonLogicSearchClassBase.searchAutocomplete({
searchIndex: SearchIndex.TAG,
@ -265,7 +266,7 @@ export const getSematicRuleFields = () => {
label: 'Tags',
type: 'select',
mainWidgetProps: jsonLogicSearchClassBase.mainWidgetProps,
operators: jsonLogicSearchClassBase.defaultSelectOperators,
operators: SEMANTIC_OPERATORS,
fieldSettings: {
asyncFetch: jsonLogicSearchClassBase.searchAutocomplete({
searchIndex: SearchIndex.GLOSSARY_TERM,
@ -280,13 +281,22 @@ export const getSematicRuleFields = () => {
const tierField = {
label: t('label.tier'),
type: 'select',
type: '!group',
mode: 'some',
fieldName: 'tags',
mainWidgetProps: jsonLogicSearchClassBase.mainWidgetProps,
operators: jsonLogicSearchClassBase.defaultSelectOperators,
fieldSettings: {
asyncFetch: jsonLogicSearchClassBase.autoCompleteTier,
useAsyncSearch: true,
defaultField: 'tagFQN',
subfields: {
tagFQN: {
label: 'Tags',
type: 'multiselect',
mainWidgetProps: jsonLogicSearchClassBase.mainWidgetProps,
operators: SEMANTIC_OPERATORS,
fieldSettings: {
asyncFetch: jsonLogicSearchClassBase.autoCompleteTier,
useAsyncSearch: true,
listValues: jsonLogicSearchClassBase.autoCompleteTier,
},
},
},
};

View File

@ -55,6 +55,13 @@ class JSONLogicSearchClassBase {
text: {
operators: ['like', 'not_like', 'regexp'],
},
multiselect: {
operators: [
...(this.baseConfig.types.multiselect?.widgets?.multiselect
?.operators || []),
'array_contains',
],
},
},
// Limits source to user input values, not other fields
valueSources: ['value'],
@ -66,6 +73,12 @@ class JSONLogicSearchClassBase {
text: {
operators: ['like', 'not_like', 'regexp'],
},
select: {
operators: [
...(this.baseConfig.types.select?.widgets?.select?.operators || []),
'array_contains',
],
},
},
valueSources: ['value'],
},
@ -100,7 +113,7 @@ class JSONLogicSearchClassBase {
...this.baseConfig.widgets.text,
},
};
configOperators = {
configOperators: Config['operators'] = {
...this.baseConfig.operators,
like: {
...this.baseConfig.operators.like,
@ -141,7 +154,6 @@ class JSONLogicSearchClassBase {
label: t('label.is-entity', { entity: t('label.reviewer') }),
labelForFormat: t('label.is-entity', { entity: t('label.reviewer') }),
cardinality: 0,
unary: true,
jsonLogic: 'isReviewer',
sqlOp: 'IS REVIEWER',
},
@ -149,10 +161,17 @@ class JSONLogicSearchClassBase {
label: t('label.is-entity', { entity: t('label.owner') }),
labelForFormat: t('label.is-entity', { entity: t('label.owner') }),
cardinality: 0,
unary: true,
jsonLogic: 'isOwner',
sqlOp: 'IS OWNER',
},
array_contains: {
label: t('label.contain-plural'),
labelForFormat: t('label.contain-plural'),
valueTypes: ['multiselect', 'select'],
cardinality: 1,
valueSources: ['value'],
jsonLogic: 'contains',
},
};
mapFields: Record<string, FieldOrGroup>;