Merge branch 'master' into 2632-feature-dockerfile-for-federation

This commit is contained in:
clauspeterhuebner 2023-02-16 14:07:40 +01:00 committed by GitHub
commit c2a5482d6a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 660 additions and 211 deletions

View File

@ -497,7 +497,7 @@ jobs:
report_name: Coverage Admin Interface report_name: Coverage Admin Interface
type: lcov type: lcov
result_path: ./coverage/lcov.info result_path: ./coverage/lcov.info
min_coverage: 96 min_coverage: 97
token: ${{ github.token }} token: ${{ github.token }}
############################################################################## ##############################################################################

View File

@ -42,29 +42,72 @@ describe('ContributionLink', () => {
expect(wrapper.find('div.contribution-link').exists()).toBe(true) expect(wrapper.find('div.contribution-link').exists()).toBe(true)
}) })
describe('function editContributionLinkData', () => { it('has one contribution link in table', () => {
beforeEach(() => { expect(wrapper.find('div.contribution-link-list').find('tbody').findAll('tr')).toHaveLength(1)
wrapper.vm.editContributionLinkData() })
it('has contribution form not visible by default', () => {
expect(wrapper.find('#newContribution').isVisible()).toBe(false)
})
describe('click on create new contribution', () => {
beforeEach(async () => {
await wrapper.find('[data-test="new-contribution-link-button"]').trigger('click')
}) })
it('emits toggle::collapse new Contribution', async () => {
await expect(wrapper.vm.$root.$emit('bv::toggle::collapse', 'newContribution')).toBeTruthy() it('shows the contribution form', () => {
expect(wrapper.find('#newContribution').isVisible()).toBe(true)
})
describe('click on create new contribution again', () => {
beforeEach(async () => {
await wrapper.find('[data-test="new-contribution-link-button"]').trigger('click')
})
it('closes the contribution form', () => {
expect(wrapper.find('#newContribution').isVisible()).toBe(false)
})
})
describe('click on close button', () => {
beforeEach(async () => {
await wrapper.find('button.btn-secondary').trigger('click')
})
it('closes the contribution form', () => {
expect(wrapper.find('#newContribution').isVisible()).toBe(false)
})
}) })
}) })
describe('function closeContributionForm', () => { describe('edit contribution link', () => {
beforeEach(async () => { beforeEach(async () => {
await wrapper.setData({ visible: true }) await wrapper
wrapper.vm.closeContributionForm() .find('div.contribution-link-list')
.find('tbody')
.findAll('tr')
.at(0)
.findAll('button')
.at(1)
.trigger('click')
}) })
it('emits toggle::collapse close Contribution-Form ', async () => { it('shows the contribution form', () => {
await expect(wrapper.vm.$root.$emit('bv::toggle::collapse', 'newContribution')).toBeTruthy() expect(wrapper.find('#newContribution').isVisible()).toBe(true)
}) })
it('editContributionLink is false', async () => {
await expect(wrapper.vm.editContributionLink).toBe(false) it('does not show the new contribution button', () => {
expect(wrapper.find('[data-test="new-contribution-link-button"]').exists()).toBe(false)
}) })
it('contributionLinkData is empty', async () => {
await expect(wrapper.vm.contributionLinkData).toEqual({}) describe('click on close button', () => {
beforeEach(async () => {
await wrapper.find('button.btn-secondary').trigger('click')
})
it('closes the contribution form', () => {
expect(wrapper.find('#newContribution').isVisible()).toBe(false)
})
}) })
}) })
}) })

View File

@ -10,8 +10,9 @@
> >
<b-button <b-button
v-if="!editContributionLink" v-if="!editContributionLink"
v-b-toggle.newContribution @click="visible = !visible"
class="my-3 d-flex justify-content-left" class="my-3 d-flex justify-content-left"
data-test="new-contribution-link-button"
> >
{{ $t('math.plus') }} {{ $t('contributionLink.newContributionLink') }} {{ $t('math.plus') }} {{ $t('contributionLink.newContributionLink') }}
</b-button> </b-button>

View File

@ -70,8 +70,6 @@ export default {
formatter: (value, key, item) => { formatter: (value, key, item) => {
if (value) { if (value) {
return this.$d(new Date(value)) return this.$d(new Date(value))
} else {
return null
} }
}, },
}, },
@ -81,8 +79,6 @@ export default {
formatter: (value, key, item) => { formatter: (value, key, item) => {
if (value) { if (value) {
return this.$d(new Date(value)) return this.$d(new Date(value))
} else {
return null
} }
}, },
}, },

View File

