From 11b65b295ba53ece8114fdda2482662fc071549e Mon Sep 17 00:00:00 2001 From: Hariom Balhara <1780212+hariombalhara@users.noreply.github.com> Date: Tue, 17 Feb 2026 21:15:36 +0530 Subject: [PATCH 1/3] test: add missing negation operator tests for TEXT, NUMBER, and compound rules (#27690) * Fix exclusion filter - include all team members * Fix display when members aren't saved in the DB * Update tests * test: add missing negation operator tests for TEXT, NUMBER, and compound rules Co-Authored-By: hariom@cal.com * fix: revert non-intentional changes to AddMembersWithSwitch.tsx Co-Authored-By: hariom@cal.com --------- Co-authored-by: Joe Au-Yeung Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> --- ...MatchingAttributeLogic.integration-test.ts | 268 ++++++++++++++++++ 1 file changed, 268 insertions(+) diff --git a/packages/features/routing-forms/lib/findTeamMembersMatchingAttributeLogic.integration-test.ts b/packages/features/routing-forms/lib/findTeamMembersMatchingAttributeLogic.integration-test.ts index 730c3d41981b2c..088b8630347581 100644 --- a/packages/features/routing-forms/lib/findTeamMembersMatchingAttributeLogic.integration-test.ts +++ b/packages/features/routing-forms/lib/findTeamMembersMatchingAttributeLogic.integration-test.ts @@ -1953,4 +1953,272 @@ describe("findTeamMembersMatchingAttributeLogic", () => { }); }); }); + + describe("negation operators with TEXT and NUMBER attribute types", () => { + describe("TEXT not_equal", () => { + it("should match users without the attribute (undefined != 'sales manager' is true)", async () => { + const JobTitleAttribute = { + id: "job-title-attr", + name: "Job Title", + type: AttributeType.TEXT, + slug: "job-title", + options: [ + { id: "job-sales-mgr", value: "Sales Manager", slug: "sales-manager" }, + { id: "job-engineer", value: "Engineer", slug: "engineer" }, + ], + }; + + const { createdUsers } = await createAttributesScenario({ + attributes: [JobTitleAttribute], + teamMembersWithAttributeOptionValuePerAttribute: [ + { attributes: { [JobTitleAttribute.id]: "Sales Manager" } }, + { attributes: { [JobTitleAttribute.id]: "Engineer" } }, + { attributes: {} }, + ], + }); + + const attributesQueryValue = buildQueryValue({ + rules: [ + { + raqbFieldId: JobTitleAttribute.id, + value: ["sales manager"], + operator: "not_equal", + valueSrc: ["value"], + valueType: ["text"], + }, + ], + }) as AttributesQueryValue; + + const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogic({ + dynamicFieldValueOperands: { fields: [], response: {} }, + attributesQueryValue, + teamId: testFixtures.team.id, + orgId: testFixtures.org.id, + }); + + expect(result).toEqual( + expect.arrayContaining([ + { userId: createdUsers[1].userId, result: RaqbLogicResult.MATCH }, + { userId: createdUsers[2].userId, result: RaqbLogicResult.MATCH }, + ]) + ); + expect(result).not.toContainEqual({ userId: createdUsers[0].userId, result: RaqbLogicResult.MATCH }); + }); + }); + + describe("TEXT not_like", () => { + it("should match users without the attribute (undefined not contains 'engineer' is true)", async () => { + const JobTitleAttribute = { + id: "job-title-attr-2", + name: "Job Title", + type: AttributeType.TEXT, + slug: "job-title-2", + options: [ + { id: "job-sr-eng", value: "Senior Engineer", slug: "senior-engineer" }, + { id: "job-designer", value: "Designer", slug: "designer" }, + ], + }; + + const { createdUsers } = await createAttributesScenario({ + attributes: [JobTitleAttribute], + teamMembersWithAttributeOptionValuePerAttribute: [ + { attributes: { [JobTitleAttribute.id]: "Senior Engineer" } }, + { attributes: { [JobTitleAttribute.id]: "Designer" } }, + { attributes: {} }, + ], + }); + + const attributesQueryValue = buildQueryValue({ + rules: [ + { + raqbFieldId: JobTitleAttribute.id, + value: ["engineer"], + operator: "not_like", + valueSrc: ["value"], + valueType: ["text"], + }, + ], + }) as AttributesQueryValue; + + const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogic({ + dynamicFieldValueOperands: { fields: [], response: {} }, + attributesQueryValue, + teamId: testFixtures.team.id, + orgId: testFixtures.org.id, + }); + + expect(result).toEqual( + expect.arrayContaining([ + { userId: createdUsers[1].userId, result: RaqbLogicResult.MATCH }, + { userId: createdUsers[2].userId, result: RaqbLogicResult.MATCH }, + ]) + ); + expect(result).not.toContainEqual({ userId: createdUsers[0].userId, result: RaqbLogicResult.MATCH }); + }); + }); + + describe("NUMBER not_equal", () => { + it("should match users without the attribute (undefined != '5' is true)", async () => { + const ExperienceAttribute = { + id: "exp-attr", + name: "Experience Years", + type: AttributeType.NUMBER, + slug: "experience-years", + options: [ + { id: "exp-5", value: "5", slug: "5" }, + { id: "exp-10", value: "10", slug: "10" }, + ], + }; + + const { createdUsers } = await createAttributesScenario({ + attributes: [ExperienceAttribute], + teamMembersWithAttributeOptionValuePerAttribute: [ + { attributes: { [ExperienceAttribute.id]: "5" } }, + { attributes: { [ExperienceAttribute.id]: "10" } }, + { attributes: {} }, + ], + }); + + const attributesQueryValue = buildQueryValue({ + rules: [ + { + raqbFieldId: ExperienceAttribute.id, + value: ["5"], + operator: "not_equal", + valueSrc: ["value"], + valueType: ["number"], + }, + ], + }) as AttributesQueryValue; + + const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogic({ + dynamicFieldValueOperands: { fields: [], response: {} }, + attributesQueryValue, + teamId: testFixtures.team.id, + orgId: testFixtures.org.id, + }); + + expect(result).toEqual( + expect.arrayContaining([ + { userId: createdUsers[1].userId, result: RaqbLogicResult.MATCH }, + { userId: createdUsers[2].userId, result: RaqbLogicResult.MATCH }, + ]) + ); + expect(result).not.toContainEqual({ userId: createdUsers[0].userId, result: RaqbLogicResult.MATCH }); + }); + }); + }); + + describe("compound rules with users missing some attributes", () => { + const DepartmentAttribute = { + id: "dept-attr-2", + name: "Department", + type: AttributeType.SINGLE_SELECT, + slug: "department-2", + options: [ + { id: "dept-sales-2", value: "Sales", slug: "sales" }, + { id: "dept-eng-2", value: "Engineering", slug: "engineering" }, + ], + }; + + const LocationAttribute = { + id: "loc-attr-2", + name: "Location", + type: AttributeType.SINGLE_SELECT, + slug: "location-2", + options: [ + { id: "loc-nyc-2", value: "NYC", slug: "nyc" }, + { id: "loc-la-2", value: "LA", slug: "la" }, + ], + }; + + it("positive AND negation: should match user with one attribute but missing the negated one", async () => { + const { createdUsers } = await createAttributesScenario({ + attributes: [DepartmentAttribute, LocationAttribute], + teamMembersWithAttributeOptionValuePerAttribute: [ + { + attributes: { [DepartmentAttribute.id]: "Sales", [LocationAttribute.id]: "NYC" }, + }, + { attributes: { [DepartmentAttribute.id]: "Sales" } }, + { attributes: {} }, + ], + }); + + const attributesQueryValue = buildSelectTypeFieldQueryValue({ + rules: [ + { + raqbFieldId: DepartmentAttribute.id, + value: ["dept-sales-2"], + operator: "select_equals", + }, + { + raqbFieldId: LocationAttribute.id, + value: ["loc-nyc-2"], + operator: "select_not_equals", + }, + ], + }) as AttributesQueryValue; + + const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogic({ + dynamicFieldValueOperands: { fields: [], response: {} }, + attributesQueryValue, + teamId: testFixtures.team.id, + orgId: testFixtures.org.id, + }); + + // User 0: Dept=Sales (== matches) AND Location=NYC (!= fails) -> NO MATCH + // User 1: Dept=Sales (== matches) AND Location=undefined (!= matches) -> MATCH + // User 2: no Dept (== fails) -> NO MATCH (AND short-circuits) + expect(result).toEqual([{ userId: createdUsers[1].userId, result: RaqbLogicResult.MATCH }]); + }); + + it("multiple negation rules: should match users missing different attributes", async () => { + const { createdUsers } = await createAttributesScenario({ + attributes: [DepartmentAttribute, LocationAttribute], + teamMembersWithAttributeOptionValuePerAttribute: [ + { + attributes: { [DepartmentAttribute.id]: "Sales", [LocationAttribute.id]: "NYC" }, + }, + { attributes: { [DepartmentAttribute.id]: "Engineering" } }, + { attributes: { [LocationAttribute.id]: "LA" } }, + { attributes: {} }, + ], + }); + + const attributesQueryValue = buildSelectTypeFieldQueryValue({ + rules: [ + { + raqbFieldId: DepartmentAttribute.id, + value: ["dept-sales-2"], + operator: "select_not_equals", + }, + { + raqbFieldId: LocationAttribute.id, + value: ["loc-nyc-2"], + operator: "select_not_equals", + }, + ], + }) as AttributesQueryValue; + + const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogic({ + dynamicFieldValueOperands: { fields: [], response: {} }, + attributesQueryValue, + teamId: testFixtures.team.id, + orgId: testFixtures.org.id, + }); + + // User 0: Dept=Sales (!= fails) -> NO MATCH + // User 1: Dept=Engineering (!= matches) AND Location=undefined (!= matches) -> MATCH + // User 2: Dept=undefined (!= matches) AND Location=LA (!= matches) -> MATCH + // User 3: both undefined (both != match) -> MATCH + expect(result).toEqual( + expect.arrayContaining([ + { userId: createdUsers[1].userId, result: RaqbLogicResult.MATCH }, + { userId: createdUsers[2].userId, result: RaqbLogicResult.MATCH }, + { userId: createdUsers[3].userId, result: RaqbLogicResult.MATCH }, + ]) + ); + expect(result).not.toContainEqual({ userId: createdUsers[0].userId, result: RaqbLogicResult.MATCH }); + }); + }); }); From 20dcef6680ddf72f8524fb964d4b86556bd02000 Mon Sep 17 00:00:00 2001 From: Deepanshu <140803342+deepanshurajput0@users.noreply.github.com> Date: Tue, 17 Feb 2026 22:12:38 +0530 Subject: [PATCH 2/3] fix: validate schedule title input to block invalid characters (#27818) * fix(availability): validate schedule title input to block invalid characters * fix: add english translation for invalid_characters_in_name * fix(schedule): allow Unicode characters in schedule name validation * fix(schedule): update validation message to match regex * fix: tsconfig target * fix(schedule): add required validation in NewScheduleButton using translation * fix(schedule): allow ASCII apostrophe in schedule name validation * Update ScheduleListItem.tsx * fix: remove unused @ts-expect-error directives * fix: remove tsconfig target change * Revert "fix: remove tsconfig target change" This reverts commit d4992caf9b71cdfe32ac9db9a509128548097a30. * Revert "fix: remove unused @ts-expect-error directives" This reverts commit 913eda500aeccdd67b15cf1898f6db376bf3dda9. * fix: update validation regex to support unicode characters * fix: remove es6 from tsconfig * Update NewScheduleButton.tsx --------- Co-authored-by: Deepanshu Verma Co-authored-by: Sahitya Chandra --- .../modules/schedules/components/NewScheduleButton.tsx | 8 ++++++++ apps/web/public/static/locales/en/common.json | 1 + 2 files changed, 9 insertions(+) diff --git a/apps/web/modules/schedules/components/NewScheduleButton.tsx b/apps/web/modules/schedules/components/NewScheduleButton.tsx index d63505949c1eb8..0c43e1cd445c0e 100644 --- a/apps/web/modules/schedules/components/NewScheduleButton.tsx +++ b/apps/web/modules/schedules/components/NewScheduleButton.tsx @@ -79,6 +79,14 @@ export function NewScheduleButton({ placeholder={t("default_schedule_name")} {...register("name", { setValueAs: (v) => (!v || v.trim() === "" ? null : v), + required:t('required'), + pattern:{ + value: new RegExp( + "^[\\p{L}\\p{M}\\p{N}\\s&\\-_'\\u2018\\u2019@.:,/]+$", + "u" + ), + message:t("invalid_characters_in_name"), + } })} /> diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 783a940ded1a2f..dda7996d16b4ef 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -9,6 +9,7 @@ "upgrade_now": "Upgrade now", "untitled": "Untitled", "accept_invitation": "Accept invitation", + "invalid_characters_in_name": "Only letters, numbers, spaces, and supported punctuation are allowed.", "max_characters": "Max. characters", "min_characters": "Min. characters", "min_characters_required": "Min. {{count}} characters required", From 3cfe295ed5e90b9d47a2ddc3987bd21b1f58c775 Mon Sep 17 00:00:00 2001 From: Shrey-Sutariya <137723744+Shrey-Sutariya@users.noreply.github.com> Date: Tue, 17 Feb 2026 22:21:21 +0530 Subject: [PATCH 3/3] Commit 1 (#28011) Co-authored-by: Romit <85230081+romitg2@users.noreply.github.com> --- .../settings/(settings-layout)/SettingsLayoutAppDirClient.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsx index 2a7128d6cad5e3..2f12e25d6df00f 100644 --- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsx +++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsx @@ -824,7 +824,7 @@ const SettingsSidebarContainer = ({ href={child.href || "/"} trackingMetadata={child.trackingMetadata} textClassNames="text-emphasis font-medium text-sm" - className={`px-2! py-1! min-h-7 h-auto w-fit ${ + className={`px-2! py-1! min-h-7 h-auto w-full ${ tab.children && index === tab.children?.length - 1 && "mb-3!" }`} disableChevron