-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Added a util helper to remove accent and case to improve search logic #14533
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Added a util helper to remove accent and case to improve search logic #14533
Conversation
🚀 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. |
There was a problem hiding this 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 formremoveAccentsAndCase
: 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
const searchNormalized = normalizeSearchText(search.trim()); | ||
return normalizeSearchText(concatenatedString).includes(searchNormalized); |
There was a problem hiding this comment.
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()); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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
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); |
There was a problem hiding this comment.
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
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'; |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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
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) | |
); | |
}); | |
}; |
const optimizedWorkspaceMembers = useMemo(() => { | ||
if (!searchFilter.trim() || searchFilter === debouncedSearchFilter) { | ||
return workspaceMembers; |
There was a problem hiding this comment.
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.
return searchTerms.every( | ||
(term) => | ||
firstName.includes(term) || | ||
lastName.includes(term) || | ||
fullName.includes(term) || | ||
email.includes(term), | ||
); |
There was a problem hiding this comment.
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)
const searchNormalized = normalizeSearchText(searchTerm); | ||
return ( | ||
normalizeSearchText(item.label).includes(searchNormalized) || | ||
normalizeSearchText(item.dataType).includes(searchNormalized) | ||
); |
There was a problem hiding this comment.
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
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], | |
); |
const searchNormalized = normalizeSearchText(searchTerm); | ||
return ( | ||
normalizeSearchText(item.label).includes(searchNormalized) || | ||
normalizeSearchText(item.dataType).includes(searchNormalized) | ||
); |
There was a problem hiding this comment.
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
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], | |
); |
There was a problem hiding this 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(); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[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:
removeAccentsAndCase(text: string)
.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.