Merge branch 'master' into slots-for-right-sidebar

This commit is contained in:
Moriz Wahl 2023-01-16 11:38:04 +01:00 committed by GitHub
commit d95dc27e17
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 425 additions and 253 deletions

View File

@ -437,7 +437,7 @@ jobs:
report_name: Coverage Frontend report_name: Coverage Frontend
type: lcov type: lcov
result_path: ./coverage/lcov.info result_path: ./coverage/lcov.info
min_coverage: 89 min_coverage: 91
token: ${{ github.token }} token: ${{ github.token }}
############################################################################## ##############################################################################

View File

@ -3,6 +3,10 @@
"streetsidesoftware.code-spell-checker", "streetsidesoftware.code-spell-checker",
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"hediet.vscode-drawio" "hediet.vscode-drawio",
"streetsidesoftware.code-spell-checker-german",
"mtxr.sqltools",
"mtxr.sqltools-driver-mysql",
"jcbuisson.vue"
] ]
} }

17
.vscode/settings.json vendored
View File

@ -1,3 +1,18 @@
{ {
"git.ignoreLimitWarning": true "git.ignoreLimitWarning": true,
"sqltools.connections": [
{
"mysqlOptions": {
"authProtocol": "default"
},
"previewLimit": 50,
"server": "localhost",
"port": 3306,
"driver": "MariaDB",
"name": "localhost",
"database": "gradido_community",
"username": "root",
"password": ""
}
],
} }

View File

@ -31,7 +31,6 @@
"express": "^4.17.1", "express": "^4.17.1",
"graphql": "^15.5.1", "graphql": "^15.5.1",
"i18n": "^0.15.1", "i18n": "^0.15.1",
"jest": "^27.2.4",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"log4js": "^6.4.6", "log4js": "^6.4.6",

View File

