Merge branch 'master' into e2e-test-setup

This commit is contained in:
mahula 2022-09-05 14:21:32 +02:00 committed by GitHub
commit aeff632d16
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
65 changed files with 2201 additions and 389 deletions

View File

@ -0,0 +1,111 @@
import { mount } from '@vue/test-utils'
import ContributionMessagesFormular from './ContributionMessagesFormular.vue'
import { toastErrorSpy, toastSuccessSpy } from '../../../test/testSetup'
const localVue = global.localVue
const apolloMutateMock = jest.fn().mockResolvedValue()
describe('ContributionMessagesFormular', () => {
let wrapper
const propsData = {
contributionId: 42,
}
const mocks = {
$t: jest.fn((t) => t),
$apollo: {
mutate: apolloMutateMock,
},
$i18n: {
locale: 'en',
},
}
const Wrapper = () => {
return mount(ContributionMessagesFormular, {
localVue,
mocks,
propsData,
})
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('has a DIV .contribution-messages-formular', () => {
expect(wrapper.find('div.contribution-messages-formular').exists()).toBe(true)
})
describe('on trigger reset', () => {
beforeEach(async () => {
wrapper.setData({
form: {
text: 'text form message',
},
})
await wrapper.find('form').trigger('reset')
})
it('form has empty text', () => {
expect(wrapper.vm.form).toEqual({
text: '',
})
})
})
describe('on trigger submit', () => {
beforeEach(async () => {
wrapper.setData({
form: {
text: 'text form message',
},
})
await wrapper.find('form').trigger('submit')
})
it('emitted "get-list-contribution-messages" with data', async () => {
expect(wrapper.emitted('get-list-contribution-messages')).toEqual(
expect.arrayContaining([expect.arrayContaining([42])]),
)
})
it('emitted "update-state" with data', async () => {
expect(wrapper.emitted('update-state')).toEqual(
expect.arrayContaining([expect.arrayContaining([42])]),
)
})
})
describe('send contribution message with error', () => {
beforeEach(async () => {
apolloMutateMock.mockRejectedValue({ message: 'OUCH!' })
wrapper = Wrapper()
await wrapper.find('form').trigger('submit')
})
it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith('OUCH!')
})
})
describe('send contribution message with success', () => {
beforeEach(async () => {
wrapper.setData({
form: {
text: 'text form message',
},
})
wrapper = Wrapper()
await wrapper.find('form').trigger('submit')
})
it('toasts an success message', () => {
expect(toastSuccessSpy).toBeCalledWith('message.request')
})
})
})
})

View File

@ -0,0 +1,67 @@
<template>
<div class="contribution-messages-formular">
<div>
<b-form @submit.prevent="onSubmit" @reset.prevent="onReset">
<b-form-textarea
id="textarea"
v-model="form.text"
:placeholder="$t('contributionLink.memo')"
rows="3"
max-rows="6"
></b-form-textarea>
<b-row class="mt-4 mb-6">
<b-col>
<b-button type="reset" variant="danger">{{ $t('form.cancel') }}</b-button>
</b-col>
<b-col class="text-right">
<b-button type="submit" variant="primary">{{ $t('form.submit') }}</b-button>
</b-col>
</b-row>
</b-form>
</div>
</div>
</template>
<script>
import { adminCreateContributionMessage } from '@/graphql/adminCreateContributionMessage'
export default {
name: 'ContributionMessagesFormular',
props: {
contributionId: {
type: Number,
required: true,
},
},
data() {
return {
form: {
text: '',
},
}
},
methods: {
onSubmit(event) {
this.$apollo
.mutate({
mutation: adminCreateContributionMessage,
variables: {
contributionId: this.contributionId,
message: this.form.text,
},
})
.then((result) => {
this.$emit('get-list-contribution-messages', this.contributionId)
this.$emit('update-state', this.contributionId)
this.form.text = ''
this.toastSuccess(this.$t('message.request'))
})
.catch((error) => {
this.toastError(error.message)
})
},
onReset(event) {
this.form.text = ''
},
},
}
</script>

View File

@ -0,0 +1,56 @@
import { mount } from '@vue/test-utils'
import ContributionMessagesList from './ContributionMessagesList.vue'
const localVue = global.localVue
const apolloQueryMock = jest.fn().mockResolvedValue()
describe('ContributionMessagesList', () => {
let wrapper
const propsData = {
contributionId: 42,
}
const mocks = {
$t: jest.fn((t) => t),
$i18n: {
locale: 'en',
},
$apollo: {
query: apolloQueryMock,
},
}
const Wrapper = () => {
return mount(ContributionMessagesList, {
localVue,
mocks,
propsData,
})
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('sends query to Apollo when created', () => {
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
contributionId: propsData.contributionId,
},
}),
)
})
it('has a DIV .contribution-messages-list', () => {
expect(wrapper.find('div.contribution-messages-list').exists()).toBe(true)
})
it('has a Component ContributionMessagesFormular', () => {
expect(wrapper.findComponent({ name: 'ContributionMessagesFormular' }).exists()).toBe(true)
})
})
})

View File

@ -0,0 +1,69 @@
<template>
<div class="contribution-messages-list">
<b-container>
{{ messages.lenght }}
<div v-for="message in messages" v-bind:key="message.id">
<contribution-messages-list-item :message="message" />
</div>
</b-container>
<contribution-messages-formular
:contributionId="contributionId"
@get-list-contribution-messages="getListContributionMessages"
@update-state="updateState"
/>
</div>
</template>
<script>
import ContributionMessagesListItem from './slots/ContributionMessagesListItem.vue'
import ContributionMessagesFormular from '../ContributionMessages/ContributionMessagesFormular.vue'
import { listContributionMessages } from '../../graphql/listContributionMessages.js'
export default {
name: 'ContributionMessagesList',
components: {
ContributionMessagesListItem,
ContributionMessagesFormular,
},
props: {
contributionId: {
type: Number,
required: true,
},
},
data() {
return {
messages: [],
}
},
methods: {
getListContributionMessages(id) {
this.$apollo
.query({
query: listContributionMessages,
variables: {
contributionId: id,
},
fetchPolicy: 'no-cache',
})
.then((result) => {
this.messages = result.data.listContributionMessages.messages
})
.catch((error) => {
this.toastError(error.message)
})
},
updateState(id) {
this.$emit('update-state', id)
},
},
created() {
this.getListContributionMessages(this.contributionId)
},
}
</script>
<style scoped>
.temp-message {
margin-top: 50px;
}
</style>

View File

@ -0,0 +1,58 @@
import { mount } from '@vue/test-utils'
import ContributionMessagesListItem from './ContributionMessagesListItem.vue'
const localVue = global.localVue
describe('ContributionMessagesListItem', () => {
let wrapper
const mocks = {
$t: jest.fn((t) => t),
$d: jest.fn((d) => d),
$store: {
state: {
moderator: {
id: 107,
},
},
},
}
const propsData = {
contributionId: 42,
state: 'PENDING0',
message: {
id: 111,
message: 'asd asda sda sda',
createdAt: '2022-08-29T12:23:27.000Z',
updatedAt: null,
type: 'DIALOG',
userFirstName: 'Peter',
userLastName: 'Lustig',
userId: 107,
__typename: 'ContributionMessage',
},
}
const Wrapper = () => {
return mount(ContributionMessagesListItem, {
localVue,
mocks,
propsData,
})
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('has a DIV .contribution-messages-list-item', () => {
expect(wrapper.find('div.contribution-messages-list-item').exists()).toBe(true)
})
it('props.message.default', () => {
expect(wrapper.vm.$options.props.message.default.call()).toEqual({})
})
})
})

View File

@ -0,0 +1,32 @@
<template>
<div class="contribution-messages-list-item">
<is-moderator v-if="isModerator" :message="message"></is-moderator>
<is-not-moderator v-else :message="message"></is-not-moderator>
</div>
</template>
<script>
import IsModerator from '@/components/ContributionMessages/slots/IsModerator.vue'
import IsNotModerator from '@/components/ContributionMessages/slots/IsNotModerator.vue'
export default {
name: 'ContributionMessagesListItem',
components: {
IsModerator,
IsNotModerator,
},
props: {
message: {
type: Object,
required: true,
default() {
return {}
},
},
},
computed: {
isModerator() {
return this.$store.state.moderator.id === this.message.userId
},
},
}
</script>

View File

@ -0,0 +1,49 @@
import { mount } from '@vue/test-utils'
import IsModerator from './IsModerator.vue'
const localVue = global.localVue
describe('IsModerator', () => {
let wrapper
const mocks = {
$t: jest.fn((t) => t),
$d: jest.fn((d) => d),
}
const propsData = {
message: {
id: 111,
message: 'asd asda sda sda',
createdAt: '2022-08-29T12:23:27.000Z',
updatedAt: null,
type: 'DIALOG',
userFirstName: 'Peter',
userLastName: 'Lustig',
userId: 107,
__typename: 'ContributionMessage',
},
}
const Wrapper = () => {
return mount(IsModerator, {
localVue,
mocks,
propsData,
})
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('has a DIV .slot-is-moderator', () => {
expect(wrapper.find('div.slot-is-moderator').exists()).toBe(true)
})
it('props.message.default', () => {
expect(wrapper.vm.$options.props.message.default.call()).toEqual({})
})
})
})

View File

@ -0,0 +1,37 @@
<template>
<div class="slot-is-moderator">
<div class="text-right">
<b-avatar square :text="initialLetters" variant="warning"></b-avatar>
<span class="ml-2 mr-2">{{ message.userFirstName }} {{ message.userLastName }}</span>
<span class="ml-2">{{ $d(new Date(message.createdAt), 'short') }}</span>
<small class="ml-4 text-success">{{ $t('moderator') }}</small>
<div class="mt-2 text-bold h4">{{ message.message }}</div>
</div>
</div>
</template>
<script>
export default {
props: {
message: {
type: Object,
default() {
return {}
},
},
},
computed: {
initialLetters() {
return `${this.message.userFirstName[0]} ${this.message.userLastName[0]}`
},
},
}
</script>
<style>
.slot-is-moderator {
clear: both;
float: right;
width: 75%;
margin-top: 20px;
margin-bottom: 20px;
}
</style>

View File

@ -0,0 +1,49 @@
import { mount } from '@vue/test-utils'
import IsNotModerator from './IsNotModerator.vue'
const localVue = global.localVue
describe('IsNotModerator', () => {
let wrapper
const mocks = {
$t: jest.fn((t) => t),
$d: jest.fn((d) => d),
}
const propsData = {
message: {
id: 113,
message: 'asda sdad ad asdasd ',
createdAt: '2022-08-29T12:25:34.000Z',
updatedAt: null,
type: 'DIALOG',
userFirstName: 'Bibi',
userLastName: 'Bloxberg',
userId: 108,
__typename: 'ContributionMessage',
},
}
const Wrapper = () => {
return mount(IsNotModerator, {
localVue,
mocks,
propsData,
})
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('has a DIV .slot-is-not-moderator', () => {
expect(wrapper.find('div.slot-is-not-moderator').exists()).toBe(true)
})
it('props.message.default', () => {
expect(wrapper.vm.$options.props.message.default.call()).toEqual({})
})
})
})

View File