@ -68,13 +68,23 @@ describe('NavBar', () => {
}) })
describe('wallet', () => { describe('wallet', () => {
const assignLocationSpy = jest.fn() const windowLocationMock = jest.fn()
const windowLocation = window.location
beforeEach(async () => { beforeEach(async () => {
delete window.location
window.location = {
assign: windowLocationMock,
}
await wrapper.findAll('.nav-item').at(5).find('a').trigger('click') await wrapper.findAll('.nav-item').at(5).find('a').trigger('click')
}) })
afterEach(() => {
delete window.location
window.location = windowLocation
})
it.skip('changes window location to wallet', () => { it.skip('changes window location to wallet', () => {
expect(assignLocationSpy).toBeCalledWith('valid-token') expect(windowLocationMock()).toBe('valid-token')
}) })
it('dispatches logout to store', () => { it('dispatches logout to store', () => {
@ -84,6 +94,7 @@ describe('NavBar', () => {
describe('logout', () => { describe('logout', () => {
const windowLocationMock = jest.fn() const windowLocationMock = jest.fn()
const windowLocation = window.location
beforeEach(async () => { beforeEach(async () => {
delete window.location delete window.location
window.location = { window.location = {
@ -92,6 +103,11 @@ describe('NavBar', () => {
await wrapper.findAll('.nav-item').at(5).find('a').trigger('click') await wrapper.findAll('.nav-item').at(5).find('a').trigger('click')
}) })
afterEach(() => {
delete window.location
window.location = windowLocation
})
it('redirects to /logout', () => { it('redirects to /logout', () => {
expect(windowLocationMock).toBeCalledWith('http://localhost/login') expect(windowLocationMock).toBeCalledWith('http://localhost/login')
}) })

View File

@ -10,12 +10,11 @@
<b-collapse id="nav-collapse" is-nav> <b-collapse id="nav-collapse" is-nav>
<b-navbar-nav> <b-navbar-nav>
<b-nav-item to="/user">{{ $t('navbar.user_search') }}</b-nav-item> <b-nav-item to="/user">{{ $t('navbar.user_search') }}</b-nav-item>
<b-nav-item <b-nav-item class="bg-color-creation p-1" to="/creation-confirm">
v-show="$store.state.openCreations > 0" {{ $t('creation') }}
class="bg-color-creation p-1" <b-badge v-show="$store.state.openCreations > 0" variant="danger">
to="/creation-confirm" {{ $store.state.openCreations }}
> </b-badge>
{{ $store.state.openCreations }} {{ $t('navbar.open_creation') }}
</b-nav-item> </b-nav-item>
<b-nav-item to="/contribution-links"> <b-nav-item to="/contribution-links">
{{ $t('navbar.automaticContributions') }} {{ $t('navbar.automaticContributions') }}
@ -55,7 +54,4 @@ export default {
height: 2rem; height: 2rem;
padding-left: 10px; padding-left: 10px;
} }
.bg-color-creation {
background-color: #cf1010dc;
}
</style> </style>

View File

@ -13,7 +13,8 @@
<b-row> <b-row>
<b-col class="col-3">{{ $t('creation_for_month') }}</b-col> <b-col class="col-3">{{ $t('creation_for_month') }}</b-col>
<b-col class="h3"> <b-col class="h3">
{{ $d(new Date(item.date), 'month') }} {{ $d(new Date(item.date), 'year') }} {{ $d(new Date(item.contributionDate), 'month') }}
{{ $d(new Date(item.contributionDate), 'year') }}
</b-col> </b-col>
</b-row> </b-row>
<b-row> <b-row>

View File

@ -1,6 +1,17 @@
<template> <template>
<div class="open-creations-table"> <div class="open-creations-table">
<b-table-lite :items="items" :fields="fields" caption-top striped hover stacked="md"> <b-table-lite
:items="items"
:fields="fields"
caption-top
striped
hover
stacked="md"
:tbody-tr-class="rowClass"
>
<template #cell(state)="row">
<b-icon :icon="getStatusIcon(row.item.state)"></b-icon>
</template>
<template #cell(bookmark)="row"> <template #cell(bookmark)="row">
<b-button <b-button
variant="danger" variant="danger"
@ -37,6 +48,16 @@
</b-button> </b-button>
</div> </div>
</template> </template>
<template #cell(reActive)>
<b-button variant="warning" size="md" class="mr-2">
<b-icon icon="arrow-up" variant="light"></b-icon>
</b-button>
</template>
<template #cell(chatCreation)="row">
<b-button v-if="row.item.messagesCount > 0" @click="rowToggleDetails(row, 0)">
<b-icon icon="chat-dots"></b-icon>
</b-button>
</template>
<template #cell(deny)="row"> <template #cell(deny)="row">
<div v-if="$store.state.moderator.id !== row.item.userId"> <div v-if="$store.state.moderator.id !== row.item.userId">
<b-button <b-button
@ -100,6 +121,14 @@ import RowDetails from '../RowDetails.vue'
import EditCreationFormular from '../EditCreationFormular.vue' import EditCreationFormular from '../EditCreationFormular.vue'
import ContributionMessagesList from '../ContributionMessages/ContributionMessagesList.vue' import ContributionMessagesList from '../ContributionMessages/ContributionMessagesList.vue'
const iconMap = {
IN_PROGRESS: 'question-square',
PENDING: 'bell-fill',
CONFIRMED: 'check',
DELETED: 'trash',
DENIED: 'x-circle',
}
export default { export default {
name: 'OpenCreationsTable', name: 'OpenCreationsTable',
mixins: [toggleRowDetails], mixins: [toggleRowDetails],
@ -129,6 +158,14 @@ export default {
} }
}, },
methods: { methods: {
getStatusIcon(status) {
return iconMap[status] ? iconMap[status] : 'default-icon'
},
rowClass(item, type) {
if (!item || type !== 'row') return
if (item.state === 'CONFIRMED') return 'table-success'
if (item.state === 'DENIED') return 'table-info'
},
updateCreationData(data) { updateCreationData(data) {
const row = data.row const row = data.row
this.$emit('update-contributions', data) this.$emit('update-contributions', data)

View File

@ -0,0 +1,34 @@
import gql from 'graphql-tag'
export const listAllContributions = gql`
query (
$currentPage: Int = 1
$pageSize: Int = 25
$order: Order = DESC
$statusFilter: [ContributionStatus!]
) {
listAllContributions(
currentPage: $currentPage
pageSize: $pageSize
order: $order
statusFilter: $statusFilter
) {
contributionCount
contributionList {
id
firstName
lastName
amount
memo
createdAt
contributionDate
confirmedAt
confirmedBy
state
messagesCount
deniedAt
deniedBy
}
}
}
`

View File

@ -1,20 +0,0 @@
import gql from 'graphql-tag'
export const listUnconfirmedContributions = gql`
query {
listUnconfirmedContributions {
id
firstName
lastName
userId
email
amount
memo
date
moderator
creation
state
messageCount
}
}
`

View File

@ -1,6 +1,7 @@
{ {
"all_emails": "Alle Nutzer", "all_emails": "Alle Nutzer",
"back": "zurück", "back": "zurück",
"chat": "Chat",
"contributionLink": { "contributionLink": {
"amount": "Betrag", "amount": "Betrag",
"changeSaved": "Änderungen gespeichert", "changeSaved": "Änderungen gespeichert",
@ -29,6 +30,15 @@
"validFrom": "Startdatum", "validFrom": "Startdatum",
"validTo": "Enddatum" "validTo": "Enddatum"
}, },
"contributions": {
"all": "Alle",
"confirms": "Bestätigt",
"deleted": "Gelöscht",
"denied": "Abgelehnt",
"open": "Offen"
},
"created": "Geschöpft",
"createdAt": "Angelegt",
"creation": "Schöpfung", "creation": "Schöpfung",
"creationList": "Schöpfungsliste", "creationList": "Schöpfungsliste",
"creation_form": { "creation_form": {
@ -48,7 +58,6 @@
"update_creation": "Schöpfung aktualisieren" "update_creation": "Schöpfung aktualisieren"
}, },
"creation_for_month": "Schöpfung für Monat", "creation_for_month": "Schöpfung für Monat",
"date": "Datum",
"delete": "Löschen", "delete": "Löschen",
"deleted": "gelöscht", "deleted": "gelöscht",
"deleted_user": "Alle gelöschten Nutzer", "deleted_user": "Alle gelöschten Nutzer",
@ -92,13 +101,13 @@
"message": { "message": {
"request": "Die Anfrage wurde gesendet." "request": "Die Anfrage wurde gesendet."
}, },
"mod": "Mod",
"moderator": "Moderator", "moderator": "Moderator",
"name": "Name", "name": "Name",
"navbar": { "navbar": {
"automaticContributions": "Automatische Beiträge", "automaticContributions": "Automatische Beiträge",
"logout": "Abmelden", "logout": "Abmelden",
"my-account": "Mein Konto", "my-account": "Mein Konto",
"open_creation": "Offene Schöpfungen",
"statistic": "Statistik", "statistic": "Statistik",
"user_search": "Nutzersuche" "user_search": "Nutzersuche"
}, },

View File

@ -1,6 +1,7 @@
{ {
"all_emails": "All users", "all_emails": "All users",
"back": "back", "back": "back",
"chat": "Chat",
"contributionLink": { "contributionLink": {
"amount": "Amount", "amount": "Amount",
"changeSaved": "Changes saved", "changeSaved": "Changes saved",
@ -29,6 +30,15 @@
"validFrom": "Start-date", "validFrom": "Start-date",
"validTo": "End-Date" "validTo": "End-Date"
}, },
"contributions": {
"all": "All",
"confirms": "Confirmed",
"deleted": "Deleted",
"denied": "Denied",
"open": "Open"
},
"created": "Confirmed",
"createdAt": "Created",
"creation": "Creation", "creation": "Creation",
"creationList": "Creation list", "creationList": "Creation list",
"creation_form": { "creation_form": {
@ -48,7 +58,6 @@
"update_creation": "Creation update" "update_creation": "Creation update"
}, },
"creation_for_month": "Creation for month", "creation_for_month": "Creation for month",
"date": "Date",
"delete": "Delete", "delete": "Delete",
"deleted": "deleted", "deleted": "deleted",
"deleted_user": "All deleted user", "deleted_user": "All deleted user",
@ -92,13 +101,13 @@
"message": { "message": {
"request": "Request has been sent." "request": "Request has been sent."
}, },
"mod": "Mod",
"moderator": "Moderator", "moderator": "Moderator",
"name": "Name", "name": "Name",
"navbar": { "navbar": {
"automaticContributions": "Automatic Contributions", "automaticContributions": "Automatic Contributions",
"logout": "Logout", "logout": "Logout",
"my-account": "My Account", "my-account": "My Account",
"open_creation": "Open creations",
"statistic": "Statistic", "statistic": "Statistic",
"user_search": "User search" "user_search": "User search"
}, },

View File

@ -5,7 +5,7 @@ import { toastErrorSpy } from '../../test/testSetup'
const localVue = global.localVue const localVue = global.localVue
const apolloQueryMock = jest.fn().mockResolvedValueOnce({ const apolloQueryMock = jest.fn().mockResolvedValue({
data: { data: {
listContributionLinks: { listContributionLinks: {
links: [ links: [
@ -47,6 +47,7 @@ describe('ContributionLinks', () => {
beforeEach(() => { beforeEach(() => {
wrapper = Wrapper() wrapper = Wrapper()
}) })
describe('apollo returns', () => { describe('apollo returns', () => {
it('calls listContributionLinks', () => { it('calls listContributionLinks', () => {
expect(apolloQueryMock).toBeCalledWith( expect(apolloQueryMock).toBeCalledWith(
@ -57,7 +58,7 @@ describe('ContributionLinks', () => {
}) })
}) })
describe.skip('query transaction with error', () => { describe('query transaction with error', () => {
beforeEach(() => { beforeEach(() => {
apolloQueryMock.mockRejectedValue({ message: 'OUCH!' }) apolloQueryMock.mockRejectedValue({ message: 'OUCH!' })
wrapper = Wrapper() wrapper = Wrapper()

View File

@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils'
import CreationConfirm from './CreationConfirm.vue' import CreationConfirm from './CreationConfirm.vue'
import { adminDeleteContribution } from '../graphql/adminDeleteContribution' import { adminDeleteContribution } from '../graphql/adminDeleteContribution'
import { denyContribution } from '../graphql/denyContribution' import { denyContribution } from '../graphql/denyContribution'
import { listUnconfirmedContributions } from '../graphql/listUnconfirmedContributions' import { listAllContributions } from '../graphql/listAllContributions'
import { confirmContribution } from '../graphql/confirmContribution' import { confirmContribution } from '../graphql/confirmContribution'
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup' import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
import VueApollo from 'vue-apollo' import VueApollo from 'vue-apollo'
@ -38,50 +38,68 @@ const mocks = {
const defaultData = () => { const defaultData = () => {
return { return {
listUnconfirmedContributions: [ listAllContributions: {
{ contributionCount: 2,
id: 1, contributionList: [
firstName: 'Bibi', {
lastName: 'Bloxberg', id: 1,
userId: 99, firstName: 'Bibi',
email: 'bibi@bloxberg.de', lastName: 'Bloxberg',
amount: 500, userId: 99,
memo: 'Danke für alles', email: 'bibi@bloxberg.de',
date: new Date(), amount: 500,
moderator: 1, memo: 'Danke für alles',
state: 'PENDING', date: new Date(),
creation: [500, 500, 500], moderator: 1,
messageCount: 0, state: 'PENDING',
}, creation: [500, 500, 500],
{ messagesCount: 0,
id: 2, deniedBy: null,
firstName: 'Räuber', deniedAt: null,
lastName: 'Hotzenplotz', confirmedBy: null,
userId: 100, confirmedAt: null,
email: 'raeuber@hotzenplotz.de', contributionDate: new Date(),
amount: 1000000, deletedBy: null,
memo: 'Gut Ergattert', deletedAt: null,
date: new Date(), createdAt: new Date(),
moderator: 1, },
state: 'PENDING', {
creation: [500, 500, 500], id: 2,
messageCount: 0, firstName: 'Räuber',
}, lastName: 'Hotzenplotz',
], userId: 100,
email: 'raeuber@hotzenplotz.de',
amount: 1000000,
memo: 'Gut Ergattert',
date: new Date(),
moderator: 1,
state: 'PENDING',
creation: [500, 500, 500],
messagesCount: 0,
deniedBy: null,
deniedAt: null,
confirmedBy: null,
confirmedAt: null,
contributionDate: new Date(),
deletedBy: null,
deletedAt: null,
createdAt: new Date(),
},
],
},
} }
} }
describe('CreationConfirm', () => { describe('CreationConfirm', () => {
let wrapper let wrapper
const listUnconfirmedContributionsMock = jest.fn()
const adminDeleteContributionMock = jest.fn() const adminDeleteContributionMock = jest.fn()
const adminDenyContributionMock = jest.fn() const adminDenyContributionMock = jest.fn()
const confirmContributionMock = jest.fn() const confirmContributionMock = jest.fn()
mockClient.setRequestHandler( mockClient.setRequestHandler(
listUnconfirmedContributions, listAllContributions,
listUnconfirmedContributionsMock jest
.fn()
.mockRejectedValueOnce({ message: 'Ouch!' }) .mockRejectedValueOnce({ message: 'Ouch!' })
.mockResolvedValue({ data: defaultData() }), .mockResolvedValue({ data: defaultData() }),
) )
@ -117,6 +135,10 @@ describe('CreationConfirm', () => {
it('toast an error message', () => { it('toast an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Ouch!') expect(toastErrorSpy).toBeCalledWith('Ouch!')
}) })
it('has statusFilter ["IN_PROGRESS", "PENDING"]', () => {
expect(wrapper.vm.statusFilter).toEqual(['IN_PROGRESS', 'PENDING'])
})
}) })
describe('server response is succes', () => { describe('server response is succes', () => {
@ -125,17 +147,7 @@ describe('CreationConfirm', () => {
}) })
it('has two pending creations', () => { it('has two pending creations', () => {
expect(wrapper.vm.pendingCreations).toHaveLength(2) expect(wrapper.find('tbody').findAll('tr')).toHaveLength(2)
})
})
describe('store', () => {
it('commits resetOpenCreations to store', () => {
expect(storeCommitMock).toBeCalledWith('resetOpenCreations')
})
it('commits setOpenCreations to store', () => {
expect(storeCommitMock).toBeCalledWith('setOpenCreations', 2)
}) })
}) })
@ -316,5 +328,94 @@ describe('CreationConfirm', () => {
}) })
}) })
}) })
describe('filter tabs', () => {
describe('click tab "confirmed"', () => {
let refetchSpy
beforeEach(async () => {
jest.clearAllMocks()
refetchSpy = jest.spyOn(wrapper.vm.$apollo.queries.ListAllContributions, 'refetch')
await wrapper.find('a[data-test="confirmed"]').trigger('click')
})
it('has statusFilter set to ["CONFIRMED"]', () => {
expect(
wrapper.vm.$apollo.queries.ListAllContributions.observer.options.variables,
).toMatchObject({ statusFilter: ['CONFIRMED'] })
})
it('refetches contributions', () => {
expect(refetchSpy).toBeCalled()
})
describe('click tab "open"', () => {
beforeEach(async () => {
jest.clearAllMocks()
refetchSpy = jest.spyOn(wrapper.vm.$apollo.queries.ListAllContributions, 'refetch')
await wrapper.find('a[data-test="open"]').trigger('click')
})
it('has statusFilter set to ["IN_PROGRESS", "PENDING"]', () => {
expect(
wrapper.vm.$apollo.queries.ListAllContributions.observer.options.variables,
).toMatchObject({ statusFilter: ['IN_PROGRESS', 'PENDING'] })
})
it('refetches contributions', () => {
expect(refetchSpy).toBeCalled()
})
})
describe('click tab "denied"', () => {
beforeEach(async () => {
jest.clearAllMocks()
refetchSpy = jest.spyOn(wrapper.vm.$apollo.queries.ListAllContributions, 'refetch')
await wrapper.find('a[data-test="denied"]').trigger('click')
})
it('has statusFilter set to ["DENIED"]', () => {
expect(
wrapper.vm.$apollo.queries.ListAllContributions.observer.options.variables,
).toMatchObject({ statusFilter: ['DENIED'] })
})
it('refetches contributions', () => {
expect(refetchSpy).toBeCalled()
})
})
describe('click tab "all"', () => {
beforeEach(async () => {
jest.clearAllMocks()
refetchSpy = jest.spyOn(wrapper.vm.$apollo.queries.ListAllContributions, 'refetch')
await wrapper.find('a[data-test="all"]').trigger('click')
})
it('has statusFilter set to ["IN_PROGRESS", "PENDING", "CONFIRMED", "DENIED", "DELETED"]', () => {
expect(
wrapper.vm.$apollo.queries.ListAllContributions.observer.options.variables,
).toMatchObject({
statusFilter: ['IN_PROGRESS', 'PENDING', 'CONFIRMED', 'DENIED', 'DELETED'],
})
})
it('refetches contributions', () => {
expect(refetchSpy).toBeCalled()
})
})
})
})
describe('update status', () => {
beforeEach(async () => {
await wrapper.findComponent({ name: 'OpenCreationsTable' }).vm.$emit('update-state', 2)
})
it.skip('updates the status', () => {
expect(wrapper.vm.items.find((obj) => obj.id === 2).messagesCount).toBe(1)
expect(wrapper.vm.items.find((obj) => obj.id === 2).state).toBe('IN_PROGRESS')
})
})
}) })
}) })

View File

@ -1,6 +1,50 @@
<!-- eslint-disable @intlify/vue-i18n/no-dynamic-keys --> <!-- eslint-disable @intlify/vue-i18n/no-dynamic-keys -->
<template> <template>
<div class="creation-confirm"> <div class="creation-confirm">
<div>
<b-tabs v-model="tabIndex" content-class="mt-3" fill>
<b-tab active :title-link-attributes="{ 'data-test': 'open' }">
<template #title>
{{ $t('contributions.open') }}
<b-badge v-if="$store.state.openCreations > 0" variant="danger">
{{ $store.state.openCreations }}
</b-badge>
</template>
</b-tab>
<b-tab
:title="$t('contributions.confirms')"
:title-link-attributes="{ 'data-test': 'confirmed' }"
/>
<b-tab
:title="$t('contributions.denied')"
:title-link-attributes="{ 'data-test': 'denied' }"
/>
<b-tab
:title="$t('contributions.deleted')"
:title-link-attributes="{ 'data-test': 'deleted' }"
/>
<b-tab :title="$t('contributions.all')" :title-link-attributes="{ 'data-test': 'all' }" />
</b-tabs>
</div>
<open-creations-table
class="mt-4"
:items="items"
:fields="fields"
@show-overlay="showOverlay"
@update-state="updateStatus"
@update-contributions="$apollo.queries.AllContributions.refetch()"
/>
<b-pagination
pills
size="lg"
v-model="currentPage"
:per-page="pageSize"
:total-rows="rows"
align="center"
:hide-ellipsis="true"
></b-pagination>
<div v-if="overlay" id="overlay" @dblclick="overlay = false"> <div v-if="overlay" id="overlay" @dblclick="overlay = false">
<overlay :item="item" @overlay-cancel="overlay = false"> <overlay :item="item" @overlay-cancel="overlay = false">
<template #title> <template #title>
@ -24,24 +68,24 @@
</template> </template>
</overlay> </overlay>
</div> </div>
<open-creations-table
class="mt-4"
:items="pendingCreations"
:fields="fields"
@show-overlay="showOverlay"
@update-state="updateState"
@update-contributions="$apollo.queries.PendingContributions.refetch()"
/>
</div> </div>
</template> </template>
<script> <script>
import Overlay from '../components/Overlay.vue' import Overlay from '../components/Overlay.vue'
import OpenCreationsTable from '../components/Tables/OpenCreationsTable.vue' import OpenCreationsTable from '../components/Tables/OpenCreationsTable.vue'
import { listUnconfirmedContributions } from '../graphql/listUnconfirmedContributions' import { listAllContributions } from '../graphql/listAllContributions'
import { adminDeleteContribution } from '../graphql/adminDeleteContribution' import { adminDeleteContribution } from '../graphql/adminDeleteContribution'
import { confirmContribution } from '../graphql/confirmContribution' import { confirmContribution } from '../graphql/confirmContribution'
import { denyContribution } from '../graphql/denyContribution' import { denyContribution } from '../graphql/denyContribution'
const FILTER_TAB_MAP = [
['IN_PROGRESS', 'PENDING'],
['CONFIRMED'],
['DENIED'],
['DELETED'],
['IN_PROGRESS', 'PENDING', 'CONFIRMED', 'DENIED', 'DELETED'],
]
export default { export default {
name: 'CreationConfirm', name: 'CreationConfirm',
components: { components: {
@ -50,10 +94,14 @@ export default {
}, },
data() { data() {
return { return {
pendingCreations: [], tabIndex: 0,
items: [],
overlay: false, overlay: false,
item: {}, item: {},
variant: 'confirm', variant: 'confirm',
rows: 0,
currentPage: 1,
pageSize: 25,
} }
}, },
methods: { methods: {
@ -112,7 +160,7 @@ export default {
}) })
}, },
updatePendingCreations(id) { updatePendingCreations(id) {
this.pendingCreations = this.pendingCreations.filter((obj) => obj.id !== id) this.items = this.items.filter((obj) => obj.id !== id)
this.$store.commit('openCreationsMinus', 1) this.$store.commit('openCreationsMinus', 1)
}, },
showOverlay(item, variant) { showOverlay(item, variant) {
@ -120,38 +168,155 @@ export default {
this.item = item this.item = item
this.variant = variant this.variant = variant
}, },
updateState(id) { updateStatus(id) {
this.pendingCreations.find((obj) => obj.id === id).messagesCount++ this.items.find((obj) => obj.id === id).messagesCount++
this.pendingCreations.find((obj) => obj.id === id).state = 'IN_PROGRESS' this.items.find((obj) => obj.id === id).state = 'IN_PROGRESS'
},
},
watch: {
statusFilter() {
this.$apollo.queries.ListAllContributions.refetch()
}, },
}, },
computed: { computed: {
fields() { fields() {
return [ return [
{ key: 'bookmark', label: this.$t('delete') }, [
{ key: 'deny', label: this.$t('deny') }, { key: 'bookmark', label: this.$t('delete') },
{ key: 'email', label: this.$t('e_mail') }, { key: 'deny', label: this.$t('deny') },
{ key: 'firstName', label: this.$t('firstname') }, { key: 'email', label: this.$t('e_mail') },
{ key: 'lastName', label: this.$t('lastname') }, { key: 'firstName', label: this.$t('firstname') },
{ { key: 'lastName', label: this.$t('lastname') },
key: 'amount', {
label: this.$t('creation'), key: 'amount',
formatter: (value) => { label: this.$t('creation'),
return value + ' GDD' formatter: (value) => {
return value + ' GDD'
},
}, },
}, { key: 'memo', label: this.$t('text'), class: 'text-break' },
{ key: 'memo', label: this.$t('text'), class: 'text-break' }, {
{ key: 'contributionDate',
key: 'date', label: this.$t('created'),
label: this.$t('date'), formatter: (value) => {
formatter: (value) => { return this.$d(new Date(value), 'short')
return this.$d(new Date(value), 'short') },
}, },
}, { key: 'moderator', label: this.$t('moderator') },
{ key: 'moderator', label: this.$t('moderator') }, { key: 'editCreation', label: this.$t('edit') },
{ key: 'editCreation', label: this.$t('edit') }, { key: 'confirm', label: this.$t('save') },
{ key: 'confirm', label: this.$t('save') }, ],
] [
{ key: 'firstName', label: this.$t('firstname') },
{ key: 'lastName', label: this.$t('lastname') },
{
key: 'amount',
label: this.$t('creation'),
formatter: (value) => {
return value + ' GDD'
},
},
{ key: 'memo', label: this.$t('text'), class: 'text-break' },
{
key: 'contributionDate',
label: this.$t('created'),
formatter: (value) => {
return this.$d(new Date(value), 'short')
},
},
{
key: 'createdAt',
label: this.$t('createdAt'),
formatter: (value) => {
return this.$d(new Date(value), 'short')
},
},
{
key: 'confirmedAt',
label: this.$t('contributions.confirms'),
formatter: (value) => {
return this.$d(new Date(value), 'short')
},
},
{ key: 'chatCreation', label: this.$t('chat') },
],
[
{ key: 'reActive', label: 'reActive' },
{ key: 'firstName', label: this.$t('firstname') },
{ key: 'lastName', label: this.$t('lastname') },
{
key: 'amount',
label: this.$t('creation'),
formatter: (value) => {
return value + ' GDD'
},
},
{ key: 'memo', label: this.$t('text'), class: 'text-break' },
{
key: 'contributionDate',
label: this.$t('created'),
formatter: (value) => {
return this.$d(new Date(value), 'short')
},
},
{
key: 'createdAt',
label: this.$t('createdAt'),
formatter: (value) => {
return this.$d(new Date(value), 'short')
},
},
{
key: 'deniedAt',
label: this.$t('contributions.denied'),
formatter: (value) => {
return this.$d(new Date(value), 'short')
},
},
{ key: 'deniedBy', label: this.$t('mod') },
{ key: 'chatCreation', label: this.$t('chat') },
],
[],
[
{ key: 'state', label: 'state' },
{ key: 'firstName', label: this.$t('firstname') },
{ key: 'lastName', label: this.$t('lastname') },
{
key: 'amount',
label: this.$t('creation'),
formatter: (value) => {
return value + ' GDD'
},
},
{ key: 'memo', label: this.$t('text'), class: 'text-break' },
{
key: 'contributionDate',
label: this.$t('created'),
formatter: (value) => {
return this.$d(new Date(value), 'short')
},
},
{
key: 'createdAt',
label: this.$t('createdAt'),
formatter: (value) => {
return this.$d(new Date(value), 'short')
},
},
{
key: 'confirmedAt',
label: this.$t('contributions.confirms'),
formatter: (value) => {
return this.$d(new Date(value), 'short')
},
},
{ key: 'confirmedBy', label: this.$t('mod') },
{ key: 'chatCreation', label: this.$t('chat') },
],
][this.tabIndex]
},
statusFilter() {
return FILTER_TAB_MAP[this.tabIndex]
}, },
overlayTitle() { overlayTitle() {
return `overlay.${this.variant}.title` return `overlay.${this.variant}.title`
@ -182,18 +347,21 @@ export default {
}, },
}, },
apollo: { apollo: {
PendingContributions: { ListAllContributions: {
query() { query() {
return listUnconfirmedContributions return listAllContributions
}, },
variables() { variables() {
// may be at some point we need a pagination here // may be at some point we need a pagination here
return {} return {
currentPage: this.currentPage,
pageSize: this.pageSize,
statusFilter: this.statusFilter,
}
}, },
update({ listUnconfirmedContributions }) { update({ listAllContributions }) {
this.$store.commit('resetOpenCreations') this.rows = listAllContributions.contributionCount
this.pendingCreations = listUnconfirmedContributions this.items = listAllContributions.contributionList
this.$store.commit('setOpenCreations', listUnconfirmedContributions.length)
}, },
error({ message }) { error({ message }) {
this.toastError(message) this.toastError(message)

View File

@ -1,41 +1,18 @@
import { mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import Overview from './Overview.vue' import Overview from './Overview.vue'
import { listUnconfirmedContributions } from '@/graphql/listUnconfirmedContributions.js' import { listAllContributions } from '../graphql/listAllContributions'
import VueApollo from 'vue-apollo'
import { createMockClient } from 'mock-apollo-client'
import { toastErrorSpy } from '../../test/testSetup'
const mockClient = createMockClient()
const apolloProvider = new VueApollo({
defaultClient: mockClient,
})
const localVue = global.localVue const localVue = global.localVue
const apolloQueryMock = jest localVue.use(VueApollo)
.fn()
.mockResolvedValueOnce({
data: {
listUnconfirmedContributions: [
{
pending: true,
},
{
pending: true,
},
{
pending: true,
},
],
},
})
.mockResolvedValue({
data: {
listUnconfirmedContributions: [
{
pending: true,
},
{
pending: true,
},
{
pending: true,
},
],
},
})
const storeCommitMock = jest.fn() const storeCommitMock = jest.fn()
@ -43,44 +20,114 @@ const mocks = {
$t: jest.fn((t) => t), $t: jest.fn((t) => t),
$n: jest.fn((n) => n), $n: jest.fn((n) => n),
$d: jest.fn((d) => d), $d: jest.fn((d) => d),
$apollo: {
query: apolloQueryMock,
},
$store: { $store: {
commit: storeCommitMock, commit: storeCommitMock,
state: { state: {
openCreations: 2, openCreations: 1,
}, },
}, },
} }
const defaultData = () => {
return {
listAllContributions: {
contributionCount: 2,
contributionList: [
{
id: 1,
firstName: 'Bibi',
lastName: 'Bloxberg',
userId: 99,
email: 'bibi@bloxberg.de',
amount: 500,
memo: 'Danke für alles',
date: new Date(),
moderator: 1,
state: 'PENDING',
creation: [500, 500, 500],
messagesCount: 0,
deniedBy: null,
deniedAt: null,
confirmedBy: null,
confirmedAt: null,
contributionDate: new Date(),
deletedBy: null,
deletedAt: null,
createdAt: new Date(),
},
{
id: 2,
firstName: 'Räuber',
lastName: 'Hotzenplotz',
userId: 100,
email: 'raeuber@hotzenplotz.de',
amount: 1000000,
memo: 'Gut Ergattert',
date: new Date(),
moderator: 1,
state: 'PENDING',
creation: [500, 500, 500],
messagesCount: 0,
deniedBy: null,
deniedAt: null,
confirmedBy: null,
confirmedAt: null,
contributionDate: new Date(),
deletedBy: null,
deletedAt: null,
createdAt: new Date(),
},
],
},
}
}
describe('Overview', () => { describe('Overview', () => {
let wrapper let wrapper
const listAllContributionsMock = jest.fn()
mockClient.setRequestHandler(
listAllContributions,
listAllContributionsMock
.mockRejectedValueOnce({ message: 'Ouch!' })
.mockResolvedValue({ data: defaultData() }),
)
const Wrapper = () => { const Wrapper = () => {
return mount(Overview, { localVue, mocks }) return mount(Overview, { localVue, mocks, apolloProvider })
} }
describe('mount', () => { describe('mount', () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks()
wrapper = Wrapper() wrapper = Wrapper()
}) })
it('calls listUnconfirmedContributions', () => { describe('server response for get pending creations is error', () => {
expect(apolloQueryMock).toBeCalledWith( it('toast an error message', () => {
expect.objectContaining({ expect(toastErrorSpy).toBeCalledWith('Ouch!')
query: listUnconfirmedContributions, })
}), })
)
it('calls the listAllContributions query', () => {
expect(listAllContributionsMock).toBeCalledWith({
currentPage: 1,
order: 'DESC',
pageSize: 25,
statusFilter: ['IN_PROGRESS', 'PENDING'],
})
}) })
it('commits three pending creations to store', () => { it('commits three pending creations to store', () => {
expect(storeCommitMock).toBeCalledWith('setOpenCreations', 3) expect(storeCommitMock).toBeCalledWith('setOpenCreations', 2)
}) })
describe('with open creations', () => { describe('with open creations', () => {
it('renders a link to confirm creations', () => { beforeEach(() => {
expect(wrapper.find('a[href="creation-confirm"]').text()).toContain('2') mocks.$store.state.openCreations = 2
})
it('renders a link to confirm 2 creations', () => {
expect(wrapper.find('[data-test="open-creation"]').text()).toContain('2')
expect(wrapper.find('a[href="creation-confirm"]').exists()).toBeTruthy() expect(wrapper.find('a[href="creation-confirm"]').exists()).toBeTruthy()
}) })
}) })
@ -91,7 +138,7 @@ describe('Overview', () => {
}) })
it('renders a link to confirm creations', () => { it('renders a link to confirm creations', () => {
expect(wrapper.find('a[href="creation-confirm"]').text()).toContain('0') expect(wrapper.find('[data-test="open-creation"]').text()).toContain('0')
expect(wrapper.find('a[href="creation-confirm"]').exists()).toBeTruthy() expect(wrapper.find('a[href="creation-confirm"]').exists()).toBeTruthy()
}) })
}) })

View File

@ -24,31 +24,40 @@
> >
<b-card-text> <b-card-text>
<b-link to="creation-confirm"> <b-link to="creation-confirm">
<h1>{{ $store.state.openCreations }}</h1> <h1 data-test="open-creation">{{ $store.state.openCreations }}</h1>
</b-link> </b-link>
</b-card-text> </b-card-text>
</b-card> </b-card>
</div> </div>
</template> </template>
<script> <script>
import { listUnconfirmedContributions } from '@/graphql/listUnconfirmedContributions.js' import { listAllContributions } from '../graphql/listAllContributions'
export default { export default {
name: 'overview', name: 'overview',
methods: { data() {
getPendingCreations() { return {
this.$apollo statusFilter: ['IN_PROGRESS', 'PENDING'],
.query({ }
query: listUnconfirmedContributions,
fetchPolicy: 'network-only',
})
.then((result) => {
this.$store.commit('setOpenCreations', result.data.listUnconfirmedContributions.length)
})
},
}, },
created() { apollo: {
this.getPendingCreations() AllContributions: {
query() {
return listAllContributions
},
variables() {
// may be at some point we need a pagination here
return {
statusFilter: this.statusFilter,
}
},
update({ listAllContributions }) {
this.$store.commit('setOpenCreations', listAllContributions.contributionCount)
},
error({ message }) {
this.toastError(message)
},
},
}, },
} }
</script> </script>

View File

@ -181,6 +181,7 @@ export class ContributionResolver {
.select('c') .select('c')
.from(DbContribution, 'c') .from(DbContribution, 'c')
.innerJoinAndSelect('c.user', 'u') .innerJoinAndSelect('c.user', 'u')
.leftJoinAndSelect('c.messages', 'm')
.where(where) .where(where)
.orderBy('c.createdAt', order) .orderBy('c.createdAt', order)
.limit(pageSize) .limit(pageSize)