@ -35,6 +35,7 @@ export enum RIGHTS {
SEARCH_ADMIN_USERS = 'SEARCH_ADMIN_USERS', SEARCH_ADMIN_USERS = 'SEARCH_ADMIN_USERS',
CREATE_CONTRIBUTION_MESSAGE = 'CREATE_CONTRIBUTION_MESSAGE', CREATE_CONTRIBUTION_MESSAGE = 'CREATE_CONTRIBUTION_MESSAGE',
LIST_ALL_CONTRIBUTION_MESSAGES = 'LIST_ALL_CONTRIBUTION_MESSAGES', LIST_ALL_CONTRIBUTION_MESSAGES = 'LIST_ALL_CONTRIBUTION_MESSAGES',
OPEN_CREATIONS = 'OPEN_CREATIONS',
// Admin // Admin
SEARCH_USERS = 'SEARCH_USERS', SEARCH_USERS = 'SEARCH_USERS',
SET_USER_ROLE = 'SET_USER_ROLE', SET_USER_ROLE = 'SET_USER_ROLE',

View File

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

View File

@ -0,0 +1,14 @@
import { ObjectType, Field, Int } from 'type-graphql'
import Decimal from 'decimal.js-light'
@ObjectType()
export class OpenCreation {
@Field(() => Int)
month: number
@Field(() => Int)
year: number
@Field(() => Decimal)
amount: Decimal
}

View File

@ -1,13 +1,11 @@
import { ObjectType, Field } from 'type-graphql' import { ObjectType, Field } from 'type-graphql'
import { KlickTipp } from './KlickTipp' import { KlickTipp } from './KlickTipp'
import { User as dbUser } from '@entity/User' import { User as dbUser } from '@entity/User'
import Decimal from 'decimal.js-light'
import { FULL_CREATION_AVAILABLE } from '../resolver/const/const'
import { UserContact } from './UserContact' import { UserContact } from './UserContact'
@ObjectType() @ObjectType()
export class User { export class User {
constructor(user: dbUser, creation: Decimal[] = FULL_CREATION_AVAILABLE) { constructor(user: dbUser) {
this.id = user.id this.id = user.id
this.gradidoID = user.gradidoID this.gradidoID = user.gradidoID
this.alias = user.alias this.alias = user.alias
@ -26,7 +24,6 @@ export class User {
this.isAdmin = user.isAdmin this.isAdmin = user.isAdmin
this.klickTipp = null this.klickTipp = null
this.hasElopage = null this.hasElopage = null
this.creation = creation
this.hideAmountGDD = user.hideAmountGDD this.hideAmountGDD = user.hideAmountGDD
this.hideAmountGDT = user.hideAmountGDT this.hideAmountGDT = user.hideAmountGDT
} }
@ -34,9 +31,6 @@ export class User {
@Field(() => Number) @Field(() => Number)
id: number id: number
// `public_key` binary(32) DEFAULT NULL,
// `privkey` binary(80) DEFAULT NULL,
@Field(() => String) @Field(() => String)
gradidoID: string gradidoID: string
@ -62,9 +56,6 @@ export class User {
@Field(() => Date, { nullable: true }) @Field(() => Date, { nullable: true })
deletedAt: Date | null deletedAt: Date | null
// `password` bigint(20) unsigned DEFAULT 0,
// `email_hash` binary(32) DEFAULT NULL,
@Field(() => Date) @Field(() => Date)
createdAt: Date createdAt: Date
@ -84,8 +75,6 @@ export class User {
@Field(() => Number, { nullable: true }) @Field(() => Number, { nullable: true })
publisherId: number | null publisherId: number | null
// `passphrase` text COLLATE utf8mb4_unicode_ci DEFAULT NULL,
@Field(() => Date, { nullable: true }) @Field(() => Date, { nullable: true })
isAdmin: Date | null isAdmin: Date | null
@ -94,7 +83,4 @@ export class User {
@Field(() => Boolean, { nullable: true }) @Field(() => Boolean, { nullable: true })
hasElopage: boolean | null hasElopage: boolean | null
@Field(() => [Decimal])
creation: Decimal[]
} }

View File

@ -11,8 +11,9 @@ import { Transaction as DbTransaction } from '@entity/Transaction'
import { AdminCreateContributions } from '@model/AdminCreateContributions' import { AdminCreateContributions } from '@model/AdminCreateContributions'
import { AdminUpdateContribution } from '@model/AdminUpdateContribution' import { AdminUpdateContribution } from '@model/AdminUpdateContribution'
import { Contribution, ContributionListResult } from '@model/Contribution' import { Contribution, ContributionListResult } from '@model/Contribution'
import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
import { Decay } from '@model/Decay' import { Decay } from '@model/Decay'
import { OpenCreation } from '@model/OpenCreation'
import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
import { TransactionTypeId } from '@enum/TransactionTypeId' import { TransactionTypeId } from '@enum/TransactionTypeId'
import { Order } from '@enum/Order' import { Order } from '@enum/Order'
import { ContributionType } from '@enum/ContributionType' import { ContributionType } from '@enum/ContributionType'
@ -27,6 +28,7 @@ import { RIGHTS } from '@/auth/RIGHTS'
import { Context, getUser, getClientTimezoneOffset } from '@/server/context' import { Context, getUser, getClientTimezoneOffset } from '@/server/context'
import { backendLogger as logger } from '@/server/logger' import { backendLogger as logger } from '@/server/logger'
import { import {
getCreationDates,
getUserCreation, getUserCreation,
getUserCreations, getUserCreations,
validateContribution, validateContribution,
@ -691,4 +693,23 @@ export class ContributionResolver {
) )
// return userTransactions.map((t) => new Transaction(t, new User(user), communityUser)) // return userTransactions.map((t) => new Transaction(t, new User(user), communityUser))
} }
@Authorized([RIGHTS.OPEN_CREATIONS])
@Query(() => [OpenCreation])
async openCreations(
@Arg('userId', () => Int, { nullable: true }) userId: number | null,
@Ctx() context: Context,
): Promise<OpenCreation[]> {
const id = userId || getUser(context).id
const clientTimezoneOffset = getClientTimezoneOffset(context)
const creationDates = getCreationDates(clientTimezoneOffset)
const creations = await getUserCreation(id, clientTimezoneOffset)
return creationDates.map((date, index) => {
return {
month: date.getMonth(),
year: date.getFullYear(),
amount: creations[index],
}
})
}
} }

View File

@ -58,7 +58,7 @@ import {
EventSendConfirmationEmail, EventSendConfirmationEmail,
EventActivateAccount, EventActivateAccount,
} from '@/event/Event' } from '@/event/Event'
import { getUserCreation, getUserCreations } from './util/creations' import { getUserCreations } from './util/creations'
import { isValidPassword } from '@/password/EncryptorUtils' import { isValidPassword } from '@/password/EncryptorUtils'
import { FULL_CREATION_AVAILABLE } from './const/const' import { FULL_CREATION_AVAILABLE } from './const/const'
import { encryptPassword, verifyPassword } from '@/password/PasswordEncryptor' import { encryptPassword, verifyPassword } from '@/password/PasswordEncryptor'
@ -114,9 +114,8 @@ export class UserResolver {
async verifyLogin(@Ctx() context: Context): Promise<User> { async verifyLogin(@Ctx() context: Context): Promise<User> {
logger.info('verifyLogin...') logger.info('verifyLogin...')
// TODO refactor and do not have duplicate code with login(see below) // TODO refactor and do not have duplicate code with login(see below)
const clientTimezoneOffset = getClientTimezoneOffset(context)
const userEntity = getUser(context) const userEntity = getUser(context)
const user = new User(userEntity, await getUserCreation(userEntity.id, clientTimezoneOffset)) const user = new User(userEntity)
// Elopage Status & Stored PublisherId // Elopage Status & Stored PublisherId
user.hasElopage = await this.hasElopage(context) user.hasElopage = await this.hasElopage(context)
@ -132,7 +131,6 @@ export class UserResolver {
@Ctx() context: Context, @Ctx() context: Context,
): Promise<User> { ): Promise<User> {
logger.info(`login with ${email}, ***, ${publisherId} ...`) logger.info(`login with ${email}, ***, ${publisherId} ...`)
const clientTimezoneOffset = getClientTimezoneOffset(context)
email = email.trim().toLowerCase() email = email.trim().toLowerCase()
const dbUser = await findUserByEmail(email) const dbUser = await findUserByEmail(email)
if (dbUser.deletedAt) { if (dbUser.deletedAt) {
@ -163,7 +161,7 @@ export class UserResolver {
logger.addContext('user', dbUser.id) logger.addContext('user', dbUser.id)
logger.debug('validation of login credentials successful...') logger.debug('validation of login credentials successful...')
const user = new User(dbUser, await getUserCreation(dbUser.id, clientTimezoneOffset)) const user = new User(dbUser)
logger.debug(`user= ${JSON.stringify(user, null, 2)}`) logger.debug(`user= ${JSON.stringify(user, null, 2)}`)
i18n.setLocale(user.language) i18n.setLocale(user.language)

View File

@ -101,15 +101,19 @@ export const getUserCreation = async (
} }
const getCreationMonths = (timezoneOffset: number): number[] => { const getCreationMonths = (timezoneOffset: number): number[] => {
return getCreationDates(timezoneOffset).map((date) => date.getMonth() + 1)
}
export const getCreationDates = (timezoneOffset: number): Date[] => {
const clientNow = new Date() const clientNow = new Date()
clientNow.setTime(clientNow.getTime() - timezoneOffset * 60 * 1000) clientNow.setTime(clientNow.getTime() - timezoneOffset * 60 * 1000)
logger.info( logger.info(
`getCreationMonths -- offset: ${timezoneOffset} -- clientNow: ${clientNow.toISOString()}`, `getCreationMonths -- offset: ${timezoneOffset} -- clientNow: ${clientNow.toISOString()}`,
) )
return [ return [
new Date(clientNow.getFullYear(), clientNow.getMonth() - 2, 1).getMonth() + 1, new Date(clientNow.getFullYear(), clientNow.getMonth() - 2, 1),
new Date(clientNow.getFullYear(), clientNow.getMonth() - 1, 1).getMonth() + 1, new Date(clientNow.getFullYear(), clientNow.getMonth() - 1, 1),
clientNow.getMonth() + 1, clientNow,
] ]
} }

View File

@ -231,3 +231,32 @@ This opens the `crontab` in edit-mode and insert the following entry:
```bash ```bash
0 4 * * * find /tmp -name "yarn--*" -ctime +1 -exec rm -r {} \; > /dev/null 0 4 * * * find /tmp -name "yarn--*" -ctime +1 -exec rm -r {} \; > /dev/null
``` ```
## Define Cronjob To start backup script automatically
At least at production stage we need a daily backup of our database. This can be done by adding a cronjob
to start the existing backup.sh script.
### On production / stage3 / stage2
To check for existing cronjobs for the `gradido` user, please
Run:
```bash
crontab -l
```
This show all existing entries of the crontab for user `gradido`
To install/add the cronjob for a daily backup at 3:00am please
Run:
```bash
crontab -e
```
and insert the following line
```bash
0 3 * * * ~/gradido/deployment/bare_metal/backup.sh
```

View File

@ -0,0 +1,103 @@
import { mount } from '@vue/test-utils'
import OpenCreationsAmount from './OpenCreationsAmount.vue'
const localVue = global.localVue
describe('OpenCreationsAmount', () => {
let wrapper
const mocks = {
$t: jest.fn((t) => t),
$d: jest.fn((date, formatter = null) => {
return { date, formatter }
}),
}
const thisMonth = new Date()
const lastMonth = new Date(thisMonth.getFullYear(), thisMonth.getMonth() - 1)
const propsData = {
minimalDate: lastMonth,
maxGddLastMonth: 400,
maxGddThisMonth: 600,
}
const Wrapper = () => {
return mount(OpenCreationsAmount, {
localVue,
mocks,
propsData,
})
}
describe('mount', () => {
beforeEach(() => {
jest.clearAllMocks()
wrapper = Wrapper()
})
it('renders the component', () => {
expect(wrapper.find('div.appBoxShadow').exists()).toBe(true)
})
it('renders two dates', () => {
expect(mocks.$d).toBeCalledTimes(2)
})
it('renders the date of last month', () => {
expect(mocks.$d).toBeCalledWith(lastMonth, 'monthAndYear')
})
it('renders the date of this month', () => {
expect(mocks.$d).toBeCalledWith(expect.any(Date), 'monthAndYear')
})
describe('open creations for both months', () => {
it('renders submitted contributions text', () => {
expect(mocks.$t).toBeCalledWith('contribution.submit')
})
it('does not render max reached text', () => {
expect(mocks.$t).not.toBeCalledWith('maxReached')
})
it('renders submitted hours last month', () => {
expect(wrapper.findAll('div.row').at(1).findAll('div.col').at(2).text()).toBe('30 h')
})
it('renders available hours last month', () => {
expect(wrapper.findAll('div.row').at(1).findAll('div.col').at(3).text()).toBe('20 h')
})
it('renders submitted hours this month', () => {
expect(wrapper.findAll('div.row').at(2).findAll('div.col').at(2).text()).toBe('20 h')
})
it('renders available hours this month', () => {
expect(wrapper.findAll('div.row').at(2).findAll('div.col').at(3).text()).toBe('30 h')
})
})
describe('no creations available for last month', () => {
beforeEach(() => {
wrapper.setProps({ maxGddLastMonth: 0 })
})
it('renders submitted contributions text', () => {
expect(mocks.$t).toBeCalledWith('contribution.submit')
})
it('renders max reached text', () => {
expect(mocks.$t).toBeCalledWith('maxReached')
})
it('renders submitted hours last month', () => {
expect(wrapper.findAll('div.row').at(1).findAll('div.col').at(2).text()).toBe('50 h')
})
it('renders available hours last month', () => {
expect(wrapper.findAll('div.row').at(1).findAll('div.col').at(3).text()).toBe('0 h')
})
})
})
})

View File

@ -14,9 +14,9 @@
{{ maxGddLastMonth > 0 ? $t('contribution.submit') : $t('maxReached') }} {{ maxGddLastMonth > 0 ? $t('contribution.submit') : $t('maxReached') }}
</b-col> </b-col>
<b-col class="d-none d-md-inline text-197 text-center"> <b-col class="d-none d-md-inline text-197 text-center">
{{ (1000 - maxGddLastMonth) / 20 }} {{ $t('h') }} {{ hoursSubmittedLastMonth }} {{ $t('h') }}
</b-col> </b-col>
<b-col class="text-4 text-center">{{ maxGddLastMonth / 20 }} {{ $t('h') }}</b-col> <b-col class="text-4 text-center">{{ hoursAvailableLastMonth }} {{ $t('h') }}</b-col>
</b-row> </b-row>
<b-row class="font-weight-bold"> <b-row class="font-weight-bold">
@ -25,9 +25,9 @@
{{ maxGddThisMonth > 0 ? $t('contribution.submit') : $t('maxReached') }} {{ maxGddThisMonth > 0 ? $t('contribution.submit') : $t('maxReached') }}
</b-col> </b-col>
<b-col class="d-none d-md-inline text-197 text-center"> <b-col class="d-none d-md-inline text-197 text-center">
{{ (1000 - maxGddThisMonth) / 20 }} {{ $t('h') }} {{ hoursSubmittedThisMonth }} {{ $t('h') }}
</b-col> </b-col>
<b-col class="text-4 text-center">{{ maxGddThisMonth / 20 }} {{ $t('h') }}</b-col> <b-col class="text-4 text-center">{{ hoursAvailableThisMonth }} {{ $t('h') }}</b-col>
</b-row> </b-row>
</div> </div>
</div> </div>
@ -40,5 +40,19 @@ export default {
maxGddLastMonth: { type: Number, required: true }, maxGddLastMonth: { type: Number, required: true },
maxGddThisMonth: { type: Number, required: true }, maxGddThisMonth: { type: Number, required: true },
}, },
computed: {
hoursSubmittedThisMonth() {
return (1000 - this.maxGddThisMonth) / 20
},
hoursSubmittedLastMonth() {
return (1000 - this.maxGddLastMonth) / 20
},
hoursAvailableThisMonth() {
return this.maxGddThisMonth / 20
},
hoursAvailableLastMonth() {
return this.maxGddLastMonth / 20
},
},
} }
</script> </script>

View File

@ -1,112 +1,115 @@
<template> <template>
<b-row class="transaction-form"> <div class="transaction-form">
<b-col cols="12"> <b-row>
<b-card class="appBoxShadow gradido-border-radius" body-class="p-3"> <b-col cols="12">
<validation-observer v-slot="{ handleSubmit }" ref="formValidator"> <b-card class="appBoxShadow gradido-border-radius" body-class="p-3">
<b-form role="form" @submit.prevent="handleSubmit(onSubmit)" @reset="onReset"> <validation-observer v-slot="{ handleSubmit }" ref="formValidator">
<b-form-radio-group v-model="radioSelected" class="container"> <b-form role="form" @submit.prevent="handleSubmit(onSubmit)" @reset="onReset">
<b-row class="mb-4"> <b-form-radio-group v-model="radioSelected" class="container">
<b-col cols="12" lg="6"> <b-row class="mb-4">
<b-row class="bg-248 gradido-border-radius pt-lg-2 mr-lg-2"> <b-col cols="12" lg="6">
<b-col cols="10" @click="radioSelected = sendTypes.send" class="pointer"> <b-row class="bg-248 gradido-border-radius pt-lg-2 mr-lg-2">
{{ $t('send_gdd') }} <b-col cols="10" @click="radioSelected = sendTypes.send" class="pointer">
</b-col> {{ $t('send_gdd') }}
<b-col cols="2"> </b-col>
<b-form-radio <b-col cols="2">
name="shipping" <b-form-radio
size="lg" name="shipping"
:value="sendTypes.send" size="lg"
stacked :value="sendTypes.send"
class="custom-radio-button pointer" stacked
></b-form-radio> class="custom-radio-button pointer"
</b-col> ></b-form-radio>
</b-row> </b-col>
</b-col> </b-row>
</b-col>
<b-col>
<b-row class="bg-248 gradido-border-radius pt-lg-2 ml-lg-2 mt-2 mt-lg-0">
<b-col cols="10" @click="radioSelected = sendTypes.link" class="pointer">
{{ $t('send_per_link') }}
</b-col>
<b-col cols="2" class="pointer">
<b-form-radio
name="shipping"
:value="sendTypes.link"
size="lg"
class="custom-radio-button"
></b-form-radio>
</b-col>
</b-row>
</b-col>
</b-row>
<div class="mt-4 mb-4" v-if="radioSelected === sendTypes.link">
<h2 class="alert-heading">{{ $t('gdd_per_link.header') }}</h2>
<div>
{{ $t('gdd_per_link.choose-amount') }}
</div>
</div>
</b-form-radio-group>
<b-row>
<b-col> <b-col>
<b-row class="bg-248 gradido-border-radius pt-lg-2 ml-lg-2 mt-2 mt-lg-0"> <b-row>
<b-col cols="10" @click="radioSelected = sendTypes.link" class="pointer"> <b-col cols="12">
{{ $t('send_per_link') }} <div v-if="radioSelected === sendTypes.send">
<input-email
:name="$t('form.recipient')"
:label="$t('form.recipient')"
:placeholder="$t('form.email')"
v-model="form.email"
:disabled="isBalanceDisabled"
@onValidation="onValidation"
/>
</div>
</b-col> </b-col>
<b-col cols="2" class="pointer"> <b-col cols="12" lg="6">
<b-form-radio <input-amount
name="shipping" v-model="form.amount"
:value="sendTypes.link" :name="$t('form.amount')"
size="lg" :label="$t('form.amount')"
class="custom-radio-button" :placeholder="'0.01'"
></b-form-radio> :rules="{ required: true, gddSendAmount: [0.01, balance] }"
typ="TransactionForm"
:disabled="isBalanceDisabled"
></input-amount>
</b-col> </b-col>
</b-row> </b-row>
</b-col> </b-col>
</b-row> </b-row>
<div class="mt-4 mb-4" v-if="radioSelected === sendTypes.link"> <b-row>
<h2 class="alert-heading">{{ $t('gdd_per_link.header') }}</h2> <b-col>
<div> <input-textarea
{{ $t('gdd_per_link.choose-amount') }} v-model="form.memo"
</div> :name="$t('form.message')"
:label="$t('form.message')"
:placeholder="$t('form.message')"
:rules="{ required: true, min: 5, max: 255 }"
:disabled="isBalanceDisabled"
/>
</b-col>
</b-row>
<div v-if="!!isBalanceDisabled" class="text-danger mt-5">
{{ $t('form.no_gdd_available') }}
</div> </div>
</b-form-radio-group> <b-row v-else class="test-buttons mt-5">
<b-row> <b-col>
<b-col> <b-button type="reset" variant="secondary" @click="onReset">
<b-row> {{ $t('form.reset') }}
<b-col cols="12"> </b-button>
<div v-if="radioSelected === sendTypes.send"> </b-col>
<input-email <b-col class="text-right">
:name="$t('form.recipient')" <b-button type="submit" variant="gradido">
:label="$t('form.recipient')" {{ $t('form.check_now') }}
:placeholder="$t('form.email')" </b-button>
v-model="form.email" </b-col>
:disabled="isBalanceDisabled" </b-row>
/> </b-form>
</div> </validation-observer>
</b-col> </b-card>
<b-col cols="12" lg="6"> </b-col>
<input-amount </b-row>
v-model="form.amount" </div>
:name="$t('form.amount')"
:label="$t('form.amount')"
:placeholder="'0.01'"
:rules="{ required: true, gddSendAmount: [0.01, balance] }"
typ="TransactionForm"
:disabled="isBalanceDisabled"
></input-amount>
</b-col>
</b-row>
</b-col>
</b-row>
<b-row>
<b-col>
<input-textarea
v-model="form.memo"
:name="$t('form.message')"
:label="$t('form.message')"
:placeholder="$t('form.message')"
:rules="{ required: true, min: 5, max: 255 }"
:disabled="isBalanceDisabled"
/>
</b-col>
</b-row>
<div v-if="!!isBalanceDisabled" class="text-danger mt-5">
{{ $t('form.no_gdd_available') }}
</div>
<b-row v-else class="test-buttons mt-5">
<b-col>
<b-button type="reset" variant="secondary" @click="onReset">
{{ $t('form.reset') }}
</b-button>
</b-col>
<b-col class="text-right">
<b-button type="submit" variant="gradido">
{{ $t('form.check_now') }}
</b-button>
</b-col>
</b-row>
</b-form>
</validation-observer>
</b-card>
</b-col>
</b-row>
</template> </template>
<script> <script>
import { SEND_TYPES } from '@/pages/Send.vue' import { SEND_TYPES } from '@/pages/Send.vue'
@ -140,6 +143,9 @@ export default {
} }
}, },
methods: { methods: {
onValidation() {
this.$refs.formValidator.validate()
},
onSubmit() { onSubmit() {
this.$emit('set-transaction', { this.$emit('set-transaction', {
selected: this.radioSelected, selected: this.radioSelected,
@ -153,6 +159,7 @@ export default {
this.form.email = '' this.form.email = ''
this.form.amount = '' this.form.amount = ''
this.form.memo = '' this.form.memo = ''
this.$refs.formValidator.validate()
}, },
setNewRecipientEmail() { setNewRecipientEmail() {
this.form.email = this.recipientEmail ? this.recipientEmail : this.form.email this.form.email = this.recipientEmail ? this.recipientEmail : this.form.email
@ -177,6 +184,9 @@ export default {
created() { created() {
this.setNewRecipientEmail() this.setNewRecipientEmail()
}, },
mounted() {
if (this.form.email !== '') this.$refs.formValidator.validate()
},
} }
</script> </script>
<style> <style>

View File

@ -19,7 +19,7 @@
<div v-else>{{ errorResult }}</div> <div v-else>{{ errorResult }}</div>
</div> </div>
<p class="text-center mt-5"> <p class="text-center mt-5">
<b-button variant="secondary" @click="$emit('on-reset')"> <b-button variant="secondary" @click="$emit('on-back')">
{{ $t('form.close') }} {{ $t('form.close') }}
</b-button> </b-button>
</p> </p>

View File

@ -22,6 +22,7 @@
@focus="amountFocused = true" @focus="amountFocused = true"
@blur="normalizeAmount(true)" @blur="normalizeAmount(true)"
:disabled="disabled" :disabled="disabled"
autocomplete="off"
></b-form-input> ></b-form-input>
<b-form-invalid-feedback v-bind="ariaMsg"> <b-form-invalid-feedback v-bind="ariaMsg">
@ -63,7 +64,7 @@ export default {
}, },
data() { data() {
return { return {
currentValue: '', currentValue: this.value,
amountValue: 0.0, amountValue: 0.0,
amountFocused: false, amountFocused: false,
} }

View File

@ -21,6 +21,7 @@
@focus="emailFocused = true" @focus="emailFocused = true"
@blur="normalizeEmail()" @blur="normalizeEmail()"
:disabled="disabled" :disabled="disabled"
autocomplete="off"
></b-form-input> ></b-form-input>
<b-form-invalid-feedback v-bind="ariaMsg"> <b-form-invalid-feedback v-bind="ariaMsg">
{{ errors[0] }} {{ errors[0] }}
@ -62,7 +63,10 @@ export default {
this.$emit('input', this.currentValue) this.$emit('input', this.currentValue)
}, },
value() { value() {
if (this.value !== this.currentValue) this.currentValue = this.value if (this.value !== this.currentValue) {
this.currentValue = this.value
}
this.$emit('onValidation')
}, },
}, },
methods: { methods: {

View File

@ -58,7 +58,7 @@ describe('InputTextarea', () => {
}) })
it('has the value ""', () => { it('has the value ""', () => {
expect(wrapper.vm.currentValue).toEqual('') expect(wrapper.vm.currentValue).toEqual('Long enough')
}) })
it('has the label "input-field-label"', () => { it('has the label "input-field-label"', () => {
@ -72,9 +72,8 @@ describe('InputTextarea', () => {
describe('input value changes', () => { describe('input value changes', () => {
it('emits input with new value', async () => { it('emits input with new value', async () => {
await wrapper.find('textarea').setValue('Long enough') await wrapper.find('textarea').setValue('New Text')
expect(wrapper.emitted('input')).toBeTruthy() expect(wrapper.emitted('input')).toEqual([['New Text']])
expect(wrapper.emitted('input')).toEqual([['Long enough']])
}) })
}) })

View File

@ -41,7 +41,7 @@ export default {
}, },
data() { data() {
return { return {
currentValue: '', currentValue: this.value,
} }
}, },
computed: { computed: {

View File

@ -19,7 +19,7 @@
<b-icon icon="layers" aria-hidden="true"></b-icon> <b-icon icon="layers" aria-hidden="true"></b-icon>
<span class="ml-2">{{ $t('gdt.gdt') }}</span> <span class="ml-2">{{ $t('gdt.gdt') }}</span>
</b-nav-item> </b-nav-item>
<b-nav-item to="/community#my" class="" active-class="activeRoute"> <b-nav-item to="/community" class="" active-class="activeRoute">
<b-icon icon="people" aria-hidden="true"></b-icon> <b-icon icon="people" aria-hidden="true"></b-icon>
<span class="ml-2">{{ $t('creation') }}</span> <span class="ml-2">{{ $t('creation') }}</span>
</b-nav-item> </b-nav-item>

View File

@ -1,20 +1,20 @@
<template> <template>
<div class="nav-community"> <div class="nav-community container">
<b-row class="nav-row"> <b-row class="nav-row">
<b-col cols="12" lg="4" md="4"> <b-col cols="12" lg="4" md="4" class="px-0">
<b-btn active-class="btn-active" block variant="link" to="#edit"> <b-btn active-class="btn-active" block variant="link" to="/community#edit">
<b-icon icon="pencil" class="mr-2" /> <b-icon icon="pencil" class="mr-2" />
{{ $t('community.submitContribution') }} {{ $t('community.submitContribution') }}
</b-btn> </b-btn>
</b-col> </b-col>
<b-col cols="12" lg="4" md="4"> <b-col cols="12" lg="4" md="4" class="px-0">
<b-btn active-class="btn-active" block variant="link" to="#my"> <b-btn active-class="btn-active" block variant="link" to="/community#my">
<b-icon icon="person" class="mr-2" /> <b-icon icon="person" class="mr-2" />
{{ $t('community.myContributions') }} {{ $t('community.myContributions') }}
</b-btn> </b-btn>
</b-col> </b-col>
<b-col cols="12" lg="4" md="4"> <b-col cols="12" lg="4" md="4" class="px-0">
<b-btn active-class="btn-active" block variant="link" to="#all"> <b-btn active-class="btn-active" block variant="link" to="/community#all">
<b-icon icon="people" class="mr-2" /> <b-icon icon="people" class="mr-2" />
{{ $t('community.community') }} {{ $t('community.community') }}
</b-btn> </b-btn>

View File

@ -24,7 +24,7 @@
:size="72" :size="72"
:color="'#fff'" :color="'#fff'"
:username="`${transaction.linkedUser.firstName} ${transaction.linkedUser.lastName}`" :username="`${transaction.linkedUser.firstName} ${transaction.linkedUser.lastName}`"
:initials="`${transaction.linkedUser.firstName[0]} ${transaction.linkedUser.lastName[0]}`" :initials="`${transaction.linkedUser.firstName[0]}${transaction.linkedUser.lastName[0]}`"
></avatar> ></avatar>
</div> </div>
</b-col> </b-col>

View File

@ -154,7 +154,6 @@ export const login = gql`
hasElopage hasElopage
publisherId publisherId
isAdmin isAdmin
creation
hideAmountGDD hideAmountGDD
hideAmountGDT hideAmountGDT
} }

View File

@ -250,3 +250,13 @@ export const listContributionMessages = gql`
} }
} }
` `
export const openCreations = gql`
query {
openCreations {
year
month
amount
}
}
`

View File

@ -2,33 +2,14 @@ import { mount } from '@vue/test-utils'
import Community from './Community' import Community from './Community'
import { toastErrorSpy, toastSuccessSpy } from '@test/testSetup' import { toastErrorSpy, toastSuccessSpy } from '@test/testSetup'
import { createContribution, updateContribution, deleteContribution } from '@/graphql/mutations' import { createContribution, updateContribution, deleteContribution } from '@/graphql/mutations'
import { listContributions, listAllContributions, verifyLogin } from '@/graphql/queries' import { listContributions, listAllContributions } from '@/graphql/queries'
import VueRouter from 'vue-router'
import routes from '../routes/routes'
const localVue = global.localVue const localVue = global.localVue
localVue.use(VueRouter)
const mockStoreDispach = jest.fn() const mockStoreDispach = jest.fn()
const apolloQueryMock = jest.fn() const apolloQueryMock = jest.fn()
const apolloMutationMock = jest.fn() const apolloMutationMock = jest.fn()
const apolloRefetchMock = jest.fn()
const router = new VueRouter({
base: '/',
routes,
linkActiveClass: 'active',
mode: 'history',
// scrollBehavior: (to, from, savedPosition) => {
// if (savedPosition) {
// return savedPosition
// }
// if (to.hash) {
// return { selector: to.hash }
// }
// return { x: 0, y: 0 }
// },
})
describe('Community', () => { describe('Community', () => {
let wrapper let wrapper
@ -39,6 +20,11 @@ describe('Community', () => {
$apollo: { $apollo: {
query: apolloQueryMock, query: apolloQueryMock,
mutate: apolloMutationMock, mutate: apolloMutationMock,
queries: {
OpenCreations: {
refetch: apolloRefetchMock,
},
},
}, },
$store: { $store: {
dispatch: mockStoreDispach, dispatch: mockStoreDispach,
@ -49,12 +35,17 @@ describe('Community', () => {
$i18n: { $i18n: {
locale: 'en', locale: 'en',
}, },
$router: {
push: jest.fn(),
},
$route: {
hash: 'my',
},
} }
const Wrapper = () => { const Wrapper = () => {
return mount(Community, { return mount(Community, {
localVue, localVue,
router,
mocks, mocks,
}) })
} }
@ -207,10 +198,7 @@ describe('Community', () => {
}) })
it('verifies the login (to get the new creations available)', () => { it('verifies the login (to get the new creations available)', () => {
expect(apolloQueryMock).toBeCalledWith({ expect(apolloRefetchMock).toBeCalled()
query: verifyLogin,
fetchPolicy: 'network-only',
})
}) })
it('set all data to the default values)', () => { it('set all data to the default values)', () => {
@ -294,10 +282,7 @@ describe('Community', () => {
}) })
it('verifies the login (to get the new creations available)', () => { it('verifies the login (to get the new creations available)', () => {
expect(apolloQueryMock).toBeCalledWith({ expect(apolloRefetchMock).toBeCalled()
query: verifyLogin,
fetchPolicy: 'network-only',
})
}) })
it('set all data to the default values)', () => { it('set all data to the default values)', () => {
@ -376,10 +361,7 @@ describe('Community', () => {
}) })
it('verifies the login (to get the new creations available)', () => { it('verifies the login (to get the new creations available)', () => {
expect(apolloQueryMock).toBeCalledWith({ expect(apolloRefetchMock).toBeCalled()
query: verifyLogin,
fetchPolicy: 'network-only',
})
}) })
}) })

View File

@ -5,8 +5,8 @@
<b-tab no-body> <b-tab no-body>
<open-creations-amount <open-creations-amount
:minimalDate="minimalDate" :minimalDate="minimalDate"
:maxGddThisMonth="maxGddThisMonth" :maxGddLastMonth="maxForMonths[0]"
:maxGddLastMonth="maxGddLastMonth" :maxGddThisMonth="maxForMonths[1]"
/> />
<div class="mb-3"></div> <div class="mb-3"></div>
<contribution-form <contribution-form
@ -15,8 +15,8 @@
v-model="form" v-model="form"
:isThisMonth="isThisMonth" :isThisMonth="isThisMonth"
:minimalDate="minimalDate" :minimalDate="minimalDate"
:maxGddLastMonth="maxGddLastMonth" :maxGddLastMonth="maxForMonths[0]"
:maxGddThisMonth="maxGddThisMonth" :maxGddThisMonth="maxForMonths[1]"
/> />
</b-tab> </b-tab>
<b-tab no-body> <b-tab no-body>
@ -52,7 +52,7 @@ import OpenCreationsAmount from '@/components/Contributions/OpenCreationsAmount.
import ContributionForm from '@/components/Contributions/ContributionForm.vue' import ContributionForm from '@/components/Contributions/ContributionForm.vue'
import ContributionList from '@/components/Contributions/ContributionList.vue' import ContributionList from '@/components/Contributions/ContributionList.vue'
import { createContribution, updateContribution, deleteContribution } from '@/graphql/mutations' import { createContribution, updateContribution, deleteContribution } from '@/graphql/mutations'
import { listContributions, listAllContributions, verifyLogin } from '@/graphql/queries' import { listContributions, listAllContributions, openCreations } from '@/graphql/queries'
export default { export default {
name: 'Community', name: 'Community',
@ -82,6 +82,7 @@ export default {
}, },
updateAmount: '', updateAmount: '',
maximalDate: new Date(), maximalDate: new Date(),
openCreations: [],
} }
}, },
mounted() { mounted() {
@ -90,6 +91,23 @@ export default {
this.hashLink = this.$route.hash this.hashLink = this.$route.hash
}) })
}, },
apollo: {
OpenCreations: {
query() {
return openCreations
},
fetchPolicy: 'network-only',
variables() {
return {}
},
update({ openCreations }) {
this.openCreations = openCreations
},
error({ message }) {
this.toastError(message)
},
},
},
watch: { watch: {
$route(to, from) { $route(to, from) {
this.tabIndex = this.tabLinkHashes.findIndex((hashLink) => hashLink === to.hash) this.tabIndex = this.tabLinkHashes.findIndex((hashLink) => hashLink === to.hash)
@ -120,17 +138,20 @@ export default {
formDate.getMonth() === this.maximalDate.getMonth() formDate.getMonth() === this.maximalDate.getMonth()
) )
}, },
maxGddLastMonth() { amountToAdd() {
// when existing contribution is edited, the amount is added back on top of the amount // when existing contribution is edited, the amount is added back on top of the amount
return this.form.id && !this.isThisMonth if (this.form.id) return parseInt(this.updateAmount)
? parseInt(this.$store.state.creation[1]) + parseInt(this.updateAmount) return 0
: parseInt(this.$store.state.creation[1])
}, },
maxGddThisMonth() { maxForMonths() {
// when existing contribution is edited, the amount is added back on top of the amount const formDate = new Date(this.form.date)
return this.form.id && this.isThisMonth if (this.openCreations && this.openCreations.length)
? parseInt(this.$store.state.creation[2]) + parseInt(this.updateAmount) return this.openCreations.slice(1).map((creation) => {
: parseInt(this.$store.state.creation[2]) if (creation.year === formDate.getFullYear() && creation.month === formDate.getMonth())
return parseInt(creation.amount) + this.amountToAdd
return parseInt(creation.amount)
})
return [0, 0]
}, },
}, },
methods: { methods: {
@ -160,7 +181,7 @@ export default {
currentPage: this.currentPage, currentPage: this.currentPage,
pageSize: this.pageSize, pageSize: this.pageSize,
}) })
this.verifyLogin() this.$apollo.queries.OpenCreations.refetch()
}) })
.catch((err) => { .catch((err) => {
this.toastError(err.message) this.toastError(err.message)
@ -188,7 +209,7 @@ export default {
currentPage: this.currentPage, currentPage: this.currentPage,
pageSize: this.pageSize, pageSize: this.pageSize,
}) })
this.verifyLogin() this.$apollo.queries.OpenCreations.refetch()
}) })
.catch((err) => { .catch((err) => {
this.toastError(err.message) this.toastError(err.message)
@ -213,7 +234,7 @@ export default {
currentPage: this.currentPage, currentPage: this.currentPage,
pageSize: this.pageSize, pageSize: this.pageSize,
}) })
this.verifyLogin() this.$apollo.queries.OpenCreations.refetch()
}) })
.catch((err) => { .catch((err) => {
this.toastError(err.message) this.toastError(err.message)
@ -259,7 +280,7 @@ export default {
if (this.items.find((item) => item.state === 'IN_PROGRESS')) { if (this.items.find((item) => item.state === 'IN_PROGRESS')) {
this.tabIndex = 1 this.tabIndex = 1
if (this.$route.hash !== '#my') { if (this.$route.hash !== '#my') {
this.$router.push({ path: '#my' }) this.$router.push({ path: '/community#my' })
} }
this.toastInfo('Du hast eine Rückfrage auf eine Contribution. Bitte beantworte diese!') this.toastInfo('Du hast eine Rückfrage auf eine Contribution. Bitte beantworte diese!')
} }
@ -268,22 +289,6 @@ export default {
this.toastError(err.message) this.toastError(err.message)
}) })
}, },
verifyLogin() {
this.$apollo
.query({
query: verifyLogin,
fetchPolicy: 'network-only',
})
.then((result) => {
const {
data: { verifyLogin },
} = result
this.$store.dispatch('login', verifyLogin)
})
.catch(() => {
this.$emit('logout')
})
},
updateContributionForm(item) { updateContributionForm(item) {
this.form.id = item.id this.form.id = item.id
this.form.date = item.contributionDate this.form.date = item.contributionDate
@ -303,8 +308,6 @@ export default {
}, },
created() { 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.
this.verifyLogin()
this.updateListContributions({ this.updateListContributions({
currentPage: this.currentPage, currentPage: this.currentPage,
pageSize: this.pageSize, pageSize: this.pageSize,
@ -315,6 +318,7 @@ export default {
}) })
this.updateTransactions(0) this.updateTransactions(0)
this.tabIndex = 1 this.tabIndex = 1
this.$router.push({ path: '/community#my' })
}, },
} }
</script> </script>

