diff --git a/cypress/component/NcSelect.cy.ts b/cypress/component/NcSelect.cy.ts new file mode 100644 index 0000000000..9efaa6f3e4 --- /dev/null +++ b/cypress/component/NcSelect.cy.ts @@ -0,0 +1,74 @@ +import { mount } from 'cypress/vue2' +import NcSelect from '../../src/components/NcSelect/NcSelect.vue' + +describe('NcSelect', () => { + const mountSelect = () => mount(NcSelect, { + propsData: { + userSelect: true, + inputClass: 'cypress-search-input', + options: [ + { + id: '0-john', + displayName: 'John', + isNoUser: false, + subname: 'john@example.org', + icon: '', + }, + { + id: '0-emma', + displayName: 'Emma', + isNoUser: false, + subname: 'emma@example.org', + icon: '', + }, + { + id: '0-olivia', + displayName: 'Olivia', + isNoUser: false, + subname: 'olivia@example.org', + icon: '', + }, + ], + }, + }) + + it('has options', () => { + mountSelect() + + cy.get('.select').click() + cy.contains('.option', 'Olivia').should('exist') + cy.contains('.option', 'John').should('exist') + cy.contains('.option', 'Emma').should('exist') + }) + + it('can filter by name', () => { + mountSelect() + + cy.get('.cypress-search-input').scrollIntoView().type('Em') + cy.contains('.option', 'Emma').should('exist') + cy.document().find('.option').should('have.length', 1) + }) + + it('can filter by mail', () => { + mountSelect() + + cy.get('.cypress-search-input').scrollIntoView().type('olivia@example') + cy.contains('.option', 'Olivia').should('exist') + cy.document().find('.option').should('have.length', 1) + }) + + it('can filter by mail in ticks', () => { + mountSelect() + + // Until this it should not be visible + cy.get('.cypress-search-input').clear().type('O. <') + cy.contains('.option', 'Olivia').should('not.exist') + // now it should match + cy.get('.cypress-search-input').type('olivia') + cy.contains('.option', 'Olivia').should('exist') + // and with full search query it should also exist + cy.get('.cypress-search-input').type('@example.org>') + cy.contains('.option', 'Olivia').should('exist') + cy.document().find('.option').should('have.length', 1) + }) +}) diff --git a/src/components/NcListItemIcon/NcListItemIcon.vue b/src/components/NcListItemIcon/NcListItemIcon.vue index 091c152ec5..4b5428daf8 100644 --- a/src/components/NcListItemIcon/NcListItemIcon.vue +++ b/src/components/NcListItemIcon/NcListItemIcon.vue @@ -139,11 +139,11 @@ It might be used for list rendering or within the multiselect for example
+ :search="searchParts[0]" /> + :search="searchParts[1]" /> {{ userStatus.icon }} {{ userStatus.message }} @@ -314,6 +314,21 @@ export default { '--margin': this.margin + 'px', } }, + + /** + * Seperates the search property into two parts, the first one is the search part on the name, the second on the subname. + * @return {[string, string]} + */ + searchParts() { + // Match the email notation like "Jane " with the email address as matching group + const EMAIL_NOTATION = /^([^<]*)<([^>]+)>?$/ + + const match = this.search.match(EMAIL_NOTATION) + if (this.isNoUser || !match) { + return [this.search, this.search] + } + return [match[1].trim(), match[2]] + }, }, beforeMount() { diff --git a/src/components/NcSelect/NcSelect.vue b/src/components/NcSelect/NcSelect.vue index 8c140baa5f..3ba6c27a88 100644 --- a/src/components/NcSelect/NcSelect.vue +++ b/src/components/NcSelect/NcSelect.vue @@ -951,14 +951,19 @@ export default { }, localFilterBy() { + // Match the email notation like "Jane " with the email address as matching group + const EMAIL_NOTATION = /[^<]*<([^>]+)/ + if (this.filterBy !== null) { return this.filterBy } if (this.userSelect) { return (option, label, search) => { - return (`${label} ${option.subname}` || '') - .toLocaleLowerCase() - .indexOf(search.toLocaleLowerCase()) > -1 + const match = search.match(EMAIL_NOTATION) + return (match && option.subname?.toLocaleLowerCase?.()?.indexOf(match[1].toLocaleLowerCase()) > -1) + || (`${label} ${option.subname}` + .toLocaleLowerCase() + .indexOf(search.toLocaleLowerCase()) > -1) } } return VueSelect.props.filterBy.default diff --git a/tests/unit/components/NcListItemIcon/list-item-icon.spec.ts b/tests/unit/components/NcListItemIcon/list-item-icon.spec.ts new file mode 100644 index 0000000000..7e3f8710b7 --- /dev/null +++ b/tests/unit/components/NcListItemIcon/list-item-icon.spec.ts @@ -0,0 +1,104 @@ +/** + * @copyright Copyright (c) 2023 Ferdinand Thiessen + * + * @author Ferdinand Thiessen + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import { shallowMount } from '@vue/test-utils' +import NcListItemIcon from '../../../../src/components/NcListItemIcon/NcListItemIcon.vue' + +describe('NcListItemIcon', () => { + it('highlights the search query', async () => { + const wrapper = shallowMount(NcListItemIcon, { + propsData: { + name: 'J. Doe', + subname: 'privacy@example.com', + search: 'doe', + }, + }) + + const highlights = wrapper.findAllComponents({ name: 'NcHighlight' }) + expect(highlights).toHaveLength(2) + expect(highlights.at(0).props('text')).toBe('J. Doe') + expect(highlights.at(0).props('search')).toBe('doe') + expect(highlights.at(1).props('text')).toBe('privacy@example.com') + expect(highlights.at(1).props('search')).toBe('doe') + }) + + it('highlights the email notation query', async () => { + const wrapper = shallowMount(NcListItemIcon, { + propsData: { + name: 'J. Doe', + subname: 'privacy@example.com', + search: 'Manager ', + }, + }) + + const highlights = wrapper.findAllComponents({ name: 'NcHighlight' }) + expect(highlights).toHaveLength(2) + expect(highlights.at(0).props('search')).toBe('Manager') + expect(highlights.at(1).props('search')).toBe('privacy@example.com') + }) + + it('highlights the email notation query even if only partly typed', async () => { + const wrapper = shallowMount(NcListItemIcon, { + propsData: { + name: 'J. Doe', + subname: 'privacy@example.com', + search: 'Manager { + const wrapper = shallowMount(NcListItemIcon, { + propsData: { + name: 'J. Doe', + subname: 'privacy@example.com', + search: 'Doe <', + }, + }) + + const highlights = wrapper.findAllComponents({ name: 'NcHighlight' }) + expect(highlights).toHaveLength(2) + expect(highlights.at(0).props('search')).toBe('Doe <') + expect(highlights.at(1).props('search')).toBe('Doe <') + }) + + it('does not highlight the email notation query when no user', async () => { + const wrapper = shallowMount(NcListItemIcon, { + propsData: { + name: 'J. Doe', + subname: 'privacy@example.com', + search: 'Doe ', + isNoUser: true, + }, + }) + + const highlights = wrapper.findAllComponents({ name: 'NcHighlight' }) + expect(highlights).toHaveLength(2) + expect(highlights.at(0).props('search')).toBe('Doe ') + expect(highlights.at(1).props('search')).toBe('Doe ') + }) +})