mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge branch 'master' into 2010-send-email-to-transaction-link-sender
This commit is contained in:
commit
37d9ef1c30
47
CHANGELOG.md
47
CHANGELOG.md
@ -4,8 +4,55 @@ All notable changes to this project will be documented in this file. Dates are d
|
||||
|
||||
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
|
||||
#### [1.11.0](https://github.com/gradido/gradido/compare/1.10.1...1.11.0)
|
||||
|
||||
- Fix navbar community item [`#2102`](https://github.com/gradido/gradido/pull/2102)
|
||||
- Add validation date info to copied text after transaction link creation [`#2101`](https://github.com/gradido/gradido/pull/2101)
|
||||
- Remove member area from menu (desktop and mobile), when user has no elopage account [`#2099`](https://github.com/gradido/gradido/pull/2099)
|
||||
- fix: Use Inner Join for Contribution and User [`#2100`](https://github.com/gradido/gradido/pull/2100)
|
||||
- Enable copying the link, username, amount, and memo text after transaction link creation [`#2098`](https://github.com/gradido/gradido/pull/2098)
|
||||
- feat: Insert Missing Contributions Migration [`#2053`](https://github.com/gradido/gradido/pull/2053)
|
||||
- [Bug] Wallet improvments for Contributions [`#2090`](https://github.com/gradido/gradido/pull/2090)
|
||||
- [Fix] Add createdAt & contributionDate to ContributionListItems [`#2093`](https://github.com/gradido/gradido/pull/2093)
|
||||
- fix: 🍰 Reset Amount In Contribution Form And Write A Test [`#2086`](https://github.com/gradido/gradido/pull/2086)
|
||||
- [Feat] Replace logic to validation-provider. [`#2088`](https://github.com/gradido/gradido/pull/2088)
|
||||
- fix: Add Confirm Dialog on Delete Contribution [`#2087`](https://github.com/gradido/gradido/pull/2087)
|
||||
- fix: Admin Cannot Edit User Contribution [`#2085`](https://github.com/gradido/gradido/pull/2085)
|
||||
- fix: Update contribution_date when Moved by Seed [`#2083`](https://github.com/gradido/gradido/pull/2083)
|
||||
- chore: 🍰 Provide Volume For Backend Log-Files In Docker [`#2067`](https://github.com/gradido/gradido/pull/2067)
|
||||
- feat: 🍰 Community Contribution Site And Form [`#2042`](https://github.com/gradido/gradido/pull/2042)
|
||||
- [Refactor] Move MEMO_MIN_CHARS and MEMO_MAX_CHARS to const file [`#2082`](https://github.com/gradido/gradido/pull/2082)
|
||||
- Fix: Test memo length on createContribution & updateContribution [`#2080`](https://github.com/gradido/gradido/pull/2080)
|
||||
- chore: 🍰 Change `image` Entries In Docker Compose Files And Get Apple M1 Running [`#2050`](https://github.com/gradido/gradido/pull/2050)
|
||||
- fix: Windows 0D 0A Linebreaks to Unix 0A [`#2064`](https://github.com/gradido/gradido/pull/2064)
|
||||
- Add contributionDate to the Contribution object. [`#2066`](https://github.com/gradido/gradido/pull/2066)
|
||||
- fix: Add Contributions to User [`#2062`](https://github.com/gradido/gradido/pull/2062)
|
||||
- Feat: ContributionResolver - delete mutation [`#2035`](https://github.com/gradido/gradido/pull/2035)
|
||||
- Fix: Add count to list contributions [`#2061`](https://github.com/gradido/gradido/pull/2061)
|
||||
- docu: Explain how `.env` Files are Working [`#2022`](https://github.com/gradido/gradido/pull/2022)
|
||||
- [WIP] 1794 feature event protocol 1 implement the basics of the business event protocol [`#1997`](https://github.com/gradido/gradido/pull/1997)
|
||||
- docs: 🍰 Document The Setup Of The GraphQL Playground [`#2060`](https://github.com/gradido/gradido/pull/2060)
|
||||
- Feat: List all contribution [`#2057`](https://github.com/gradido/gradido/pull/2057)
|
||||
- feat: Do not log IntrospectionQuery from Query Browser [`#2059`](https://github.com/gradido/gradido/pull/2059)
|
||||
- Feat: Add confirmedBy and confirmedAt for the contribution query. [`#2052`](https://github.com/gradido/gradido/pull/2052)
|
||||
- Add open creations to webapp [`#2048`](https://github.com/gradido/gradido/pull/2048)
|
||||
- Prevent session expiration modal from displaying negative seconds, when session is expired for more than 0 seconds [`#2054`](https://github.com/gradido/gradido/pull/2054)
|
||||
- feat: mutation contribution update [`#2032`](https://github.com/gradido/gradido/pull/2032)
|
||||
- feat: Login Returns Open Creations for User [`#2046`](https://github.com/gradido/gradido/pull/2046)
|
||||
- Migrate transaction to valid dataset for gradido node [`#2029`](https://github.com/gradido/gradido/pull/2029)
|
||||
- feat: implement contribution list query [`#2031`](https://github.com/gradido/gradido/pull/2031)
|
||||
- add code for moving user creation date if transaction before exist [`#2034`](https://github.com/gradido/gradido/pull/2034)
|
||||
- change text from page [`#2037`](https://github.com/gradido/gradido/pull/2037)
|
||||
- Transaction link: copy link, text and more [`#2030`](https://github.com/gradido/gradido/pull/2030)
|
||||
- change welcome in community text [`#2025`](https://github.com/gradido/gradido/pull/2025)
|
||||
- changed link color in navbar and language switch [`#2024`](https://github.com/gradido/gradido/pull/2024)
|
||||
- feat: ContributionResolver - createContribution [`#2009`](https://github.com/gradido/gradido/pull/2009)
|
||||
|
||||
#### [1.10.1](https://github.com/gradido/gradido/compare/1.10.0...1.10.1)
|
||||
|
||||
> 30 June 2022
|
||||
|
||||
- release: 1.10.1 [`#2021`](https://github.com/gradido/gradido/pull/2021)
|
||||
- automatic session logout with info modal [`#2001`](https://github.com/gradido/gradido/pull/2001)
|
||||
- 1910 separate text for the slideshow images. [`#1998`](https://github.com/gradido/gradido/pull/1998)
|
||||
- Origin/1921 additional parameter checks for createContributionLinks [`#1996`](https://github.com/gradido/gradido/pull/1996)
|
||||
|
||||
44
DOCKER_MORE_CLOSELY.md
Normal file
44
DOCKER_MORE_CLOSELY.md
Normal file
@ -0,0 +1,44 @@
|
||||
# Docker More Closely
|
||||
|
||||
## Apple M1 Platform
|
||||
|
||||
***Attention:** For using Docker commands in Apple M1 environments!*
|
||||
|
||||
### Enviroment Variable For Apple M1 Platform
|
||||
|
||||
To set the Docker platform environment variable in your terminal tab, run:
|
||||
|
||||
```bash
|
||||
# set env variable for your shell
|
||||
$ export DOCKER_DEFAULT_PLATFORM=linux/amd64
|
||||
```
|
||||
|
||||
### Docker Compose Override File For Apple M1 Platform
|
||||
|
||||
For Docker compose `up` or `build` commands, you can use our Apple M1 override file that specifies the M1 platform:
|
||||
|
||||
```bash
|
||||
# in main folder
|
||||
|
||||
# for development
|
||||
$ docker compose -f docker-compose.yml -f docker-compose.override.yml -f docker-compose.apple-m1.override.yml up
|
||||
|
||||
# for production
|
||||
$ docker compose -f docker-compose.yml -f docker-compose.apple-m1.override.yml up
|
||||
```
|
||||
|
||||
## Analysing Docker Builds
|
||||
|
||||
To analyze a Docker build, there is a wonderful tool called [dive](https://github.com/wagoodman/dive). Please sponsor if you're using it!
|
||||
|
||||
The `dive build` command is exactly the right one to fulfill what we are looking for.
|
||||
We can use it just like the `docker build` command and get an analysis afterwards.
|
||||
|
||||
So, in our main folder, we use it in the following way:
|
||||
|
||||
```bash
|
||||
# in main folder
|
||||
$ dive build --target <layer-name> -t "gradido/<app-name>:local-<layer-name>" <app-folder-name-or-dot>/
|
||||
```
|
||||
|
||||
For the specific applications, see our [publish.yml](.github/workflows/publish.yml).
|
||||
@ -3,7 +3,7 @@
|
||||
"description": "Administraion Interface for Gradido",
|
||||
"main": "index.js",
|
||||
"author": "Moriz Wahl",
|
||||
"version": "1.10.1",
|
||||
"version": "1.11.0",
|
||||
"license": "Apache-2.0",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
|
||||
@ -28,7 +28,7 @@ const propsData = {
|
||||
amount: 210,
|
||||
memo: 'Aktives Grundeinkommen für Januar 2022',
|
||||
date: '2022-01-01T00:00:00.000Z',
|
||||
moderator: 1,
|
||||
moderator: null,
|
||||
creation: [790, 1000, 1000],
|
||||
__typename: 'PendingCreation',
|
||||
},
|
||||
@ -66,7 +66,7 @@ const propsData = {
|
||||
},
|
||||
},
|
||||
{ key: 'moderator', label: 'moderator' },
|
||||
{ key: 'edit_creation', label: 'edit' },
|
||||
{ key: 'editCreation', label: 'edit' },
|
||||
{ key: 'confirm', label: 'save' },
|
||||
],
|
||||
toggleDetails: false,
|
||||
@ -113,6 +113,10 @@ describe('OpenCreationsTable', () => {
|
||||
expect(wrapper.findAll('tr').at(1).find('.bi-pencil-square').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('has no button.bi-pencil-square for user contribution ', () => {
|
||||
expect(wrapper.findAll('tr').at(2).find('.bi-pencil-square').exists()).toBe(false)
|
||||
})
|
||||
|
||||
describe('show edit details', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.findAll('tr').at(1).find('.bi-pencil-square').trigger('click')
|
||||
|
||||
@ -11,8 +11,14 @@
|
||||
<b-icon icon="x" variant="light"></b-icon>
|
||||
</b-button>
|
||||
</template>
|
||||
<template #cell(edit_creation)="row">
|
||||
<b-button variant="info" size="md" @click="rowToggleDetails(row, 0)" class="mr-2">
|
||||
<template #cell(editCreation)="row">
|
||||
<b-button
|
||||
v-if="row.item.moderator"
|
||||
variant="info"
|
||||
size="md"
|
||||
@click="rowToggleDetails(row, 0)"
|
||||
class="mr-2"
|
||||
>
|
||||
<b-icon :icon="row.detailsShowing ? 'x' : 'pencil-square'" aria-label="Help"></b-icon>
|
||||
</b-button>
|
||||
</template>
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
:per-page="perPage"
|
||||
:total-rows="rows"
|
||||
align="center"
|
||||
:hide-ellipsis="true"
|
||||
></b-pagination>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -35,6 +35,7 @@
|
||||
"creation_form": {
|
||||
"creation_failed": "Ausstehende Schöpfung für {email} konnte nicht erzeugt werden.",
|
||||
"creation_for": "Aktives Grundeinkommen für",
|
||||
"deleteNow": "Möchtest du diesen Beitrag zur Gemeinschaft wirklich löschen?",
|
||||
"enter_text": "Text eintragen",
|
||||
"form": "Schöpfungsformular",
|
||||
"min_characters": "Mindestens 10 Zeichen eingeben",
|
||||
|
||||
@ -35,6 +35,7 @@
|
||||
"creation_form": {
|
||||
"creation_failed": "Could not create pending creation for {email}",
|
||||
"creation_for": "Active Basic Income for",
|
||||
"deleteNow": "Do you really want to delete this contribution to the community?",
|
||||
"enter_text": "Enter text",
|
||||
"form": "Creation form",
|
||||
"min_characters": "Enter at least 10 characters",
|
||||
|
||||
@ -29,6 +29,7 @@
|
||||
per-page="perPage"
|
||||
:total-rows="rows"
|
||||
align="center"
|
||||
:hide-ellipsis="true"
|
||||
></b-pagination>
|
||||
</b-col>
|
||||
<b-col cols="12" lg="6" class="shadow p-3 mb-5 rounded bg-info">
|
||||
|
||||
@ -18,7 +18,7 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
|
||||
amount: 500,
|
||||
memo: 'Danke für alles',
|
||||
date: new Date(),
|
||||
moderator: 0,
|
||||
moderator: 2,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
@ -28,7 +28,7 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
|
||||
amount: 1000000,
|
||||
memo: 'Gut Ergattert',
|
||||
date: new Date(),
|
||||
moderator: 0,
|
||||
moderator: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -80,28 +80,54 @@ describe('CreationConfirm', () => {
|
||||
})
|
||||
|
||||
describe('remove creation with success', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.findAll('tr').at(1).findAll('button').at(0).trigger('click')
|
||||
})
|
||||
let spy
|
||||
|
||||
it('calls the adminDeleteContribution mutation', () => {
|
||||
expect(apolloMutateMock).toBeCalledWith({
|
||||
mutation: adminDeleteContribution,
|
||||
variables: { id: 1 },
|
||||
describe('admin confirms deletion', () => {
|
||||
beforeEach(async () => {
|
||||
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
|
||||
spy.mockImplementation(() => Promise.resolve('some value'))
|
||||
await wrapper.findAll('tr').at(1).findAll('button').at(0).trigger('click')
|
||||
})
|
||||
|
||||
it('opens a modal', () => {
|
||||
expect(spy).toBeCalled()
|
||||
})
|
||||
|
||||
it('calls the adminDeleteContribution mutation', () => {
|
||||
expect(apolloMutateMock).toBeCalledWith({
|
||||
mutation: adminDeleteContribution,
|
||||
variables: { id: 1 },
|
||||
})
|
||||
})
|
||||
|
||||
it('commits openCreationsMinus to store', () => {
|
||||
expect(storeCommitMock).toBeCalledWith('openCreationsMinus', 1)
|
||||
})
|
||||
|
||||
it('toasts a success message', () => {
|
||||
expect(toastSuccessSpy).toBeCalledWith('creation_form.toasted_delete')
|
||||
})
|
||||
})
|
||||
|
||||
it('commits openCreationsMinus to store', () => {
|
||||
expect(storeCommitMock).toBeCalledWith('openCreationsMinus', 1)
|
||||
})
|
||||
describe('admin cancels deletion', () => {
|
||||
beforeEach(async () => {
|
||||
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
|
||||
spy.mockImplementation(() => Promise.resolve(false))
|
||||
await wrapper.findAll('tr').at(1).findAll('button').at(0).trigger('click')
|
||||
})
|
||||
|
||||
it('toasts a success message', () => {
|
||||
expect(toastSuccessSpy).toBeCalledWith('creation_form.toasted_delete')
|
||||
it('does not call the adminDeleteContribution mutation', () => {
|
||||
expect(apolloMutateMock).not.toBeCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('remove creation with error', () => {
|
||||
let spy
|
||||
|
||||
beforeEach(async () => {
|
||||
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
|
||||
spy.mockImplementation(() => Promise.resolve('some value'))
|
||||
apolloMutateMock.mockRejectedValue({ message: 'Ouchhh!' })
|
||||
await wrapper.findAll('tr').at(1).findAll('button').at(0).trigger('click')
|
||||
})
|
||||
|
||||
@ -34,20 +34,23 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
removeCreation(item) {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: adminDeleteContribution,
|
||||
variables: {
|
||||
id: item.id,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
this.updatePendingCreations(item.id)
|
||||
this.toastSuccess(this.$t('creation_form.toasted_delete'))
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toastError(error.message)
|
||||
})
|
||||
this.$bvModal.msgBoxConfirm(this.$t('creation_form.deleteNow')).then(async (value) => {
|
||||
if (value)
|
||||
await this.$apollo
|
||||
.mutate({
|
||||
mutation: adminDeleteContribution,
|
||||
variables: {
|
||||
id: item.id,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
this.updatePendingCreations(item.id)
|
||||
this.toastSuccess(this.$t('creation_form.toasted_delete'))
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toastError(error.message)
|
||||
})
|
||||
})
|
||||
},
|
||||
confirmCreation() {
|
||||
this.$apollo
|
||||
@ -114,7 +117,7 @@ export default {
|
||||
},
|
||||
},
|
||||
{ key: 'moderator', label: this.$t('moderator') },
|
||||
{ key: 'edit_creation', label: this.$t('edit') },
|
||||
{ key: 'editCreation', label: this.$t('edit') },
|
||||
{ key: 'confirm', label: this.$t('save') },
|
||||
]
|
||||
},
|
||||
|
||||
@ -52,6 +52,7 @@
|
||||
per-page="perPage"
|
||||
:total-rows="rows"
|
||||
align="center"
|
||||
:hide-ellipsis="true"
|
||||
></b-pagination>
|
||||
<div></div>
|
||||
</div>
|
||||
|
||||
@ -1,60 +1,60 @@
|
||||
CONFIG_VERSION=v9.2022-07-07
|
||||
|
||||
# Server
|
||||
PORT=4000
|
||||
JWT_SECRET=secret123
|
||||
JWT_EXPIRES_IN=10m
|
||||
GRAPHIQL=false
|
||||
GDT_API_URL=https://gdt.gradido.net
|
||||
|
||||
# Database
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_USER=root
|
||||
DB_PASSWORD=
|
||||
DB_DATABASE=gradido_community
|
||||
TYPEORM_LOGGING_RELATIVE_PATH=typeorm.backend.log
|
||||
|
||||
# Klicktipp
|
||||
KLICKTIPP=false
|
||||
KLICKTTIPP_API_URL=https://api.klicktipp.com
|
||||
KLICKTIPP_USER=gradido_test
|
||||
KLICKTIPP_PASSWORD=secret321
|
||||
KLICKTIPP_APIKEY_DE=SomeFakeKeyDE
|
||||
KLICKTIPP_APIKEY_EN=SomeFakeKeyEN
|
||||
|
||||
# Community
|
||||
COMMUNITY_NAME=Gradido Entwicklung
|
||||
COMMUNITY_URL=http://localhost/
|
||||
COMMUNITY_REGISTER_URL=http://localhost/register
|
||||
COMMUNITY_REDEEM_URL=http://localhost/redeem/{code}
|
||||
COMMUNITY_REDEEM_CONTRIBUTION_URL=http://localhost/redeem/CL-{code}
|
||||
COMMUNITY_DESCRIPTION=Die lokale Entwicklungsumgebung von Gradido.
|
||||
|
||||
# Login Server
|
||||
LOGIN_APP_SECRET=21ffbbc616fe
|
||||
LOGIN_SERVER_KEY=a51ef8ac7ef1abf162fb7a65261acd7a
|
||||
|
||||
# EMail
|
||||
EMAIL=false
|
||||
EMAIL_USERNAME=gradido_email
|
||||
EMAIL_SENDER=info@gradido.net
|
||||
EMAIL_PASSWORD=xxx
|
||||
EMAIL_SMTP_URL=gmail.com
|
||||
EMAIL_SMTP_PORT=587
|
||||
EMAIL_LINK_VERIFICATION=http://localhost/checkEmail/{optin}{code}
|
||||
EMAIL_LINK_SETPASSWORD=http://localhost/reset-password/{optin}
|
||||
EMAIL_LINK_FORGOTPASSWORD=http://localhost/forgot-password
|
||||
EMAIL_LINK_OVERVIEW=http://localhost/overview
|
||||
EMAIL_CODE_VALID_TIME=1440
|
||||
EMAIL_CODE_REQUEST_TIME=10
|
||||
|
||||
# Webhook
|
||||
WEBHOOK_ELOPAGE_SECRET=secret
|
||||
|
||||
# EventProtocol
|
||||
EVENT_PROTOCOL_DISABLED=false
|
||||
|
||||
# SET LOG LEVEL AS NEEDED IN YOUR .ENV
|
||||
# POSSIBLE VALUES: all | trace | debug | info | warn | error | fatal
|
||||
# LOG_LEVEL=info
|
||||
CONFIG_VERSION=v9.2022-07-07
|
||||
|
||||
# Server
|
||||
PORT=4000
|
||||
JWT_SECRET=secret123
|
||||
JWT_EXPIRES_IN=10m
|
||||
GRAPHIQL=false
|
||||
GDT_API_URL=https://gdt.gradido.net
|
||||
|
||||
# Database
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_USER=root
|
||||
DB_PASSWORD=
|
||||
DB_DATABASE=gradido_community
|
||||
TYPEORM_LOGGING_RELATIVE_PATH=typeorm.backend.log
|
||||
|
||||
# Klicktipp
|
||||
KLICKTIPP=false
|
||||
KLICKTTIPP_API_URL=https://api.klicktipp.com
|
||||
KLICKTIPP_USER=gradido_test
|
||||
KLICKTIPP_PASSWORD=secret321
|
||||
KLICKTIPP_APIKEY_DE=SomeFakeKeyDE
|
||||
KLICKTIPP_APIKEY_EN=SomeFakeKeyEN
|
||||
|
||||
# Community
|
||||
COMMUNITY_NAME=Gradido Entwicklung
|
||||
COMMUNITY_URL=http://localhost/
|
||||
COMMUNITY_REGISTER_URL=http://localhost/register
|
||||
COMMUNITY_REDEEM_URL=http://localhost/redeem/{code}
|
||||
COMMUNITY_REDEEM_CONTRIBUTION_URL=http://localhost/redeem/CL-{code}
|
||||
COMMUNITY_DESCRIPTION=Die lokale Entwicklungsumgebung von Gradido.
|
||||
|
||||
# Login Server
|
||||
LOGIN_APP_SECRET=21ffbbc616fe
|
||||
LOGIN_SERVER_KEY=a51ef8ac7ef1abf162fb7a65261acd7a
|
||||
|
||||
# EMail
|
||||
EMAIL=false
|
||||
EMAIL_USERNAME=gradido_email
|
||||
EMAIL_SENDER=info@gradido.net
|
||||
EMAIL_PASSWORD=xxx
|
||||
EMAIL_SMTP_URL=gmail.com
|
||||
EMAIL_SMTP_PORT=587
|
||||
EMAIL_LINK_VERIFICATION=http://localhost/checkEmail/{optin}{code}
|
||||
EMAIL_LINK_SETPASSWORD=http://localhost/reset-password/{optin}
|
||||
EMAIL_LINK_FORGOTPASSWORD=http://localhost/forgot-password
|
||||
EMAIL_LINK_OVERVIEW=http://localhost/overview
|
||||
EMAIL_CODE_VALID_TIME=1440
|
||||
EMAIL_CODE_REQUEST_TIME=10
|
||||
|
||||
# Webhook
|
||||
WEBHOOK_ELOPAGE_SECRET=secret
|
||||
|
||||
# EventProtocol
|
||||
EVENT_PROTOCOL_DISABLED=false
|
||||
|
||||
# SET LOG LEVEL AS NEEDED IN YOUR .ENV
|
||||
# POSSIBLE VALUES: all | trace | debug | info | warn | error | fatal
|
||||
# LOG_LEVEL=info
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gradido-backend",
|
||||
"version": "1.10.1",
|
||||
"version": "1.11.0",
|
||||
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
|
||||
"main": "src/index.ts",
|
||||
"repository": "https://github.com/gradido/gradido/backend",
|
||||
|
||||
@ -26,9 +26,11 @@ export enum RIGHTS {
|
||||
LIST_TRANSACTION_LINKS = 'LIST_TRANSACTION_LINKS',
|
||||
GDT_BALANCE = 'GDT_BALANCE',
|
||||
CREATE_CONTRIBUTION = 'CREATE_CONTRIBUTION',
|
||||
DELETE_CONTRIBUTION = 'DELETE_CONTRIBUTION',
|
||||
LIST_CONTRIBUTIONS = 'LIST_CONTRIBUTIONS',
|
||||
LIST_ALL_CONTRIBUTIONS = 'LIST_ALL_CONTRIBUTIONS',
|
||||
UPDATE_CONTRIBUTION = 'UPDATE_CONTRIBUTION',
|
||||
LIST_CONTRIBUTION_LINKS = 'LIST_CONTRIBUTION_LINKS',
|
||||
// Admin
|
||||
SEARCH_USERS = 'SEARCH_USERS',
|
||||
SET_USER_ROLE = 'SET_USER_ROLE',
|
||||
@ -44,7 +46,6 @@ export enum RIGHTS {
|
||||
CREATION_TRANSACTION_LIST = 'CREATION_TRANSACTION_LIST',
|
||||
LIST_TRANSACTION_LINKS_ADMIN = 'LIST_TRANSACTION_LINKS_ADMIN',
|
||||
CREATE_CONTRIBUTION_LINK = 'CREATE_CONTRIBUTION_LINK',
|
||||
LIST_CONTRIBUTION_LINKS = 'LIST_CONTRIBUTION_LINKS',
|
||||
DELETE_CONTRIBUTION_LINK = 'DELETE_CONTRIBUTION_LINK',
|
||||
UPDATE_CONTRIBUTION_LINK = 'UPDATE_CONTRIBUTION_LINK',
|
||||
}
|
||||
|
||||
@ -24,9 +24,11 @@ export const ROLE_USER = new Role('user', [
|
||||
RIGHTS.LIST_TRANSACTION_LINKS,
|
||||
RIGHTS.GDT_BALANCE,
|
||||
RIGHTS.CREATE_CONTRIBUTION,
|
||||
RIGHTS.DELETE_CONTRIBUTION,
|
||||
RIGHTS.LIST_CONTRIBUTIONS,
|
||||
RIGHTS.LIST_ALL_CONTRIBUTIONS,
|
||||
RIGHTS.UPDATE_CONTRIBUTION,
|
||||
RIGHTS.LIST_CONTRIBUTION_LINKS,
|
||||
])
|
||||
export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights
|
||||
|
||||
|
||||
@ -1,129 +1,129 @@
|
||||
// ATTENTION: DO NOT PUT ANY SECRETS IN HERE (or the .env)
|
||||
|
||||
import dotenv from 'dotenv'
|
||||
import Decimal from 'decimal.js-light'
|
||||
dotenv.config()
|
||||
|
||||
Decimal.set({
|
||||
precision: 25,
|
||||
rounding: Decimal.ROUND_HALF_UP,
|
||||
})
|
||||
|
||||
const constants = {
|
||||
DB_VERSION: '0043-add_event_protocol_table',
|
||||
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
|
||||
LOG4JS_CONFIG: 'log4js-config.json',
|
||||
// default log level on production should be info
|
||||
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
|
||||
CONFIG_VERSION: {
|
||||
DEFAULT: 'DEFAULT',
|
||||
EXPECTED: 'v9.2022-07-07',
|
||||
CURRENT: '',
|
||||
},
|
||||
}
|
||||
|
||||
const server = {
|
||||
PORT: process.env.PORT || 4000,
|
||||
JWT_SECRET: process.env.JWT_SECRET || 'secret123',
|
||||
JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN || '10m',
|
||||
GRAPHIQL: process.env.GRAPHIQL === 'true' || false,
|
||||
GDT_API_URL: process.env.GDT_API_URL || 'https://gdt.gradido.net',
|
||||
PRODUCTION: process.env.NODE_ENV === 'production' || false,
|
||||
}
|
||||
|
||||
const database = {
|
||||
DB_HOST: process.env.DB_HOST || 'localhost',
|
||||
DB_PORT: process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 3306,
|
||||
DB_USER: process.env.DB_USER || 'root',
|
||||
DB_PASSWORD: process.env.DB_PASSWORD || '',
|
||||
DB_DATABASE: process.env.DB_DATABASE || 'gradido_community',
|
||||
TYPEORM_LOGGING_RELATIVE_PATH: process.env.TYPEORM_LOGGING_RELATIVE_PATH || 'typeorm.backend.log',
|
||||
}
|
||||
|
||||
const klicktipp = {
|
||||
KLICKTIPP: process.env.KLICKTIPP === 'true' || false,
|
||||
KLICKTTIPP_API_URL: process.env.KLICKTIPP_API_URL || 'https://api.klicktipp.com',
|
||||
KLICKTIPP_USER: process.env.KLICKTIPP_USER || 'gradido_test',
|
||||
KLICKTIPP_PASSWORD: process.env.KLICKTIPP_PASSWORD || 'secret321',
|
||||
KLICKTIPP_APIKEY_DE: process.env.KLICKTIPP_APIKEY_DE || 'SomeFakeKeyDE',
|
||||
KLICKTIPP_APIKEY_EN: process.env.KLICKTIPP_APIKEY_EN || 'SomeFakeKeyEN',
|
||||
}
|
||||
|
||||
const community = {
|
||||
COMMUNITY_NAME: process.env.COMMUNITY_NAME || 'Gradido Entwicklung',
|
||||
COMMUNITY_URL: process.env.COMMUNITY_URL || 'http://localhost/',
|
||||
COMMUNITY_REGISTER_URL: process.env.COMMUNITY_REGISTER_URL || 'http://localhost/register',
|
||||
COMMUNITY_REDEEM_URL: process.env.COMMUNITY_REDEEM_URL || 'http://localhost/redeem/{code}',
|
||||
COMMUNITY_REDEEM_CONTRIBUTION_URL:
|
||||
process.env.COMMUNITY_REDEEM_CONTRIBUTION_URL || 'http://localhost/redeem/CL-{code}',
|
||||
COMMUNITY_DESCRIPTION:
|
||||
process.env.COMMUNITY_DESCRIPTION || 'Die lokale Entwicklungsumgebung von Gradido.',
|
||||
}
|
||||
|
||||
const loginServer = {
|
||||
LOGIN_APP_SECRET: process.env.LOGIN_APP_SECRET || '21ffbbc616fe',
|
||||
LOGIN_SERVER_KEY: process.env.LOGIN_SERVER_KEY || 'a51ef8ac7ef1abf162fb7a65261acd7a',
|
||||
}
|
||||
|
||||
const email = {
|
||||
EMAIL: process.env.EMAIL === 'true' || false,
|
||||
EMAIL_USERNAME: process.env.EMAIL_USERNAME || 'gradido_email',
|
||||
EMAIL_SENDER: process.env.EMAIL_SENDER || 'info@gradido.net',
|
||||
EMAIL_PASSWORD: process.env.EMAIL_PASSWORD || 'xxx',
|
||||
EMAIL_SMTP_URL: process.env.EMAIL_SMTP_URL || 'gmail.com',
|
||||
EMAIL_SMTP_PORT: process.env.EMAIL_SMTP_PORT || '587',
|
||||
EMAIL_LINK_VERIFICATION:
|
||||
process.env.EMAIL_LINK_VERIFICATION || 'http://localhost/checkEmail/{optin}{code}',
|
||||
EMAIL_LINK_SETPASSWORD:
|
||||
process.env.EMAIL_LINK_SETPASSWORD || 'http://localhost/reset-password/{optin}',
|
||||
EMAIL_LINK_FORGOTPASSWORD:
|
||||
process.env.EMAIL_LINK_FORGOTPASSWORD || 'http://localhost/forgot-password',
|
||||
EMAIL_LINK_OVERVIEW: process.env.EMAIL_LINK_OVERVIEW || 'http://localhost/overview',
|
||||
// time in minutes a optin code is valid
|
||||
EMAIL_CODE_VALID_TIME: process.env.EMAIL_CODE_VALID_TIME
|
||||
? parseInt(process.env.EMAIL_CODE_VALID_TIME) || 1440
|
||||
: 1440,
|
||||
// time in minutes that must pass to request a new optin code
|
||||
EMAIL_CODE_REQUEST_TIME: process.env.EMAIL_CODE_REQUEST_TIME
|
||||
? parseInt(process.env.EMAIL_CODE_REQUEST_TIME) || 10
|
||||
: 10,
|
||||
}
|
||||
|
||||
const webhook = {
|
||||
// Elopage
|
||||
WEBHOOK_ELOPAGE_SECRET: process.env.WEBHOOK_ELOPAGE_SECRET || 'secret',
|
||||
}
|
||||
|
||||
const eventProtocol = {
|
||||
// global switch to enable writing of EventProtocol-Entries
|
||||
EVENT_PROTOCOL_DISABLED: process.env.EVENT_PROTOCOL_DISABLED === 'true' || false,
|
||||
}
|
||||
|
||||
// This is needed by graphql-directive-auth
|
||||
process.env.APP_SECRET = server.JWT_SECRET
|
||||
|
||||
// Check config version
|
||||
constants.CONFIG_VERSION.CURRENT = process.env.CONFIG_VERSION || constants.CONFIG_VERSION.DEFAULT
|
||||
if (
|
||||
![constants.CONFIG_VERSION.EXPECTED, constants.CONFIG_VERSION.DEFAULT].includes(
|
||||
constants.CONFIG_VERSION.CURRENT,
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
`Fatal: Config Version incorrect - expected "${constants.CONFIG_VERSION.EXPECTED}" or "${constants.CONFIG_VERSION.DEFAULT}", but found "${constants.CONFIG_VERSION.CURRENT}"`,
|
||||
)
|
||||
}
|
||||
|
||||
const CONFIG = {
|
||||
...constants,
|
||||
...server,
|
||||
...database,
|
||||
...klicktipp,
|
||||
...community,
|
||||
...email,
|
||||
...loginServer,
|
||||
...webhook,
|
||||
...eventProtocol,
|
||||
}
|
||||
|
||||
export default CONFIG
|
||||
// ATTENTION: DO NOT PUT ANY SECRETS IN HERE (or the .env)
|
||||
|
||||
import dotenv from 'dotenv'
|
||||
import Decimal from 'decimal.js-light'
|
||||
dotenv.config()
|
||||
|
||||
Decimal.set({
|
||||
precision: 25,
|
||||
rounding: Decimal.ROUND_HALF_UP,
|
||||
})
|
||||
|
||||
const constants = {
|
||||
DB_VERSION: '0044-insert_missing_contributions',
|
||||
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
|
||||
LOG4JS_CONFIG: 'log4js-config.json',
|
||||
// default log level on production should be info
|
||||
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
|
||||
CONFIG_VERSION: {
|
||||
DEFAULT: 'DEFAULT',
|
||||
EXPECTED: 'v9.2022-07-07',
|
||||
CURRENT: '',
|
||||
},
|
||||
}
|
||||
|
||||
const server = {
|
||||
PORT: process.env.PORT || 4000,
|
||||
JWT_SECRET: process.env.JWT_SECRET || 'secret123',
|
||||
JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN || '10m',
|
||||
GRAPHIQL: process.env.GRAPHIQL === 'true' || false,
|
||||
GDT_API_URL: process.env.GDT_API_URL || 'https://gdt.gradido.net',
|
||||
PRODUCTION: process.env.NODE_ENV === 'production' || false,
|
||||
}
|
||||
|
||||
const database = {
|
||||
DB_HOST: process.env.DB_HOST || 'localhost',
|
||||
DB_PORT: process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 3306,
|
||||
DB_USER: process.env.DB_USER || 'root',
|
||||
DB_PASSWORD: process.env.DB_PASSWORD || '',
|
||||
DB_DATABASE: process.env.DB_DATABASE || 'gradido_community',
|
||||
TYPEORM_LOGGING_RELATIVE_PATH: process.env.TYPEORM_LOGGING_RELATIVE_PATH || 'typeorm.backend.log',
|
||||
}
|
||||
|
||||
const klicktipp = {
|
||||
KLICKTIPP: process.env.KLICKTIPP === 'true' || false,
|
||||
KLICKTTIPP_API_URL: process.env.KLICKTIPP_API_URL || 'https://api.klicktipp.com',
|
||||
KLICKTIPP_USER: process.env.KLICKTIPP_USER || 'gradido_test',
|
||||
KLICKTIPP_PASSWORD: process.env.KLICKTIPP_PASSWORD || 'secret321',
|
||||
KLICKTIPP_APIKEY_DE: process.env.KLICKTIPP_APIKEY_DE || 'SomeFakeKeyDE',
|
||||
KLICKTIPP_APIKEY_EN: process.env.KLICKTIPP_APIKEY_EN || 'SomeFakeKeyEN',
|
||||
}
|
||||
|
||||
const community = {
|
||||
COMMUNITY_NAME: process.env.COMMUNITY_NAME || 'Gradido Entwicklung',
|
||||
COMMUNITY_URL: process.env.COMMUNITY_URL || 'http://localhost/',
|
||||
COMMUNITY_REGISTER_URL: process.env.COMMUNITY_REGISTER_URL || 'http://localhost/register',
|
||||
COMMUNITY_REDEEM_URL: process.env.COMMUNITY_REDEEM_URL || 'http://localhost/redeem/{code}',
|
||||
COMMUNITY_REDEEM_CONTRIBUTION_URL:
|
||||
process.env.COMMUNITY_REDEEM_CONTRIBUTION_URL || 'http://localhost/redeem/CL-{code}',
|
||||
COMMUNITY_DESCRIPTION:
|
||||
process.env.COMMUNITY_DESCRIPTION || 'Die lokale Entwicklungsumgebung von Gradido.',
|
||||
}
|
||||
|
||||
const loginServer = {
|
||||
LOGIN_APP_SECRET: process.env.LOGIN_APP_SECRET || '21ffbbc616fe',
|
||||
LOGIN_SERVER_KEY: process.env.LOGIN_SERVER_KEY || 'a51ef8ac7ef1abf162fb7a65261acd7a',
|
||||
}
|
||||
|
||||
const email = {
|
||||
EMAIL: process.env.EMAIL === 'true' || false,
|
||||
EMAIL_USERNAME: process.env.EMAIL_USERNAME || 'gradido_email',
|
||||
EMAIL_SENDER: process.env.EMAIL_SENDER || 'info@gradido.net',
|
||||
EMAIL_PASSWORD: process.env.EMAIL_PASSWORD || 'xxx',
|
||||
EMAIL_SMTP_URL: process.env.EMAIL_SMTP_URL || 'gmail.com',
|
||||
EMAIL_SMTP_PORT: process.env.EMAIL_SMTP_PORT || '587',
|
||||
EMAIL_LINK_VERIFICATION:
|
||||
process.env.EMAIL_LINK_VERIFICATION || 'http://localhost/checkEmail/{optin}{code}',
|
||||
EMAIL_LINK_SETPASSWORD:
|
||||
process.env.EMAIL_LINK_SETPASSWORD || 'http://localhost/reset-password/{optin}',
|
||||
EMAIL_LINK_FORGOTPASSWORD:
|
||||
process.env.EMAIL_LINK_FORGOTPASSWORD || 'http://localhost/forgot-password',
|
||||
EMAIL_LINK_OVERVIEW: process.env.EMAIL_LINK_OVERVIEW || 'http://localhost/overview',
|
||||
// time in minutes a optin code is valid
|
||||
EMAIL_CODE_VALID_TIME: process.env.EMAIL_CODE_VALID_TIME
|
||||
? parseInt(process.env.EMAIL_CODE_VALID_TIME) || 1440
|
||||
: 1440,
|
||||
// time in minutes that must pass to request a new optin code
|
||||
EMAIL_CODE_REQUEST_TIME: process.env.EMAIL_CODE_REQUEST_TIME
|
||||
? parseInt(process.env.EMAIL_CODE_REQUEST_TIME) || 10
|
||||
: 10,
|
||||
}
|
||||
|
||||
const webhook = {
|
||||
// Elopage
|
||||
WEBHOOK_ELOPAGE_SECRET: process.env.WEBHOOK_ELOPAGE_SECRET || 'secret',
|
||||
}
|
||||
|
||||
const eventProtocol = {
|
||||
// global switch to enable writing of EventProtocol-Entries
|
||||
EVENT_PROTOCOL_DISABLED: process.env.EVENT_PROTOCOL_DISABLED === 'true' || false,
|
||||
}
|
||||
|
||||
// This is needed by graphql-directive-auth
|
||||
process.env.APP_SECRET = server.JWT_SECRET
|
||||
|
||||
// Check config version
|
||||
constants.CONFIG_VERSION.CURRENT = process.env.CONFIG_VERSION || constants.CONFIG_VERSION.DEFAULT
|
||||
if (
|
||||
![constants.CONFIG_VERSION.EXPECTED, constants.CONFIG_VERSION.DEFAULT].includes(
|
||||
constants.CONFIG_VERSION.CURRENT,
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
`Fatal: Config Version incorrect - expected "${constants.CONFIG_VERSION.EXPECTED}" or "${constants.CONFIG_VERSION.DEFAULT}", but found "${constants.CONFIG_VERSION.CURRENT}"`,
|
||||
)
|
||||
}
|
||||
|
||||
const CONFIG = {
|
||||
...constants,
|
||||
...server,
|
||||
...database,
|
||||
...klicktipp,
|
||||
...community,
|
||||
...email,
|
||||
...loginServer,
|
||||
...webhook,
|
||||
...eventProtocol,
|
||||
}
|
||||
|
||||
export default CONFIG
|
||||
|
||||
@ -15,6 +15,7 @@ export class Contribution {
|
||||
this.deletedAt = contribution.deletedAt
|
||||
this.confirmedAt = contribution.confirmedAt
|
||||
this.confirmedBy = contribution.confirmedBy
|
||||
this.contributionDate = contribution.contributionDate
|
||||
}
|
||||
|
||||
@Field(() => Number)
|
||||
@ -43,6 +44,9 @@ export class Contribution {
|
||||
|
||||
@Field(() => Number, { nullable: true })
|
||||
confirmedBy: number | null
|
||||
|
||||
@Field(() => Date)
|
||||
contributionDate: Date
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
|
||||
@ -1857,11 +1857,17 @@ describe('AdminResolver', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// TODO: Set this test in new location to have datas
|
||||
describe('listContributionLinks', () => {
|
||||
it('returns an error', async () => {
|
||||
it('returns an empty object', async () => {
|
||||
await expect(query({ query: listContributionLinks })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('401 Unauthorized')],
|
||||
data: {
|
||||
listContributionLinks: {
|
||||
count: 0,
|
||||
links: [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@ -54,11 +54,11 @@ import {
|
||||
updateCreations,
|
||||
} from './util/creations'
|
||||
import {
|
||||
CONTRIBUTIONLINK_MEMO_MAX_CHARS,
|
||||
CONTRIBUTIONLINK_MEMO_MIN_CHARS,
|
||||
CONTRIBUTIONLINK_NAME_MAX_CHARS,
|
||||
CONTRIBUTIONLINK_NAME_MIN_CHARS,
|
||||
FULL_CREATION_AVAILABLE,
|
||||
MEMO_MAX_CHARS,
|
||||
MEMO_MIN_CHARS,
|
||||
} from './const/const'
|
||||
|
||||
// const EMAIL_OPT_IN_REGISTER = 1
|
||||
@ -595,11 +595,8 @@ export class AdminResolver {
|
||||
logger.error(`The memo must be initialized!`)
|
||||
throw new Error(`The memo must be initialized!`)
|
||||
}
|
||||
if (
|
||||
memo.length < CONTRIBUTIONLINK_MEMO_MIN_CHARS ||
|
||||
memo.length > CONTRIBUTIONLINK_MEMO_MAX_CHARS
|
||||
) {
|
||||
const msg = `The value of 'memo' with a length of ${memo.length} did not fulfill the requested bounderies min=${CONTRIBUTIONLINK_MEMO_MIN_CHARS} and max=${CONTRIBUTIONLINK_MEMO_MAX_CHARS}`
|
||||
if (memo.length < MEMO_MIN_CHARS || memo.length > MEMO_MAX_CHARS) {
|
||||
const msg = `The value of 'memo' with a length of ${memo.length} did not fulfill the requested bounderies min=${MEMO_MIN_CHARS} and max=${MEMO_MAX_CHARS}`
|
||||
logger.error(`${msg}`)
|
||||
throw new Error(`${msg}`)
|
||||
}
|
||||
|
||||
@ -4,7 +4,9 @@
|
||||
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
|
||||
import {
|
||||
adminUpdateContribution,
|
||||
confirmContribution,
|
||||
createContribution,
|
||||
deleteContribution,
|
||||
updateContribution,
|
||||
} from '@/seeds/graphql/mutations'
|
||||
import { listAllContributions, listContributions, login } from '@/seeds/graphql/queries'
|
||||
@ -64,6 +66,42 @@ describe('ContributionResolver', () => {
|
||||
})
|
||||
|
||||
describe('input not valid', () => {
|
||||
it('throws error when memo length smaller than 5 chars', async () => {
|
||||
const date = new Date()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContribution,
|
||||
variables: {
|
||||
amount: 100.0,
|
||||
memo: 'Test',
|
||||
creationDate: date.toString(),
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('memo text is too short (5 characters minimum)')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('throws error when memo length greater than 255 chars', async () => {
|
||||
const date = new Date()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContribution,
|
||||
variables: {
|
||||
amount: 100.0,
|
||||
memo: 'Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test',
|
||||
creationDate: date.toString(),
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('memo text is too long (255 characters maximum)')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('throws error when creationDate not-valid', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
@ -311,6 +349,48 @@ describe('ContributionResolver', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Memo length smaller than 5 chars', () => {
|
||||
it('throws error', async () => {
|
||||
const date = new Date()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: updateContribution,
|
||||
variables: {
|
||||
contributionId: result.data.createContribution.id,
|
||||
amount: 100.0,
|
||||
memo: 'Test',
|
||||
creationDate: date.toString(),
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('memo text is too short (5 characters minimum)')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Memo length greater than 255 chars', () => {
|
||||
it('throws error', async () => {
|
||||
const date = new Date()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: updateContribution,
|
||||
variables: {
|
||||
contributionId: result.data.createContribution.id,
|
||||
amount: 100.0,
|
||||
memo: 'Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test',
|
||||
creationDate: date.toString(),
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('memo text is too long (255 characters maximum)')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('wrong user tries to update the contribution', () => {
|
||||
beforeAll(async () => {
|
||||
await query({
|
||||
@ -487,6 +567,11 @@ describe('ContributionResolver', () => {
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
resetToken()
|
||||
})
|
||||
|
||||
it('returns allCreation', async () => {
|
||||
await expect(
|
||||
query({
|
||||
@ -522,4 +607,129 @@ describe('ContributionResolver', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('deleteContribution', () => {
|
||||
describe('unauthenticated', () => {
|
||||
it('returns an error', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: deleteContribution,
|
||||
variables: {
|
||||
id: -1,
|
||||
},
|
||||
}),
|
||||
).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(),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
resetToken()
|
||||
})
|
||||
|
||||
describe('wrong contribution id', () => {
|
||||
it('returns an error', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: deleteContribution,
|
||||
variables: {
|
||||
id: -1,
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('Contribution not found for given id.')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('other user sends a deleteContribtuion', () => {
|
||||
it('returns an error', async () => {
|
||||
await query({
|
||||
query: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
})
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: deleteContribution,
|
||||
variables: {
|
||||
id: result.data.createContribution.id,
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('Can not delete contribution of another user')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('User deletes own contribution', () => {
|
||||
it('deletes successfully', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: deleteContribution,
|
||||
variables: {
|
||||
id: result.data.createContribution.id,
|
||||
},
|
||||
}),
|
||||
).resolves.toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('User deletes already confirmed contribution', () => {
|
||||
it('throws an error', async () => {
|
||||
await query({
|
||||
query: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
})
|
||||
await mutate({
|
||||
mutation: confirmContribution,
|
||||
variables: {
|
||||
id: result.data.createContribution.id,
|
||||
},
|
||||
})
|
||||
await query({
|
||||
query: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: deleteContribution,
|
||||
variables: {
|
||||
id: result.data.createContribution.id,
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('A confirmed contribution can not be deleted')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -3,7 +3,7 @@ import { Context, getUser } from '@/server/context'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import { Contribution as dbContribution } from '@entity/Contribution'
|
||||
import { Arg, Args, Authorized, Ctx, Int, Mutation, Query, Resolver } from 'type-graphql'
|
||||
import { FindOperator, IsNull } from '@dbTools/typeorm'
|
||||
import { FindOperator, IsNull, getConnection } from '@dbTools/typeorm'
|
||||
import ContributionArgs from '@arg/ContributionArgs'
|
||||
import Paginated from '@arg/Paginated'
|
||||
import { Order } from '@enum/Order'
|
||||
@ -11,6 +11,7 @@ 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'
|
||||
|
||||
@Resolver()
|
||||
export class ContributionResolver {
|
||||
@ -20,6 +21,16 @@ export class ContributionResolver {
|
||||
@Args() { amount, memo, creationDate }: ContributionArgs,
|
||||
@Ctx() context: Context,
|
||||
): Promise<UnconfirmedContribution> {
|
||||
if (memo.length > MEMO_MAX_CHARS) {
|
||||
logger.error(`memo text is too long: memo.length=${memo.length} > (${MEMO_MAX_CHARS}`)
|
||||
throw new Error(`memo text is too long (${MEMO_MAX_CHARS} characters maximum)`)
|
||||
}
|
||||
|
||||
if (memo.length < MEMO_MIN_CHARS) {
|
||||
logger.error(`memo text is too short: memo.length=${memo.length} < (${MEMO_MIN_CHARS}`)
|
||||
throw new Error(`memo text is too short (${MEMO_MIN_CHARS} characters minimum)`)
|
||||
}
|
||||
|
||||
const user = getUser(context)
|
||||
const creations = await getUserCreation(user.id)
|
||||
logger.trace('creations', creations)
|
||||
@ -38,6 +49,27 @@ export class ContributionResolver {
|
||||
return new UnconfirmedContribution(contribution, user, creations)
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.DELETE_CONTRIBUTION])
|
||||
@Mutation(() => Boolean)
|
||||
async deleteContribution(
|
||||
@Arg('id', () => Int) id: number,
|
||||
@Ctx() context: Context,
|
||||
): Promise<boolean> {
|
||||
const user = getUser(context)
|
||||
const contribution = await dbContribution.findOne(id)
|
||||
if (!contribution) {
|
||||
throw new Error('Contribution not found for given id.')
|
||||
}
|
||||
if (contribution.userId !== user.id) {
|
||||
throw new Error('Can not delete contribution of another user')
|
||||
}
|
||||
if (contribution.confirmedAt) {
|
||||
throw new Error('A confirmed contribution can not be deleted')
|
||||
}
|
||||
const res = await contribution.softRemove()
|
||||
return !!res
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.LIST_CONTRIBUTIONS])
|
||||
@Query(() => ContributionListResult)
|
||||
async listContributions(
|
||||
@ -74,14 +106,15 @@ export class ContributionResolver {
|
||||
@Args()
|
||||
{ currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated,
|
||||
): Promise<ContributionListResult> {
|
||||
const [dbContributions, count] = await dbContribution.findAndCount({
|
||||
relations: ['user'],
|
||||
order: {
|
||||
createdAt: order,
|
||||
},
|
||||
skip: (currentPage - 1) * pageSize,
|
||||
take: pageSize,
|
||||
})
|
||||
const [dbContributions, count] = await getConnection()
|
||||
.createQueryBuilder()
|
||||
.select('c')
|
||||
.from(dbContribution, 'c')
|
||||
.innerJoinAndSelect('c.user', 'u')
|
||||
.orderBy('c.createdAt', order)
|
||||
.limit(pageSize)
|
||||
.offset((currentPage - 1) * pageSize)
|
||||
.getManyAndCount()
|
||||
return new ContributionListResult(
|
||||
count,
|
||||
dbContributions.map(
|
||||
@ -98,6 +131,16 @@ export class ContributionResolver {
|
||||
@Args() { amount, memo, creationDate }: ContributionArgs,
|
||||
@Ctx() context: Context,
|
||||
): Promise<UnconfirmedContribution> {
|
||||
if (memo.length > MEMO_MAX_CHARS) {
|
||||
logger.error(`memo text is too long: memo.length=${memo.length} > (${MEMO_MAX_CHARS}`)
|
||||
throw new Error(`memo text is too long (${MEMO_MAX_CHARS} characters maximum)`)
|
||||
}
|
||||
|
||||
if (memo.length < MEMO_MIN_CHARS) {
|
||||
logger.error(`memo text is too short: memo.length=${memo.length} < (${MEMO_MIN_CHARS}`)
|
||||
throw new Error(`memo text is too short (${MEMO_MIN_CHARS} characters minimum)`)
|
||||
}
|
||||
|
||||
const user = getUser(context)
|
||||
|
||||
const contributionToUpdate = await dbContribution.findOne({
|
||||
|
||||
@ -37,9 +37,7 @@ import { virtualLinkTransaction, virtualDecayTransaction } from '@/util/virtualT
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
import { BalanceResolver } from './BalanceResolver'
|
||||
|
||||
const MEMO_MAX_CHARS = 255
|
||||
const MEMO_MIN_CHARS = 5
|
||||
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
|
||||
|
||||
export const executeTransaction = async (
|
||||
amount: Decimal,
|
||||
|
||||
@ -198,7 +198,7 @@ describe('UserResolver', () => {
|
||||
it('sets "de" as default language', async () => {
|
||||
await mutate({
|
||||
mutation: createUser,
|
||||
variables: { ...variables, email: 'bibi@bloxberg.de', language: 'es' },
|
||||
variables: { ...variables, email: 'bibi@bloxberg.de', language: 'fr' },
|
||||
})
|
||||
await expect(User.find()).resolves.toEqual(
|
||||
expect.arrayContaining([
|
||||
|
||||
@ -43,7 +43,7 @@ const isPassword = (password: string): boolean => {
|
||||
return !!password.match(/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^a-zA-Z0-9 \\t\\n\\r]).{8,}$/)
|
||||
}
|
||||
|
||||
const LANGUAGES = ['de', 'en']
|
||||
const LANGUAGES = ['de', 'en', 'es']
|
||||
const DEFAULT_LANGUAGE = 'de'
|
||||
const isLanguage = (language: string): boolean => {
|
||||
return LANGUAGES.includes(language)
|
||||
|
||||
@ -8,5 +8,5 @@ export const FULL_CREATION_AVAILABLE = [
|
||||
]
|
||||
export const CONTRIBUTIONLINK_NAME_MAX_CHARS = 100
|
||||
export const CONTRIBUTIONLINK_NAME_MIN_CHARS = 5
|
||||
export const CONTRIBUTIONLINK_MEMO_MAX_CHARS = 255
|
||||
export const CONTRIBUTIONLINK_MEMO_MIN_CHARS = 5
|
||||
export const MEMO_MAX_CHARS = 255
|
||||
export const MEMO_MIN_CHARS = 5
|
||||
|
||||
@ -35,12 +35,17 @@ export const creationFactory = async (
|
||||
if (creation.confirmed) {
|
||||
await mutate({ mutation: confirmContribution, variables: { id: pendingCreation.id } })
|
||||
|
||||
const confirmedCreation = await Contribution.findOneOrFail({ id: pendingCreation.id })
|
||||
|
||||
if (creation.moveCreationDate) {
|
||||
const transaction = await Transaction.findOneOrFail({
|
||||
where: { userId: user.id, creationDate: new Date(creation.creationDate) },
|
||||
order: { balanceDate: 'DESC' },
|
||||
})
|
||||
if (transaction.decay.equals(0) && transaction.creationDate) {
|
||||
confirmedCreation.contributionDate = new Date(
|
||||
nMonthsBefore(transaction.creationDate, creation.moveCreationDate),
|
||||
)
|
||||
transaction.creationDate = new Date(
|
||||
nMonthsBefore(transaction.creationDate, creation.moveCreationDate),
|
||||
)
|
||||
@ -48,6 +53,7 @@ export const creationFactory = async (
|
||||
nMonthsBefore(transaction.balanceDate, creation.moveCreationDate),
|
||||
)
|
||||
await transaction.save()
|
||||
await confirmedCreation.save()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@ -255,3 +255,9 @@ export const updateContribution = gql`
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const deleteContribution = gql`
|
||||
mutation ($id: Int!) {
|
||||
deleteContribution(id: $id)
|
||||
}
|
||||
`
|
||||
|
||||
83
database/entity/0039-contributions_table/User.ts
Normal file
83
database/entity/0039-contributions_table/User.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import {
|
||||
BaseEntity,
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
DeleteDateColumn,
|
||||
OneToMany,
|
||||
JoinColumn,
|
||||
} from 'typeorm'
|
||||
import { Contribution } from '../Contribution'
|
||||
|
||||
@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
|
||||
export class User extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
|
||||
@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: '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[]
|
||||
}
|
||||
34
database/migrations/0044-insert_missing_contributions.ts
Normal file
34
database/migrations/0044-insert_missing_contributions.ts
Normal file
@ -0,0 +1,34 @@
|
||||
/* MIGRATION TO INSERT contributions for all transactions with type creation that do not have a contribution yet */
|
||||
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn(
|
||||
`INSERT INTO gradido_community.contributions
|
||||
(user_id, created_at, contribution_date, memo, amount, moderator_id, confirmed_by, confirmed_at, transaction_id)
|
||||
SELECT
|
||||
user_id,
|
||||
balance_date,
|
||||
creation_date AS contribution_date,
|
||||
memo,
|
||||
amount,
|
||||
20 AS moderator_id,
|
||||
502 AS confirmed_by,
|
||||
balance_date AS confirmed_at,
|
||||
id
|
||||
FROM
|
||||
gradido_community.transactions
|
||||
WHERE
|
||||
type_id = 1
|
||||
AND NOT EXISTS(
|
||||
SELECT * FROM gradido_community.contributions
|
||||
WHERE gradido_community.contributions.transaction_id = gradido_community.transactions.id);`,
|
||||
)
|
||||
}
|
||||
|
||||
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn(
|
||||
'DELETE FROM `contributions` WHERE `contributions`.`moderator_id` = 20 AND `contributions`.`confirmed_by` = 502 AND `contributions`.`created_at` = `contributions`.`confirmed_at`;',
|
||||
)
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gradido-database",
|
||||
"version": "1.10.1",
|
||||
"version": "1.11.0",
|
||||
"description": "Gradido Database Tool to execute database migrations",
|
||||
"main": "src/index.ts",
|
||||
"repository": "https://github.com/gradido/gradido/database",
|
||||
|
||||
@ -1,84 +1,84 @@
|
||||
GRADIDO_LOG_PATH=/home/gradido/gradido/deployment/bare_metal/log
|
||||
|
||||
# start script
|
||||
DEPLOY_SEED_DATA=false
|
||||
|
||||
# nginx
|
||||
NGINX_REWRITE_LEGACY_URLS=true
|
||||
NGINX_SSL=true
|
||||
NGINX_SERVER_NAME=stage1.gradido.net
|
||||
NGINX_SSL_CERTIFICATE=/etc/letsencrypt/live/stage1.gradido.net/fullchain.pem
|
||||
NGINX_SSL_CERTIFICATE_KEY=/etc/letsencrypt/live/stage1.gradido.net/privkey.pem
|
||||
NGINX_SSL_DHPARAM=/etc/letsencrypt/ssl-dhparams.pem
|
||||
NGINX_SSL_INCLUDE=/etc/letsencrypt/options-ssl-nginx.conf
|
||||
NGINX_UPDATE_PAGE_ROOT=/home/gradido/gradido/deployment/bare_metal/nginx/update-page
|
||||
|
||||
# webhook
|
||||
WEBHOOK_GITHUB_SECRET=secret
|
||||
WEBHOOK_GITHUB_BRANCH=master
|
||||
|
||||
# community
|
||||
COMMUNITY_NAME="Gradido Development Stage1"
|
||||
COMMUNITY_URL=https://stage1.gradido.net/
|
||||
COMMUNITY_REGISTER_URL=https://stage1.gradido.net/register
|
||||
COMMUNITY_REDEEM_URL=https://stage1.gradido.net/redeem/{code}
|
||||
COMMUNITY_REDEEM_CONTRIBUTION_URL=https://stage1.gradido.net/redeem/CL-{code}
|
||||
COMMUNITY_DESCRIPTION="Gradido Development Stage1 Test Community"
|
||||
|
||||
# backend
|
||||
BACKEND_CONFIG_VERSION=v9.2022-07-07
|
||||
|
||||
JWT_EXPIRES_IN=10m
|
||||
GDT_API_URL=https://gdt.gradido.net
|
||||
|
||||
TYPEORM_LOGGING_RELATIVE_PATH=../deployment/bare_metal/log/typeorm.backend.log
|
||||
|
||||
KLICKTIPP=false
|
||||
KLICKTIPP_USER=
|
||||
KLICKTIPP_PASSWORD=
|
||||
KLICKTIPP_APIKEY_DE=
|
||||
KLICKTIPP_APIKEY_EN=
|
||||
|
||||
EMAIL=true
|
||||
EMAIL_USERNAME=peter@lustig.de
|
||||
EMAIL_SENDER=peter@lustig.de
|
||||
EMAIL_PASSWORD=1234
|
||||
EMAIL_SMTP_URL=smtp.lustig.de
|
||||
EMAIL_LINK_VERIFICATION=https://stage1.gradido.net/checkEmail/{optin}{code}
|
||||
EMAIL_LINK_SETPASSWORD=https://stage1.gradido.net/reset-password/{optin}
|
||||
EMAIL_LINK_FORGOTPASSWORD=https://stage1.gradido.net/forgot-password
|
||||
EMAIL_LINK_OVERVIEW=https://stage1.gradido.net/overview
|
||||
EMAIL_CODE_VALID_TIME=1440
|
||||
EMAIL_CODE_REQUEST_TIME=10
|
||||
|
||||
WEBHOOK_ELOPAGE_SECRET=secret
|
||||
|
||||
# EventProtocol
|
||||
EVENT_PROTOCOL_DISABLED=false
|
||||
|
||||
|
||||
# database
|
||||
DATABASE_CONFIG_VERSION=v1.2022-03-18
|
||||
|
||||
# frontend
|
||||
FRONTEND_CONFIG_VERSION=v2.2022-04-07
|
||||
|
||||
GRAPHQL_URI=https://stage1.gradido.net/graphql
|
||||
ADMIN_AUTH_URL=https://stage1.gradido.net/admin/authenticate?token={token}
|
||||
|
||||
DEFAULT_PUBLISHER_ID=2896
|
||||
|
||||
META_URL=http://localhost
|
||||
META_TITLE_DE="Gradido – Dein Dankbarkeitskonto"
|
||||
META_TITLE_EN="Gradido - Your gratitude account"
|
||||
META_DESCRIPTION_DE="Dankbarkeit ist die Währung der neuen Zeit. Immer mehr Menschen entfalten ihr Potenzial und gestalten eine gute Zukunft für alle."
|
||||
META_DESCRIPTION_EN="Gratitude is the currency of the new age. More and more people are unleashing their potential and shaping a good future for all."
|
||||
META_KEYWORDS_DE="Grundeinkommen, Währung, Dankbarkeit, Schenk-Ökonomie, Natürliche Ökonomie des Lebens, Ökonomie, Ökologie, Potenzialentfaltung, Schenken und Danken, Kreislauf des Lebens, Geldsystem"
|
||||
META_KEYWORDS_EN="Basic Income, Currency, Gratitude, Gift Economy, Natural Economy of Life, Economy, Ecology, Potential Development, Giving and Thanking, Cycle of Life, Monetary System"
|
||||
META_AUTHOR="Bernd Hückstädt - Gradido-Akademie"
|
||||
|
||||
# admin
|
||||
ADMIN_CONFIG_VERSION=v1.2022-03-18
|
||||
|
||||
WALLET_AUTH_URL=https://stage1.gradido.net/authenticate?token={token}
|
||||
GRADIDO_LOG_PATH=/home/gradido/gradido/deployment/bare_metal/log
|
||||
|
||||
# start script
|
||||
DEPLOY_SEED_DATA=false
|
||||
|
||||
# nginx
|
||||
NGINX_REWRITE_LEGACY_URLS=true
|
||||
NGINX_SSL=true
|
||||
NGINX_SERVER_NAME=stage1.gradido.net
|
||||
NGINX_SSL_CERTIFICATE=/etc/letsencrypt/live/stage1.gradido.net/fullchain.pem
|
||||
NGINX_SSL_CERTIFICATE_KEY=/etc/letsencrypt/live/stage1.gradido.net/privkey.pem
|
||||
NGINX_SSL_DHPARAM=/etc/letsencrypt/ssl-dhparams.pem
|
||||
NGINX_SSL_INCLUDE=/etc/letsencrypt/options-ssl-nginx.conf
|
||||
NGINX_UPDATE_PAGE_ROOT=/home/gradido/gradido/deployment/bare_metal/nginx/update-page
|
||||
|
||||
# webhook
|
||||
WEBHOOK_GITHUB_SECRET=secret
|
||||
WEBHOOK_GITHUB_BRANCH=master
|
||||
|
||||
# community
|
||||
COMMUNITY_NAME="Gradido Development Stage1"
|
||||
COMMUNITY_URL=https://stage1.gradido.net/
|
||||
COMMUNITY_REGISTER_URL=https://stage1.gradido.net/register
|
||||
COMMUNITY_REDEEM_URL=https://stage1.gradido.net/redeem/{code}
|
||||
COMMUNITY_REDEEM_CONTRIBUTION_URL=https://stage1.gradido.net/redeem/CL-{code}
|
||||
COMMUNITY_DESCRIPTION="Gradido Development Stage1 Test Community"
|
||||
|
||||
# backend
|
||||
BACKEND_CONFIG_VERSION=v9.2022-07-07
|
||||
|
||||
JWT_EXPIRES_IN=10m
|
||||
GDT_API_URL=https://gdt.gradido.net
|
||||
|
||||
TYPEORM_LOGGING_RELATIVE_PATH=../deployment/bare_metal/log/typeorm.backend.log
|
||||
|
||||
KLICKTIPP=false
|
||||
KLICKTIPP_USER=
|
||||
KLICKTIPP_PASSWORD=
|
||||
KLICKTIPP_APIKEY_DE=
|
||||
KLICKTIPP_APIKEY_EN=
|
||||
|
||||
EMAIL=true
|
||||
EMAIL_USERNAME=peter@lustig.de
|
||||
EMAIL_SENDER=peter@lustig.de
|
||||
EMAIL_PASSWORD=1234
|
||||
EMAIL_SMTP_URL=smtp.lustig.de
|
||||
EMAIL_LINK_VERIFICATION=https://stage1.gradido.net/checkEmail/{optin}{code}
|
||||
EMAIL_LINK_SETPASSWORD=https://stage1.gradido.net/reset-password/{optin}
|
||||
EMAIL_LINK_FORGOTPASSWORD=https://stage1.gradido.net/forgot-password
|
||||
EMAIL_LINK_OVERVIEW=https://stage1.gradido.net/overview
|
||||
EMAIL_CODE_VALID_TIME=1440
|
||||
EMAIL_CODE_REQUEST_TIME=10
|
||||
|
||||
WEBHOOK_ELOPAGE_SECRET=secret
|
||||
|
||||
# EventProtocol
|
||||
EVENT_PROTOCOL_DISABLED=false
|
||||
|
||||
|
||||
# database
|
||||
DATABASE_CONFIG_VERSION=v1.2022-03-18
|
||||
|
||||
# frontend
|
||||
FRONTEND_CONFIG_VERSION=v2.2022-04-07
|
||||
|
||||
GRAPHQL_URI=https://stage1.gradido.net/graphql
|
||||
ADMIN_AUTH_URL=https://stage1.gradido.net/admin/authenticate?token={token}
|
||||
|
||||
DEFAULT_PUBLISHER_ID=2896
|
||||
|
||||
META_URL=http://localhost
|
||||
META_TITLE_DE="Gradido – Dein Dankbarkeitskonto"
|
||||
META_TITLE_EN="Gradido - Your gratitude account"
|
||||
META_DESCRIPTION_DE="Dankbarkeit ist die Währung der neuen Zeit. Immer mehr Menschen entfalten ihr Potenzial und gestalten eine gute Zukunft für alle."
|
||||
META_DESCRIPTION_EN="Gratitude is the currency of the new age. More and more people are unleashing their potential and shaping a good future for all."
|
||||
META_KEYWORDS_DE="Grundeinkommen, Währung, Dankbarkeit, Schenk-Ökonomie, Natürliche Ökonomie des Lebens, Ökonomie, Ökologie, Potenzialentfaltung, Schenken und Danken, Kreislauf des Lebens, Geldsystem"
|
||||
META_KEYWORDS_EN="Basic Income, Currency, Gratitude, Gift Economy, Natural Economy of Life, Economy, Ecology, Potential Development, Giving and Thanking, Cycle of Life, Monetary System"
|
||||
META_AUTHOR="Bernd Hückstädt - Gradido-Akademie"
|
||||
|
||||
# admin
|
||||
ADMIN_CONFIG_VERSION=v1.2022-03-18
|
||||
|
||||
WALLET_AUTH_URL=https://stage1.gradido.net/authenticate?token={token}
|
||||
WALLET_URL=https://stage1.gradido.net/login
|
||||
43
docker-compose.apple-m1.override.yml
Normal file
43
docker-compose.apple-m1.override.yml
Normal file
@ -0,0 +1,43 @@
|
||||
# This file defines the Apple M1 chip settings. It overrides docker-compose.override.yml,
|
||||
# which defines the development settings.
|
||||
# To use it it is required to explicitly define if you want to build with it:
|
||||
# > docker-compose -f docker-compose.yml -f docker-compose.override.yml -f docker-compose.apple-m1.override.yml up
|
||||
|
||||
version: "3.4"
|
||||
|
||||
services:
|
||||
########################################################
|
||||
# FRONTEND #############################################
|
||||
########################################################
|
||||
frontend:
|
||||
platform: linux/amd64
|
||||
|
||||
########################################################
|
||||
# ADMIN INTERFACE ######################################
|
||||
########################################################
|
||||
admin:
|
||||
platform: linux/amd64
|
||||
|
||||
#########################################################
|
||||
## MARIADB ##############################################
|
||||
#########################################################
|
||||
mariadb:
|
||||
platform: linux/amd64
|
||||
|
||||
########################################################
|
||||
# BACKEND ##############################################
|
||||
########################################################
|
||||
backend:
|
||||
platform: linux/amd64
|
||||
|
||||
########################################################
|
||||
# DATABASE #############################################
|
||||
########################################################
|
||||
database:
|
||||
platform: linux/amd64
|
||||
|
||||
#########################################################
|
||||
## NGINX ################################################
|
||||
#########################################################
|
||||
nginx:
|
||||
platform: linux/amd64
|
||||
@ -1,11 +1,13 @@
|
||||
version: "3.4"
|
||||
|
||||
services:
|
||||
|
||||
########################################################
|
||||
# FRONTEND #############################################
|
||||
########################################################
|
||||
frontend:
|
||||
image: gradido/frontend:development
|
||||
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
|
||||
image: gradido/frontend:local-development
|
||||
build:
|
||||
target: development
|
||||
environment:
|
||||
@ -22,7 +24,8 @@ services:
|
||||
# ADMIN INTERFACE ######################################
|
||||
########################################################
|
||||
admin:
|
||||
image: gradido/admin:development
|
||||
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
|
||||
image: gradido/admin:local-development
|
||||
build:
|
||||
target: development
|
||||
environment:
|
||||
@ -39,7 +42,8 @@ services:
|
||||
# BACKEND ##############################################
|
||||
########################################################
|
||||
backend:
|
||||
image: gradido/backend:development
|
||||
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
|
||||
image: gradido/backend:local-development
|
||||
build:
|
||||
target: development
|
||||
networks:
|
||||
@ -62,10 +66,11 @@ services:
|
||||
########################################################
|
||||
database:
|
||||
# we always run on production here since else the service lingers
|
||||
# feel free to change this behaviour if it seems useful
|
||||
# Due to problems with the volume caching the built files
|
||||
# we changed this to test build. This keeps the service running.
|
||||
image: gradido/database:test_up
|
||||
# feel free to change this behaviour if it seems useful
|
||||
# Due to problems with the volume caching the built files
|
||||
# we changed this to test build. This keeps the service running.
|
||||
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
|
||||
image: gradido/database:local-test_up
|
||||
build:
|
||||
target: test_up
|
||||
environment:
|
||||
@ -89,9 +94,7 @@ services:
|
||||
#########################################################
|
||||
## NGINX ################################################
|
||||
#########################################################
|
||||
nginx:
|
||||
volumes:
|
||||
- ./logs/nginx:/var/log/nginx
|
||||
# nginx:
|
||||
|
||||
#########################################################
|
||||
## PHPMYADMIN ###########################################
|
||||
|
||||
@ -6,6 +6,7 @@ services:
|
||||
# BACKEND ##############################################
|
||||
########################################################
|
||||
backend:
|
||||
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
|
||||
image: gradido/backend:test
|
||||
build:
|
||||
target: test
|
||||
|
||||
@ -6,11 +6,13 @@
|
||||
version: "3.4"
|
||||
|
||||
services:
|
||||
|
||||
########################################################
|
||||
# FRONTEND #############################################
|
||||
########################################################
|
||||
frontend:
|
||||
image: gradido/frontend:latest
|
||||
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
|
||||
image: gradido/frontend:local-production
|
||||
build:
|
||||
context: ./frontend
|
||||
target: production
|
||||
@ -35,7 +37,8 @@ services:
|
||||
# ADMIN INTERFACE ######################################
|
||||
########################################################
|
||||
admin:
|
||||
image: gradido/admin:latest
|
||||
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
|
||||
image: gradido/admin:local-production
|
||||
build:
|
||||
context: ./admin
|
||||
target: production
|
||||
@ -77,7 +80,8 @@ services:
|
||||
# BACKEND ##############################################
|
||||
########################################################
|
||||
backend:
|
||||
image: gradido/backend:latest
|
||||
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
|
||||
image: gradido/backend:local-production
|
||||
build:
|
||||
# since we have to include the entities from ./database we cannot define the context as ./backend
|
||||
# this might blow build image size to the moon ?!
|
||||
@ -103,12 +107,16 @@ services:
|
||||
# Application only envs
|
||||
#env_file:
|
||||
# - ./frontend/.env
|
||||
volumes:
|
||||
# <host_machine_directy>:<container_directory> – mirror bidirectional path in local context with path in Docker container
|
||||
- ./logs/backend:/logs/backend
|
||||
|
||||
########################################################
|
||||
# DATABASE #############################################
|
||||
########################################################
|
||||
database:
|
||||
#image: gradido/database:production_up
|
||||
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
|
||||
image: gradido/database:local-production_up
|
||||
build:
|
||||
context: ./database
|
||||
target: production_up
|
||||
@ -144,6 +152,8 @@ services:
|
||||
- admin
|
||||
ports:
|
||||
- 80:80
|
||||
volumes:
|
||||
- ./logs/nginx:/var/log/nginx
|
||||
|
||||
networks:
|
||||
external-net:
|
||||
|
||||
BIN
docu/Locales/GRADIDO_register_page_spanish.xlsx
Normal file
BIN
docu/Locales/GRADIDO_register_page_spanish.xlsx
Normal file
Binary file not shown.
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bootstrap-vue-gradido-wallet",
|
||||
"version": "1.10.1",
|
||||
"version": "1.11.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "node run/server.js",
|
||||
|
||||
@ -3,8 +3,11 @@
|
||||
<b-input-group v-if="canCopyLink" size="lg" class="mb-3" prepend="Link">
|
||||
<b-form-input :value="link" type="text" readonly></b-form-input>
|
||||
<b-input-group-append>
|
||||
<b-button size="sm" text="Button" variant="primary" @click="CopyLink">
|
||||
{{ $t('gdd_per_link.copy') }}
|
||||
<b-button size="sm" text="Button" variant="primary" @click="copyLinkWithText">
|
||||
{{ $t('gdd_per_link.copy-link-with-text') }}
|
||||
</b-button>
|
||||
<b-button size="sm" text="Button" variant="primary" @click="copyLink">
|
||||
{{ $t('gdd_per_link.copy-link') }}
|
||||
</b-button>
|
||||
<b-button variant="primary" class="text-light" @click="$emit('show-qr-code-button')">
|
||||
<b-img src="img/svg/qr-code.svg" width="19" class="svg"></b-img>
|
||||
@ -18,29 +21,10 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { copyLinks } from '../mixins/copyLinks'
|
||||
export default {
|
||||
name: 'ClipboardCopy',
|
||||
props: {
|
||||
link: { type: String, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
canCopyLink: true,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
CopyLink() {
|
||||
navigator.clipboard
|
||||
.writeText(this.link)
|
||||
.then(() => {
|
||||
this.toastSuccess(this.$t('gdd_per_link.link-copied'))
|
||||
})
|
||||
.catch(() => {
|
||||
this.canCopyLink = false
|
||||
this.toastError(this.$t('gdd_per_link.not-copied'))
|
||||
})
|
||||
},
|
||||
},
|
||||
mixins: [copyLinks],
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
|
||||
@ -97,24 +97,14 @@ describe('ContentFooter', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('has a link to the members area', () => {
|
||||
expect(wrapper.findAll('a.nav-link').at(2).text()).toEqual('navigation.members_area')
|
||||
})
|
||||
|
||||
it('links to the elopage', () => {
|
||||
expect(wrapper.findAll('a.nav-link').at(2).attributes('href')).toEqual(
|
||||
'https://elopage.com/s/gradido/sign_in?locale=en',
|
||||
)
|
||||
})
|
||||
|
||||
it('links to the whitepaper', () => {
|
||||
expect(wrapper.findAll('a.nav-link').at(3).attributes('href')).toEqual(
|
||||
expect(wrapper.findAll('a.nav-link').at(2).attributes('href')).toEqual(
|
||||
'https://docs.google.com/document/d/1kcX1guOi6tDgnFHD9tf7fB_MneKTx-0nHJxzdN8ygNs/edit?usp=sharing',
|
||||
)
|
||||
})
|
||||
|
||||
it('links to the support', () => {
|
||||
expect(wrapper.findAll('a.nav-link').at(4).attributes('href')).toEqual(
|
||||
expect(wrapper.findAll('a.nav-link').at(3).attributes('href')).toEqual(
|
||||
'https://gradido.net/en/contact/',
|
||||
)
|
||||
})
|
||||
@ -142,20 +132,14 @@ describe('ContentFooter', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('links to the German elopage when locale is de', () => {
|
||||
expect(wrapper.findAll('a.nav-link').at(2).attributes('href')).toEqual(
|
||||
'https://elopage.com/s/gradido/sign_in?locale=de',
|
||||
)
|
||||
})
|
||||
|
||||
it('links to the German whitepaper when locale is de', () => {
|
||||
expect(wrapper.findAll('a.nav-link').at(3).attributes('href')).toEqual(
|
||||
expect(wrapper.findAll('a.nav-link').at(2).attributes('href')).toEqual(
|
||||
'https://docs.google.com/document/d/1jZp-DiiMPI9ZPNXmjsvOQ1BtnfDFfx8BX7CDmA8KKjY/edit?usp=sharing',
|
||||
)
|
||||
})
|
||||
|
||||
it('links to the German support-page when locale is de', () => {
|
||||
expect(wrapper.findAll('a.nav-link').at(4).attributes('href')).toEqual(
|
||||
expect(wrapper.findAll('a.nav-link').at(3).attributes('href')).toEqual(
|
||||
'https://gradido.net/de/contact/',
|
||||
)
|
||||
})
|
||||
|
||||
@ -34,12 +34,6 @@
|
||||
<b-nav-item :href="`https://gradido.net/${$i18n.locale}/datenschutz/`" target="_blank">
|
||||
{{ $t('footer.privacy_policy') }}
|
||||
</b-nav-item>
|
||||
<b-nav-item
|
||||
:href="`https://elopage.com/s/gradido/sign_in?locale=${$i18n.locale}`"
|
||||
target="_blank"
|
||||
>
|
||||
{{ $t('navigation.members_area') }}
|
||||
</b-nav-item>
|
||||
<b-nav-item
|
||||
:href="
|
||||
$i18n.locale === 'de'
|
||||
|
||||
114
frontend/src/components/Contributions/ContributionForm.spec.js
Normal file
114
frontend/src/components/Contributions/ContributionForm.spec.js
Normal file
@ -0,0 +1,114 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import ContributionForm from './ContributionForm.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
describe('ContributionForm', () => {
|
||||
let wrapper
|
||||
|
||||
const propsData = {
|
||||
value: {
|
||||
id: null,
|
||||
date: '',
|
||||
memo: '',
|
||||
amount: '',
|
||||
},
|
||||
}
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$d: jest.fn((d) => d),
|
||||
$store: {
|
||||
state: {
|
||||
creation: ['1000', '1000', '1000'],
|
||||
},
|
||||
},
|
||||
$i18n: {
|
||||
locale: 'en',
|
||||
},
|
||||
}
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(ContributionForm, {
|
||||
localVue,
|
||||
mocks,
|
||||
propsData,
|
||||
})
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('has a DIV .contribution-form', () => {
|
||||
expect(wrapper.find('div.contribution-form').exists()).toBe(true)
|
||||
})
|
||||
|
||||
describe('empty form data', () => {
|
||||
describe('buttons', () => {
|
||||
it('has reset enabled', () => {
|
||||
expect(wrapper.find('button[type="reset"]').attributes('disabled')).toBeFalsy()
|
||||
})
|
||||
|
||||
it('has submit disabled', () => {
|
||||
expect(wrapper.find('button[type="submit"]').attributes('disabled')).toBe('disabled')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('set contrubtion', () => {
|
||||
describe('fill in form data', () => {
|
||||
const now = new Date().toISOString()
|
||||
|
||||
beforeEach(async () => {
|
||||
await wrapper.setData({
|
||||
form: {
|
||||
id: null,
|
||||
date: now,
|
||||
memo: 'Mein Beitrag zur Gemeinschaft für diesen Monat ...',
|
||||
amount: '200',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe('buttons', () => {
|
||||
it('has reset enabled', () => {
|
||||
expect(wrapper.find('button[type="reset"]').attributes('disabled')).toBeFalsy()
|
||||
})
|
||||
|
||||
it('has submit enabled', () => {
|
||||
expect(wrapper.find('button[type="submit"]').attributes('disabled')).toBeFalsy()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('update contrubtion', () => {
|
||||
describe('fill in form data and "id"', () => {
|
||||
const now = new Date().toISOString()
|
||||
|
||||
beforeEach(async () => {
|
||||
await wrapper.setData({
|
||||
form: {
|
||||
id: 2,
|
||||
date: now,
|
||||
memo: 'Mein Beitrag zur Gemeinschaft für diesen Monat ...',
|
||||
amount: '200',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe('buttons', () => {
|
||||
it('has reset enabled', () => {
|
||||
expect(wrapper.find('button[type="reset"]').attributes('disabled')).toBeFalsy()
|
||||
})
|
||||
|
||||
it('has submit enabled', () => {
|
||||
expect(wrapper.find('button[type="submit"]').attributes('disabled')).toBeFalsy()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
183
frontend/src/components/Contributions/ContributionForm.vue
Normal file
183
frontend/src/components/Contributions/ContributionForm.vue
Normal file
@ -0,0 +1,183 @@
|
||||
<template>
|
||||
<div class="container contribution-form">
|
||||
<div class="my-3">
|
||||
<h3>{{ $t('contribution.formText.yourContribution') }}</h3>
|
||||
{{ $t('contribution.formText.bringYourTalentsTo') }}
|
||||
<ul class="my-3">
|
||||
<li v-html="lastMonthObject"></li>
|
||||
<li v-html="thisMonthObject"></li>
|
||||
</ul>
|
||||
|
||||
<div class="my-3">
|
||||
<b>{{ $t('contribution.formText.describeYourCommunity') }}</b>
|
||||
</div>
|
||||
</div>
|
||||
<b-form ref="form" @submit.prevent="submit" class="border p-3">
|
||||
<label>{{ $t('contribution.selectDate') }} {{ $t('math.asterisk') }}</label>
|
||||
<b-form-datepicker
|
||||
id="contribution-date"
|
||||
v-model="form.date"
|
||||
size="lg"
|
||||
:locale="$i18n.locale"
|
||||
:max="maximalDate"
|
||||
:min="minimalDate"
|
||||
class="mb-4"
|
||||
reset-value=""
|
||||
:label-no-date-selected="$t('contribution.noDateSelected')"
|
||||
required
|
||||
>
|
||||
<template #nav-prev-year><span></span></template>
|
||||
<template #nav-next-year><span></span></template>
|
||||
</b-form-datepicker>
|
||||
<validation-provider
|
||||
:rules="{
|
||||
min: minlength,
|
||||
max: maxlength,
|
||||
}"
|
||||
:name="$t('form.message')"
|
||||
v-slot="{ errors }"
|
||||
>
|
||||
<label class="mt-3">{{ $t('contribution.activity') }} {{ $t('math.asterisk') }}</label>
|
||||
<b-form-textarea
|
||||
id="contribution-memo"
|
||||
v-model="form.memo"
|
||||
rows="3"
|
||||
max-rows="6"
|
||||
:placeholder="$t('contribution.yourActivity')"
|
||||
required
|
||||
></b-form-textarea>
|
||||
<b-col v-if="errors">
|
||||
<span v-for="error in errors" class="errors" :key="error">{{ error }}</span>
|
||||
</b-col>
|
||||
</validation-provider>
|
||||
<label class="mt-3">{{ $t('form.amount') }} {{ $t('math.asterisk') }}</label>
|
||||
<b-input-group size="lg" prepend="GDD">
|
||||
<b-form-input
|
||||
id="contribution-amount"
|
||||
v-model="form.amount"
|
||||
type="text"
|
||||
:formatter="numberFormat"
|
||||
></b-form-input>
|
||||
</b-input-group>
|
||||
<div
|
||||
v-if="isThisMonth && parseInt(form.amount) > parseInt(maxGddThisMonth)"
|
||||
class="text-danger text-right"
|
||||
>
|
||||
{{ $t('contribution.formText.maxGDDforMonth', { amount: maxGddThisMonth }) }}
|
||||
</div>
|
||||
<div
|
||||
v-if="!isThisMonth && parseInt(form.amount) > parseInt(maxGddLastMonth)"
|
||||
class="text-danger text-right"
|
||||
>
|
||||
{{ $t('contribution.formText.maxGDDforMonth', { amount: maxGddLastMonth }) }}
|
||||
</div>
|
||||
<b-row class="mt-3">
|
||||
<b-col>
|
||||
<b-button class="test-cancel" type="reset" variant="secondary" @click="reset">
|
||||
{{ $t('form.cancel') }}
|
||||
</b-button>
|
||||
</b-col>
|
||||
<b-col class="text-right">
|
||||
<b-button class="test-submit" type="submit" variant="primary" :disabled="disabled">
|
||||
{{ form.id ? $t('form.change') : $t('contribution.submit') }}
|
||||
</b-button>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-form>
|
||||
<p class="p-2">{{ $t('math.asterisk') }} {{ $t('form.mandatoryField') }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'ContributionForm',
|
||||
props: {
|
||||
value: { type: Object, required: true },
|
||||
updateAmount: { type: String, required: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
minlength: 5,
|
||||
maxlength: 255,
|
||||
maximalDate: new Date(),
|
||||
form: this.value, // includes 'id'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
numberFormat(value) {
|
||||
return value.replace(/\D/g, '')
|
||||
},
|
||||
submit() {
|
||||
this.form.amount = this.numberFormat(this.form.amount)
|
||||
if (this.form.id) {
|
||||
this.$emit('update-contribution', this.form)
|
||||
} else {
|
||||
this.$emit('set-contribution', this.form)
|
||||
}
|
||||
this.reset()
|
||||
},
|
||||
reset() {
|
||||
this.$refs.form.reset()
|
||||
this.form.id = null
|
||||
this.form.date = ''
|
||||
this.form.memo = ''
|
||||
this.form.amount = ''
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
/*
|
||||
* minimalDate() = Sets the date to the 1st of the previous month.
|
||||
*
|
||||
*/
|
||||
minimalDate() {
|
||||
return new Date(this.maximalDate.getFullYear(), this.maximalDate.getMonth() - 1, 1)
|
||||
},
|
||||
disabled() {
|
||||
if (
|
||||
this.form.date === '' ||
|
||||
this.form.memo.length < this.minlength ||
|
||||
parseInt(this.form.amount) <= 0 ||
|
||||
parseInt(this.form.amount) > 1000 ||
|
||||
(this.isThisMonth && parseInt(this.form.amount) > parseInt(this.maxGddThisMonth)) ||
|
||||
(!this.isThisMonth && parseInt(this.form.amount) > parseInt(this.maxGddLastMonth))
|
||||
)
|
||||
return true
|
||||
return false
|
||||
},
|
||||
lastMonthObject() {
|
||||
// new Date().getMonth === 1 If the current month is January, then one year must be gone back in the previous month
|
||||
const obj = {
|
||||
monthAndYear: this.$d(new Date(this.minimalDate), 'monthAndYear'),
|
||||
creation: this.maxGddLastMonth,
|
||||
}
|
||||
return this.$t('contribution.formText.openAmountForMonth', obj)
|
||||
},
|
||||
thisMonthObject() {
|
||||
const obj = {
|
||||
monthAndYear: this.$d(new Date(), 'monthAndYear'),
|
||||
creation: this.maxGddThisMonth,
|
||||
}
|
||||
return this.$t('contribution.formText.openAmountForMonth', obj)
|
||||
},
|
||||
isThisMonth() {
|
||||
return new Date(this.form.date).getMonth() === new Date().getMonth()
|
||||
},
|
||||
maxGddLastMonth() {
|
||||
// When edited, the amount is added back on top of the amount
|
||||
return this.form.id && !this.isThisMonth
|
||||
? parseInt(this.$store.state.creation[1]) + parseInt(this.updateAmount)
|
||||
: this.$store.state.creation[1]
|
||||
},
|
||||
maxGddThisMonth() {
|
||||
// When edited, the amount is added back on top of the amount
|
||||
return this.form.id && this.isThisMonth
|
||||
? parseInt(this.$store.state.creation[2]) + parseInt(this.updateAmount)
|
||||
: this.$store.state.creation[2]
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
span.errors {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
120
frontend/src/components/Contributions/ContributionList.spec.js
Normal file
120
frontend/src/components/Contributions/ContributionList.spec.js
Normal file
@ -0,0 +1,120 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import ContributionList from './ContributionList.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
describe('ContributionList', () => {
|
||||
let wrapper
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$d: jest.fn((d) => d),
|
||||
}
|
||||
|
||||
const propsData = {
|
||||
contributionCount: 3,
|
||||
showPagination: true,
|
||||
pageSize: 25,
|
||||
items: [
|
||||
{
|
||||
id: 0,
|
||||
date: '07/06/2022',
|
||||
memo: 'Ich habe 10 Stunden die Elbwiesen von Müll befreit.',
|
||||
amount: '200',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
date: '06/22/2022',
|
||||
memo: 'Ich habe 30 Stunden Frau Müller beim EInkaufen und im Haushalt geholfen.',
|
||||
amount: '600',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
date: '05/04/2022',
|
||||
memo:
|
||||
'Ich habe 50 Stunden den Nachbarkindern bei ihren Hausaufgaben geholfen und Nachhilfeunterricht gegeben.',
|
||||
amount: '1000',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(ContributionList, {
|
||||
localVue,
|
||||
mocks,
|
||||
propsData,
|
||||
})
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('has a DIV .contribution-list', () => {
|
||||
expect(wrapper.find('div.contribution-list').exists()).toBe(true)
|
||||
})
|
||||
|
||||
describe('pagination', () => {
|
||||
describe('list count smaller than page size', () => {
|
||||
it('has no pagination buttons', () => {
|
||||
expect(wrapper.find('ul.pagination').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('list count greater than page size', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.setProps({ contributionCount: 33 })
|
||||
})
|
||||
|
||||
it('has pagination buttons', () => {
|
||||
expect(wrapper.find('ul.pagination').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('switch page', () => {
|
||||
const scrollToMock = jest.fn()
|
||||
window.scrollTo = scrollToMock
|
||||
|
||||
beforeEach(async () => {
|
||||
await wrapper.setProps({ contributionCount: 33 })
|
||||
wrapper.findComponent({ name: 'BPagination' }).vm.$emit('input', 2)
|
||||
})
|
||||
|
||||
it('emits update contribution list', () => {
|
||||
expect(wrapper.emitted('update-list-contributions')).toEqual([
|
||||
[{ currentPage: 2, pageSize: 25 }],
|
||||
])
|
||||
})
|
||||
|
||||
it('scrolls to top', () => {
|
||||
expect(scrollToMock).toBeCalledWith(0, 0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('update contribution', () => {
|
||||
beforeEach(() => {
|
||||
wrapper
|
||||
.findComponent({ name: 'ContributionListItem' })
|
||||
.vm.$emit('update-contribution-form', 'item')
|
||||
})
|
||||
|
||||
it('emits update contribution form', () => {
|
||||
expect(wrapper.emitted('update-contribution-form')).toEqual([['item']])
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete contribution', () => {
|
||||
beforeEach(() => {
|
||||
wrapper
|
||||
.findComponent({ name: 'ContributionListItem' })
|
||||
.vm.$emit('delete-contribution', { id: 2 })
|
||||
})
|
||||
|
||||
it('emits delete contribution', () => {
|
||||
expect(wrapper.emitted('delete-contribution')).toEqual([[{ id: 2 }]])
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
77
frontend/src/components/Contributions/ContributionList.vue
Normal file
77
frontend/src/components/Contributions/ContributionList.vue
Normal file
@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<div class="contribution-list container">
|
||||
<div class="list-group" v-for="item in items" :key="item.id">
|
||||
<contribution-list-item
|
||||
v-bind="item"
|
||||
@update-contribution-form="updateContributionForm"
|
||||
@delete-contribution="deleteContribution"
|
||||
/>
|
||||
</div>
|
||||
<b-pagination
|
||||
v-if="isPaginationVisible"
|
||||
class="mt-3"
|
||||
pills
|
||||
size="lg"
|
||||
v-model="currentPage"
|
||||
:per-page="pageSize"
|
||||
:total-rows="contributionCount"
|
||||
align="center"
|
||||
:hide-ellipsis="true"
|
||||
></b-pagination>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import ContributionListItem from '@/components/Contributions/ContributionListItem.vue'
|
||||
|
||||
export default {
|
||||
name: 'ContributionList',
|
||||
components: {
|
||||
ContributionListItem,
|
||||
},
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
contributionCount: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
showPagination: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
pageSize: { type: Number, default: 25 },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentPage: 1,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateListContributions() {
|
||||
this.$emit('update-list-contributions', {
|
||||
currentPage: this.currentPage,
|
||||
pageSize: this.pageSize,
|
||||
})
|
||||
window.scrollTo(0, 0)
|
||||
},
|
||||
updateContributionForm(item) {
|
||||
this.$emit('update-contribution-form', item)
|
||||
},
|
||||
deleteContribution(item) {
|
||||
this.$emit('delete-contribution', item)
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isPaginationVisible() {
|
||||
return this.showPagination && this.pageSize < this.contributionCount
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentPage() {
|
||||
this.updateListContributions()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,145 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import ContributionListItem from './ContributionListItem.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
describe('ContributionListItem', () => {
|
||||
let wrapper
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$d: jest.fn((d) => d),
|
||||
}
|
||||
|
||||
const propsData = {
|
||||
id: 1,
|
||||
createdAt: '26/07/2022',
|
||||
contributionDate: '07/06/2022',
|
||||
memo: 'Ich habe 10 Stunden die Elbwiesen von Müll befreit.',
|
||||
amount: '200',
|
||||
}
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(ContributionListItem, {
|
||||
localVue,
|
||||
mocks,
|
||||
propsData,
|
||||
})
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('has a DIV .contribution-list-item', () => {
|
||||
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')
|
||||
})
|
||||
|
||||
it('is x-circle when deletedAt is present', async () => {
|
||||
await wrapper.setProps({ deletedAt: new Date().toISOString() })
|
||||
expect(wrapper.vm.icon).toBe('x-circle')
|
||||
})
|
||||
|
||||
it('is check when confirmedAt is present', async () => {
|
||||
await wrapper.setProps({ confirmedAt: new Date().toISOString() })
|
||||
expect(wrapper.vm.icon).toBe('check')
|
||||
})
|
||||
})
|
||||
|
||||
describe('contribution variant', () => {
|
||||
it('is primary by default', () => {
|
||||
expect(wrapper.vm.variant).toBe('primary')
|
||||
})
|
||||
|
||||
it('is danger when deletedAt is present', async () => {
|
||||
await wrapper.setProps({ deletedAt: new Date().toISOString() })
|
||||
expect(wrapper.vm.variant).toBe('danger')
|
||||
})
|
||||
|
||||
it('is success at when confirmedAt is present', async () => {
|
||||
await wrapper.setProps({ confirmedAt: new Date().toISOString() })
|
||||
expect(wrapper.vm.variant).toBe('success')
|
||||
})
|
||||
})
|
||||
|
||||
describe('date', () => {
|
||||
it('is equal to createdAt', () => {
|
||||
expect(wrapper.vm.date).toBe(wrapper.vm.createdAt)
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete contribution', () => {
|
||||
let spy
|
||||
|
||||
describe('edit contribution', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.findAll('div.pointer').at(0).trigger('click')
|
||||
})
|
||||
|
||||
it('emits update contribution form', () => {
|
||||
expect(wrapper.emitted('update-contribution-form')).toEqual([
|
||||
[
|
||||
{
|
||||
id: 1,
|
||||
contributionDate: '07/06/2022',
|
||||
memo: 'Ich habe 10 Stunden die Elbwiesen von Müll befreit.',
|
||||
amount: '200',
|
||||
},
|
||||
],
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('confirm deletion', () => {
|
||||
beforeEach(() => {
|
||||
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
|
||||
spy.mockImplementation(() => Promise.resolve(true))
|
||||
wrapper.findAll('div.pointer').at(1).trigger('click')
|
||||
})
|
||||
|
||||
it('opens the modal', () => {
|
||||
expect(spy).toBeCalledWith('contribution.delete')
|
||||
})
|
||||
|
||||
it('emits delete contribution', () => {
|
||||
expect(wrapper.emitted('delete-contribution')).toEqual([[{ id: 1 }]])
|
||||
})
|
||||
})
|
||||
|
||||
describe('cancel deletion', () => {
|
||||
beforeEach(async () => {
|
||||
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
|
||||
spy.mockImplementation(() => Promise.resolve(false))
|
||||
await wrapper.findAll('div.pointer').at(1).trigger('click')
|
||||
})
|
||||
|
||||
it('does not emit delete contribution', () => {
|
||||
expect(wrapper.emitted('delete-contribution')).toBeFalsy()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
114
frontend/src/components/Contributions/ContributionListItem.vue
Normal file
114
frontend/src/components/Contributions/ContributionListItem.vue
Normal file
@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<div class="contribution-list-item">
|
||||
<slot>
|
||||
<div class="border p-3 w-100 mb-1" :class="`border-${variant}`">
|
||||
<div class="d-inline-flex">
|
||||
<div class="mr-2"><b-icon :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' : ''">
|
||||
{{ amount | GDD }}
|
||||
</div>
|
||||
{{ $t('math.minus') }}
|
||||
<div class="mx-2">{{ $d(new Date(date), 'short') }}</div>
|
||||
</div>
|
||||
<div class="mr-2">
|
||||
<span>{{ $t('contribution.date') }}</span>
|
||||
<span>
|
||||
{{ $d(new Date(contributionDate), 'monthAndYear') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mr-2">{{ memo }}</div>
|
||||
<div v-if="type === 'pending' && !firstName" class="d-flex flex-row-reverse">
|
||||
<div
|
||||
class="pointer ml-5"
|
||||
@click="
|
||||
$emit('update-contribution-form', {
|
||||
id: id,
|
||||
contributionDate: contributionDate,
|
||||
memo: memo,
|
||||
amount: amount,
|
||||
})
|
||||
"
|
||||
>
|
||||
<b-icon icon="pencil" class="h2"></b-icon>
|
||||
</div>
|
||||
<div class="pointer" @click="deleteContribution({ id })">
|
||||
<b-icon icon="trash" class="h2"></b-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'ContributionListItem',
|
||||
props: {
|
||||
id: {
|
||||
type: Number,
|
||||
},
|
||||
amount: {
|
||||
type: String,
|
||||
},
|
||||
memo: {
|
||||
type: String,
|
||||
},
|
||||
firstName: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
lastName: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
createdAt: {
|
||||
type: String,
|
||||
},
|
||||
contributionDate: {
|
||||
type: String,
|
||||
},
|
||||
deletedAt: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
confirmedBy: {
|
||||
type: Number,
|
||||
required: false,
|
||||
},
|
||||
confirmedAt: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
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'
|
||||
return 'bell-fill'
|
||||
},
|
||||
variant() {
|
||||
if (this.deletedAt) return 'danger'
|
||||
if (this.confirmedAt) return 'success'
|
||||
return 'primary'
|
||||
},
|
||||
date() {
|
||||
// if (this.deletedAt) return this.deletedAt
|
||||
// if (this.confirmedAt) return this.confirmedAt
|
||||
// return this.contributionDate
|
||||
return this.createdAt
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
deleteContribution(item) {
|
||||
this.$bvModal.msgBoxConfirm(this.$t('contribution.delete')).then(async (value) => {
|
||||
if (value) this.$emit('delete-contribution', item)
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -3,7 +3,13 @@
|
||||
<b-col>
|
||||
<b-card class="p-0 gradido-custom-background">
|
||||
<div class="h3 mb-4">{{ $t('gdd_per_link.created') }}</div>
|
||||
<clipboard-copy :link="link" @show-qr-code-button="showQrCodeButton" />
|
||||
<clipboard-copy
|
||||
:link="link"
|
||||
:amount="amount"
|
||||
:memo="memo"
|
||||
:validUntil="validUntil"
|
||||
@show-qr-code-button="showQrCodeButton"
|
||||
></clipboard-copy>
|
||||
|
||||
<div class="text-center">
|
||||
<figure-qr-code v-if="showQrcode" :link="link" />
|
||||
@ -27,10 +33,10 @@ export default {
|
||||
FigureQrCode,
|
||||
},
|
||||
props: {
|
||||
link: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
link: { type: String, required: true },
|
||||
amount: { type: String, required: true },
|
||||
memo: { type: String, required: true },
|
||||
validUntil: { type: String, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@ -69,6 +69,7 @@
|
||||
:per-page="pageSize"
|
||||
:total-rows="transactionCount"
|
||||
align="center"
|
||||
:hide-ellipsis="true"
|
||||
></b-pagination>
|
||||
|
||||
<div v-if="transactionCount <= 0" class="mt-4 text-center">
|
||||
|
||||
@ -36,6 +36,7 @@
|
||||
:per-page="pageSize"
|
||||
:total-rows="transactionGdtCount"
|
||||
align="center"
|
||||
:hide-ellipsis="true"
|
||||
></b-pagination>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -45,7 +45,7 @@ describe('LanguageSwitch', () => {
|
||||
expect(wrapper.find('div.language-switch').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
describe('with locales en and de', () => {
|
||||
describe('with locales en, de and es', () => {
|
||||
describe('empty store', () => {
|
||||
describe('navigator language is "en-US"', () => {
|
||||
const languageGetter = jest.spyOn(navigator, 'language', 'get')
|
||||
@ -69,11 +69,22 @@ describe('LanguageSwitch', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('navigator language is "es-ES" (not supported)', () => {
|
||||
describe('navigator language is "es-ES"', () => {
|
||||
const languageGetter = jest.spyOn(navigator, 'language', 'get')
|
||||
|
||||
it('shows Español as language ', async () => {
|
||||
languageGetter.mockReturnValue('es-ES')
|
||||
wrapper.vm.setCurrentLanguage()
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.find('button.dropdown-toggle').text()).toBe('Español - es')
|
||||
})
|
||||
})
|
||||
|
||||
describe('navigator language is "fr-FR" (not supported)', () => {
|
||||
const languageGetter = jest.spyOn(navigator, 'language', 'get')
|
||||
|
||||
it('shows English as language ', async () => {
|
||||
languageGetter.mockReturnValue('es-ES')
|
||||
languageGetter.mockReturnValue('fr-FR')
|
||||
wrapper.vm.setCurrentLanguage()
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.find('button.dropdown-toggle').text()).toBe('English - en')
|
||||
@ -101,9 +112,18 @@ describe('LanguageSwitch', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('language "es" in store', () => {
|
||||
it('shows Español as language', async () => {
|
||||
wrapper.vm.$store.state.language = 'es'
|
||||
wrapper.vm.setCurrentLanguage()
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.find('button.dropdown-toggle').text()).toBe('Español - es')
|
||||
})
|
||||
})
|
||||
|
||||
describe('dropdown menu', () => {
|
||||
it('has English and German as languages to choose', () => {
|
||||
expect(wrapper.findAll('li')).toHaveLength(2)
|
||||
expect(wrapper.findAll('li')).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('has English as first language to choose', () => {
|
||||
@ -113,6 +133,10 @@ describe('LanguageSwitch', () => {
|
||||
it('has German as second language to choose', () => {
|
||||
expect(wrapper.findAll('li').at(1).text()).toBe('Deutsch')
|
||||
})
|
||||
|
||||
it('has Español as second language to choose', () => {
|
||||
expect(wrapper.findAll('li').at(2).text()).toBe('Español')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -66,10 +66,19 @@ describe('LanguageSwitch', () => {
|
||||
expect(wrapper.findAll('span.locales').at(1).text()).toBe('Deutsch')
|
||||
})
|
||||
})
|
||||
describe('navigator language is "es-ES" (not supported)', () => {
|
||||
describe('navigator language is "es-ES"', () => {
|
||||
const languageGetter = jest.spyOn(navigator, 'language', 'get')
|
||||
it('shows Español as language ', async () => {
|
||||
languageGetter.mockReturnValue('es-ES')
|
||||
wrapper.vm.setCurrentLanguage()
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.findAll('span.locales').at(2).text()).toBe('Español')
|
||||
})
|
||||
})
|
||||
describe('navigator language is "fr-FR" (not supported)', () => {
|
||||
const languageGetter = jest.spyOn(navigator, 'language', 'get')
|
||||
it('shows English as language ', async () => {
|
||||
languageGetter.mockReturnValue('es-ES')
|
||||
languageGetter.mockReturnValue('fr-FR')
|
||||
wrapper.vm.setCurrentLanguage()
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.findAll('span.locales').at(0).text()).toBe('English')
|
||||
@ -93,9 +102,17 @@ describe('LanguageSwitch', () => {
|
||||
expect(wrapper.findAll('span.locales').at(1).text()).toBe('Deutsch')
|
||||
})
|
||||
})
|
||||
describe('language "es" in store', () => {
|
||||
it('shows Español as language', async () => {
|
||||
wrapper.vm.$store.state.language = 'es'
|
||||
wrapper.vm.setCurrentLanguage()
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.findAll('span.locales').at(2).text()).toBe('Español')
|
||||
})
|
||||
})
|
||||
describe('language menu', () => {
|
||||
it('has English and German as languages to choose', () => {
|
||||
expect(wrapper.findAll('span.locales')).toHaveLength(2)
|
||||
it('has English, German and Español as languages to choose', () => {
|
||||
expect(wrapper.findAll('span.locales')).toHaveLength(3)
|
||||
})
|
||||
it('has English as first language to choose', () => {
|
||||
expect(wrapper.findAll('span.locales').at(0).text()).toBe('English')
|
||||
@ -103,6 +120,9 @@ describe('LanguageSwitch', () => {
|
||||
it('has German as second language to choose', () => {
|
||||
expect(wrapper.findAll('span.locales').at(1).text()).toBe('Deutsch')
|
||||
})
|
||||
it('has Español as third language to choose', () => {
|
||||
expect(wrapper.findAll('span.locales').at(2).text()).toBe('Español')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -16,6 +16,7 @@ export default {
|
||||
options: [
|
||||
{ value: 'de', text: this.$t('settings.language.de') },
|
||||
{ value: 'en', text: this.$t('settings.language.en') },
|
||||
{ value: 'es', text: this.$t('settings.language.es') },
|
||||
],
|
||||
}
|
||||
},
|
||||
|
||||
@ -17,7 +17,7 @@ const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$store: {
|
||||
state: {
|
||||
hasElopage: false,
|
||||
hasElopage: true,
|
||||
isAdmin: true,
|
||||
},
|
||||
},
|
||||
@ -39,15 +39,17 @@ describe('Navbar', () => {
|
||||
expect(wrapper.find('div.component-navbar').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
describe('navigation Navbar', () => {
|
||||
describe('navigation Navbar (general elements)', () => {
|
||||
it('has .navbar-brand in the navbar', () => {
|
||||
expect(wrapper.find('.navbar-brand').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('has b-navbar-toggle in the navbar', () => {
|
||||
expect(wrapper.find('.navbar-toggler').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('has ten b-nav-item in the navbar', () => {
|
||||
expect(wrapper.findAll('.nav-item')).toHaveLength(10)
|
||||
expect(wrapper.findAll('.nav-item')).toHaveLength(11)
|
||||
})
|
||||
|
||||
it('has first nav-item "amount GDD" in navbar', () => {
|
||||
@ -57,31 +59,57 @@ describe('Navbar', () => {
|
||||
it('has first nav-item "navigation.overview" in navbar', () => {
|
||||
expect(wrapper.findAll('.nav-item').at(3).text()).toEqual('navigation.overview')
|
||||
})
|
||||
|
||||
it('has first nav-item "navigation.send" in navbar', () => {
|
||||
expect(wrapper.findAll('.nav-item').at(4).text()).toEqual('navigation.send')
|
||||
})
|
||||
|
||||
it('has first nav-item "navigation.transactions" in navbar', () => {
|
||||
expect(wrapper.findAll('.nav-item').at(5).text()).toEqual('navigation.transactions')
|
||||
})
|
||||
it('has first nav-item "navigation.profile" in navbar', () => {
|
||||
expect(wrapper.findAll('.nav-item').at(6).text()).toEqual('navigation.profile')
|
||||
|
||||
it('has first nav-item "navigation.transactions" in navbar', () => {
|
||||
expect(wrapper.findAll('.nav-item').at(6).text()).toEqual('navigation.community')
|
||||
})
|
||||
|
||||
it('has first nav-item "navigation.profile" in navbar', () => {
|
||||
expect(wrapper.findAll('.nav-item').at(7).text()).toEqual('navigation.profile')
|
||||
})
|
||||
})
|
||||
|
||||
describe('navigation Navbar (user has an elopage account)', () => {
|
||||
it('has a link to the members area', () => {
|
||||
expect(wrapper.findAll('.nav-item').at(7).text()).toContain('navigation.members_area')
|
||||
expect(wrapper.findAll('.nav-item').at(7).find('a').attributes('href')).toBe(
|
||||
expect(wrapper.findAll('.nav-item').at(8).text()).toContain('navigation.members_area')
|
||||
expect(wrapper.findAll('.nav-item').at(8).find('a').attributes('href')).toBe(
|
||||
'https://elopage.com',
|
||||
)
|
||||
})
|
||||
|
||||
it('has first nav-item "navigation.admin_area" in navbar', () => {
|
||||
expect(wrapper.findAll('.nav-item').at(9).text()).toEqual('navigation.admin_area')
|
||||
})
|
||||
|
||||
it('has first nav-item "navigation.logout" in navbar', () => {
|
||||
expect(wrapper.findAll('.nav-item').at(10).text()).toEqual('navigation.logout')
|
||||
})
|
||||
})
|
||||
|
||||
describe('navigation Navbar (user has no elopage account)', () => {
|
||||
beforeAll(() => {
|
||||
mocks.$store.state.hasElopage = false
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('has first nav-item "navigation.admin_area" in navbar', () => {
|
||||
expect(wrapper.findAll('.nav-item').at(8).text()).toEqual('navigation.admin_area')
|
||||
})
|
||||
|
||||
it('has first nav-item "navigation.logout" in navbar', () => {
|
||||
expect(wrapper.findAll('.nav-item').at(9).text()).toEqual('navigation.logout')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('check watch visible true', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.setProps({ visible: true })
|
||||
|
||||
@ -52,17 +52,18 @@
|
||||
<b-icon icon="layout-text-sidebar-reverse" aria-hidden="true"></b-icon>
|
||||
{{ $t('navigation.transactions') }}
|
||||
</b-nav-item>
|
||||
<b-nav-item to="/community" class="mb-3">
|
||||
<b-icon icon="people" aria-hidden="true"></b-icon>
|
||||
{{ $t('navigation.community') }}
|
||||
</b-nav-item>
|
||||
<b-nav-item to="/profile" class="mb-3">
|
||||
<b-icon icon="gear" aria-hidden="true"></b-icon>
|
||||
{{ $t('navigation.profile') }}
|
||||
</b-nav-item>
|
||||
<br />
|
||||
<b-nav-item :href="elopageUri" class="mb-3" target="_blank">
|
||||
<b-nav-item v-if="$store.state.hasElopage" :href="elopageUri" class="mb-3" target="_blank">
|
||||
<b-icon icon="link45deg" aria-hidden="true"></b-icon>
|
||||
{{ $t('navigation.members_area') }}
|
||||
<b-badge v-if="!$store.state.hasElopage" pill variant="danger">
|
||||
{{ $t('math.exclaim') }}
|
||||
</b-badge>
|
||||
</b-nav-item>
|
||||
<b-nav-item class="mb-3" v-if="$store.state.isAdmin" @click="$emit('admin')">
|
||||
<b-icon icon="shield-check" aria-hidden="true"></b-icon>
|
||||
|
||||
@ -27,15 +27,12 @@ describe('Sidebar', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('renders the component', () => {
|
||||
expect(wrapper.find('div#component-sidebar').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
describe('navigation Navbar', () => {
|
||||
it('has seven b-nav-item in the navbar', () => {
|
||||
expect(wrapper.findAll('.nav-item')).toHaveLength(7)
|
||||
})
|
||||
|
||||
describe('navigation Navbar (general elements)', () => {
|
||||
it('has first nav-item "navigation.overview" in navbar', () => {
|
||||
expect(wrapper.findAll('.nav-item').at(0).text()).toEqual('navigation.overview')
|
||||
})
|
||||
@ -47,16 +44,49 @@ describe('Sidebar', () => {
|
||||
it('has first nav-item "navigation.transactions" in navbar', () => {
|
||||
expect(wrapper.findAll('.nav-item').at(2).text()).toEqual('navigation.transactions')
|
||||
})
|
||||
|
||||
it('has first nav-item "navigation.community" in navbar', () => {
|
||||
expect(wrapper.findAll('.nav-item').at(3).text()).toContain('navigation.community')
|
||||
})
|
||||
|
||||
it('has first nav-item "navigation.profile" in navbar', () => {
|
||||
expect(wrapper.findAll('.nav-item').at(3).text()).toEqual('navigation.profile')
|
||||
expect(wrapper.findAll('.nav-item').at(4).text()).toEqual('navigation.profile')
|
||||
})
|
||||
})
|
||||
|
||||
describe('navigation Navbar (user has an elopage account)', () => {
|
||||
it('has eight b-nav-item in the navbar', () => {
|
||||
expect(wrapper.findAll('.nav-item')).toHaveLength(8)
|
||||
})
|
||||
|
||||
it('has a link to the members area', () => {
|
||||
expect(wrapper.findAll('.nav-item').at(4).text()).toContain('navigation.members_area')
|
||||
expect(wrapper.findAll('.nav-item').at(4).find('a').attributes('href')).toBe('#')
|
||||
expect(wrapper.findAll('.nav-item').at(5).text()).toEqual('navigation.members_area')
|
||||
expect(wrapper.findAll('.nav-item').at(5).find('a').attributes('href')).toBe('#')
|
||||
})
|
||||
|
||||
it('has first nav-item "navigation.admin_area" in navbar', () => {
|
||||
expect(wrapper.findAll('.nav-item').at(6).text()).toEqual('navigation.admin_area')
|
||||
})
|
||||
|
||||
it('has first nav-item "navigation.logout" in navbar', () => {
|
||||
expect(wrapper.findAll('.nav-item').at(7).text()).toEqual('navigation.logout')
|
||||
})
|
||||
})
|
||||
|
||||
describe('navigation Navbar (user has no elopage account)', () => {
|
||||
beforeAll(() => {
|
||||
mocks.$store.state.hasElopage = false
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('has seven b-nav-item in the navbar', () => {
|
||||
expect(wrapper.findAll('.nav-item')).toHaveLength(7)
|
||||
})
|
||||
|
||||
it('has first nav-item "navigation.admin_area" in navbar', () => {
|
||||
expect(wrapper.findAll('.nav-item').at(5).text()).toEqual('navigation.admin_area')
|
||||
})
|
||||
|
||||
it('has first nav-item "navigation.logout" in navbar', () => {
|
||||
expect(wrapper.findAll('.nav-item').at(6).text()).toEqual('navigation.logout')
|
||||
})
|
||||
|
||||
@ -16,6 +16,10 @@
|
||||
<b-icon icon="layout-text-sidebar-reverse" aria-hidden="true"></b-icon>
|
||||
{{ $t('navigation.transactions') }}
|
||||
</b-nav-item>
|
||||
<b-nav-item to="/community" class="mb-3">
|
||||
<b-icon icon="people" aria-hidden="true"></b-icon>
|
||||
{{ $t('navigation.community') }}
|
||||
</b-nav-item>
|
||||
<b-nav-item to="/profile" class="mb-3">
|
||||
<b-icon icon="gear" aria-hidden="true"></b-icon>
|
||||
{{ $t('navigation.profile') }}
|
||||
@ -23,12 +27,14 @@
|
||||
</b-nav>
|
||||
<hr />
|
||||
<b-nav vertical class="w-100">
|
||||
<b-nav-item class="mb-3" :href="elopageUri" target="_blank">
|
||||
<b-nav-item
|
||||
v-if="$store.state.hasElopage"
|
||||
class="mb-3"
|
||||
:href="elopageUri"
|
||||
target="_blank"
|
||||
>
|
||||
<b-icon icon="link45deg" aria-hidden="true"></b-icon>
|
||||
{{ $t('navigation.members_area') }}
|
||||
<b-badge v-if="!$store.state.hasElopage" pill variant="danger">
|
||||
{{ $t('math.exclaim') }}
|
||||
</b-badge>
|
||||
</b-nav-item>
|
||||
<b-nav-item class="mb-3" v-if="$store.state.isAdmin" @click="$emit('admin')">
|
||||
<b-icon icon="shield-check" aria-hidden="true"></b-icon>
|
||||
|
||||
@ -18,17 +18,17 @@
|
||||
<b-icon icon="three-dots-vertical"></b-icon>
|
||||
</template>
|
||||
|
||||
<b-dropdown-item v-if="validLink" class="test-copy-link" @click="copy">
|
||||
<b-dropdown-item v-if="validLink" class="test-copy-link" @click="copyLink">
|
||||
<b-icon icon="clipboard"></b-icon>
|
||||
{{ $t('gdd_per_link.copy') }}
|
||||
{{ $t('gdd_per_link.copy-link') }}
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item
|
||||
v-if="validLink"
|
||||
class="test-copy-text pt-3"
|
||||
@click="copyLinkWithText()"
|
||||
@click="copyLinkWithText"
|
||||
>
|
||||
<b-icon icon="clipboard-plus"></b-icon>
|
||||
{{ $t('gdd_per_link.copy-with-text') }}
|
||||
{{ $t('gdd_per_link.copy-link-with-text') }}
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item
|
||||
v-if="validLink"
|
||||
@ -76,9 +76,11 @@ import MemoRow from '../TransactionRows/MemoRow'
|
||||
import DateRow from '../TransactionRows/DateRow'
|
||||
import DecayRow from '../TransactionRows/DecayRow'
|
||||
import FigureQrCode from '@/components/QrCode/FigureQrCode.vue'
|
||||
import { copyLinks } from '../../mixins/copyLinks'
|
||||
|
||||
export default {
|
||||
name: 'TransactionLink',
|
||||
mixins: [copyLinks],
|
||||
components: {
|
||||
TypeIcon,
|
||||
AmountAndNameRow,
|
||||
@ -88,43 +90,10 @@ export default {
|
||||
FigureQrCode,
|
||||
},
|
||||
props: {
|
||||
amount: { type: String, required: true },
|
||||
link: { type: String, required: true },
|
||||
holdAvailableAmount: { type: String, required: true },
|
||||
id: { type: Number, required: true },
|
||||
memo: { type: String, required: true },
|
||||
validUntil: { type: String, required: true },
|
||||
},
|
||||
methods: {
|
||||
copy() {
|
||||
navigator.clipboard
|
||||
.writeText(this.link)
|
||||
.then(() => {
|
||||
this.toastSuccess(this.$t('gdd_per_link.link-copied'))
|
||||
})
|
||||
.catch(() => {
|
||||
this.$bvModal.show('modalPopoverCopyError' + this.id)
|
||||
this.toastError(this.$t('gdd_per_link.not-copied'))
|
||||
})
|
||||
},
|
||||
copyLinkWithText() {
|
||||
navigator.clipboard
|
||||
.writeText(
|
||||
`${this.link}
|
||||
${this.$store.state.firstName} ${this.$t('transaction-link.send_you')} ${this.amount} Gradido.
|
||||
"${this.memo}"
|
||||
${this.$t('gdd_per_link.credit-your-gradido')} ${this.$t('gdd_per_link.validUntilDate', {
|
||||
date: this.$d(new Date(this.validUntil), 'short'),
|
||||
})}`,
|
||||
)
|
||||
.then(() => {
|
||||
this.toastSuccess(this.$t('gdd_per_link.link-and-text-copied'))
|
||||
})
|
||||
.catch(() => {
|
||||
this.$bvModal.show('modalPopoverCopyError' + this.id)
|
||||
this.toastError(this.$t('gdd_per_link.not-copied'))
|
||||
})
|
||||
},
|
||||
deleteLink() {
|
||||
this.$bvModal.msgBoxConfirm(this.$t('gdd_per_link.delete-the-link')).then(async (value) => {
|
||||
if (value)
|
||||
|
||||
@ -74,6 +74,9 @@ export const createTransactionLink = gql`
|
||||
mutation($amount: Decimal!, $memo: String!) {
|
||||
createTransactionLink(amount: $amount, memo: $memo) {
|
||||
link
|
||||
amount
|
||||
memo
|
||||
validUntil
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -89,3 +92,33 @@ export const redeemTransactionLink = gql`
|
||||
redeemTransactionLink(code: $code)
|
||||
}
|
||||
`
|
||||
|
||||
export const createContribution = gql`
|
||||
mutation($creationDate: String!, $memo: String!, $amount: Decimal!) {
|
||||
createContribution(creationDate: $creationDate, memo: $memo, amount: $amount) {
|
||||
amount
|
||||
memo
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const updateContribution = gql`
|
||||
mutation($contributionId: Int!, $amount: Decimal!, $memo: String!, $creationDate: String!) {
|
||||
updateContribution(
|
||||
contributionId: $contributionId
|
||||
amount: $amount
|
||||
memo: $memo
|
||||
creationDate: $creationDate
|
||||
) {
|
||||
id
|
||||
amount
|
||||
memo
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const deleteContribution = gql`
|
||||
mutation($id: Int!) {
|
||||
deleteContribution(id: $id)
|
||||
}
|
||||
`
|
||||
|
||||
@ -162,3 +162,50 @@ export const listTransactionLinks = gql`
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const listContributions = gql`
|
||||
query(
|
||||
$currentPage: Int = 1
|
||||
$pageSize: Int = 25
|
||||
$order: Order = DESC
|
||||
$filterConfirmed: Boolean = false
|
||||
) {
|
||||
listContributions(
|
||||
currentPage: $currentPage
|
||||
pageSize: $pageSize
|
||||
order: $order
|
||||
filterConfirmed: $filterConfirmed
|
||||
) {
|
||||
contributionCount
|
||||
contributionList {
|
||||
id
|
||||
amount
|
||||
memo
|
||||
createdAt
|
||||
contributionDate
|
||||
confirmedAt
|
||||
confirmedBy
|
||||
deletedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const listAllContributions = gql`
|
||||
query($currentPage: Int = 1, $pageSize: Int = 25, $order: Order = DESC) {
|
||||
listAllContributions(currentPage: $currentPage, pageSize: $pageSize, order: $order) {
|
||||
contributionCount
|
||||
contributionList {
|
||||
id
|
||||
firstName
|
||||
lastName
|
||||
amount
|
||||
memo
|
||||
createdAt
|
||||
contributionDate
|
||||
confirmedAt
|
||||
confirmedBy
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -3,6 +3,7 @@ import VueI18n from 'vue-i18n'
|
||||
|
||||
import en from 'vee-validate/dist/locale/en'
|
||||
import de from 'vee-validate/dist/locale/de'
|
||||
import es from 'vee-validate/dist/locale/es'
|
||||
|
||||
Vue.use(VueI18n)
|
||||
|
||||
@ -26,6 +27,12 @@ function loadLocaleMessages() {
|
||||
...messages[locale],
|
||||
}
|
||||
}
|
||||
if (locale === 'es') {
|
||||
messages[locale] = {
|
||||
validations: es,
|
||||
...messages[locale],
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
return messages
|
||||
@ -58,6 +65,19 @@ const numberFormats = {
|
||||
useGrouping: false,
|
||||
},
|
||||
},
|
||||
es: {
|
||||
decimal: {
|
||||
style: 'decimal',
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
},
|
||||
ungroupedDecimal: {
|
||||
style: 'decimal',
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
useGrouping: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const dateTimeFormats = {
|
||||
@ -75,6 +95,19 @@ const dateTimeFormats = {
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
},
|
||||
monthShort: {
|
||||
month: 'short',
|
||||
},
|
||||
month: {
|
||||
month: 'long',
|
||||
},
|
||||
year: {
|
||||
year: 'numeric',
|
||||
},
|
||||
monthAndYear: {
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
},
|
||||
},
|
||||
de: {
|
||||
short: {
|
||||
@ -90,6 +123,47 @@ const dateTimeFormats = {
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
},
|
||||
monthShort: {
|
||||
month: 'short',
|
||||
},
|
||||
month: {
|
||||
month: 'long',
|
||||
},
|
||||
year: {
|
||||
year: 'numeric',
|
||||
},
|
||||
monthAndYear: {
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
},
|
||||
},
|
||||
es: {
|
||||
short: {
|
||||
day: 'numeric',
|
||||
month: 'numeric',
|
||||
year: 'numeric',
|
||||
},
|
||||
long: {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
weekday: 'long',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
},
|
||||
monthShort: {
|
||||
month: 'short',
|
||||
},
|
||||
month: {
|
||||
month: 'long',
|
||||
},
|
||||
year: {
|
||||
year: 'numeric',
|
||||
},
|
||||
monthAndYear: {
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -26,9 +26,38 @@
|
||||
"community": "Gemeinschaft",
|
||||
"continue-to-registration": "Weiter zur Registrierung",
|
||||
"current-community": "Aktuelle Gemeinschaft",
|
||||
"myContributions": "Meine Beiträge zum Gemeinwohl",
|
||||
"other-communities": "Weitere Gemeinschaften",
|
||||
"submitContribution": "Beitrag einreichen",
|
||||
"switch-to-this-community": "zu dieser Gemeinschaft wechseln"
|
||||
},
|
||||
"contribution": {
|
||||
"activity": "Tätigkeit",
|
||||
"alert": {
|
||||
"communityNoteList": "Hier findest du alle eingereichten und bestätigten Beiträge von allen Mitgliedern aus dieser Gemeinschaft.",
|
||||
"confirm": "bestätigt",
|
||||
"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"
|
||||
},
|
||||
"date": "Beitrag für:",
|
||||
"delete": "Beitrag löschen! Bist du sicher?",
|
||||
"deleted": "Der Beitrag wurde gelöscht! Wird aber sichtbar bleiben.",
|
||||
"formText": {
|
||||
"bringYourTalentsTo": "Bring dich mit deinen Talenten in die Gemeinschaft ein! Dein freiwilliges Engagement honorieren wir mit 20 GDD pro Stunde bis maximal 1.000 GDD im Monat.",
|
||||
"describeYourCommunity": "Beschreibe deine Gemeinwohl-Tätigkeit mit Angabe der Stunden und trage einen Betrag von 20 GDD pro Stunde ein! Nach Bestätigung durch einen Moderator wird der Betrag deinem Konto gutgeschrieben.",
|
||||
"maxGDDforMonth": "Du kannst für den ausgewählten Monat nur noch maximal {amount} GDD einreichen.",
|
||||
"openAmountForMonth": "Für <b>{monthAndYear}</b> kannst du noch <b>{creation}</b> GDD einreichen.",
|
||||
"yourContribution": "Dein Beitrag zum Gemeinwohl"
|
||||
},
|
||||
"noDateSelected": "Wähle irgendein Datum im Monat",
|
||||
"selectDate": "Wann war dein Beitrag?",
|
||||
"submit": "Einreichen",
|
||||
"submitted": "Der Beitrag wurde eingereicht.",
|
||||
"updated": "Der Beitrag wurde geändert.",
|
||||
"yourActivity": "Bitte trage eine Tätigkeit ein!"
|
||||
},
|
||||
"contribution-link": {
|
||||
"thanksYouWith": "dankt dir mit"
|
||||
},
|
||||
@ -78,17 +107,18 @@
|
||||
"amount": "Betrag",
|
||||
"at": "am",
|
||||
"cancel": "Abbrechen",
|
||||
"change": "Ändern",
|
||||
"check_now": "Jetzt prüfen",
|
||||
"close": "Schließen",
|
||||
"current_balance": "Aktueller Kontostand",
|
||||
"date": "Datum",
|
||||
"description": "Beschreibung",
|
||||
"edit": "Bearbeiten",
|
||||
"email": "E-Mail",
|
||||
"firstname": "Vorname",
|
||||
"from": "Von",
|
||||
"generate_now": "Jetzt generieren",
|
||||
"lastname": "Nachname",
|
||||
"mandatoryField": "Pflichtfeld",
|
||||
"memo": "Nachricht",
|
||||
"message": "Nachricht",
|
||||
"new_balance": "Neuer Kontostand nach Bestätigung",
|
||||
@ -123,8 +153,8 @@
|
||||
"GDD": "GDD",
|
||||
"gdd_per_link": {
|
||||
"choose-amount": "Wähle einen Betrag aus, welchen du per Link versenden möchtest. Du kannst auch noch eine Nachricht eintragen. Beim Klick „Jetzt generieren“ wird ein Link erstellt, den du versenden kannst.",
|
||||
"copy": "kopieren",
|
||||
"copy-with-text": "Link und Text kopieren",
|
||||
"copy-link": "Link kopieren",
|
||||
"copy-link-with-text": "Link und Text kopieren",
|
||||
"created": "Der Link wurde erstellt!",
|
||||
"credit-your-gradido": "Damit die Gradido gutgeschrieben werden können, klicke auf den Link!",
|
||||
"decay-14-day": "Vergänglichkeit für 14 Tage",
|
||||
@ -176,8 +206,8 @@
|
||||
"login": "Anmeldung",
|
||||
"math": {
|
||||
"aprox": "~",
|
||||
"asterisk": "*",
|
||||
"equal": "=",
|
||||
"exclaim": "!",
|
||||
"minus": "−",
|
||||
"pipe": "|"
|
||||
},
|
||||
@ -193,6 +223,7 @@
|
||||
},
|
||||
"navigation": {
|
||||
"admin_area": "Adminbereich",
|
||||
"community": "Gemeinschaft",
|
||||
"logout": "Abmelden",
|
||||
"members_area": "Mitgliederbereich",
|
||||
"overview": "Übersicht",
|
||||
@ -215,6 +246,7 @@
|
||||
"changeLanguage": "Sprache ändern",
|
||||
"de": "Deutsch",
|
||||
"en": "English",
|
||||
"es": "Español",
|
||||
"success": "Deine Sprache wurde erfolgreich geändert."
|
||||
},
|
||||
"name": {
|
||||
@ -271,6 +303,7 @@
|
||||
"days": "Tage",
|
||||
"hours": "Stunden",
|
||||
"minutes": "Minuten",
|
||||
"month": "Monat",
|
||||
"months": "Monate",
|
||||
"seconds": "Sekunden",
|
||||
"years": "Jahr"
|
||||
|
||||
@ -26,9 +26,38 @@
|
||||
"community": "Community",
|
||||
"continue-to-registration": "Continue to registration",
|
||||
"current-community": "Current community",
|
||||
"myContributions": "My contributions to the common good",
|
||||
"other-communities": "Other communities",
|
||||
"submitContribution": "Submit contribution",
|
||||
"switch-to-this-community": "Switch to this community"
|
||||
},
|
||||
"contribution": {
|
||||
"activity": "Activity",
|
||||
"alert": {
|
||||
"communityNoteList": "Here you will find all submitted and confirmed contributions from all members of this community.",
|
||||
"confirm": "confirmed",
|
||||
"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"
|
||||
},
|
||||
"date": "Contribution for:",
|
||||
"delete": "Delete Contribution! Are you sure?",
|
||||
"deleted": "The contribution has been deleted! But it will remain visible.",
|
||||
"formText": {
|
||||
"bringYourTalentsTo": "Bring your talents to the community! Your voluntary commitment will be rewarded with 20 GDD per hour up to a maximum of 1,000 GDD per month.",
|
||||
"describeYourCommunity": "Describe your community service activity with hours and enter an amount of 20 GDD per hour! After confirmation by a moderator, the amount will be credited to your account.",
|
||||
"maxGDDforMonth": "You can only submit a maximum of {amount} GDD for the selected month.",
|
||||
"openAmountForMonth": "For <b>{monthAndYear}</b>, you can still submit <b>{creation}</b> GDD.",
|
||||
"yourContribution": "Your contribution to the common good"
|
||||
},
|
||||
"noDateSelected": "Choose any date in the month",
|
||||
"selectDate": "When was your contribution?",
|
||||
"submit": "Submit",
|
||||
"submitted": "The contribution was submitted.",
|
||||
"updated": "The contribution was changed.",
|
||||
"yourActivity": "Please enter an activity!"
|
||||
},
|
||||
"contribution-link": {
|
||||
"thanksYouWith": "thanks you with"
|
||||
},
|
||||
@ -78,17 +107,18 @@
|
||||
"amount": "Amount",
|
||||
"at": "at",
|
||||
"cancel": "Cancel",
|
||||
"change": "Change",
|
||||
"check_now": "Check now",
|
||||
"close": "Close",
|
||||
"current_balance": "Current Balance",
|
||||
"date": "Date",
|
||||
"description": "Description",
|
||||
"edit": "Edit",
|
||||
"email": "Email",
|
||||
"firstname": "Firstname",
|
||||
"from": "from",
|
||||
"generate_now": "Generate now",
|
||||
"lastname": "Lastname",
|
||||
"mandatoryField": "mandatory field",
|
||||
"memo": "Message",
|
||||
"message": "Message",
|
||||
"new_balance": "Account balance after confirmation",
|
||||
@ -123,8 +153,8 @@
|
||||
"GDD": "GDD",
|
||||
"gdd_per_link": {
|
||||
"choose-amount": "Select an amount that you would like to send via link. You can also enter a message. Click 'Generate now' to create a link that you can share.",
|
||||
"copy": "copy",
|
||||
"copy-with-text": "Copy link and text",
|
||||
"copy-link": "Copy link",
|
||||
"copy-link-with-text": "Copy link and text",
|
||||
"created": "Link was created!",
|
||||
"credit-your-gradido": "For the Gradido to be credited, click on the link!",
|
||||
"decay-14-day": "Decay for 14 days",
|
||||
@ -176,8 +206,8 @@
|
||||
"login": "Login",
|
||||
"math": {
|
||||
"aprox": "~",
|
||||
"asterisk": "*",
|
||||
"equal": "=",
|
||||
"exclaim": "!",
|
||||
"minus": "−",
|
||||
"pipe": "|"
|
||||
},
|
||||
@ -193,6 +223,7 @@
|
||||
},
|
||||
"navigation": {
|
||||
"admin_area": "Admin Area",
|
||||
"community": "Community",
|
||||
"logout": "Logout",
|
||||
"members_area": "Members area",
|
||||
"overview": "Overview",
|
||||
@ -215,6 +246,7 @@
|
||||
"changeLanguage": "Change language",
|
||||
"de": "Deutsch",
|
||||
"en": "English",
|
||||
"es": "Español",
|
||||
"success": "Your language has been successfully updated."
|
||||
},
|
||||
"name": {
|
||||
@ -271,6 +303,7 @@
|
||||
"days": "Days",
|
||||
"hours": "Hours",
|
||||
"minutes": "Minutes",
|
||||
"month": "Month",
|
||||
"months": "Months",
|
||||
"seconds": "Seconds",
|
||||
"years": "Year"
|
||||
|
||||
324
frontend/src/locales/es.json
Normal file
324
frontend/src/locales/es.json
Normal file
@ -0,0 +1,324 @@
|
||||
{
|
||||
"100": "100%",
|
||||
"1000thanks": "1000 Gracias, por estar con nosotros!",
|
||||
"125": "125%",
|
||||
"85": "85%",
|
||||
"advanced-calculation": "Proyección",
|
||||
"auth": {
|
||||
"left": {
|
||||
"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",
|
||||
"community": {
|
||||
"choose-another-community": "Escoger otra comunidad",
|
||||
"community": "Comunidad",
|
||||
"continue-to-registration": "Continuar con el registro",
|
||||
"current-community": "Comunidad actual",
|
||||
"myContributions": "Mis contribuciones al bien común",
|
||||
"other-communities": "Otras comunidades",
|
||||
"submitContribution": "Aportar una contribución",
|
||||
"switch-to-this-community": "cambiar a esta comunidad"
|
||||
},
|
||||
"contribution": {
|
||||
"activity": "Actividad",
|
||||
"alert": {
|
||||
"communityNoteList": "Aquí encontrarás todas las contribuciones enviadas y confirmadas de todos los miembros de esta comunidad.",
|
||||
"confirm": "confirmado",
|
||||
"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"
|
||||
},
|
||||
"date": "Contribución para:",
|
||||
"delete": "Eliminar la contribución. ¿Estás seguro?",
|
||||
"deleted": "¡La contribución ha sido borrada! Pero seguirá siendo visible.",
|
||||
"formText": {
|
||||
"bringYourTalentsTo": "¡Contribuye a la comunidad con tus talentos! Premiamos tu compromiso voluntario con 20 GDD por hora hasta un máximo de 1.000 GDD al mes.",
|
||||
"describeYourCommunity": "¡Describe tu contribución al bien-común con detalles de las horas e introduce una cantidad de 20 GDD por hora! Tras la confirmación de un moderador, el importe se abonará en tu cuenta.",
|
||||
"maxGDDforMonth": "Sólo puede presentar un máximo de {amount} GDD para el mes seleccionado.",
|
||||
"openAmountForMonth": "Para <b>{monthAndYear}</b> aún puedes presentar <b>{creation}</b> GDD.",
|
||||
"yourContribution": "Tu contribución a la comunidad."
|
||||
},
|
||||
"noDateSelected": "Elige cualquier fecha del mes.",
|
||||
"selectDate": "¿Cuando fue tu contribución?",
|
||||
"submit": "Enviar",
|
||||
"submitted": "Tu contribución ha sido enviada.",
|
||||
"updated": "La contribución se modificó.",
|
||||
"yourActivity": "¡Por favor, introduce una actividad!"
|
||||
},
|
||||
"contribution-link": {
|
||||
"thanksYouWith": "agradecidos con"
|
||||
},
|
||||
"decay": {
|
||||
"before_startblock_transaction": "Esta transacción no implica disminución en su valor.",
|
||||
"calculation_decay": "Cálculo de la disminución gradual del valor",
|
||||
"calculation_total": "Cálculo de la suma total",
|
||||
"decay": "Disminución gradual del valor",
|
||||
"decay_introduced": "La disminución gradual empezó el:",
|
||||
"decay_since_last_transaction": "Disminución gradual",
|
||||
"last_transaction": "Transacción anterior",
|
||||
"past_time": "Tiempo transcurrido",
|
||||
"Starting_block_decay": "Startblock disminución gradual",
|
||||
"total": "Total",
|
||||
"types": {
|
||||
"creation": "Creado",
|
||||
"noDecay": "sin disminución gradual",
|
||||
"receive": "Recibido",
|
||||
"send": "Enviado"
|
||||
}
|
||||
},
|
||||
"delete": "Eliminar",
|
||||
"em-dash": "—",
|
||||
"error": {
|
||||
"email-already-sent": "Ya te hemos enviado un correo electrónico hace menos de 10 minutos.",
|
||||
"empty-transactionlist": "Ha habido un error en la transmisión del número de sus transacciones.",
|
||||
"error": "Error!",
|
||||
"no-account": "Lamentablemente no hemos podido encontrar una cuenta (activada) con estos datos.",
|
||||
"no-transactionlist": "Lamentablemente, hubo un error. No se ha transmitido ninguna transacción desde el servidor.",
|
||||
"no-user": "No hay usuario con estas referencias.",
|
||||
"session-expired": "La sesión se cerró por razones de seguridad.",
|
||||
"unknown-error": "Error desconocido: "
|
||||
},
|
||||
"followUs": "sigue nos:",
|
||||
"footer": {
|
||||
"app_version": "App versión {version}",
|
||||
"copyright": {
|
||||
"link": "Gradido-Akademie",
|
||||
"year": "© {year}"
|
||||
},
|
||||
"imprint": "Aviso legal",
|
||||
"privacy_policy": "Protección de Datos",
|
||||
"short_hash": "({shortHash})",
|
||||
"whitepaper": "Whitepaper"
|
||||
},
|
||||
"form": {
|
||||
"amount": "Importe",
|
||||
"at": "am",
|
||||
"cancel": "Cancelar",
|
||||
"change": "Cambiar",
|
||||
"check_now": "Revisar",
|
||||
"close": "Cerrar",
|
||||
"current_balance": "Saldo de cuenta actual",
|
||||
"date": "Fecha",
|
||||
"description": "Descripción",
|
||||
"email": "E-Mail",
|
||||
"firstname": "Nombre",
|
||||
"from": "De",
|
||||
"generate_now": "crear ahora",
|
||||
"lastname": "Apellido",
|
||||
"mandatoryField": "campo obligatorio",
|
||||
"memo": "Mensaje",
|
||||
"message": "Noticia",
|
||||
"new_balance": "Saldo de cuenta nuevo depués de confirmación",
|
||||
"no_gdd_available": "No dispones de GDD para enviar.",
|
||||
"password": "Contraseña",
|
||||
"passwordRepeat": "Repetir contraseña",
|
||||
"password_new": "contraseña nueva",
|
||||
"password_new_repeat": "Repetir contraseña nueva",
|
||||
"password_old": "contraseña antigua",
|
||||
"recipient": "Destinatario",
|
||||
"reset": "Restablecer",
|
||||
"save": "Guardar",
|
||||
"scann_code": "<strong>QR Code Scanner</strong> - Escanea el código QR de tu pareja",
|
||||
"sender": "Remitente",
|
||||
"send_check": "Confirma tu transacción. Por favor revisa toda la información nuevamente!",
|
||||
"send_now": "Enviar ahora",
|
||||
"send_transaction_error": "Desafortunadamente, la transacción no se pudo ejecutar!",
|
||||
"send_transaction_success": "Su transacción fue ejecutada con éxito",
|
||||
"sorry": "Disculpa",
|
||||
"thx": "Gracias",
|
||||
"time": "Tiempo",
|
||||
"to": "hasta",
|
||||
"to1": "para",
|
||||
"validation": {
|
||||
"gddSendAmount": "El campo {_field_} debe ser un número entre {min} y {max} con un máximo de dos decimales",
|
||||
"is-not": "No es posible transferirte Gradidos a ti mismo",
|
||||
"usernmae-regex": "El nombre de usuario debe comenzar con una letra seguida de al menos dos caracteres alfanuméricos.",
|
||||
"usernmae-unique": "Este nombre de usuario ya está adjudicado."
|
||||
},
|
||||
"your_amount": "Tu importe"
|
||||
},
|
||||
"GDD": "GDD",
|
||||
"gdd_per_link": {
|
||||
"choose-amount": "Selecciona una cantidad que te gustaría enviar a través de un enlace. También puedes ingresar un mensaje. Cuando haces clic en 'Generar ahora', se crea un enlace que puedes enviar.",
|
||||
"copy-link": "Copiar enlace",
|
||||
"copy-link-with-text": "Copiar texto y enlace",
|
||||
"created": "El enlace ha sido creado",
|
||||
"credit-your-gradido": "Para que se te acrediten los Gradidos, haz clic en el enlace!",
|
||||
"decay-14-day": "Disminución gradual por 14 días",
|
||||
"delete-the-link": "Eliminar el enlace?",
|
||||
"deleted": "El enlace ha sido eliminado!",
|
||||
"expiredOn": "Vencido el:",
|
||||
"has-account": "Ya tienes una cuenta Gradido?",
|
||||
"header": "Transferir Gradidos por medio de un enlace",
|
||||
"isFree": "Gradido es gratis en todo el mundo.",
|
||||
"link-and-text-copied": "El enlace y su mensaje se han copiado en el portapapeles. Ahora puedes ponerlo en un correo electrónico o mensaje.",
|
||||
"link-copied": "El enlace se ha copiado en el portapapeles. Ahora puedes pegarlo en un correo electrónico o mensaje.",
|
||||
"link-deleted": "El enlace se eliminó el {date}.",
|
||||
"link-expired": "El enlace ya no es válido. La validez expiró el {date}.",
|
||||
"link-overview": "Resumen de enlaces",
|
||||
"links_count": "Enlaces activos",
|
||||
"links_sum": "Enlaces abiertos y códigos QR",
|
||||
"no-account": "Aún no tienes una cuenta de Gradido?",
|
||||
"no-redeem": "No puedes canjear tu propio enlace!",
|
||||
"not-copied": "¡Desafortunadamente, su dispositivo no permite copiar! Copie el enlace manualmente!",
|
||||
"redeem": "Canjear",
|
||||
"redeem-text": "¿Quieres canjear el importe ahora?",
|
||||
"redeemed": "¡Canjeado con éxito! Tu cuenta ha sido acreditada con {n} GDD.",
|
||||
"redeemed-at": "El enlace ya se canjeó el {date}.",
|
||||
"redeemed-title": "canjeado",
|
||||
"to-login": "iniciar sesión",
|
||||
"to-register": "Registre una nueva cuenta.",
|
||||
"validUntil": "Válido hasta",
|
||||
"validUntilDate": "El enlace es válido hasta el {date} ."
|
||||
},
|
||||
"gdt": {
|
||||
"calculation": "Cálculo del Gradido Transform",
|
||||
"contribution": "Importe",
|
||||
"conversion": "Conversión",
|
||||
"conversion-gdt-euro": "Conversión Euro / Gradido Transform (GDT)",
|
||||
"credit": "Abono",
|
||||
"factor": "Factor",
|
||||
"formula": "Formula de cálculo",
|
||||
"funding": "Las donaciones",
|
||||
"gdt": "Gradido Transform",
|
||||
"gdt-received": "Gradido Transform (GDT) recibido",
|
||||
"no-transactions": "Aún no tienes un Gradido Transform (GDT).",
|
||||
"not-reachable": "No es posible acceder al servidor GDT.",
|
||||
"publisher": "Tu nuevo miembro referido ha pagado la cuota",
|
||||
"raise": "Aumento",
|
||||
"recruited-member": "Miembro invitado"
|
||||
},
|
||||
"language": "Idioma",
|
||||
"link-load": "recargar el último enlace |recargar los últimos {n} enlaces | descargar más {n} enlaces",
|
||||
"login": "iniciar sesión",
|
||||
"math": {
|
||||
"aprox": "~",
|
||||
"asterisk": "*",
|
||||
"equal": "=",
|
||||
"minus": "−",
|
||||
"pipe": "|"
|
||||
},
|
||||
"message": {
|
||||
"activateEmail": "Tu cuenta aún no ha sido activada. Por favor revisa tu correo electrónico y haz clic en el enlace de activación o solicita uno nuevo enlace de activación a través de la página restablecer contraseña.",
|
||||
"checkEmail": "Tu correo electrónico ha sido verificado con éxito. Puedes registrarte ahora.",
|
||||
"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.",
|
||||
"reset": "Tu contraseña ha sido cambiada.",
|
||||
"title": "Gracias!",
|
||||
"unsetPassword": "Tu contraseña aún no ha sido configurada. Por favor reinícialo."
|
||||
},
|
||||
"navigation": {
|
||||
"admin_area": "Área de administración",
|
||||
"community": "Comunidad",
|
||||
"logout": "Salir",
|
||||
"members_area": "Área de afiliados",
|
||||
"overview": "Resumen",
|
||||
"profile": "Mi Perfil",
|
||||
"send": "Enviar",
|
||||
"support": "Soporte",
|
||||
"transactions": "Transacciones"
|
||||
},
|
||||
"qrCode": "Código QR",
|
||||
"send_gdd": "Enviar GDD",
|
||||
"send_per_link": "Enviar GDD mediante un enlace",
|
||||
"session": {
|
||||
"extend": "Permanecer en sesión iniciada",
|
||||
"lightText": "Si no has realizado ninguna acción durante más de 10 minutos, se cerrará tu sesión por razones de seguridad.",
|
||||
"logoutIn": "Cerrar sesión en ",
|
||||
"warningText": "Aún estas?"
|
||||
},
|
||||
"settings": {
|
||||
"language": {
|
||||
"changeLanguage": "Cambiar idioma",
|
||||
"de": "Deutsch",
|
||||
"en": "English",
|
||||
"es": "Español",
|
||||
"success": "Tu idioma ha sido cambiado con éxito."
|
||||
},
|
||||
"name": {
|
||||
"change-name": "Cambiar nombre",
|
||||
"change-success": "Tu nombre ha sido cambiado con éxito."
|
||||
},
|
||||
"newsletter": {
|
||||
"newsletter": "Informaciones por correo electrónico",
|
||||
"newsletterFalse": "No recibirás informaciones por correo electrónico.",
|
||||
"newsletterTrue": "Recibirás informaciones por correo electrónico."
|
||||
},
|
||||
"password": {
|
||||
"change-password": "Cambiar contraseña",
|
||||
"forgot_pwd": "Olvide la contraseña?",
|
||||
"resend_subtitle": "Su enlace de activación ha caducado. Puedes solicitar uno nuevo aquí.",
|
||||
"reset": "Restablecer contraseña",
|
||||
"reset-password": {
|
||||
"text": "Ahora introduce una nueva contraseña con la que quieras acceder a tu cuenta de Gradido en el futuro.."
|
||||
},
|
||||
"send_now": "Enviar",
|
||||
"set": "Establecer contraseña",
|
||||
"set-password": {
|
||||
"text": "Ahora guarda tu nueva contraseña, que podrás utilizar para acceder a tu cuenta de Gradido en el futuro."
|
||||
},
|
||||
"subtitle": "Si has olvidado tu contraseña, puedes restablecerla aquí."
|
||||
}
|
||||
},
|
||||
"signin": "Iniciar sesión",
|
||||
"signup": "Registrarse",
|
||||
"site": {
|
||||
"forgotPassword": {
|
||||
"heading": "Por favor, introduce la dirección de correo electrónico con la que estas registrado en Gradido."
|
||||
},
|
||||
"login": {
|
||||
"heading": "Inicia sesión con tus datos de acceso. Manténlos seguros en todo momento!"
|
||||
},
|
||||
"resetPassword": {
|
||||
"heading": "Por favor, introduce tu contraseña y repítela."
|
||||
},
|
||||
"signup": {
|
||||
"agree": "Acepto la <a href='https://gradido.net/de/datenschutz/' target='_blank' >Política de privacidad</a>.",
|
||||
"dont_match": "Las contraseñas no coinciden.",
|
||||
"heading": "Regístrate introduciendo todos los datos completos y en los campos correctos.",
|
||||
"lowercase": "Se requiere una letra minúscula.",
|
||||
"minimum": "Al menos 8 caracteres.",
|
||||
"no-whitespace": "Sin espacios ni tabulaciones.",
|
||||
"one_number": "Se requiere un número.",
|
||||
"special-char": "Caracteres especiales requeridos (por ejemplo, _ o &)",
|
||||
"uppercase": "Letra mayúscula requerida."
|
||||
}
|
||||
},
|
||||
"success": "Lo lograste",
|
||||
"time": {
|
||||
"days": "Días",
|
||||
"hours": "Horas",
|
||||
"minutes": "Minutos",
|
||||
"month": "Mes",
|
||||
"months": "Meses",
|
||||
"seconds": "Segundos",
|
||||
"years": "Año"
|
||||
},
|
||||
"transaction": {
|
||||
"gdd-text": "Transacciones Gradido",
|
||||
"gdt-text": "Transacciones GradidoTransform ",
|
||||
"nullTransactions": "Todavía no tienes ninguna transacción en tu cuenta.",
|
||||
"receiverDeleted": "La cuenta del destinatario ha sido eliminada.",
|
||||
"receiverNotFound": "Destinatario no encontrado",
|
||||
"show_all": "Ver todas las transacciones de <strong>{count}</strong>"
|
||||
},
|
||||
"transaction-link": {
|
||||
"send_you": "te envía"
|
||||
},
|
||||
"via_link": "atraves de un enlace",
|
||||
"welcome": "Bienvenido a la comunidad."
|
||||
}
|
||||
@ -11,6 +11,12 @@ const locales = [
|
||||
iso: 'de-DE',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: 'Español',
|
||||
code: 'es',
|
||||
iso: 'es-ES',
|
||||
enabled: true,
|
||||
},
|
||||
]
|
||||
|
||||
export default locales
|
||||
|
||||
44
frontend/src/mixins/copyLinks.js
Normal file
44
frontend/src/mixins/copyLinks.js
Normal file
@ -0,0 +1,44 @@
|
||||
export const copyLinks = {
|
||||
props: {
|
||||
link: { type: String, required: true },
|
||||
amount: { type: String, required: true },
|
||||
memo: { type: String, required: true },
|
||||
validUntil: { type: String, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
canCopyLink: true,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
copyLink() {
|
||||
navigator.clipboard
|
||||
.writeText(this.link)
|
||||
.then(() => {
|
||||
this.toastSuccess(this.$t('gdd_per_link.link-copied'))
|
||||
})
|
||||
.catch(() => {
|
||||
this.canCopyLink = false
|
||||
this.toastError(this.$t('gdd_per_link.not-copied'))
|
||||
})
|
||||
},
|
||||
copyLinkWithText() {
|
||||
navigator.clipboard
|
||||
.writeText(
|
||||
`${this.link}
|
||||
${this.$store.state.firstName} ${this.$t('transaction-link.send_you')} ${this.amount} Gradido.
|
||||
"${this.memo}"
|
||||
${this.$t('gdd_per_link.credit-your-gradido')} ${this.$t('gdd_per_link.validUntilDate', {
|
||||
date: this.$d(new Date(this.validUntil), 'short'),
|
||||
})}`,
|
||||
)
|
||||
.then(() => {
|
||||
this.toastSuccess(this.$t('gdd_per_link.link-and-text-copied'))
|
||||
})
|
||||
.catch(() => {
|
||||
this.canCopyLink = false
|
||||
this.toastError(this.$t('gdd_per_link.not-copied'))
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
408
frontend/src/pages/Community.spec.js
Normal file
408
frontend/src/pages/Community.spec.js
Normal file
@ -0,0 +1,408 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Community from './Community'
|
||||
import { toastErrorSpy, toastSuccessSpy } from '@test/testSetup'
|
||||
import { createContribution, updateContribution, deleteContribution } from '@/graphql/mutations'
|
||||
import { listContributions, listAllContributions, verifyLogin } from '@/graphql/queries'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const mockStoreDispach = jest.fn()
|
||||
const apolloQueryMock = jest.fn()
|
||||
const apolloMutationMock = jest.fn()
|
||||
|
||||
describe('Community', () => {
|
||||
let wrapper
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$d: jest.fn((d) => d),
|
||||
$apollo: {
|
||||
query: apolloQueryMock,
|
||||
mutate: apolloMutationMock,
|
||||
},
|
||||
$store: {
|
||||
dispatch: mockStoreDispach,
|
||||
state: {
|
||||
creation: ['1000', '1000', '1000'],
|
||||
},
|
||||
},
|
||||
$i18n: {
|
||||
locale: 'en',
|
||||
},
|
||||
}
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(Community, {
|
||||
localVue,
|
||||
mocks,
|
||||
})
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
apolloQueryMock.mockResolvedValue({
|
||||
data: {
|
||||
listContributions: {
|
||||
contributionList: [
|
||||
{
|
||||
id: 1555,
|
||||
amount: '200',
|
||||
memo: 'Fleisig, fleisig am Arbeiten mein lieber Freund, 50 Zeichen sind viel',
|
||||
createdAt: '2022-07-15T08:47:06.000Z',
|
||||
deletedAt: null,
|
||||
confirmedBy: null,
|
||||
confirmedAt: null,
|
||||
},
|
||||
],
|
||||
contributionCount: 1,
|
||||
},
|
||||
listAllContributions: {
|
||||
contributionList: [
|
||||
{
|
||||
id: 1555,
|
||||
amount: '200',
|
||||
memo: 'Fleisig, fleisig am Arbeiten mein lieber Freund, 50 Zeichen sind viel',
|
||||
createdAt: '2022-07-15T08:47:06.000Z',
|
||||
deletedAt: null,
|
||||
confirmedBy: null,
|
||||
confirmedAt: null,
|
||||
},
|
||||
{
|
||||
id: 1556,
|
||||
amount: '400',
|
||||
memo: 'Ein anderer lieber Freund ist auch sehr felißig am Arbeiten!!!!',
|
||||
createdAt: '2022-07-16T08:47:06.000Z',
|
||||
deletedAt: null,
|
||||
confirmedBy: null,
|
||||
confirmedAt: null,
|
||||
},
|
||||
],
|
||||
contributionCount: 2,
|
||||
},
|
||||
},
|
||||
})
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('has a DIV .community-page', () => {
|
||||
expect(wrapper.find('div.community-page').exists()).toBe(true)
|
||||
})
|
||||
|
||||
describe('tabs', () => {
|
||||
it('has three tabs', () => {
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
describe('API calls after creation', () => {
|
||||
it('emits update transactions', () => {
|
||||
expect(wrapper.emitted('update-transactions')).toEqual([[0]])
|
||||
})
|
||||
|
||||
it('queries list of own contributions', () => {
|
||||
expect(apolloQueryMock).toBeCalledWith({
|
||||
fetchPolicy: 'no-cache',
|
||||
query: listContributions,
|
||||
variables: {
|
||||
currentPage: 1,
|
||||
pageSize: 25,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('queries list of all contributions', () => {
|
||||
expect(apolloQueryMock).toBeCalledWith({
|
||||
fetchPolicy: 'no-cache',
|
||||
query: listAllContributions,
|
||||
variables: {
|
||||
currentPage: 1,
|
||||
pageSize: 25,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe('server response is error', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
apolloQueryMock.mockRejectedValue({ message: 'Ups' })
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('toasts two errors', () => {
|
||||
expect(toastErrorSpy).toBeCalledTimes(2)
|
||||
expect(toastErrorSpy).toBeCalledWith('Ups')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('set contrubtion', () => {
|
||||
describe('with success', () => {
|
||||
const now = new Date().toISOString()
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
apolloMutationMock.mockResolvedValue({
|
||||
data: {
|
||||
createContribution: true,
|
||||
},
|
||||
})
|
||||
await wrapper.setData({
|
||||
form: {
|
||||
id: null,
|
||||
date: now,
|
||||
memo: 'Mein Beitrag zur Gemeinschaft für diesen Monat ...',
|
||||
amount: '200',
|
||||
},
|
||||
})
|
||||
await wrapper.find('form').trigger('submit')
|
||||
})
|
||||
|
||||
it('calls the create contribution mutation', () => {
|
||||
expect(apolloMutationMock).toBeCalledWith({
|
||||
fetchPolicy: 'no-cache',
|
||||
mutation: createContribution,
|
||||
variables: {
|
||||
creationDate: now,
|
||||
memo: 'Mein Beitrag zur Gemeinschaft für diesen Monat ...',
|
||||
amount: '200',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('toasts a success message', () => {
|
||||
expect(toastSuccessSpy).toBeCalledWith('contribution.submitted')
|
||||
})
|
||||
|
||||
it('updates the contribution list', () => {
|
||||
expect(apolloQueryMock).toBeCalledWith({
|
||||
fetchPolicy: 'no-cache',
|
||||
query: listContributions,
|
||||
variables: {
|
||||
currentPage: 1,
|
||||
pageSize: 25,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('verifies the login (to get the new creations available)', () => {
|
||||
expect(apolloQueryMock).toBeCalledWith({
|
||||
query: verifyLogin,
|
||||
fetchPolicy: 'network-only',
|
||||
})
|
||||
})
|
||||
|
||||
it('set all data to the default values)', () => {
|
||||
expect(wrapper.vm.form.id).toBe(null)
|
||||
expect(wrapper.vm.form.date).toBe('')
|
||||
expect(wrapper.vm.form.memo).toBe('')
|
||||
expect(wrapper.vm.form.amount).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with error', () => {
|
||||
const now = new Date().toISOString()
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
apolloMutationMock.mockRejectedValue({
|
||||
message: 'Ouch!',
|
||||
})
|
||||
await wrapper.setData({
|
||||
form: {
|
||||
id: null,
|
||||
date: now,
|
||||
memo: 'Mein Beitrag zur Gemeinschaft für diesen Monat ...',
|
||||
amount: '200',
|
||||
},
|
||||
})
|
||||
await wrapper.find('form').trigger('submit')
|
||||
})
|
||||
|
||||
it('toasts the error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Ouch!')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('update contrubtion', () => {
|
||||
describe('with success', () => {
|
||||
const now = new Date().toISOString()
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
apolloMutationMock.mockResolvedValue({
|
||||
data: {
|
||||
updateContribution: true,
|
||||
},
|
||||
})
|
||||
await wrapper
|
||||
.findComponent({ name: 'ContributionForm' })
|
||||
.vm.$emit('update-contribution', {
|
||||
id: 2,
|
||||
date: now,
|
||||
memo: 'Mein Beitrag zur Gemeinschaft für diesen Monat ...',
|
||||
amount: '400',
|
||||
})
|
||||
})
|
||||
|
||||
it('calls the update contribution mutation', () => {
|
||||
expect(apolloMutationMock).toBeCalledWith({
|
||||
fetchPolicy: 'no-cache',
|
||||
mutation: updateContribution,
|
||||
variables: {
|
||||
contributionId: 2,
|
||||
creationDate: now,
|
||||
memo: 'Mein Beitrag zur Gemeinschaft für diesen Monat ...',
|
||||
amount: '400',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('toasts a success message', () => {
|
||||
expect(toastSuccessSpy).toBeCalledWith('contribution.updated')
|
||||
})
|
||||
|
||||
it('updates the contribution list', () => {
|
||||
expect(apolloQueryMock).toBeCalledWith({
|
||||
fetchPolicy: 'no-cache',
|
||||
query: listContributions,
|
||||
variables: {
|
||||
currentPage: 1,
|
||||
pageSize: 25,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('verifies the login (to get the new creations available)', () => {
|
||||
expect(apolloQueryMock).toBeCalledWith({
|
||||
query: verifyLogin,
|
||||
fetchPolicy: 'network-only',
|
||||
})
|
||||
})
|
||||
|
||||
it('set all data to the default values)', () => {
|
||||
expect(wrapper.vm.form.id).toBe(null)
|
||||
expect(wrapper.vm.form.date).toBe('')
|
||||
expect(wrapper.vm.form.memo).toBe('')
|
||||
expect(wrapper.vm.form.amount).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with error', () => {
|
||||
const now = new Date().toISOString()
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
apolloMutationMock.mockRejectedValue({
|
||||
message: 'Oh No!',
|
||||
})
|
||||
await wrapper
|
||||
.findComponent({ name: 'ContributionForm' })
|
||||
.vm.$emit('update-contribution', {
|
||||
id: 2,
|
||||
date: now,
|
||||
memo: 'Mein Beitrag zur Gemeinschaft für diesen Monat ...',
|
||||
amount: '400',
|
||||
})
|
||||
})
|
||||
|
||||
it('toasts the error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Oh No!')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete contribution', () => {
|
||||
let contributionListComponent
|
||||
|
||||
beforeEach(async () => {
|
||||
await wrapper.setData({ tabIndex: 1 })
|
||||
contributionListComponent = await wrapper.findComponent({ name: 'ContributionList' })
|
||||
})
|
||||
|
||||
describe('with success', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
apolloMutationMock.mockResolvedValue({
|
||||
data: {
|
||||
deleteContribution: true,
|
||||
},
|
||||
})
|
||||
contributionListComponent.vm.$emit('delete-contribution', { id: 2 })
|
||||
})
|
||||
|
||||
it('calls the API', () => {
|
||||
expect(apolloMutationMock).toBeCalledWith({
|
||||
fetchPolicy: 'no-cache',
|
||||
mutation: deleteContribution,
|
||||
variables: {
|
||||
id: 2,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('toasts a success message', () => {
|
||||
expect(toastSuccessSpy).toBeCalledWith('contribution.deleted')
|
||||
})
|
||||
|
||||
it('updates the contribution list', () => {
|
||||
expect(apolloQueryMock).toBeCalledWith({
|
||||
fetchPolicy: 'no-cache',
|
||||
query: listContributions,
|
||||
variables: {
|
||||
currentPage: 1,
|
||||
pageSize: 25,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('verifies the login (to get the new creations available)', () => {
|
||||
expect(apolloQueryMock).toBeCalledWith({
|
||||
query: verifyLogin,
|
||||
fetchPolicy: 'network-only',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with error', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
apolloMutationMock.mockRejectedValue({
|
||||
message: 'Oh my god!',
|
||||
})
|
||||
contributionListComponent.vm.$emit('delete-contribution', { id: 2 })
|
||||
})
|
||||
|
||||
it('toasts the error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Oh my god!')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('update contribution form', () => {
|
||||
const now = new Date().toISOString()
|
||||
beforeEach(async () => {
|
||||
await wrapper.setData({ tabIndex: 1 })
|
||||
await wrapper
|
||||
.findComponent({ name: 'ContributionList' })
|
||||
.vm.$emit('update-contribution-form', {
|
||||
id: 2,
|
||||
contributionDate: now,
|
||||
memo: 'Mein Beitrag zur Gemeinschaft für diesen Monat ...',
|
||||
amount: '400',
|
||||
})
|
||||
})
|
||||
|
||||
it('sets the form data to the new values', () => {
|
||||
expect(wrapper.vm.form.id).toBe(2)
|
||||
expect(wrapper.vm.form.date).toBe(now)
|
||||
expect(wrapper.vm.form.memo).toBe('Mein Beitrag zur Gemeinschaft für diesen Monat ...')
|
||||
expect(wrapper.vm.form.amount).toBe('400')
|
||||
})
|
||||
|
||||
it('sets tab index back to 0', () => {
|
||||
expect(wrapper.vm.tabIndex).toBe(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
276
frontend/src/pages/Community.vue
Normal file
276
frontend/src/pages/Community.vue
Normal file
@ -0,0 +1,276 @@
|
||||
<template>
|
||||
<div class="community-page">
|
||||
<div>
|
||||
<b-tabs v-model="tabIndex" content-class="mt-3" align="center">
|
||||
<b-tab :title="$t('community.submitContribution')" active>
|
||||
<contribution-form
|
||||
@set-contribution="setContribution"
|
||||
@update-contribution="updateContribution"
|
||||
v-model="form"
|
||||
:updateAmount="updateAmount"
|
||||
/>
|
||||
</b-tab>
|
||||
<b-tab :title="$t('community.myContributions')">
|
||||
<div>
|
||||
<b-alert show dismissible fade variant="secondary" class="text-dark">
|
||||
<h4 class="alert-heading">{{ $t('community.myContributions') }}</h4>
|
||||
<p>
|
||||
{{ $t('contribution.alert.myContributionNoteList') }}
|
||||
</p>
|
||||
<ul class="h2">
|
||||
<li>
|
||||
<b-icon icon="bell-fill" variant="primary"></b-icon>
|
||||
{{ $t('contribution.alert.pending') }}
|
||||
</li>
|
||||
<li>
|
||||
<b-icon icon="check" variant="success"></b-icon>
|
||||
{{ $t('contribution.alert.confirm') }}
|
||||
</li>
|
||||
<li>
|
||||
<b-icon icon="x-circle" variant="danger"></b-icon>
|
||||
{{ $t('contribution.alert.rejected') }}
|
||||
</li>
|
||||
</ul>
|
||||
<hr />
|
||||
<p class="mb-0">
|
||||
{{ $t('contribution.alert.myContributionNoteSupport') }}
|
||||
</p>
|
||||
</b-alert>
|
||||
</div>
|
||||
<contribution-list
|
||||
:items="items"
|
||||
@update-list-contributions="updateListContributions"
|
||||
@update-contribution-form="updateContributionForm"
|
||||
@delete-contribution="deleteContribution"
|
||||
:contributionCount="contributionCount"
|
||||
:showPagination="true"
|
||||
:pageSize="pageSize"
|
||||
/>
|
||||
</b-tab>
|
||||
<b-tab :title="$t('navigation.community')">
|
||||
<b-alert show dismissible fade variant="secondary" class="text-dark">
|
||||
<h4 class="alert-heading">{{ $t('navigation.community') }}</h4>
|
||||
<p>
|
||||
{{ $t('contribution.alert.communityNoteList') }}
|
||||
</p>
|
||||
<ul class="h2">
|
||||
<li>
|
||||
<b-icon icon="bell-fill" variant="primary"></b-icon>
|
||||
{{ $t('contribution.alert.pending') }}
|
||||
</li>
|
||||
<li>
|
||||
<b-icon icon="check" variant="success"></b-icon>
|
||||
{{ $t('contribution.alert.confirm') }}
|
||||
</li>
|
||||
</ul>
|
||||
</b-alert>
|
||||
<contribution-list
|
||||
:items="itemsAll"
|
||||
@update-list-contributions="updateListAllContributions"
|
||||
@update-contribution-form="updateContributionForm"
|
||||
:contributionCount="contributionCountAll"
|
||||
:showPagination="true"
|
||||
:pageSize="pageSizeAll"
|
||||
/>
|
||||
</b-tab>
|
||||
</b-tabs>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import ContributionForm from '@/components/Contributions/ContributionForm.vue'
|
||||
import ContributionList from '@/components/Contributions/ContributionList.vue'
|
||||
import { createContribution, updateContribution, deleteContribution } from '@/graphql/mutations'
|
||||
import { listContributions, listAllContributions, verifyLogin } from '@/graphql/queries'
|
||||
|
||||
export default {
|
||||
name: 'Community',
|
||||
components: {
|
||||
ContributionForm,
|
||||
ContributionList,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tabIndex: 0,
|
||||
items: [],
|
||||
itemsAll: [],
|
||||
currentPage: 1,
|
||||
pageSize: 25,
|
||||
pageSizeAll: 25,
|
||||
contributionCount: 0,
|
||||
contributionCountAll: 0,
|
||||
form: {
|
||||
id: null,
|
||||
date: '',
|
||||
memo: '',
|
||||
amount: '',
|
||||
},
|
||||
updateAmount: '',
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setContribution(data) {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
fetchPolicy: 'no-cache',
|
||||
mutation: createContribution,
|
||||
variables: {
|
||||
creationDate: data.date,
|
||||
memo: data.memo,
|
||||
amount: data.amount,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
this.toastSuccess(this.$t('contribution.submitted'))
|
||||
this.updateListContributions({
|
||||
currentPage: this.currentPage,
|
||||
pageSize: this.pageSize,
|
||||
})
|
||||
this.updateListAllContributions({
|
||||
currentPage: this.currentPage,
|
||||
pageSize: this.pageSize,
|
||||
})
|
||||
this.verifyLogin()
|
||||
})
|
||||
.catch((err) => {
|
||||
this.toastError(err.message)
|
||||
})
|
||||
},
|
||||
updateContribution(data) {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
fetchPolicy: 'no-cache',
|
||||
mutation: updateContribution,
|
||||
variables: {
|
||||
contributionId: data.id,
|
||||
creationDate: data.date,
|
||||
memo: data.memo,
|
||||
amount: data.amount,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
this.toastSuccess(this.$t('contribution.updated'))
|
||||
this.updateListContributions({
|
||||
currentPage: this.currentPage,
|
||||
pageSize: this.pageSize,
|
||||
})
|
||||
this.updateListAllContributions({
|
||||
currentPage: this.currentPage,
|
||||
pageSize: this.pageSize,
|
||||
})
|
||||
this.verifyLogin()
|
||||
})
|
||||
.catch((err) => {
|
||||
this.toastError(err.message)
|
||||
})
|
||||
},
|
||||
deleteContribution(data) {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
fetchPolicy: 'no-cache',
|
||||
mutation: deleteContribution,
|
||||
variables: {
|
||||
id: data.id,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
this.toastSuccess(this.$t('contribution.deleted'))
|
||||
this.updateListContributions({
|
||||
currentPage: this.currentPage,
|
||||
pageSize: this.pageSize,
|
||||
})
|
||||
this.updateListAllContributions({
|
||||
currentPage: this.currentPage,
|
||||
pageSize: this.pageSize,
|
||||
})
|
||||
this.verifyLogin()
|
||||
})
|
||||
.catch((err) => {
|
||||
this.toastError(err.message)
|
||||
})
|
||||
},
|
||||
updateListAllContributions(pagination) {
|
||||
this.$apollo
|
||||
.query({
|
||||
fetchPolicy: 'no-cache',
|
||||
query: listAllContributions,
|
||||
variables: {
|
||||
currentPage: pagination.currentPage,
|
||||
pageSize: pagination.pageSize,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
const {
|
||||
data: { listAllContributions },
|
||||
} = result
|
||||
this.contributionCountAll = listAllContributions.contributionCount
|
||||
this.itemsAll = listAllContributions.contributionList
|
||||
})
|
||||
.catch((err) => {
|
||||
this.toastError(err.message)
|
||||
})
|
||||
},
|
||||
updateListContributions(pagination) {
|
||||
this.$apollo
|
||||
.query({
|
||||
fetchPolicy: 'no-cache',
|
||||
query: listContributions,
|
||||
variables: {
|
||||
currentPage: pagination.currentPage,
|
||||
pageSize: pagination.pageSize,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
const {
|
||||
data: { listContributions },
|
||||
} = result
|
||||
this.contributionCount = listContributions.contributionCount
|
||||
this.items = listContributions.contributionList
|
||||
})
|
||||
.catch((err) => {
|
||||
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) {
|
||||
this.form.id = item.id
|
||||
this.form.date = item.contributionDate
|
||||
this.form.memo = item.memo
|
||||
this.form.amount = item.amount
|
||||
this.updateAmount = item.amount
|
||||
this.tabIndex = 0
|
||||
},
|
||||
updateTransactions(pagination) {
|
||||
this.$emit('update-transactions', pagination)
|
||||
},
|
||||
},
|
||||
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({
|
||||
currentPage: this.currentPage,
|
||||
pageSize: this.pageSize,
|
||||
})
|
||||
this.updateListAllContributions({
|
||||
currentPage: this.currentPage,
|
||||
pageSize: this.pageSize,
|
||||
})
|
||||
this.updateTransactions(0)
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -25,9 +25,11 @@ describe('Send', () => {
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$n: jest.fn((n) => String(n)),
|
||||
$d: jest.fn((d) => d),
|
||||
$store: {
|
||||
state: {
|
||||
email: 'sender@example.org',
|
||||
firstName: 'Testy',
|
||||
},
|
||||
},
|
||||
$apollo: {
|
||||
@ -160,11 +162,15 @@ describe('Send', () => {
|
||||
})
|
||||
|
||||
describe('transaction form link', () => {
|
||||
const now = new Date().toISOString()
|
||||
beforeEach(async () => {
|
||||
apolloMutationMock.mockResolvedValue({
|
||||
data: {
|
||||
createTransactionLink: {
|
||||
link: 'http://localhost/redeem/0123456789',
|
||||
amount: '56.78',
|
||||
memo: 'Make the best of the link!',
|
||||
validUntil: now,
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -228,18 +234,64 @@ describe('Send', () => {
|
||||
navigator.clipboard = navigatorClipboard
|
||||
})
|
||||
|
||||
describe('copy with success', () => {
|
||||
describe('copy link with success', () => {
|
||||
beforeEach(async () => {
|
||||
navigatorClipboardMock.mockResolvedValue()
|
||||
await wrapper.findAll('button').at(0).trigger('click')
|
||||
await wrapper.findAll('button').at(1).trigger('click')
|
||||
})
|
||||
|
||||
it('should call clipboard.writeText', () => {
|
||||
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
|
||||
'http://localhost/redeem/0123456789',
|
||||
)
|
||||
})
|
||||
it('toasts success message', () => {
|
||||
expect(toastSuccessSpy).toBeCalledWith('gdd_per_link.link-copied')
|
||||
})
|
||||
})
|
||||
|
||||
describe('copy with error', () => {
|
||||
describe('copy link with error', () => {
|
||||
beforeEach(async () => {
|
||||
navigatorClipboardMock.mockRejectedValue()
|
||||
await wrapper.findAll('button').at(1).trigger('click')
|
||||
})
|
||||
|
||||
it('toasts error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('gdd_per_link.not-copied')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('copy link and text with success', () => {
|
||||
const navigatorClipboard = navigator.clipboard
|
||||
beforeAll(() => {
|
||||
delete navigator.clipboard
|
||||
navigator.clipboard = { writeText: navigatorClipboardMock }
|
||||
})
|
||||
afterAll(() => {
|
||||
navigator.clipboard = navigatorClipboard
|
||||
})
|
||||
|
||||
describe('copy link and text with success', () => {
|
||||
beforeEach(async () => {
|
||||
navigatorClipboardMock.mockResolvedValue()
|
||||
await wrapper.findAll('button').at(0).trigger('click')
|
||||
})
|
||||
|
||||
it('should call clipboard.writeText', () => {
|
||||
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
|
||||
'http://localhost/redeem/0123456789\n' +
|
||||
'Testy transaction-link.send_you 56.78 Gradido.\n' +
|
||||
'"Make the best of the link!"\n' +
|
||||
'gdd_per_link.credit-your-gradido gdd_per_link.validUntilDate',
|
||||
)
|
||||
})
|
||||
it('toasts success message', () => {
|
||||
expect(toastSuccessSpy).toBeCalledWith('gdd_per_link.link-and-text-copied')
|
||||
})
|
||||
})
|
||||
|
||||
describe('copy link and text with error', () => {
|
||||
beforeEach(async () => {
|
||||
navigatorClipboardMock.mockRejectedValue()
|
||||
await wrapper.findAll('button').at(0).trigger('click')
|
||||
@ -253,7 +305,7 @@ describe('Send', () => {
|
||||
|
||||
describe('close button click', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.findAll('button').at(2).trigger('click')
|
||||
await wrapper.findAll('button').at(3).trigger('click')
|
||||
})
|
||||
|
||||
it('Shows the TransactionForm', () => {
|
||||
|
||||
@ -41,7 +41,13 @@
|
||||
></transaction-result-send-error>
|
||||
</template>
|
||||
<template #transactionResultLink>
|
||||
<transaction-result-link :link="link" @on-reset="onReset"></transaction-result-link>
|
||||
<transaction-result-link
|
||||
:link="link"
|
||||
:amount="amount"
|
||||
:memo="memo"
|
||||
:validUntil="validUntil"
|
||||
@on-reset="onReset"
|
||||
></transaction-result-link>
|
||||
</template>
|
||||
</gdd-send>
|
||||
<hr />
|
||||
@ -144,7 +150,15 @@ export default {
|
||||
})
|
||||
.then((result) => {
|
||||
this.$emit('set-tunneled-email', null)
|
||||
this.link = result.data.createTransactionLink.link
|
||||
const {
|
||||
data: {
|
||||
createTransactionLink: { link, amount, memo, validUntil },
|
||||
},
|
||||
} = result
|
||||
this.link = link
|
||||
this.amount = amount
|
||||
this.memo = memo
|
||||
this.validUntil = validUntil
|
||||
this.transactionData = { ...EMPTY_TRANSACTION_DATA }
|
||||
this.currentTransactionStep = TRANSACTION_STEPS.transactionResultLink
|
||||
this.updateTransactions({})
|
||||
|
||||
@ -109,7 +109,7 @@ export default {
|
||||
return this.$route.params.code.search(/^CL-/) === 0
|
||||
},
|
||||
itemType() {
|
||||
// link wurde gelöscht: am, von
|
||||
// link is deleted: at, from
|
||||
if (this.linkData.deletedAt) {
|
||||
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
|
||||
this.redeemedBoxText = this.$t('gdd_per_link.link-deleted', {
|
||||
|
||||
@ -50,7 +50,7 @@ describe('router', () => {
|
||||
})
|
||||
|
||||
it('has sixteen routes defined', () => {
|
||||
expect(routes).toHaveLength(16)
|
||||
expect(routes).toHaveLength(17)
|
||||
})
|
||||
|
||||
describe('overview', () => {
|
||||
@ -75,6 +75,17 @@ describe('router', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('community', () => {
|
||||
it('requires authorization', () => {
|
||||
expect(routes.find((r) => r.path === '/community').meta.requiresAuth).toBeTruthy()
|
||||
})
|
||||
|
||||
it('loads the "Community" page', async () => {
|
||||
const component = await routes.find((r) => r.path === '/community').component()
|
||||
expect(component.default.name).toBe('Community')
|
||||
})
|
||||
})
|
||||
|
||||
describe('profile', () => {
|
||||
it('requires authorization', () => {
|
||||
expect(routes.find((r) => r.path === '/profile').meta.requiresAuth).toBeTruthy()
|
||||
|
||||
@ -38,6 +38,13 @@ const routes = [
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/community',
|
||||
component: () => import('@/pages/Community.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/login/:code?',
|
||||
component: () => import('@/pages/Login.vue'),
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gradido",
|
||||
"version": "1.10.1",
|
||||
"version": "1.11.0",
|
||||
"description": "Gradido",
|
||||
"main": "index.js",
|
||||
"repository": "git@github.com:gradido/gradido.git",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user