mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
Merge pull request #3460 from gradido/refactor_graphql_admin_contribution_list
refactor(admin): admin contribution list
This commit is contained in:
commit
b49f18c9a7
@ -68,6 +68,16 @@ const defaultData = {
|
||||
},
|
||||
}
|
||||
|
||||
const defaultUser = {
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
humhubUsername: 'peter.lustig',
|
||||
createdAt: new Date().toString(),
|
||||
emailContact: {
|
||||
email: 'peter.lustig@example.com',
|
||||
},
|
||||
}
|
||||
|
||||
describe('ContributionMessagesList', () => {
|
||||
let wrapper
|
||||
let mockMessages
|
||||
@ -98,6 +108,7 @@ describe('ContributionMessagesList', () => {
|
||||
memo: 'test memo',
|
||||
userId: 108,
|
||||
status: 'PENDING',
|
||||
user: defaultUser,
|
||||
},
|
||||
hideResubmission: true,
|
||||
},
|
||||
@ -139,7 +150,12 @@ describe('ContributionMessagesList', () => {
|
||||
})
|
||||
|
||||
it('does not render the ContributionMessagesFormular when status is not PENDING or IN_PROGRESS', async () => {
|
||||
await wrapper.setProps({ contribution: { status: 'COMPLETED' } })
|
||||
await wrapper.setProps({
|
||||
contribution: {
|
||||
status: 'COMPLETED',
|
||||
user: defaultUser,
|
||||
},
|
||||
})
|
||||
expect(wrapper.find('contribution-messages-formular-stub').exists()).toBe(false)
|
||||
})
|
||||
|
||||
|
||||
@ -3,16 +3,16 @@
|
||||
<BListGroup>
|
||||
<BListGroupItem>
|
||||
<routerLink :to="searchLink" :title="$t('goTo.userSearch')">
|
||||
{{ contribution.firstName }} {{ contribution.lastName }}
|
||||
{{ contribution.user.firstName }} {{ contribution.user.lastName }}
|
||||
</routerLink>
|
||||
|
||||
<a :href="mailtoLink">{{ contribution.email }}</a>
|
||||
<a :href="mailtoLink">{{ email }}</a>
|
||||
<IBiFilter id="filter-by-email" class="ms-1 pointer" @click="searchForEmail" />
|
||||
<BTooltip target="filter-by-email" triggers="hover">
|
||||
{{ $t('filter.byEmail') }}
|
||||
</BTooltip>
|
||||
|
||||
{{ contribution.username }}
|
||||
{{ contribution.user.humhubUsername }}
|
||||
|
||||
<span>
|
||||
<a
|
||||
@ -29,7 +29,8 @@
|
||||
</span>
|
||||
</BListGroupItem>
|
||||
<BListGroupItem>
|
||||
{{ $t('registered') }}: {{ new Date(contribution.createdAt).toLocaleString() }}
|
||||
{{ $t('registered') }}: {{ new Date(contribution.user.createdAt).toLocaleDateString() }},
|
||||
{{ $t('createdAt') }}: {{ new Date(contribution.createdAt).toLocaleDateString() }}
|
||||
</BListGroupItem>
|
||||
</BListGroup>
|
||||
<BContainer>
|
||||
@ -84,12 +85,15 @@ const emit = defineEmits([
|
||||
'update-contributions',
|
||||
'search-for-email',
|
||||
])
|
||||
const email = computed(() => {
|
||||
return props.contribution.user.emailContact.email
|
||||
})
|
||||
const { toastError } = useAppToast()
|
||||
const mailtoLink = computed(() => {
|
||||
return `mailto:${props.contribution.email}`
|
||||
return `mailto:${email.value}`
|
||||
})
|
||||
const searchLink = computed(() => {
|
||||
return `/user?search=${props.contribution.email}`
|
||||
return `/user?search=${email.value}`
|
||||
})
|
||||
const humhubProfileLink = computed(() => {
|
||||
if (CONFIG.HUMHUB_ACTIVE !== true) {
|
||||
@ -135,7 +139,7 @@ const updateContributions = () => {
|
||||
}
|
||||
|
||||
const searchForEmail = () => {
|
||||
emit('search-for-email', props.contribution.email)
|
||||
emit('search-for-email', email.value)
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
|
||||
@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils'
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import CreationTransactionList from './CreationTransactionList.vue'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { adminListContributions } from '../graphql/adminListContributions'
|
||||
import { adminListContributionsShort } from '../graphql/adminListContributions.graphql'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAppToast } from '@/composables/useToast'
|
||||
|
||||
@ -65,13 +65,17 @@ describe('CreationTransactionList', () => {
|
||||
it('calls useQuery with correct parameters', () => {
|
||||
expect(useQuery).toHaveBeenCalled()
|
||||
const call = useQuery.mock.calls[0]
|
||||
expect(call[0]).toBe(adminListContributions)
|
||||
expect(call[0]).toBe(adminListContributionsShort)
|
||||
expect(call[1]).toEqual(
|
||||
expect.objectContaining({
|
||||
currentPage: expect.any(Number),
|
||||
pageSize: expect.any(Number),
|
||||
order: 'DESC',
|
||||
userId: 1,
|
||||
filter: {
|
||||
userId: 1,
|
||||
},
|
||||
paginated: {
|
||||
currentPage: expect.any(Number),
|
||||
pageSize: expect.any(Number),
|
||||
order: 'DESC',
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@ -45,7 +45,7 @@
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { adminListContributions } from '../graphql/adminListContributions'
|
||||
import { adminListContributionsShort } from '../graphql/adminListContributions.graphql'
|
||||
import { BTable, BPagination, BButton, BCollapse, vBToggle } from 'bootstrap-vue-next'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
@ -93,11 +93,15 @@ const fields = [
|
||||
{ key: 'memo', label: t('transactionlist.memo'), class: 'text-break' },
|
||||
]
|
||||
|
||||
const { result, refetch } = useQuery(adminListContributions, {
|
||||
currentPage: currentPage.value,
|
||||
pageSize: perPage.value,
|
||||
order: 'DESC',
|
||||
userId: props.userId,
|
||||
const { result, refetch } = useQuery(adminListContributionsShort, {
|
||||
filter: {
|
||||
userId: props.userId,
|
||||
},
|
||||
paginated: {
|
||||
currentPage: currentPage.value,
|
||||
pageSize: perPage.value,
|
||||
order: 'DESC',
|
||||
},
|
||||
})
|
||||
|
||||
watch(result, (newResult) => {
|
||||
|
||||
70
admin/src/graphql/adminListContributions.graphql
Normal file
70
admin/src/graphql/adminListContributions.graphql
Normal file
@ -0,0 +1,70 @@
|
||||
#import './fragments.graphql'
|
||||
|
||||
query adminListContributions(
|
||||
$filter: SearchContributionsFilterArgs
|
||||
$paginated: Paginated
|
||||
) {
|
||||
adminListContributions(paginated: $paginated, filter: $filter) {
|
||||
contributionCount
|
||||
contributionList {
|
||||
id
|
||||
user {
|
||||
emailContact {
|
||||
email
|
||||
}
|
||||
...UserCommonFields
|
||||
humhubUsername
|
||||
createdAt
|
||||
}
|
||||
amount
|
||||
memo
|
||||
createdAt
|
||||
contributionDate
|
||||
confirmedAt
|
||||
confirmedBy
|
||||
updatedAt
|
||||
updatedBy
|
||||
status
|
||||
messagesCount
|
||||
deniedAt
|
||||
deniedBy
|
||||
deletedAt
|
||||
deletedBy
|
||||
moderatorId
|
||||
userId
|
||||
resubmissionAt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query adminListContributionsShort(
|
||||
$filter: SearchContributionsFilterArgs
|
||||
$paginated: Paginated
|
||||
) {
|
||||
adminListContributions(
|
||||
paginated: $paginated,
|
||||
filter: $filter
|
||||
) {
|
||||
contributionCount
|
||||
contributionList {
|
||||
id
|
||||
amount
|
||||
memo
|
||||
createdAt
|
||||
contributionDate
|
||||
confirmedAt
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
query adminListContributionsCount(
|
||||
$filter: SearchContributionsFilterArgs
|
||||
) {
|
||||
adminListContributions(
|
||||
filter: $filter
|
||||
) {
|
||||
contributionCount
|
||||
}
|
||||
}
|
||||
@ -1,52 +0,0 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const adminListContributions = gql`
|
||||
query (
|
||||
$currentPage: Int = 1
|
||||
$pageSize: Int = 25
|
||||
$order: Order = DESC
|
||||
$statusFilter: [ContributionStatus!]
|
||||
$userId: Int
|
||||
$query: String
|
||||
$noHashtag: Boolean
|
||||
$hideResubmission: Boolean
|
||||
) {
|
||||
adminListContributions(
|
||||
currentPage: $currentPage
|
||||
pageSize: $pageSize
|
||||
order: $order
|
||||
statusFilter: $statusFilter
|
||||
userId: $userId
|
||||
query: $query
|
||||
noHashtag: $noHashtag
|
||||
hideResubmission: $hideResubmission
|
||||
) {
|
||||
contributionCount
|
||||
contributionList {
|
||||
id
|
||||
firstName
|
||||
lastName
|
||||
email
|
||||
username
|
||||
humhubUsername
|
||||
amount
|
||||
memo
|
||||
createdAt
|
||||
contributionDate
|
||||
confirmedAt
|
||||
confirmedBy
|
||||
updatedAt
|
||||
updatedBy
|
||||
status
|
||||
messagesCount
|
||||
deniedAt
|
||||
deniedBy
|
||||
deletedAt
|
||||
deletedBy
|
||||
moderatorId
|
||||
userId
|
||||
resubmissionAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -26,4 +26,11 @@ fragment AiChatMessageFields on ChatGptMessage {
|
||||
content
|
||||
role
|
||||
threadId
|
||||
}
|
||||
}
|
||||
|
||||
fragment UserCommonFields on User {
|
||||
id
|
||||
firstName
|
||||
lastName
|
||||
alias
|
||||
}
|
||||
|
||||
@ -158,7 +158,9 @@ describe('CreationConfirm', () => {
|
||||
|
||||
expect(mockRefetch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
query: 'test query',
|
||||
filter: expect.objectContaining({
|
||||
query: 'test query',
|
||||
}),
|
||||
}),
|
||||
)
|
||||
|
||||
@ -167,7 +169,9 @@ describe('CreationConfirm', () => {
|
||||
|
||||
expect(mockRefetch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
noHashtag: true,
|
||||
filter: expect.objectContaining({
|
||||
noHashtag: true,
|
||||
}),
|
||||
}),
|
||||
)
|
||||
})
|
||||
@ -179,8 +183,12 @@ describe('CreationConfirm', () => {
|
||||
expect(wrapper.vm.currentPage).toBe(1)
|
||||
expect(mockRefetch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
currentPage: 1,
|
||||
statusFilter: ['DENIED'],
|
||||
paginated: expect.objectContaining({
|
||||
currentPage: 1,
|
||||
}),
|
||||
filter: expect.objectContaining({
|
||||
statusFilter: ['DENIED'],
|
||||
}),
|
||||
}),
|
||||
)
|
||||
})
|
||||
@ -191,7 +199,9 @@ describe('CreationConfirm', () => {
|
||||
|
||||
expect(mockRefetch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
currentPage: 2,
|
||||
paginated: expect.objectContaining({
|
||||
currentPage: 2,
|
||||
}),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@ -102,7 +102,7 @@ import Overlay from '../components/Overlay'
|
||||
import OpenCreationsTable from '../components/Tables/OpenCreationsTable'
|
||||
import UserQuery from '../components/UserQuery'
|
||||
import AiChat from '../components/AiChat'
|
||||
import { adminListContributions } from '../graphql/adminListContributions'
|
||||
import { adminListContributions } from '../graphql/adminListContributions.graphql'
|
||||
import { adminDeleteContribution } from '../graphql/adminDeleteContribution'
|
||||
import { confirmContribution } from '../graphql/confirmContribution'
|
||||
import { denyContribution } from '../graphql/denyContribution'
|
||||
@ -282,12 +282,17 @@ watch(tabIndex, () => {
|
||||
const { onResult, onError, result, refetch } = useQuery(
|
||||
adminListContributions,
|
||||
{
|
||||
currentPage: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
statusFilter: statusFilter.value,
|
||||
query: query.value,
|
||||
noHashtag: noHashtag.value,
|
||||
hideResubmission: hideResubmission.value,
|
||||
filter: {
|
||||
statusFilter: statusFilter.value,
|
||||
query: query.value,
|
||||
noHashtag: noHashtag.value,
|
||||
hideResubmission: hideResubmission.value,
|
||||
},
|
||||
paginated: {
|
||||
currentPage: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
order: 'DESC',
|
||||
},
|
||||
},
|
||||
{
|
||||
fetchPolicy: 'no-cache',
|
||||
@ -296,12 +301,16 @@ const { onResult, onError, result, refetch } = useQuery(
|
||||
|
||||
watch([statusFilter, query, noHashtag, hideResubmission, currentPage], () => {
|
||||
refetch({
|
||||
currentPage: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
statusFilter: statusFilter.value,
|
||||
query: query.value,
|
||||
noHashtag: noHashtag.value,
|
||||
hideResubmission: hideResubmission.value,
|
||||
filter: {
|
||||
statusFilter: statusFilter.value,
|
||||
query: query.value,
|
||||
noHashtag: noHashtag.value,
|
||||
hideResubmission: hideResubmission.value,
|
||||
},
|
||||
paginated: {
|
||||
currentPage: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils'
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { nextTick, ref } from 'vue'
|
||||
import Overview from './Overview.vue'
|
||||
import { adminListContributions } from '@/graphql/adminListContributions'
|
||||
import { adminListContributionsCount } from '@/graphql/adminListContributions.graphql'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { createStore } from 'vuex'
|
||||
import { useAppToast } from '@/composables/useToast'
|
||||
@ -72,9 +72,11 @@ describe('Overview', () => {
|
||||
}
|
||||
|
||||
it('calls useQuery with correct parameters', () => {
|
||||
expect(useQuery).toHaveBeenCalledWith(adminListContributions, {
|
||||
statusFilter: ['IN_PROGRESS', 'PENDING'],
|
||||
hideResubmission: true,
|
||||
expect(useQuery).toHaveBeenCalledWith(adminListContributionsCount, {
|
||||
filter: {
|
||||
statusFilter: ['IN_PROGRESS', 'PENDING'],
|
||||
hideResubmission: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { adminListContributions } from '@/graphql/adminListContributions'
|
||||
import { adminListContributionsCount } from '@/graphql/adminListContributions.graphql'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
@ -33,9 +33,11 @@ const { t } = useI18n()
|
||||
|
||||
const { toastError } = useAppToast()
|
||||
|
||||
const { result, onResult, onError } = useQuery(adminListContributions, {
|
||||
statusFilter: statusFilter.value,
|
||||
hideResubmission: true,
|
||||
const { result, onResult, onError } = useQuery(adminListContributionsCount, {
|
||||
filter: {
|
||||
statusFilter: statusFilter.value,
|
||||
hideResubmission: true,
|
||||
},
|
||||
})
|
||||
|
||||
onResult(({ data }) => {
|
||||
|
||||
@ -36,6 +36,7 @@
|
||||
"gradido-config": "file:../config",
|
||||
"gradido-database": "file:../database",
|
||||
"graphql": "^15.5.1",
|
||||
"graphql-parse-resolve-info": "^4.13.0",
|
||||
"graphql-request": "5.0.0",
|
||||
"graphql-type-json": "0.3.2",
|
||||
"helmet": "^5.1.1",
|
||||
|
||||
@ -17,4 +17,5 @@ export const MODERATOR_RIGHTS = [
|
||||
RIGHTS.ADMIN_LIST_ALL_CONTRIBUTION_MESSAGES,
|
||||
RIGHTS.DENY_CONTRIBUTION,
|
||||
RIGHTS.ADMIN_OPEN_CREATIONS,
|
||||
RIGHTS.VIEW_USER_CONTACT,
|
||||
]
|
||||
|
||||
@ -42,6 +42,7 @@ export enum RIGHTS {
|
||||
HUMHUB_AUTO_LOGIN = 'HUMHUB_AUTO_LOGIN',
|
||||
PROJECT_BRANDING_VIEW = 'PROJECT_BRANDING_VIEW',
|
||||
LIST_HUMHUB_SPACES = 'LIST_HUMHUB_SPACES',
|
||||
VIEW_OWN_USER_CONTACT = 'VIEW_OWN_USER_CONTACT',
|
||||
// Moderator
|
||||
SEARCH_USERS = 'SEARCH_USERS',
|
||||
ADMIN_CREATE_CONTRIBUTION = 'ADMIN_CREATE_CONTRIBUTION',
|
||||
@ -59,6 +60,7 @@ export enum RIGHTS {
|
||||
DENY_CONTRIBUTION = 'DENY_CONTRIBUTION',
|
||||
ADMIN_OPEN_CREATIONS = 'ADMIN_OPEN_CREATIONS',
|
||||
ADMIN_LIST_ALL_CONTRIBUTION_MESSAGES = 'ADMIN_LIST_ALL_CONTRIBUTION_MESSAGES',
|
||||
VIEW_USER_CONTACT = 'VIEW_USER_CONTACT',
|
||||
// Moderator AI
|
||||
AI_SEND_MESSAGE = 'AI_SEND_MESSAGE',
|
||||
// Admin
|
||||
|
||||
@ -33,4 +33,5 @@ export const USER_RIGHTS = [
|
||||
RIGHTS.HUMHUB_AUTO_LOGIN,
|
||||
RIGHTS.PROJECT_BRANDING_VIEW,
|
||||
RIGHTS.LIST_HUMHUB_SPACES,
|
||||
RIGHTS.VIEW_USER_CONTACT,
|
||||
]
|
||||
|
||||
@ -1,20 +1,27 @@
|
||||
/* eslint-disable type-graphql/invalid-nullable-input-type */
|
||||
import { IsPositive, IsEnum } from 'class-validator'
|
||||
import { ArgsType, Field, Int } from 'type-graphql'
|
||||
import { ArgsType, Field, Int, InputType } from 'type-graphql'
|
||||
|
||||
import { Order } from '@enum/Order'
|
||||
|
||||
@ArgsType()
|
||||
@InputType()
|
||||
export class Paginated {
|
||||
@Field(() => Int, { nullable: true })
|
||||
@Field(() => Int)
|
||||
@IsPositive()
|
||||
currentPage?: number
|
||||
currentPage: number
|
||||
|
||||
@Field(() => Int, { nullable: true })
|
||||
@Field(() => Int)
|
||||
@IsPositive()
|
||||
pageSize?: number
|
||||
pageSize: number
|
||||
|
||||
@Field(() => Order, { nullable: true })
|
||||
@Field(() => Order)
|
||||
@IsEnum(Order)
|
||||
order?: Order
|
||||
order: Order
|
||||
|
||||
public constructor(pageSize?: number, currentPage?: number, order?: Order) {
|
||||
this.pageSize = pageSize ?? 3
|
||||
this.currentPage = currentPage ?? 1
|
||||
this.order = order ?? Order.DESC
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import { IsBoolean, IsPositive, IsString } from 'class-validator'
|
||||
import { Field, ArgsType, Int } from 'type-graphql'
|
||||
import { Field, ArgsType, Int, InputType } from 'type-graphql'
|
||||
|
||||
import { ContributionStatus } from '@enum/ContributionStatus'
|
||||
|
||||
import { isContributionStatusArray } from '@/graphql/validator/ContributionStatusArray'
|
||||
|
||||
@ArgsType()
|
||||
@InputType()
|
||||
export class SearchContributionsFilterArgs {
|
||||
@Field(() => [ContributionStatus], { nullable: true, defaultValue: null })
|
||||
@isContributionStatusArray()
|
||||
|
||||
@ -1,24 +1,16 @@
|
||||
import { Contribution as dbContribution } from '@entity/Contribution'
|
||||
import { User } from '@entity/User'
|
||||
import { User as DbUser } from '@entity/User'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import { ObjectType, Field, Int } from 'type-graphql'
|
||||
|
||||
import { PublishNameType } from '@enum/PublishNameType'
|
||||
|
||||
import { PublishNameLogic } from '@/data/PublishName.logic'
|
||||
import { User } from './User'
|
||||
|
||||
@ObjectType()
|
||||
export class Contribution {
|
||||
constructor(contribution: dbContribution, user?: User | null) {
|
||||
constructor(contribution: dbContribution, user?: DbUser | null) {
|
||||
this.id = contribution.id
|
||||
this.firstName = user?.firstName ?? null
|
||||
this.lastName = user?.lastName ?? null
|
||||
this.email = user?.emailContact?.email ?? null
|
||||
this.username = user?.alias ?? null
|
||||
if (user) {
|
||||
const publishNameLogic = new PublishNameLogic(user)
|
||||
this.humhubUsername = publishNameLogic.getUsername(user.humhubPublishName as PublishNameType)
|
||||
}
|
||||
this.amount = contribution.amount
|
||||
this.memo = contribution.memo
|
||||
this.createdAt = contribution.createdAt
|
||||
@ -36,26 +28,26 @@ export class Contribution {
|
||||
this.moderatorId = contribution.moderatorId
|
||||
this.userId = contribution.userId
|
||||
this.resubmissionAt = contribution.resubmissionAt
|
||||
if (user) {
|
||||
this.user = new User(user)
|
||||
}
|
||||
}
|
||||
|
||||
@Field(() => Int)
|
||||
id: number
|
||||
|
||||
@Field(() => Int, { nullable: true })
|
||||
userId: number | null
|
||||
|
||||
@Field(() => User, { nullable: true })
|
||||
user: User | null
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
firstName: string | null
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
lastName: string | null
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
email: string | null
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
username: string | null
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
humhubUsername: string | null
|
||||
|
||||
@Field(() => Decimal)
|
||||
amount: Decimal
|
||||
|
||||
@ -101,9 +93,6 @@ export class Contribution {
|
||||
@Field(() => Int, { nullable: true })
|
||||
moderatorId: number | null
|
||||
|
||||
@Field(() => Int, { nullable: true })
|
||||
userId: number | null
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
resubmissionAt: Date | null
|
||||
}
|
||||
|
||||
@ -5,10 +5,12 @@ import { ObjectType, Field, Int } from 'type-graphql'
|
||||
import { GmsPublishLocationType } from '@enum/GmsPublishLocationType'
|
||||
import { PublishNameType } from '@enum/PublishNameType'
|
||||
|
||||
import { PublishNameLogic } from '@/data/PublishName.logic'
|
||||
import { Point2Location } from '@/graphql/resolver/util/Location2Point'
|
||||
|
||||
import { KlickTipp } from './KlickTipp'
|
||||
import { Location } from './Location'
|
||||
import { UserContact } from './UserContact'
|
||||
|
||||
@ObjectType()
|
||||
export class User {
|
||||
@ -22,8 +24,13 @@ export class User {
|
||||
}
|
||||
this.gradidoID = user.gradidoID
|
||||
this.alias = user.alias
|
||||
|
||||
const publishNameLogic = new PublishNameLogic(user)
|
||||
this.humhubUsername = publishNameLogic.getUsername(user.humhubPublishName as PublishNameType)
|
||||
|
||||
if (user.emailContact) {
|
||||
this.emailChecked = user.emailContact.emailChecked
|
||||
this.emailContact = new UserContact(user.emailContact)
|
||||
}
|
||||
this.firstName = user.firstName
|
||||
this.lastName = user.lastName
|
||||
@ -63,6 +70,9 @@ export class User {
|
||||
@Field(() => String, { nullable: true })
|
||||
alias: string | null
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
humhubUsername: string | null
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
firstName: string | null
|
||||
|
||||
@ -115,6 +125,9 @@ export class User {
|
||||
@Field(() => [String])
|
||||
roles: string[]
|
||||
|
||||
@Field(() => UserContact, { nullable: true })
|
||||
emailContact: UserContact | null
|
||||
|
||||
@Field(() => Location, { nullable: true })
|
||||
userLocation: Location | null
|
||||
}
|
||||
|
||||
42
backend/src/graphql/model/UserContact.ts
Normal file
42
backend/src/graphql/model/UserContact.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { UserContact as DbUserContact } from '@entity/UserContact'
|
||||
import { ObjectType, Field, Int } from 'type-graphql'
|
||||
|
||||
@ObjectType()
|
||||
export class UserContact {
|
||||
constructor(userContact: DbUserContact) {
|
||||
Object.assign(this, userContact)
|
||||
}
|
||||
|
||||
@Field(() => Int)
|
||||
id: number
|
||||
|
||||
@Field(() => Int)
|
||||
userId: number
|
||||
|
||||
@Field(() => String)
|
||||
email: string
|
||||
|
||||
@Field(() => Boolean)
|
||||
gmsPublishEmail: boolean
|
||||
|
||||
@Field(() => Boolean)
|
||||
emailChecked: boolean
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
countryCode: string | null
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
phone: string | null
|
||||
|
||||
@Field(() => Int)
|
||||
gmsPublishPhone: number
|
||||
|
||||
@Field(() => Date)
|
||||
createdAt: Date
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
updatedAt: Date | null
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
deletedAt: Date | null
|
||||
}
|
||||
@ -2782,13 +2782,15 @@ describe('ContributionResolver', () => {
|
||||
const { errors: errorObjects } = await query({
|
||||
query: adminListContributions,
|
||||
variables: {
|
||||
statusFilter: ['INVALID_STATUS'],
|
||||
filter: {
|
||||
statusFilter: ['INVALID_STATUS'],
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(errorObjects).toMatchObject([
|
||||
{
|
||||
message:
|
||||
'Variable "$statusFilter" got invalid value "INVALID_STATUS" at "statusFilter[0]"; Value "INVALID_STATUS" does not exist in "ContributionStatus" enum.',
|
||||
'Variable "$filter" got invalid value "INVALID_STATUS" at "filter.statusFilter[0]"; Value "INVALID_STATUS" does not exist in "ContributionStatus" enum.',
|
||||
extensions: {
|
||||
code: 'BAD_USER_INPUT',
|
||||
},
|
||||
@ -2801,6 +2803,7 @@ describe('ContributionResolver', () => {
|
||||
data: { adminListContributions: contributionListObject },
|
||||
} = await query({
|
||||
query: adminListContributions,
|
||||
variables: { paginated: { pageSize: 20 } },
|
||||
})
|
||||
|
||||
expect(contributionListObject.contributionList).toHaveLength(18)
|
||||
@ -2809,165 +2812,255 @@ describe('ContributionResolver', () => {
|
||||
contributionList: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(100),
|
||||
firstName: 'Peter',
|
||||
id: expect.any(Number),
|
||||
lastName: 'Lustig',
|
||||
memo: '#firefighters',
|
||||
messagesCount: 0,
|
||||
status: 'PENDING',
|
||||
user: expect.objectContaining({
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
emailContact: expect.objectContaining({
|
||||
email: 'peter@lustig.de',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(50),
|
||||
firstName: 'Bibi',
|
||||
id: expect.any(Number),
|
||||
lastName: 'Bloxberg',
|
||||
memo: 'Herzlich Willkommen bei Gradido liebe Bibi!',
|
||||
messagesCount: 0,
|
||||
status: 'CONFIRMED',
|
||||
user: expect.objectContaining({
|
||||
firstName: 'Bibi',
|
||||
lastName: 'Bloxberg',
|
||||
emailContact: expect.objectContaining({
|
||||
email: 'bibi@bloxberg.de',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(50),
|
||||
firstName: 'Bibi',
|
||||
id: expect.any(Number),
|
||||
lastName: 'Bloxberg',
|
||||
memo: 'Herzlich Willkommen bei Gradido liebe Bibi!',
|
||||
messagesCount: 0,
|
||||
status: 'CONFIRMED',
|
||||
user: expect.objectContaining({
|
||||
firstName: 'Bibi',
|
||||
lastName: 'Bloxberg',
|
||||
emailContact: expect.objectContaining({
|
||||
email: 'bibi@bloxberg.de',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(450),
|
||||
firstName: 'Bibi',
|
||||
id: expect.any(Number),
|
||||
lastName: 'Bloxberg',
|
||||
memo: 'Herzlich Willkommen bei Gradido liebe Bibi!',
|
||||
messagesCount: 0,
|
||||
status: 'CONFIRMED',
|
||||
user: expect.objectContaining({
|
||||
firstName: 'Bibi',
|
||||
lastName: 'Bloxberg',
|
||||
emailContact: expect.objectContaining({
|
||||
email: 'bibi@bloxberg.de',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(400),
|
||||
firstName: 'Peter',
|
||||
id: expect.any(Number),
|
||||
lastName: 'Lustig',
|
||||
memo: 'Herzlich Willkommen bei Gradido!',
|
||||
messagesCount: 0,
|
||||
status: 'PENDING',
|
||||
user: expect.objectContaining({
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
emailContact: expect.objectContaining({
|
||||
email: 'peter@lustig.de',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(100),
|
||||
firstName: 'Bob',
|
||||
id: expect.any(Number),
|
||||
lastName: 'der Baumeister',
|
||||
memo: 'Confirmed Contribution',
|
||||
messagesCount: 0,
|
||||
status: 'CONFIRMED',
|
||||
user: expect.objectContaining({
|
||||
firstName: 'Bob',
|
||||
lastName: 'der Baumeister',
|
||||
emailContact: expect.objectContaining({
|
||||
email: 'bob@baumeister.de',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(100),
|
||||
firstName: 'Peter',
|
||||
id: expect.any(Number),
|
||||
lastName: 'Lustig',
|
||||
memo: 'Test env contribution',
|
||||
messagesCount: 0,
|
||||
status: 'PENDING',
|
||||
user: expect.objectContaining({
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
emailContact: expect.objectContaining({
|
||||
email: 'peter@lustig.de',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(200),
|
||||
firstName: 'Bibi',
|
||||
id: expect.any(Number),
|
||||
lastName: 'Bloxberg',
|
||||
memo: 'Aktives Grundeinkommen',
|
||||
messagesCount: 0,
|
||||
status: 'PENDING',
|
||||
user: expect.objectContaining({
|
||||
firstName: 'Bibi',
|
||||
lastName: 'Bloxberg',
|
||||
emailContact: expect.objectContaining({
|
||||
email: 'bibi@bloxberg.de',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(200),
|
||||
firstName: 'Peter',
|
||||
id: expect.any(Number),
|
||||
lastName: 'Lustig',
|
||||
memo: 'Das war leider zu Viel!',
|
||||
messagesCount: 1,
|
||||
status: 'DELETED',
|
||||
user: expect.objectContaining({
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
emailContact: expect.objectContaining({
|
||||
email: 'peter@lustig.de',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(166),
|
||||
firstName: 'Räuber',
|
||||
id: expect.any(Number),
|
||||
lastName: 'Hotzenplotz',
|
||||
memo: 'Whatever contribution',
|
||||
messagesCount: 0,
|
||||
status: 'DENIED',
|
||||
user: expect.objectContaining({
|
||||
firstName: 'Räuber',
|
||||
lastName: 'Hotzenplotz',
|
||||
emailContact: expect.objectContaining({
|
||||
email: 'raeuber@hotzenplotz.de',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(166),
|
||||
firstName: 'Räuber',
|
||||
id: expect.any(Number),
|
||||
lastName: 'Hotzenplotz',
|
||||
memo: 'Whatever contribution',
|
||||
messagesCount: 0,
|
||||
status: 'DELETED',
|
||||
user: expect.objectContaining({
|
||||
firstName: 'Räuber',
|
||||
lastName: 'Hotzenplotz',
|
||||
emailContact: expect.objectContaining({
|
||||
email: 'raeuber@hotzenplotz.de',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(166),
|
||||
firstName: 'Räuber',
|
||||
id: expect.any(Number),
|
||||
lastName: 'Hotzenplotz',
|
||||
memo: 'Whatever contribution',
|
||||
messagesCount: 0,
|
||||
status: 'CONFIRMED',
|
||||
user: expect.objectContaining({
|
||||
firstName: 'Räuber',
|
||||
lastName: 'Hotzenplotz',
|
||||
emailContact: expect.objectContaining({
|
||||
email: 'raeuber@hotzenplotz.de',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(100),
|
||||
firstName: 'Bibi',
|
||||
id: expect.any(Number),
|
||||
lastName: 'Bloxberg',
|
||||
memo: 'Test contribution to delete',
|
||||
messagesCount: 0,
|
||||
status: 'DELETED',
|
||||
user: expect.objectContaining({
|
||||
firstName: 'Bibi',
|
||||
lastName: 'Bloxberg',
|
||||
emailContact: expect.objectContaining({
|
||||
email: 'bibi@bloxberg.de',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(100),
|
||||
firstName: 'Bibi',
|
||||
id: expect.any(Number),
|
||||
lastName: 'Bloxberg',
|
||||
memo: 'Test contribution to deny',
|
||||
messagesCount: 0,
|
||||
status: 'DENIED',
|
||||
user: expect.objectContaining({
|
||||
firstName: 'Bibi',
|
||||
lastName: 'Bloxberg',
|
||||
emailContact: expect.objectContaining({
|
||||
email: 'bibi@bloxberg.de',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(100),
|
||||
firstName: 'Bibi',
|
||||
id: expect.any(Number),
|
||||
lastName: 'Bloxberg',
|
||||
memo: 'Test contribution to confirm',
|
||||
messagesCount: 0,
|
||||
status: 'CONFIRMED',
|
||||
user: expect.objectContaining({
|
||||
firstName: 'Bibi',
|
||||
lastName: 'Bloxberg',
|
||||
emailContact: expect.objectContaining({
|
||||
email: 'bibi@bloxberg.de',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(100),
|
||||
firstName: 'Bibi',
|
||||
id: expect.any(Number),
|
||||
lastName: 'Bloxberg',
|
||||
memo: 'Test IN_PROGRESS contribution',
|
||||
messagesCount: 1,
|
||||
status: 'IN_PROGRESS',
|
||||
user: expect.objectContaining({
|
||||
firstName: 'Bibi',
|
||||
lastName: 'Bloxberg',
|
||||
emailContact: expect.objectContaining({
|
||||
email: 'bibi@bloxberg.de',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(10),
|
||||
firstName: 'Bibi',
|
||||
id: expect.any(Number),
|
||||
lastName: 'Bloxberg',
|
||||
memo: 'Test PENDING contribution update',
|
||||
messagesCount: 2,
|
||||
status: 'PENDING',
|
||||
user: expect.objectContaining({
|
||||
firstName: 'Bibi',
|
||||
lastName: 'Bloxberg',
|
||||
emailContact: expect.objectContaining({
|
||||
email: 'bibi@bloxberg.de',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(1000),
|
||||
firstName: 'Bibi',
|
||||
id: expect.any(Number),
|
||||
lastName: 'Bloxberg',
|
||||
memo: 'Herzlich Willkommen bei Gradido!',
|
||||
messagesCount: 0,
|
||||
status: 'CONFIRMED',
|
||||
user: expect.objectContaining({
|
||||
firstName: 'Bibi',
|
||||
lastName: 'Bloxberg',
|
||||
emailContact: expect.objectContaining({
|
||||
email: 'bibi@bloxberg.de',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
})
|
||||
@ -2979,10 +3072,14 @@ describe('ContributionResolver', () => {
|
||||
} = await query({
|
||||
query: adminListContributions,
|
||||
variables: {
|
||||
currentPage: 1,
|
||||
pageSize: 2,
|
||||
order: Order.DESC,
|
||||
statusFilter: ['PENDING'],
|
||||
paginated: {
|
||||
currentPage: 1,
|
||||
pageSize: 2,
|
||||
order: Order.DESC,
|
||||
},
|
||||
filter: {
|
||||
statusFilter: ['PENDING'],
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(contributionListObject.contributionList).toHaveLength(2)
|
||||
@ -2991,21 +3088,31 @@ describe('ContributionResolver', () => {
|
||||
contributionList: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
amount: '100',
|
||||
firstName: 'Peter',
|
||||
id: expect.any(Number),
|
||||
lastName: 'Lustig',
|
||||
memo: '#firefighters',
|
||||
messagesCount: 0,
|
||||
status: 'PENDING',
|
||||
user: expect.objectContaining({
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
emailContact: expect.objectContaining({
|
||||
email: 'peter@lustig.de',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: '400',
|
||||
firstName: 'Peter',
|
||||
id: expect.any(Number),
|
||||
lastName: 'Lustig',
|
||||
memo: 'Herzlich Willkommen bei Gradido!',
|
||||
messagesCount: 0,
|
||||
status: 'PENDING',
|
||||
user: expect.objectContaining({
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
emailContact: expect.objectContaining({
|
||||
email: 'peter@lustig.de',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
expect.not.objectContaining({
|
||||
status: 'DENIED',
|
||||
@ -3030,7 +3137,10 @@ describe('ContributionResolver', () => {
|
||||
} = await query({
|
||||
query: adminListContributions,
|
||||
variables: {
|
||||
query: 'Peter',
|
||||
filter: {
|
||||
query: 'Peter',
|
||||
},
|
||||
paginated: { pageSize: 20 },
|
||||
},
|
||||
})
|
||||
expect(contributionListObject.contributionList).toHaveLength(4)
|
||||
@ -3039,39 +3149,59 @@ describe('ContributionResolver', () => {
|
||||
contributionList: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(100),
|
||||
firstName: 'Peter',
|
||||
id: expect.any(Number),
|
||||
lastName: 'Lustig',
|
||||
memo: '#firefighters',
|
||||
messagesCount: 0,
|
||||
status: 'PENDING',
|
||||
user: expect.objectContaining({
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
emailContact: expect.objectContaining({
|
||||
email: 'peter@lustig.de',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(400),
|
||||
firstName: 'Peter',
|
||||
id: expect.any(Number),
|
||||
lastName: 'Lustig',
|
||||
memo: 'Herzlich Willkommen bei Gradido!',
|
||||
messagesCount: 0,
|
||||
status: 'PENDING',
|
||||
user: expect.objectContaining({
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
emailContact: expect.objectContaining({
|
||||
email: 'peter@lustig.de',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(100),
|
||||
firstName: 'Peter',
|
||||
id: expect.any(Number),
|
||||
lastName: 'Lustig',
|
||||
memo: 'Test env contribution',
|
||||
messagesCount: 0,
|
||||
status: 'PENDING',
|
||||
user: expect.objectContaining({
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
emailContact: expect.objectContaining({
|
||||
email: 'peter@lustig.de',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(200),
|
||||
firstName: 'Peter',
|
||||
id: expect.any(Number),
|
||||
lastName: 'Lustig',
|
||||
memo: 'Das war leider zu Viel!',
|
||||
messagesCount: 1,
|
||||
status: 'DELETED',
|
||||
user: expect.objectContaining({
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
emailContact: expect.objectContaining({
|
||||
email: 'peter@lustig.de',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
})
|
||||
@ -3083,8 +3213,11 @@ describe('ContributionResolver', () => {
|
||||
} = await query({
|
||||
query: adminListContributions,
|
||||
variables: {
|
||||
query: 'Peter',
|
||||
noHashtag: true,
|
||||
filter: {
|
||||
query: 'Peter',
|
||||
noHashtag: true,
|
||||
},
|
||||
paginated: { pageSize: 20 },
|
||||
},
|
||||
})
|
||||
expect(contributionListObject.contributionList).toHaveLength(3)
|
||||
@ -3093,30 +3226,45 @@ describe('ContributionResolver', () => {
|
||||
contributionList: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(400),
|
||||
firstName: 'Peter',
|
||||
id: expect.any(Number),
|
||||
lastName: 'Lustig',
|
||||
memo: 'Herzlich Willkommen bei Gradido!',
|
||||
messagesCount: 0,
|
||||
status: 'PENDING',
|
||||
user: expect.objectContaining({
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
emailContact: expect.objectContaining({
|
||||
email: 'peter@lustig.de',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(100),
|
||||
firstName: 'Peter',
|
||||
id: expect.any(Number),
|
||||
lastName: 'Lustig',
|
||||
memo: 'Test env contribution',
|
||||
messagesCount: 0,
|
||||
status: 'PENDING',
|
||||
user: expect.objectContaining({
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
emailContact: expect.objectContaining({
|
||||
email: 'peter@lustig.de',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(200),
|
||||
firstName: 'Peter',
|
||||
id: expect.any(Number),
|
||||
lastName: 'Lustig',
|
||||
memo: 'Das war leider zu Viel!',
|
||||
messagesCount: 1,
|
||||
status: 'DELETED',
|
||||
user: expect.objectContaining({
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
emailContact: expect.objectContaining({
|
||||
email: 'peter@lustig.de',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
})
|
||||
@ -3128,7 +3276,9 @@ describe('ContributionResolver', () => {
|
||||
} = await query({
|
||||
query: adminListContributions,
|
||||
variables: {
|
||||
query: '#firefighter',
|
||||
filter: {
|
||||
query: '#firefighter',
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(contributionListObject.contributionList).toHaveLength(1)
|
||||
@ -3137,12 +3287,17 @@ describe('ContributionResolver', () => {
|
||||
contributionList: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(100),
|
||||
firstName: 'Peter',
|
||||
id: expect.any(Number),
|
||||
lastName: 'Lustig',
|
||||
memo: '#firefighters',
|
||||
messagesCount: 0,
|
||||
status: 'PENDING',
|
||||
user: expect.objectContaining({
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
emailContact: expect.objectContaining({
|
||||
email: 'peter@lustig.de',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
})
|
||||
@ -3154,8 +3309,10 @@ describe('ContributionResolver', () => {
|
||||
} = await query({
|
||||
query: adminListContributions,
|
||||
variables: {
|
||||
query: '#firefighter',
|
||||
noHashtag: true,
|
||||
filter: {
|
||||
query: '#firefighter',
|
||||
noHashtag: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(contributionListObject.contributionList).toHaveLength(0)
|
||||
@ -3171,7 +3328,10 @@ describe('ContributionResolver', () => {
|
||||
} = await query({
|
||||
query: adminListContributions,
|
||||
variables: {
|
||||
query: 'RAEUBER', // only found in lowercase in the email
|
||||
filter: {
|
||||
query: 'RAEUBER', // only found in lowercase in the email
|
||||
},
|
||||
paginated: { pageSize: 20 },
|
||||
},
|
||||
})
|
||||
expect(contributionListObject.contributionList).toHaveLength(3)
|
||||
@ -3180,30 +3340,45 @@ describe('ContributionResolver', () => {
|
||||
contributionList: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(166),
|
||||
firstName: 'Räuber',
|
||||
id: expect.any(Number),
|
||||
lastName: 'Hotzenplotz',
|
||||
memo: 'Whatever contribution',
|
||||
messagesCount: 0,
|
||||
status: 'DENIED',
|
||||
user: expect.objectContaining({
|
||||
firstName: 'Räuber',
|
||||
lastName: 'Hotzenplotz',
|
||||
emailContact: expect.objectContaining({
|
||||
email: 'raeuber@hotzenplotz.de',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(166),
|
||||
firstName: 'Räuber',
|
||||
id: expect.any(Number),
|
||||
lastName: 'Hotzenplotz',
|
||||
memo: 'Whatever contribution',
|
||||
messagesCount: 0,
|
||||
status: 'DELETED',
|
||||
user: expect.objectContaining({
|
||||
firstName: 'Räuber',
|
||||
lastName: 'Hotzenplotz',
|
||||
emailContact: expect.objectContaining({
|
||||
email: 'raeuber@hotzenplotz.de',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(166),
|
||||
firstName: 'Räuber',
|
||||
id: expect.any(Number),
|
||||
lastName: 'Hotzenplotz',
|
||||
memo: 'Whatever contribution',
|
||||
messagesCount: 0,
|
||||
status: 'CONFIRMED',
|
||||
user: expect.objectContaining({
|
||||
firstName: 'Räuber',
|
||||
lastName: 'Hotzenplotz',
|
||||
emailContact: expect.objectContaining({
|
||||
email: 'raeuber@hotzenplotz.de',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
})
|
||||
|
||||
@ -4,7 +4,20 @@ import { Transaction as DbTransaction } from '@entity/Transaction'
|
||||
import { User as DbUser } from '@entity/User'
|
||||
import { UserContact } from '@entity/UserContact'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import { Arg, Args, Authorized, Ctx, Int, Mutation, Query, Resolver } from 'type-graphql'
|
||||
import { GraphQLResolveInfo } from 'graphql'
|
||||
import {
|
||||
Arg,
|
||||
Args,
|
||||
Authorized,
|
||||
Ctx,
|
||||
FieldResolver,
|
||||
Info,
|
||||
Int,
|
||||
Mutation,
|
||||
Query,
|
||||
Resolver,
|
||||
Root,
|
||||
} from 'type-graphql'
|
||||
|
||||
import { AdminCreateContributionArgs } from '@arg/AdminCreateContributionArgs'
|
||||
import { AdminUpdateContributionArgs } from '@arg/AdminUpdateContributionArgs'
|
||||
@ -20,6 +33,7 @@ import { Contribution, ContributionListResult } from '@model/Contribution'
|
||||
import { Decay } from '@model/Decay'
|
||||
import { OpenCreation } from '@model/OpenCreation'
|
||||
import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
|
||||
import { User } from '@model/User'
|
||||
|
||||
import { RIGHTS } from '@/auth/RIGHTS'
|
||||
import {
|
||||
@ -48,11 +62,12 @@ import { fullName } from '@/util/utilities'
|
||||
|
||||
import { findContribution } from './util/contributions'
|
||||
import { getUserCreation, validateContribution, getOpenCreations } from './util/creations'
|
||||
import { extractGraphQLFields, extractGraphQLFieldsForSelect } from './util/extractGraphQLFields'
|
||||
import { findContributions } from './util/findContributions'
|
||||
import { getLastTransaction } from './util/getLastTransaction'
|
||||
import { sendTransactionsToDltConnector } from './util/sendTransactionsToDltConnector'
|
||||
|
||||
@Resolver()
|
||||
@Resolver(() => Contribution)
|
||||
export class ContributionResolver {
|
||||
@Authorized([RIGHTS.ADMIN_LIST_CONTRIBUTIONS])
|
||||
@Query(() => Contribution)
|
||||
@ -321,15 +336,37 @@ export class ContributionResolver {
|
||||
@Authorized([RIGHTS.ADMIN_LIST_CONTRIBUTIONS])
|
||||
@Query(() => ContributionListResult)
|
||||
async adminListContributions(
|
||||
@Args() paginated: Paginated,
|
||||
@Args() filter: SearchContributionsFilterArgs,
|
||||
): Promise<ContributionListResult> {
|
||||
const [dbContributions, count] = await findContributions(paginated, filter, true, {
|
||||
user: {
|
||||
emailContact: true,
|
||||
},
|
||||
messages: true,
|
||||
@Arg('filter', () => SearchContributionsFilterArgs, {
|
||||
defaultValue: new SearchContributionsFilterArgs(),
|
||||
})
|
||||
filter: SearchContributionsFilterArgs,
|
||||
@Arg('paginated', () => Paginated, { defaultValue: new Paginated() }) paginated: Paginated,
|
||||
@Info() info: GraphQLResolveInfo,
|
||||
): Promise<ContributionListResult> {
|
||||
// Check if only count was requested (without contributionList)
|
||||
const fields = Object.keys(extractGraphQLFields(info))
|
||||
const countOnly: boolean = fields.includes('contributionCount') && fields.length === 1
|
||||
// check if related user was requested
|
||||
const userRequested =
|
||||
fields.includes('user') || filter.userId !== undefined || filter.query !== undefined
|
||||
// check if related emailContact was requested
|
||||
const emailContactRequested = fields.includes('user.emailContact') || filter.query !== undefined
|
||||
// check if related messages were requested
|
||||
const messagesRequested = ['messagesCount', 'messages'].some((field) => fields.includes(field))
|
||||
const [dbContributions, count] = await findContributions(
|
||||
paginated,
|
||||
filter,
|
||||
true,
|
||||
{
|
||||
user: userRequested
|
||||
? {
|
||||
emailContact: emailContactRequested,
|
||||
}
|
||||
: false,
|
||||
messages: messagesRequested,
|
||||
},
|
||||
countOnly,
|
||||
)
|
||||
|
||||
return new ContributionListResult(
|
||||
count,
|
||||
@ -573,4 +610,21 @@ export class ContributionResolver {
|
||||
|
||||
return !!res
|
||||
}
|
||||
|
||||
// Field resolvers
|
||||
@Authorized([RIGHTS.USER])
|
||||
@FieldResolver(() => User)
|
||||
async user(
|
||||
@Root() contribution: DbContribution,
|
||||
@Info() info: GraphQLResolveInfo,
|
||||
): Promise<User> {
|
||||
let user = contribution.user
|
||||
if (!user) {
|
||||
const queryBuilder = DbUser.createQueryBuilder('user')
|
||||
queryBuilder.where('user.id = :userId', { userId: contribution.userId })
|
||||
extractGraphQLFieldsForSelect(info, queryBuilder, 'user')
|
||||
user = await queryBuilder.getOneOrFail()
|
||||
}
|
||||
return new User(user)
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,8 +8,21 @@ import { ProjectBranding } from '@entity/ProjectBranding'
|
||||
import { TransactionLink as DbTransactionLink } from '@entity/TransactionLink'
|
||||
import { User as DbUser } from '@entity/User'
|
||||
import { UserContact as DbUserContact } from '@entity/UserContact'
|
||||
import { GraphQLResolveInfo } from 'graphql'
|
||||
import i18n from 'i18n'
|
||||
import { Resolver, Query, Args, Arg, Authorized, Ctx, Mutation, Int } from 'type-graphql'
|
||||
import {
|
||||
Resolver,
|
||||
Query,
|
||||
Args,
|
||||
Arg,
|
||||
Authorized,
|
||||
Ctx,
|
||||
Mutation,
|
||||
Int,
|
||||
Root,
|
||||
FieldResolver,
|
||||
Info,
|
||||
} from 'type-graphql'
|
||||
import { IRestResponse } from 'typed-rest-client'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
@ -29,6 +42,7 @@ import { SearchAdminUsersResult } from '@model/AdminUser'
|
||||
import { GmsUserAuthenticationResult } from '@model/GmsUserAuthenticationResult'
|
||||
import { User } from '@model/User'
|
||||
import { UserAdmin, SearchUsersResult } from '@model/UserAdmin'
|
||||
import { UserContact } from '@model/UserContact'
|
||||
import { UserLocationResult } from '@model/UserLocationResult'
|
||||
|
||||
import { HumHubClient } from '@/apis/humhub/HumHubClient'
|
||||
@ -79,6 +93,7 @@ import { authenticateGmsUserPlayground } from './util/authenticateGmsUserPlaygro
|
||||
import { getHomeCommunity } from './util/communities'
|
||||
import { compareGmsRelevantUserSettings } from './util/compareGmsRelevantUserSettings'
|
||||
import { getUserCreations } from './util/creations'
|
||||
import { extractGraphQLFieldsForSelect } from './util/extractGraphQLFields'
|
||||
import { findUserByIdentifier } from './util/findUserByIdentifier'
|
||||
import { findUsers } from './util/findUsers'
|
||||
import { getKlicktippState } from './util/getKlicktippState'
|
||||
@ -126,7 +141,7 @@ const newGradidoID = async (): Promise<string> => {
|
||||
return gradidoId
|
||||
}
|
||||
|
||||
@Resolver()
|
||||
@Resolver(() => User)
|
||||
export class UserResolver {
|
||||
@Authorized([RIGHTS.VERIFY_LOGIN])
|
||||
@Query(() => User)
|
||||
@ -1035,6 +1050,30 @@ export class UserResolver {
|
||||
const modelUser = new User(foundDbUser)
|
||||
return modelUser
|
||||
}
|
||||
|
||||
// FIELD RESOLVERS
|
||||
@FieldResolver(() => UserContact)
|
||||
async emailContact(
|
||||
@Root() user: DbUser,
|
||||
@Ctx() context: Context,
|
||||
@Info() info: GraphQLResolveInfo,
|
||||
): Promise<UserContact> {
|
||||
// Check if user has the necessary permissions to view user contact
|
||||
// Either they need VIEW_USER_CONTACT right, or they need VIEW_OWN_USER_CONTACT and must be viewing their own contact
|
||||
if (!context.role?.hasRight(RIGHTS.VIEW_USER_CONTACT)) {
|
||||
if (!context.role?.hasRight(RIGHTS.VIEW_OWN_USER_CONTACT) || context.user?.id !== user.id) {
|
||||
throw new LogError('User does not have permission to view this user contact', user.id)
|
||||
}
|
||||
}
|
||||
let userContact = user.emailContact
|
||||
if (!userContact) {
|
||||
const queryBuilder = DbUserContact.createQueryBuilder('userContact')
|
||||
queryBuilder.where('userContact.userId = :userId', { userId: user.id })
|
||||
extractGraphQLFieldsForSelect(info, queryBuilder, 'userContact')
|
||||
userContact = await queryBuilder.getOneOrFail()
|
||||
}
|
||||
return new UserContact(userContact)
|
||||
}
|
||||
}
|
||||
|
||||
export async function findUserByEmail(email: string): Promise<DbUser> {
|
||||
|
||||
48
backend/src/graphql/resolver/util/extractGraphQLFields.ts
Normal file
48
backend/src/graphql/resolver/util/extractGraphQLFields.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { ObjectLiteral, SelectQueryBuilder } from '@dbTools/typeorm'
|
||||
import { GraphQLResolveInfo } from 'graphql'
|
||||
import {
|
||||
parseResolveInfo,
|
||||
ResolveTree,
|
||||
simplifyParsedResolveInfoFragmentWithType,
|
||||
} from 'graphql-parse-resolve-info'
|
||||
|
||||
/**
|
||||
* Extracts the requested fields from GraphQL
|
||||
* @param info GraphQLResolveInfo
|
||||
*/
|
||||
export function extractGraphQLFields(info: GraphQLResolveInfo): object {
|
||||
const parsedInfo = parseResolveInfo(info)
|
||||
if (!parsedInfo) {
|
||||
throw new Error('Could not parse resolve info')
|
||||
}
|
||||
|
||||
return simplifyParsedResolveInfoFragmentWithType(parsedInfo as ResolveTree, info.returnType)
|
||||
.fields
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the requested fields from GraphQL and applies them to a TypeORM query.
|
||||
* @param info GraphQLResolveInfo
|
||||
* @param queryBuilder TypeORM QueryBuilder
|
||||
* @param alias the table alias for select
|
||||
*/
|
||||
export function extractGraphQLFieldsForSelect<T extends ObjectLiteral>(
|
||||
info: GraphQLResolveInfo,
|
||||
queryBuilder: SelectQueryBuilder<T>,
|
||||
alias: string,
|
||||
) {
|
||||
const requestedFields = Object.keys(extractGraphQLFields(info))
|
||||
|
||||
if (requestedFields.length > 0) {
|
||||
// Filter out fields that don't exist in the entity type T
|
||||
const entityName = queryBuilder.alias.charAt(0).toUpperCase() + queryBuilder.alias.slice(1)
|
||||
const metadata = queryBuilder.connection.getMetadata(entityName)
|
||||
const validFields = requestedFields.filter(
|
||||
(field) => metadata.findColumnWithPropertyName(field) !== undefined,
|
||||
)
|
||||
|
||||
if (requestedFields.length > 0) {
|
||||
queryBuilder.select(validFields.map((field) => `${alias}.${field}`))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -14,7 +14,6 @@ import { Paginated } from '@arg/Paginated'
|
||||
import { SearchContributionsFilterArgs } from '@arg/SearchContributionsFilterArgs'
|
||||
import { Connection } from '@typeorm/connection'
|
||||
|
||||
import { Order } from '@/graphql/enum/Order'
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
interface Relations {
|
||||
@ -36,10 +35,11 @@ function joinRelationsRecursive(
|
||||
}
|
||||
|
||||
export const findContributions = async (
|
||||
{ pageSize = 3, currentPage = 1, order = Order.DESC }: Paginated,
|
||||
{ pageSize, currentPage, order }: Paginated,
|
||||
filter: SearchContributionsFilterArgs,
|
||||
withDeleted = false,
|
||||
relations: Relations | undefined = undefined,
|
||||
countOnly = false,
|
||||
): Promise<[DbContribution[], number]> => {
|
||||
const connection = await Connection.getInstance()
|
||||
if (!connection) {
|
||||
@ -76,6 +76,9 @@ export const findContributions = async (
|
||||
}),
|
||||
)
|
||||
}
|
||||
if (countOnly) {
|
||||
return [[], await queryBuilder.getCount()]
|
||||
}
|
||||
return queryBuilder
|
||||
.orderBy('Contribution.createdAt', order)
|
||||
.addOrderBy('Contribution.id', order)
|
||||
|
||||
@ -300,29 +300,18 @@ query ($currentPage: Int = 1, $pageSize: Int = 5, $order: Order = DESC, $statusF
|
||||
// from admin interface
|
||||
|
||||
export const adminListContributions = gql`
|
||||
query (
|
||||
$currentPage: Int = 1
|
||||
$pageSize: Int = 25
|
||||
$order: Order = DESC
|
||||
$statusFilter: [ContributionStatus!]
|
||||
$userId: Int
|
||||
$query: String
|
||||
$noHashtag: Boolean
|
||||
) {
|
||||
adminListContributions(
|
||||
currentPage: $currentPage
|
||||
pageSize: $pageSize
|
||||
order: $order
|
||||
statusFilter: $statusFilter
|
||||
userId: $userId
|
||||
query: $query
|
||||
noHashtag: $noHashtag
|
||||
) {
|
||||
query ($filter: SearchContributionsFilterArgs, $paginated: Paginated) {
|
||||
adminListContributions(filter: $filter, paginated: $paginated) {
|
||||
contributionCount
|
||||
contributionList {
|
||||
id
|
||||
firstName
|
||||
lastName
|
||||
user {
|
||||
emailContact {
|
||||
email
|
||||
}
|
||||
firstName
|
||||
lastName
|
||||
}
|
||||
amount
|
||||
memo
|
||||
createdAt
|
||||
|
||||
@ -3852,6 +3852,14 @@ graphql-extensions@^0.16.0:
|
||||
apollo-server-env "^3.2.0"
|
||||
apollo-server-types "^0.10.0"
|
||||
|
||||
graphql-parse-resolve-info@^4.13.0:
|
||||
version "4.13.0"
|
||||
resolved "https://registry.yarnpkg.com/graphql-parse-resolve-info/-/graphql-parse-resolve-info-4.13.0.tgz#03627032e25917bd6f9ed89e768568c61200e6ff"
|
||||
integrity sha512-VVJ1DdHYcR7hwOGQKNH+QTzuNgsLA8l/y436HtP9YHoX6nmwXRWq3xWthU3autMysXdm0fQUbhTZCx0W9ICozw==
|
||||
dependencies:
|
||||
debug "^4.1.1"
|
||||
tslib "^2.0.1"
|
||||
|
||||
graphql-query-complexity@^0.7.0:
|
||||
version "0.7.2"
|
||||
resolved "https://registry.yarnpkg.com/graphql-query-complexity/-/graphql-query-complexity-0.7.2.tgz#7fc6bb20930ab1b666ecf3bbfb24b65b6f08ecc4"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user