Merge pull request #1019 from Human-Connection/1000_find_users_by_email_address

Implement paginating user view for admins
This commit is contained in:
Wolfgang Huß 2019-07-16 09:12:44 +02:00 committed by GitHub
commit 71527afd3f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 286 additions and 17 deletions

View File

@ -81,6 +81,9 @@ type User {
input _UserFilter {
AND: [_UserFilter!]
OR: [_UserFilter!]
name_contains: String
about_contains: String
slug_contains: String
id: ID
id_not: ID
id_in: [ID!]

View File

@ -683,6 +683,13 @@ import Factory from './factories'
to: 'o3',
}),
])
await Promise.all(
[...Array(30).keys()].map(i => {
return f.create('User')
}),
)
/* eslint-disable-next-line no-console */
console.log('Seeded Data...')
process.exit(0)

View File

@ -192,7 +192,19 @@
"name": "Organisationen"
},
"users": {
"name": "Benutzer"
"name": "Benutzer",
"form": {
"placeholder": "E-Mail, Name oder Beschreibung"
},
"table": {
"columns": {
"name": "Name",
"slug": "Slug",
"role": "Rolle",
"createdAt": "Erstellt am"
}
},
"empty": "Keine Benutzer gefunden"
},
"pages": {
"name": "Seiten"

View File

@ -193,7 +193,19 @@
"name": "Organizations"
},
"users": {
"name": "Users"
"name": "Users",
"form": {
"placeholder": "E-Mail, name or description"
},
"table": {
"columns": {
"name": "Name",
"slug": "Slug",
"role": "Role",
"createdAt": "Created at"
}
},
"empty": "No users found"
},
"pages": {
"name": "Pages"

View File

@ -62,6 +62,7 @@
"date-fns": "2.0.0-beta.2",
"express": "~4.17.1",
"graphql": "~14.4.2",
"isemail": "^3.2.0",
"jsonwebtoken": "~8.5.1",
"linkify-it": "~2.2.0",
"nuxt": "~2.8.1",

View File

@ -24,11 +24,10 @@ export default {
name: this.$t('admin.dashboard.name'),
path: `/admin`,
},
// TODO implement
/* {
{
name: this.$t('admin.users.name'),
path: `/admin/users`
}, */
path: `/admin/users`,
},
// TODO implement
/* {
name: this.$t('admin.organizations.name'),

View File

@ -0,0 +1,70 @@
import { mount, createLocalVue } from '@vue/test-utils'
import Users from './users.vue'
import Styleguide from '@human-connection/styleguide'
const localVue = createLocalVue()
localVue.use(Styleguide)
describe('Users', () => {
let wrapper
let Wrapper
let mocks
beforeEach(() => {
mocks = {
$t: jest.fn(),
$apollo: {
loading: false,
},
}
})
describe('mount', () => {
Wrapper = () => {
return mount(Users, {
mocks,
localVue,
})
}
it('renders', () => {
wrapper = Wrapper()
expect(wrapper.is('div')).toBe(true)
})
describe('search', () => {
let searchAction
beforeEach(() => {
searchAction = (wrapper, { query }) => {
wrapper.find('input').setValue(query)
wrapper.find('form').trigger('submit')
return wrapper
}
})
describe('query looks like an email address', () => {
it('searches users for exact email address', async () => {
const wrapper = await searchAction(Wrapper(), { query: 'email@example.org' })
expect(wrapper.vm.email).toEqual('email@example.org')
expect(wrapper.vm.filter).toBe(null)
})
})
describe('query is just text', () => {
it('tries to find matching users by `name`, `slug` or `about`', async () => {
const wrapper = await searchAction(await Wrapper(), { query: 'Find me' })
const expected = {
OR: [
{ name_contains: 'Find me' },
{ slug_contains: 'Find me' },
{ about_contains: 'Find me' },
],
}
expect(wrapper.vm.email).toBe(null)
expect(wrapper.vm.filter).toEqual(expected)
})
})
})
})
})

View File

@ -1,15 +1,173 @@
<template>
<ds-card :header="$t('admin.users.name')">
<hc-empty icon="tasks" message="Coming Soon…" />
</ds-card>
<div>
<ds-space>
<ds-card :header="$t('admin.users.name')">
<ds-form v-model="form" @submit="submit">
<ds-flex gutter="small">
<ds-flex-item width="90%">
<ds-input
model="query"
:placeholder="$t('admin.users.form.placeholder')"
icon="search"
/>
</ds-flex-item>
<ds-flex-item width="30px">
<ds-button primary type="submit" icon="search" :loading="$apollo.loading" />
</ds-flex-item>
</ds-flex>
</ds-form>
</ds-card>
</ds-space>
<ds-card v-if="User && User.length">
<ds-table :data="User" :fields="fields" condensed>
<template slot="index" slot-scope="scope">
{{ scope.row.index }}.
</template>
<template slot="name" slot-scope="scope">
<nuxt-link
:to="{
name: 'profile-id-slug',
params: { id: scope.row.id, slug: scope.row.slug },
}"
>
<b>{{ scope.row.name | truncate(20) }}</b>
</nuxt-link>
</template>
<template slot="slug" slot-scope="scope">
<nuxt-link
:to="{
name: 'profile-id-slug',
params: { id: scope.row.id, slug: scope.row.slug },
}"
>
<b>{{ scope.row.slug | truncate(20) }}</b>
</nuxt-link>
</template>
<template slot="createdAt" slot-scope="scope">
{{ scope.row.createdAt | dateTime }}
</template>
</ds-table>
<ds-flex direction="row-reverse">
<ds-flex-item width="50px">
<ds-button @click="next" :disabled="!hasNext" icon="arrow-right" primary />
</ds-flex-item>
<ds-flex-item width="50px">
<ds-button @click="back" :disabled="!hasPrevious" icon="arrow-left" primary />
</ds-flex-item>
</ds-flex>
</ds-card>
<ds-card v-else>
<ds-placeholder>
{{ $t('admin.users.empty') }}
</ds-placeholder>
</ds-card>
</div>
</template>
<script>
import HcEmpty from '~/components/Empty.vue'
import gql from 'graphql-tag'
import isemail from 'isemail'
export default {
components: {
HcEmpty,
data() {
const pageSize = 15
return {
offset: 0,
pageSize,
first: pageSize,
User: [],
hasNext: false,
email: null,
filter: null,
form: {
formData: {
query: '',
},
},
}
},
computed: {
hasPrevious() {
return this.offset > 0
},
fields() {
return {
index: '#',
name: this.$t('admin.users.table.columns.name'),
slug: this.$t('admin.users.table.columns.slug'),
createdAt: this.$t('admin.users.table.columns.createdAt'),
contributionsCount: {
label: '🖉',
align: 'right',
},
commentedCount: {
label: '🗨',
align: 'right',
},
shoutedCount: {
label: '❤',
align: 'right',
},
role: {
label: this.$t('admin.users.table.columns.role'),
align: 'right',
},
}
},
},
apollo: {
User: {
query() {
return gql(`
query($filter: _UserFilter, $first: Int, $offset: Int, $email: String) {
User(email: $email, filter: $filter, first: $first, offset: $offset, orderBy: createdAt_desc) {
id
name
slug
role
createdAt
contributionsCount
commentedCount
shoutedCount
}
}
`)
},
variables() {
const { offset, first, email, filter } = this
const variables = { first, offset }
if (email) variables.email = email
if (filter) variables.filter = filter
return variables
},
update({ User }) {
if (!User) return []
this.hasNext = User.length >= this.pageSize
if (User.length <= 0 && this.offset > 0) return this.User // edge case, avoid a blank page
return User.map((u, i) => Object.assign({}, u, { index: this.offset + i }))
},
},
},
methods: {
back() {
this.offset = Math.max(this.offset - this.pageSize, 0)
},
next() {
this.offset += this.pageSize
},
submit(formData) {
this.offset = 0
const { query } = formData
if (isemail.validate(query)) {
this.email = query
this.filter = null
} else {
this.email = null
this.filter = {
OR: [{ name_contains: query }, { slug_contains: query }, { about_contains: query }],
}
}
},
},
}
</script>

View File

@ -6047,6 +6047,13 @@ isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
isemail@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/isemail/-/isemail-3.2.0.tgz#59310a021931a9fb06bbb51e155ce0b3f236832c"
integrity sha512-zKqkK+O+dGqevc93KNsbZ/TqTUFd46MwWjYOoMrjIMZ51eU7DtQG3Wmd9SQQT7i7RVnuTPEiYEWHU3MSbxC1Tg==
dependencies:
punycode "2.x.x"
isexe@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
@ -9173,16 +9180,16 @@ punycode@1.3.2:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=
punycode@2.x.x, punycode@^2.1.0, punycode@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
punycode@^1.2.4, punycode@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
punycode@^2.1.0, punycode@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
q@^1.1.2:
version "1.5.1"
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"