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 ')
+ })
+})