Skip to content

Conversation

SaurabhJain708
Copy link
Contributor

[WIP] Make Local Search Non-Accent-Sensitive

fixes(#14468)


Description:
This PR adds accent- and case-insensitive search for the SettingsWorkspaceMembers component’s local search. It ensures that searching for names or emails works correctly even if the user types letters without accents.

Implementation:

  • Added a helper util: removeAccentsAndCase(text: string)
  • This util:
    • Normalizes text to NFD form
    • Removes diacritics (accents)
    • Converts text to lowercase
  • Replaced current .includes(searchFilter) logic with a normalized comparison:

Video ->

Screencast.from.2025-09-16.16-42-50.webm

Opening a draft pr for initial strategy review before extending it to other components.

Copy link
Contributor

github-actions bot commented Sep 16, 2025

🚀 Preview Environment Ready!

Your preview environment is available at: http://bore.pub:55351

This environment will automatically shut down when the PR is closed or after 5 hours.

@FelixMalfait FelixMalfait self-assigned this Sep 19, 2025
@FelixMalfait FelixMalfait marked this pull request as ready for review September 19, 2025 08:29
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

Greptile Summary

This PR implements comprehensive accent and case-insensitive search functionality across the Twenty application by introducing text normalization utilities and updating search logic in multiple components. The implementation addresses issue #14468 to improve search experience for international users who may type without accents but expect to find results containing accented characters.

Core Implementation:
The PR introduces two utility functions for text normalization:

  • normalizeSearchText: Handles null/undefined inputs gracefully and normalizes text using Unicode NFD form
  • removeAccentsAndCase: Similar functionality but without null handling

Both utilities use Unicode normalization (NFD) to decompose accented characters, then remove diacritics using the \p{Diacritic} regex pattern and convert to lowercase.

Scope of Changes:
The changes affect numerous search interfaces across the application, including:

  • Settings components (workspace members, roles, AI agents, data model objects)
  • Form inputs (SelectInput, MultiSelectInput, spreadsheet import)
  • Command menu search functionality
  • Admin configuration variables

Integration Pattern:
Each component follows a consistent pattern where the previous simple case-insensitive search logic (toLowerCase().includes()) is replaced with normalized text comparison. The implementation normalizes both search terms and target text before comparison.

Special Considerations:
The SettingsWorkspaceMembers component includes both client-side and server-side filtering with debounced queries to balance immediate responsiveness with comprehensive search accuracy. Multi-word search support is also implemented by splitting search terms.

Confidence score: 3/5

  • This PR has implementation inconsistencies and potential issues that need attention before merging
  • Score reflects concerns about code duplication between utility functions, performance implications of repeated normalization, and missing comprehensive test coverage
  • Pay close attention to the duplicate utility functions and potential performance impact in components with large datasets

Context used:

Context - Follow the convention of having only one utility function per exported file in the project. (link)
Context - Always consider adding tests for new functionality, especially for edge cases like empty responses. (link)

18 files reviewed, 14 comments

Edit Code Review Bot Settings | Greptile

Comment on lines +9 to +10
const searchNormalized = normalizeSearchText(search.trim());
return normalizeSearchText(concatenatedString).includes(searchNormalized);
Copy link
Contributor

Choose a reason for hiding this comment

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

style: Performance consideration: normalizeSearchText is called twice per comparison. Consider pre-normalizing the search term once in the parent function.


const checkInShortcuts = (action: ActionConfig, search: string) => {
const concatenatedString = action.hotKeys?.join('') ?? '';
return concatenatedString.toLowerCase().includes(search.toLowerCase().trim());
const searchNormalized = normalizeSearchText(search.trim());
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: Inconsistent trimming: search is trimmed here but not in checkInLabels function on line 16.

field.name.toLowerCase().includes(searchFilter.toLowerCase()),
);
availableFieldMetadataItems.filter((field) => {
const searchTerm = normalizeSearchText(searchFilter);
Copy link
Contributor

Choose a reason for hiding this comment

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

style: Consider extracting normalizeSearchText(searchFilter) outside the filter callback to avoid repeated computation for each field item

Suggested change
const searchTerm = normalizeSearchText(searchFilter);
const filteredAvailableFieldMetadataItems = (() => {
const searchTerm = normalizeSearchText(searchFilter);
return availableFieldMetadataItems.filter((field) => {
return (
normalizeSearchText(field.label).includes(searchTerm) ||
normalizeSearchText(field.name).includes(searchTerm)
);
});
})();

@@ -54,9 +55,10 @@ export const SelectInput = ({
const optionsToSelect = useMemo(
() =>
options.filter((option) => {
const searchTerm = normalizeSearchText(searchFilter);
Copy link
Contributor

Choose a reason for hiding this comment

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

style: Consider memoizing the normalized search term to avoid recalculating it for each option

Suggested change
const searchTerm = normalizeSearchText(searchFilter);
const optionsToSelect = useMemo(
() => {
const searchTerm = normalizeSearchText(searchFilter);
return options.filter((option) => {
return (
option.value !== selectedOption?.value &&
normalizeSearchText(option.label).includes(searchTerm)
);
}) || [];
},
[options, searchFilter, selectedOption?.value],
);

@@ -12,6 +12,7 @@ import {
useGetApiKeysQuery,
} from '~/generated-metadata/graphql';
import { type ApiKeyForRole } from '~/generated/graphql';
import { normalizeSearchText } from '~/utils/normalizeSearchText';
Copy link
Contributor

Choose a reason for hiding this comment

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

style: The codebase has duplicate utilities: normalizeSearchText and removeAccentsAndCase serve identical purposes. This violates the project convention of one utility per file.

Context Used: Context - Follow the convention of having only one utility function per exported file in the project. (link)

return options.filter(
(option) =>
return options.filter((option) => {
const searchNormalized = normalizeSearchText(searchFilter);
Copy link
Contributor

Choose a reason for hiding this comment

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

style: Consider moving the normalization of searchFilter outside the filter callback to avoid repeated normalization for each option

Suggested change
const searchNormalized = normalizeSearchText(searchFilter);
export const getSubFieldOptions = (
fieldMetadataItem: FieldMetadataItem,
options: readonly Readonly<SpreadsheetImportFieldOption>[],
searchFilter: string,
): readonly Readonly<SpreadsheetImportFieldOption>[] => {
const searchNormalized = normalizeSearchText(searchFilter);
return options.filter((option) => {
return (
option.fieldMetadataItemId === fieldMetadataItem.id &&
normalizeSearchText(option.label).includes(searchNormalized)
);
});
};

Comment on lines +218 to +220
const optimizedWorkspaceMembers = useMemo(() => {
if (!searchFilter.trim() || searchFilter === debouncedSearchFilter) {
return workspaceMembers;
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: This condition logic seems incorrect. When searchFilter === debouncedSearchFilter, you return workspaceMembers without client-side filtering, but server-side filtering may still be active. This could show incorrect results.

Comment on lines +232 to +238
return searchTerms.every(
(term) =>
firstName.includes(term) ||
lastName.includes(term) ||
fullName.includes(term) ||
email.includes(term),
);
Copy link
Contributor

Choose a reason for hiding this comment

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

style: Consider extracting this filtering logic to a utility function per coding guidelines

Context Used: Rule - Extract utility functions like 'includesExpectedScopes' to utils modules instead of keeping them in service classes. (link)

Comment on lines +162 to +166
const searchNormalized = normalizeSearchText(searchTerm);
return (
normalizeSearchText(item.label).includes(searchNormalized) ||
normalizeSearchText(item.dataType).includes(searchNormalized)
);
Copy link
Contributor

Choose a reason for hiding this comment

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

style: Consider extracting searchNormalized outside the filter callback to avoid recalculating it for each item, improving performance for large lists

Suggested change
const searchNormalized = normalizeSearchText(searchTerm);
return (
normalizeSearchText(item.label).includes(searchNormalized) ||
normalizeSearchText(item.dataType).includes(searchNormalized)
);
const filteredActiveItems = useMemo(
() => {
const searchNormalized = normalizeSearchText(searchTerm);
return sortedActiveObjectSettingsDetailItems.filter((item) => {
return (
normalizeSearchText(item.label).includes(searchNormalized) ||
normalizeSearchText(item.dataType).includes(searchNormalized)
);
});
},
[sortedActiveObjectSettingsDetailItems, searchTerm],
);

Comment on lines +174 to +178
const searchNormalized = normalizeSearchText(searchTerm);
return (
normalizeSearchText(item.label).includes(searchNormalized) ||
normalizeSearchText(item.dataType).includes(searchNormalized)
);
Copy link
Contributor

Choose a reason for hiding this comment

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

style: Same performance optimization applies here - extract searchNormalized outside the filter callback

Suggested change
const searchNormalized = normalizeSearchText(searchTerm);
return (
normalizeSearchText(item.label).includes(searchNormalized) ||
normalizeSearchText(item.dataType).includes(searchNormalized)
);
const filteredDisabledItems = useMemo(
() => {
const searchNormalized = normalizeSearchText(searchTerm);
return sortedDisabledObjectSettingsDetailItems.filter((item) => {
return (
normalizeSearchText(item.label).includes(searchNormalized) ||
normalizeSearchText(item.dataType).includes(searchNormalized)
);
});
},
[sortedDisabledObjectSettingsDetailItems, searchTerm],
);

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

This PR is being reviewed by Cursor Bugbot

Details

You are on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle.

To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

.normalize('NFD')
.replace(/\p{Diacritic}/gu, '')
.toLowerCase();
};
Copy link

Choose a reason for hiding this comment

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

Bug: Unused Function Duplicates Existing Functionality

The removeAccentsAndCase utility function is defined but unused. It duplicates normalizeSearchText's functionality, but without null/undefined input handling. This looks like accidentally committed development code.

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants