Skip to content

feat: add IS/IS_NOT option in filters#18426

Open
bugisthegod wants to merge 6 commits intotwentyhq:mainfrom
bugisthegod:feat/add-is-isnot-operand
Open

feat: add IS/IS_NOT option in filters#18426
bugisthegod wants to merge 6 commits intotwentyhq:mainfrom
bugisthegod:feat/add-is-isnot-operand

Conversation

@bugisthegod
Copy link
Contributor

@bugisthegod bugisthegod commented Mar 5, 2026

Summary

Add IS/IS_NOT option for Full name, text, phone, Links , email. Fixes twentyhq/core-team-issues#2314

In the advanced filter, only show is/is_not option in phone (primaryPhoneNumber), Links (primaryLinkUrl), email (primaryEmail).

name.is.option.mp4
Recording.at.2026-03-05.14.27.51.mp4

Note: test file getOperandsForFilterType.test.ts is misplaced (should be under record-filter/utils/tests/) . It seems it was forgotten when moving — leftover from #9604 .

Question For the Maintainer

  1. Should DOES_NOT_CONTAIN include records where the field is NULL? Currently it doesn't — only records with an existing value that doesn't match are returned. I'm making IS_NOT include NULLs, so wanted to confirm the intended behavior for consistency.

  2. Debounce problem. When a user types quickly (e.g. "Google"), the filter fires on every keystroke, causing results to momentarily include values that should be excluded. Should we add debounce to filter inputs?