@ -0,0 +1,36 @@
<template>
<div class="slot-is-not-moderator">
<div>
<b-avatar :text="initialLetters" variant="info"></b-avatar>
<span class="ml-2 mr-2 text-bold">
{{ message.userFirstName }} {{ message.userLastName }}
</span>
<span class="ml-2">{{ $d(new Date(message.createdAt), 'short') }}</span>
<div class="mt-2 text-bold h4">{{ message.message }}</div>
</div>
</div>
</template>
<script>
export default {
props: {
message: {
type: Object,
default() {
return {}
},
},
},
computed: {
initialLetters() {
return `${this.message.userFirstName[0]} ${this.message.userLastName[0]}`
},
},
}
</script>
<style>
.slot-is-not-moderator {
clear: both;
width: 75%;
margin-top: 20px;
}
</style>

View File

@ -21,6 +21,19 @@
>
<b-icon :icon="row.detailsShowing ? 'x' : 'pencil-square'" aria-label="Help"></b-icon>
</b-button>
<b-button v-else @click="rowToggleDetails(row, 0)">
<b-icon icon="chat-dots"></b-icon>
<b-icon
v-if="row.item.state === 'PENDING' && row.item.messageCount > 0"
icon="exclamation-circle-fill"
variant="warning"
></b-icon>
<b-icon
v-if="row.item.state === 'IN_PROGRESS' && row.item.messageCount > 0"
icon="question-diamond"
variant="light"
></b-icon>
</b-button>
</template>
<template #cell(confirm)="row">
<b-button variant="success" size="md" @click="$emit('show-overlay', row.item)" class="mr-2">
@ -33,10 +46,10 @@
type="show-creation"
slotName="show-creation"
:index="0"
@row-toggle-details="rowToggleDetails"
@row-toggle-details="rowToggleDetails(row, 0)"
>
<template #show-creation>
<div>
<div v-if="row.item.moderator">
<edit-creation-formular
type="singleCreation"
:creation="row.item.creation"
@ -44,6 +57,12 @@
:row="row"
:creationUserData="creationUserData"
@update-creation-data="updateCreationData"
/>
</div>
<div v-else>
<contribution-messages-list
:contributionId="row.item.id"
@update-state="updateState"
@update-user-data="updateUserData"
/>
</div>
@ -58,6 +77,7 @@
import { toggleRowDetails } from '../../mixins/toggleRowDetails'
import RowDetails from '../RowDetails.vue'
import EditCreationFormular from '../EditCreationFormular.vue'
import ContributionMessagesList from '../ContributionMessages/ContributionMessagesList.vue'
export default {
name: 'OpenCreationsTable',
@ -65,6 +85,7 @@ export default {
components: {
EditCreationFormular,
RowDetails,
ContributionMessagesList,
},
props: {
items: {
@ -98,6 +119,9 @@ export default {
updateUserData(rowItem, newCreation) {
rowItem.creation = newCreation
},
updateState(id) {
this.$emit('update-state', id)
},
},
}
</script>

View File

@ -0,0 +1,15 @@
import gql from 'graphql-tag'
export const adminCreateContributionMessage = gql`
mutation ($contributionId: Float!, $message: String!) {
adminCreateContributionMessage(contributionId: $contributionId, message: $message) {
id
message
createdAt
updatedAt
type
userFirstName
userLastName
}
}
`

View File

@ -0,0 +1,24 @@
import gql from 'graphql-tag'
export const listContributionMessages = gql`
query ($contributionId: Float!, $pageSize: Int = 25, $currentPage: Int = 1, $order: Order = ASC) {
listContributionMessages(
contributionId: $contributionId
pageSize: $pageSize
currentPage: $currentPage
order: $order
) {
count
messages {
id
message
createdAt
updatedAt
type
userFirstName
userLastName
userId
}
}
}
`

View File

@ -12,6 +12,8 @@ export const listUnconfirmedContributions = gql`
date
moderator
creation
state
messageCount
}
}
`

View File

@ -69,6 +69,10 @@
},
"short_hash": "({shortHash})"
},
"form": {
"cancel": "Abbrechen",
"submit": "Senden"
},
"GDD": "GDD",
"hide_details": "Details verbergen",
"lastname": "Nachname",
@ -78,6 +82,9 @@
"pipe": "|",
"plus": "+"
},
"message": {
"request": "Die Anfrage wurde gesendet."
},
"moderator": "Moderator",
"multiple_creation_text": "Bitte wähle ein oder mehrere Mitglieder aus für die du Schöpfen möchtest.",
"name": "Name",

View File

@ -69,6 +69,10 @@
},
"short_hash": "({shortHash})"
},
"form": {
"cancel": "Cancel",
"submit": "Send"
},
"GDD": "GDD",
"hide_details": "Hide details",
"lastname": "Lastname",
@ -78,6 +82,9 @@
"pipe": "|",
"plus": "+"
},
"message": {
"request": "Request has been sent."
},
"moderator": "Moderator",
"multiple_creation_text": "Please select one or more members for which you would like to perform creations.",
"name": "Name",

View File

