Merge pull request #3174 from gradido/admin_extend_user_search

feat(admin): contribution filtering by memo
This commit is contained in:
einhornimmond 2023-08-16 15:01:24 +02:00 committed by GitHub
commit 322327155d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 122 additions and 84 deletions

View File

@ -5,7 +5,7 @@
type="text"
class="test-input-criteria"
v-model="currentValue"
:placeholder="$t('user_search')"
:placeholder="placeholderText"
></b-form-input>
<b-input-group-append class="test-click-clear-criteria" @click="currentValue = ''">
<b-input-group-text class="pointer">
@ -20,12 +20,18 @@ export default {
name: 'UserQuery',
props: {
value: { type: String, default: '' },
placeholder: { type: String, default: '' },
},
data() {
return {
currentValue: this.value,
}
},
computed: {
placeholderText() {
return this.placeholder || this.$t('user_search')
},
},
watch: {
currentValue() {
if (this.value !== this.currentValue) {

View File

@ -8,6 +8,7 @@ export const adminListContributions = gql`
$statusFilter: [ContributionStatus!]
$userId: Int
$query: String
$noHashtag: Boolean
) {
adminListContributions(
currentPage: $currentPage
@ -16,6 +17,7 @@ export const adminListContributions = gql`
statusFilter: $statusFilter
userId: $userId
query: $query
noHashtag: $noHashtag
) {
contributionCount
contributionList {

View File

@ -89,6 +89,7 @@
"submit": "Senden"
},
"GDD": "GDD",
"hashtag_symbol": "#",
"help": {
"help": "Hilfe",
"transactionlist": {
@ -124,6 +125,10 @@
"user_search": "Nutzersuche"
},
"not_open_creations": "Keine offenen Schöpfungen",
"no_filter": "Keine Filterung",
"no_filter_tooltip": "Es wird nicht nach Hashtags gefiltert",
"no_hashtag": "Ohne Hashtag",
"no_hashtag_tooltip": "Zeigt nur Schöpfungen ohne Hashtag im Kommentar an",
"open": "offen",
"open_creations": "Offene Schöpfungen",
"overlay": {
@ -216,6 +221,7 @@
"tabTitle": "Nutzer-Rolle"
},
"user_deleted": "Nutzer ist gelöscht.",
"user_memo_search": "Nutzer-Kommentar-Suche",
"user_recovered": "Nutzer ist wiederhergestellt.",
"user_search": "Nutzer-Suche"
}

View File

@ -89,6 +89,7 @@
"submit": "Send"
},
"GDD": "GDD",
"hashtag_symbol": "#",
"help": {
"help": "Help",
"transactionlist": {
@ -124,6 +125,10 @@
"user_search": "User search"
},
"not_open_creations": "No open creations",
"no_filter": "No Filter",
"no_filter_tooltip": "It is not filtered by hashtags",
"no_hashtag": "No Hashtag",
"no_hashtag_tooltip": "Displays only contributions without hashtag in comment",
"open": "open",
"open_creations": "Open creations",
"overlay": {
@ -216,6 +221,7 @@
"tabTitle": "User Role"
},
"user_deleted": "User is deleted.",
"user_memo_search": "User and Memo search",
"user_recovered": "User is recovered.",
"user_search": "User search"
}

View File

@ -339,6 +339,7 @@ describe('CreationConfirm', () => {
it('refetches contributions with proper filter', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
noHashtag: null,
order: 'DESC',
pageSize: 25,
query: '',
@ -355,6 +356,7 @@ describe('CreationConfirm', () => {
it('refetches contributions with proper filter', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
noHashtag: null,
order: 'DESC',
pageSize: 25,
query: '',
@ -372,6 +374,7 @@ describe('CreationConfirm', () => {
it('refetches contributions with proper filter', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
noHashtag: null,
order: 'DESC',
pageSize: 25,
query: '',
@ -389,6 +392,7 @@ describe('CreationConfirm', () => {
it('refetches contributions with proper filter', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
noHashtag: null,
order: 'DESC',
pageSize: 25,
query: '',
@ -406,6 +410,7 @@ describe('CreationConfirm', () => {
it('refetches contributions with proper filter', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
noHashtag: null,
order: 'DESC',
pageSize: 25,
query: '',
@ -427,6 +432,7 @@ describe('CreationConfirm', () => {
it('calls the API again', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 2,
noHashtag: null,
order: 'DESC',
pageSize: 25,
query: '',
@ -443,6 +449,7 @@ describe('CreationConfirm', () => {
it('refetches contributions with proper filter and current page = 1', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
noHashtag: null,
order: 'DESC',
pageSize: 25,
query: '',
@ -465,6 +472,7 @@ describe('CreationConfirm', () => {
it('calls the API with query', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
noHashtag: null,
order: 'DESC',
pageSize: 25,
query: 'query',
@ -480,6 +488,7 @@ describe('CreationConfirm', () => {
it('calls the API with empty query', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
noHashtag: null,
order: 'DESC',
pageSize: 25,
query: '',

View File

@ -1,7 +1,13 @@
<!-- eslint-disable @intlify/vue-i18n/no-dynamic-keys -->
<template>
<div class="creation-confirm">
<user-query class="mb-4 mt-2" v-model="query" />
<user-query class="mb-2 mt-2" v-model="query" :placeholder="$t('user_memo_search')" />
<div class="mb-4">
<b-button class="noHashtag" variant="light" @click="swapNoHashtag" v-b-tooltip="tooltipText">
<span :style="hashtagColor">{{ $t('hashtag_symbol') }}</span>
{{ noHashtag ? $t('no_hashtag') : $t('no_filter') }}
</b-button>
</div>
<div>
<b-tabs v-model="tabIndex" content-class="mt-3" fill>
<b-tab active :title-link-attributes="{ 'data-test': 'open' }">
@ -118,6 +124,7 @@ export default {
currentPage: 1,
pageSize: 25,
query: '',
noHashtag: null,
}
},
watch: {
@ -126,6 +133,10 @@ export default {
},
},
methods: {
swapNoHashtag() {
this.noHashtag = !!(this.noHashtag === null || this.noHashtag === false)
this.query()
},
deleteCreation() {
this.$apollo
.mutate({
@ -198,6 +209,12 @@ export default {
},
},
computed: {
hashtagColor() {
return this.noHashtag ? 'color: red' : 'color: black'
},
tooltipText() {
return this.noHashtag ? this.$t('no_hashtag_tooltip') : this.$t('no_filter_tooltip')
},
fields() {
return [
[
@ -414,6 +431,7 @@ export default {
pageSize: this.pageSize,
statusFilter: this.statusFilter,
query: this.query,
noHashtag: this.noHashtag,
}
},
fetchPolicy: 'no-cache',

View File

@ -5,12 +5,12 @@ import { Order } from '@enum/Order'
@ArgsType()
export class Paginated {
@Field(() => Int, { nullable: true })
currentPage?: number
@Field(() => Int, { defaultValue: 1 })
currentPage: number
@Field(() => Int, { nullable: true })
pageSize?: number
@Field(() => Int, { defaultValue: 3 })
pageSize: number
@Field(() => Order, { nullable: true })
order?: Order
@Field(() => Order, { defaultValue: Order.DESC })
order: Order
}

View File

@ -0,0 +1,18 @@
import { Field, ArgsType, Int } from 'type-graphql'
import { ContributionStatus } from '@enum/ContributionStatus'
@ArgsType()
export class SearchContributionsFilterArgs {
@Field(() => [ContributionStatus], { nullable: true, defaultValue: null })
statusFilter?: ContributionStatus[] | null
@Field(() => Int, { nullable: true })
userId?: number | null
@Field(() => String, { nullable: true, defaultValue: '' })
query?: string | null
@Field(() => Boolean, { nullable: true })
noHashtag?: boolean | null
}

View File

@ -11,10 +11,10 @@ import { AdminCreateContributionArgs } from '@arg/AdminCreateContributionArgs'
import { AdminUpdateContributionArgs } from '@arg/AdminUpdateContributionArgs'
import { ContributionArgs } from '@arg/ContributionArgs'
import { Paginated } from '@arg/Paginated'
import { SearchContributionsFilterArgs } from '@arg/SearchContributionsFilterArgs'
import { ContributionMessageType } from '@enum/ContributionMessageType'
import { ContributionStatus } from '@enum/ContributionStatus'
import { ContributionType } from '@enum/ContributionType'
import { Order } from '@enum/Order'
import { TransactionTypeId } from '@enum/TransactionTypeId'
import { AdminUpdateContribution } from '@model/AdminUpdateContribution'
import { Contribution, ContributionListResult } from '@model/Contribution'
@ -128,20 +128,16 @@ export class ContributionResolver {
async listContributions(
@Ctx() context: Context,
@Args()
{ currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated,
paginated: Paginated,
@Arg('statusFilter', () => [ContributionStatus], { nullable: true })
statusFilter?: ContributionStatus[] | null,
): Promise<ContributionListResult> {
const user = getUser(context)
const [dbContributions, count] = await findContributions({
order,
currentPage,
pageSize,
withDeleted: true,
relations: { messages: true },
userId: user.id,
statusFilter,
const filter = new SearchContributionsFilterArgs()
filter.statusFilter = statusFilter
filter.userId = user.id
const [dbContributions, count] = await findContributions(paginated, filter, true, {
messages: true,
})
return new ContributionListResult(
@ -160,16 +156,14 @@ export class ContributionResolver {
@Query(() => ContributionListResult)
async listAllContributions(
@Args()
{ currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated,
paginated: Paginated,
@Arg('statusFilter', () => [ContributionStatus], { nullable: true })
statusFilter?: ContributionStatus[] | null,
): Promise<ContributionListResult> {
const [dbContributions, count] = await findContributions({
order,
currentPage,
pageSize,
relations: { user: true },
statusFilter,
const filter = new SearchContributionsFilterArgs()
filter.statusFilter = statusFilter
const [dbContributions, count] = await findContributions(paginated, filter, false, {
user: true,
})
return new ContributionListResult(
@ -374,29 +368,14 @@ export class ContributionResolver {
@Authorized([RIGHTS.ADMIN_LIST_CONTRIBUTIONS])
@Query(() => ContributionListResult)
async adminListContributions(
@Args()
{ currentPage = 1, pageSize = 3, order = Order.DESC }: Paginated,
@Arg('statusFilter', () => [ContributionStatus], { nullable: true })
statusFilter?: ContributionStatus[] | null,
@Arg('userId', () => Int, { nullable: true })
userId?: number | null,
@Arg('query', () => String, { nullable: true })
query?: string | null,
@Args() paginated: Paginated,
@Args() filter: SearchContributionsFilterArgs,
): Promise<ContributionListResult> {
const [dbContributions, count] = await findContributions({
order,
currentPage,
pageSize,
withDeleted: true,
userId,
relations: {
user: {
emailContact: true,
},
messages: true,
const [dbContributions, count] = await findContributions(paginated, filter, true, {
user: {
emailContact: true,
},
statusFilter,
query,
messages: true,
})
return new ContributionListResult(

View File

@ -1,74 +1,68 @@
import { In, Like } from '@dbTools/typeorm'
import { In, Like, Not } from '@dbTools/typeorm'
import { Contribution as DbContribution } from '@entity/Contribution'
import { ContributionStatus } from '@enum/ContributionStatus'
import { Order } from '@enum/Order'
import { Paginated } from '@arg/Paginated'
import { SearchContributionsFilterArgs } from '@arg/SearchContributionsFilterArgs'
interface Relations {
[key: string]: boolean | Relations
}
interface FindContributionsOptions {
order: Order
currentPage: number
pageSize: number
withDeleted?: boolean
relations?: Relations | undefined
userId?: number | null
statusFilter?: ContributionStatus[] | null
query?: string | null
}
export const findContributions = async (
options: FindContributionsOptions,
paginate: Paginated,
filter: SearchContributionsFilterArgs,
withDeleted = false,
relations: Relations | undefined = undefined,
): Promise<[DbContribution[], number]> => {
const { order, currentPage, pageSize, withDeleted, relations, userId, statusFilter, query } = {
withDeleted: false,
relations: undefined,
query: '',
...options,
}
const requiredWhere = {
...(statusFilter?.length && { contributionStatus: In(statusFilter) }),
...(userId && { userId }),
...(filter.statusFilter?.length && { contributionStatus: In(filter.statusFilter) }),
...(filter.userId && { userId: filter.userId }),
...(filter.noHashtag && { memo: Not(Like(`%#%`)) }),
}
const where =
query && relations?.user
let where =
filter.query && relations?.user
? [
{
...requiredWhere,
...requiredWhere, // And
user: {
firstName: Like(`%${query}%`),
firstName: Like(`%${filter.query}%`),
},
},
}, // Or
{
...requiredWhere,
user: {
lastName: Like(`%${query}%`),
lastName: Like(`%${filter.query}%`),
},
},
}, // Or
{
...requiredWhere,
...requiredWhere, // And
user: {
emailContact: {
email: Like(`%${query}%`),
email: Like(`%${filter.query}%`),
},
},
}, // Or
{
...requiredWhere, // And
memo: Like(`%${filter.query}%`),
},
]
: requiredWhere
if (!relations?.user && filter.query) {
where = [{ ...requiredWhere, memo: Like(`%${filter.query}%`) }]
}
return DbContribution.findAndCount({
relations,
where,
withDeleted,
order: {
createdAt: order,
id: order,
createdAt: paginate.order,
id: paginate.order,
},
skip: (currentPage - 1) * pageSize,
take: pageSize,
skip: (paginate.currentPage - 1) * paginate.pageSize,
take: paginate.pageSize,
})
}