type.fast.mp4
  1. Should we extract the PHONES, full_name, address filter logic into its own util (like computeGqlOperationFilterForPhones.ts, based on PR Implemented LINKS and EMAILS sub-field fitering #11984 which want more readable and maintainable), consistent with how EMAILS and LINKS are structured? If yes, submit it in this pr or open a separate PR for that.

Copilot AI review requested due to automatic review settings March 5, 2026 14:29
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 6 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts">

<violation number="1" location="packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts:699">
P1: FULL_NAME IS/IS_NOT operand parsing is whitespace-fragile: uses `split(' ')` without trimming, causing exact-match `eq` comparisons to fail when input has extra spaces (e.g., "John  Doe" produces `lastPart=" Doe"` with leading space)</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 5, 2026

Greptile Summary

This PR adds IS and IS_NOT filter operands for TEXT, FULL_NAME, PHONES (primaryPhoneNumber), LINKS (primaryLinkUrl), and EMAILS (primaryEmail) fields, exposing them in both the standard filter bar and the advanced filter UI where appropriate.

Key changes:

  • FILTER_OPERANDS_MAP and COMPOSITE_FIELD_FILTER_OPERANDS_MAP updated with IS/IS_NOT for all affected types.
  • computeGqlOperationFilterForEmails and computeGqlOperationFilterForLinks extended with IS/IS_NOT switch cases (for subfield and top-level paths).
  • turnRecordFilterIntoGqlOperationFilter extended for TEXT (simple eq/not+NULL) and FULL_NAME (single-word OR match; multi-word firstName+lastName split) and PHONES subfield.
  • Tests added for TEXT, FULL_NAME (single/multi-word), and all three composite subfields.

Critical issue: FILTER_OPERANDS_MAP.PHONES now includes IS/IS_NOT (making them available for a top-level, non-subfield PHONES filter), but the implementation in turnRecordFilterIntoGqlOperationFilter.ts only handles IS/IS_NOT inside the switch (subFieldName) block (i.e. only when isSubFieldFilter is true). The !isSubFieldFilter branch for PHONES has no IS/IS_NOT cases and will throw a runtime error. There is no test covering this path, which masked the gap.

Confidence Score: 2/5

  • Not safe to merge — top-level PHONES IS/IS_NOT is exposed in the UI operands list but throws a runtime error in the filter computation layer.
  • The IS/IS_NOT logic is correctly implemented for EMAILS, LINKS, TEXT, and FULL_NAME, and the PHONES subfield (primaryPhoneNumber) path works. However, the top-level PHONES IS/IS_NOT case is entirely unhandled in turnRecordFilterIntoGqlOperationFilter.ts and will throw for any user who selects IS/IS_NOT on a phone field without using the subfield picker. The missing test coverage masked this gap.
  • packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts — the PHONES top-level switch needs IS/IS_NOT cases added.

Important Files Changed

Filename Overview
packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts Adds IS/IS_NOT handling for TEXT, FULL_NAME (with multi-word split), and PHONES (subfield only). Critical gap: top-level PHONES IS/IS_NOT cases are missing from the non-subfield branch, causing a runtime throw when those operands are used without a subfield.
packages/twenty-front/src/modules/object-record/record-filter/utils/getRecordFilterOperands.ts Correctly adds IS/IS_NOT to FILTER_OPERANDS_MAP for TEXT, EMAILS, FULL_NAME, LINKS, PHONES and populates COMPOSITE_FIELD_FILTER_OPERANDS_MAP for primaryPhoneNumber, primaryEmail, primaryLinkUrl. Top-level PHONES IS/IS_NOT exposure is inconsistent with the missing implementation in the filter computation layer.
packages/twenty-shared/src/utils/filter/compute-record-gql-operation-filter/for-composite-field/computeGqlOperationFilterForEmails.ts Adds IS/IS_NOT for primaryEmail (subfield and top-level). Logic is correct and mirrors existing DOES_NOT_CONTAIN NULL-handling pattern.
packages/twenty-shared/src/utils/filter/compute-record-gql-operation-filter/for-composite-field/computeGqlOperationFilterForLinks.ts Adds IS/IS_NOT for primaryLinkLabel/primaryLinkUrl (subfield) and top-level primaryLinkUrl. IS/IS_NOT silently applies to primaryLinkLabel even though it's not exposed in the operands map.
packages/twenty-shared/src/utils/filter/tests/turnRecordFilterIntoGqlOperationFilter.test.ts Good coverage added for TEXT, FULL_NAME (single/multi-word), PHONES subfield, EMAILS subfield, LINKS subfield. Missing tests for top-level PHONES IS/IS_NOT without a subfield, which would expose the runtime error in the implementation.
packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/tests/getOperandsForFilterType.test.ts Test cases correctly updated for the new IS/IS_NOT operands across TEXT, FULL_NAME, LINKS, EMAILS, PHONES field types and their subfields.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[User selects IS / IS_NOT operand] --> B{Field type?}
    B -->|TEXT| C[turnRecordFilter: eq / not+NULL]
    B -->|FULL_NAME| D{Has subfield?}
    D -->|No| E[Split value: firstName OR lastName OR firstName+lastName combo]
    D -->|Yes| F[eq on subFieldName / not+NULL on subFieldName]
    B -->|EMAILS| G[computeGqlOperationFilterForEmails]
    G -->|primaryEmail subfield or top-level| H[primaryEmail eq / not+NULL]
    B -->|LINKS| I[computeGqlOperationFilterForLinks]
    I -->|primaryLinkUrl or primaryLinkLabel subfield| J[subfield eq / not+NULL]
    I -->|top-level| K[primaryLinkUrl eq / not+NULL]
    B -->|PHONES subfield primaryPhoneNumber| L[primaryPhoneNumber eq / not+NULL]
    B -->|PHONES top-level| M[⚠️ NOT IMPLEMENTED → throws runtime error]

    style M fill:#f55,color:#fff
Loading

Comments Outside Diff (3)

  1. packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts, line 1432-1508 (link)

    Missing IS/IS_NOT handling for top-level PHONES filter

    The top-level PHONES branch (i.e. !isSubFieldFilter) only handles CONTAINS and DOES_NOT_CONTAIN. It falls through to the default throw for every other operand. However, FILTER_OPERANDS_MAP.PHONES in getRecordFilterOperands.ts now includes IS and IS_NOT, so the UI will surface those operands for a top-level PHONES field. Selecting them at runtime will throw:

    Error: Unknown operand IS for PHONES filter
    

    The subfield-scoped IS/IS_NOT (for primaryPhoneNumber) is correctly implemented in the switch (subFieldName) block below, but the guard that sends execution into that block is if (!isSubFieldFilter) which short-circuits for any top-level filter before reaching the subfield switch.

    Either:

    1. Add IS / IS_NOT cases to the !isSubFieldFilter switch that delegate to primaryPhoneNumber (consistent with how IS for top-level EMAILS/LINKS already delegates), or
    2. Remove IS / IS_NOT from FILTER_OPERANDS_MAP.PHONES (keeping them only in COMPOSITE_FIELD_FILTER_OPERANDS_MAP.PHONES.primaryPhoneNumber).

    The same gap is not present for EMAILS and LINKS because those field types delegate to computeGqlOperationFilterForEmails / computeGqlOperationFilterForLinks which handle all operands in one place. For PHONES the logic is inline and was only extended inside the subfield switch.

  2. packages/twenty-shared/src/utils/filter/__tests__/turnRecordFilterIntoGqlOperationFilter.test.ts, line 879-924 (link)

    No test for top-level PHONES IS/IS_NOT (no subfield)

    All new PHONES tests exercise IS/IS_NOT exclusively via 'primaryPhoneNumber' subfield. There is no test covering the top-level PHONES filter (subfield = undefined). This gap masks the runtime error described in the implementation comment (the top-level switch does not handle these operands). A test like the one below would immediately expose the issue:

    it('should handle IS on top-level PHONES', () => {
      const result = turnRecordFilterIntoRecordGqlOperationFilter({
        filterValueDependencies,
        recordFilter: makeFilter(
          'f-phones',
          RecordFilterOperand.IS,
          '5551234',
          'PHONES',
          // no subFieldName
        ),
        fieldMetadataItems: fields,
      });
      // define expected output once the implementation is added
      expect(result).toBeDefined();
    });

    Context Used: Rule from dashboard - Always consider adding tests for new functionality, especially for edge cases like empty responses. (source)

  3. packages/twenty-shared/src/utils/filter/compute-record-gql-operation-filter/for-composite-field/computeGqlOperationFilterForLinks.ts, line 27-82 (link)

    IS/IS_NOT applied to primaryLinkLabel despite no operand support

    The case 'primaryLinkLabel': case 'primaryLinkUrl': block now handles IS and IS_NOT. The COMPOSITE_FIELD_FILTER_OPERANDS_MAP.LINKS only exposes IS/IS_NOT for primaryLinkUrl, so the UI will never surface them for primaryLinkLabel. The code silently accepts IS/IS_NOT for primaryLinkLabel subfield filters at the API level (e.g. via a direct API call or future UI change), which may not be intended. Consider either restricting the IS/IS_NOT cases to primaryLinkUrl only by splitting the switch branches, or explicitly documenting that this is intentional.

Last reviewed commit: 1941ecc

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds support for exact-match filtering via new IS / IS_NOT operands across multiple filter types, and updates UI operand availability to expose these where appropriate (notably in advanced filters for specific composite subfields).

Changes:

  • Implement IS / IS_NOT translation to GQL operation filters for TEXT, FULL_NAME, and selected composite subfields (primaryEmail, primaryLinkUrl, primaryPhoneNumber).
  • Update frontend operand selection logic to include IS / IS_NOT for relevant filter types, with advanced-filter-specific subfield restrictions for Emails/Links/Phones.
  • Add/extend unit tests to cover the new operand behaviors.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts Adds GQL filter generation for IS / IS_NOT (notably TEXT, FULL_NAME, and PHONES.primaryPhoneNumber).
packages/twenty-shared/src/utils/filter/compute-record-gql-operation-filter/for-composite-field/computeGqlOperationFilterForLinks.ts Adds IS / IS_NOT support for Links composite fields/subfields.
packages/twenty-shared/src/utils/filter/compute-record-gql-operation-filter/for-composite-field/computeGqlOperationFilterForEmails.ts Adds IS / IS_NOT support for Emails composite fields/subfields.
packages/twenty-shared/src/utils/filter/tests/turnRecordFilterIntoGqlOperationFilter.test.ts Adds test coverage for new IS / IS_NOT operand mappings.
packages/twenty-front/src/modules/object-record/record-filter/utils/getRecordFilterOperands.ts Exposes IS / IS_NOT operands in the UI, with subfield-specific restrictions for advanced filtering.
packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/tests/getOperandsForFilterType.test.ts Updates operand expectations in tests to include the new operands.
Comments suppressed due to low confidence (1)

packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/tests/getOperandsForFilterType.test.ts:23

  • This test suite validates getRecordFilterOperands, but it lives under object-filter-dropdown/utils/__tests__ and still uses the legacy getOperandsForFilterType naming. Moving/renaming it to record-filter/utils/__tests__ (e.g. getRecordFilterOperands.test.ts) would make it easier to discover and aligns the test location with the unit under test (as noted in the PR description).
  const isOperands = [RecordFilterOperand.IS, RecordFilterOperand.IS_NOT];

  const numberOperands = [
    RecordFilterOperand.IS,
    RecordFilterOperand.IS_NOT,
    RecordFilterOperand.GREATER_THAN_OR_EQUAL,

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts">

<violation number="1" location="packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts:1540">
P1: Subfield phone filters can generate wildcard '%%' queries when normalized values are empty but original values contain non-digit characters. The subfield branch lacks the empty-value guard that exists in the non-subfield branch (if (!isNonEmptyString(filterValue)) { return; }), allowing queries like `like '%%'` that can incorrectly match all records and cause expensive broad scans.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

…lOperationFilter.ts

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
@bugisthegod bugisthegod changed the title feat: add IS/IS_NOT operand feat: add IS/IS_NOT option in filters Mar 5, 2026
@bugisthegod
Copy link
Contributor Author

Hi @Bonapara , could you take a look at this PR when you get a chance? Let me know if anything needs to be changed. Thanks!

@lucasbordeau lucasbordeau self-requested a review March 10, 2026 15:33
@lucasbordeau lucasbordeau self-assigned this Mar 10, 2026
@lucasbordeau
Copy link
Contributor

I'll check if that is corresponding to the initial need, after reading the issue and testing it, I'm not sure.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Filter with exact search - Add "IS" option instead of "CONTAINS" in the Filters

3 participants