View File

@ -13,7 +13,7 @@ const navigatorClipboardMock = jest.fn()
const localVue = global.localVue const localVue = global.localVue
describe.skip('Send', () => { describe('Send', () => {
let wrapper let wrapper
const propsData = { const propsData = {

View File

@ -47,9 +47,6 @@ export const mutations = {
hasElopage: (state, hasElopage) => { hasElopage: (state, hasElopage) => {
state.hasElopage = hasElopage state.hasElopage = hasElopage
}, },
creation: (state, creation) => {
state.creation = creation
},
hideAmountGDD: (state, hideAmountGDD) => { hideAmountGDD: (state, hideAmountGDD) => {
state.hideAmountGDD = !!hideAmountGDD state.hideAmountGDD = !!hideAmountGDD
}, },
@ -69,7 +66,6 @@ export const actions = {
commit('hasElopage', data.hasElopage) commit('hasElopage', data.hasElopage)
commit('publisherId', data.publisherId) commit('publisherId', data.publisherId)
commit('isAdmin', data.isAdmin) commit('isAdmin', data.isAdmin)
commit('creation', data.creation)
commit('hideAmountGDD', data.hideAmountGDD) commit('hideAmountGDD', data.hideAmountGDD)
commit('hideAmountGDT', data.hideAmountGDT) commit('hideAmountGDT', data.hideAmountGDT)
}, },
@ -83,7 +79,6 @@ export const actions = {
commit('hasElopage', false) commit('hasElopage', false)
commit('publisherId', null) commit('publisherId', null)
commit('isAdmin', false) commit('isAdmin', false)
commit('creation', null)
commit('hideAmountGDD', false) commit('hideAmountGDD', false)
commit('hideAmountGDT', true) commit('hideAmountGDT', true)
localStorage.clear() localStorage.clear()
@ -111,7 +106,6 @@ try {
newsletterState: null, newsletterState: null,
hasElopage: false, hasElopage: false,
publisherId: null, publisherId: null,
creation: null,
hideAmountGDD: null, hideAmountGDD: null,
hideAmountGDT: null, hideAmountGDT: null,
}, },

View File

@ -30,7 +30,6 @@ const {
publisherId, publisherId,
isAdmin, isAdmin,
hasElopage, hasElopage,
creation,
hideAmountGDD, hideAmountGDD,
hideAmountGDT, hideAmountGDT,
} = mutations } = mutations
@ -143,14 +142,6 @@ describe('Vuex store', () => {
}) })
}) })
describe('creation', () => {
it('sets the state of creation', () => {
const state = { creation: null }
creation(state, true)
expect(state.creation).toEqual(true)
})
})
describe('hideAmountGDD', () => { describe('hideAmountGDD', () => {
it('sets the state of hideAmountGDD', () => { it('sets the state of hideAmountGDD', () => {
const state = { hideAmountGDD: false } const state = { hideAmountGDD: false }
@ -183,14 +174,13 @@ describe('Vuex store', () => {
hasElopage: false, hasElopage: false,
publisherId: 1234, publisherId: 1234,
isAdmin: true, isAdmin: true,
creation: ['1000', '1000', '1000'],
hideAmountGDD: false, hideAmountGDD: false,
hideAmountGDT: true, hideAmountGDT: true,
} }
it('calls eleven commits', () => { it('calls eleven commits', () => {
login({ commit, state }, commitedData) login({ commit, state }, commitedData)
expect(commit).toHaveBeenCalledTimes(11) expect(commit).toHaveBeenCalledTimes(10)
}) })
it('commits email', () => { it('commits email', () => {
@ -233,19 +223,14 @@ describe('Vuex store', () => {
expect(commit).toHaveBeenNthCalledWith(8, 'isAdmin', true) expect(commit).toHaveBeenNthCalledWith(8, 'isAdmin', true)
}) })
it('commits creation', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(9, 'creation', ['1000', '1000', '1000'])
})
it('commits hideAmountGDD', () => { it('commits hideAmountGDD', () => {
login({ commit, state }, commitedData) login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(10, 'hideAmountGDD', false) expect(commit).toHaveBeenNthCalledWith(9, 'hideAmountGDD', false)
}) })
it('commits hideAmountGDT', () => { it('commits hideAmountGDT', () => {
login({ commit, state }, commitedData) login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(11, 'hideAmountGDT', true) expect(commit).toHaveBeenNthCalledWith(10, 'hideAmountGDT', true)
}) })
}) })
@ -255,7 +240,7 @@ describe('Vuex store', () => {
it('calls eleven commits', () => { it('calls eleven commits', () => {
logout({ commit, state }) logout({ commit, state })
expect(commit).toHaveBeenCalledTimes(11) expect(commit).toHaveBeenCalledTimes(10)
}) })
it('commits token', () => { it('commits token', () => {
@ -298,19 +283,14 @@ describe('Vuex store', () => {
expect(commit).toHaveBeenNthCalledWith(8, 'isAdmin', false) expect(commit).toHaveBeenNthCalledWith(8, 'isAdmin', false)
}) })
it('commits creation', () => {
logout({ commit, state })
expect(commit).toHaveBeenNthCalledWith(9, 'creation', null)
})
it('commits hideAmountGDD', () => { it('commits hideAmountGDD', () => {
logout({ commit, state }) logout({ commit, state })
expect(commit).toHaveBeenNthCalledWith(10, 'hideAmountGDD', false) expect(commit).toHaveBeenNthCalledWith(9, 'hideAmountGDD', false)
}) })
it('commits hideAmountGDT', () => { it('commits hideAmountGDT', () => {
logout({ commit, state }) logout({ commit, state })
expect(commit).toHaveBeenNthCalledWith(11, 'hideAmountGDT', true) expect(commit).toHaveBeenNthCalledWith(10, 'hideAmountGDT', true)
}) })
// how to get this working? // how to get this working?
it.skip('calls localStorage.clear()', () => { it.skip('calls localStorage.clear()', () => {