Skip to content

Commit

Permalink
Merge pull request #6779 from owncloud/accounts-filter
Browse files Browse the repository at this point in the history
Add search to usersManagement
  • Loading branch information
kulmann authored Apr 13, 2022
2 parents 18e3a7d + d7fb93c commit fb6ca30
Show file tree
Hide file tree
Showing 11 changed files with 312 additions and 15 deletions.
3 changes: 2 additions & 1 deletion packages/web-app-user-management/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"description": "ownCloud user management",
"license": "AGPL-3.0",
"dependencies": {
"email-validator": "^2.0.4"
"email-validator": "^2.0.4",
"mark.js": "^8.11.1"
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<template>
<oc-table
ref="table"
:sort-by="sortBy"
:sort-dir="sortDir"
:fields="fields"
Expand Down Expand Up @@ -50,13 +51,10 @@
</template>

<script>
const orderBy = (list, prop, desc) => {
return [...list].sort((a, b) => {
a = a[prop] || ''
b = b[prop] || ''
return desc ? b.localeCompare(a) : a.localeCompare(b)
})
}
import { onBeforeUnmount, ref } from '@vue/composition-api'
import { Registry } from '../../services'
import Fuse from 'fuse.js'
import Mark from 'mark.js'
export default {
name: 'GroupsList',
Expand All @@ -70,10 +68,20 @@ export default {
required: true
}
},
setup() {
const searchTerm = ref('')
const token = Registry.search.subscribe('updateTerm', ({ term }) => (searchTerm.value = term))
onBeforeUnmount(() => Registry.search.unsubscribe('updateTerm', token))
return {
searchTerm
}
},
data() {
return {
sortBy: 'displayName',
sortDir: 'asc'
sortDir: 'asc',
markInstance: null
}
},
computed: {
Expand Down Expand Up @@ -114,13 +122,52 @@ export default {
return this.$gettextInterpolate(translated, { groupCount: this.groups.length })
},
data() {
return orderBy(this.groups, this.sortBy, this.sortDir === 'desc')
const orderedGroups = this.orderBy(this.groups, this.sortBy, this.sortDir === 'desc')
return this.filter(orderedGroups, this.searchTerm)
},
highlighted() {
return this.selectedGroups.map((group) => group.id)
}
},
watch: {
searchTerm() {
if (!this.markInstance) {
return
}
this.markInstance.unmark()
this.markInstance.mark(this.searchTerm, {
element: 'span',
className: 'highlight-mark',
exclude: ['th *', 'tfoot *']
})
}
},
mounted() {
this.$nextTick(() => {
this.markInstance = new Mark(this.$refs.table.$el)
})
},
methods: {
filter(groups, searchTerm) {
if (!(searchTerm || '').trim()) {
return groups
}
const groupsSearchEngine = new Fuse(groups, {
includeScore: true,
useExtendedSearch: true,
threshold: 0.3,
keys: ['displayName']
})
return groupsSearchEngine.search(searchTerm).map((r) => r.item)
},
orderBy(list, prop, desc) {
return [...list].sort((a, b) => {
a = a[prop] || ''
b = b[prop] || ''
return desc ? b.localeCompare(a) : a.localeCompare(b)
})
},
handleSort(event) {
this.sortBy = event.sortBy
this.sortDir = event.sortDir
Expand All @@ -132,3 +179,10 @@ export default {
}
}
</script>

<style lang="scss">
.highlight-mark {
background: yellow;
color: var(--oc-color-text-muted);
}
</style>
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<template>
<oc-table
ref="table"
:sort-by="sortBy"
:sort-dir="sortDir"
:fields="fields"
Expand Down Expand Up @@ -31,7 +32,7 @@
<template #avatar="{ item }">
<avatar-image :width="32" :userid="item.id" :user-name="item.displayName" />
</template>
<template #role="{ item }"> {{ getUserRole(item) }} </template>
<template #role="{ item }"> {{ getUserRole(item) }}</template>
<template #actions="{ item }">
<oc-button v-oc-tooltip="$gettext('Details')" @click="$emit('clickDetails', item)">
<oc-icon size="small" name="information" />
Expand All @@ -49,6 +50,11 @@
</template>

<script>
import { onBeforeUnmount, ref } from '@vue/composition-api'
import { Registry } from '../../services'
import Fuse from 'fuse.js'
import Mark from 'mark.js'
export default {
name: 'UsersList',
props: {
Expand All @@ -65,10 +71,20 @@ export default {
required: true
}
},
setup() {
const searchTerm = ref('')
const token = Registry.search.subscribe('updateTerm', ({ term }) => (searchTerm.value = term))
onBeforeUnmount(() => Registry.search.unsubscribe('updateTerm', token))
return {
searchTerm
}
},
data() {
return {
sortBy: 'onPremisesSamAccountName',
sortDir: 'asc'
sortDir: 'asc',
markInstance: null
}
},
computed: {
Expand Down Expand Up @@ -125,13 +141,45 @@ export default {
]
},
data() {
return this.orderBy(this.users, this.sortBy, this.sortDir === 'desc')
const orderedUsers = this.orderBy(this.users, this.sortBy, this.sortDir === 'desc')
return this.filter(orderedUsers, this.searchTerm)
},
highlighted() {
return this.selectedUsers.map((user) => user.id)
}
},
watch: {
searchTerm() {
if (!this.markInstance) {
return
}
this.markInstance.unmark()
this.markInstance.mark(this.searchTerm, {
element: 'span',
className: 'highlight-mark',
exclude: ['th *', 'tfoot *']
})
}
},
mounted() {
this.$nextTick(() => {
this.markInstance = new Mark(this.$refs.table.$el)
})
},
methods: {
filter(users, searchTerm) {
if (!(searchTerm || '').trim()) {
return users
}
const usersSearchEngine = new Fuse(users, {
includeScore: true,
useExtendedSearch: true,
threshold: 0.3,
keys: ['displayName', 'mail', 'onPremisesSamAccountName']
})
return usersSearchEngine.search(searchTerm).map((r) => r.item)
},
orderBy(list, prop, desc) {
return [...list].sort((user1, user2) => {
if (prop === 'role') {
Expand Down Expand Up @@ -161,3 +209,10 @@ export default {
}
}
</script>

<style lang="scss">
.highlight-mark {
background: yellow;
color: var(--oc-color-text-muted);
}
</style>
10 changes: 8 additions & 2 deletions packages/web-app-user-management/src/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Registry } from './services'
import translations from '../l10n/translations'
import Users from './views/Users.vue'
import Groups from './views/Groups.vue'

import { FilterSearch } from './search'
import { bus } from 'web-pkg/src/instance'
// just a dummy function to trick gettext tools
function $gettext(msg) {
return msg
Expand Down Expand Up @@ -54,5 +56,9 @@ export default {
appInfo,
routes,
translations,
navItems
navItems,
ready({ router }) {
Registry.search = new FilterSearch(router)
bus.publish('app.search.register.provider', Registry.search)
}
}
55 changes: 55 additions & 0 deletions packages/web-app-user-management/src/search/filter/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { SearchProvider } from 'search/src/types'
import { EventBus } from 'web-pkg/src/event'
import VueRouter, { Route } from 'vue-router'

function $gettext(msg) {
return msg
}

const kind = (currentRoute: Route): 'users' | 'groups' | undefined => {
switch (currentRoute.name) {
case 'user-management-users':
return 'users'
case 'user-management-groups':
return 'groups'
default:
return undefined
}
}

export default class Provider extends EventBus implements SearchProvider {
public readonly id: string
private readonly router: VueRouter

constructor(router: VueRouter) {
super()

this.id = 'usersManagement.filter'
this.router = router
}

public get label(): string {
switch (kind(this.router.currentRoute)) {
case 'users':
return $gettext('Search users ↵')
case 'groups':
return $gettext('Search groups ↵')
}
}

public activate(term: string): void {
/* noop */
}

public reset(): void {
/* noop */
}

public updateTerm(term: string): void {
this.publish('updateTerm', { kind: kind(this.router.currentRoute), term })
}

public get available(): boolean {
return !!kind(this.router.currentRoute)
}
}
1 change: 1 addition & 0 deletions packages/web-app-user-management/src/search/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as FilterSearch } from './filter'
1 change: 1 addition & 0 deletions packages/web-app-user-management/src/services/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as Registry } from './registry'
5 changes: 5 additions & 0 deletions packages/web-app-user-management/src/services/registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { FilterSearch } from '../search'

export default class Registry {
static search: FilterSearch
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import Vuex from 'vuex'
import { mount, createLocalVue } from '@vue/test-utils'
import UsersList from '../../../../src/components/Groups/GroupsList'

const localVue = createLocalVue()
localVue.use(Vuex)

afterEach(() => jest.clearAllMocks())

describe('GroupsList', () => {
describe('method "orderBy"', () => {
it('should return an ascending ordered list while desc is set to false', () => {
const wrapper = getMountedWrapper()

expect(
wrapper.vm.orderBy(
[{ displayName: 'users' }, { displayName: 'admins' }],
'displayName',
false
)
).toEqual([{ displayName: 'admins' }, { displayName: 'users' }])
})
it('should return an descending ordered list while desc is set to true', () => {
const wrapper = getMountedWrapper()

expect(
wrapper.vm.orderBy(
[{ displayName: 'admins' }, { displayName: 'users' }],
'displayName',
true
)
).toEqual([{ displayName: 'users' }, { displayName: 'admins' }])
})
})

describe('method "filter"', () => {
it('should return a list containing record admins if search term is "ad"', () => {
const wrapper = getMountedWrapper()

expect(
wrapper.vm.filter([{ displayName: 'users' }, { displayName: 'admins' }], 'ad')
).toEqual([{ displayName: 'admins' }])
})
it('should return an an empty list if search term does not match any entry', () => {
const wrapper = getMountedWrapper()

expect(
wrapper.vm.filter([{ displayName: 'admins' }, { displayName: 'users' }], 'ownClouders')
).toEqual([])
})
})
})

function getMountedWrapper({ propsData = {} } = {}) {
return mount(UsersList, {
localVue,
mocks: {
$gettext: jest.fn(),
$gettextInterpolate: jest.fn()
},
propsData: {
groups: [],
selectedGroups: [],
...propsData
},
stubs: {
'avatar-image': true,
'oc-checkbox': true,
'oc-button': true,
'oc-table': { template: '<div></div>' }
}
})
}
Loading

0 comments on commit fb6ca30

Please sign in to comment.