Skip to content

Commit

Permalink
Merge pull request #4653 from nextcloud-libraries/feat/allow-to-filte…
Browse files Browse the repository at this point in the history
…r-by-mail-notation

feat(NcSelect): Allow to filter users by email notation
  • Loading branch information
Pytal authored Oct 18, 2023
2 parents 0caa439 + 468fbed commit 8d42225
Show file tree
Hide file tree
Showing 4 changed files with 203 additions and 5 deletions.
74 changes: 74 additions & 0 deletions cypress/component/NcSelect.cy.ts
Original file line number Diff line number Diff line change
@@ -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: '[email protected]',
icon: '',
},
{
id: '0-emma',
displayName: 'Emma',
isNoUser: false,
subname: '[email protected]',
icon: '',
},
{
id: '0-olivia',
displayName: 'Olivia',
isNoUser: false,
subname: '[email protected]',
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)
})
})
19 changes: 17 additions & 2 deletions src/components/NcListItemIcon/NcListItemIcon.vue
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,11 @@ It might be used for list rendering or within the multiselect for example
<div class="option__details">
<NcHighlight class="option__lineone"
:text="name"
:search="search" />
:search="searchParts[0]" />
<NcHighlight v-if="isValidSubname && isSizeBigEnough"
class="option__linetwo"
:text="subname"
:search="search" />
:search="searchParts[1]" />
<span v-else-if="hasStatus">
<span>{{ userStatus.icon }}</span>
<span>{{ userStatus.message }}</span>
Expand Down Expand Up @@ -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 <[email protected]>" 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() {
Expand Down
11 changes: 8 additions & 3 deletions src/components/NcSelect/NcSelect.vue
Original file line number Diff line number Diff line change
Expand Up @@ -951,14 +951,19 @@ export default {
},
localFilterBy() {
// Match the email notation like "Jane <[email protected]>" 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
Expand Down
104 changes: 104 additions & 0 deletions tests/unit/components/NcListItemIcon/list-item-icon.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/**
* @copyright Copyright (c) 2023 Ferdinand Thiessen <[email protected]>
*
* @author Ferdinand Thiessen <[email protected]>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/

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: '[email protected]',
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('[email protected]')
expect(highlights.at(1).props('search')).toBe('doe')
})

it('highlights the email notation query', async () => {
const wrapper = shallowMount(NcListItemIcon, {
propsData: {
name: 'J. Doe',
subname: '[email protected]',
search: 'Manager <[email protected]>',
},
})

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('[email protected]')
})

it('highlights the email notation query even if only partly typed', async () => {
const wrapper = shallowMount(NcListItemIcon, {
propsData: {
name: 'J. Doe',
subname: '[email protected]',
search: 'Manager <privacy',
},
})

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

it('does not highlight the email notation query when missing email', async () => {
const wrapper = shallowMount(NcListItemIcon, {
propsData: {
name: 'J. Doe',
subname: '[email protected]',
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: '[email protected]',
search: 'Doe <[email protected]>',
isNoUser: true,
},
})

const highlights = wrapper.findAllComponents({ name: 'NcHighlight' })
expect(highlights).toHaveLength(2)
expect(highlights.at(0).props('search')).toBe('Doe <[email protected]>')
expect(highlights.at(1).props('search')).toBe('Doe <[email protected]>')
})
})

0 comments on commit 8d42225

Please sign in to comment.