@ -9,6 +9,7 @@
:fields="fields"
@remove-creation="removeCreation"
@show-overlay="showOverlay"
@update-state="updateState"
/>
</div>
</template>
@ -93,6 +94,10 @@ export default {
this.overlay = true
this.item = item
},
updateState(id) {
this.pendingCreations.find((obj) => obj.id === id).messagesCount++
this.pendingCreations.find((obj) => obj.id === id).state = 'IN_PROGRESS'
},
},
computed: {
fields() {

View File

@ -89,7 +89,6 @@ export default {
this.$apollo
.query({
query: communityStatistics,
fetchPolicy: 'network-only',
})
.then((result) => {
this.statistics.totalUsers = result.data.communityStatistics.totalUsers

View File

@ -33,6 +33,8 @@ export enum RIGHTS {
LIST_CONTRIBUTION_LINKS = 'LIST_CONTRIBUTION_LINKS',
COMMUNITY_STATISTICS = 'COMMUNITY_STATISTICS',
SEARCH_ADMIN_USERS = 'SEARCH_ADMIN_USERS',
CREATE_CONTRIBUTION_MESSAGE = 'CREATE_CONTRIBUTION_MESSAGE',
LIST_ALL_CONTRIBUTION_MESSAGES = 'LIST_ALL_CONTRIBUTION_MESSAGES',
// Admin
SEARCH_USERS = 'SEARCH_USERS',
SET_USER_ROLE = 'SET_USER_ROLE',
@ -50,4 +52,5 @@ export enum RIGHTS {
CREATE_CONTRIBUTION_LINK = 'CREATE_CONTRIBUTION_LINK',
DELETE_CONTRIBUTION_LINK = 'DELETE_CONTRIBUTION_LINK',
UPDATE_CONTRIBUTION_LINK = 'UPDATE_CONTRIBUTION_LINK',
ADMIN_CREATE_CONTRIBUTION_MESSAGE = 'ADMIN_CREATE_CONTRIBUTION_MESSAGE',
}

View File

@ -31,6 +31,8 @@ export const ROLE_USER = new Role('user', [
RIGHTS.SEARCH_ADMIN_USERS,
RIGHTS.LIST_CONTRIBUTION_LINKS,
RIGHTS.COMMUNITY_STATISTICS,
RIGHTS.CREATE_CONTRIBUTION_MESSAGE,
RIGHTS.LIST_ALL_CONTRIBUTION_MESSAGES,
])
export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights

View File

@ -0,0 +1,11 @@
import { ArgsType, Field, InputType } from 'type-graphql'
@InputType()
@ArgsType()
export default class ContributionMessageArgs {
@Field(() => Number)
contributionId: number
@Field(() => String)
message: string
}

View File

@ -1,7 +1,7 @@
import { ObjectType, Field, Int } from 'type-graphql'
import Decimal from 'decimal.js-light'
import { Contribution as dbContribution } from '@entity/Contribution'
import { User } from './User'
import { User } from '@entity/User'
@ObjectType()
export class Contribution {
@ -16,6 +16,8 @@ export class Contribution {
this.confirmedAt = contribution.confirmedAt
this.confirmedBy = contribution.confirmedBy
this.contributionDate = contribution.contributionDate
this.state = contribution.contributionStatus
this.messagesCount = contribution.messages ? contribution.messages.length : 0
}
@Field(() => Number)
@ -47,6 +49,12 @@ export class Contribution {
@Field(() => Date)
contributionDate: Date
@Field(() => Number)
messagesCount: number
@Field(() => String)
state: string
}
@ObjectType()

View File

@ -0,0 +1,49 @@
import { Field, ObjectType } from 'type-graphql'
import { ContributionMessage as DbContributionMessage } from '@entity/ContributionMessage'
import { User } from '@entity/User'
@ObjectType()
export class ContributionMessage {
constructor(contributionMessage: DbContributionMessage, user: User) {
this.id = contributionMessage.id
this.message = contributionMessage.message
this.createdAt = contributionMessage.createdAt
this.updatedAt = contributionMessage.updatedAt
this.type = contributionMessage.type
this.userFirstName = user.firstName
this.userLastName = user.lastName
this.userId = user.id
}
@Field(() => Number)
id: number
@Field(() => String)
message: string
@Field(() => Date)
createdAt: Date
@Field(() => Date, { nullable: true })
updatedAt?: Date | null
@Field(() => String)
type: string
@Field(() => String, { nullable: true })
userFirstName: string | null
@Field(() => String, { nullable: true })
userLastName: string | null
@Field(() => Number, { nullable: true })
userId: number | null
}
@ObjectType()
export class ContributionMessageListResult {
@Field(() => Number)
count: number
@Field(() => [ContributionMessage])
messages: ContributionMessage[]
}

View File

@ -5,7 +5,7 @@ import { User } from '@entity/User'
@ObjectType()
export class UnconfirmedContribution {
constructor(contribution: Contribution, user: User, creations: Decimal[]) {
constructor(contribution: Contribution, user: User | undefined, creations: Decimal[]) {
this.id = contribution.id
this.userId = contribution.userId
this.amount = contribution.amount
@ -14,7 +14,10 @@ export class UnconfirmedContribution {
this.firstName = user ? user.firstName : ''
this.lastName = user ? user.lastName : ''
this.email = user ? user.email : ''
this.moderator = contribution.moderatorId
this.creation = creations
this.state = contribution.contributionStatus
this.messageCount = contribution.messages ? contribution.messages.length : 0
}
@Field(() => String)
@ -46,4 +49,10 @@ export class UnconfirmedContribution {
@Field(() => [Decimal])
creation: Decimal[]
@Field(() => String)
state: string
@Field(() => Number)
messageCount: number
}

View File

@ -62,6 +62,10 @@ import {
MEMO_MAX_CHARS,
MEMO_MIN_CHARS,
} from './const/const'
import { ContributionMessage as DbContributionMessage } from '@entity/ContributionMessage'
import ContributionMessageArgs from '@arg/ContributionMessageArgs'
import { ContributionMessageType } from '@enum/MessageType'
import { ContributionMessage } from '@model/ContributionMessage'
// const EMAIL_OPT_IN_REGISTER = 1
// const EMAIL_OPT_UNKNOWN = 3 // elopage?
@ -357,7 +361,14 @@ export class AdminResolver {
@Authorized([RIGHTS.LIST_UNCONFIRMED_CONTRIBUTIONS])
@Query(() => [UnconfirmedContribution])
async listUnconfirmedContributions(): Promise<UnconfirmedContribution[]> {
const contributions = await Contribution.find({ where: { confirmedAt: IsNull() } })
const contributions = await getConnection()
.createQueryBuilder()
.select('c')
.from(Contribution, 'c')
.leftJoinAndSelect('c.messages', 'm')
.where({ confirmedAt: IsNull() })
.getMany()
if (contributions.length === 0) {
return []
}
@ -370,18 +381,11 @@ export class AdminResolver {
const user = users.find((u) => u.id === contribution.userId)
const creation = userCreations.find((c) => c.id === contribution.userId)
return {
id: contribution.id,
userId: contribution.userId,
date: contribution.contributionDate,
memo: contribution.memo,
amount: contribution.amount,
moderator: contribution.moderatorId,
firstName: user ? user.firstName : '',
lastName: user ? user.lastName : '',
email: user ? user.email : '',
creation: creation ? creation.creations : FULL_CREATION_AVAILABLE,
}
return new UnconfirmedContribution(
contribution,
user,
creation ? creation.creations : FULL_CREATION_AVAILABLE,
)
})
}
@ -696,4 +700,46 @@ export class AdminResolver {
logger.debug(`updateContributionLink successful!`)
return new ContributionLink(dbContributionLink)
}
@Authorized([RIGHTS.ADMIN_CREATE_CONTRIBUTION_MESSAGE])
@Mutation(() => ContributionMessage)
async adminCreateContributionMessage(
@Args() { contributionId, message }: ContributionMessageArgs,
@Ctx() context: Context,
): Promise<ContributionMessage> {
const user = getUser(context)
const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect()
await queryRunner.startTransaction('READ UNCOMMITTED')
const contributionMessage = DbContributionMessage.create()
try {
const contribution = await Contribution.findOne({ id: contributionId })
if (!contribution) {
throw new Error('Contribution not found')
}
contributionMessage.contributionId = contributionId
contributionMessage.createdAt = new Date()
contributionMessage.message = message
contributionMessage.userId = user.id
contributionMessage.type = ContributionMessageType.DIALOG
await queryRunner.manager.insert(DbContributionMessage, contributionMessage)
if (
contribution.contributionStatus === ContributionStatus.DELETED ||
contribution.contributionStatus === ContributionStatus.DENIED ||
contribution.contributionStatus === ContributionStatus.PENDING
) {
contribution.contributionStatus = ContributionStatus.IN_PROGRESS
await queryRunner.manager.update(Contribution, { id: contributionId }, contribution)
}
await queryRunner.commitTransaction()
} catch (e) {
await queryRunner.rollbackTransaction()
logger.error(`ContributionMessage was not successful: ${e}`)
throw new Error(`ContributionMessage was not successful: ${e}`)
} finally {
await queryRunner.release()
}
return new ContributionMessage(contributionMessage, user)
}
}

View File

@ -0,0 +1,297 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { cleanDB, resetToken, testEnvironment } from '@test/helpers'
import { GraphQLError } from 'graphql'
import {
adminCreateContributionMessage,
createContribution,
createContributionMessage,
} from '@/seeds/graphql/mutations'
import { listContributionMessages, login } from '@/seeds/graphql/queries'
import { userFactory } from '@/seeds/factory/user'
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
import { peterLustig } from '@/seeds/users/peter-lustig'
let mutate: any, query: any, con: any
let testEnv: any
let result: any
beforeAll(async () => {
testEnv = await testEnvironment()
mutate = testEnv.mutate
query = testEnv.query
con = testEnv.con
await cleanDB()
})
afterAll(async () => {
await cleanDB()
await con.close()
})
describe('ContributionMessageResolver', () => {
describe('adminCreateContributionMessage', () => {
describe('unauthenticated', () => {
it('returns an error', async () => {
await expect(
mutate({
mutation: adminCreateContributionMessage,
variables: { contributionId: 1, message: 'This is a test message' },
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
)
})
})
describe('authenticated', () => {
beforeAll(async () => {
await userFactory(testEnv, bibiBloxberg)
await userFactory(testEnv, peterLustig)
await query({
query: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
result = await mutate({
mutation: createContribution,
variables: {
amount: 100.0,
memo: 'Test env contribution',
creationDate: new Date().toString(),
},
})
await query({
query: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
})
afterAll(() => {
resetToken()
})
describe('input not valid', () => {
it('throws error when contribution does not exist', async () => {
await expect(
mutate({
mutation: adminCreateContributionMessage,
variables: {
contributionId: -1,
message: 'Test',
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [
new GraphQLError(
'ContributionMessage was not successful: Error: Contribution not found',
),
],
}),
)
})
})
describe('valid input', () => {
it('creates ContributionMessage', async () => {
await expect(
mutate({
mutation: adminCreateContributionMessage,
variables: {
contributionId: result.data.createContribution.id,
message: 'Admin Test',
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
adminCreateContributionMessage: expect.objectContaining({
id: expect.any(Number),
message: 'Admin Test',
type: 'DIALOG',
userFirstName: 'Peter',
userLastName: 'Lustig',
}),
},
}),
)
})
})
})
})
describe('createContributionMessage', () => {
describe('unauthenticated', () => {
it('returns an error', async () => {
await expect(
mutate({
mutation: createContributionMessage,
variables: { contributionId: 1, message: 'This is a test message' },
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
)
})
})
describe('authenticated', () => {
beforeAll(async () => {
await query({
query: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
})
afterAll(() => {
resetToken()
})
describe('input not valid', () => {
it('throws error when contribution does not exist', async () => {
await expect(
mutate({
mutation: createContributionMessage,
variables: {
contributionId: -1,
message: 'Test',
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [
new GraphQLError(
'ContributionMessage was not successful: Error: Contribution not found',
),
],
}),
)
})
it('throws error when other user tries to send createContributionMessage', async () => {
await query({
query: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
await expect(
mutate({
mutation: createContributionMessage,
variables: {
contributionId: result.data.createContribution.id,
message: 'Test',
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [
new GraphQLError(
'ContributionMessage was not successful: Error: Can not send message to contribution of another user',
),
],
}),
)
})
})
describe('valid input', () => {
beforeAll(async () => {
await query({
query: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
})
it('creates ContributionMessage', async () => {
await expect(
mutate({
mutation: createContributionMessage,
variables: {
contributionId: result.data.createContribution.id,
message: 'User Test',
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
createContributionMessage: expect.objectContaining({
id: expect.any(Number),
message: 'User Test',
type: 'DIALOG',
userFirstName: 'Bibi',
userLastName: 'Bloxberg',
}),
},
}),
)
})
})
})
})
describe('listContributionMessages', () => {
describe('unauthenticated', () => {
it('returns an error', async () => {
await expect(
mutate({
mutation: listContributionMessages,
variables: { contributionId: 1 },
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
)
})
})
describe('authenticated', () => {
beforeAll(async () => {
await query({
query: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
})
afterAll(() => {
resetToken()
})
it('returns a list of contributionmessages', async () => {
await expect(
mutate({
mutation: listContributionMessages,
variables: { contributionId: result.data.createContribution.id },
}),
).resolves.toEqual(
expect.objectContaining({
data: {
listContributionMessages: {
count: 2,
messages: expect.arrayContaining([
expect.objectContaining({
id: expect.any(Number),
message: 'Admin Test',
type: 'DIALOG',
userFirstName: 'Peter',
userLastName: 'Lustig',
}),
expect.objectContaining({
id: expect.any(Number),
message: 'User Test',
type: 'DIALOG',
userFirstName: 'Bibi',
userLastName: 'Bloxberg',
}),
]),
},
},
}),
)
})
})
})
})

View File

@ -0,0 +1,84 @@
import { backendLogger as logger } from '@/server/logger'
import { RIGHTS } from '@/auth/RIGHTS'
import { Context, getUser } from '@/server/context'
import { ContributionMessage as DbContributionMessage } from '@entity/ContributionMessage'
import { Arg, Args, Authorized, Ctx, Mutation, Query, Resolver } from 'type-graphql'
import ContributionMessageArgs from '@arg/ContributionMessageArgs'
import { Contribution } from '@entity/Contribution'
import { ContributionMessageType } from '@enum/MessageType'
import { ContributionStatus } from '@enum/ContributionStatus'
import { getConnection } from '@dbTools/typeorm'
import { ContributionMessage, ContributionMessageListResult } from '@model/ContributionMessage'
import Paginated from '@arg/Paginated'
import { Order } from '@enum/Order'
@Resolver()
export class ContributionMessageResolver {
@Authorized([RIGHTS.CREATE_CONTRIBUTION_MESSAGE])
@Mutation(() => ContributionMessage)
async createContributionMessage(
@Args() { contributionId, message }: ContributionMessageArgs,
@Ctx() context: Context,
): Promise<ContributionMessage> {
const user = getUser(context)
const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect()
await queryRunner.startTransaction('READ UNCOMMITTED')
const contributionMessage = DbContributionMessage.create()
try {
const contribution = await Contribution.findOne({ id: contributionId })
if (!contribution) {
throw new Error('Contribution not found')
}
if (contribution.userId !== user.id) {
throw new Error('Can not send message to contribution of another user')
}
contributionMessage.contributionId = contributionId
contributionMessage.createdAt = new Date()
contributionMessage.message = message
contributionMessage.userId = user.id
contributionMessage.type = ContributionMessageType.DIALOG
await queryRunner.manager.insert(DbContributionMessage, contributionMessage)
if (contribution.contributionStatus === ContributionStatus.IN_PROGRESS) {
contribution.contributionStatus = ContributionStatus.PENDING
await queryRunner.manager.update(Contribution, { id: contributionId }, contribution)
}
await queryRunner.commitTransaction()
} catch (e) {
await queryRunner.rollbackTransaction()
logger.error(`ContributionMessage was not successful: ${e}`)
throw new Error(`ContributionMessage was not successful: ${e}`)
} finally {
await queryRunner.release()
}
return new ContributionMessage(contributionMessage, user)
}
@Authorized([RIGHTS.LIST_ALL_CONTRIBUTION_MESSAGES])
@Query(() => ContributionMessageListResult)
async listContributionMessages(
@Arg('contributionId') contributionId: number,
@Args()
{ currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated,
): Promise<ContributionMessageListResult> {
const [contributionMessages, count] = await getConnection()
.createQueryBuilder()
.select('cm')
.from(DbContributionMessage, 'cm')
.leftJoinAndSelect('cm.user', 'u')
.where({ contributionId: contributionId })
.orderBy('cm.createdAt', order)
.limit(pageSize)
.offset((currentPage - 1) * pageSize)
.getManyAndCount()
return {
count,
messages: contributionMessages.map(
(message) => new ContributionMessage(message, message.user),
),
}
}
}

View File

@ -11,7 +11,6 @@ import { ContributionType } from '@enum/ContributionType'
import { ContributionStatus } from '@enum/ContributionStatus'
import { Contribution, ContributionListResult } from '@model/Contribution'
import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
import { User } from '@model/User'
import { validateContribution, getUserCreation, updateCreations } from './util/creations'
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
@ -90,19 +89,23 @@ export class ContributionResolver {
userId: number
confirmedBy?: FindOperator<number> | null
} = { userId: user.id }
if (filterConfirmed) where.confirmedBy = IsNull()
const [contributions, count] = await dbContribution.findAndCount({
where,
order: {
createdAt: order,
},
withDeleted: true,
skip: (currentPage - 1) * pageSize,
take: pageSize,
})
const [contributions, count] = await getConnection()
.createQueryBuilder()
.select('c')
.from(dbContribution, 'c')
.leftJoinAndSelect('c.messages', 'm')
.where(where)
.orderBy('c.createdAt', order)
.limit(pageSize)
.offset((currentPage - 1) * pageSize)
.getManyAndCount()
return new ContributionListResult(
count,
contributions.map((contribution) => new Contribution(contribution, new User(user))),
contributions.map((contribution) => new Contribution(contribution, user)),
)
}
@ -123,9 +126,7 @@ export class ContributionResolver {
.getManyAndCount()
return new ContributionListResult(
count,
dbContributions.map(
(contribution) => new Contribution(contribution, new User(contribution.user)),
),
dbContributions.map((contribution) => new Contribution(contribution, contribution.user)),
)
}

View File

@ -261,3 +261,31 @@ export const deleteContribution = gql`
deleteContribution(id: $id)
}
`
export const createContributionMessage = gql`
mutation ($contributionId: Float!, $message: String!) {
createContributionMessage(contributionId: $contributionId, message: $message) {
id
message
createdAt
updatedAt
type
userFirstName
userLastName
}
}
`
export const adminCreateContributionMessage = gql`
mutation ($contributionId: Float!, $message: String!) {
adminCreateContributionMessage(contributionId: $contributionId, message: $message) {
id
message
createdAt
updatedAt
type
userFirstName
userLastName
}
}
`

View File

@ -292,3 +292,26 @@ export const searchAdminUsers = gql`
}
}
`
export const listContributionMessages = gql`
query ($contributionId: Float!, $pageSize: Int = 25, $currentPage: Int = 1, $order: Order = ASC) {
listContributionMessages(
contributionId: $contributionId
pageSize: $pageSize
currentPage: $currentPage
order: $order
) {
count
messages {
id
message
createdAt
updatedAt
type
userFirstName
userLastName
userId
}
}
}
`

View File

@ -8,6 +8,7 @@ import {
PrimaryGeneratedColumn,
} from 'typeorm'
import { Contribution } from '../Contribution'
import { User } from '../User'
@Entity('contribution_messages', {
engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci',
@ -26,6 +27,10 @@ export class ContributionMessage extends BaseEntity {
@Column({ name: 'user_id', unsigned: true, nullable: false })
userId: number
@ManyToOne(() => User, (user) => user.messages)
@JoinColumn({ name: 'user_id' })
user: User
@Column({ length: 2000, nullable: false, collation: 'utf8mb4_unicode_ci' })
message: string

View File

@ -0,0 +1,116 @@
import {
BaseEntity,
Entity,
PrimaryGeneratedColumn,
Column,
DeleteDateColumn,
OneToMany,
JoinColumn,
} from 'typeorm'
import { Contribution } from '../Contribution'
import { ContributionMessage } from '../ContributionMessage'
@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
export class User extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({
name: 'gradido_id',
length: 36,
nullable: false,
unique: true,
collation: 'utf8mb4_unicode_ci',
})
gradidoID: string
@Column({
name: 'alias',
length: 20,
nullable: true,
unique: true,
default: null,
collation: 'utf8mb4_unicode_ci',
})
alias: string
@Column({ name: 'public_key', type: 'binary', length: 32, default: null, nullable: true })
pubKey: Buffer
@Column({ name: 'privkey', type: 'binary', length: 80, default: null, nullable: true })
privKey: Buffer
@Column({ length: 255, unique: true, nullable: false, collation: 'utf8mb4_unicode_ci' })
email: string
@Column({
name: 'first_name',
length: 255,
nullable: true,
default: null,
collation: 'utf8mb4_unicode_ci',
})
firstName: string
@Column({
name: 'last_name',
length: 255,
nullable: true,
default: null,
collation: 'utf8mb4_unicode_ci',
})
lastName: string
@DeleteDateColumn()
deletedAt: Date | null
@Column({ type: 'bigint', default: 0, unsigned: true })
password: BigInt
@Column({ name: 'email_hash', type: 'binary', length: 32, default: null, nullable: true })
emailHash: Buffer
@Column({ name: 'created', default: () => 'CURRENT_TIMESTAMP', nullable: false })
createdAt: Date
@Column({ name: 'email_checked', type: 'bool', nullable: false, default: false })
emailChecked: boolean
@Column({ length: 4, default: 'de', collation: 'utf8mb4_unicode_ci', nullable: false })
language: string
@Column({ name: 'is_admin', type: 'datetime', nullable: true, default: null })
isAdmin: Date | null
@Column({ name: 'referrer_id', type: 'int', unsigned: true, nullable: true, default: null })
referrerId?: number | null
@Column({
name: 'contribution_link_id',
type: 'int',
unsigned: true,
nullable: true,
default: null,
})
contributionLinkId?: number | null
@Column({ name: 'publisher_id', default: 0 })
publisherId: number
@Column({
type: 'text',
name: 'passphrase',
collation: 'utf8mb4_unicode_ci',
nullable: true,
default: null,
})
passphrase: string
@OneToMany(() => Contribution, (contribution) => contribution.user)
@JoinColumn({ name: 'user_id' })
contributions?: Contribution[]
@OneToMany(() => ContributionMessage, (message) => message.user)
@JoinColumn({ name: 'user_id' })
messages?: ContributionMessage[]
}

View File

@ -1 +1 @@
export { User } from './0046-adapt_users_table_for_gradidoid/User'
export { User } from './0047-messages_tables/User'

View File

@ -68,7 +68,7 @@ module.exports = {
},
settings: {
'vue-i18n': {
localeDir: './src/locales/*.json',
localeDir: './src/locales/{en,de}.json',
// Specify the version of `vue-i18n` you are using.
// If not specified, the message will be parsed twice.
messageSyntaxVersion: '^8.22.4',

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

View File

@ -1,133 +0,0 @@
<template>
<div
class="mobil-start-box position-fixed h-100 d-inline d-sm-inline d-md-inline d-lg-none zindex1000"
>
<div class="position-absolute h1 text-white zindex1000 w-100 text-center mt-8">
{{ $t('auth.left.gratitude') }}
</div>
<div class="position-absolute h2 text-white zindex1000 w-100 text-center mt-9">
{{ $t('auth.left.oneGratitude') }}
</div>
<img
src="/img/template/Blaetter.png"
class="sheet-img position-absolute d-block d-lg-none zindex1000"
/>
<b-img
id="img0"
class="position-absolute zindex1000"
src="/img/template/logo-header.png"
alt="start background image"
></b-img>
<b-img
fluid
id="img1"
class="position-absolute h-100 w-100 overflow-hidden zindex100"
src="/img/template/gold_03.png"
alt="start background image"
></b-img>
<b-img
id="img2"
class="position-absolute zindex100"
src="/img/template/gradido_background_header.png"
alt="start background image"
></b-img>
<b-img
id="img3"
class="position-relative zindex10"
src="/img/template/Foto_01.jpg"
alt="start background image"
></b-img>
<div class="mobil-start-box-text position-fixed w-100 text-center zindex1000">
<b-button variant="gradido" to="/register" @click="$emit('set-mobile-start', false)">
{{ $t('signup') }}
</b-button>
<div class="mt-3 h3 text-white">
{{ $t('auth.left.hasAccount') }}
<b-link
to="/login"
class="text-gradido gradido-global-color-blue"
@click="$emit('set-mobile-start', false)"
>
{{ $t('auth.left.hereLogin') }}
</b-link>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'AuthMobileStart',
props: {
mobileStart: { type: Boolean, default: false },
},
}
</script>
<style>
.mobil-start-box-text {
bottom: 65px;
}
/* logo */
.mobil-start-box #img0 {
width: 200px;
}
/* background logo */
.mobil-start-box #img2 {
width: 230px;
}
/* background maske */
@media screen and (max-width: 1024px) {
.mobil-start-box #img3 {
width: 100%;
top: -100px;
}
}
@media screen and (max-width: 991px) {
.mobil-start-box #img3 {
width: 100%;
top: -148px;
}
}
@media screen and (max-height: 740px) {
.mobil-start-box #img3 {
width: 115%;
}
}
@media screen and (max-width: 650px) {
.mobil-start-box #img3 {
width: 115%;
top: 66px;
}
}
@media screen and (max-width: 450px) {
.mobil-start-box #img3 {
width: 160%;
left: -71px;
top: 35px;
min-width: 360px;
}
}
@media screen and (max-width: 310px) {
.mobil-start-box #img3 {
width: 145%;
left: -94px;
top: 24px;
min-width: 360px;
}
}
@media screen and (max-height: 700px) {
.mobil-start-box #img3 {
top: -104px;
}
}
</style>

View File

@ -1,7 +1,7 @@
<template>
<div class="auth-header position-sticky">
<b-navbar toggleable="lg" class="pr-4">
<b-navbar-brand>
<b-navbar :toggleable="false" class="pr-4">
<b-navbar-brand class="d-none d-lg-block">
<b-img
class="imgLogo position-absolute ml--3 mt-lg--2 mt-3 p-2 zindex1000"
:src="logo"
@ -16,13 +16,8 @@
></b-img>
</b-navbar-brand>
<b-img class="sheet-img position-absolute d-block d-lg-none zindex1000" :src="sheet"></b-img>
<b-navbar-toggle target="nav-collapse" class="zindex1000"></b-navbar-toggle>
<b-collapse id="nav-collapse" is-nav class="ml-5">
<b-navbar-nav class="ml-auto" right>
<b-nav-item :href="`https://gradido.net/${$i18n.locale}`" target="_blank">
{{ $t('auth.navbar.aboutGradido') }}
</b-nav-item>
<b-navbar-nav class="ml-auto d-none d-lg-flex" right>
<b-nav-item to="/register" class="authNavbar ml-lg-5">{{ $t('signup') }}</b-nav-item>
<span class="d-none d-lg-block mt-1">{{ $t('math.pipe') }}</span>
<b-nav-item to="/login" class="authNavbar">{{ $t('signin') }}</b-nav-item>
@ -49,18 +44,10 @@ export default {
color: #0e79bc !important;
}
.navbar-toggler {
font-size: 2.25rem;
}
.authNavbar > .router-link-exact-active {
color: #383838 !important;
}
button.navbar-toggler > span.navbar-toggler-icon {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba(4, 112, 6, 1)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
}
.auth-header {
font-family: 'Open Sans', sans-serif !important;
}

View File

@ -1,6 +1,6 @@
<template>
<div class="navbar-small">
<b-navbar>
<b-navbar class="navi">
<b-navbar-nav>
<b-nav-item to="/register" class="authNavbar">{{ $t('signup') }}</b-nav-item>
<span class="mt-1">{{ $t('math.pipe') }}</span>
@ -15,3 +15,9 @@ export default {
name: 'AuthNavbarSmall',
}
</script>
<style scoped>
.navi {
margin-left: 0px;
padding-left: 0px;
}
</style>

View File

@ -0,0 +1,111 @@
import { mount } from '@vue/test-utils'
import ContributionMessagesFormular from './ContributionMessagesFormular.vue'
import { toastErrorSpy, toastSuccessSpy } from '../../../test/testSetup'
const localVue = global.localVue
const apolloMutateMock = jest.fn().mockResolvedValue()
describe('ContributionMessagesFormular', () => {
let wrapper
const propsData = {
contributionId: 42,
}
const mocks = {
$t: jest.fn((t) => t),
$apollo: {
mutate: apolloMutateMock,
},
$i18n: {
locale: 'en',
},
}
const Wrapper = () => {
return mount(ContributionMessagesFormular, {
localVue,
mocks,
propsData,
})
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('has a DIV .contribution-messages-formular', () => {
expect(wrapper.find('div.contribution-messages-formular').exists()).toBe(true)
})
describe('on trigger reset', () => {
beforeEach(async () => {
wrapper.setData({
form: {
text: 'text form message',
},
})
await wrapper.find('form').trigger('reset')
})
it('form has empty text', () => {
expect(wrapper.vm.form).toEqual({
text: '',
})
})
})
describe('on trigger submit', () => {
beforeEach(async () => {
wrapper.setData({
form: {
text: 'text form message',
},
})
await wrapper.find('form').trigger('submit')
})
it('emitted "get-list-contribution-messages" with data', async () => {
expect(wrapper.emitted('get-list-contribution-messages')).toEqual(
expect.arrayContaining([expect.arrayContaining([42])]),
)
})
it('emitted "update-state" with data', async () => {
expect(wrapper.emitted('update-state')).toEqual(
expect.arrayContaining([expect.arrayContaining([42])]),
)
})
})
describe('send contribution message with error', () => {
beforeEach(async () => {
apolloMutateMock.mockRejectedValue({ message: 'OUCH!' })
wrapper = Wrapper()
await wrapper.find('form').trigger('submit')
})
it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith('OUCH!')
})
})
describe('send contribution message with success', () => {
beforeEach(async () => {
wrapper.setData({
form: {
text: 'text form message',
},
})
wrapper = Wrapper()
await wrapper.find('form').trigger('submit')
})
it('toasts an success message', () => {
expect(toastSuccessSpy).toBeCalledWith('message.reply')
})
})
})
})

View File

@ -0,0 +1,67 @@
<template>
<div class="contribution-messages-formular">
<div>
<b-form @submit.prevent="onSubmit" @reset="onReset">
<b-form-textarea
id="textarea"
v-model="form.text"
:placeholder="$t('form.memo')"
rows="3"
max-rows="6"
></b-form-textarea>
<b-row class="mt-4 mb-6">
<b-col>
<b-button type="reset" variant="danger">{{ $t('form.cancel') }}</b-button>
</b-col>
<b-col class="text-right">
<b-button type="submit" variant="primary">{{ $t('form.reply') }}</b-button>
</b-col>
</b-row>
</b-form>
</div>
</div>
</template>
<script>
import { createContributionMessage } from '../../graphql/mutations.js'
export default {
name: 'ContributionMessagesFormular',
props: {
contributionId: {
type: Number,
required: true,
},
},
data() {
return {
form: {
text: '',
},
}
},
methods: {
onSubmit() {
this.$apollo
.mutate({
mutation: createContributionMessage,
variables: {
contributionId: this.contributionId,
message: this.form.text,
},
})
.then((result) => {
this.$emit('get-list-contribution-messages', this.contributionId)
this.$emit('update-state', this.contributionId)
this.form.text = ''
this.toastSuccess(this.$t('message.reply'))
})
.catch((error) => {
this.toastError(error.message)
})
},
onReset() {
this.form.text = ''
},
},
}
</script>

View File

@ -0,0 +1,63 @@
import { mount } from '@vue/test-utils'
import ContributionMessagesList from './ContributionMessagesList.vue'
const localVue = global.localVue
describe('ContributionMessagesList', () => {
let wrapper
const propsData = {
contributionId: 42,
state: 'IN_PROGRESS',
messages: [],
}
const mocks = {
$t: jest.fn((t) => t),
$i18n: {
locale: 'en',
},
}
const Wrapper = () => {
return mount(ContributionMessagesList, {
localVue,
mocks,
propsData,
})
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('has a DIV .contribution-messages-list', () => {
expect(wrapper.find('div.contribution-messages-list').exists()).toBe(true)
})
it('has a Component ContributionMessagesFormular', () => {
expect(wrapper.findComponent({ name: 'ContributionMessagesFormular' }).exists()).toBe(true)
})
describe('get List Contribution Messages', () => {
beforeEach(() => {
wrapper.vm.getListContributionMessages()
})
it('emits getListContributionMessages', async () => {
expect(wrapper.vm.$emit('get-list-contribution-messages')).toBeTruthy()
})
})
describe('update State', () => {
beforeEach(() => {
wrapper.vm.updateState()
})
it('emits getListContributionMessages', async () => {
expect(wrapper.vm.$emit('update-state')).toBeTruthy()
})
})
})
})

View File

@ -0,0 +1,59 @@
<template>
<div class="contribution-messages-list">
<b-container>
<div v-for="message in messages" v-bind:key="message.id">
<contribution-messages-list-item :message="message" />
</div>
</b-container>
<contribution-messages-formular
v-if="['PENDING', 'IN_PROGRESS'].includes(state)"
class="mt-5"
:contributionId="contributionId"
@get-list-contribution-messages="getListContributionMessages"
@update-state="updateState"
/>
<div v-b-toggle="'collapse' + String(contributionId)" class="text-center pointer h2">
<b-icon icon="arrow-up-short"></b-icon>
{{ $t('form.close') }}
</div>
</div>
</template>
<script>
import ContributionMessagesListItem from '@/components/ContributionMessages/ContributionMessagesListItem.vue'
import ContributionMessagesFormular from '@/components/ContributionMessages/ContributionMessagesFormular.vue'
export default {
name: 'ContributionMessagesList',
components: {
ContributionMessagesListItem,
ContributionMessagesFormular,
},
props: {
contributionId: {
type: Number,
required: true,
},
state: {
type: String,
required: true,
},
messages: {
type: Array,
required: true,
},
},
methods: {
getListContributionMessages() {
this.$emit('get-list-contribution-messages', this.contributionId)
},
updateState(id) {
this.$emit('update-state', id)
},
},
}
</script>
<style scoped>
.temp-message {
margin-top: 50px;
}
</style>

View File

@ -0,0 +1,55 @@
import { mount } from '@vue/test-utils'
import ContributionMessagesList from './ContributionMessagesList.vue'
const localVue = global.localVue
describe('ContributionMessagesList', () => {
let wrapper
const mocks = {
$t: jest.fn((t) => t),
$d: jest.fn((d) => d),
$store: {
state: {
firstName: 'Peter',
lastName: 'Lustig',
},
},
}
const propsData = {
contributionId: 42,
state: 'PENDING0',
messages: [
{
id: 111,
message: 'asd asda sda sda',
createdAt: '2022-08-29T12:23:27.000Z',
updatedAt: null,
type: 'DIALOG',
userFirstName: 'Peter',
userLastName: 'Lustig',
userId: 107,
__typename: 'ContributionMessage',
},
],
}
const Wrapper = () => {
return mount(ContributionMessagesList, {
localVue,
mocks,
propsData,
})
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('has a DIV .contribution-messages-list-item', () => {
expect(wrapper.find('div.contribution-messages-list-item').exists()).toBe(true)
})
})
})

View File

@ -0,0 +1,38 @@
<template>
<div class="contribution-messages-list-item">
<is-not-moderator v-if="isNotModerator" :message="message"></is-not-moderator>
<is-moderator v-else :message="message"></is-moderator>
</div>
</template>
<script>
import IsModerator from '@/components/ContributionMessages/slots/IsModerator.vue'
import IsNotModerator from '@/components/ContributionMessages/slots/IsNotModerator.vue'
export default {
name: 'ContributionMessagesListItem',
components: {
IsModerator,
IsNotModerator,
},
props: {
message: {
type: Object,
required: true,
default() {
return {}
},
},
},
data() {
return {
storeName: `${this.$store.state.firstName} ${this.$store.state.lastName}`,
moderationName: `${this.message.userFirstName} ${this.message.userLastName}`,
}
},
computed: {
isNotModerator() {
return this.storeName === this.moderationName
},
},
}
</script>

View File

@ -0,0 +1,49 @@
import { mount } from '@vue/test-utils'
import IsModerator from './IsModerator.vue'
const localVue = global.localVue
describe('IsModerator', () => {
let wrapper
const mocks = {
$t: jest.fn((t) => t),
$d: jest.fn((d) => d),
}
const propsData = {
message: {
id: 111,
message: 'asd asda sda sda',
createdAt: '2022-08-29T12:23:27.000Z',
updatedAt: null,
type: 'DIALOG',
userFirstName: 'Peter',
userLastName: 'Lustig',
userId: 107,
__typename: 'ContributionMessage',
},
}
const Wrapper = () => {
return mount(IsModerator, {
localVue,
mocks,
propsData,
})
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('has a DIV .slot-is-moderator', () => {
expect(wrapper.find('div.slot-is-moderator').exists()).toBe(true)
})
it('props.message.default', () => {
expect(wrapper.vm.$options.props.message.default.call()).toEqual({})
})
})
})

View File

@ -0,0 +1,34 @@
<template>
<div class="slot-is-moderator">
<b-avatar square :text="initialLetters" variant="warning"></b-avatar>
<span class="ml-2 mr-2">{{ message.userFirstName }} {{ message.userLastName }}</span>
<span class="ml-2">{{ $d(new Date(message.createdAt), 'short') }}</span>
<small class="ml-4 text-success">{{ $t('community.moderator') }}</small>
<div class="mt-2 h3">{{ message.message }}</div>
</div>
</template>
<script>
export default {
props: {
message: {
type: Object,
default() {
return {}
},
},
},
computed: {
initialLetters() {
return `${this.message.userFirstName[0]} ${this.message.userLastName[0]}`
},
},
}
</script>
<style>
.slot-is-moderator {
clear: both;
/* background-color: rgb(255, 242, 227); */
width: 75%;
margin-top: 20px;
}
</style>

View File

@ -0,0 +1,49 @@
import { mount } from '@vue/test-utils'
import IsNotModerator from './IsNotModerator.vue'
const localVue = global.localVue
describe('IsNotModerator', () => {
let wrapper
const mocks = {
$t: jest.fn((t) => t),
$d: jest.fn((d) => d),
}
const propsData = {
message: {
id: 113,
message: 'asda sdad ad asdasd ',
createdAt: '2022-08-29T12:25:34.000Z',
updatedAt: null,
type: 'DIALOG',
userFirstName: 'Bibi',
userLastName: 'Bloxberg',
userId: 108,
__typename: 'ContributionMessage',
},
}
const Wrapper = () => {
return mount(IsNotModerator, {
localVue,
mocks,
propsData,
})
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('has a DIV .slot-is-not-moderator', () => {
expect(wrapper.find('div.slot-is-not-moderator').exists()).toBe(true)
})
it('props.message.default', () => {
expect(wrapper.vm.$options.props.message.default.call()).toEqual({})
})
})
})

View File

@ -0,0 +1,38 @@
<template>
<div class="slot-is-not-moderator">
<div class="text-right">
<b-avatar :text="initialLetters" variant="info"></b-avatar>
<span class="ml-2 mr-2 text-bold">
{{ message.userFirstName }} {{ message.userLastName }}
</span>
<span class="ml-2">{{ $d(new Date(message.createdAt), 'short') }}</span>
<div class="mt-2 h3">{{ message.message }}</div>
</div>
</div>
</template>
<script>
export default {
props: {
message: {
type: Object,
default() {
return {}
},
},
},
computed: {
initialLetters() {
return `${this.message.userFirstName[0]} ${this.message.userLastName[0]}`
},
},
}
</script>
<style>
.slot-is-not-moderator {
float: right;
width: 75%;
margin-top: 20px;
margin-bottom: 20px;
clear: both;
}
</style>

View File

@ -3,8 +3,10 @@
<div class="list-group" v-for="item in items" :key="item.id">
<contribution-list-item
v-bind="item"
:contributionId="item.id"
@update-contribution-form="updateContributionForm"
@delete-contribution="deleteContribution"
@update-state="updateState"
/>
</div>
<b-pagination
@ -46,6 +48,7 @@ export default {
data() {
return {
currentPage: 1,
messages: [],
}
},
methods: {
@ -62,6 +65,9 @@ export default {
deleteContribution(item) {
this.$emit('delete-contribution', item)
},
updateState(id) {
this.$emit('update-state', id)
},
},
computed: {
isPaginationVisible() {

View File

@ -12,6 +12,9 @@ describe('ContributionListItem', () => {
}
const propsData = {
contributionId: 42,
state: 'PENDING',
messagesCount: 2,
id: 1,
createdAt: '26/07/2022',
contributionDate: '07/06/2022',
@ -37,22 +40,6 @@ describe('ContributionListItem', () => {
expect(wrapper.find('div.contribution-list-item').exists()).toBe(true)
})
describe('contribution type', () => {
it('is pending by default', () => {
expect(wrapper.vm.type).toBe('pending')
})
it('is deleted when deletedAt is present', async () => {
await wrapper.setProps({ deletedAt: new Date().toISOString() })
expect(wrapper.vm.type).toBe('deleted')
})
it('is confirmed when confirmedAt is present', async () => {
await wrapper.setProps({ confirmedAt: new Date().toISOString() })
expect(wrapper.vm.type).toBe('confirmed')
})
})
describe('contribution icon', () => {
it('is bell-fill by default', () => {
expect(wrapper.vm.icon).toBe('bell-fill')
@ -83,6 +70,11 @@ describe('ContributionListItem', () => {
await wrapper.setProps({ confirmedAt: new Date().toISOString() })
expect(wrapper.vm.variant).toBe('success')
})
it('is warning at when state is IN_PROGRESS', async () => {
await wrapper.setProps({ state: 'IN_PROGRESS' })
expect(wrapper.vm.variant).toBe('warning')
})
})
describe('date', () => {
@ -133,7 +125,7 @@ describe('ContributionListItem', () => {
beforeEach(async () => {
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
spy.mockImplementation(() => Promise.resolve(false))
await wrapper.findAll('div.pointer').at(1).trigger('click')
await wrapper.findAll('div.pointer').at(2).trigger('click')
})
it('does not emit delete contribution', () => {

View File

@ -2,10 +2,19 @@
<div class="contribution-list-item">
<slot>
<div class="border p-3 w-100 mb-1" :class="`border-${variant}`">
<div>
<div class="d-inline-flex">
<div class="mr-2"><b-icon :icon="icon" :variant="variant" class="h2"></b-icon></div>
<div class="mr-2">
<b-icon
v-if="state === 'IN_PROGRESS'"
icon="question-square"
font-scale="2"
variant="warning"
></b-icon>
<b-icon v-else :icon="icon" :variant="variant" class="h2"></b-icon>
</div>
<div v-if="firstName" class="mr-3">{{ firstName }} {{ lastName }}</div>
<div class="mr-2" :class="type != 'deleted' ? 'font-weight-bold' : ''">
<div class="mr-2" :class="state !== 'DELETED' ? 'font-weight-bold' : ''">
{{ amount | GDD }}
</div>
{{ $t('math.minus') }}
@ -18,8 +27,12 @@
</span>
</div>
<div class="mr-2">{{ memo }}</div>
<div v-if="type === 'pending' && !firstName" class="d-flex flex-row-reverse">
<div
v-if="!['CONFIRMED', 'DELETED'].includes(state) && !firstName"
class="d-flex flex-row-reverse"
>
<div
v-if="!['CONFIRMED', 'DELETED'].includes(state)"
class="pointer ml-5"
@click="
$emit('update-contribution-form', {
@ -32,17 +45,57 @@
>
<b-icon icon="pencil" class="h2"></b-icon>
</div>
<div class="pointer" @click="deleteContribution({ id })">
<div
v-if="!['CONFIRMED', 'DELETED'].includes(state)"
class="pointer"
@click="deleteContribution({ id })"
>
<b-icon icon="trash" class="h2"></b-icon>
</div>
<div v-if="messagesCount > 0" class="pointer">
<b-icon
v-b-toggle="collapsId"
icon="chat-dots"
class="h2 mr-5"
@click="getListContributionMessages"
></b-icon>
</div>
</div>
</div>
<div v-if="messagesCount > 0">
<b-button
v-if="state === 'IN_PROGRESS'"
v-b-toggle="collapsId"
variant="warning"
@click="getListContributionMessages"
>
{{ $t('contribution.alert.answerQuestion') }}
</b-button>
<b-collapse :id="collapsId" class="mt-2">
<b-card>
<contribution-messages-list
:messages="messages_get"
:state="state"
:contributionId="contributionId"
@get-list-contribution-messages="getListContributionMessages"
@update-state="updateState"
/>
</b-card>
</b-collapse>
</div>
</div>
</slot>
</div>
</template>
<script>
import ContributionMessagesList from '@/components/ContributionMessages/ContributionMessagesList.vue'
import { listContributionMessages } from '../../graphql/queries.js'
export default {
name: 'ContributionListItem',
components: {
ContributionMessagesList,
},
props: {
id: {
type: Number,
@ -79,13 +132,26 @@ export default {
type: String,
required: false,
},
state: {
type: String,
required: false,
},
messagesCount: {
type: Number,
required: false,
},
contributionId: {
type: Number,
required: true,
},
},
data() {
return {
inProcess: true,
messages_get: [],
}
},
computed: {
type() {
if (this.deletedAt) return 'deleted'
if (this.confirmedAt) return 'confirmed'
return 'pending'
},
icon() {
if (this.deletedAt) return 'x-circle'
if (this.confirmedAt) return 'check'
@ -94,14 +160,15 @@ export default {
variant() {
if (this.deletedAt) return 'danger'
if (this.confirmedAt) return 'success'
if (this.state === 'IN_PROGRESS') return 'warning'
return 'primary'
},
date() {
// if (this.deletedAt) return this.deletedAt
// if (this.confirmedAt) return this.confirmedAt
// return this.contributionDate
return this.createdAt
},
collapsId() {
return 'collapse' + String(this.id)
},
},
methods: {
deleteContribution(item) {
@ -109,6 +176,27 @@ export default {
if (value) this.$emit('delete-contribution', item)
})
},
getListContributionMessages() {
// console.log('getListContributionMessages', this.contributionId)
this.$apollo
.query({
query: listContributionMessages,
variables: {
contributionId: this.contributionId,
},
fetchPolicy: 'no-cache',
})
.then((result) => {
// console.log('result', result.data.listContributionMessages.messages)
this.messages_get = result.data.listContributionMessages.messages
})
.catch((error) => {
this.toastError(error.message)
})
},
updateState(id) {
this.$emit('update-state', id)
},
},
}
</script>

View File

@ -1,14 +1,18 @@
<template>
<div class="language-switch">
<div v-b-toggle.collapse-1>
<span
v-for="lang in locales"
:key="lang.code"
class="pointer"
:class="$store.state.language === lang.code ? 'c-grey' : 'c-blau'"
>
<span v-if="lang.code === $store.state.language" class="locales mr-1">{{ lang.name }}</span>
<span v-if="lang.code === $store.state.language" class="locales mr-1">
{{ lang.name }}
</span>
<b-icon v-b-toggle.collapse-1 icon="caret-down-fill" aria-hidden="true"></b-icon>
</span>
<b-icon icon="caret-down-fill" aria-hidden="true"></b-icon>
</div>
<b-collapse id="collapse-1" class="mt-4">
<span
v-for="(lang, index) in locales"

View File

@ -122,3 +122,17 @@ export const deleteContribution = gql`
deleteContribution(id: $id)
}
`
export const createContributionMessage = gql`
mutation($contributionId: Float!, $message: String!) {
createContributionMessage(contributionId: $contributionId, message: $message) {
id
message
createdAt
updatedAt
type
userFirstName
userLastName
}
}
`

View File

@ -206,6 +206,8 @@ export const listContributions = gql`
confirmedAt
confirmedBy
deletedAt
state
messagesCount
}
}
}
@ -255,3 +257,26 @@ export const searchAdminUsers = gql`
}
}
`
export const listContributionMessages = gql`
query($contributionId: Float!, $pageSize: Int = 25, $currentPage: Int = 1, $order: Order = ASC) {
listContributionMessages(
contributionId: $contributionId
pageSize: $pageSize
currentPage: $currentPage
order: $order
) {
count
messages {
id
message
createdAt
updatedAt
type
userFirstName
userLastName
userId
}
}
}
`

View File

@ -36,20 +36,6 @@ describe('AuthLayout', () => {
wrapper = Wrapper()
})
describe('Mobile Version Start', () => {
beforeEach(() => {
wrapper.findComponent({ name: 'AuthMobileStart' }).vm.$emit('set-mobile-start', true)
})
it('has Component AuthMobileStart', () => {
expect(wrapper.findComponent({ name: 'AuthMobileStart' }).exists()).toBe(true)
})
it('has Component AuthNavbarSmall', () => {
expect(wrapper.findComponent({ name: 'AuthNavbarSmall' }).exists()).toBe(true)
})
})
describe('Desktop Version Start', () => {
beforeEach(() => {
wrapper.vm.mobileStart = false

View File

@ -1,10 +1,5 @@
<template>
<div class="auth-template">
<auth-mobile-start
v-if="mobileStart"
class="d-inline d-lg-none zindex10000"
@set-mobile-start="setMobileStart"
/>
<div class="h-100 align-middle">
<auth-navbar class="zindex10" />
@ -20,11 +15,11 @@
</b-link>
</div>
</div>
<b-row class="justify-content-md-center">
<b-col sm="12" md="8" offset-lg="6" lg="6" class="zindex1000">
<div class="right-content-box ml-3 ml-sm-4 mr-3 mr-sm-4">
<b-row class="justify-content-md-center justify-content-lg-end">
<b-col sm="12" md="8" lg="6" class="zindex1000">
<div class="ml-3 ml-sm-4 mr-3 mr-sm-4">
<b-row class="d-none d-md-block d-lg-none">
<b-col class="mb--4 d-flex justify-content-end">
<b-col class="mb--4">
<auth-navbar-small />
</b-col>
</b-row>
@ -74,20 +69,18 @@
</b-col>
</b-row>
<b-card-body class="">
<router-view @set-mobile-start="setMobileStart"></router-view>
<router-view></router-view>
</b-card-body>
</b-card>
</div>
<auth-footer v-if="!$route.meta.hideFooter" class="pr-5 mb-5"></auth-footer>
</b-col>
</b-row>
<!-- <auth-layout-gdd />-->
</div>
</div>
</template>
<script>
import AuthMobileStart from '@/components/Auth/AuthMobileStart.vue'
import AuthNavbar from '@/components/Auth/AuthNavbar.vue'
import AuthNavbarSmall from '@/components/Auth/AuthNavbarSmall.vue'
import AuthCarousel from '@/components/Auth/AuthCarousel.vue'
@ -98,7 +91,6 @@ import CONFIG from '@/config'
export default {
name: 'AuthLayout',
components: {
AuthMobileStart,
AuthNavbar,
AuthNavbarSmall,
AuthCarousel,
@ -107,14 +99,10 @@ export default {
},
data() {
return {
mobileStart: true,
communityName: CONFIG.COMMUNITY_NAME,
}
},
methods: {
setMobileStart(boolean) {
this.mobileStart = boolean
},
setTextSize(size) {
this.$refs.pageFontSize.style.fontSize = size + 'rem'
},

View File

@ -9,15 +9,10 @@
"dignity": "Würde",
"donation": "Gabe",
"gratitude": "Dankbarkeit",
"hasAccount": "Du hast schon einen Account?",
"hereLogin": "Hier Anmelden",
"learnMore": "Erfahre mehr …",
"oneDignity": "Wir schenken einander und danken mit Gradido.",
"oneDonation": "Du bist ein Geschenk für die Gemeinschaft. 1000 Dank, weil du bei uns bist.",
"oneGratitude": "Für einander, für alle Menschen, für die Natur."
},
"navbar": {
"aboutGradido": "Über Gradido"
}
},
"back": "Zurück",
@ -26,23 +21,23 @@
"community": "Gemeinschaft",
"continue-to-registration": "Weiter zur Registrierung",
"current-community": "Aktuelle Gemeinschaft",
"members": "Mitglieder",
"moderator": "Moderator",
"moderators": "Moderatoren",
"myContributions": "Meine Beiträge zum Gemeinwohl",
"openContributionLinks": "öffentliche Beitrags-Linkliste",
"openContributionLinkText": "Folgende {count} automatische Schöpfungen werden zur Zeit durch die Gemeinschaft „{name}“ bereitgestellt.",
"other-communities": "Weitere Gemeinschaften",
"statistic": "Statistik",
"submitContribution": "Beitrag einreichen",
"switch-to-this-community": "zu dieser Gemeinschaft wechseln"
},
"contribution": {
"activity": "Tätigkeit",
"alert": {
"answerQuestion": "Bitte beantworte die Nachfrage",
"communityNoteList": "Hier findest du alle eingereichten und bestätigten Beiträge von allen Mitgliedern aus dieser Gemeinschaft.",
"confirm": "bestätigt",
"in_progress": "Es gibt eine Rückfrage der Moderatoren.",
"myContributionNoteList": "Eingereichte Beiträge, die noch nicht bestätigt wurden, kannst du jederzeit bearbeiten oder löschen.",
"myContributionNoteSupport": "Es wird bald an dieser Stelle die Möglichkeit geben das ein Dialog zwischen Moderatoren und dir stattfinden kann. Solltest du jetzt Probleme haben bitte nimm Kontakt mit dem Support auf.",
"pending": "Eingereicht und wartet auf Bestätigung",
"rejected": "abgelehnt"
},
@ -135,6 +130,7 @@
"password_new_repeat": "Neues Passwort wiederholen",
"password_old": "Altes Passwort",
"recipient": "Empfänger",
"reply": "Antworten",
"reset": "Zurücksetzen",
"save": "Speichern",
"scann_code": "<strong>QR Code Scanner</strong> - Scanne den QR Code deines Partners",
@ -224,6 +220,7 @@
"email": "Wir haben dir eine E-Mail gesendet.",
"errorTitle": "Achtung!",
"register": "Du bist jetzt registriert, bitte überprüfe deine Emails und klicke auf den Aktivierungslink.",
"reply": "Danke, Deine Antwort wurde abgesendet.",
"reset": "Dein Passwort wurde geändert.",
"title": "Danke!",
"unsetPassword": "Dein Passwort wurde noch nicht gesetzt. Bitte setze es neu."
@ -308,14 +305,6 @@
"uppercase": "Großbuchstabe erforderlich."
}
},
"statistic": {
"activeUsers": "Aktive Mitglieder",
"deletedUsers": "Gelöschte Mitglieder",
"totalGradidoAvailable": "GDD insgesamt im Umlauf",
"totalGradidoCreated": "GDD insgesamt geschöpft",
"totalGradidoDecayed": "GDD insgesamt verfallen",
"totalGradidoUnbookedDecayed": "Gesamter ungebuchter GDD Verfall"
},
"success": "Erfolg",
"time": {
"days": "Tage",

View File

@ -9,15 +9,10 @@
"dignity": "Dignity",
"donation": "Donation",
"gratitude": "Gratitude",
"hasAccount": "You already have an account?",
"hereLogin": "Log in here",
"learnMore": "Learn more …",
"oneDignity": "We gift to each other and give thanks with Gradido.",
"oneDonation": "You are a gift for the community. 1000 thanks because you are with us.",
"oneGratitude": "For each other, for all people, for nature."
},
"navbar": {
"aboutGradido": "About Gradido"
}
},
"back": "Back",
@ -26,23 +21,23 @@
"community": "Community",
"continue-to-registration": "Continue to registration",
"current-community": "Current community",
"members": "Members",
"moderator": "Moderator",
"moderators": "Moderators",
"myContributions": "My contributions to the common good",
"openContributionLinks": "open Contribution links list",
"openContributionLinkText": "The following {count} automatic creations are currently provided by the \"{name}\" community.",
"other-communities": "Other communities",
"statistic": "Statistics",
"submitContribution": "Submit contribution",
"switch-to-this-community": "Switch to this community"
},
"contribution": {
"activity": "Activity",
"alert": {
"answerQuestion": "Please answer the question",
"communityNoteList": "Here you will find all submitted and confirmed contributions from all members of this community.",
"confirm": "confirmed",
"in_progress": "There is a question from the moderators.",
"myContributionNoteList": "You can edit or delete entries that have not yet been confirmed at any time.",
"myContributionNoteSupport": "Soon there will be the possibility for a dialogue between moderators and you. If you have any problems now, please contact the support.",
"pending": "Submitted and waiting for confirmation",
"rejected": "deleted"
},
@ -135,6 +130,7 @@
"password_new_repeat": "Repeat new password",
"password_old": "Old password",
"recipient": "Recipient",
"reply": "Reply",
"reset": "Reset",
"save": "Save",
"scann_code": "<strong>QR Code Scanner</strong> - Scan the QR Code of your partner",
@ -224,6 +220,7 @@
"email": "We have sent you an email.",
"errorTitle": "Attention!",
"register": "You are registered now, please check your emails and click the activation link.",
"reply": "Thank you, your reply has been sent.",
"reset": "Your password has been changed.",
"title": "Thank you!",
"unsetPassword": "Your password has not been set yet. Please set it again."
@ -308,14 +305,6 @@
"uppercase": "One uppercase letter required."
}
},
"statistic": {
"activeUsers": "Active members",
"deletedUsers": "Deleted members",
"totalGradidoAvailable": "Total GDD in circulation",
"totalGradidoCreated": "Total created GDD",
"totalGradidoDecayed": "Total GDD decay",
"totalGradidoUnbookedDecayed": "Total unbooked GDD decay"
},
"success": "Success",
"time": {
"days": "Days",

View File

@ -9,15 +9,10 @@
"dignity": "Dignidad",
"donation": "Donación",
"gratitude": "Gratitud",
"hasAccount": "Ya estas registrado?",
"hereLogin": "Regístrate aquí",
"learnMore": "Infórmate aquí …",
"oneDignity": "Damos los unos a los otros y agradecemos con Gradido.",
"oneDonation": "Eres un regalo para la comunidad. 1000 gracias por estar con nosotros.",
"oneGratitude": "Por los demás, por toda la humanidad, por la naturaleza."
},
"navbar": {
"aboutGradido": "Sobre Gradido"
}
},
"back": "Volver",
@ -27,6 +22,7 @@
"continue-to-registration": "Continuar con el registro",
"current-community": "Comunidad actual",
"members": "Miembros",
"moderator": "Moderador",
"moderators": "Moderadores",
"myContributions": "Mis contribuciones al bien común",
"openContributionLinks": "lista de enlaces de contribuciones públicas",
@ -39,10 +35,11 @@
"contribution": {
"activity": "Actividad",
"alert": {
"answerQuestion": "Por favor, contesta las preguntas",
"communityNoteList": "Aquí encontrarás todas las contribuciones enviadas y confirmadas de todos los miembros de esta comunidad.",
"confirm": "confirmado",
"in_progress": "Hay una pregunta de los moderatores.",
"myContributionNoteList": "Puedes editar o eliminar las contribuciones enviadas que aún no han sido confirmadas en cualquier momento.",
"myContributionNoteSupport": "Pronto existirá la posibilidad de que puedas dialogar con los moderadores. Si tienes algún problema ahora, ponte en contacto con el equipo de asistencia.",
"pending": "Enviado y a la espera de confirmación",
"rejected": "rechazado"
},
@ -135,6 +132,7 @@
"password_new_repeat": "Repetir contraseña nueva",
"password_old": "contraseña antigua",
"recipient": "Destinatario",
"reply": "Respuesta",
"reset": "Restablecer",
"save": "Guardar",
"scann_code": "<strong>QR Code Scanner</strong> - Escanea el código QR de tu pareja",
@ -224,6 +222,7 @@
"email": "Te hemos enviado un correo electrónico.",
"errorTitle": "Atención!",
"register": "Ya estás registrado, por favor revisa tu correo electrónico y haz clic en el enlace de activación.",
"reply": "Gracias, tu respuesta ha sido enviada.",
"reset": "Tu contraseña ha sido cambiada.",
"title": "Gracias!",
"unsetPassword": "Tu contraseña aún no ha sido configurada. Por favor reinícialo."
@ -309,12 +308,9 @@
}
},
"statistic": {
"activeUsers": "miembros activos",
"deletedUsers": "miembros eliminados",
"totalGradidoAvailable": "GDD total en circulación",
"totalGradidoCreated": "GDD total creado",
"totalGradidoDecayed": "GDD total decaído",
"totalGradidoUnbookedDecayed": "GDD no contabilizado decaído"
"totalGradidoDecayed": "GDD total decaído"
},
"success": "Lo lograste",
"time": {

View File

@ -9,15 +9,10 @@
"dignity": "Dignité",
"donation": "Donation",
"gratitude": "Gratitude",
"hasAccount": "Avez-vous déjà un compte?",
"hereLogin": "Connectez-vous ici",
"learnMore": "Pour plus de détails …",
"oneDignity": "Nous nous donnons mutuellement et rendons grâce avec Gradido.",
"oneDonation": "Vous êtes précieux pour la communauté. 1000 mercis dêtre parmi nous.",
"oneGratitude": "Les uns pour les autres, pour tout le monde, pour la nature."
},
"navbar": {
"aboutGradido": "À propos de Gradido"
}
},
"back": "Retour",
@ -27,6 +22,7 @@
"continue-to-registration": "Continuez l´inscription",
"current-community": "Communauté actuelle",
"members": "Membres",
"moderator": "Modérateur",
"moderators": "Modérateurs",
"myContributions": "Mes contributions aux biens communs",
"openContributionLinks": "liste de liens de contribution publique",
@ -39,10 +35,11 @@
"contribution": {
"activity": "Activité",
"alert": {
"answerQuestion": "S'il te plais répond à la question",
"communityNoteList": "Vous trouverez ci-contre toutes les contributions versées et certifiées de tous les membres de cette communauté.",
"confirm": " Approuvé",
"in_progress": "Il y a une question du modérateur.",
"myContributionNoteList": "À tout moment vous pouvez éditer ou supprimer les données qui n´ont pas été confirmées.",
"myContributionNoteSupport": "Vous aurez bientôt la possibilité de dialoguer avec un médiateur. Si vous rencontrez un problème maintenant, merci de contacter l´aide en ligne.",
"pending": "Inscription en attente de validation",
"rejected": "supprimé"
},
@ -135,6 +132,7 @@
"password_new_repeat": "Répétez le nouveau mot de passe",
"password_old": "Ancien mot de passe",
"recipient": "Destinataire",
"reply": "Répondre",
"reset": "Réinitialiser",
"save": "Sauvegarder",
"scann_code": "<strong>QR Code Scanner</strong> - Scannez le QR code de votre partenaire",
@ -224,6 +222,7 @@
"email": "Nous vous avons envoyé un email.",
"errorTitle": "Attention!",
"register": "Vous êtes enregistré maintenant, merci de vérifier votre boîte mail et cliquer sur le lien d´activation.",
"reply": "Merci, ta réponse a été envoyée.",
"reset": "Votre mot de passe a été modifié.",
"title": "Merci!",
"unsetPassword": "Votre mot de passe n´a pas été accepté. Merci de le réinitialiser."
@ -309,12 +308,9 @@
}
},
"statistic": {
"activeUsers": "Membres actifs",
"deletedUsers": "Membres supprimés",
"totalGradidoAvailable": "GDD total en circulation",
"totalGradidoCreated": "GDD total puisé",
"totalGradidoDecayed": "Total de GDD écoulé",
"totalGradidoUnbookedDecayed": "Total GDD non comptabilisé écoulé"
"totalGradidoDecayed": "Total de GDD écoulé"
},
"success": "Avec succès",
"time": {

View File

@ -9,15 +9,10 @@
"dignity": "Waardigheid",
"donation": "Gift",
"gratitude": "Dankbaarheid",
"hasAccount": "Je hebt al een rekening?",
"hereLogin": "Hier aanmelden",
"learnMore": "Meer ervaren …",
"oneDignity": "We geven aan elkaar en bedanken met Gradido.",
"oneDonation": "Jij bent een geschenk voor de gemeenschap. 1000 dank dat je bij ons bent.",
"oneGratitude": "Voor elkaar, voor alle mensen, voor de natuur."
},
"navbar": {
"aboutGradido": "Over Gradido"
}
},
"back": "Terug",
@ -27,6 +22,7 @@
"continue-to-registration": "Verder ter registratie",
"current-community": "Actuele gemeenschap",
"members": "Leden",
"moderator": "Moderator",
"moderators": "Moderators",
"myContributions": "Mijn bijdragen voor het algemeen belang",
"openContributionLinks": "openbare lijst van bijdragen",
@ -39,10 +35,11 @@
"contribution": {
"activity": "Activiteit",
"alert": {
"answerQuestion": "Please answer the question",
"communityNoteList": "Hier vind je alle ingediende en bevestigde bijdragen van alle leden uit deze gemeenschap.",
"confirm": "bevestigt",
"in_progress": "There is a question from the moderators.",
"myContributionNoteList": "Ingediende bijdragen, die nog niet bevestigd zijn, kun je op elk moment wijzigen of verwijderen.",
"myContributionNoteSupport": "Hier heb je binnenkort de mogelijkheid een gesprek met een moderator te voeren. Mocht je nu problemen hebben, dan neem alsjeblieft contact op met Support.",
"pending": "Ingediend en wacht op bevestiging",
"rejected": "afgewezen"
},
@ -135,6 +132,7 @@
"password_new_repeat": "Nieuw wachtwoord herhalen",
"password_old": "Oud wachtwoord",
"recipient": "Ontvanger",
"reply": "Antwoord",
"reset": "Resetten",
"save": "Opslaan",
"scann_code": "<strong>QR Code Scanner</strong> - Scan de QR Code van uw partner",
@ -224,6 +222,7 @@
"email": "We hebben jou een email gestuurd.",
"errorTitle": "Opgelet!",
"register": "Je bent nu geregistreerd. Controleer alsjeblieft je emails en klik op de activeringslink.",
"reply": "Dank u, uw antwoord is verzonden.",
"reset": "Jouw wachtwoord werd gewijzigd.",
"title": "Dankjewel!",
"unsetPassword": "Jouw wachtwoord werd nog niet ingesteld. Doe het alsjeblieft opnieuw."
@ -309,12 +308,9 @@
}
},
"statistic": {
"activeUsers": "Actieve leden",
"deletedUsers": "Verwijderde leden",
"totalGradidoAvailable": "Totaal GDD in omloop",
"totalGradidoCreated": "Totaal GDD geschept",
"totalGradidoDecayed": "Totaal GDD vervallen",
"totalGradidoUnbookedDecayed": "Totaal niet geboekte GDD vervallen"
"totalGradidoDecayed": "Totaal GDD vervallen"
},
"success": "Succes",
"time": {

View File

@ -93,9 +93,7 @@ describe('Community', () => {
expect(wrapper.findAll('div[role="tabpanel"]')).toHaveLength(3)
})
it('has first tab active by default', () => {
expect(wrapper.findAll('div[role="tabpanel"]').at(0).classes('active')).toBe(true)
})
it.todo('check for correct tabIndex if state is "IN_PROGRESS" or not')
})
describe('API calls after creation', () => {

View File

@ -2,7 +2,7 @@
<div class="community-page">
<div>
<b-tabs v-model="tabIndex" content-class="mt-3" align="center">
<b-tab :title="$t('community.submitContribution')" active>
<b-tab :title="$t('community.submitContribution')">
<contribution-form
@set-contribution="setContribution"
@update-contribution="updateContribution"
@ -22,6 +22,10 @@
<b-icon icon="bell-fill" variant="primary"></b-icon>
{{ $t('contribution.alert.pending') }}
</li>
<li>
<b-icon icon="question-square" variant="warning"></b-icon>
{{ $t('contribution.alert.in_progress') }}
</li>
<li>
<b-icon icon="check" variant="success"></b-icon>
{{ $t('contribution.alert.confirm') }}
@ -32,9 +36,6 @@
</li>
</ul>
<hr />
<p class="mb-0">
{{ $t('contribution.alert.myContributionNoteSupport') }}
</p>
</b-alert>
</div>
<contribution-list
@ -42,6 +43,7 @@
@update-list-contributions="updateListContributions"
@update-contribution-form="updateContributionForm"
@delete-contribution="deleteContribution"
@update-state="updateState"
:contributionCount="contributionCount"
:showPagination="true"
:pageSize="pageSize"
@ -226,6 +228,11 @@ export default {
} = result
this.contributionCount = listContributions.contributionCount
this.items = listContributions.contributionList
if (this.items.find((item) => item.state === 'IN_PROGRESS')) {
this.tabIndex = 1
} else {
this.tabIndex = 0
}
})
.catch((err) => {
this.toastError(err.message)
@ -258,6 +265,9 @@ export default {
updateTransactions(pagination) {
this.$emit('update-transactions', pagination)
},
updateState(id) {
this.items.find((item) => item.id === id).state = 'PENDING'
},
},
created() {
// verifyLogin is important at this point so that creation is updated on reload if they are deleted in a session in the admin area.
@ -271,6 +281,7 @@ export default {
pageSize: this.pageSize,
})
this.updateTransactions(0)
this.tabIndex = 1
},
}
</script>

View File

@ -45,12 +45,12 @@ const apolloQueryMock = jest
data: {
communityStatistics: {
totalUsers: 3113,
activeUsers: 1057,
deletedUsers: 35,
// activeUsers: 1057,
// deletedUsers: 35,
totalGradidoCreated: '4083774.05000000000000000000',
totalGradidoDecayed: '-1062639.13634129622923372197',
totalGradidoAvailable: '2513565.869444365732411569',
totalGradidoUnbookedDecayed: '-500474.6738366222166261272',
// totalGradidoUnbookedDecayed: '-500474.6738366222166261272',
},
},
})
@ -86,7 +86,6 @@ describe('InfoStatistic', () => {
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
query: listContributionLinks,
fetchPolicy: 'network-only',
}),
)
})
@ -95,16 +94,14 @@ describe('InfoStatistic', () => {
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
query: searchAdminUsers,
fetchPolicy: 'network-only',
}),
)
})
it('calls getCommunityStatistics', () => {
it.skip('calls getCommunityStatistics', () => {
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
query: communityStatistics,
fetchPolicy: 'network-only',
}),
)
})
@ -118,12 +115,12 @@ describe('InfoStatistic', () => {
wrapper = Wrapper()
})
it('toasts three error messages', () => {
it('toasts two error messages', () => {
expect(toastErrorSpy).toBeCalledWith(
'listContributionLinks has no result, use default data',
)
expect(toastErrorSpy).toBeCalledWith('searchAdminUsers has no result, use default data')
expect(toastErrorSpy).toBeCalledWith('communityStatistics has no result, use default data')
// expect(toastErrorSpy).toBeCalledWith('communityStatistics has no result, use default data')
})
})
})

View File

@ -43,6 +43,7 @@
</ul>
<b-link href="mailto: abc@example.com">{{ supportMail }}</b-link>
</b-container>
<!--
<hr />
<b-container>
<div class="h3">{{ $t('community.statistic') }}</div>
@ -51,14 +52,6 @@
{{ $t('community.members') }}
<span class="h4">{{ totalUsers }}</span>
</div>
<div>
{{ $t('statistic.activeUsers') }}
<span class="h4">{{ activeUsers }}</span>
</div>
<div>
{{ $t('statistic.deletedUsers') }}
<span class="h4">{{ deletedUsers }}</span>
</div>
<div>
{{ $t('statistic.totalGradidoCreated') }}
<span class="h4">{{ totalGradidoCreated | GDD }}</span>
@ -71,17 +64,15 @@
{{ $t('statistic.totalGradidoAvailable') }}
<span class="h4">{{ totalGradidoAvailable | GDD }}</span>
</div>
<div>
{{ $t('statistic.totalGradidoUnbookedDecayed') }}
<span class="h4">{{ totalGradidoUnbookedDecayed | GDD }}</span>
</div>
</div>
</b-container>
-->
</div>
</template>
<script>
import CONFIG from '@/config'
import { listContributionLinks, communityStatistics, searchAdminUsers } from '@/graphql/queries'
import { listContributionLinks, searchAdminUsers } from '@/graphql/queries'
// , communityStatistics
export default {
name: 'InfoStatistic',
@ -95,12 +86,9 @@ export default {
supportMail: 'support@supportemail.de',
membersCount: '1203',
totalUsers: null,
activeUsers: null,
deletedUsers: null,
totalGradidoCreated: null,
totalGradidoDecayed: null,
totalGradidoAvailable: null,
totalGradidoUnbookedDecayed: null,
}
},
methods: {
@ -108,7 +96,6 @@ export default {
this.$apollo
.query({
query: listContributionLinks,
fetchPolicy: 'network-only',
})
.then((result) => {
this.count = result.data.listContributionLinks.count
@ -122,7 +109,6 @@ export default {
this.$apollo
.query({
query: searchAdminUsers,
fetchPolicy: 'network-only',
})
.then((result) => {
this.countAdminUser = result.data.searchAdminUsers.userCount
@ -132,26 +118,25 @@ export default {
this.toastError('searchAdminUsers has no result, use default data')
})
},
/*
getCommunityStatistics() {
this.$apollo
.query({
query: communityStatistics,
fetchPolicy: 'network-only',
})
.then((result) => {
this.totalUsers = result.data.communityStatistics.totalUsers
this.activeUsers = result.data.communityStatistics.activeUsers
this.deletedUsers = result.data.communityStatistics.deletedUsers
this.totalGradidoCreated = result.data.communityStatistics.totalGradidoCreated
this.totalGradidoDecayed = result.data.communityStatistics.totalGradidoDecayed
this.totalGradidoDecayed =
Number(result.data.communityStatistics.totalGradidoDecayed) +
Number(result.data.communityStatistics.totalGradidoUnbookedDecayed)
this.totalGradidoAvailable = result.data.communityStatistics.totalGradidoAvailable
this.totalGradidoUnbookedDecayed =
result.data.communityStatistics.totalGradidoUnbookedDecayed
})
.catch(() => {
this.toastError('communityStatistics has no result, use default data')
})
},
*/
updateTransactions(pagination) {
this.$emit('update-transactions', pagination)
},
@ -159,7 +144,7 @@ export default {
created() {
this.getContributionLinks()
this.getAdminUsers()
this.getCommunityStatistics()
// this.getCommunityStatistics()
this.updateTransactions(0)
},
}