Added a util helper to remove accent and case to improve search logic (#14533)
# [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](https://github.com/user-attachments/assets/7035906c-9c5d-414d-81e5-74e53803628a) --- Opening a draft pr for initial strategy review before extending it to other components. --------- Co-authored-by: Félix Malfait <felix@twenty.com> Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
import { normalizeSearchText } from '../normalizeSearchText';
|
||||
|
||||
describe('normalizeSearchText', () => {
|
||||
it('should handle basic ASCII text', () => {
|
||||
expect(normalizeSearchText('Hello World')).toBe('hello world');
|
||||
expect(normalizeSearchText('TEST')).toBe('test');
|
||||
expect(normalizeSearchText('MixedCase123')).toBe('mixedcase123');
|
||||
});
|
||||
|
||||
it('should remove accents from various languages', () => {
|
||||
expect(normalizeSearchText('café')).toBe('cafe');
|
||||
expect(normalizeSearchText('naïve')).toBe('naive');
|
||||
expect(normalizeSearchText('résumé')).toBe('resume');
|
||||
expect(normalizeSearchText('Zürich')).toBe('zurich');
|
||||
expect(normalizeSearchText('Müller')).toBe('muller');
|
||||
expect(normalizeSearchText('niño')).toBe('nino');
|
||||
expect(normalizeSearchText('España')).toBe('espana');
|
||||
expect(normalizeSearchText('São Paulo')).toBe('sao paulo');
|
||||
expect(normalizeSearchText('João')).toBe('joao');
|
||||
expect(normalizeSearchText('Åse')).toBe('ase');
|
||||
expect(normalizeSearchText('Øyvind')).toBe('oyvind');
|
||||
});
|
||||
|
||||
it('should handle Nordic and Germanic special characters', () => {
|
||||
expect(normalizeSearchText('Øyvind')).toBe('oyvind');
|
||||
expect(normalizeSearchText('Åse')).toBe('ase');
|
||||
expect(normalizeSearchText('Æther')).toBe('aether');
|
||||
expect(normalizeSearchText('Straße')).toBe('strasse');
|
||||
expect(normalizeSearchText('Łódź')).toBe('lodz');
|
||||
expect(normalizeSearchText('Œuvre')).toBe('oeuvre');
|
||||
});
|
||||
|
||||
it('should handle mixed case with accents', () => {
|
||||
expect(normalizeSearchText('CAFÉ')).toBe('cafe');
|
||||
expect(normalizeSearchText('NaÏvE')).toBe('naive');
|
||||
expect(normalizeSearchText('ZÜRICH')).toBe('zurich');
|
||||
});
|
||||
|
||||
it('should handle null and undefined gracefully', () => {
|
||||
expect(normalizeSearchText(null)).toBe('');
|
||||
expect(normalizeSearchText(undefined)).toBe('');
|
||||
});
|
||||
|
||||
it('should handle empty strings', () => {
|
||||
expect(normalizeSearchText('')).toBe('');
|
||||
expect(normalizeSearchText(' ')).toBe(' ');
|
||||
});
|
||||
|
||||
it('should handle special characters and numbers', () => {
|
||||
expect(normalizeSearchText('user@example.com')).toBe('user@example.com');
|
||||
expect(normalizeSearchText('123-456-7890')).toBe('123-456-7890');
|
||||
expect(normalizeSearchText('Café #1')).toBe('cafe #1');
|
||||
});
|
||||
|
||||
it('should handle complex Unicode combinations', () => {
|
||||
expect(normalizeSearchText('e\u0301')).toBe('e');
|
||||
expect(normalizeSearchText('a\u0300\u0301')).toBe('a');
|
||||
expect(normalizeSearchText('é')).toBe('e');
|
||||
expect(normalizeSearchText('e\u0301')).toBe('e');
|
||||
});
|
||||
|
||||
it('should be consistent for search matching', () => {
|
||||
const searchTerm = 'cafe';
|
||||
const names = ['Café', 'CAFE', 'cafe', 'Cafe'];
|
||||
|
||||
names.forEach((name) => {
|
||||
expect(normalizeSearchText(name)).toContain(searchTerm);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
export const normalizeSearchText = (
|
||||
text: string | null | undefined,
|
||||
): string => {
|
||||
if (!text) return '';
|
||||
return text
|
||||
.normalize('NFD')
|
||||
.replace(/\p{Diacritic}/gu, '')
|
||||
.replace(/[øØ]/g, 'o')
|
||||
.replace(/[åÅ]/g, 'a')
|
||||
.replace(/[æÆ]/g, 'ae')
|
||||
.replace(/[ßẞ]/g, 'ss')
|
||||
.replace(/[ðÐ]/g, 'd')
|
||||
.replace(/[þÞ]/g, 'th')
|
||||
.replace(/[łŁ]/g, 'l')
|
||||
.replace(/[œŒ]/g, 'oe')
|
||||
.toLowerCase();
|
||||
};
|
||||
Reference in New Issue
Block a user