Merge branch 'master' into 1223-community_communication_concept2

# Conflicts:
#	docu/graphics/roadmap_publicity.drawio
#	docu/graphics/roadmap_publicity.png
This commit is contained in:
Ulf Gebhardt 2022-03-24 21:18:06 +01:00
commit e0d0056ab7
Signed by: ulfgebhardt
GPG Key ID: DA6B843E748679C9
1576 changed files with 23545 additions and 163466 deletions

View File

@ -106,82 +106,6 @@ jobs:
name: docker-database-production_up
path: /tmp/database_up.tar
##############################################################################
# JOB: DOCKER BUILD PRODUCTION LOGIN SERVER ##################################
##############################################################################
build_production_login_server:
name: Docker Build Production - Login Server
runs-on: ubuntu-latest
#needs: [nothing]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v2
with:
submodules: recursive
##########################################################################
# SET ENVS ###############################################################
##########################################################################
- name: ENV - VERSION
run: echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
- name: ENV - BUILD_DATE
run: echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
- name: ENV - BUILD_VERSION
run: echo "BUILD_VERSION=${VERSION}.${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
- name: ENV - BUILD_COMMIT
run: echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV
##########################################################################
# LOGIN SERVER ###########################################################
##########################################################################
- name: Login Server | Build `production` image
run: |
docker build -t "gradido/login_server:latest" -t "gradido/login_server:production" -t "gradido/login_server:${VERSION}" -t "gradido/login_server:${BUILD_VERSION}" login_server/
docker save "gradido/login_server" > /tmp/login_server.tar
- name: Upload Artifact
uses: actions/upload-artifact@v2
with:
name: docker-login-server-production
path: /tmp/login_server.tar
##############################################################################
# JOB: DOCKER BUILD PRODUCTION COMMUNITY SERVER ##############################
##############################################################################
build_production_community_server:
name: Docker Build Production - Community Server
runs-on: ubuntu-latest
#needs: [nothing]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v2
##########################################################################
# SET ENVS ###############################################################
##########################################################################
- name: ENV - VERSION
run: echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
- name: ENV - BUILD_DATE
run: echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
- name: ENV - BUILD_VERSION
run: echo "BUILD_VERSION=${VERSION}.${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
- name: ENV - BUILD_COMMIT
run: echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV
##########################################################################
# COMMUNITY SERVER #######################################################
##########################################################################
- name: Community Server | Build `production` image
run: |
docker build -t "gradido/community_server:latest" -t "gradido/community_server:production" -t "gradido/community_server:${VERSION}" -t "gradido/community_server:${BUILD_VERSION}" -f ./community_server/Dockerfile ./
docker save "gradido/community_server" > /tmp/community_server.tar
- name: Upload Artifact
uses: actions/upload-artifact@v2
with:
name: docker-community-server-production
path: /tmp/community_server.tar
##############################################################################
# JOB: DOCKER BUILD PRODUCTION MARIADB #######################################
##############################################################################
@ -248,7 +172,7 @@ jobs:
##########################################################################
- name: Nginx | Build `production` image
run: |
docker build -t "gradido/nginx:latest" -t "gradido/nginx:production" -t "gradido/nginx:${VERSION}" -t "gradido/nginx:${BUILD_VERSION}" -f ./nginx/Dockerfile ./
docker build -t "gradido/nginx:latest" -t "gradido/nginx:production" -t "gradido/nginx:${VERSION}" -t "gradido/nginx:${BUILD_VERSION}" nginx/
docker save "gradido/nginx" > /tmp/nginx.tar
- name: Upload Artifact
uses: actions/upload-artifact@v2
@ -262,7 +186,7 @@ jobs:
upload_to_dockerhub:
name: Upload to Dockerhub
runs-on: ubuntu-latest
needs: [build_production_frontend, build_production_backend, build_production_database_up, build_production_login_server, build_production_community_server, build_production_mariadb, build_production_nginx]
needs: [build_production_frontend, build_production_backend, build_production_database_up, build_production_mariadb, build_production_nginx]
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
@ -296,20 +220,6 @@ jobs:
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/database_up.tar
- name: Download Docker Image (Login Server)
uses: actions/download-artifact@v2
with:
name: docker-login-server-production
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/login_server.tar
- name: Download Docker Image (Community Server)
uses: actions/download-artifact@v2
with:
name: docker-community-server-production
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/community_server.tar
- name: Download Docker Image (MariaDB)
uses: actions/download-artifact@v2
with:
@ -335,10 +245,6 @@ jobs:
run: docker push --all-tags gradido/backend
- name: Push database
run: docker push --all-tags gradido/database
- name: Push login_server
run: docker push --all-tags gradido/login_server
- name: Push community_server
run: docker push --all-tags gradido/community_server
- name: Push MariaDB
run: docker push --all-tags gradido/mariadb
- name: Push Nginx
@ -399,7 +305,7 @@ jobs:
- name: yarn install
run: yarn install
- name: generate changelog
run: yarn auto-changelog --latest-version ${{ env.VERSION }} --unreleased-only
run: yarn auto-changelog --commit-limit 0 --latest-version ${{ env.VERSION }} --unreleased-only
- name: package-version-to-git-release
continue-on-error: true # Will fail if tag exists
id: create_release

View File

@ -107,54 +107,6 @@ jobs:
name: docker-database-test_up
path: /tmp/database_up.tar
##############################################################################
# JOB: DOCKER BUILD TEST LOGIN SERVER ########################################
##############################################################################
build_test_login_server:
name: Docker Build Test - Login Server
runs-on: ubuntu-latest
#needs: [nothing]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v2
with:
submodules: true
##########################################################################
# BUILD LOGIN SERVER DOCKER IMAGE ########################################
##########################################################################
- name: login server | Build `release` image
run: |
docker build -t "gradido/login_server:release" -f ./login_server/Dockerfile login_server/
##############################################################################
# JOB: DOCKER BUILD TEST COMMUNITY SERVER ####################################
##############################################################################
build_test_community_server:
name: Docker Build Test - Community Server
runs-on: ubuntu-latest
#needs: [nothing]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v2
##########################################################################
# BUILD COMMUNITY SERVER DOCKER IMAGE ####################################
##########################################################################
- name: community server | Build `test` image
run: |
docker build -t "gradido/community_server:test" -f ./community_server/Dockerfile ./
docker save "gradido/community_server:test" > /tmp/community_server.tar
- name: Upload Artifact
uses: actions/upload-artifact@v2
with:
name: docker-community-server-test
path: /tmp/community_server.tar
##############################################################################
# JOB: DOCKER BUILD TEST MARIADB #############################################
##############################################################################
@ -199,7 +151,7 @@ jobs:
##########################################################################
- name: nginx | Build `test` image
run: |
docker build -t "gradido/nginx:test" -f ./nginx/Dockerfile ./
docker build -t "gradido/nginx:test" nginx/
docker save "gradido/nginx:test" > /tmp/nginx.tar
- name: Upload Artifact
uses: actions/upload-artifact@v2
@ -233,7 +185,7 @@ jobs:
##########################################################################
# LOCALES FRONTEND #######################################################
##########################################################################
- name: frontend | Locales
- name: Frontend | Locales
run: docker run --rm gradido/frontend:test yarn run locales
##############################################################################
@ -262,9 +214,38 @@ jobs:
##########################################################################
# LINT FRONTEND ##########################################################
##########################################################################
- name: frontend | Lint
- name: Frontend | Lint
run: docker run --rm gradido/frontend:test yarn run lint
##############################################################################
# JOB: STYLELINT FRONTEND ####################################################
##############################################################################
stylelint_frontend:
name: Stylelint - Frontend
runs-on: ubuntu-latest
needs: [build_test_frontend]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v2
##########################################################################
# DOWNLOAD DOCKER IMAGE ##################################################
##########################################################################
- name: Download Docker Image (Frontend)
uses: actions/download-artifact@v2
with:
name: docker-frontend-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/frontend.tar
##########################################################################
# STYLELINT FRONTEND #####################################################
##########################################################################
- name: Frontend | Stylelint
run: docker run --rm gradido/frontend:test yarn run stylelint
##############################################################################
# JOB: LINT ADMIN INTERFACE ##################################################
##############################################################################
@ -295,7 +276,36 @@ jobs:
run: docker run --rm gradido/admin:test yarn run lint
##############################################################################
# JOB: LOCALES ADMIN ######################################################
# JOB: STYLELINT ADMIN INTERFACE ##############################################
##############################################################################
stylelint_admin:
name: Stylelint - Admin Interface
runs-on: ubuntu-latest
needs: [build_test_admin]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v2
##########################################################################
# DOWNLOAD DOCKER IMAGE ##################################################
##########################################################################
- name: Download Docker Image (Admin Interface)
uses: actions/download-artifact@v2
with:
name: docker-admin-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/admin.tar
##########################################################################
# STYLELINT ADMIN INTERFACE ##############################################
##########################################################################
- name: Admin Interface | Stylelint
run: docker run --rm gradido/admin:test yarn run stylelint
##############################################################################
# JOB: LOCALES ADMIN #########################################################
##############################################################################
locales_admin:
name: Locales - Admin
@ -428,7 +438,7 @@ jobs:
report_name: Coverage Frontend
type: lcov
result_path: ./coverage/lcov.info
min_coverage: 94
min_coverage: 95
token: ${{ github.token }}
##############################################################################
@ -470,7 +480,7 @@ jobs:
report_name: Coverage Admin Interface
type: lcov
result_path: ./coverage/lcov.info
min_coverage: 76
min_coverage: 95
token: ${{ github.token }}
##############################################################################
@ -479,7 +489,7 @@ jobs:
unit_test_backend:
name: Unit tests - Backend
runs-on: ubuntu-latest
needs: [build_test_backend,build_test_mariadb]
needs: [build_test_mariadb]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
@ -496,20 +506,18 @@ jobs:
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/mariadb.tar
- name: Download Docker Image (Backend)
uses: actions/download-artifact@v2
with:
name: docker-backend-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/backend.tar
##########################################################################
# UNIT TESTS BACKEND #####################################################
##########################################################################
- name: backend | docker-compose
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mariadb database
- name: backend | docker-compose mariadb
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mariadb
- name: Sleep for 30 seconds
run: sleep 30s
shell: bash
- name: backend | docker-compose database
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps database
- name: backend Unit tests | test
run: cd database && yarn && yarn build && cd ../backend && yarn && yarn CI_worklfow_test
run: cd database && yarn && yarn build && cd ../backend && yarn && yarn test
# run: docker-compose -f docker-compose.yml -f docker-compose.test.yml exec -T backend yarn test
##########################################################################
# COVERAGE CHECK BACKEND #################################################
@ -520,149 +528,7 @@ jobs:
report_name: Coverage Backend
type: lcov
result_path: ./backend/coverage/lcov.info
min_coverage: 40
token: ${{ github.token }}
##############################################################################
# JOB: UNIT TEST LOGIN-SERVER ###############################################
##############################################################################
unit_test_login_server:
name: Unit tests - Login-Server
runs-on: ubuntu-latest
needs: []
services:
mariadb:
image: gradido/mariadb:test
env:
MARIADB_ALLOW_EMPTY_PASSWORD: 1
MARIADB_USER: root
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping"
--health-interval=6s
--health-timeout=3s
--health-retries=4
steps:
# - name: Debug service
# run: echo "$(docker ps)"
#- name: Debug container choosing script
# run: echo "$(docker container ls | grep mariadb | awk '{ print $1 }')"
- name: get mariadb container id
run: echo "::set-output name=id::$(docker container ls | grep mariadb | awk '{ print $1 }')"
id: mariadb_container
- name: get automatic created network
run: echo "::set-output name=id::$(docker network ls | grep github_network | awk '{ print $1 }')"
id: network
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v2
with:
submodules: true
# Database migration
- name: Start database migration
run: |
docker build --target production_up -t "gradido/database:production_up" database/
docker run --network ${{ steps.network.outputs.id }} --name=database --env NODE_ENV=production --env DB_HOST=mariadb --env DB_DATABASE=gradido_community_test -d gradido/database:production_up
##########################################################################
# Build Login-Server Test Docker image ###################################
##########################################################################
- name: login server | Build `test` image
run: |
docker build -t "gradido/login_server:test" -f ./login_server/Dockerfiles/ubuntu/Dockerfile.test login_server/
##########################################################################
# UNIT TESTS BACKEND LOGIN-SERVER #######################################
##########################################################################
- name: Login-Server | Unit tests
run: |
docker run --network container:$(docker container ls | grep mariadb | awk '{ print $1 }') -v ~/coverage:/code/build_cov/coverage -v $(pwd)/configs/login_server:/etc/grd_login gradido/login_server:test
cp -r ~/coverage ./coverage
##########################################################################
# COVERAGE CHECK BACKEND LOGIN-SERVER ####################################
##########################################################################
- name: backend login | Coverage check
uses: webcraftmedia/coverage-check-action@master
with:
report_name: Coverage Backend Login
type: lcov
result_path: ./coverage/coverage.info
min_coverage: 34
token: ${{ github.token }}
##############################################################################
# JOB: UNIT TEST COMMUNITY-SERVER ###########################################
##############################################################################
unit_test_community_server:
name: Unit tests - Community Server
runs-on: ubuntu-latest
needs: [build_test_community_server]
services:
mariadb:
image: gradido/mariadb:test
env:
MARIADB_ALLOW_EMPTY_PASSWORD: 1
MARIADB_USER: root
options: --health-cmd="mysqladmin ping"
--health-interval=5s
--health-timeout=5s
--health-retries=3
steps:
- name: get mariadb container id
run: echo "::set-output name=id::$(docker container ls | grep mariadb | awk '{ print $1 }')"
id: mariadb_container
- name: get automatic created network
run: echo "::set-output name=id::$(docker network ls | grep github_network | awk '{ print $1 }')"
id: network
- name: Start database migration
run: docker run --network ${{ steps.network.outputs.id }} --name=database --env NODE_ENV=production --env DB_HOST=mariadb --env DB_DATABASE=gradido_community_test -d gradido/database:production_up
- name: get database migration container id
run: echo "::set-output name=id::$(docker container ls | grep database | awk '{ print $1 }')"
id: database_container
- name: Start Login-Server
run: docker run --network ${{ steps.network.outputs.id }} --name=login-server -d gradido/login_server:with-config
- name: get login-server container id
run: echo "::set-output name=id::$(docker container ls | grep login_server | awk '{ print $1 }')"
id: login_server_container
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v2
##########################################################################
# DOWNLOAD DOCKER IMAGE ##################################################
##########################################################################
- name: Download Docker Image (Community-Server)
uses: actions/download-artifact@v2
with:
name: docker-community-server-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/community_server.tar
# for debugging login-server
- name: check login-server
run: docker logs ${{ steps.login_server_container.outputs.id }}
- name: check mariadb
run: docker logs ${{ steps.mariadb_container.outputs.id }}
- name: check migration
run: docker logs ${{ steps.database_container.outputs.id }}
##########################################################################
# UNIT TESTS BACKEND COMMUNITY-SERVER #######################################
##########################################################################
- name: community server | Unit tests
run: |
docker run --network ${{ steps.network.outputs.id }} -v ~/coverage:/var/www/cakephp/webroot/coverage gradido/community_server:test
cp -r ~/coverage ./coverage
#########################################################################
# COVERAGE CHECK BACKEND COMMUNITY-SERVER ####################################
##########################################################################
- name: backend community | Coverage check
uses: einhornimmond/coverage-check-action@master
with:
report_name: Coverage Backend Community
type: phpunit
result_path: ./coverage/coverage.info
min_coverage: 10
min_coverage: 54
token: ${{ github.token }}
##########################################################################

6
.gitignore vendored
View File

@ -5,3 +5,9 @@ nbproject
.metadata
/.env
package-lock.json
/deployment/bare_metal/.env
/deployment/bare_metal/nginx/sites-available/gradido.conf
/deployment/bare_metal/nginx/sites-available/update-page.conf
/deployment/bare_metal/nginx/update-page/updating.html
/deployment/bare_metal/log
/deployment/bare_metal/backup

35
.gitmodules vendored
View File

@ -1,36 +1 @@
[submodule "gn"]
path = gn
url = https://github.com/gradido/gn.git
branch = master
[submodule "login_server/dependencies/tinf"]
path = login_server/dependencies/tinf
url = https://github.com/jibsen/tinf.git
[submodule "login_server/dependencies/mariadb-connector-c"]
path = login_server/dependencies/mariadb-connector-c
url = https://github.com/MariaDB/mariadb-connector-c.git
[submodule "login_server/src/proto"]
path = login_server/src/proto
url = https://github.com/gradido/gradido_protocol.git
[submodule "login_server/dependencies/spirit-po"]
path = login_server/dependencies/spirit-po
url = https://github.com/cbeck88/spirit-po.git
[submodule "login_server/dependencies/poco"]
path = login_server/dependencies/poco
url = https://github.com/pocoproject/poco.git
[submodule "login_server/dependencies/cmake-modules"]
path = login_server/dependencies/cmake-modules
url = https://github.com/viaduck/cmake-modules.git
[submodule "community_server/src/protobuf"]
path = community_server/src/protobuf
url = https://github.com/gradido/gradido_protocol.git
[submodule "login_server/dependencies/libsodium"]
path = login_server/dependencies/libsodium
url = https://github.com/jedisct1/libsodium.git
[submodule "login_server/src/proto"]
path = login_server/src/proto
url = https://github.com/gradido/gradido_protocol.git
[submodule "login_server/dependencies/protobuf"]
path = login_server/dependencies/protobuf
url = https://github.com/protocolbuffers/protobuf.git

View File

@ -4,12 +4,274 @@ 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.6.6](https://github.com/gradido/gradido/compare/1.6.5...1.6.6)
- Fix: Upper case email on register breaks account [`#1542`](https://github.com/gradido/gradido/pull/1542)
- 1106 first transaction cannot be expanded [`#1432`](https://github.com/gradido/gradido/pull/1432)
- added missing bootstrap scss. bootstrap/scss/bootstrap, plus more mis… [`#1540`](https://github.com/gradido/gradido/pull/1540)
- feat: Seed Deleted User [`#1533`](https://github.com/gradido/gradido/pull/1533)
- fix: No Creations for Deleted Users [`#1534`](https://github.com/gradido/gradido/pull/1534)
- fix: Wrong Key Name for Recover User [`#1535`](https://github.com/gradido/gradido/pull/1535)
- [Feature] : user deleted and undeleted functions for adminarea [`#1520`](https://github.com/gradido/gradido/pull/1520)
- fix: Possible SQL Exception in User Search [`#1530`](https://github.com/gradido/gradido/pull/1530)
- Feature: Make lint warnings unwanted [`#1529`](https://github.com/gradido/gradido/pull/1529)
- 1459 list data again on confirm creation [`#1467`](https://github.com/gradido/gradido/pull/1467)
- fix: Return Empty Array When No Pending Creations Are Present [`#1526`](https://github.com/gradido/gradido/pull/1526)
- Fix: Correct path of index.js in production [`#1525`](https://github.com/gradido/gradido/pull/1525)
- refactor: Get Open Creations by One Query [`#1524`](https://github.com/gradido/gradido/pull/1524)
- Admin: Langsame Benutzer-Suche [`#1472`](https://github.com/gradido/gradido/pull/1472)
- fix: Backend Unit Tests Running Again [`#1513`](https://github.com/gradido/gradido/pull/1513)
- Refactor: Combine transaction tables [`#1523`](https://github.com/gradido/gradido/pull/1523)
- Refactor: User resolver [`#1522`](https://github.com/gradido/gradido/pull/1522)
- feature: Soft-Delete for users (backend) [`#1521`](https://github.com/gradido/gradido/pull/1521)
- feature: Soft-Delete for users (database only) [`#1516`](https://github.com/gradido/gradido/pull/1516)
- refactor: Improve Decay Display [`#1517`](https://github.com/gradido/gradido/pull/1517)
- 404 page needs back to login button [`#1515`](https://github.com/gradido/gradido/pull/1515)
- feature: show current version in admin footer [`#1514`](https://github.com/gradido/gradido/pull/1514)
- fix: Never Sent Email Text [`#1512`](https://github.com/gradido/gradido/pull/1512)
- refactor: static decay block [`#1405`](https://github.com/gradido/gradido/pull/1405)
- refactor: Use Bootstrap Vue Toast [`#1499`](https://github.com/gradido/gradido/pull/1499)
- fix: Catch GDT Server Errors [`#1479`](https://github.com/gradido/gradido/pull/1479)
- Fix: Autochangelog - no commits [`#1498`](https://github.com/gradido/gradido/pull/1498)
#### [1.6.5](https://github.com/gradido/gradido/compare/1.6.4...1.6.5)
> 15 February 2022
- v1.6.5 [`#1497`](https://github.com/gradido/gradido/pull/1497)
- Fix: Elopage Hook Crash 2 [`#1481`](https://github.com/gradido/gradido/pull/1481)
#### [1.6.4](https://github.com/gradido/gradido/compare/1.6.3...1.6.4)
> 14 February 2022
- v1.6.4 [`#1478`](https://github.com/gradido/gradido/pull/1478)
- fix: Admin Email Confirmation Date and Time [`#1448`](https://github.com/gradido/gradido/pull/1448)
- Fix: Do not log password or token to the console [`#1477`](https://github.com/gradido/gradido/pull/1477)
- Fix: Elopage Hook Crash [`#1474`](https://github.com/gradido/gradido/pull/1474)
- 538 unify all buttons [`#1455`](https://github.com/gradido/gradido/pull/1455)
- 833 old error is shown for a second even if transaction is successful [`#1460`](https://github.com/gradido/gradido/pull/1460)
- fix: Wrong Email Spelling in German [`#1446`](https://github.com/gradido/gradido/pull/1446)
- fix: Redirect to Login after Register [`#1445`](https://github.com/gradido/gradido/pull/1445)
- refactor: Split User Table Component in Admin Interface [`#1443`](https://github.com/gradido/gradido/pull/1443)
#### [1.6.3](https://github.com/gradido/gradido/compare/1.6.2...1.6.3)
> 9 February 2022
- v1.6.3 [`#1447`](https://github.com/gradido/gradido/pull/1447)
- add .btn-outline-secondary in scss [`#1442`](https://github.com/gradido/gradido/pull/1442)
- Profil settings and footer refactor [`#1440`](https://github.com/gradido/gradido/pull/1440)
#### [1.6.2](https://github.com/gradido/gradido/compare/1.6.1...1.6.2)
> 8 February 2022
- v1.6.2 [`#1438`](https://github.com/gradido/gradido/pull/1438)
- updated_changelog_library [`#1437`](https://github.com/gradido/gradido/pull/1437)
- admin interface does user have member area [`#1416`](https://github.com/gradido/gradido/pull/1416)
- Refactor - Remove community_server [`#1408`](https://github.com/gradido/gradido/pull/1408)
- 1389 transactions tabs are not well designed [`#1425`](https://github.com/gradido/gradido/pull/1425)
- fix_community_name_description [`#1429`](https://github.com/gradido/gradido/pull/1429)
- remove_unnecessary_repositories [`#1406`](https://github.com/gradido/gradido/pull/1406)
- clean_database_users [`#1427`](https://github.com/gradido/gradido/pull/1427)
- remove_gradido_node [`#1431`](https://github.com/gradido/gradido/pull/1431)
- add updateTransactions function for GDD balance if reload page [`#1423`](https://github.com/gradido/gradido/pull/1423)
- 1390 display error when navigating to send form without any gdd [`#1424`](https://github.com/gradido/gradido/pull/1424)
- have an delete button for the search input [`#1413`](https://github.com/gradido/gradido/pull/1413)
- reset all selected users in mass creation [`#1422`](https://github.com/gradido/gradido/pull/1422)
- combine_user_tables [`#1411`](https://github.com/gradido/gradido/pull/1411)
- feat: Test Table Row Details Toggling [`#1420`](https://github.com/gradido/gradido/pull/1420)
- feat: Improved Tests for Mass Creation [`#1419`](https://github.com/gradido/gradido/pull/1419)
- refactor: Mixin for Creation Labels [`#1409`](https://github.com/gradido/gradido/pull/1409)
- Marque community_server as to be removed. [`#1407`](https://github.com/gradido/gradido/pull/1407)
- database_transaction_signatures [`#1368`](https://github.com/gradido/gradido/pull/1368)
- database_pending_creations [`#1367`](https://github.com/gradido/gradido/pull/1367)
- fix_seed [`#1410`](https://github.com/gradido/gradido/pull/1410)
- clean_database [`#1362`](https://github.com/gradido/gradido/pull/1362)
- multiple creation already selected users remain saved [`#1376`](https://github.com/gradido/gradido/pull/1376)
- fix: Localize Datetime in Admin Interface [`#1327`](https://github.com/gradido/gradido/pull/1327)
- feat: Remove Login Server [`#1383`](https://github.com/gradido/gradido/pull/1383)
- refactor: Tag Last Version with Login Server [`#1391`](https://github.com/gradido/gradido/pull/1391)
- if an email is not confirmed, a user cannot be added to any multiple … [`#1374`](https://github.com/gradido/gradido/pull/1374)
- cleanups_refactors [`#1404`](https://github.com/gradido/gradido/pull/1404)
- 1365 clear bootstrap version for vue2, preparation for new template [`#1366`](https://github.com/gradido/gradido/pull/1366)
- upgrade vue version from ^2.6.11 to 2.6.12 [`#1382`](https://github.com/gradido/gradido/pull/1382)
- remove vue-qrcode from dashboard-plugin [`#1364`](https://github.com/gradido/gradido/pull/1364)
- remove unused package from frontend [`#1360`](https://github.com/gradido/gradido/pull/1360)
#### [1.6.1](https://github.com/gradido/gradido/compare/1.6.0...1.6.1)
> 28 January 2022
- Hotfix elopage [`#1358`](https://github.com/gradido/gradido/pull/1358)
- change standard text für creation [`#1343`](https://github.com/gradido/gradido/pull/1343)
- Check if user email is activated to make a creation. [`#1356`](https://github.com/gradido/gradido/pull/1356)
- fix: Creation Confirmation User Ids [`#1345`](https://github.com/gradido/gradido/pull/1345)
#### [1.6.0](https://github.com/gradido/gradido/compare/1.5.1...1.6.0)
> 27 January 2022
- v1.6.0 [`#1357`](https://github.com/gradido/gradido/pull/1357)
- fix updatePendingCreation method [`#1346`](https://github.com/gradido/gradido/pull/1346)
- klicktipp_config [`#1348`](https://github.com/gradido/gradido/pull/1348)
- 1351 form to create incorrect due to change of bootstrap version [`#1352`](https://github.com/gradido/gradido/pull/1352)
- first draft of releaseplan [`#1349`](https://github.com/gradido/gradido/pull/1349)
- Rechtschreibfehler behoben, entgültig zu endgültig [`#1354`](https://github.com/gradido/gradido/pull/1354)
- production_deployment [`#1341`](https://github.com/gradido/gradido/pull/1341)
- import_production_data [`#1324`](https://github.com/gradido/gradido/pull/1324)
- remove division / 100 for GDT [`#1339`](https://github.com/gradido/gradido/pull/1339)
- fix: Language Set Correctly after Page Reload in Admin Interface [`#1334`](https://github.com/gradido/gradido/pull/1334)
- fix: Set Locale after Login [`#1328`](https://github.com/gradido/gradido/pull/1328)
- deployment_community_parameters [`#1331`](https://github.com/gradido/gradido/pull/1331)
- bold text if acive page [`#1337`](https://github.com/gradido/gradido/pull/1337)
- 1303 thanks page after set password [`#1335`](https://github.com/gradido/gradido/pull/1335)
- Changed the toasted message for successful creating a creation. [`#1332`](https://github.com/gradido/gradido/pull/1332)
- Added a required to the password repeat field, errors are shown immed… [`#1333`](https://github.com/gradido/gradido/pull/1333)
- fix_rejected_login [`#1329`](https://github.com/gradido/gradido/pull/1329)
- defined fields for Transactionlist in UserSearch in Adminarea, add Lo… [`#1322`](https://github.com/gradido/gradido/pull/1322)
- fix: Render Localized Balance in NavBar [`#1321`](https://github.com/gradido/gradido/pull/1321)
- 1227 vergaenglichkeit besser darstellen [`#1312`](https://github.com/gradido/gradido/pull/1312)
- 1300 gray out register button further when disabled [`#1316`](https://github.com/gradido/gradido/pull/1316)
- change Admin Gradido-Logo font color to white [`#1315`](https://github.com/gradido/gradido/pull/1315)
- 1299 remove text from register publisher [`#1317`](https://github.com/gradido/gradido/pull/1317)
- 1249 gdd gdt values look displaced or cut off [`#1318`](https://github.com/gradido/gradido/pull/1318)
- 1307 admin rename wallet to my account [`#1314`](https://github.com/gradido/gradido/pull/1314)
- 1011 visualize and refine the register process [`#1136`](https://github.com/gradido/gradido/pull/1136)
- New roadmap [`#1219`](https://github.com/gradido/gradido/pull/1219)
- fix: Redirect after Email Confirmation [`#1308`](https://github.com/gradido/gradido/pull/1308)
- fix_docker_logging [`#1309`](https://github.com/gradido/gradido/pull/1309)
- feat: Paginate User Table [`#1293`](https://github.com/gradido/gradido/pull/1293)
- Adding button, to link back to the login page from the error not activated page. [`#1297`](https://github.com/gradido/gradido/pull/1297)
- [WIP] new_deployment_stage2 [`#1290`](https://github.com/gradido/gradido/pull/1290)
- add icons in menu and navbarmenu [`#1298`](https://github.com/gradido/gradido/pull/1298)
- Withdrew horrizontal scrollbar [`#1295`](https://github.com/gradido/gradido/pull/1295)
- transactionCount false or transactionCount error text hide [`#1259`](https://github.com/gradido/gradido/pull/1259)
- 1265 mouse hovering over pencil in settings [`#1270`](https://github.com/gradido/gradido/pull/1270)
- Fix redirect to /logout, now redirects to the wallet login. [`#1285`](https://github.com/gradido/gradido/pull/1285)
- feat: Test and Refactor Send Email [`#1224`](https://github.com/gradido/gradido/pull/1224)
- new_deployment [`#1220`](https://github.com/gradido/gradido/pull/1220)
- Withdrew space at the end of an locale definition. [`#1283`](https://github.com/gradido/gradido/pull/1283)
- Fix removing from masscreationlist now gives right item to the left l… [`#1284`](https://github.com/gradido/gradido/pull/1284)
- onboarding process document [`#1148`](https://github.com/gradido/gradido/pull/1148)
- Don't show the language-switch on the /register page. [`#1282`](https://github.com/gradido/gradido/pull/1282)
- language_updates_bernd [`#1260`](https://github.com/gradido/gradido/pull/1260)
- Admin Logout redirectes to the wallet login page. [`#1271`](https://github.com/gradido/gradido/pull/1271)
- fix: Admin Creation List [`#1276`](https://github.com/gradido/gradido/pull/1276)
- fix: Override Existing Token in Response Header [`#1278`](https://github.com/gradido/gradido/pull/1278)
- fix: Pass Language to Admin Interface [`#1280`](https://github.com/gradido/gradido/pull/1280)
- feat: Seed Garrick Ollivander [`#1277`](https://github.com/gradido/gradido/pull/1277)
- fix: Fix Update of Open Creations [`#1273`](https://github.com/gradido/gradido/pull/1273)
- fix: Change Password in User Profile [`#1266`](https://github.com/gradido/gradido/pull/1266)
- Wallet shows the wrong gdt sum (gdtSum * 100) [`#1269`](https://github.com/gradido/gradido/pull/1269)
- 1240 account not activated needs a button to login page [`#1261`](https://github.com/gradido/gradido/pull/1261)
- reverse filter for unregistered emails [`#1256`](https://github.com/gradido/gradido/pull/1256)
- close row-details automatically when switching to multiple creation page [`#1245`](https://github.com/gradido/gradido/pull/1245)
- change creationdatas if change pending creation [`#1244`](https://github.com/gradido/gradido/pull/1244)
- 1230 details für wiederholte email bestaetigen versenden [`#1255`](https://github.com/gradido/gradido/pull/1255)
- 1221 month names of the open creation in multiple creation [`#1222`](https://github.com/gradido/gradido/pull/1222)
- Documentation Template-Overview-2021 PDF [`#1194`](https://github.com/gradido/gradido/pull/1194)
- Admin create multiple pending creations [`#1203`](https://github.com/gradido/gradido/pull/1203)
- feat: Catch No Cookies Allowed [`#1187`](https://github.com/gradido/gradido/pull/1187)
- Remove confirmation of own pending creation [`#1215`](https://github.com/gradido/gradido/pull/1215)
- feat: Test Create User Mutation [`#1217`](https://github.com/gradido/gradido/pull/1217)
- #751 Änderungen auf Federation und Community-Erstellprozess [`#969`](https://github.com/gradido/gradido/pull/969)
- 1055 concept of operations requirements [`#1129`](https://github.com/gradido/gradido/pull/1129)
- Admin Translations German English [`#1218`](https://github.com/gradido/gradido/pull/1218)
- Roadmap [`#1213`](https://github.com/gradido/gradido/pull/1213)
- pr_admin_refactor [`#1214`](https://github.com/gradido/gradido/pull/1214)
- 1197 admin interface created transactions list [`#1202`](https://github.com/gradido/gradido/pull/1202)
- migration_0006_login_users_collation [`#1207`](https://github.com/gradido/gradido/pull/1207)
- wrong_login_is_not_password_reset [`#1179`](https://github.com/gradido/gradido/pull/1179)
- Moved reverse of the getPendingCreations to the AdminResolver instead… [`#1185`](https://github.com/gradido/gradido/pull/1185)
- Withdrew password argument of createUser. [`#1206`](https://github.com/gradido/gradido/pull/1206)
- fix_optin_code_regeneration [`#1195`](https://github.com/gradido/gradido/pull/1195)
- 1057 display gdd balance when sending [`#1149`](https://github.com/gradido/gradido/pull/1149)
- 1145 refactor admin resolver from master [`#1164`](https://github.com/gradido/gradido/pull/1164)
- Error handling in GddTransactionList.vue [`#1183`](https://github.com/gradido/gradido/pull/1183)
- state-balance balanceDate can't get the date of the last transaction [`#1182`](https://github.com/gradido/gradido/pull/1182)
- feat: Frontend Refactor and Tests [`#1186`](https://github.com/gradido/gradido/pull/1186)
- fix: No Nodemon in Database Scripts [`#1167`](https://github.com/gradido/gradido/pull/1167)
- feat: Test Creation in Admin Interface [`#1172`](https://github.com/gradido/gradido/pull/1172)
- 533 refactor menu remove tim [`#1162`](https://github.com/gradido/gradido/pull/1162)
- database_docker_fixes [`#1176`](https://github.com/gradido/gradido/pull/1176)
- naming_conventions_pending_tasks [`#1184`](https://github.com/gradido/gradido/pull/1184)
- fix_i18n [`#1180`](https://github.com/gradido/gradido/pull/1180)
- fix_corrupted_sender_balance [`#1178`](https://github.com/gradido/gradido/pull/1178)
- Withdrew * 10000 and / 10000 since we need to store the full value. [`#1181`](https://github.com/gradido/gradido/pull/1181)
- catch error Client certificate revoked and pass error.session-expired… [`#1152`](https://github.com/gradido/gradido/pull/1152)
- feat: Test Apollo Provider in Frontend [`#1161`](https://github.com/gradido/gradido/pull/1161)
- fix: Pending Creations are Updated Without Page Reload [`#1160`](https://github.com/gradido/gradido/pull/1160)
- Error on upgrade database after downgrade [`#1119`](https://github.com/gradido/gradido/pull/1119)
- Login fix creation validation [`#1159`](https://github.com/gradido/gradido/pull/1159)
- login_call_resetPassword [`#1130`](https://github.com/gradido/gradido/pull/1130)
- register add tabindex-1 on password show [`#1158`](https://github.com/gradido/gradido/pull/1158)
- feat: Seed Transaction Creations [`#1146`](https://github.com/gradido/gradido/pull/1146)
- Admin confirm pending creation [`#1153`](https://github.com/gradido/gradido/pull/1153)
- Admin delete pending creation query [`#1141`](https://github.com/gradido/gradido/pull/1141)
- fix: Catch Expired Session when Coming from Admin Interface [`#1151`](https://github.com/gradido/gradido/pull/1151)
- docu_release [`#1138`](https://github.com/gradido/gradido/pull/1138)
- feat: Verify Login in Admin Interface [`#1150`](https://github.com/gradido/gradido/pull/1150)
- Admin pending creation queries [`#1140`](https://github.com/gradido/gradido/pull/1140)
- 1137 publisher id as input field on register [`#1147`](https://github.com/gradido/gradido/pull/1147)
- Admin pending creation [`#1135`](https://github.com/gradido/gradido/pull/1135)
- feat: Setup Data Seeding [`#1121`](https://github.com/gradido/gradido/pull/1121)
- fix_admin_token_renewal [`#1139`](https://github.com/gradido/gradido/pull/1139)
- backend_rights [`#1126`](https://github.com/gradido/gradido/pull/1126)
- Login admin interface [`#1125`](https://github.com/gradido/gradido/pull/1125)
- Adminbereich first step [`#1116`](https://github.com/gradido/gradido/pull/1116)
- Login hook elopage [`#1112`](https://github.com/gradido/gradido/pull/1112)
- Since we don't make a request to the login_server we need to catch if… [`#1131`](https://github.com/gradido/gradido/pull/1131)
- feat: Add Server Users Entity [`#1127`](https://github.com/gradido/gradido/pull/1127)
- Stale: 1002-language-selection-register [`#1113`](https://github.com/gradido/gradido/pull/1113)
- Error for removing coin animation [`#1120`](https://github.com/gradido/gradido/pull/1120)
- Adding a check that the user is activated before letting them login. [`#1099`](https://github.com/gradido/gradido/pull/1099)
- Changed the Auto increment so that it is done after the rollback and … [`#1128`](https://github.com/gradido/gradido/pull/1128)
- Login call unsecure login [`#1095`](https://github.com/gradido/gradido/pull/1095)
- feat: Setup Admin Interface [`#1045`](https://github.com/gradido/gradido/pull/1045)
- login_call_updateUserInfos [`#1084`](https://github.com/gradido/gradido/pull/1084)
- fix: Await Resolved Promises in Backend Unit Tests [`#1079`](https://github.com/gradido/gradido/pull/1079)
- feat: Raise Test Coverage Frontend [`#1102`](https://github.com/gradido/gradido/pull/1102)
- login_call_logout [`#1096`](https://github.com/gradido/gradido/pull/1096)
- login_call_hasElopage [`#1083`](https://github.com/gradido/gradido/pull/1083)
- login_call_create_user [`#1070`](https://github.com/gradido/gradido/pull/1070)
- 1036 register page breaks without community [`#1043`](https://github.com/gradido/gradido/pull/1043)
- Apollo create transactions without signation [`#1044`](https://github.com/gradido/gradido/pull/1044)
- change getCustomRepository to createUser function [`#1046`](https://github.com/gradido/gradido/pull/1046)
- fresh_install_instructions [`#1065`](https://github.com/gradido/gradido/pull/1065)
- login_call_check_username [`#1037`](https://github.com/gradido/gradido/pull/1037)
- feat: Setup Unit Tests for Resolvers in Backend [`#951`](https://github.com/gradido/gradido/pull/951)
- Login hotfixes 1.5.1 [`#1075`](https://github.com/gradido/gradido/pull/1075)
- vscode_suggest_extensions [`#1073`](https://github.com/gradido/gradido/pull/1073)
- Documentation on the coin creation and the steps to have the accounts… [`#1052`](https://github.com/gradido/gradido/pull/1052)
- fix open community server user account creation [`#1072`](https://github.com/gradido/gradido/pull/1072)
- when creating the register page, everything in the form is set to blank [`#1025`](https://github.com/gradido/gradido/pull/1025)
- Spelling error fixed 'Berechnungsformel' [`#1048`](https://github.com/gradido/gradido/pull/1048)
- checkEmail.vue page text is displayed correctly now [`#1051`](https://github.com/gradido/gradido/pull/1051)
- Adminarea old [`#1058`](https://github.com/gradido/gradido/pull/1058)
- migrate_login_database [`#1031`](https://github.com/gradido/gradido/pull/1031)
- Text changes for german and english. [`#1041`](https://github.com/gradido/gradido/pull/1041)
- refactor: Transaction Component [`#1026`](https://github.com/gradido/gradido/pull/1026)
- 1017 fixe dashboard layout safari [`#1038`](https://github.com/gradido/gradido/pull/1038)
- 707 separate account overview and send [`#970`](https://github.com/gradido/gradido/pull/970)
- Link to the funding contributions of gradido.net memberships [`#984`](https://github.com/gradido/gradido/pull/984)
- fix: Decay Rounded to Ceil [`#1021`](https://github.com/gradido/gradido/pull/1021)
- fix: Flaky Dashboard Layout Test on Logout [`#1024`](https://github.com/gradido/gradido/pull/1024)
- Move Entity models into database [`#956`](https://github.com/gradido/gradido/pull/956)
- Text 'No decay' replaced by the number 0.00 [`#1023`](https://github.com/gradido/gradido/pull/1023)
- change text 'eingetraten' to 'eingetragen' [`#1022`](https://github.com/gradido/gradido/pull/1022)
- fix: Test Default Publisher ID in Sidebar Menu [`#987`](https://github.com/gradido/gradido/pull/987)
- increase_frontend_coverage [`#1020`](https://github.com/gradido/gradido/pull/1020)
- analyse_bundle [`#1019`](https://github.com/gradido/gradido/pull/1019)
- release_issue_template [`#1013`](https://github.com/gradido/gradido/pull/1013)
- fix_changelog [`#1014`](https://github.com/gradido/gradido/pull/1014)
#### [1.5.1](https://github.com/gradido/gradido/compare/1.5.0...1.5.1)
> 15 October 2021
- fix isExitInDb [`#994`](https://github.com/gradido/gradido/pull/994)
- fix [`80228ef`](https://github.com/gradido/gradido/commit/80228ef842d4087ea4b80934b15b8112611e3e33)
#### [1.5.0](https://github.com/gradido/gradido/compare/1.4.0...1.5.0)
@ -86,9 +348,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- update docker files [`#830`](https://github.com/gradido/gradido/pull/830)
- added publisher_id field to user [`#245`](https://github.com/gradido/gradido/pull/245)
- webpack update [`#811`](https://github.com/gradido/gradido/pull/811)
- resolvers [`562ad9a`](https://github.com/gradido/gradido/commit/562ad9ae31d97f90a371452bed1ffe10ebf2d3a5)
- deleted inputs (now args) [`8ab542a`](https://github.com/gradido/gradido/commit/8ab542a28acf6b78d7a9e7fe7757363d225f7b4f)
- fix UserCard_CoinAnimation to properly use the store, have 100% coverage and other minor fixes & simplifications [`ce826de`](https://github.com/gradido/gradido/commit/ce826deb1d6d92caba514713539dca2da3f74de7)
#### [1.4.0](https://github.com/gradido/gradido/compare/1.3.1...1.4.0)
@ -130,9 +389,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- feat: Vue Apollo Client [`#701`](https://github.com/gradido/gradido/pull/701)
- change text from Geld to Gradidos [`#711`](https://github.com/gradido/gradido/pull/711)
- fix fix [`#728`](https://github.com/gradido/gradido/pull/728)
- sort locales [`ec12a28`](https://github.com/gradido/gradido/commit/ec12a28f81577d530f58b42b7f8c2c7d20dffd64)
- feat: Unify and Sort Locales [`aba4f4d`](https://github.com/gradido/gradido/commit/aba4f4d20e0a13016e3528a1c5c30c111eb3a9f1)
- feat: Increase Coverage [`3c061bc`](https://github.com/gradido/gradido/commit/3c061bcb8d1a3a47442ed6a351e1428e15b314aa)
#### [1.3.1](https://github.com/gradido/gradido/compare/1.3.0...1.3.1)
@ -141,9 +397,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- fix: Translations and Formula Display [`#727`](https://github.com/gradido/gradido/pull/727)
- 612 docu structure [`#688`](https://github.com/gradido/gradido/pull/688)
- Community update for gdt list GDT transaction format [`#726`](https://github.com/gradido/gradido/pull/726)
- [#612] new directory structure in /docu/Concepts [`10bf3b0`](https://github.com/gradido/gradido/commit/10bf3b0cdfa6c44f879be0155e93f636601a051b)
- #612 additional documents [`ac0ed4f`](https://github.com/gradido/gradido/commit/ac0ed4fee81caff26d09b5de47dd130f12abdb45)
- #612 docu restructuring [`e67e1c4`](https://github.com/gradido/gradido/commit/e67e1c41e78264698e6fae4cf1d29751de7e7b29)
#### [1.3.0](https://github.com/gradido/gradido/compare/1.2.1...1.3.0)
@ -165,9 +418,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- Backend Setup [`#584`](https://github.com/gradido/gradido/pull/584)
- text-size in textarea and font-variante if focus [`#677`](https://github.com/gradido/gradido/pull/677)
- 680 app large maximum width [`#681`](https://github.com/gradido/gradido/pull/681)
- linting, server is working [`34b30b2`](https://github.com/gradido/gradido/commit/34b30b216b6fafcb5b686d4b023b05f2e9766bdf)
- server stack seems to work. Graphql does not load properly yet [`43f7cf8`](https://github.com/gradido/gradido/commit/43f7cf87679713d436a64d569d6af1594a12ee33)
- initial commit, base packages [`fdf0979`](https://github.com/gradido/gradido/commit/fdf0979830fece04208a6b3bb06bb5323a3c149b)
#### [1.2.1](https://github.com/gradido/gradido/compare/1.2.0...1.2.1)
@ -178,9 +428,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- fix unneccessary migration run on fresh (docker) setup [`#654`](https://github.com/gradido/gradido/pull/654)
- move back decay as standalone transaction in old frontend [`#656`](https://github.com/gradido/gradido/pull/656)
- fix display error with creation [`#652`](https://github.com/gradido/gradido/pull/652)
- release [`a0b8056`](https://github.com/gradido/gradido/commit/a0b8056c17b22570a1b1dbb6fa6ce71e561b04af)
- update content for frontend [`d37ce09`](https://github.com/gradido/gradido/commit/d37ce0949ef97d2a6c6ffaf0be31db9f6d92e743)
- exchange positions [`bc000ef`](https://github.com/gradido/gradido/commit/bc000efd87c9701480c4aeaa7b819ab49bfe8f01)
#### [1.2.0](https://github.com/gradido/gradido/compare/1.1.1...1.2.0)
@ -219,9 +466,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- Update Password Reset E-Mail Subject Encoding [`#579`](https://github.com/gradido/gradido/pull/579)
- move decay between transactions into the transactions [`#483`](https://github.com/gradido/gradido/pull/483)
- fix #591 [`#591`](https://github.com/gradido/gradido/issues/591)
- fix style decay startblick [`cc7778b`](https://github.com/gradido/gradido/commit/cc7778b55d1baaa7be2d9440480e0fb27bb9a930)
- Remove dynamic cast because it lead to errors again and agin (Poco::AutoPtr don't work correct with that) [`0db5912`](https://github.com/gradido/gradido/commit/0db5912a67158be8f313c01f06350f8339cb0e28)
- Remove dynamic cast because it lead to errors again and agin (Poco::AutoPtr don't work correct with that) [`cee7d7a`](https://github.com/gradido/gradido/commit/cee7d7ac3c4c8c1f481cc3a87fb15422c858413b)
#### [1.1.1](https://github.com/gradido/gradido/compare/1.1.0...1.1.1)
@ -232,9 +476,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- update transfer email text [`#574`](https://github.com/gradido/gradido/pull/574)
- update mysql because tuple has changed [`#576`](https://github.com/gradido/gradido/pull/576)
- Login fix pending transactions [`#578`](https://github.com/gradido/gradido/pull/578)
- add test to prevent bug in future [`630d667`](https://github.com/gradido/gradido/commit/630d667e996870a1bf9aa9586b0467d58419e525)
- use standard path. add nginx example [`ac249b4`](https://github.com/gradido/gradido/commit/ac249b46830a8039aec52d30b48084b50a264b6f)
- add autodeploy bash scripts [`f49cf4d`](https://github.com/gradido/gradido/commit/f49cf4d7f8054d87efa1e12055a7ef0c6d3b9872)
#### [1.1.0](https://github.com/gradido/gradido/compare/1.0.2...1.1.0)
@ -276,9 +517,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- login without hedera [`#478`](https://github.com/gradido/gradido/pull/478)
- fix: Show Correct Version Number in Footer [`#475`](https://github.com/gradido/gradido/pull/475)
- refactor: Remove Element-UI [`#476`](https://github.com/gradido/gradido/pull/476)
- remove components Charts, Notification, SearchUser, ButtonCheckbox, Button RadioGroup, Breadcrumb [`159bff7`](https://github.com/gradido/gradido/commit/159bff71df20a5c48f93389b2f990f7fe54e53b9)
- fix bug, update dockerfiles to use dependencies without grpc [`dedcebd`](https://github.com/gradido/gradido/commit/dedcebdb95ee0f3dfd2ad62074d4181af38476a2)
- add warning to able to forward warnings from community server to client [`2fc3fe9`](https://github.com/gradido/gradido/commit/2fc3fe94a09bae199bf2f34f9df90e8fc3879c2b)
#### [1.0.2](https://github.com/gradido/gradido/compare/1.0.1...1.0.2)
@ -305,17 +543,12 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- add dynamic error email if transaction failed [`#452`](https://github.com/gradido/gradido/pull/452)
- ceil the last decay [`#449`](https://github.com/gradido/gradido/pull/449)
- feat: Raise Coverage of Frontend Unit Tets to 18% [`#447`](https://github.com/gradido/gradido/pull/447)
- parse cpsp files automatic in build [`a4a12bb`](https://github.com/gradido/gradido/commit/a4a12bb62b4000e035ff15e17c5a5f5861653ff6)
- translate german html encoded error messages to english and use gettext for automatic translation [`d339627`](https://github.com/gradido/gradido/commit/d33962736d94c1cb7a12ff775bc2c8d7505d646e)
- 100% coverage of GddTransactionList [`96fb245`](https://github.com/gradido/gradido/commit/96fb245821c69f4d321204a663247d5eee60d92f)
#### [1.0.1](https://github.com/gradido/gradido/compare/1.0.0...1.0.1)
> 14 May 2021
- Login crash fix [`#444`](https://github.com/gradido/gradido/pull/444)
- add try catch blocks to prevent login-server from crashing [`22ff220`](https://github.com/gradido/gradido/commit/22ff22072956f8b843037c75c5b16b7ff5d6a2a3)
- fix [`14a4243`](https://github.com/gradido/gradido/commit/14a424347817b1fe6912a113bffd70e55d688112)
### [1.0.0](https://github.com/gradido/gradido/compare/0.9.4...1.0.0)
@ -435,9 +668,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- Background color change [`#117`](https://github.com/gradido/gradido/pull/117)
- Delete unused files [`#116`](https://github.com/gradido/gradido/pull/116)
- store aufräumen teil 1 [`#115`](https://github.com/gradido/gradido/pull/115)
- add migrations table for automatic table data migration [`40a9a8c`](https://github.com/gradido/gradido/commit/40a9a8c2b587f5bef0fcc54136ed7bd13dd91b2b)
- update yarn.lock after running yarn install [`7f38c80`](https://github.com/gradido/gradido/commit/7f38c801213ad886e9d34a8d43b00ae423f5f2a0)
- use new function for balance overview in old frontend, update balance in session on every php-request [`97c570c`](https://github.com/gradido/gradido/commit/97c570c08cc51ed17a69eb8be8d987f95f3c2ce0)
#### [0.9.4](https://github.com/gradido/gradido/compare/0.9.3...0.9.4)
@ -445,9 +675,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- Vue with nginx [`#84`](https://github.com/gradido/gradido/pull/84)
- Build on run [`#103`](https://github.com/gradido/gradido/pull/103)
- update debug docker to use dependencies container pushed to docker hub [`1f002f4`](https://github.com/gradido/gradido/commit/1f002f4ed0b12d4b2bf63efceabe546d0c5b58ea)
- removed email tasks complete [`8a143be`](https://github.com/gradido/gradido/commit/8a143be8423d7bd894d4f512848895df8b9694b0)
- build login-server on docker-compose up in a docker volume so it rebuild only neccessary parts if some c++ files have changed [`0da5279`](https://github.com/gradido/gradido/commit/0da527917523530186e6effe63dc001fc99bd3e3)
#### [0.9.3](https://github.com/gradido/gradido/compare/0.9.2...0.9.3)
@ -476,9 +703,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- Add Feature in user search old frontend because Support has requested the feature long ago [`#56`](https://github.com/gradido/gradido/pull/56)
- sprache angepasst, for login, pwd, sigin [`#54`](https://github.com/gradido/gradido/pull/54)
- Improve workflows [`#53`](https://github.com/gradido/gradido/pull/53)
- setup eslint with tougher rules [`1f13507`](https://github.com/gradido/gradido/commit/1f13507eacfd93c2248fb841de5f481c9eb1e6bd)
- semicolon rule implemented [`6762a02`](https://github.com/gradido/gradido/commit/6762a028f2a3e4f2713b26bed81029defe686ad7)
- dev meeting, bernd [`a99de7f`](https://github.com/gradido/gradido/commit/a99de7f5d1f7557c0877eae565aa4263d65aaaf3)
#### [0.9.2](https://github.com/gradido/gradido/compare/0.9.1...0.9.2)
@ -488,9 +712,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- Reload after login fixed [`#50`](https://github.com/gradido/gradido/pull/50)
- Monorepo login server [`#48`](https://github.com/gradido/gradido/pull/48)
- Stage0 [`#3`](https://github.com/gradido/gradido/pull/3)
- Add auto-sign Transaction functionality [`5592275`](https://github.com/gradido/gradido/commit/55922753a7ffd9552be132501d744da491c409b5)
- read in login the real client ip X-Real-IP from nginx forwarded not from community server [`512d307`](https://github.com/gradido/gradido/commit/512d307a19b955bb6e26ae8b274def354829b50f)
- move check if all passwords allow direct into pwdValidation so it will work with every code which ask for password [`e2c38c1`](https://github.com/gradido/gradido/commit/e2c38c1a0fc25a4a2bc922c4bbc44d86b6d00d8b)
#### 0.9.1
@ -502,6 +723,3 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- [WIP] 2 create a dockerfile for the frontend application [`#6`](https://github.com/gradido/gradido/pull/6)
- Master - first step [`#1`](https://github.com/gradido/gradido/pull/1)
- Add docker compose [`#7`](https://github.com/gradido/gradido/pull/7)
- style 404 side :) [`c7bdf89`](https://github.com/gradido/gradido/commit/c7bdf8978594b932615e48f9bb1c19d3c3bf3fcf)
- publish workflow test [`df6f66f`](https://github.com/gradido/gradido/commit/df6f66ffe70baa9ed3f70b460a6c0c14011bb944)
- many translations. translation structure [`bf68547`](https://github.com/gradido/gradido/commit/bf685479767d19c246c4d6abe3577dc3cb666346)

View File

@ -60,14 +60,12 @@ docker-compose -f docker-compose.yml up
- [frontend](./frontend) Wallet frontend
- [backend](./backend) GraphQL & Business logic backend
- [mariadb](./mariadb) Database backend
- [login_server](./login_server) User credential storage & business logic backend
- [community_server](./community_server/) Business logic backend
We are currently restructuring the service to reduce dependencies and unify business logic into one place. Furthermore the databases defined for each service will be unified into one.
### Open the wallet
Once you have `docker-compose` up and running, you can open [http://localhost/vue](http://localhost/vue) and create yourself a new wallet account.
Once you have `docker-compose` up and running, you can open [http://localhost/](http://localhost/) and create yourself a new wallet account.
## How to release
@ -90,7 +88,7 @@ Note: The Changelog will be regenerated with all tags on release on the external
| Problem | Issue | Solution | Description |
| ------- | ----- | -------- | ----------- |
| docker-compose raises database connection errors | [#1062](https://github.com/gradido/gradido/issues/1062) | End `ctrl+c` and restart the `docker-compose up` after a successful build | Several Database connection related errors occur in the docker-compose log. |
| Wallet page is empty | [#1063](https://github.com/gradido/gradido/issues/1063) | Accept Cookies and Local Storage in your Browser | The page stays empty when navigating to [http://localhost/vue](http://localhost/vue) |
| Wallet page is empty | [#1063](https://github.com/gradido/gradido/issues/1063) | Accept Cookies and Local Storage in your Browser | The page stays empty when navigating to [http://localhost/](http://localhost/) |
## Useful Links

View File

@ -1,3 +1,4 @@
node_modules
.git
.gitignore
.gitignore
!.eslintignore

View File

@ -1,3 +1,6 @@
CONFIG_VERSION=v1.2022-03-18
GRAPHQL_URI=http://localhost:4000/graphql
WALLET_AUTH_URL=http://localhost/vue/authenticate?token=$1
WALLET_AUTH_URL=http://localhost/authenticate?token={token}
WALLET_URL=http://localhost/login
DEBUG_DISABLE_AUTH=false

6
admin/.env.template Normal file
View File

@ -0,0 +1,6 @@
CONFIG_VERSION=$ADMIN_CONFIG_VERSION
GRAPHQL_URI=$GRAPHQL_URI
WALLET_AUTH_URL=$WALLET_AUTH_URL
WALLET_URL=$WALLET_URL
DEBUG_DISABLE_AUTH=false

View File

@ -1,4 +1,3 @@
node_modules
coverage
**/*.min.js
dist
node_modules/
dist/
coverage/

View File

@ -8,14 +8,42 @@ module.exports = {
parserOptions: {
parser: 'babel-eslint',
},
extends: ['standard', 'plugin:vue/essential', 'plugin:prettier/recommended'],
extends: [
'standard',
'plugin:vue/essential',
'plugin:prettier/recommended',
'plugin:@intlify/vue-i18n/recommended',
],
// required to lint *.vue files
plugins: ['vue', 'prettier', 'jest'],
overrides: [
{
files: ['*.json'],
extends: ['plugin:@intlify/vue-i18n/recommended'],
},
],
// add your custom rules here
rules: {
'no-console': ['error'],
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'vue/component-name-in-template-casing': ['error', 'kebab-case'],
'vue/no-static-inline-styles': [
'error',
{
allowBinding: false,
},
],
'@intlify/vue-i18n/no-dynamic-keys': 'error',
'@intlify/vue-i18n/no-unused-keys': [
'error',
{
src: './src',
extensions: ['.js', '.vue'],
ignores: [],
enableFix: false,
},
],
'@intlify/vue-i18n/no-missing-keys-in-other-locales': 'error',
'prettier/prettier': [
'error',
{
@ -23,4 +51,12 @@ module.exports = {
},
],
},
settings: {
'vue-i18n': {
localeDir: './src/locales/*.json',
// Specify the version of `vue-i18n` you are using.
// If not specified, the message will be parsed twice.
messageSyntaxVersion: '^8.26.5',
},
},
}

3
admin/.gitignore vendored
View File

@ -2,7 +2,8 @@ node_modules/
dist/
.cache/
.env
/.env
/.env.bak
# coverage folder
coverage/

18
admin/.stylelintrc.js Normal file
View File

@ -0,0 +1,18 @@
'use strict';
module.exports = {
extends: ["stylelint-config-standard-scss", "stylelint-config-recommended-vue"],
overrides: [
{
files: "**/*.{scss}",
customSyntax: "postcss-scss",
extends: ["stylelint-config-standard-scss"],
},
{
files: "**/*.vue",
customSyntax: "postcss-html",
extends: ["stylelint-config-recommended-vue"],
}
]
};

View File

@ -3,17 +3,19 @@
"description": "Administraion Interface for Gradido",
"main": "index.js",
"author": "Moriz Wahl",
"version": "0.1.0",
"version": "1.6.6",
"license": "MIT",
"private": false,
"scripts": {
"start": "node run/server.js",
"serve": "vue-cli-service serve --open",
"dev": "yarn run serve",
"build": "vue-cli-service build",
"lint": "eslint --ext .js,.vue .",
"test": "jest --coverage",
"locales": "scripts/missing-keys.sh && scripts/sort.sh"
"dev": "yarn run serve",
"analyse-bundle": "yarn build && webpack-bundle-analyzer dist/webpack.stats.json",
"lint": "eslint --max-warnings=0 --ext .js,.vue,.json .",
"stylelint": "stylelint --max-warnings=0 '**/*.{scss,vue}'",
"test": "TZ=UTC jest --coverage",
"locales": "scripts/sort.sh"
},
"dependencies": {
"@babel/core": "^7.15.8",
@ -28,28 +30,28 @@
"babel-plugin-component": "^1.1.1",
"babel-preset-env": "^1.7.0",
"babel-preset-vue": "^2.0.2",
"bootstrap": "^5.1.3",
"bootstrap": "4.3.1",
"bootstrap-vue": "^2.21.2",
"core-js": "^3.6.5",
"dotenv-webpack": "^7.0.3",
"express": "^4.17.1",
"graphql": "^15.6.1",
"identity-obj-proxy": "^3.0.0",
"jest": "26.6.3",
"moment": "^2.29.1",
"portal-vue": "^2.1.7",
"regenerator-runtime": "^0.13.9",
"stats-webpack-plugin": "^0.7.0",
"vue": "^2.6.11",
"vue-apollo": "^3.0.8",
"vue-i18n": "^8.26.5",
"vue-jest": "^3.0.7",
"vue-moment": "^4.1.0",
"vue-router": "^3.5.3",
"vue-toasted": "^1.1.28",
"vuex": "^3.6.2",
"vuex-persistedstate": "^4.1.0"
},
"devDependencies": {
"@babel/eslint-parser": "^7.15.8",
"@intlify/eslint-plugin-vue-i18n": "^1.4.0",
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-service": "~4.5.0",
@ -66,6 +68,13 @@
"eslint-plugin-promise": "^5.1.1",
"eslint-plugin-vue": "^7.20.0",
"jest-environment-jsdom-sixteen": "^2.0.0",
"postcss": "^8.4.8",
"postcss-html": "^1.3.0",
"postcss-scss": "^4.0.3",
"stylelint": "^14.5.3",
"stylelint-config-recommended-vue": "^1.3.0",
"stylelint-config-standard-scss": "^3.0.0",
"vue-cli-plugin-i18n": "^2.3.1",
"vue-template-compiler": "^2.6.11"
},
"browserslist": [

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1,15 +1,21 @@
// Imports
const express = require('express')
const serveStatic = require('serve-static')
const path = require('path')
// Port
// Host & Port
const hostname = '127.0.0.1'
const port = process.env.PORT || 8080
// Express Server
const app = express()
// eslint-disable-next-line node/no-path-concat
app.use(serveStatic(__dirname + '/../dist'))
app.listen(port)
// Serve files
app.use(express.static(path.join(__dirname, '../dist')))
// Default to index.html
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, '../dist/index.html'))
})
// eslint-disable-next-line no-console
console.log(`http://admin:${port} server started.`)
app.listen(port, hostname, () => {
// eslint-disable-next-line no-console
console.log('Listening at http://%s:%s/', hostname, port)
})

View File

@ -13,3 +13,11 @@ export default {
components: { defaultLayout },
}
</script>
<style>
.pointer {
cursor: pointer;
}
.pointer:hover {
background-color: rgb(216, 213, 213);
}
</style>

View File

@ -1,24 +1,21 @@
import { mount } from '@vue/test-utils'
import ConfirmRegisterMailFormular from './ConfirmRegisterMailFormular.vue'
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
const localVue = global.localVue
const apolloMutateMock = jest.fn().mockResolvedValue()
const toastSuccessMock = jest.fn()
const toastErrorMock = jest.fn()
const mocks = {
$t: jest.fn((t) => t),
$apollo: {
mutate: apolloMutateMock,
},
$toasted: {
success: toastSuccessMock,
error: toastErrorMock,
},
}
const propsData = {
checked: false,
email: 'bob@baumeister.de',
dateLastSend: '',
}
@ -53,7 +50,7 @@ describe('ConfirmRegisterMailFormular', () => {
})
it('toasts a success message', () => {
expect(toastSuccessMock).toBeCalledWith('unregister_mail.success')
expect(toastSuccessSpy).toBeCalledWith('unregister_mail.success')
})
})
@ -65,7 +62,7 @@ describe('ConfirmRegisterMailFormular', () => {
})
it('toasts an error message', () => {
expect(toastErrorMock).toBeCalledWith('unregister_mail.error')
expect(toastErrorSpy).toBeCalledWith('unregister_mail.error')
})
})
})

View File

@ -1,19 +1,24 @@
<template>
<div class="component-confirm-register-mail">
<div class="shadow p-3 mb-5 bg-white rounded">
<div class="h5">
{{ $t('unregister_mail.text', { date: dateLastSend, mail: email }) }}
</div>
<div v-if="checked">{{ $t('unregister_mail.text_true') }}</div>
<div v-else>
{{
dateLastSend === ''
? $t('unregister_mail.never_sent', { email })
: $t('unregister_mail.text_false', { date: dateLastSend, email })
}}
<!-- Using components -->
<b-input-group :prepend="$t('unregister_mail.info')" class="mt-3">
<b-form-input readonly :value="email"></b-form-input>
<b-input-group-append>
<b-button variant="outline-success" class="test-button" @click="sendRegisterMail">
{{ $t('unregister_mail.button') }}
</b-button>
</b-input-group-append>
</b-input-group>
<!-- Using components -->
<b-input-group :prepend="$t('unregister_mail.info')" class="mt-3">
<b-form-input readonly :value="email"></b-form-input>
<b-input-group-append>
<b-button variant="outline-success" class="test-button" @click="sendRegisterMail">
{{ $t('unregister_mail.button') }}
</b-button>
</b-input-group-append>
</b-input-group>
</div>
</div>
</div>
</template>
@ -23,6 +28,9 @@ import { sendActivationEmail } from '../graphql/sendActivationEmail'
export default {
name: 'ConfirmRegisterMail',
props: {
checked: {
type: Boolean,
},
email: {
type: String,
},
@ -40,10 +48,10 @@ export default {
},
})
.then(() => {
this.$toasted.success(this.$t('unregister_mail.success', { email: this.email }))
this.toastSuccess(this.$t('unregister_mail.success', { email: this.email }))
})
.catch((error) => {
this.$toasted.error(this.$t('unregister_mail.error', { message: error.message }))
this.toastError(this.$t('unregister_mail.error', { message: error.message }))
})
},
},

View File

@ -1,15 +1,45 @@
<template>
<div class="">
<div>
<hr />
<br />
<div class="text-center">
{{ $t('gradido_admin_footer') }}
<div><small>Version: 1.0.0</small></div>
</div>
<b-row align-v="center" class="mt-4 justify-content-lg-between">
<b-col>
<div class="copyright text-center text-lg-center text-muted">
{{ $t('footer.copyright.year', { year }) }}
<a
:href="`https://gradido.net/${$i18n.locale}`"
class="font-weight-bold ml-1"
target="_blank"
>
{{ $t('footer.copyright.link') }}
</a>
{{ $t('math.pipe') }}
<a href="https://github.com/gradido/gradido/releases/latest" target="_blank">
{{ $t('footer.app_version', { version }) }}
</a>
<a
v-if="hash"
:href="'https://github.com/gradido/gradido/commit/' + hash"
target="_blank"
>
{{ $t('footer.short_hash', { shortHash }) }}
</a>
</div>
</b-col>
</b-row>
</div>
</template>
<script>
import CONFIG from '../config'
export default {
name: 'ContentFooter',
data() {
return {
year: new Date().getFullYear(),
version: CONFIG.APP_VERSION,
hash: CONFIG.BUILD_COMMIT,
shortHash: CONFIG.BUILD_COMMIT_SHORT,
}
},
}
</script>

View File

@ -1,39 +1,25 @@
import { mount } from '@vue/test-utils'
import CreationFormular from './CreationFormular.vue'
import { createPendingCreation } from '../graphql/createPendingCreation'
import { createPendingCreations } from '../graphql/createPendingCreations'
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
const localVue = global.localVue
const apolloMock = jest.fn().mockResolvedValue({
data: {
verifyLogin: {
name: 'success',
id: 0,
},
},
})
const apolloMutateMock = jest.fn().mockResolvedValue({
data: {
createPendingCreation: [0, 0, 0],
},
})
const stateCommitMock = jest.fn()
const toastedErrorMock = jest.fn()
const toastedSuccessMock = jest.fn()
const mocks = {
$t: jest.fn((t) => t),
$moment: jest.fn(() => {
return {
format: jest.fn((m) => m),
subtract: jest.fn(() => {
return {
format: jest.fn((m) => m),
}
}),
}
$t: jest.fn((t, options) => (options ? [t, options] : t)),
$d: jest.fn((d) => {
const date = new Date(d)
return date.toISOString().split('T')[0]
}),
$apollo: {
query: apolloMock,
mutate: apolloMutateMock,
},
$store: {
@ -45,16 +31,17 @@ const mocks = {
},
},
},
$toasted: {
error: toastedErrorMock,
success: toastedSuccessMock,
},
}
const propsData = {
type: '',
creation: [],
itemsMassCreation: {},
}
const now = new Date(Date.now())
const getCreationDate = (sub) => {
const date = sub === 0 ? now : new Date(now.getFullYear(), now.getMonth() - sub, 1, 0)
return date.toISOString().split('T')[0]
}
describe('CreationFormular', () => {
@ -73,21 +60,24 @@ describe('CreationFormular', () => {
expect(wrapper.find('.component-creation-formular').exists()).toBeTruthy()
})
describe('server sends back moderator data', () => {
it('called store commit with mocked data', () => {
expect(stateCommitMock).toBeCalledWith('moderator', { name: 'success', id: 0 })
})
})
describe('server throws error for moderator data call', () => {
beforeEach(() => {
jest.clearAllMocks()
apolloMock.mockRejectedValueOnce({ message: 'Ouch!' })
wrapper = Wrapper()
describe('text and value form props', () => {
beforeEach(async () => {
wrapper = mount(CreationFormular, {
localVue,
mocks,
propsData: {
creationUserData: { memo: 'Memo from property', amount: 42 },
...propsData,
},
})
})
it('has called store commit with fake data', () => {
expect(stateCommitMock).toBeCalledWith('moderator', { id: 0, name: 'Test Moderator' })
it('has text taken from props', () => {
expect(wrapper.vm.text).toBe('Memo from property')
})
it('has value taken from props', () => {
expect(wrapper.vm.value).toBe(42)
})
})
@ -96,68 +86,22 @@ describe('CreationFormular', () => {
expect(wrapper.findAll('input[type="radio"]').length).toBe(3)
})
describe('with mass creation', () => {
beforeEach(async () => {
jest.clearAllMocks()
await wrapper.setProps({ type: 'massCreation', creation: [200, 400, 600] })
await wrapper.setData({ rangeMin: 180 })
await wrapper.setData({ text: 'Test create coins' })
await wrapper.setData({ value: 90 })
})
describe('first radio button', () => {
beforeEach(async () => {
await wrapper.findAll('input[type="radio"]').at(0).setChecked()
})
it('emits update-radio-selected with index 0', () => {
expect(wrapper.emitted()['update-radio-selected']).toEqual([
[expect.arrayContaining([0])],
])
})
})
describe('second radio button', () => {
beforeEach(async () => {
await wrapper.findAll('input[type="radio"]').at(1).setChecked()
})
it('emits update-radio-selected with index 1', () => {
expect(wrapper.emitted()['update-radio-selected']).toEqual([
[expect.arrayContaining([1])],
])
})
})
describe('third radio button', () => {
beforeEach(async () => {
await wrapper.findAll('input[type="radio"]').at(2).setChecked()
})
it('emits update-radio-selected with index 2', () => {
expect(wrapper.emitted()['update-radio-selected']).toEqual([
[expect.arrayContaining([2])],
])
})
})
})
describe('with single creation', () => {
beforeEach(async () => {
jest.clearAllMocks()
await wrapper.setProps({ type: 'singleCreation', creation: [200, 400, 600] })
await wrapper.setData({ rangeMin: 180 })
await wrapper.setData({ text: 'Test create coins' })
await wrapper.setData({ value: 90 })
await wrapper.setProps({
type: 'singleCreation',
creation: [200, 400, 600],
item: { email: 'benjamin@bluemchen.de' },
})
await wrapper.findAll('input[type="radio"]').at(1).setChecked()
await wrapper.find('input[type="number"]').setValue(90)
})
describe('first radio button', () => {
beforeEach(async () => {
await wrapper.findAll('input[type="radio"]').at(0).setChecked()
})
it('sets rangeMin to 0', () => {
expect(wrapper.vm.rangeMin).toBe(0)
await wrapper.find('textarea').setValue('Test create coins')
})
it('sets rangeMax to 200', () => {
@ -170,18 +114,50 @@ describe('CreationFormular', () => {
})
it('sends ... to apollo', () => {
expect(apolloMutateMock).toBeCalled()
expect(apolloMutateMock).toBeCalledWith(
expect.objectContaining({
mutation: createPendingCreation,
variables: {
email: 'benjamin@bluemchen.de',
creationDate: getCreationDate(2),
amount: 90,
memo: 'Test create coins',
moderator: 0,
},
}),
)
})
it('emits update-user-data', () => {
expect(wrapper.emitted('update-user-data')).toEqual([
[{ email: 'benjamin@bluemchen.de' }, [0, 0, 0]],
])
})
it('toasts a success message', () => {
expect(toastSuccessSpy).toBeCalledWith([
'creation_form.toasted',
{ email: 'benjamin@bluemchen.de', value: '90' },
])
})
it('updates open creations in store', () => {
expect(stateCommitMock).toBeCalledWith('openCreationsPlus', 1)
})
it('resets the form data', () => {
expect(wrapper.vm.value).toBe(0)
})
})
describe('sendForm', () => {
describe('sendForm with server error', () => {
beforeEach(async () => {
apolloMutateMock.mockRejectedValueOnce({ message: 'Ouch!' })
await wrapper.find('.test-submit').trigger('click')
})
it('toasts an error message', () => {
expect(toastedErrorMock).toBeCalledWith('Ouch!')
expect(toastErrorSpy).toBeCalledWith('Ouch!')
})
})
@ -311,7 +287,7 @@ describe('CreationFormular', () => {
})
it('toast success message', () => {
expect(toastedSuccessMock).toBeCalled()
expect(toastSuccessSpy).toBeCalled()
})
it('store commit openCreationPlus', () => {
@ -359,6 +335,124 @@ describe('CreationFormular', () => {
})
})
})
describe('mass creation with success', () => {
beforeEach(async () => {
jest.clearAllMocks()
apolloMutateMock.mockResolvedValue({
data: {
createPendingCreations: {
success: true,
successfulCreation: ['bob@baumeister.de', 'bibi@bloxberg.de'],
failedCreation: [],
},
},
})
await wrapper.setProps({
type: 'massCreation',
creation: [200, 400, 600],
items: [{ email: 'bob@baumeister.de' }, { email: 'bibi@bloxberg.de' }],
})
await wrapper.findAll('input[type="radio"]').at(1).setChecked()
await wrapper.find('textarea').setValue('Test mass create coins')
await wrapper.find('input[type="number"]').setValue(200)
await wrapper.find('.test-submit').trigger('click')
})
it('calls the API', () => {
expect(apolloMutateMock).toBeCalledWith(
expect.objectContaining({
mutation: createPendingCreations,
variables: {
pendingCreations: [
{
email: 'bob@baumeister.de',
creationDate: getCreationDate(1),
amount: 200,
memo: 'Test mass create coins',
moderator: 0,
},
{
email: 'bibi@bloxberg.de',
creationDate: getCreationDate(1),
amount: 200,
memo: 'Test mass create coins',
moderator: 0,
},
],
},
}),
)
})
it('updates open creations in store', () => {
expect(stateCommitMock).toBeCalledWith('openCreationsPlus', 2)
})
it('emits remove-all-bookmark', () => {
expect(wrapper.emitted('remove-all-bookmark')).toBeTruthy()
})
})
describe('mass creation with success but all failed', () => {
beforeEach(async () => {
jest.clearAllMocks()
apolloMutateMock.mockResolvedValue({
data: {
createPendingCreations: {
success: true,
successfulCreation: [],
failedCreation: ['bob@baumeister.de', 'bibi@bloxberg.de'],
},
},
})
await wrapper.setProps({
type: 'massCreation',
creation: [200, 400, 600],
items: [{ email: 'bob@baumeister.de' }, { email: 'bibi@bloxberg.de' }],
})
await wrapper.findAll('input[type="radio"]').at(1).setChecked()
await wrapper.find('textarea').setValue('Test mass create coins')
await wrapper.find('input[type="number"]').setValue(200)
await wrapper.find('.test-submit').trigger('click')
})
it('updates open creations in store', () => {
expect(stateCommitMock).toBeCalledWith('openCreationsPlus', 0)
})
it('emits remove all bookmarks', () => {
expect(wrapper.emitted('remove-all-bookmark')).toBeTruthy()
})
it('emits toast failed creations with two emails', () => {
expect(wrapper.emitted('toast-failed-creations')).toEqual([
[['bob@baumeister.de', 'bibi@bloxberg.de']],
])
})
})
describe('mass creation with error', () => {
beforeEach(async () => {
jest.clearAllMocks()
apolloMutateMock.mockRejectedValue({
message: 'Oh no!',
})
await wrapper.setProps({
type: 'massCreation',
creation: [200, 400, 600],
items: [{ email: 'bob@baumeister.de' }, { email: 'bibi@bloxberg.de' }],
})
await wrapper.findAll('input[type="radio"]').at(1).setChecked()
await wrapper.find('textarea').setValue('Test mass create coins')
await wrapper.find('input[type="number"]').setValue(200)
await wrapper.find('.test-submit').trigger('click')
})
it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Oh no!')
})
})
})
})
})

View File

@ -3,53 +3,19 @@
{{ $t('creation_form.form') }}
<div class="shadow p-3 mb-5 bg-white rounded">
<b-form ref="creationForm">
<b-row class="m-4">
<div class="ml-4">
<label>{{ $t('creation_form.select_month') }}</label>
<b-col class="text-left">
<b-form-radio
id="beforeLastMonth"
v-model="radioSelected"
:value="beforeLastMonth"
:disabled="creation[0] === 0"
size="lg"
@change="updateRadioSelected(beforeLastMonth, 0, creation[0])"
>
<label for="beforeLastMonth">
{{ beforeLastMonth.short }} {{ creation[0] != null ? creation[0] + ' GDD' : '' }}
</label>
</b-form-radio>
</b-col>
<b-col>
<b-form-radio
id="lastMonth"
v-model="radioSelected"
:value="lastMonth"
:disabled="creation[1] === 0"
size="lg"
@change="updateRadioSelected(lastMonth, 1, creation[1])"
>
<label for="lastMonth">
{{ lastMonth.short }} {{ creation[1] != null ? creation[1] + ' GDD' : '' }}
</label>
</b-form-radio>
</b-col>
<b-col class="text-right">
<b-form-radio
id="currentMonth"
v-model="radioSelected"
:value="currentMonth"
:disabled="creation[2] === 0"
size="lg"
@change="updateRadioSelected(currentMonth, 2, creation[2])"
>
<label for="currentMonth">
{{ currentMonth.short }} {{ creation[2] != null ? creation[2] + ' GDD' : '' }}
</label>
</b-form-radio>
</b-col>
</div>
<b-row class="ml-4">
<b-form-radio-group
v-model="selected"
:options="radioOptions"
value-field="item"
text-field="name"
name="month-selection"
></b-form-radio-group>
</b-row>
<b-row class="m-4" v-show="createdIndex != null">
<b-row class="m-4" v-show="selected !== ''">
<label>{{ $t('creation_form.select_value') }}</label>
<div>
<b-input-group prepend="GDD" append=".00">
@ -60,7 +26,6 @@
:max="rangeMax"
></b-form-input>
</b-input-group>
<b-input-group prepend="0" :append="String(rangeMax)" class="mt-3">
<b-form-input
type="range"
@ -72,7 +37,7 @@
</b-input-group>
</div>
</b-row>
<b-row class="m-4">
<div class="m-4">
<label>{{ $t('creation_form.enter_text') }}</label>
<div>
<b-form-textarea
@ -83,9 +48,9 @@
rows="3"
></b-form-textarea>
</div>
</b-row>
</div>
<b-row class="m-4">
<b-col class="text-center">
<b-col class="text-left">
<b-button type="reset" variant="danger" @click="$refs.creationForm.reset()">
{{ $t('creation_form.reset') }}
</b-button>
@ -98,18 +63,17 @@
variant="success"
class="test-submit"
@click="submitCreation"
:disabled="radioSelected === '' || value <= 0 || text.length < 10"
:disabled="selected === '' || value <= 0 || text.length < 10"
>
{{ $t('creation_form.update_creation') }}
</b-button>
<b-button
v-else
type="button"
variant="success"
class="test-submit"
@click="submitCreation"
:disabled="radioSelected === '' || value <= 0 || text.length < 10"
:disabled="selected === '' || value <= 0 || text.length < 10"
>
{{ $t('creation_form.submit_creation') }}
</b-button>
@ -121,11 +85,12 @@
</div>
</template>
<script>
import { verifyLogin } from '../graphql/verifyLogin'
import { createPendingCreation } from '../graphql/createPendingCreation'
import { createPendingCreations } from '../graphql/createPendingCreations'
import { creationMonths } from '../mixins/creationMonths'
export default {
name: 'CreationFormular',
mixins: [creationMonths],
props: {
type: {
type: String,
@ -164,142 +129,97 @@ export default {
},
data() {
return {
radioSelected: '',
text: !this.creationUserData.memo ? '' : this.creationUserData.memo,
value: !this.creationUserData.amount ? 0 : this.creationUserData.amount,
rangeMin: 0,
rangeMax: 1000,
currentMonth: {
short: this.$moment().format('MMMM'),
long: this.$moment().format('YYYY-MM-DD'),
year: this.$moment().format('YYYY'),
},
lastMonth: {
short: this.$moment().subtract(1, 'month').format('MMMM'),
long: this.$moment().subtract(1, 'month').format('YYYY-MM') + '-01',
year: this.$moment().subtract(1, 'month').format('YYYY'),
},
beforeLastMonth: {
short: this.$moment().subtract(2, 'month').format('MMMM'),
long: this.$moment().subtract(2, 'month').format('YYYY-MM') + '-01',
year: this.$moment().subtract(2, 'month').format('YYYY'),
},
submitObj: null,
isdisabled: true,
createdIndex: null,
selected: '',
}
},
methods: {
// Auswählen eines Zeitraumes
updateRadioSelected(name, index, openCreation) {
this.createdIndex = index
updateRadioSelected(name) {
// do we want to reset the memo everytime the month changes?
this.text = this.$t('creation_form.creation_for') + ' ' + name.short + ' ' + name.year
// Wenn Mehrfachschöpfung
if (this.type === 'massCreation') {
// An Creation.vue emitten und radioSelectedMass aktualisieren
this.$emit('update-radio-selected', [name, index])
} else if (this.type === 'singleCreation') {
if (this.type === 'singleCreation') {
this.rangeMin = 0
// Der maximale offene Betrag an GDD die für ein User noch geschöpft werden kann
this.rangeMax = openCreation
this.rangeMax = name.creation
}
},
submitCreation() {
let submitObj = []
if (this.type === 'massCreation') {
// Die anzahl der Mitglieder aus der Mehrfachschöpfung
const i = Object.keys(this.items).length
// hinweis das eine Mehrfachschöpfung ausgeführt wird an (Anzahl der MItgleider an die geschöpft wird)
// eslint-disable-next-line no-console
console.log('SUBMIT CREATION => ' + this.type + ' >> für VIELE ' + i + ' Mitglieder')
this.submitObj = []
this.items.forEach((item) => {
this.submitObj.push({
submitObj.push({
email: item.email,
creationDate: this.radioSelected.long,
creationDate: this.selected.date,
amount: Number(this.value),
memo: this.text,
moderator: Number(this.$store.state.moderator.id),
})
})
// eslint-disable-next-line no-console
console.log('MehrfachSCHÖPFUNG ABSENDEN FÜR >> ' + i + ' Mitglieder')
this.$apollo
.mutate({
mutation: createPendingCreations,
variables: {
pendingCreations: this.submitObj,
pendingCreations: submitObj,
},
fetchPolicy: 'no-cache',
})
.then((result) => {
const failedCreations = []
this.$store.commit(
'openCreationsPlus',
result.data.createPendingCreations.successfulCreation.length,
)
if (result.data.createPendingCreations.failedCreation.length > 0) {
result.data.createPendingCreations.failedCreation.forEach((failed) => {
this.$toasted.error('Could not created PendingCreation for ' + failed)
result.data.createPendingCreations.failedCreation.forEach((email) => {
failedCreations.push(email)
})
}
this.$emit('remove-all-bookmark')
this.$emit('toast-failed-creations', failedCreations)
})
.catch((error) => {
this.$toasted.error(error.message)
this.toastError(error.message)
})
} else if (this.type === 'singleCreation') {
this.submitObj = {
submitObj = {
email: this.item.email,
creationDate: this.radioSelected.long,
creationDate: this.selected.date,
amount: Number(this.value),
memo: this.text,
moderator: Number(this.$store.state.moderator.id),
}
this.$apollo
.mutate({
mutation: createPendingCreation,
variables: this.submitObj,
variables: submitObj,
})
.then((result) => {
this.$emit('update-user-data', this.item, result.data.createPendingCreation)
this.$toasted.success(
this.$store.commit('openCreationsPlus', 1)
this.toastSuccess(
this.$t('creation_form.toasted', {
value: this.value,
email: this.item.email,
}),
)
this.$store.commit('openCreationsPlus', 1)
this.submitObj = null
this.createdIndex = null
// das creation Formular reseten
// what is this? Tests says that this.text is not reseted
this.$refs.creationForm.reset()
// Den geschöpften Wert auf o setzen
this.value = 0
})
.catch((error) => {
this.$toasted.error(error.message)
this.submitObj = null
// das creation Formular reseten
this.toastError(error.message)
this.$refs.creationForm.reset()
// Den geschöpften Wert auf o setzen
this.value = 0
})
}
},
searchModeratorData() {
this.$apollo
.query({ query: verifyLogin })
.then((result) => {
this.$store.commit('moderator', result.data.verifyLogin)
})
.catch(() => {
this.$store.commit('moderator', { id: 0, name: 'Test Moderator' })
})
},
},
created() {
this.searchModeratorData()
watch: {
selected() {
this.updateRadioSelected(this.selected)
},
},
}
</script>

View File

@ -1,71 +1,49 @@
import { mount } from '@vue/test-utils'
import CreationTransactionListFormular from './CreationTransactionListFormular.vue'
import { toastErrorSpy } from '../../test/testSetup'
const localVue = global.localVue
const apolloQueryMock = jest.fn().mockResolvedValue({
data: {
transactionList: {
transactions: [
{
type: 'created',
balance: 100,
decayStart: 0,
decayEnd: 0,
decayDuration: 0,
memo: 'Testing',
transactionId: 1,
name: 'Bibi',
email: 'bibi@bloxberg.de',
date: new Date(),
decay: {
balance: 0.01,
decayStart: 0,
decayEnd: 0,
decayDuration: 0,
decayStartBlock: 0,
},
creationTransactionList: [
{
id: 1,
amount: 100,
balanceDate: 0,
creationDate: new Date(),
memo: 'Testing',
linkedUser: {
firstName: 'Gradido',
lastName: 'Akademie',
},
{
type: 'created',
balance: 200,
decayStart: 0,
decayEnd: 0,
decayDuration: 0,
memo: 'Testing 2',
transactionId: 2,
name: 'Bibi',
email: 'bibi@bloxberg.de',
date: new Date(),
decay: {
balance: 0.01,
decayStart: 0,
decayEnd: 0,
decayDuration: 0,
decayStartBlock: 0,
},
},
{
id: 2,
amount: 200,
balanceDate: 0,
creationDate: new Date(),
memo: 'Testing 2',
linkedUser: {
firstName: 'Gradido',
lastName: 'Akademie',
},
],
},
},
],
},
})
const toastedErrorMock = jest.fn()
const mocks = {
$d: jest.fn((t) => t),
$t: jest.fn((t) => t),
$apollo: {
query: apolloQueryMock,
},
$toasted: {
global: {
error: toastedErrorMock,
},
},
}
const propsData = {
userId: 1,
fields: ['date', 'balance', 'name', 'memo', 'decay'],
}
describe('CreationTransactionListFormular', () => {
@ -87,7 +65,6 @@ describe('CreationTransactionListFormular', () => {
currentPage: 1,
pageSize: 25,
order: 'DESC',
onlyCreations: true,
userId: 1,
},
}),
@ -109,7 +86,7 @@ describe('CreationTransactionListFormular', () => {
})
it('toast error', () => {
expect(toastedErrorMock).toBeCalledWith('OUCH!')
expect(toastErrorSpy).toBeCalledWith('OUCH!')
})
})
})

View File

@ -1,11 +1,11 @@
<template>
<div class="component-creation-transaction-list">
{{ $t('transactionlist.title') }}
<b-table striped hover :items="items"></b-table>
<b-table striped hover :fields="fields" :items="items"></b-table>
</div>
</template>
<script>
import { transactionList } from '../graphql/transactionList'
import { creationTransactionList } from '../graphql/creationTransactionList'
export default {
name: 'CreationTransactionList',
props: {
@ -13,6 +13,37 @@ export default {
},
data() {
return {
fields: [
{
key: 'creationDate',
label: this.$t('transactionlist.date'),
formatter: (value, key, item) => {
return this.$d(new Date(value))
},
},
{
key: 'amount',
label: this.$t('transactionlist.amount'),
formatter: (value, key, item) => {
return `${value} GDD`
},
},
{
key: 'linkedUser',
label: this.$t('transactionlist.community'),
formatter: (value, key, item) => {
return `${value.firstName} ${value.lastName}`
},
},
{ key: 'memo', label: this.$t('transactionlist.memo') },
{
key: 'balanceDate',
label: this.$t('transactionlist.balanceDate'),
formatter: (value, key, item) => {
return this.$d(new Date(value))
},
},
],
items: [],
}
},
@ -20,20 +51,19 @@ export default {
getTransactions() {
this.$apollo
.query({
query: transactionList,
query: creationTransactionList,
variables: {
currentPage: 1,
pageSize: 25,
order: 'DESC',
onlyCreations: true,
userId: parseInt(this.userId),
},
})
.then((result) => {
this.items = result.data.transactionList.transactions
this.items = result.data.creationTransactionList
})
.catch((error) => {
this.$toasted.global.error(error.message)
this.toastError(error.message)
})
},
},

View File

@ -0,0 +1,248 @@
import { mount } from '@vue/test-utils'
import DeletedUserFormular from './DeletedUserFormular.vue'
import { deleteUser } from '../graphql/deleteUser'
import { unDeleteUser } from '../graphql/unDeleteUser'
import { toastErrorSpy } from '../../test/testSetup'
const localVue = global.localVue
const date = new Date()
const apolloMutateMock = jest.fn().mockResolvedValue({
data: {
deleteUser: date,
},
})
const mocks = {
$t: jest.fn((t) => t),
$apollo: {
mutate: apolloMutateMock,
},
$store: {
state: {
moderator: {
id: 0,
name: 'test moderator',
},
},
},
}
const propsData = {
item: {},
}
describe('DeletedUserFormular', () => {
let wrapper
const Wrapper = () => {
return mount(DeletedUserFormular, { localVue, mocks, propsData })
}
describe('mount', () => {
beforeEach(() => {
jest.clearAllMocks()
wrapper = Wrapper()
})
it('has a DIV element with the class.delete-user-formular', () => {
expect(wrapper.find('.deleted-user-formular').exists()).toBeTruthy()
})
})
describe('delete self', () => {
beforeEach(() => {
wrapper.setProps({
item: {
userId: 0,
},
})
})
it('shows a text that you cannot delete yourself', () => {
expect(wrapper.text()).toBe('removeNotSelf')
})
})
describe('delete other user', () => {
beforeEach(() => {
wrapper.setProps({
item: {
userId: 1,
deletedAt: null,
},
})
})
it('has a checkbox', () => {
expect(wrapper.find('input[type="checkbox"]').exists()).toBeTruthy()
})
it('shows the text "delete_user"', () => {
expect(wrapper.text()).toBe('delete_user')
})
describe('click on checkbox', () => {
beforeEach(async () => {
await wrapper.find('input[type="checkbox"]').setChecked()
})
it('has a confirmation button', () => {
expect(wrapper.find('button').exists()).toBeTruthy()
})
it('has the button text "delete_user"', () => {
expect(wrapper.find('button').text()).toBe('delete_user')
})
describe('confirm delete with success', () => {
beforeEach(async () => {
await wrapper.find('button').trigger('click')
})
it('calls the API', () => {
expect(apolloMutateMock).toBeCalledWith(
expect.objectContaining({
mutation: deleteUser,
variables: {
userId: 1,
},
}),
)
})
it('emits update deleted At', () => {
expect(wrapper.emitted('updateDeletedAt')).toEqual(
expect.arrayContaining([
expect.arrayContaining([
{
userId: 1,
deletedAt: date,
},
]),
]),
)
})
it('unchecks the checkbox', () => {
expect(wrapper.find('input').attributes('checked')).toBe(undefined)
})
})
describe('confirm delete with error', () => {
beforeEach(async () => {
apolloMutateMock.mockRejectedValue({ message: 'Oh no!' })
await wrapper.find('button').trigger('click')
})
it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Oh no!')
})
})
describe('click on checkbox again', () => {
beforeEach(async () => {
await wrapper.find('input[type="checkbox"]').setChecked(false)
})
it('has no confirmation button anymore', () => {
expect(wrapper.find('button').exists()).toBeFalsy()
})
})
})
})
describe('recover user', () => {
beforeEach(() => {
wrapper.setProps({
item: {
userId: 1,
deletedAt: date,
},
})
})
it('has a checkbox', () => {
expect(wrapper.find('input[type="checkbox"]').exists()).toBeTruthy()
})
it('shows the text "undelete_user"', () => {
expect(wrapper.text()).toBe('undelete_user')
})
describe('click on checkbox', () => {
beforeEach(async () => {
apolloMutateMock.mockResolvedValue({
data: {
unDeleteUser: null,
},
})
await wrapper.find('input[type="checkbox"]').setChecked()
})
it('has a confirmation button', () => {
expect(wrapper.find('button').exists()).toBeTruthy()
})
it('has the button text "undelete_user"', () => {
expect(wrapper.find('button').text()).toBe('undelete_user')
})
describe('confirm recover with success', () => {
beforeEach(async () => {
await wrapper.find('button').trigger('click')
})
it('calls the API', () => {
expect(apolloMutateMock).toBeCalledWith(
expect.objectContaining({
mutation: unDeleteUser,
variables: {
userId: 1,
},
}),
)
})
it('emits update deleted At', () => {
expect(wrapper.emitted('updateDeletedAt')).toEqual(
expect.arrayContaining([
expect.arrayContaining([
{
userId: 1,
deletedAt: null,
},
]),
]),
)
})
it('unchecks the checkbox', () => {
expect(wrapper.find('input').attributes('checked')).toBe(undefined)
})
})
describe('confirm recover with error', () => {
beforeEach(async () => {
apolloMutateMock.mockRejectedValue({ message: 'Oh no!' })
await wrapper.find('button').trigger('click')
})
it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Oh no!')
})
})
describe('click on checkbox again', () => {
beforeEach(async () => {
await wrapper.find('input[type="checkbox"]').setChecked(false)
})
it('has no confirmation button anymore', () => {
expect(wrapper.find('button').exists()).toBeFalsy()
})
})
})
})
})

View File

@ -0,0 +1,85 @@
<template>
<div class="deleted-user-formular">
<div v-if="item.userId === $store.state.moderator.id" class="mt-5 mb-5">
{{ $t('removeNotSelf') }}
</div>
<div v-else class="mt-5">
<b-form-checkbox switch size="lg" v-model="checked">
<div>{{ item.deletedAt ? $t('undelete_user') : $t('delete_user') }}</div>
</b-form-checkbox>
<div class="mt-3 mb-5">
<b-button v-if="checked && item.deletedAt === null" variant="danger" @click="deleteUser">
{{ $t('delete_user') }}
</b-button>
<b-button v-if="checked && item.deletedAt !== null" variant="success" @click="unDeleteUser">
{{ $t('undelete_user') }}
</b-button>
</div>
</div>
</div>
</template>
<script>
import { deleteUser } from '../graphql/deleteUser'
import { unDeleteUser } from '../graphql/unDeleteUser'
export default {
name: 'DeletedUser',
props: {
item: {
type: Object,
},
},
data() {
return {
checked: false,
}
},
methods: {
deleteUser() {
this.$apollo
.mutate({
mutation: deleteUser,
variables: {
userId: this.item.userId,
},
})
.then((result) => {
this.$emit('updateDeletedAt', {
userId: this.item.userId,
deletedAt: result.data.deleteUser,
})
this.checked = false
})
.catch((error) => {
this.toastError(error.message)
})
},
unDeleteUser() {
this.$apollo
.mutate({
mutation: unDeleteUser,
variables: {
userId: this.item.userId,
},
})
.then((result) => {
this.toastSuccess(this.$t('user_recovered'))
this.$emit('updateDeletedAt', {
userId: this.item.userId,
deletedAt: result.data.unDeleteUser,
})
this.checked = false
})
.catch((error) => {
this.toastError(error.message)
})
},
},
}
</script>
<style>
.input-group-text {
background-color: rgb(255, 252, 205);
}
</style>

View File

@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils'
import EditCreationFormular from './EditCreationFormular.vue'
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
const localVue = global.localVue
@ -7,28 +8,21 @@ const apolloMutateMock = jest.fn().mockResolvedValue({
data: {
updatePendingCreation: {
creation: [0, 0, 0],
amount: 500,
date: new Date(),
memo: 'qwertzuiopasdfghjkl',
memo: 'Test Schöpfung 2',
moderator: 0,
},
},
})
const stateCommitMock = jest.fn()
const toastedErrorMock = jest.fn()
const toastedSuccessMock = jest.fn()
const mocks = {
$t: jest.fn((t) => t),
$moment: jest.fn(() => {
return {
format: jest.fn((m) => m),
subtract: jest.fn(() => {
return {
format: jest.fn((m) => m),
}
}),
}
$d: jest.fn((d) => {
const date = new Date(d)
return date.toISOString().split('T')[0]
}),
$apollo: {
mutate: apolloMutateMock,
@ -42,10 +36,12 @@ const mocks = {
},
commit: stateCommitMock,
},
$toasted: {
error: toastedErrorMock,
success: toastedSuccessMock,
},
}
const now = new Date(Date.now())
const getCreationDate = (sub) => {
const date = sub === 0 ? now : new Date(now.getFullYear(), now.getMonth() - sub, 1, 0)
return date.toISOString().split('T')[0]
}
const propsData = {
@ -53,7 +49,7 @@ const propsData = {
creationUserData: {
memo: 'Test schöpfung 1',
amount: 100,
date: '2021-12-01',
date: getCreationDate(0),
},
item: {
id: 0,
@ -82,196 +78,79 @@ describe('EditCreationFormular', () => {
expect(wrapper.findAll('input[type="radio"]').length).toBe(3)
})
describe('with single creation', () => {
it('has the third radio button checked', () => {
expect(wrapper.findAll('input[type="radio"]').at(0).element.checked).toBeFalsy()
expect(wrapper.findAll('input[type="radio"]').at(1).element.checked).toBeFalsy()
expect(wrapper.findAll('input[type="radio"]').at(2).element.checked).toBeTruthy()
})
it('has rangeMax of 700', () => {
expect(wrapper.find('input[type="number"]').attributes('max')).toBe('700')
})
describe('change and save memo and value with success', () => {
beforeEach(async () => {
jest.clearAllMocks()
await wrapper.setProps({ creation: [200, 400, 600] })
await wrapper.setData({ rangeMin: 180 })
await wrapper.setData({ text: 'Test create coins' })
await wrapper.setData({ value: 90 })
await wrapper.find('input[type="number"]').setValue(500)
await wrapper.find('textarea').setValue('Test Schöpfung 2')
await wrapper.find('.test-submit').trigger('click')
})
describe('first radio button', () => {
beforeEach(async () => {
await wrapper.findAll('input[type="radio"]').at(0).setChecked()
})
it('sets rangeMin to 0', () => {
expect(wrapper.vm.rangeMin).toBe(0)
})
it('sets rangeMax to 200', () => {
expect(wrapper.vm.rangeMax).toBe(200)
})
describe('sendForm', () => {
beforeEach(async () => {
await wrapper.find('.test-submit').trigger('click')
})
it('sends ... to apollo', () => {
expect(apolloMutateMock).toBeCalledWith(
expect.objectContaining({
variables: {
amount: 90,
creationDate: 'YYYY-MM-01',
email: 'bob@baumeister.de',
id: 0,
memo: 'Test create coins',
moderator: 0,
},
}),
)
})
it('emits update-user-data', () => {
expect(wrapper.emitted('update-user-data')).toBeTruthy()
expect(wrapper.emitted('update-user-data')).toEqual([
[
{
id: 0,
email: 'bob@baumeister.de',
},
[0, 0, 0],
],
])
})
it('toast success message', () => {
expect(toastedSuccessMock).toBeCalledWith('creation_form.toasted_update')
})
describe('sendForm with error', () => {
beforeEach(async () => {
jest.clearAllMocks()
apolloMutateMock.mockRejectedValue({
message: 'Ouch!',
})
wrapper = Wrapper()
await wrapper.setProps({ type: 'singleCreation', creation: [200, 400, 600] })
await wrapper.setData({ text: 'Test create coins' })
await wrapper.setData({ value: 90 })
await wrapper.findAll('input[type="radio"]').at(0).setChecked()
await wrapper.setData({ rangeMin: 100 })
await wrapper.find('.test-submit').trigger('click')
})
it('toast error message', () => {
expect(toastedErrorMock).toBeCalledWith('Ouch!')
})
})
})
it('calls the API', () => {
expect(apolloMutateMock).toBeCalledWith(
expect.objectContaining({
variables: {
id: 0,
email: 'bob@baumeister.de',
creationDate: getCreationDate(0),
amount: 500,
memo: 'Test Schöpfung 2',
moderator: 0,
},
}),
)
})
describe('second radio button', () => {
beforeEach(async () => {
await wrapper.findAll('input[type="radio"]').at(1).setChecked()
})
it('sets rangeMin to 0', () => {
expect(wrapper.vm.rangeMin).toBe(0)
})
it('sets rangeMax to 400', () => {
expect(wrapper.vm.rangeMax).toBe(400)
})
describe('sendForm', () => {
beforeEach(async () => {
await wrapper.find('.test-submit').trigger('click')
})
it('sends ... to apollo', () => {
expect(apolloMutateMock).toBeCalledWith(
expect.objectContaining({
variables: {
amount: 90,
creationDate: 'YYYY-MM-01',
email: 'bob@baumeister.de',
id: 0,
memo: 'Test create coins',
moderator: 0,
},
}),
)
})
describe('sendForm with error', () => {
beforeEach(async () => {
jest.clearAllMocks()
apolloMutateMock.mockRejectedValue({
message: 'Ouch!',
})
wrapper = Wrapper()
await wrapper.setProps({ creation: [200, 400, 600] })
await wrapper.setData({ text: 'Test create coins' })
await wrapper.setData({ value: 100 })
await wrapper.findAll('input[type="radio"]').at(1).setChecked()
await wrapper.setData({ rangeMin: 180 })
await wrapper.find('.test-submit').trigger('click')
})
it('toast error message', () => {
expect(toastedErrorMock).toBeCalledWith('Ouch!')
})
})
})
it('emits update-user-data', () => {
expect(wrapper.emitted('update-user-data')).toEqual([
[
{
id: 0,
email: 'bob@baumeister.de',
},
[0, 0, 0],
],
])
})
describe('third radio button', () => {
beforeEach(async () => {
await wrapper.setData({ rangeMin: 180 })
await wrapper.findAll('input[type="radio"]').at(2).setChecked()
})
it('emits update-creation-data', () => {
expect(wrapper.emitted('update-creation-data')).toEqual([
[
{
amount: 500,
date: expect.any(Date),
memo: 'Test Schöpfung 2',
moderator: 0,
row: expect.any(Object),
},
],
])
})
it('sets rangeMin to 180', () => {
expect(wrapper.vm.rangeMin).toBe(180)
})
it('toasts a success message', () => {
expect(toastSuccessSpy).toBeCalledWith('creation_form.toasted_update')
})
})
it('sets rangeMax to 700', () => {
expect(wrapper.vm.rangeMax).toBe(700)
})
describe('change and save memo and value with error', () => {
beforeEach(async () => {
apolloMutateMock.mockRejectedValue({ message: 'Oh no!' })
await wrapper.find('input[type="number"]').setValue(500)
await wrapper.find('textarea').setValue('Test Schöpfung 2')
await wrapper.find('.test-submit').trigger('click')
})
describe('sendForm with success', () => {
beforeEach(async () => {
await wrapper.find('.test-submit').trigger('click')
})
it('sends ... to apollo', () => {
expect(apolloMutateMock).toBeCalledWith(
expect.objectContaining({
variables: {
amount: 90,
creationDate: 'YYYY-MM-DD',
email: 'bob@baumeister.de',
id: 0,
memo: 'Test create coins',
moderator: 0,
},
}),
)
})
})
describe('sendForm with error', () => {
beforeEach(async () => {
jest.clearAllMocks()
apolloMutateMock.mockRejectedValue({
message: 'Ouch!',
})
wrapper = Wrapper()
await wrapper.setProps({ creation: [200, 400, 600] })
await wrapper.setData({ text: 'Test create coins' })
await wrapper.setData({ value: 90 })
await wrapper.findAll('input[type="radio"]').at(2).setChecked()
await wrapper.setData({ rangeMin: 180 })
await wrapper.find('.test-submit').trigger('click')
})
it('toast error message', () => {
expect(toastedErrorMock).toBeCalledWith('Ouch!')
})
})
it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Oh no!')
})
})
})

View File

@ -2,68 +2,19 @@
<div class="component-edit-creation-formular">
<div class="shadow p-3 mb-5 bg-white rounded">
<b-form ref="updateCreationForm">
<b-row class="m-4">
<div class="ml-4">
<label>{{ $t('creation_form.select_month') }}</label>
<b-col class="text-left">
<b-form-radio
id="beforeLastMonth"
v-model="radioSelected"
:value="beforeLastMonth"
:disabled="selectedOpenCreationAmount[0] === 0"
size="lg"
@change="updateRadioSelected(beforeLastMonth, 0, selectedOpenCreationAmount[0])"
>
<label for="beforeLastMonth">
{{ beforeLastMonth.short }}
{{
selectedOpenCreationAmount[0] != null
? selectedOpenCreationAmount[0] + ' GDD'
: ''
}}
</label>
</b-form-radio>
</b-col>
<b-col>
<b-form-radio
id="lastMonth"
v-model="radioSelected"
:value="lastMonth"
:disabled="selectedOpenCreationAmount[1] === 0"
size="lg"
@change="updateRadioSelected(lastMonth, 1, selectedOpenCreationAmount[1])"
>
<label for="lastMonth">
{{ lastMonth.short }}
{{
selectedOpenCreationAmount[1] != null
? selectedOpenCreationAmount[1] + ' GDD'
: ''
}}
</label>
</b-form-radio>
</b-col>
<b-col class="text-right">
<b-form-radio
id="currentMonth"
v-model="radioSelected"
:value="currentMonth"
:disabled="selectedOpenCreationAmount[2] === 0"
size="lg"
@change="updateRadioSelected(currentMonth, 2, selectedOpenCreationAmount[2])"
>
<label for="currentMonth">
{{ currentMonth.short }}
{{
selectedOpenCreationAmount[2] != null
? selectedOpenCreationAmount[2] + ' GDD'
: ''
}}
</label>
</b-form-radio>
</b-col>
</b-row>
</div>
<b-row class="m-4">
<b-form-radio-group
v-model="selected"
:options="radioOptions"
value-field="item"
text-field="name"
name="month-selection"
></b-form-radio-group>
</b-row>
<div class="m-4">
<label>{{ $t('creation_form.select_value') }}</label>
<div>
<b-input-group prepend="GDD" append=".00">
@ -74,7 +25,6 @@
:max="rangeMax"
></b-form-input>
</b-input-group>
<b-input-group prepend="0" :append="String(rangeMax)" class="mt-3">
<b-form-input
type="range"
@ -85,8 +35,8 @@
></b-form-input>
</b-input-group>
</div>
</b-row>
<b-row class="m-4">
</div>
<div class="m-4">
<label>{{ $t('creation_form.enter_text') }}</label>
<div>
<b-form-textarea
@ -97,9 +47,9 @@
rows="3"
></b-form-textarea>
</div>
</b-row>
</div>
<b-row class="m-4">
<b-col class="text-center">
<b-col class="text-left">
<b-button type="reset" variant="danger" @click="$refs.updateCreationForm.reset()">
{{ $t('creation_form.reset') }}
</b-button>
@ -111,7 +61,7 @@
variant="success"
class="test-submit"
@click="submitCreation"
:disabled="radioSelected === '' || value <= 0 || text.length < 10"
:disabled="selected === '' || value <= 0 || text.length < 10"
>
{{ $t('creation_form.update_creation') }}
</b-button>
@ -124,8 +74,11 @@
</template>
<script>
import { updatePendingCreation } from '../graphql/updatePendingCreation'
import { creationMonths } from '../mixins/creationMonths'
export default {
name: 'EditCreationFormular',
mixins: [creationMonths],
props: {
item: {
type: Object,
@ -149,51 +102,26 @@ export default {
},
data() {
return {
radioSelected: '',
text: !this.creationUserData.memo ? '' : this.creationUserData.memo,
value: !this.creationUserData.amount ? 0 : this.creationUserData.amount,
rangeMin: 0,
rangeMax: 1000,
currentMonth: {
short: this.$moment().format('MMMM'),
long: this.$moment().format('YYYY-MM-DD'),
},
lastMonth: {
short: this.$moment().subtract(1, 'month').format('MMMM'),
long: this.$moment().subtract(1, 'month').format('YYYY-MM') + '-01',
},
beforeLastMonth: {
short: this.$moment().subtract(2, 'month').format('MMMM'),
long: this.$moment().subtract(2, 'month').format('YYYY-MM') + '-01',
},
submitObj: null,
isdisabled: true,
createdIndex: null,
selectedOpenCreationAmount: {},
selected: '',
}
},
methods: {
updateRadioSelected(name, index, openCreation) {
this.createdIndex = index
this.rangeMin = 0
this.rangeMax = this.creation[index]
},
submitCreation() {
this.submitObj = {
id: this.item.id,
email: this.item.email,
creationDate: this.radioSelected.long,
amount: Number(this.value),
memo: this.text,
moderator: Number(this.$store.state.moderator.id),
}
// hinweis das eine ein einzelne Schöpfung abgesendet wird an (email)
this.$apollo
.mutate({
mutation: updatePendingCreation,
variables: this.submitObj,
variables: {
id: this.item.id,
email: this.item.email,
creationDate: this.selected.date,
amount: Number(this.value),
memo: this.text,
moderator: Number(this.$store.state.moderator.id),
},
})
.then((result) => {
this.$emit('update-user-data', this.item, result.data.updatePendingCreation.creation)
@ -204,22 +132,19 @@ export default {
moderator: Number(result.data.updatePendingCreation.moderator),
row: this.row,
})
this.$toasted.success(
this.toastSuccess(
this.$t('creation_form.toasted_update', {
value: this.value,
email: this.item.email,
}),
)
this.submitObj = null
this.createdIndex = null
// das creation Formular reseten
this.$refs.updateCreationForm.reset()
// Den geschöpften Wert auf o setzen
this.value = 0
})
.catch((error) => {
this.$toasted.error(error.message)
this.submitObj = null
this.toastError(error.message)
// das creation Formular reseten
this.$refs.updateCreationForm.reset()
// Den geschöpften Wert auf o setzen
@ -229,25 +154,10 @@ export default {
},
created() {
if (this.creationUserData.date) {
switch (this.$moment(this.creationUserData.date).format('MMMM')) {
case this.currentMonth.short:
this.createdIndex = 2
this.radioSelected = this.currentMonth
break
case this.lastMonth.short:
this.createdIndex = 1
this.radioSelected = this.lastMonth
break
case this.beforeLastMonth.short:
this.createdIndex = 0
this.radioSelected = this.beforeLastMonth
break
default:
throw new Error('Something went wrong')
}
this.selectedOpenCreationAmount[this.createdIndex] =
this.creation[this.createdIndex] + this.creationUserData.amount
this.rangeMax = this.selectedOpenCreationAmount[this.createdIndex]
const month = this.$d(new Date(this.creationUserData.date), 'month')
const index = this.radioOptions.findIndex((obj) => obj.item.short === month)
this.selected = this.radioOptions[index].item
this.rangeMax = this.creation[index] + this.creationUserData.amount
}
},
}

View File

@ -53,13 +53,17 @@ describe('NavBar', () => {
})
describe('logout', () => {
// const assignLocationSpy = jest.fn()
const windowLocationMock = jest.fn()
beforeEach(async () => {
delete window.location
window.location = {
assign: windowLocationMock,
}
await wrapper.findAll('a').at(6).trigger('click')
})
it('redirects to /logout', () => {
expect(routerPushMock).toBeCalledWith('/logout')
expect(windowLocationMock).toBeCalledWith('http://localhost/login')
})
it('dispatches logout to store', () => {

View File

@ -2,7 +2,7 @@
<div class="component-nabvar">
<b-navbar toggleable="md" type="dark" variant="success" class="p-3">
<b-navbar-brand to="/">
<img src="img/brand/green.png" class="navbar-brand-img" alt="..." />
<img src="img/brand/gradido_logo_w.png" class="navbar-brand-img" alt="..." />
</b-navbar-brand>
<b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
@ -19,7 +19,7 @@
>
{{ $store.state.openCreations }} {{ $t('navbar.open_creation') }}
</b-nav-item>
<b-nav-item @click="wallet">{{ $t('navbar.wallet') }}</b-nav-item>
<b-nav-item @click="wallet">{{ $t('navbar.my-account') }}</b-nav-item>
<b-nav-item @click="logout">{{ $t('navbar.logout') }}</b-nav-item>
</b-navbar-nav>
</b-collapse>
@ -33,11 +33,12 @@ export default {
name: 'navbar',
methods: {
logout() {
window.location.assign(CONFIG.WALLET_URL)
// window.location = CONFIG.WALLET_URL
this.$store.dispatch('logout')
this.$router.push('/logout')
},
wallet() {
window.location = CONFIG.WALLET_AUTH_URL.replace('$1', this.$store.state.token)
window.location = CONFIG.WALLET_AUTH_URL.replace('{token}', this.$store.state.token)
this.$store.dispatch('logout') // logout without redirect
},
},

View File

@ -3,11 +3,15 @@ import NotFoundPage from './NotFoundPage'
const localVue = global.localVue
const mocks = {
$t: jest.fn((t) => t),
}
describe('NotFoundPage', () => {
let wrapper
const Wrapper = () => {
return mount(NotFoundPage, { localVue })
return mount(NotFoundPage, { localVue, mocks })
}
describe('mount', () => {
@ -18,5 +22,9 @@ describe('NotFoundPage', () => {
it('has a svg', () => {
expect(wrapper.find('svg').exists()).toBeTruthy()
})
it('has a back button', () => {
expect(wrapper.find('.test-back').exists()).toBeTruthy()
})
})
})

View File

@ -4,7 +4,7 @@
<div class="header py-1 py-lg-1 pt-lg-3">
<b-container>
<div class="header-body text-center mb-3">
<a href="login" to="login">
<a href="#!" @click="goback">
<div class="container">
<div class="row">
<div class="col-sm-12 col-md-12 mt-5 mb-5">
@ -1185,6 +1185,11 @@
</div>
</b-container>
</div>
<div class="text-center">
<b-button class="test-back" variant="light" @click="goback">
{{ $t('back') }}
</b-button>
</div>
</div>
</template>
@ -1213,6 +1218,11 @@ export default {
},
}
},
methods: {
goback() {
this.$router.go(-1)
},
},
}
</script>
<style>

View File

@ -0,0 +1,31 @@
import { mount } from '@vue/test-utils'
import Overlay from './Overlay.vue'
const localVue = global.localVue
const propsData = {
item: {},
}
const mocks = {
$t: jest.fn((t) => t),
$d: jest.fn((d) => String(d)),
}
describe('Overlay', () => {
let wrapper
const Wrapper = () => {
return mount(Overlay, { localVue, mocks, propsData })
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('has a DIV element with the class.component-overlay', () => {
expect(wrapper.find('.component-overlay').exists()).toBeTruthy()
})
})
})

View File

@ -0,0 +1,67 @@
<template>
<div class="component-overlay">
<b-jumbotron class="bg-light p-4">
<template #header>{{ $t('overlay.confirm.title') }}</template>
<template #lead>
<b-row class="mt-4">
<b-col class="col-3">{{ $t('transactionlist.amount') }}</b-col>
<b-col class="h3">
<b>{{ item.amount }} {{ $t('GDD') }}</b>
</b-col>
</b-row>
<b-row>
<b-col class="col-3">{{ $t('creation_for_month') }}</b-col>
<b-col class="h3">
{{ $d(new Date(item.date), 'month') }} {{ $d(new Date(item.date), 'year') }}
</b-col>
</b-row>
<b-row>
<b-col class="col-3">{{ $t('transactionlist.memo') }}</b-col>
<b-col>{{ item.memo }}</b-col>
</b-row>
<b-row class="mt-3">
<b-col class="col-3">{{ $t('name') }}</b-col>
<b-col>{{ item.firstName }} {{ item.lastName }}</b-col>
</b-row>
<b-row>
<b-col class="col-3">{{ $t('e_mail') }}</b-col>
<b-col>{{ item.email }}</b-col>
</b-row>
</template>
<hr class="my-4" />
<p>{{ $t('overlay.confirm.text') }}</p>
<p>
{{ $t('overlay.confirm.question') }}
</p>
<b-container>
<b-row>
<b-col>
<b-button size="md" variant="danger" class="m-3" @click="$emit('overlay-cancel')">
{{ $t('overlay.confirm.cancel') }}
</b-button>
</b-col>
<b-col class="text-right">
<b-button
size="md"
variant="success"
class="m-3 text-right"
@click="$emit('confirm-creation', item)"
>
{{ $t('overlay.confirm.yes') }}
</b-button>
</b-col>
</b-row>
</b-container>
</b-jumbotron>
</div>
</template>
<script>
export default {
name: 'overlay',
props: {
item: { type: Object, required: true },
},
}
</script>

View File

@ -1,10 +1,7 @@
<template>
<b-card class="shadow-lg pl-3 pr-3 mb-5 bg-white rounded">
<b-row class="mb-2">
<b-col></b-col>
</b-row>
<slot :name="slotName" />
<b-button size="sm" @click="$emit('row-toogle-details', row, index)">
<b-button size="sm" @click="$emit('row-toggle-details', row, index)">
<b-icon
:icon="type === 'PageCreationConfirm' ? 'x' : 'eye-slash-fill'"
aria-label="Help"

View File

@ -0,0 +1,86 @@
<template>
<div class="component-open-creations-table">
<b-table-lite :items="items" :fields="fields" caption-top striped hover stacked="md">
<template #cell(bookmark)="row">
<b-button
variant="danger"
size="md"
@click="$emit('remove-creation', row.item)"
class="mr-2"
>
<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">
<b-icon :icon="row.detailsShowing ? 'x' : 'pencil-square'" aria-label="Help"></b-icon>
</b-button>
</template>
<template #cell(confirm)="row">
<b-button variant="success" size="md" @click="$emit('show-overlay', row.item)" class="mr-2">
<b-icon icon="check" scale="2" variant=""></b-icon>
</b-button>
</template>
<template #row-details="row">
<row-details
:row="row"
type="show-creation"
slotName="show-creation"
:index="0"
@row-toggle-details="rowToggleDetails"
>
<template #show-creation>
<div>
<edit-creation-formular
type="singleCreation"
:creation="row.item.creation"
:item="row.item"
:row="row"
:creationUserData="creationUserData"
@update-creation-data="updateCreationData"
@update-user-data="updateUserData"
/>
</div>
</template>
</row-details>
</template>
</b-table-lite>
</div>
</template>
<script>
import { toggleRowDetails } from '../../mixins/toggleRowDetails'
import RowDetails from '../RowDetails.vue'
import EditCreationFormular from '../EditCreationFormular.vue'
export default {
name: 'OpenCreationsTable',
mixins: [toggleRowDetails],
components: {
EditCreationFormular,
RowDetails,
},
props: {
items: {
type: Array,
required: true,
},
fields: {
type: Array,
required: true,
},
},
methods: {
updateCreationData(data) {
this.creationUserData.amount = data.amount
this.creationUserData.date = data.date
this.creationUserData.memo = data.memo
this.creationUserData.moderator = data.moderator
data.row.toggleDetails()
},
updateUserData(rowItem, newCreation) {
rowItem.creation = newCreation
},
},
}
</script>

View File

@ -0,0 +1,125 @@
import { mount } from '@vue/test-utils'
import SearchUserTable from './SearchUserTable.vue'
const date = new Date()
const localVue = global.localVue
const apolloMutateMock = jest.fn().mockResolvedValue({})
const apolloQueryMock = jest.fn().mockResolvedValue({})
const propsData = {
items: [
{
userId: 1,
firstName: 'Bibi',
lastName: 'Bloxberg',
email: 'bibi@bloxberg.de',
creation: [200, 400, 600],
emailChecked: true,
},
{
userId: 2,
firstName: 'Benjamin',
lastName: 'Blümchen',
email: 'benjamin@bluemchen.de',
creation: [1000, 1000, 1000],
emailChecked: true,
},
{
userId: 3,
firstName: 'Peter',
lastName: 'Lustig',
email: 'peter@lustig.de',
creation: [0, 0, 0],
emailChecked: true,
},
{
userId: 4,
firstName: 'New',
lastName: 'User',
email: 'new@user.ch',
creation: [1000, 1000, 1000],
emailChecked: false,
},
],
fields: [
{ key: 'email', label: 'e_mail' },
{ key: 'firstName', label: 'firstname' },
{ key: 'lastName', label: 'lastname' },
{
key: 'creation',
label: 'creationLabel',
formatter: (value, key, item) => {
return value.join(' | ')
},
},
{ key: 'status', label: 'status' },
],
}
const mocks = {
$t: jest.fn((t) => t),
$d: jest.fn((d) => d),
$apollo: {
mutate: apolloMutateMock,
query: apolloQueryMock,
},
$store: {
state: {
moderator: {
id: 0,
name: 'test moderator',
},
},
},
}
describe('SearchUserTable', () => {
let wrapper
const Wrapper = () => {
return mount(SearchUserTable, { localVue, mocks, propsData })
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('has a table with four rows', () => {
expect(wrapper.findAll('tbody > tr')).toHaveLength(4)
})
describe('show row details', () => {
beforeEach(async () => {
await wrapper.findAll('tbody > tr').at(1).trigger('click')
})
describe('deleted at', () => {
beforeEach(async () => {
await wrapper.find('div.deleted-user-formular').vm.$emit('updateDeletedAt', {
userId: 1,
deletedAt: date,
})
})
it('emits updateDeletedAt', () => {
expect(wrapper.emitted('updateDeletedAt')).toEqual([[1, date]])
})
})
describe('updateUserData', () => {
beforeEach(async () => {
await wrapper
.find('div.component-creation-formular')
.vm.$emit('update-user-data', propsData.items[1], [250, 500, 750])
})
it('updates the item', () => {
expect(wrapper.vm.items[1].creation).toEqual([250, 500, 750])
})
})
})
})
})

View File

@ -0,0 +1,141 @@
<template>
<div class="search-user-table">
<b-table
tbody-tr-class="pointer"
:items="myItems"
:fields="fields"
caption-top
striped
hover
stacked="md"
select-mode="single"
selectableonRowSelected
@row-clicked="onRowClicked"
>
<template #cell(creation)="data">
<div v-html="data.value"></div>
</template>
<template #cell(status)="row">
<div class="text-right">
<b-avatar v-if="row.item.deletedAt" class="mr-3" variant="light">
<b-iconstack font-scale="2">
<b-icon stacked icon="person" variant="info" scale="0.75"></b-icon>
<b-icon stacked icon="slash-circle" variant="danger"></b-icon>
</b-iconstack>
</b-avatar>
<span v-if="!row.item.deletedAt">
<b-avatar
v-if="!row.item.emailChecked"
icon="envelope"
class="align-center mr-3"
variant="danger"
></b-avatar>
<b-avatar
v-if="!row.item.hasElopage"
variant="danger"
class="mr-3"
src="img/elopage_favicon.png"
></b-avatar>
</span>
<b-icon
variant="dark"
:icon="row.detailsShowing ? 'caret-up-fill' : 'caret-down'"
:title="row.item.enabled ? $t('enabled') : $t('deleted')"
></b-icon>
</div>
</template>
<template #row-details="row">
<b-card ref="rowDetails" class="shadow-lg pl-3 pr-3 mb-5 bg-white rounded">
<creation-formular
v-if="!row.item.deletedAt"
type="singleCreation"
pagetype="singleCreation"
:creation="row.item.creation"
:item="row.item"
:creationUserData="creationUserData"
@update-user-data="updateUserData"
/>
<div v-else>{{ $t('userIsDeleted') }}</div>
<confirm-register-mail-formular
v-if="!row.item.deletedAt"
:checked="row.item.emailChecked"
:email="row.item.email"
:dateLastSend="
row.item.emailConfirmationSend
? $d(new Date(row.item.emailConfirmationSend), 'long')
: ''
"
/>
<creation-transaction-list-formular
v-if="!row.item.deletedAt"
:userId="row.item.userId"
/>
<deleted-user-formular :item="row.item" @updateDeletedAt="updateDeletedAt" />
</b-card>
</template>
</b-table>
</div>
</template>
<script>
import CreationFormular from '../CreationFormular.vue'
import ConfirmRegisterMailFormular from '../ConfirmRegisterMailFormular.vue'
import CreationTransactionListFormular from '../CreationTransactionListFormular.vue'
import DeletedUserFormular from '../DeletedUserFormular.vue'
export default {
name: 'SearchUserTable',
components: {
CreationFormular,
ConfirmRegisterMailFormular,
CreationTransactionListFormular,
DeletedUserFormular,
},
props: {
items: {
type: Array,
required: true,
},
fields: {
type: Array,
required: true,
},
},
data() {
return {
creationUserData: {},
}
},
methods: {
updateUserData(rowItem, newCreation) {
rowItem.creation = newCreation
},
updateDeletedAt({ userId, deletedAt }) {
this.$emit('updateDeletedAt', userId, deletedAt)
},
async onRowClicked(item) {
const status = this.myItems.find((obj) => obj === item)._showDetails
this.myItems.forEach((obj) => {
if (obj === item) {
obj._showDetails = !status
} else {
obj._showDetails = false
}
})
await this.$nextTick()
if (!status && this.$refs.rowDetails) {
this.$refs.rowDetails.focus()
}
},
},
computed: {
myItems() {
return this.items.map((item) => {
return { ...item, _showDetails: false }
})
},
},
}
</script>

View File

@ -0,0 +1,35 @@
<template>
<div class="component-select-users-table">
<b-table-lite :items="items" :fields="fields" caption-top striped hover stacked="md">
<template #cell(bookmark)="row">
<div>
<b-button
v-if="row.item.emailChecked"
variant="warning"
size="md"
@click="$emit('push-item', row.item)"
class="mr-2"
>
<b-icon icon="plus" variant="success"></b-icon>
</b-button>
<div v-else>{{ $t('e_mail') }}{{ $t('math.exclaim') }}</div>
</div>
</template>
</b-table-lite>
</div>
</template>
<script>
export default {
name: 'SelectUsersTable',
props: {
items: {
type: Array,
required: true,
},
fields: {
type: Array,
required: true,
},
},
}
</script>

View File

@ -0,0 +1,26 @@
<template>
<div class="component-selected-users-table">
<b-table-lite :items="items" :fields="fields" caption-top striped hover stacked="md">
<template #cell(bookmark)="row">
<b-button variant="danger" size="md" @click="$emit('remove-item', row.item)" class="mr-2">
<b-icon icon="x" variant="light"></b-icon>
</b-button>
</template>
</b-table-lite>
</div>
</template>
<script>
export default {
name: 'SelectedUsersTable',
props: {
items: {
type: Array,
required: true,
},
fields: {
type: Array,
required: true,
},
},
}
</script>

View File

@ -1,33 +0,0 @@
import { mount } from '@vue/test-utils'
import UserTable from './UserTable.vue'
const localVue = global.localVue
describe('UserTable', () => {
let wrapper
const propsData = {
type: 'Type',
itemsUser: [],
fieldsTable: [],
creation: [],
}
const mocks = {
$t: jest.fn((t) => t),
}
const Wrapper = () => {
return mount(UserTable, { localVue, propsData, mocks })
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('has a DIV element with the class.component-user-table', () => {
expect(wrapper.find('.component-user-table').exists()).toBeTruthy()
})
})
})

View File

@ -1,339 +0,0 @@
<template>
<div class="component-user-table">
<div v-show="overlay" id="overlay" class="">
<b-jumbotron class="bg-light p-4">
<template #header>{{ overlayText.header }}</template>
<template #lead>
{{ overlayText.text1 }}
</template>
<hr class="my-4" />
<p>
{{ overlayText.text2 }}
</p>
<b-button size="md" variant="danger" class="m-3" @click="overlayCancel">
{{ overlayText.button_cancel }}
</b-button>
<b-button
size="md"
variant="success"
class="m-3 text-right"
@click="overlayOK(overlayBookmarkType, overlayItem)"
>
{{ overlayText.button_ok }}
</b-button>
</b-jumbotron>
</div>
<b-table-lite
:items="itemsUser"
:fields="fieldsTable"
:filter="criteria"
caption-top
striped
hover
stacked="md"
>
<template #cell(creation)="data">
<div v-html="data.value"></div>
</template>
<template #cell(edit_creation)="row">
<b-button variant="info" size="md" @click="rowToogleDetails(row, 0)" class="mr-2">
<b-icon :icon="row.detailsShowing ? 'x' : 'pencil-square'" aria-label="Help"></b-icon>
</b-button>
</template>
<template #cell(show_details)="row">
<b-button variant="info" size="md" @click="rowToogleDetails(row, 0)" class="mr-2">
<b-icon :icon="row.detailsShowing ? 'eye-slash-fill' : 'eye-fill'"></b-icon>
</b-button>
</template>
<template #cell(confirm_mail)="row">
<b-button
:variant="row.item.emailChecked ? 'success' : 'danger'"
size="md"
@click="rowToogleDetails(row, 1)"
class="mr-2"
>
<b-icon
:icon="row.item.emailChecked ? 'envelope-open' : 'envelope'"
aria-label="Help"
></b-icon>
</b-button>
</template>
<template #cell(transactions_list)="row">
<b-button variant="warning" size="md" @click="rowToogleDetails(row, 2)" class="mr-2">
<b-icon icon="list"></b-icon>
</b-button>
</template>
<template #row-details="row">
<row-details
:row="row"
:type="type"
:slotName="slotName"
:index="slotIndex"
@row-toogle-details="rowToogleDetails"
>
<template #show-creation>
<div>
<creation-formular
v-if="type === 'PageUserSearch'"
type="singleCreation"
:pagetype="type"
:creation="row.item.creation"
:item="row.item"
:creationUserData="creationUserData"
@update-creation-data="updateCreationData"
@update-user-data="updateUserData"
/>
<edit-creation-formular
v-else
type="singleCreation"
:pagetype="type"
:creation="row.item.creation"
:item="row.item"
:row="row"
:creationUserData="creationUserData"
@update-creation-data="updateCreationData"
@update-user-data="updateUserData"
/>
</div>
</template>
<template #show-register-mail>
<confirm-register-mail-formular
:email="row.item.email"
:dateLastSend="$moment().subtract(1, 'month').format('dddd, DD.MMMM.YYYY HH:mm'),"
/>
</template>
<template #show-transaction-list>
<creation-transaction-list-formular :userId="row.item.userId" />
</template>
</row-details>
</template>
<template #cell(bookmark)="row">
<b-button
variant="warning"
v-show="type === 'UserListSearch'"
size="md"
@click="bookmarkPush(row.item)"
class="mr-2"
>
<b-icon icon="plus" variant="success"></b-icon>
</b-button>
<b-button
variant="danger"
v-show="type === 'UserListMassCreation' || type === 'PageCreationConfirm'"
size="md"
@click="overlayShow('remove', row.item)"
class="mr-2"
>
<b-icon icon="x" variant="light"></b-icon>
</b-button>
</template>
<template #cell(confirm)="row">
<b-button
variant="success"
v-show="type === 'PageCreationConfirm'"
size="md"
@click="overlayShow('confirm', row.item)"
class="mr-2"
>
<b-icon icon="check" scale="2" variant=""></b-icon>
</b-button>
</template>
</b-table-lite>
</div>
</template>
<script>
import CreationFormular from '../components/CreationFormular.vue'
import EditCreationFormular from '../components/EditCreationFormular.vue'
import ConfirmRegisterMailFormular from '../components/ConfirmRegisterMailFormular.vue'
import CreationTransactionListFormular from '../components/CreationTransactionListFormular.vue'
import RowDetails from '../components/RowDetails.vue'
import { confirmPendingCreation } from '../graphql/confirmPendingCreation'
const slotNames = ['show-creation', 'show-register-mail', 'show-transaction-list']
export default {
name: 'UserTable',
props: {
type: {
type: String,
required: true,
},
itemsUser: {
type: Array,
required: true,
},
fieldsTable: {
type: Array,
required: true,
},
criteria: {
type: String,
required: false,
default: '',
},
creation: {
type: Array,
required: false,
},
},
components: {
CreationFormular,
EditCreationFormular,
ConfirmRegisterMailFormular,
CreationTransactionListFormular,
RowDetails,
},
data() {
return {
showCreationFormular: null,
showConfirmRegisterMailFormular: null,
showCreationTransactionListFormular: null,
creationUserData: {},
overlay: false,
overlayBookmarkType: '',
overlayItem: [],
overlayText: [
{
header: '-',
text1: '--',
text2: '---',
button_ok: 'OK',
button_cancel: 'Cancel',
},
],
slotIndex: 0,
openRow: null,
}
},
methods: {
rowToogleDetails(row, index) {
if (this.openRow) {
if (this.openRow.index === row.index) {
if (index === this.slotIndex) {
row.toggleDetails()
this.openRow = null
} else {
this.slotIndex = index
}
} else {
this.openRow.toggleDetails()
row.toggleDetails()
this.slotIndex = index
this.openRow = row
}
} else {
row.toggleDetails()
this.slotIndex = index
this.openRow = row
if (this.type === 'PageCreationConfirm') {
this.creationUserData = row.item
}
}
},
overlayShow(bookmarkType, item) {
this.overlay = true
this.overlayBookmarkType = bookmarkType
this.overlayItem = item
if (bookmarkType === 'remove') {
this.overlayText.header = this.$t('overlay.remove.title')
this.overlayText.text1 = this.$t('overlay.remove.text')
this.overlayText.text2 = this.$t('overlay.remove.question')
this.overlayText.button_ok = this.$t('overlay.remove.yes')
this.overlayText.button_cancel = this.$t('overlay.remove.no')
}
if (bookmarkType === 'confirm') {
this.overlayText.header = this.$t('overlay.confirm.title')
this.overlayText.text1 = this.$t('overlay.confirm.text')
this.overlayText.text2 = this.$t('overlay.confirm.question')
this.overlayText.button_ok = this.$t('overlay.confirm.yes')
this.overlayText.button_cancel = this.$t('overlay.confirm.no')
}
},
overlayOK(bookmarkType, item) {
if (bookmarkType === 'remove') {
this.bookmarkRemove(item)
}
if (bookmarkType === 'confirm') {
this.bookmarkConfirm(item)
}
this.overlay = false
},
overlayCancel() {
this.overlay = false
},
bookmarkPush(item) {
this.$emit('update-item', item, 'push')
},
bookmarkRemove(item) {
if (this.type === 'UserListMassCreation') {
this.$emit('update-item', item, 'remove')
}
if (this.type === 'PageCreationConfirm') {
this.$emit('remove-confirm-result', item, 'remove')
}
},
bookmarkConfirm(item) {
this.$apollo
.mutate({
mutation: confirmPendingCreation,
variables: {
id: item.id,
},
})
.then(() => {
this.$emit('remove-confirm-result', item, 'confirmed')
})
.catch((error) => {
this.$toasted.error(error.message)
})
},
updateCreationData(data) {
this.creationUserData.amount = data.amount
this.creationUserData.date = data.date
this.creationUserData.memo = data.memo
this.creationUserData.moderator = data.moderator
data.row.toggleDetails()
},
updateUserData(rowItem, newCreation) {
rowItem.creation = newCreation
},
},
computed: {
slotName() {
return slotNames[this.slotIndex]
},
},
}
</script>
<style>
#overlay {
position: fixed;
display: flex;
align-items: center;
width: 100%;
height: 100%;
top: 0;
left: 0;
right: 0;
bottom: 0;
padding-left: 5%;
background-color: rgba(12, 11, 11, 0.781);
z-index: 1000000;
cursor: pointer;
}
</style>

View File

@ -4,11 +4,20 @@
// Load Package Details for some default values
const pkg = require('../../package')
const constants = {
CONFIG_VERSION: {
DEFAULT: 'DEFAULT',
EXPECTED: 'v1.2022-03-18',
CURRENT: '',
},
}
const version = {
APP_VERSION: pkg.version,
BUILD_COMMIT: process.env.BUILD_COMMIT || null,
// self reference of `version.BUILD_COMMIT` is not possible at this point, hence the duplicate code
BUILD_COMMIT_SHORT: (process.env.BUILD_COMMIT || '0000000').substr(0, 7),
BUILD_COMMIT_SHORT: (process.env.BUILD_COMMIT || '0000000').slice(0, 7),
PORT: process.env.PORT || 8080,
}
const environment = {
@ -19,21 +28,32 @@ const environment = {
const endpoints = {
GRAPHQL_URI: process.env.GRAPHQL_URI || 'http://localhost:4000/graphql',
WALLET_AUTH_URL: process.env.WALLET_AUTH_URL || 'http://localhost/vue/authenticate?token=$1',
WALLET_AUTH_URL: process.env.WALLET_AUTH_URL || 'http://localhost/authenticate?token={token}',
WALLET_URL: process.env.WALLET_URL || 'http://localhost/login',
}
const debug = {
DEBUG_DISABLE_AUTH: process.env.DEBUG_DISABLE_AUTH === 'true' || false,
}
const options = {}
// 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,
...version,
...environment,
...endpoints,
...options,
...debug,
}
export default CONFIG
module.exports = CONFIG

View File

@ -1,7 +1,7 @@
import gql from 'graphql-tag'
export const confirmPendingCreation = gql`
mutation ($id: Float!) {
mutation ($id: Int!) {
confirmPendingCreation(id: $id)
}
`

View File

@ -0,0 +1,22 @@
import gql from 'graphql-tag'
export const creationTransactionList = gql`
query ($currentPage: Int = 1, $pageSize: Int = 25, $order: Order = DESC, $userId: Int!) {
creationTransactionList(
currentPage: $currentPage
pageSize: $pageSize
order: $order
userId: $userId
) {
id
amount
balanceDate
creationDate
memo
linkedUser {
firstName
lastName
}
}
}
`

View File

@ -1,7 +1,7 @@
import gql from 'graphql-tag'
export const deletePendingCreation = gql`
mutation ($id: Float!) {
mutation ($id: Int!) {
deletePendingCreation(id: $id)
}
`

View File

@ -0,0 +1,7 @@
import gql from 'graphql-tag'
export const deleteUser = gql`
mutation ($userId: Int!) {
deleteUser(userId: $userId)
}
`

View File

@ -1,14 +1,32 @@
import gql from 'graphql-tag'
export const searchUsers = gql`
query ($searchText: String!) {
searchUsers(searchText: $searchText) {
userId
firstName
lastName
email
creation
emailChecked
query (
$searchText: String!
$currentPage: Int
$pageSize: Int
$notActivated: Boolean
$isDeleted: Boolean
) {
searchUsers(
searchText: $searchText
currentPage: $currentPage
pageSize: $pageSize
notActivated: $notActivated
isDeleted: $isDeleted
) {
userCount
userList {
userId
firstName
lastName
email
creation
emailChecked
hasElopage
emailConfirmationSend
deletedAt
}
}
}
`

View File

@ -1,44 +0,0 @@
import gql from 'graphql-tag'
export const transactionList = gql`
query (
$currentPage: Int = 1
$pageSize: Int = 25
$order: Order = DESC
$onlyCreations: Boolean = false
$userId: Int = null
) {
transactionList(
currentPage: $currentPage
pageSize: $pageSize
order: $order
onlyCreations: $onlyCreations
userId: $userId
) {
gdtSum
count
balance
decay
decayDate
transactions {
type
balance
decayStart
decayEnd
decayDuration
memo
transactionId
name
email
date
decay {
balance
decayStart
decayEnd
decayDuration
decayStartBlock
}
}
}
}
`

View File

@ -0,0 +1,7 @@
import gql from 'graphql-tag'
export const unDeleteUser = gql`
mutation ($userId: Int!) {
unDeleteUser(userId: $userId)
}
`

View File

@ -7,6 +7,7 @@ export const verifyLogin = gql`
lastName
isAdmin
id
language
}
}
`

View File

@ -54,12 +54,21 @@ const dateTimeFormats = {
},
long: {
year: 'numeric',
month: 'short',
month: 'long',
day: 'numeric',
weekday: 'short',
weekday: 'long',
hour: 'numeric',
minute: 'numeric',
},
monthShort: {
month: 'short',
},
month: {
month: 'long',
},
year: {
year: 'numeric',
},
},
de: {
short: {
@ -69,12 +78,21 @@ const dateTimeFormats = {
},
long: {
day: 'numeric',
month: 'short',
month: 'long',
year: 'numeric',
weekday: 'short',
weekday: 'long',
hour: 'numeric',
minute: 'numeric',
},
monthShort: {
month: 'short',
},
month: {
month: 'long',
},
year: {
year: 'numeric',
},
},
}

View File

@ -1,8 +1,10 @@
{
"bookmark": "bookmark",
"confirmed": "bestätigt",
"all_emails": "Alle Nutzer",
"back": "zurück",
"creation": "Schöpfung",
"creation_form": {
"creation_for": "Schöpfung für ",
"creation_failed": "Ausstehende Schöpfung für {email} konnte nicht erzeugt werden.",
"creation_for": "Aktives Grundeinkommen für",
"enter_text": "Text eintragen",
"form": "Schöpfungsformular",
"min_characters": "Mindestens 10 Zeichen eingeben",
@ -11,55 +13,87 @@
"select_value": "Betrag auswählen",
"submit_creation": "Schöpfung einreichen",
"toasted": "Offene Schöpfung ({value} GDD) für {email} wurde gespeichert und liegt zur Bestätigung bereit",
"toasted_created": "Schöpfung wurde erfolgreich gespeichert",
"toasted_delete": "Offene Schöpfung wurde gelöscht",
"toasted_update": "`Offene Schöpfung {value} GDD) für {email} wurde geändert und liegt zur Bestätigung bereit",
"update_creation": "Schöpfung aktualisieren"
},
"details": "Details",
"creation_for_month": "Schöpfung für Monat",
"date": "Datum",
"delete": "Löschen",
"deleted": "gelöscht",
"deleted_user": "Alle gelöschten Nutzer",
"delete_user": "Nutzer löschen",
"edit": "Bearbeiten",
"enabled": "aktiviert",
"error": "Fehler",
"e_mail": "E-Mail",
"firstname": "Vorname",
"gradido_admin_footer": "Gradido Akademie Adminkonsole",
"hide_details": "Details verbergen von",
"footer": {
"app_version": "App version {version}",
"copyright": {
"link": "Gradido Akademie Adminkonsole",
"year": "© {year}"
},
"short_hash": "({shortHash})"
},
"GDD": "GDD",
"hide_details": "Details verbergen",
"lastname": "Nachname",
"math": {
"exclaim": "!",
"pipe": "|"
},
"moderator": "Moderator",
"multiple_creation_text": "Bitte wähle ein oder mehrere Mitglieder aus für die du Schöpfen möchtest.",
"name": "Name",
"navbar": {
"logout": "Abmelden",
"multi_creation": "Mehrfachschöpfung",
"my-account": "Mein Konto",
"open_creation": "Offene Schöpfungen",
"overview": "Übersicht",
"user_search": "Nutzersuche",
"wallet": "Wallet"
"user_search": "Nutzersuche"
},
"not_open_creations": "Keine offenen Schöpfungen",
"open_creation": "Offene Schöpfung",
"open_creations": "Offene Schöpfungen",
"overlay": {
"confirm": {
"no": "Nein, nicht speichern.",
"question": "Willst du diese vorgespeicherte Schöpfung wirklich vollziehen und entgültig speichern?",
"cancel": "Abbrechen",
"question": "Willst du diese vorgespeicherte Schöpfung wirklich vollziehen und endgültig speichern?",
"text": "Nach dem Speichern ist der Datensatz nicht mehr änderbar und kann auch nicht mehr gelöscht werden. Bitte überprüfe genau, dass alles stimmt.",
"title": "Schöpfung bestätigen!",
"yes": "Ja, Schöpfung bestätigen und speichern!"
},
"remove": {
"no": "Nein, nicht löschen.",
"question": "Willst du die vorgespeicherte Schöpfung wirklich löschen?",
"text": "Nach dem Löschen gibt es keine Möglichkeit mehr diesen Datensatz wiederherzustellen. Es wird aber der gesamte Vorgang in der Logdatei als Übersicht gespeichert.",
"title": "Achtung! Schöpfung löschen!",
"yes": "Ja, Schöpfung löschen!"
}
},
"remove": "Entfernen",
"transaction": "Transaktion",
"removeNotSelf": "Als Admin / Moderator kannst du dich nicht selber löschen.",
"remove_all": "alle Nutzer entfernen",
"save": "Speichern",
"status": "Status",
"success": "Erfolg",
"text": "Text",
"transactionlist": {
"amount": "Betrag",
"balanceDate": "Schöpfungsdatum",
"community": "Gemeinschaft",
"date": "Datum",
"memo": "Nachricht",
"title": "Alle geschöpften Transaktionen für den Nutzer"
},
"unregistered_emails": "Unregistrierte E-Mails",
"undelete_user": "Nutzer wiederherstellen",
"unregistered_emails": "Nur unregistrierte Nutzer",
"unregister_mail": {
"button": "Registrierungs-Email bestätigen, jetzt senden",
"error": "Fehler beim Senden des Bestätigungs-Links an den Benutzer: {message}",
"info": "Email bestätigen, wiederholt senden an:",
"never_sent": "Es scheint so, als ob wir nie eine E-Mail an {email} geschickt haben",
"success": "Erfolgreiches Senden des Bestätigungs-Links an die E-Mail des Nutzers! ({email})",
"text": " Die letzte Email wurde am <b>{date} Uhr</b> an das Mitglied ({mail}) gesendet."
"text_false": " Die letzte Email wurde am {date} Uhr an das Mitglied ({email}) gesendet.",
"text_true": " Die Email wurde bestätigt."
},
"userIsDeleted": "Der Nutzer ist gelöscht. Es können keine GDD mehr geschöpft werden.",
"user_deleted": "Nutzer ist gelöscht.",
"user_recovered": "Nutzer ist wiederhergestellt.",
"user_search": "Nutzer-Suche"
}

View File

@ -1,8 +1,10 @@
{
"bookmark": "Remember",
"confirmed": "confirmed",
"all_emails": "All users",
"back": "back",
"creation": "Creation",
"creation_form": {
"creation_for": "Creation for ",
"creation_failed": "Could not create pending creation for {email}",
"creation_for": "Active Basic Income for",
"enter_text": "Enter text",
"form": "Creation form",
"min_characters": "Enter at least 10 characters",
@ -11,55 +13,87 @@
"select_value": "Select amount",
"submit_creation": "Submit creation",
"toasted": "Open creation ({value} GDD) for {email} has been saved and is ready for confirmation.",
"toasted_created": "Creation has been successfully saved",
"toasted_delete": "Open creation has been deleted",
"toasted_update": "Open creation {value} GDD) for {email} has been changed and is ready for confirmation.",
"update_creation": "Creation update"
},
"details": "Details",
"e_mail": "E-Mail",
"creation_for_month": "Creation for month",
"date": "Date",
"delete": "Delete",
"deleted": "deleted",
"deleted_user": "All deleted user",
"delete_user": "Delete user",
"edit": "Edit",
"enabled": "enabled",
"error": "Error",
"e_mail": "E-mail",
"firstname": "Firstname",
"gradido_admin_footer": "Gradido Academy Admin Console",
"hide_details": "Hide details from",
"footer": {
"app_version": "App version {version}",
"copyright": {
"link": "Gradido Academy Admin Console",
"year": "© {year}"
},
"short_hash": "({shortHash})"
},
"GDD": "GDD",
"hide_details": "Hide details",
"lastname": "Lastname",
"math": {
"exclaim": "!",
"pipe": "|"
},
"moderator": "Moderator",
"multiple_creation_text": "Please select one or more members for which you would like to perform creations.",
"name": "Name",
"navbar": {
"logout": "Logout",
"multi_creation": "Multiple creation",
"my-account": "My Account",
"open_creation": "Open creations",
"overview": "Overview",
"user_search": "User search",
"wallet": "Wallet"
"user_search": "User search"
},
"not_open_creations": "No open creations",
"open_creation": "Open creation",
"open_creations": "Open creations",
"overlay": {
"confirm": {
"no": "No, do not save.",
"cancel": "Cancel",
"question": "Do you really want to carry out and finally save this pre-stored creation?",
"text": "After saving, the record can no longer be changed or deleted. Please check carefully that everything is correct.",
"title": "Confirm creation!",
"yes": "Yes, confirm and save creation!"
},
"remove": {
"no": "No, do not delete.",
"question": "Do you really want to delete the pre-stored creation?",
"text": "After deletion, there is no possibility to restore this data record. However, the entire process is saved in the log file as an overview.",
"title": "Attention! Delete creation!",
"yes": "Yes, delete creation!"
}
},
"remove": "Remove",
"transaction": "Transaction",
"removeNotSelf": "As admin / moderator you cannot delete yourself.",
"remove_all": "Remove all users",
"save": "Speichern",
"status": "Status",
"success": "Success",
"text": "Text",
"transactionlist": {
"amount": "Amount",
"balanceDate": "Creation date",
"community": "Community",
"date": "Date",
"memo": "Message",
"title": "All creation-transactions for the user"
},
"unregistered_emails": "Unregistered e-mails",
"undelete_user": "Undelete User",
"unregistered_emails": "Only unregistered users",
"unregister_mail": {
"button": "Confirm registration email, send now",
"error": "Error sending the confirmation link to the user: {message}",
"info": "Confirm email, send repeatedly to:",
"never_sent": "It seems we did never send an email to the member {email}",
"success": "Successfully send the confirmation link to the user's email! ({email})",
"text": " The last email was sent to the member ({mail}) on <b>{date} clock</b>."
"text_false": "The last email was sent to the member ({email}) on {date}.",
"text_true": "The email was confirmed."
},
"userIsDeleted": "The user is deleted. No more GDD can be created.",
"user_deleted": "User is deleted.",
"user_recovered": "User is recovered.",
"user_search": "User search"
}

View File

@ -13,39 +13,31 @@ import i18n from './i18n'
import VueApollo from 'vue-apollo'
import PortalVue from 'portal-vue'
import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'
import moment from 'vue-moment'
import Toasted from 'vue-toasted'
import { toasters } from './mixins/toaster'
import { apolloProvider } from './plugins/apolloProvider'
Vue.use(PortalVue)
Vue.use(BootstrapVue)
Vue.use(IconsPlugin)
Vue.use(moment)
Vue.use(VueApollo)
Vue.use(Toasted, {
position: 'top-center',
duration: 5000,
fullWidth: true,
action: {
text: 'x',
onClick: (e, toastObject) => {
toastObject.goAway(0)
},
},
})
Vue.mixin(toasters)
addNavigationGuards(router, store, apolloProvider.defaultClient)
addNavigationGuards(router, store, apolloProvider.defaultClient, i18n)
i18n.locale =
store.state.moderator && store.state.moderator.language ? store.state.moderator.language : 'en'
new Vue({
moment,
router,
store,
i18n,

View File

@ -6,7 +6,6 @@ import Vue from 'vue'
import VueApollo from 'vue-apollo'
import i18n from './i18n'
import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
import moment from 'vue-moment'
import store from './store/store'
import router from './router/router'
@ -14,8 +13,15 @@ jest.mock('vue')
jest.mock('vue-apollo')
jest.mock('vuex')
jest.mock('vue-i18n')
jest.mock('vue-moment')
jest.mock('./store/store')
jest.mock('./store/store', () => {
return {
state: {
moderator: {
language: 'es',
},
},
}
})
jest.mock('./i18n')
jest.mock('./router/router')
@ -82,10 +88,6 @@ describe('main', () => {
expect(Vue.use).toBeCalledWith(IconsPlugin)
})
it('calls Moment', () => {
expect(Vue.use).toBeCalledWith(moment)
})
it('creates a store', () => {
expect(Vue).toBeCalledWith(
expect.objectContaining({
@ -101,4 +103,8 @@ describe('main', () => {
}),
)
})
it('sets the locale from store', () => {
expect(i18n.locale).toBe('es')
})
})

View File

@ -0,0 +1,41 @@
export const creationMonths = {
props: {
creation: {
type: Array,
default: () => [1000, 1000, 1000],
},
},
computed: {
creationDates() {
const now = new Date(Date.now())
const dates = [now]
for (let i = 1; i < 3; i++) {
dates.push(new Date(now.getFullYear(), now.getMonth() - i, 1))
}
return dates.reverse()
},
creationDateObjects() {
const result = []
this.creationDates.forEach((date) => {
result.push({
short: this.$d(date, 'month'),
long: this.$d(date, 'short'),
year: this.$d(date, 'year'),
date: this.$d(date, 'short', 'en'),
})
})
return result
},
radioOptions() {
return this.creationDateObjects.map((obj, idx) => {
return {
item: { ...obj, creation: this.creation[idx] },
name: obj.short + (this.creation[idx] ? ' ' + this.creation[idx] + ' GDD' : ''),
}
})
},
creationLabel() {
return this.creationDates.map((date) => this.$d(date, 'monthShort')).join(' | ')
},
},
}

View File

@ -0,0 +1,30 @@
export const toasters = {
methods: {
toastSuccess(message) {
this.toast(message, {
title: this.$t('success'),
variant: 'success',
})
},
toastError(message) {
this.toast(message, {
title: this.$t('error'),
variant: 'danger',
})
},
toast(message, options) {
// for unit tests, check that replace is present
if (message.replace) message = message.replace(/^GraphQL error: /, '')
this.$root.$bvToast.toast(message, {
autoHideDelay: 5000,
appendToast: true,
solid: true,
toaster: 'b-toaster-top-right',
headerClass: 'gdd-toaster-title',
bodyClass: 'gdd-toaster-body',
toastClass: 'gdd-toaster',
...options,
})
},
},
}

View File

@ -0,0 +1,34 @@
export const toggleRowDetails = {
data() {
return {
slotIndex: 0,
openRow: null,
creationUserData: {},
}
},
methods: {
rowToggleDetails(row, index) {
if (this.openRow) {
if (this.openRow.index === row.index) {
if (index === this.slotIndex) {
row.toggleDetails()
this.openRow = null
} else {
this.slotIndex = index
}
} else {
this.openRow.toggleDetails()
row.toggleDetails()
this.slotIndex = index
this.openRow = row
this.creationUserData = row.item
}
} else {
row.toggleDetails()
this.slotIndex = index
this.openRow = row
this.creationUserData = row.item
}
},
},
}

View File

@ -0,0 +1,141 @@
import { toggleRowDetails } from './toggleRowDetails'
import { mount } from '@vue/test-utils'
const localVue = global.localVue
const Component = {
render() {},
mixins: [toggleRowDetails],
}
const toggleDetailsMock = jest.fn()
const secondToggleDetailsMock = jest.fn()
const row = {
toggleDetails: toggleDetailsMock,
index: 0,
item: {
data: 'item-data',
},
}
let wrapper
describe('toggleRowDetails', () => {
beforeEach(() => {
jest.clearAllMocks()
wrapper = mount(Component, { localVue })
})
it('sets default data', () => {
expect(wrapper.vm.slotIndex).toBe(0)
expect(wrapper.vm.openRow).toBe(null)
expect(wrapper.vm.creationUserData).toEqual({})
})
describe('no open row', () => {
beforeEach(() => {
wrapper.vm.rowToggleDetails(row, 2)
})
it('calls toggleDetails', () => {
expect(toggleDetailsMock).toBeCalled()
})
it('updates slot index', () => {
expect(wrapper.vm.slotIndex).toBe(2)
})
it('updates open row', () => {
expect(wrapper.vm.openRow).toEqual(
expect.objectContaining({
index: 0,
item: {
data: 'item-data',
},
}),
)
})
it('updates creation user data', () => {
expect(wrapper.vm.creationUserData).toEqual({ data: 'item-data' })
})
})
describe('with open row', () => {
beforeEach(() => {
wrapper.setData({ openRow: row })
})
describe('row index is open row index', () => {
describe('index is slot index', () => {
beforeEach(() => {
wrapper.vm.rowToggleDetails(row, 0)
})
it('calls toggleDetails', () => {
expect(toggleDetailsMock).toBeCalled()
})
it('sets open row to null', () => {
expect(wrapper.vm.openRow).toBe(null)
})
})
describe('index is not slot index', () => {
beforeEach(() => {
wrapper.vm.rowToggleDetails(row, 2)
})
it('does not call toggleDetails', () => {
expect(toggleDetailsMock).not.toBeCalled()
})
it('updates slot index', () => {
expect(wrapper.vm.slotIndex).toBe(2)
})
})
})
describe('row index is not open row index', () => {
beforeEach(() => {
wrapper.vm.rowToggleDetails(
{
toggleDetails: secondToggleDetailsMock,
index: 2,
item: {
data: 'new-item-data',
},
},
2,
)
})
it('closes the open row', () => {
expect(toggleDetailsMock).toBeCalled()
})
it('opens the new row', () => {
expect(secondToggleDetailsMock).toBeCalled()
})
it('updates slot index', () => {
expect(wrapper.vm.slotIndex).toBe(2)
})
it('updates open row', () => {
expect(wrapper.vm.openRow).toEqual({
toggleDetails: secondToggleDetailsMock,
index: 2,
item: {
data: 'new-item-data',
},
})
})
it('updates creation user data', () => {
expect(wrapper.vm.creationUserData).toEqual({ data: 'new-item-data' })
})
})
})
})

View File

@ -1,39 +1,48 @@
import { shallowMount } from '@vue/test-utils'
import { mount } from '@vue/test-utils'
import Creation from './Creation.vue'
import Vue from 'vue'
import { toastErrorSpy } from '../../test/testSetup'
const localVue = global.localVue
const apolloQueryMock = jest.fn().mockResolvedValue({
data: {
searchUsers: [
{
id: 1,
firstName: 'Bibi',
lastName: 'Bloxberg',
email: 'bibi@bloxberg.de',
creation: [200, 400, 600],
},
{
id: 2,
firstName: 'Benjamin',
lastName: 'Blümchen',
email: 'benjamin@bluemchen.de',
creation: [800, 600, 400],
},
],
searchUsers: {
userCount: 2,
userList: [
{
userId: 1,
firstName: 'Bibi',
lastName: 'Bloxberg',
email: 'bibi@bloxberg.de',
creation: [200, 400, 600],
emailChecked: true,
},
{
userId: 2,
firstName: 'Benjamin',
lastName: 'Blümchen',
email: 'benjamin@bluemchen.de',
creation: [800, 600, 400],
emailChecked: true,
},
],
},
},
})
const toastErrorMock = jest.fn()
const storeCommitMock = jest.fn()
const mocks = {
$t: jest.fn((t) => t),
$t: jest.fn((t, options) => (options ? [t, options] : t)),
$d: jest.fn((d) => d),
$apollo: {
query: apolloQueryMock,
},
$toasted: {
error: toastErrorMock,
$store: {
commit: storeCommitMock,
state: {
userSelectedInMassCreation: [],
},
},
}
@ -41,11 +50,12 @@ describe('Creation', () => {
let wrapper
const Wrapper = () => {
return shallowMount(Creation, { localVue, mocks })
return mount(Creation, { localVue, mocks })
}
describe('shallowMount', () => {
describe('mount', () => {
beforeEach(() => {
jest.clearAllMocks()
wrapper = Wrapper()
})
@ -55,165 +65,243 @@ describe('Creation', () => {
describe('apollo returns user array', () => {
it('calls the searchUser query', () => {
expect(apolloQueryMock).toBeCalled()
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
searchText: '',
currentPage: 1,
pageSize: 25,
},
}),
)
})
it('sets the data of itemsList', () => {
expect(wrapper.vm.itemsList).toEqual([
it('has two rows in the left table', () => {
expect(wrapper.findAll('table').at(0).findAll('tbody > tr')).toHaveLength(2)
})
it('has nwo rows in the right table', () => {
expect(wrapper.findAll('table').at(1).findAll('tbody > tr')).toHaveLength(0)
})
it('has correct data in first row ', () => {
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain('Bibi')
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain(
'Bloxberg',
)
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain(
'200 | 400 | 600',
)
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain(
'bibi@bloxberg.de',
)
})
it('has correct data in second row ', () => {
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(1).text()).toContain(
'Benjamin',
)
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(1).text()).toContain(
'Blümchen',
)
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(1).text()).toContain(
'800 | 600 | 400',
)
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(1).text()).toContain(
'benjamin@bluemchen.de',
)
})
})
describe('push item', () => {
beforeEach(() => {
wrapper.findAll('table').at(0).findAll('tbody > tr').at(1).find('button').trigger('click')
})
it('has one item in left table', () => {
expect(wrapper.findAll('table').at(0).findAll('tbody > tr')).toHaveLength(1)
})
it('has one item in right table', () => {
expect(wrapper.findAll('table').at(1).findAll('tbody > tr')).toHaveLength(1)
})
it('has the correct user in left table', () => {
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain(
'bibi@bloxberg.de',
)
})
it('has the correct user in right table', () => {
expect(wrapper.findAll('table').at(1).findAll('tbody > tr').at(0).text()).toContain(
'benjamin@bluemchen.de',
)
})
it('updates userSelectedInMassCreation in store', () => {
expect(storeCommitMock).toBeCalledWith('setUserSelectedInMassCreation', [
{
id: 1,
firstName: 'Bibi',
lastName: 'Bloxberg',
email: 'bibi@bloxberg.de',
creation: [200, 400, 600],
showDetails: false,
},
{
id: 2,
userId: 2,
firstName: 'Benjamin',
lastName: 'Blümchen',
email: 'benjamin@bluemchen.de',
creation: [800, 600, 400],
showDetails: false,
emailChecked: true,
},
])
})
describe('remove item', () => {
beforeEach(async () => {
await wrapper
.findAll('table')
.at(1)
.findAll('tbody > tr')
.at(0)
.find('button')
.trigger('click')
})
it('has two items in left table', () => {
expect(wrapper.findAll('table').at(0).findAll('tbody > tr')).toHaveLength(2)
})
it('has the removed user in first row', () => {
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain(
'benjamin@bluemchen.de',
)
})
it('has no items in right table', () => {
expect(wrapper.findAll('table').at(1).findAll('tbody > tr')).toHaveLength(0)
})
it('commits empty array as userSelectedInMassCreation', () => {
expect(storeCommitMock).toBeCalledWith('setUserSelectedInMassCreation', [])
})
})
describe('remove all bookmarks', () => {
beforeEach(async () => {
jest.clearAllMocks()
await wrapper.find('button.btn-light').trigger('click')
})
it('has no items in right table', () => {
expect(wrapper.findAll('table').at(1).findAll('tbody > tr')).toHaveLength(0)
})
it('commits empty array to userSelectedInMassCreation', () => {
expect(storeCommitMock).toBeCalledWith('setUserSelectedInMassCreation', [])
})
it('calls searchUsers', () => {
expect(apolloQueryMock).toBeCalled()
})
})
})
describe('store has items in userSelectedInMassCreation', () => {
beforeEach(() => {
mocks.$store.state.userSelectedInMassCreation = [
{
userId: 2,
firstName: 'Benjamin',
lastName: 'Blümchen',
email: 'benjamin@bluemchen.de',
creation: [800, 600, 400],
showDetails: false,
emailChecked: true,
},
]
wrapper = Wrapper()
})
it('has one item in left table', () => {
expect(wrapper.findAll('table').at(0).findAll('tbody > tr')).toHaveLength(1)
})
it('has one item in right table', () => {
expect(wrapper.findAll('table').at(1).findAll('tbody > tr')).toHaveLength(1)
})
it('has the stored user in second row', () => {
expect(wrapper.findAll('table').at(1).findAll('tbody > tr').at(0).text()).toContain(
'benjamin@bluemchen.de',
)
})
})
describe('failed creations', () => {
beforeEach(async () => {
await wrapper
.findComponent({ name: 'CreationFormular' })
.vm.$emit('toast-failed-creations', ['bibi@bloxberg.de', 'benjamin@bluemchen.de'])
})
it('toasts two error messages', () => {
expect(toastErrorSpy).toBeCalledWith([
'creation_form.creation_failed',
{ email: 'bibi@bloxberg.de' },
])
expect(toastErrorSpy).toBeCalledWith([
'creation_form.creation_failed',
{ email: 'benjamin@bluemchen.de' },
])
})
})
describe('update item', () => {
describe('watchers', () => {
beforeEach(() => {
jest.clearAllMocks()
})
describe('push', () => {
beforeEach(() => {
wrapper.findComponent({ name: 'UserTable' }).vm.$emit(
'update-item',
{
id: 2,
firstName: 'Benjamin',
lastName: 'Blümchen',
email: 'benjamin@bluemchen.de',
creation: [800, 600, 400],
showDetails: false,
},
'push',
describe('search criteria', () => {
beforeEach(async () => {
await wrapper.setData({ criteria: 'XX' })
})
it('calls API when criteria changes', async () => {
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
searchText: 'XX',
currentPage: 1,
pageSize: 25,
},
}),
)
})
it('removes the pushed item from itemsList', () => {
expect(wrapper.vm.itemsList).toEqual([
{
id: 1,
firstName: 'Bibi',
lastName: 'Bloxberg',
email: 'bibi@bloxberg.de',
creation: [200, 400, 600],
showDetails: false,
},
])
})
it('adds the pushed item to itemsMassCreation', () => {
expect(wrapper.vm.itemsMassCreation).toEqual([
{
id: 2,
firstName: 'Benjamin',
lastName: 'Blümchen',
email: 'benjamin@bluemchen.de',
creation: [800, 600, 400],
showDetails: false,
},
])
})
describe('remove', () => {
beforeEach(() => {
wrapper.findComponent({ name: 'UserTable' }).vm.$emit(
'update-item',
{
id: 2,
firstName: 'Benjamin',
lastName: 'Blümchen',
email: 'benjamin@bluemchen.de',
creation: [800, 600, 400],
showDetails: false,
},
'remove',
describe('reset search criteria', () => {
it('calls the API', async () => {
jest.clearAllMocks()
await wrapper.find('.test-click-clear-criteria').trigger('click')
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
searchText: '',
currentPage: 1,
pageSize: 25,
},
}),
)
})
it('removes the item from itemsMassCreation', () => {
expect(wrapper.vm.itemsMassCreation).toEqual([])
})
it('adds the item to itemsList', () => {
expect(wrapper.vm.itemsList).toEqual([
{
id: 1,
firstName: 'Bibi',
lastName: 'Bloxberg',
email: 'bibi@bloxberg.de',
creation: [200, 400, 600],
showDetails: false,
},
{
id: 2,
firstName: 'Benjamin',
lastName: 'Blümchen',
email: 'benjamin@bluemchen.de',
creation: [800, 600, 400],
showDetails: false,
},
])
})
})
})
describe('error', () => {
const consoleErrorMock = jest.fn()
const warnHandler = Vue.config.warnHandler
beforeEach(() => {
Vue.config.warnHandler = (w) => {}
// eslint-disable-next-line no-console
console.error = consoleErrorMock
wrapper.findComponent({ name: 'UserTable' }).vm.$emit('update-item', {}, 'no-rule')
})
afterEach(() => {
Vue.config.warnHandler = warnHandler
})
it('throws an error', () => {
expect(consoleErrorMock).toBeCalledWith(expect.objectContaining({ message: 'no-rule' }))
})
})
})
describe('remove all bookmarks', () => {
beforeEach(async () => {
await wrapper.findComponent({ name: 'UserTable' }).vm.$emit(
'update-item',
{
id: 2,
firstName: 'Benjamin',
lastName: 'Blümchen',
email: 'benjamin@bluemchen.de',
creation: [800, 600, 400],
showDetails: false,
},
'push',
it('calls API when currentPage changes', async () => {
await wrapper.setData({ currentPage: 2 })
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
searchText: '',
currentPage: 2,
pageSize: 25,
},
}),
)
wrapper.findComponent({ name: 'CreationFormular' }).vm.$emit('remove-all-bookmark')
})
it('removes all items from itemsMassCreation', () => {
expect(wrapper.vm.itemsMassCreation).toEqual([])
})
it('adds all items to itemsList', () => {
expect(wrapper.vm.itemsList).toHaveLength(2)
})
})
@ -226,7 +314,7 @@ describe('Creation', () => {
})
it('toasts an error message', () => {
expect(toastErrorMock).toBeCalledWith('Ouch')
expect(toastErrorSpy).toBeCalledWith('Ouch')
})
})
})

View File

@ -1,35 +1,52 @@
<template>
<div class="creation">
<b-row>
<b-col cols="12" lg="5">
<label>Usersuche</label>
<b-input
type="text"
v-model="criteria"
class="shadow p-3 mb-5 bg-white rounded"
placeholder="User suche"
></b-input>
<user-table
<b-col cols="12" lg="6">
<label>{{ $t('user_search') }}</label>
<b-input-group>
<b-form-input
type="text"
class="test-input-criteria"
v-model="criteria"
:placeholder="$t('user_search')"
></b-form-input>
<b-input-group-append class="test-click-clear-criteria" @click="criteria = ''">
<b-input-group-text class="pointer">
<b-icon icon="x" />
</b-input-group-text>
</b-input-group-append>
</b-input-group>
<select-users-table
v-if="itemsList.length > 0"
type="UserListSearch"
:itemsUser="itemsList"
:fieldsTable="Searchfields"
:criteria="criteria"
:creation="creation"
@update-item="updateItem"
:items="itemsList"
:fields="Searchfields"
@push-item="pushItem"
/>
<b-pagination
pills
v-model="currentPage"
per-page="perPage"
:total-rows="rows"
align="center"
></b-pagination>
</b-col>
<b-col cols="12" lg="7" class="shadow p-3 mb-5 rounded bg-info">
<user-table
v-show="itemsMassCreation.length > 0"
class="shadow p-3 mb-5 bg-white rounded"
type="UserListMassCreation"
:itemsUser="itemsMassCreation"
:fieldsTable="fields"
:criteria="null"
:creation="creation"
@update-item="updateItem"
/>
<b-col cols="12" lg="6" class="shadow p-3 mb-5 rounded bg-info">
<div v-show="itemsMassCreation.length > 0">
<div class="text-right pr-4 mb-1">
<b-button @click="removeAllBookmarks()" variant="light">
<b-icon icon="x" scale="2" variant="danger"></b-icon>
{{ $t('remove_all') }}
</b-button>
</div>
<selected-users-table
class="shadow p-3 mb-5 bg-white rounded"
:items="itemsMassCreation"
:fields="fields"
@remove-item="removeItem"
/>
</div>
<div v-if="itemsMassCreation.length === 0">
{{ $t('multiple_creation_text') }}
</div>
@ -38,7 +55,8 @@
type="massCreation"
:creation="creation"
:items="itemsMassCreation"
@remove-all-bookmark="removeAllBookmark"
@remove-all-bookmark="removeAllBookmarks"
@toast-failed-creations="toastFailedCreations"
/>
</b-col>
</b-row>
@ -46,37 +64,30 @@
</template>
<script>
import CreationFormular from '../components/CreationFormular.vue'
import UserTable from '../components/UserTable.vue'
import SelectUsersTable from '../components/Tables/SelectUsersTable.vue'
import SelectedUsersTable from '../components/Tables/SelectedUsersTable.vue'
import { searchUsers } from '../graphql/searchUsers'
import { creationMonths } from '../mixins/creationMonths'
export default {
name: 'Creation',
mixins: [creationMonths],
components: {
CreationFormular,
UserTable,
SelectUsersTable,
SelectedUsersTable,
},
data() {
return {
showArrays: false,
Searchfields: [
{ key: 'bookmark', label: 'bookmark' },
{ key: 'firstName', label: this.$t('firstname') },
{ key: 'lastName', label: this.$t('lastname') },
{ key: 'creation', label: this.$t('open_creations') },
{ key: 'email', label: this.$t('e_mail') },
],
fields: [
{ key: 'email', label: this.$t('e_mail') },
{ key: 'firstName', label: this.$t('firstname') },
{ key: 'lastName', label: this.$t('lastname') },
{ key: 'creation', label: this.$t('open_creations') },
{ key: 'bookmark', label: this.$t('remove') },
],
itemsList: [],
itemsMassCreation: [],
itemsMassCreation: this.$store.state.userSelectedInMassCreation,
radioSelectedMass: '',
criteria: '',
creation: [null, null, null],
rows: 0,
currentPage: 1,
perPage: 25,
now: Date.now(),
}
},
async created() {
@ -89,44 +100,95 @@ export default {
query: searchUsers,
variables: {
searchText: this.criteria,
currentPage: this.currentPage,
pageSize: this.perPage,
},
fetchPolicy: 'network-only',
})
.then((result) => {
this.itemsList = result.data.searchUsers.map((user) => {
this.rows = result.data.searchUsers.userCount
this.itemsList = result.data.searchUsers.userList.map((user) => {
return {
...user,
showDetails: false,
}
})
if (this.itemsMassCreation.length !== 0) {
const selectedIndices = this.itemsMassCreation.map((item) => item.userId)
this.itemsList = this.itemsList.filter((item) => !selectedIndices.includes(item.userId))
}
})
.catch((error) => {
this.$toasted.error(error.message)
this.toastError(error.message)
})
},
updateItem(e, event) {
let index = 0
let findArr = {}
switch (event) {
case 'push':
findArr = this.itemsList.find((arr) => arr.id === e.id)
index = this.itemsList.indexOf(findArr)
this.itemsList.splice(index, 1)
this.itemsMassCreation.push(e)
break
case 'remove':
findArr = this.itemsMassCreation.find((arr) => arr.id === e.id)
index = this.itemsMassCreation.indexOf(findArr)
this.itemsMassCreation.splice(index, 1)
this.itemsList.push(e)
break
default:
throw new Error(event)
}
pushItem(selectedItem) {
this.itemsMassCreation = [
this.itemsList.find((item) => selectedItem.userId === item.userId),
...this.itemsMassCreation,
]
this.itemsList = this.itemsList.filter((item) => selectedItem.userId !== item.userId)
this.$store.commit('setUserSelectedInMassCreation', this.itemsMassCreation)
},
removeAllBookmark() {
this.itemsMassCreation.forEach((item) => this.itemsList.push(item))
removeItem(selectedItem) {
this.itemsList = [
this.itemsMassCreation.find((item) => selectedItem.userId === item.userId),
...this.itemsList,
]
this.itemsMassCreation = this.itemsMassCreation.filter(
(item) => selectedItem.userId !== item.userId,
)
this.$store.commit('setUserSelectedInMassCreation', this.itemsMassCreation)
},
removeAllBookmarks() {
this.itemsMassCreation = []
this.$store.commit('setUserSelectedInMassCreation', [])
this.getUsers()
},
toastFailedCreations(failedCreations) {
failedCreations.forEach((email) =>
this.toastError(this.$t('creation_form.creation_failed', { email })),
)
},
},
computed: {
Searchfields() {
return [
{ key: 'bookmark', label: 'bookmark' },
{ key: 'firstName', label: this.$t('firstname') },
{ key: 'lastName', label: this.$t('lastname') },
{
key: 'creation',
label: this.creationLabel,
formatter: (value, key, item) => {
return value.join(' | ')
},
},
{ key: 'email', label: this.$t('e_mail') },
]
},
fields() {
return [
{ key: 'email', label: this.$t('e_mail') },
{ key: 'firstName', label: this.$t('firstname') },
{ key: 'lastName', label: this.$t('lastname') },
{
key: 'creation',
label: this.creationLabel,
formatter: (value, key, item) => {
return value.join(' | ')
},
},
{ key: 'bookmark', label: this.$t('remove') },
]
},
},
watch: {
currentPage() {
this.getUsers()
},
criteria() {
this.getUsers()
},
},
}

View File

@ -1,12 +1,12 @@
import { mount } from '@vue/test-utils'
import CreationConfirm from './CreationConfirm.vue'
import { deletePendingCreation } from '../graphql/deletePendingCreation'
import { confirmPendingCreation } from '../graphql/confirmPendingCreation'
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
const localVue = global.localVue
const storeCommitMock = jest.fn()
const toastedErrorMock = jest.fn()
const toastedSuccessMock = jest.fn()
const apolloQueryMock = jest.fn().mockResolvedValue({
data: {
getPendingCreations: [
@ -26,7 +26,7 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
lastName: 'Hotzenplotz',
email: 'raeuber@hotzenplotz.de',
amount: 1000000,
memo: 'Gut Ergatert',
memo: 'Gut Ergattert',
date: new Date(),
moderator: 0,
},
@ -38,6 +38,7 @@ const apolloMutateMock = jest.fn().mockResolvedValue({})
const mocks = {
$t: jest.fn((t) => t),
$d: jest.fn((d) => d),
$store: {
commit: storeCommitMock,
},
@ -45,15 +46,6 @@ const mocks = {
query: apolloQueryMock,
mutate: apolloMutateMock,
},
$toasted: {
error: toastedErrorMock,
success: toastedSuccessMock,
},
$moment: jest.fn((value) => {
return {
format: jest.fn((format) => value),
}
}),
}
describe('CreationConfirm', () => {
@ -73,46 +65,23 @@ describe('CreationConfirm', () => {
expect(wrapper.find('div.creation-confirm').exists()).toBeTruthy()
})
it('has two pending creations', () => {
expect(wrapper.vm.pendingCreations).toHaveLength(2)
})
describe('store', () => {
it('commits resetOpenCreations to store', () => {
expect(storeCommitMock).toBeCalledWith('resetOpenCreations')
})
it('commits setOpenCreations to store', () => {
expect(storeCommitMock).toBeCalledWith('setOpenCreations', 2)
})
})
describe('delete creation delete with success', () => {
describe('remove creation with success', () => {
beforeEach(async () => {
apolloQueryMock.mockResolvedValue({
data: {
getPendingCreations: [
{
id: 1,
firstName: 'Bibi',
lastName: 'Bloxberg',
email: 'bibi@bloxberg.de',
amount: 500,
memo: 'Danke für alles',
date: new Date(),
moderator: 0,
},
{
id: 2,
firstName: 'Räuber',
lastName: 'Hotzenplotz',
email: 'raeuber@hotzenplotz.de',
amount: 1000000,
memo: 'Gut Ergatert',
date: new Date(),
moderator: 0,
},
],
},
})
await wrapper
.findComponent({ name: 'UserTable' })
.vm.$emit('remove-confirm-result', { id: 1 }, 'remove')
await wrapper.findAll('tr').at(1).findAll('button').at(0).trigger('click')
})
it('calls the deletePendingCreation mutation', () => {
@ -127,85 +96,85 @@ describe('CreationConfirm', () => {
})
it('toasts a success message', () => {
expect(toastedSuccessMock).toBeCalledWith('Pending Creation has been deleted')
expect(toastSuccessSpy).toBeCalledWith('creation_form.toasted_delete')
})
})
describe('delete creation delete with error', () => {
describe('remove creation with error', () => {
beforeEach(async () => {
apolloMutateMock.mockRejectedValue({ message: 'Ouchhh!' })
await wrapper
.findComponent({ name: 'UserTable' })
.vm.$emit('remove-confirm-result', { id: 1 }, 'remove')
await wrapper.findAll('tr').at(1).findAll('button').at(0).trigger('click')
})
it('toasts an error message', () => {
expect(toastedErrorMock).toBeCalledWith('Ouchhh!')
expect(toastErrorSpy).toBeCalledWith('Ouchhh!')
})
})
describe('confirm creation delete with success', () => {
describe('confirm creation with success', () => {
beforeEach(async () => {
apolloQueryMock.mockResolvedValue({
data: {
getPendingCreations: [
{
id: 1,
firstName: 'Bibi',
lastName: 'Bloxberg',
email: 'bibi@bloxberg.de',
amount: 500,
memo: 'Danke für alles',
date: new Date(),
moderator: 0,
},
{
id: 2,
firstName: 'Räuber',
lastName: 'Hotzenplotz',
email: 'raeuber@hotzenplotz.de',
amount: 1000000,
memo: 'Gut Ergatert',
date: new Date(),
moderator: 0,
},
],
},
})
await wrapper
.findComponent({ name: 'UserTable' })
.vm.$emit('remove-confirm-result', { id: 1 }, 'confirmed')
apolloMutateMock.mockResolvedValue({})
await wrapper.findAll('tr').at(2).findAll('button').at(2).trigger('click')
})
it('calls the deletePendingCreation mutation', () => {
expect(apolloMutateMock).not.toBeCalledWith({
mutation: deletePendingCreation,
variables: { id: 1 },
describe('overlay', () => {
it('opens the overlay', () => {
expect(wrapper.find('#overlay').isVisible()).toBeTruthy()
})
describe('cancel confirmation', () => {
beforeEach(async () => {
await wrapper.find('#overlay').findAll('button').at(0).trigger('click')
})
it('closes the overlay', async () => {
expect(wrapper.find('#overlay').exists()).toBeFalsy()
})
it('still has 2 items in the table', () => {
expect(wrapper.findAll('tbody > tr')).toHaveLength(2)
})
})
describe('confirm creation', () => {
beforeEach(async () => {
await wrapper.find('#overlay').findAll('button').at(1).trigger('click')
})
it('calls the confirmPendingCreation mutation', () => {
expect(apolloMutateMock).toBeCalledWith({
mutation: confirmPendingCreation,
variables: { id: 2 },
})
})
it('commits openCreationsMinus to store', () => {
expect(storeCommitMock).toBeCalledWith('openCreationsMinus', 1)
})
it('toasts a success message', () => {
expect(toastSuccessSpy).toBeCalledWith('creation_form.toasted_created')
})
it('has 1 item left in the table', () => {
expect(wrapper.findAll('tbody > tr')).toHaveLength(1)
})
})
describe('confirm creation with error', () => {
beforeEach(async () => {
apolloMutateMock.mockRejectedValue({ message: 'Ouchhh!' })
await wrapper.find('#overlay').findAll('button').at(1).trigger('click')
})
it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Ouchhh!')
})
})
})
it('commits openCreationsMinus to store', () => {
expect(storeCommitMock).toBeCalledWith('openCreationsMinus', 1)
})
it('toasts a success message', () => {
expect(toastedSuccessMock).toBeCalledWith('Pending Creation has been deleted')
})
})
describe('delete creation delete with error', () => {
beforeEach(async () => {
await wrapper
.findComponent({ name: 'UserTable' })
.vm.$emit('remove-confirm-result', { id: 1 }, 'confirm')
})
it('toasts an error message', () => {
expect(toastedErrorMock).toBeCalledWith('Case confirm is not supported')
})
})
describe('server response is error', () => {
describe('server response for get pending creations is error', () => {
beforeEach(() => {
jest.clearAllMocks()
apolloQueryMock.mockRejectedValue({
@ -215,7 +184,7 @@ describe('CreationConfirm', () => {
})
it('toast an error message', () => {
expect(toastedErrorMock).toBeCalledWith('Ouch!')
expect(toastErrorSpy).toBeCalledWith('Ouch!')
})
})
})

View File

@ -1,85 +1,71 @@
<template>
<div class="creation-confirm">
<user-table
<div v-if="overlay" id="overlay" @dblclick="overlay = false">
<overlay :item="item" @overlay-cancel="overlay = false" @confirm-creation="confirmCreation" />
</div>
<open-creations-table
class="mt-4"
type="PageCreationConfirm"
:itemsUser="confirmResult"
:fieldsTable="fields"
@remove-confirm-result="removeConfirmResult"
:items="pendingCreations"
:fields="fields"
@remove-creation="removeCreation"
@show-overlay="showOverlay"
/>
</div>
</template>
<script>
import UserTable from '../components/UserTable.vue'
import Overlay from '../components/Overlay.vue'
import OpenCreationsTable from '../components/Tables/OpenCreationsTable.vue'
import { getPendingCreations } from '../graphql/getPendingCreations'
import { deletePendingCreation } from '../graphql/deletePendingCreation'
import { confirmPendingCreation } from '../graphql/confirmPendingCreation'
export default {
name: 'CreationConfirm',
components: {
UserTable,
OpenCreationsTable,
Overlay,
},
data() {
return {
showArrays: false,
fields: [
{ key: 'bookmark', label: 'löschen' },
{ key: 'email', label: 'Email' },
{ key: 'firstName', label: 'Vorname' },
{ key: 'lastName', label: 'Nachname' },
{
key: 'amount',
label: 'Schöpfung',
formatter: (value) => {
return value + ' GDD'
},
},
{ key: 'memo', label: 'Text' },
{
key: 'date',
label: 'Datum',
formatter: (value) => {
return this.$moment(value).format('ll')
},
},
{ key: 'moderator', label: 'Moderator' },
{ key: 'edit_creation', label: 'ändern' },
{ key: 'confirm', label: 'speichern' },
],
confirmResult: [],
pendingCreations: [],
overlay: false,
item: {},
}
},
methods: {
removeConfirmResult(e, event) {
let index = 0
const findArr = this.confirmResult.find((arr) => arr.id === e.id)
switch (event) {
case 'remove':
this.$apollo
.mutate({
mutation: deletePendingCreation,
variables: {
id: findArr.id,
},
})
.then((result) => {
index = this.confirmResult.indexOf(findArr)
this.confirmResult.splice(index, 1)
this.$store.commit('openCreationsMinus', 1)
this.$toasted.success('Pending Creation has been deleted')
})
.catch((error) => {
this.$toasted.error(error.message)
})
break
case 'confirmed':
this.confirmResult.splice(index, 1)
this.$store.commit('openCreationsMinus', 1)
this.$toasted.success('Pending Creation has been deleted')
break
default:
this.$toasted.error('Case ' + event + ' is not supported')
}
removeCreation(item) {
this.$apollo
.mutate({
mutation: deletePendingCreation,
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
.mutate({
mutation: confirmPendingCreation,
variables: {
id: this.item.id,
},
})
.then((result) => {
this.overlay = false
this.updatePendingCreations(this.item.id)
this.toastSuccess(this.$t('creation_form.toasted_created'))
})
.catch((error) => {
this.overlay = false
this.toastError(error.message)
})
},
getPendingCreations() {
this.$apollo
@ -89,16 +75,69 @@ export default {
})
.then((result) => {
this.$store.commit('resetOpenCreations')
this.confirmResult = result.data.getPendingCreations
this.pendingCreations = result.data.getPendingCreations
this.$store.commit('setOpenCreations', result.data.getPendingCreations.length)
})
.catch((error) => {
this.$toasted.error(error.message)
this.toastError(error.message)
})
},
updatePendingCreations(id) {
this.pendingCreations = this.pendingCreations.filter((obj) => obj.id !== id)
this.$store.commit('openCreationsMinus', 1)
},
showOverlay(item) {
this.overlay = true
this.item = item
},
},
computed: {
fields() {
return [
{ key: 'bookmark', label: this.$t('delete') },
{ key: 'email', label: this.$t('e_mail') },
{ key: 'firstName', label: this.$t('firstname') },
{ key: 'lastName', label: this.$t('lastname') },
{
key: 'amount',
label: this.$t('creation'),
formatter: (value) => {
return value + ' GDD'
},
},
{ key: 'memo', label: this.$t('text') },
{
key: 'date',
label: this.$t('date'),
formatter: (value) => {
return this.$d(new Date(value), 'short')
},
},
{ key: 'moderator', label: this.$t('moderator') },
{ key: 'edit_creation', label: this.$t('edit') },
{ key: 'confirm', label: this.$t('save') },
]
},
},
async created() {
await this.getPendingCreations()
},
}
</script>
<style>
#overlay {
position: fixed;
display: flex;
align-items: center;
width: 100%;
height: 100%;
top: 0;
left: 0;
right: 0;
bottom: 0;
padding-left: 5%;
background-color: rgba(12, 11, 11, 0.781);
z-index: 1000000;
cursor: pointer;
}
</style>

View File

@ -1,42 +1,61 @@
import { mount } from '@vue/test-utils'
import UserSearch from './UserSearch.vue'
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
const localVue = global.localVue
const apolloQueryMock = jest.fn().mockResolvedValue({
data: {
searchUsers: [
{
firstName: 'Bibi',
lastName: 'Bloxberg',
email: 'bibi@bloxberg.de',
creation: [200, 400, 600],
emailChecked: false,
},
],
searchUsers: {
userCount: 1,
userList: [
{
userId: 1,
firstName: 'Bibi',
lastName: 'Bloxberg',
email: 'bibi@bloxberg.de',
creation: [200, 400, 600],
emailChecked: true,
deletedAt: null,
},
{
userId: 2,
firstName: 'Benjamin',
lastName: 'Blümchen',
email: 'benjamin@bluemchen.de',
creation: [1000, 1000, 1000],
emailChecked: true,
deletedAt: null,
},
{
userId: 3,
firstName: 'Peter',
lastName: 'Lustig',
email: 'peter@lustig.de',
creation: [0, 0, 0],
emailChecked: true,
deletedAt: null,
},
{
userId: 4,
firstName: 'New',
lastName: 'User',
email: 'new@user.ch',
creation: [1000, 1000, 1000],
emailChecked: false,
deletedAt: null,
},
],
},
},
})
const toastErrorMock = jest.fn()
const mocks = {
$t: jest.fn((t) => t),
$d: jest.fn((d) => String(d)),
$apollo: {
query: apolloQueryMock,
},
$toasted: {
error: toastErrorMock,
},
$moment: jest.fn(() => {
return {
format: jest.fn((m) => m),
subtract: jest.fn(() => {
return {
format: jest.fn((m) => m),
}
}),
}
}),
}
describe('UserSearch', () => {
@ -48,6 +67,7 @@ describe('UserSearch', () => {
describe('mount', () => {
beforeEach(() => {
jest.clearAllMocks()
wrapper = Wrapper()
})
@ -55,13 +75,130 @@ describe('UserSearch', () => {
expect(wrapper.find('div.user-search').exists()).toBeTruthy()
})
it('calls the API', () => {
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
searchText: '',
currentPage: 1,
pageSize: 25,
notActivated: false,
isDeleted: false,
},
}),
)
})
describe('unconfirmed emails', () => {
beforeEach(async () => {
await wrapper.find('button.btn-block').trigger('click')
await wrapper.find('button.unconfirmedRegisterMails').trigger('click')
})
it('filters the users by unconfirmed emails', () => {
expect(wrapper.vm.searchResult).toHaveLength(0)
it('calls API with filter', () => {
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
searchText: '',
currentPage: 1,
pageSize: 25,
notActivated: true,
isDeleted: false,
},
}),
)
})
})
describe('deleted Users', () => {
beforeEach(async () => {
await wrapper.find('button.deletedUserSearch').trigger('click')
})
it('calls API with filter', () => {
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
searchText: '',
currentPage: 1,
pageSize: 25,
notActivated: false,
isDeleted: true,
},
}),
)
})
})
describe('pagination', () => {
beforeEach(async () => {
wrapper.setData({ currentPage: 2 })
})
it('calls the API with new page', () => {
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
searchText: '',
currentPage: 2,
pageSize: 25,
notActivated: false,
isDeleted: false,
},
}),
)
})
})
describe('user search', () => {
beforeEach(async () => {
wrapper.setData({ criteria: 'search string' })
})
it('calls the API with search string', () => {
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
searchText: 'search string',
currentPage: 1,
pageSize: 25,
notActivated: false,
isDeleted: false,
},
}),
)
})
describe('reset the search field', () => {
it('calls the API with empty criteria', async () => {
jest.clearAllMocks()
await wrapper.find('.test-click-clear-criteria').trigger('click')
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
searchText: '',
currentPage: 1,
pageSize: 25,
notActivated: false,
isDeleted: false,
},
}),
)
})
})
})
describe('delete user', () => {
const now = new Date()
beforeEach(async () => {
wrapper.findComponent({ name: 'SearchUserTable' }).vm.$emit('updateDeletedAt', 4, now)
})
it('marks the user as deleted', () => {
expect(wrapper.vm.searchResult.find((obj) => obj.userId === 4).deletedAt).toEqual(now)
})
it('toasts a success message', () => {
expect(toastSuccessSpy).toBeCalledWith('user_deleted')
})
})
@ -74,7 +211,7 @@ describe('UserSearch', () => {
})
it('toasts an error message', () => {
expect(toastErrorMock).toBeCalledWith('Ouch')
expect(toastErrorSpy).toBeCalledWith('Ouch')
})
})
})

View File

@ -1,94 +1,81 @@
<template>
<div class="user-search">
<div style="text-align: right">
<b-button block variant="danger" @click="unconfirmedRegisterMails">
<b-icon icon="envelope" variant="light"></b-icon>
{{ $t('unregistered_emails') }}
<div class="user-search-first-div">
<b-button class="unconfirmedRegisterMails" variant="light" @click="unconfirmedRegisterMails">
<b-icon icon="envelope" variant="danger"></b-icon>
{{ filterCheckedEmails ? $t('all_emails') : $t('unregistered_emails') }}
</b-button>
<b-button class="deletedUserSearch" variant="light" @click="deletedUserSearch">
<b-icon icon="x-circle" variant="danger"></b-icon>
{{ filterDeletedUser ? $t('all_emails') : $t('deleted_user') }}
</b-button>
</div>
<label>{{ $t('user_search') }}</label>
<b-input
type="text"
v-model="criteria"
class="shadow p-3 mb-3 bg-white rounded"
:placeholder="$t('user_search')"
@input="getUsers"
></b-input>
<user-table
<div>
<b-input-group>
<b-form-input
type="text"
class="test-input-criteria"
v-model="criteria"
:placeholder="$t('user_search')"
></b-form-input>
<b-input-group-append class="test-click-clear-criteria" @click="criteria = ''">
<b-input-group-text class="pointer">
<b-icon icon="x" />
</b-input-group-text>
</b-input-group-append>
</b-input-group>
</div>
<search-user-table
type="PageUserSearch"
:itemsUser="searchResult"
:fieldsTable="fields"
:criteria="criteria"
:items="searchResult"
:fields="fields"
@updateDeletedAt="updateDeletedAt"
/>
<b-pagination
pills
size="lg"
v-model="currentPage"
per-page="perPage"
:total-rows="rows"
align="center"
></b-pagination>
<div></div>
</div>
</template>
<script>
import UserTable from '../components/UserTable.vue'
import SearchUserTable from '../components/Tables/SearchUserTable.vue'
import { searchUsers } from '../graphql/searchUsers'
import { creationMonths } from '../mixins/creationMonths'
export default {
name: 'UserSearch',
mixins: [creationMonths],
components: {
UserTable,
SearchUserTable,
},
data() {
return {
showArrays: false,
fields: [
{ key: 'email', label: this.$t('e_mail') },
{ key: 'firstName', label: this.$t('firstname') },
{ key: 'lastName', label: this.$t('lastname') },
{
key: 'creation',
label: this.$t('open_creation'),
formatter: (value, key, item) => {
return (
`
<div>` +
this.$moment().subtract(2, 'month').format('MMMM') +
` - ` +
String(value[0]) +
` GDD</div>
<div>` +
this.$moment().subtract(1, 'month').format('MMMM') +
` - ` +
String(value[1]) +
` GDD</div>
<div>` +
this.$moment().format('MMMM') +
` - ` +
String(value[2]) +
` GDD</div>
`
)
},
},
{ key: 'show_details', label: this.$t('details') },
{ key: 'confirm_mail', label: this.$t('confirmed') },
{ key: 'transactions_list', label: this.$t('transaction') },
],
searchResult: [],
massCreation: [],
criteria: '',
currentMonth: {
short: this.$moment().format('MMMM'),
},
lastMonth: {
short: this.$moment().subtract(1, 'month').format('MMMM'),
},
beforeLastMonth: {
short: this.$moment().subtract(2, 'month').format('MMMM'),
},
filterCheckedEmails: false,
filterDeletedUser: false,
rows: 0,
currentPage: 1,
perPage: 25,
now: Date.now(),
}
},
methods: {
unconfirmedRegisterMails() {
this.searchResult = this.searchResult.filter((user) => {
return user.emailChecked
})
this.filterCheckedEmails = !this.filterCheckedEmails
this.getUsers()
},
deletedUserSearch() {
this.filterDeletedUser = !this.filterDeletedUser
this.getUsers()
},
getUsers() {
this.$apollo
@ -96,18 +83,61 @@ export default {
query: searchUsers,
variables: {
searchText: this.criteria,
currentPage: this.currentPage,
pageSize: this.perPage,
notActivated: this.filterCheckedEmails,
isDeleted: this.filterDeletedUser,
},
})
.then((result) => {
this.searchResult = result.data.searchUsers
this.rows = result.data.searchUsers.userCount
this.searchResult = result.data.searchUsers.userList
})
.catch((error) => {
this.$toasted.error(error.message)
this.toastError(error.message)
})
},
updateDeletedAt(userId, deletedAt) {
this.searchResult.find((obj) => obj.userId === userId).deletedAt = deletedAt
this.toastSuccess(deletedAt ? this.$t('user_deleted') : this.$t('user_recovered'))
},
},
watch: {
currentPage() {
this.getUsers()
},
criteria() {
this.getUsers()
},
},
computed: {
fields() {
return [
{ key: 'email', label: this.$t('e_mail') },
{ key: 'firstName', label: this.$t('firstname') },
{ key: 'lastName', label: this.$t('lastname') },
{
key: 'creation',
label: this.creationLabel,
formatter: (value, key, item) => {
return value.join(' | ')
},
},
// { key: 'show_details', label: this.$t('details') },
// { key: 'confirm_mail', label: this.$t('confirmed') },
// { key: 'has_elopage', label: 'elopage' },
// { key: 'transactions_list', label: this.$t('transaction') },
{ key: 'status', label: this.$t('status') },
]
},
},
created() {
this.getUsers()
},
}
</script>
<style>
.user-search-first-div {
text-align: right;
}
</style>

View File

@ -2,8 +2,6 @@ import { ApolloClient, ApolloLink, InMemoryCache, HttpLink } from 'apollo-boost'
import VueApollo from 'vue-apollo'
import CONFIG from '../config'
import store from '../store/store'
import router from '../router/router'
import i18n from '../i18n'
const httpLink = new HttpLink({ uri: CONFIG.GRAPHQL_URI })
@ -16,9 +14,8 @@ const authLink = new ApolloLink((operation, forward) => {
})
return forward(operation).map((response) => {
if (response.errors && response.errors[0].message === '403.13 - Client certificate revoked') {
response.errors[0].message = i18n.t('error.session-expired')
store.dispatch('logout', null)
if (router.currentRoute.path !== '/logout') router.push('/logout')
window.location.assign(CONFIG.WALLET_URL)
return response
}
const newToken = operation.getContext().response.headers.get('token')

View File

@ -4,12 +4,10 @@ import CONFIG from '../config'
import VueApollo from 'vue-apollo'
import store from '../store/store'
import router from '../router/router'
import i18n from '../i18n'
jest.mock('vue-apollo')
jest.mock('../store/store')
jest.mock('../router/router')
jest.mock('../i18n')
jest.mock('apollo-boost', () => {
@ -59,13 +57,11 @@ describe('apolloProvider', () => {
errors: [{ message: '403.13 - Client certificate revoked' }],
}
// mock router
const routerPushMock = jest.fn()
router.push = routerPushMock
router.currentRoute = {
path: '/overview',
const windowLocationMock = jest.fn()
delete window.location
window.location = {
assign: windowLocationMock,
}
// mock context
const setContextMock = jest.fn()
const getContextMock = jest.fn(() => {
@ -128,21 +124,8 @@ describe('apolloProvider', () => {
expect(storeDispatchMock).toBeCalledWith('logout', null)
})
describe('current route is not logout', () => {
it('redirects to logout', () => {
expect(routerPushMock).toBeCalledWith('/logout')
})
})
describe('current route is logout', () => {
beforeEach(() => {
jest.clearAllMocks()
router.currentRoute.path = '/logout'
})
it('does not redirect to logout', () => {
expect(routerPushMock).not.toBeCalled()
})
it('redirects to logout', () => {
expect(windowLocationMock).toBeCalledWith('http://localhost/login')
})
})

View File

@ -1,7 +1,7 @@
import { verifyLogin } from '../graphql/verifyLogin'
import CONFIG from '../config'
const addNavigationGuards = (router, store, apollo) => {
const addNavigationGuards = (router, store, apollo, i18n) => {
// store token on `authenticate`
router.beforeEach(async (to, from, next) => {
if (to.path === '/authenticate' && to.query && to.query.token) {
@ -14,6 +14,7 @@ const addNavigationGuards = (router, store, apollo) => {
.then((result) => {
const moderator = result.data.verifyLogin
if (moderator.isAdmin) {
i18n.locale = moderator.language
store.commit('moderator', moderator)
next({ path: '/' })
} else {

View File

@ -6,9 +6,11 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
data: {
verifyLogin: {
isAdmin: true,
language: 'de',
},
},
})
const i18nLocaleMock = jest.fn()
const store = {
commit: storeCommitMock,
@ -21,7 +23,11 @@ const apollo = {
query: apolloQueryMock,
}
addNavigationGuards(router, store, apollo)
const i18n = {
locale: i18nLocaleMock,
}
addNavigationGuards(router, store, apollo, i18n)
describe('navigation guards', () => {
beforeEach(() => {
@ -33,19 +39,23 @@ describe('navigation guards', () => {
const next = jest.fn()
describe('with valid token and as admin', () => {
beforeEach(() => {
navGuard({ path: '/authenticate', query: { token: 'valid-token' } }, {}, next)
beforeEach(async () => {
await navGuard({ path: '/authenticate', query: { token: 'valid-token' } }, {}, next)
})
it('commits the token to the store', async () => {
it('commits the token to the store', () => {
expect(storeCommitMock).toBeCalledWith('token', 'valid-token')
})
it('commits the moderator to the store', () => {
expect(storeCommitMock).toBeCalledWith('moderator', { isAdmin: true })
it.skip('sets the locale', () => {
expect(i18nLocaleMock).toBeCalledWith('de')
})
it('redirects to /', async () => {
it('commits the moderator to the store', () => {
expect(storeCommitMock).toBeCalledWith('moderator', { isAdmin: true, language: 'de' })
})
it('redirects to /', () => {
expect(next).toBeCalledWith({ path: '/' })
})
})

View File

@ -24,6 +24,9 @@ export const mutations = {
moderator: (state, moderator) => {
state.moderator = moderator
},
setUserSelectedInMassCreation: (state, userSelectedInMassCreation) => {
state.userSelectedInMassCreation = userSelectedInMassCreation
},
}
export const actions = {
@ -44,6 +47,7 @@ const store = new Vuex.Store({
token: CONFIG.DEBUG_DISABLE_AUTH ? 'validToken' : null,
moderator: null,
openCreations: 0,
userSelectedInMassCreation: [],
},
// Syncronous mutation of the state
mutations,

View File

@ -10,6 +10,7 @@ const {
resetOpenCreations,
setOpenCreations,
moderator,
setUserSelectedInMassCreation,
} = mutations
const { logout } = actions
@ -64,6 +65,14 @@ describe('Vuex store', () => {
expect(state.openCreations).toEqual(12)
})
})
describe('setUserSelectedInMassCreation', () => {
it('sets userSelectedInMassCreation to given value', () => {
const state = { userSelectedInMassCreation: [] }
setUserSelectedInMassCreation(state, [0, 1, 2])
expect(state.userSelectedInMassCreation).toEqual([0, 1, 2])
})
})
})
describe('actions', () => {

View File

@ -5,11 +5,18 @@ import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
// without this async calls are not working
import 'regenerator-runtime'
import { toasters } from '../src/mixins/toaster'
export const toastErrorSpy = jest.spyOn(toasters.methods, 'toastError')
export const toastSuccessSpy = jest.spyOn(toasters.methods, 'toastSuccess')
global.localVue = createLocalVue()
global.localVue.use(BootstrapVue)
global.localVue.use(IconsPlugin)
global.localVue.mixin(toasters)
// throw errors for vue warnings to force the programmers to take care about warnings
Vue.config.warnHandler = (w) => {
throw new Error(w)

View File

@ -2,11 +2,12 @@ const path = require('path')
const webpack = require('webpack')
const Dotenv = require('dotenv-webpack')
const StatsPlugin = require('stats-webpack-plugin')
const CONFIG = require('./src/config')
// vue.config.js
module.exports = {
devServer: {
port: process.env.PORT || 8080,
port: CONFIG.PORT,
},
pluginOptions: {
i18n: {
@ -14,6 +15,7 @@ module.exports = {
fallbackLocale: 'de',
localeDir: 'locales',
enableInSFC: false,
enableLegacy: false,
},
},
lintOnSave: true,
@ -33,7 +35,7 @@ module.exports = {
// 'process.env.DOCKER_WORKDIR': JSON.stringify(process.env.DOCKER_WORKDIR),
// 'process.env.BUILD_DATE': JSON.stringify(process.env.BUILD_DATE),
// 'process.env.BUILD_VERSION': JSON.stringify(process.env.BUILD_VERSION),
'process.env.BUILD_COMMIT': JSON.stringify(process.env.BUILD_COMMIT),
'process.env.BUILD_COMMIT': JSON.stringify(CONFIG.BUILD_COMMIT),
// 'process.env.PORT': JSON.stringify(process.env.PORT),
}),
// generate webpack stats to allow analysis of the bundlesize
@ -45,7 +47,7 @@ module.exports = {
},
css: {
// Enable CSS source maps.
sourceMap: process.env.NODE_ENV !== 'production',
sourceMap: CONFIG.NODE_ENV !== 'production',
},
outputDir: path.resolve(__dirname, './dist'),
}

File diff suppressed because it is too large Load Diff

View File

@ -1,36 +1,48 @@
CONFIG_VERSION=v1.2022-03-18
# 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
#EMAIL=true
#EMAIL_USERNAME=
#EMAIL_SENDER=
#EMAIL_PASSWORD=
#EMAIL_SMTP_URL=
#EMAIL_SMTP_PORT=587
#RESEND_TIME=1 minute, 60 => 1hour, 1440 (60 minutes * 24 hours) => 24 hours
#RESEND_TIME=
RESEND_TIME=10
# 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
#EMAIL_LINK_VERIFICATION=http://localhost/vue/checkEmail/$1
# Community
COMMUNITY_NAME=Gradido Entwicklung
COMMUNITY_URL=http://localhost/
COMMUNITY_REGISTER_URL=http://localhost/register
COMMUNITY_DESCRIPTION=Die lokale Entwicklungsumgebung von Gradido.
#KLICKTIPP_USER=
#KLICKTIPP_PASSWORD=
#KLICKTIPP_APIKEY_DE=
#KLICKTIPP_APIKEY_EN=
#KLICKTIPP=true
COMMUNITY_NAME=
COMMUNITY_URL=
COMMUNITY_REGISTER_URL=
COMMUNITY_DESCRIPTION=
# 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/{optin}
EMAIL_CODE_VALID_TIME=10
# Webhook
WEBHOOK_ELOPAGE_SECRET=secret

48
backend/.env.template Normal file
View File

@ -0,0 +1,48 @@
CONFIG_VERSION=$BACKEND_CONFIG_VERSION
# Server
JWT_SECRET=$JWT_SECRET
JWT_EXPIRES_IN=10m
GRAPHIQL=false
GDT_API_URL=$GDT_API_URL
# Database
DB_HOST=localhost
DB_PORT=3306
DB_USER=$DB_USER
DB_PASSWORD=$DB_PASSWORD
DB_DATABASE=gradido_community
TYPEORM_LOGGING_RELATIVE_PATH=$TYPEORM_LOGGING_RELATIVE_PATH
# Klicktipp
KLICKTIPP=$KLICKTIPP
KLICKTTIPP_API_URL=https://api.klicktipp.com
KLICKTIPP_USER=$KLICKTIPP_USER
KLICKTIPP_PASSWORD=$KLICKTIPP_PASSWORD
KLICKTIPP_APIKEY_DE=$KLICKTIPP_APIKEY_DE
KLICKTIPP_APIKEY_EN=$KLICKTIPP_APIKEY_EN
# Community
COMMUNITY_NAME=$COMMUNITY_NAME
COMMUNITY_URL=$COMMUNITY_URL
COMMUNITY_REGISTER_URL=$COMMUNITY_REGISTER_URL
COMMUNITY_DESCRIPTION=$COMMUNITY_DESCRIPTION
# Login Server
LOGIN_APP_SECRET=21ffbbc616fe
LOGIN_SERVER_KEY=a51ef8ac7ef1abf162fb7a65261acd7a
# EMail
RESEND_TIME=10
EMAIL=$EMAIL
EMAIL_USERNAME=$EMAIL_USERNAME
EMAIL_SENDER=$EMAIL_SENDER
EMAIL_PASSWORD=$EMAIL_PASSWORD
EMAIL_SMTP_URL=$EMAIL_SMTP_URL
EMAIL_SMTP_PORT=587
EMAIL_LINK_VERIFICATION=$EMAIL_LINK_VERIFICATION
EMAIL_LINK_SETPASSWORD=$EMAIL_LINK_SETPASSWORD
RESEND_TIME=10
# Webhook
WEBHOOK_ELOPAGE_SECRET=$WEBHOOK_ELOPAGE_SECRET

1
backend/.gitignore vendored
View File

@ -1,5 +1,6 @@
/node_modules/
/.env
/.env.bak
/build/
package-json.lock
coverage

View File

@ -3,10 +3,20 @@ module.exports = {
verbose: true,
preset: 'ts-jest',
collectCoverage: true,
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**'],
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'],
setupFiles: ['<rootDir>/test/testSetup.ts'],
modulePathIgnorePatterns: ['<rootDir>/build/'],
moduleNameMapper: {
'@entity/(.*)': '<rootDir>/../database/build/entity/$1',
// This is hack to fix a problem with the library `ts-mysql-migrate` which does differentiate between its ts/js state
'@/(.*)': '<rootDir>/src/$1',
'@model/(.*)': '<rootDir>/src/graphql/model/$1',
'@arg/(.*)': '<rootDir>/src/graphql/arg/$1',
'@enum/(.*)': '<rootDir>/src/graphql/enum/$1',
'@repository/(.*)': '<rootDir>/src/typeorm/repository/$1',
'@test/(.*)': '<rootDir>/test/$1',
'@entity/(.*)':
process.env.NODE_ENV === 'development'
? '<rootDir>/../database/entity/$1'
: '<rootDir>/../database/build/entity/$1',
'@dbTools/(.*)':
process.env.NODE_ENV === 'development'
? '<rootDir>/../database/src/$1'

View File

@ -1,6 +1,6 @@
{
"name": "gradido-backend",
"version": "1.5.1",
"version": "1.6.6",
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
"main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/backend",
@ -10,38 +10,39 @@
"scripts": {
"build": "tsc --build",
"clean": "tsc --build --clean",
"start": "node build/index.js",
"dev": "nodemon -w src --ext ts --exec ts-node src/index.ts",
"lint": "eslint . --ext .js,.ts",
"CI_worklfow_test": "jest --runInBand --coverage ",
"test": "NODE_ENV=development jest --runInBand --coverage "
"start": "TZ=UTC TS_NODE_BASEURL=./build node -r tsconfig-paths/register build/src/index.js",
"dev": "TZ=UTC nodemon -w src --ext ts --exec ts-node -r tsconfig-paths/register src/index.ts",
"lint": "eslint --max-warnings=0 --ext .js,.ts .",
"test": "TZ=UTC NODE_ENV=development jest --runInBand --coverage --forceExit --detectOpenHandles",
"seed": "TZ=UTC ts-node -r tsconfig-paths/register src/seeds/index.ts"
},
"dependencies": {
"@types/jest": "^27.0.2",
"@types/lodash.clonedeep": "^4.5.6",
"apollo-log": "^1.1.0",
"apollo-server-express": "^2.25.2",
"apollo-server-testing": "^2.25.2",
"axios": "^0.21.1",
"body-parser": "^1.19.0",
"class-validator": "^0.13.1",
"cors": "^2.8.5",
"decimal.js-light": "^2.5.1",
"dotenv": "^10.0.0",
"express": "^4.17.1",
"graphql": "^15.5.1",
"jest": "^27.2.4",
"jsonwebtoken": "^8.5.1",
"module-alias": "^2.2.2",
"moment": "^2.29.1",
"lodash.clonedeep": "^4.5.0",
"mysql2": "^2.3.0",
"nodemailer": "^6.6.5",
"random-bigint": "^0.0.1",
"reflect-metadata": "^0.1.13",
"sodium-native": "^3.3.0",
"ts-jest": "^27.0.5",
"type-graphql": "^1.1.1",
"typeorm": "^0.2.38"
"type-graphql": "^1.1.1"
},
"devDependencies": {
"@types/express": "^4.17.12",
"@types/faker": "^5.5.9",
"@types/jsonwebtoken": "^8.5.2",
"@types/node": "^16.10.3",
"@types/nodemailer": "^6.4.4",
@ -54,13 +55,11 @@
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-promise": "^5.1.0",
"faker": "^5.5.3",
"nodemon": "^2.0.7",
"prettier": "^2.3.1",
"ts-node": "^10.0.0",
"tsconfig-paths": "^3.14.0",
"typescript": "^4.3.4"
},
"_moduleAliases": {
"@entity": "../database/build/entity",
"@dbTools": "../database/build/src"
}
}

View File

@ -1,11 +1,11 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { KlicktippConnector } from './klicktippConnector'
import CONFIG from '../config'
import CONFIG from '@/config'
const klicktippConnector = new KlicktippConnector()
export const signIn = async (
export const klicktippSignIn = async (
email: string,
language: string,
firstName?: string,

View File

@ -7,5 +7,6 @@ export const INALIENABLE_RIGHTS = [
RIGHTS.CREATE_USER,
RIGHTS.SEND_RESET_PASSWORD_EMAIL,
RIGHTS.SET_PASSWORD,
RIGHTS.CHECK_USERNAME,
RIGHTS.QUERY_TRANSACTION_LINK,
RIGHTS.QUERY_OPT_IN,
]

View File

@ -1,5 +1,5 @@
import jwt from 'jsonwebtoken'
import CONFIG from '../config/'
import CONFIG from '@/config/'
import { CustomJwtPayload } from './CustomJwtPayload'
export const decode = (token: string): CustomJwtPayload | null => {

View File

@ -16,9 +16,14 @@ export enum RIGHTS {
CREATE_USER = 'CREATE_USER',
SEND_RESET_PASSWORD_EMAIL = 'SEND_RESET_PASSWORD_EMAIL',
SET_PASSWORD = 'SET_PASSWORD',
QUERY_OPT_IN = 'QUERY_OPT_IN',
UPDATE_USER_INFOS = 'UPDATE_USER_INFOS',
CHECK_USERNAME = 'CHECK_USERNAME',
HAS_ELOPAGE = 'HAS_ELOPAGE',
CREATE_TRANSACTION_LINK = 'CREATE_TRANSACTION_LINK',
DELETE_TRANSACTION_LINK = 'DELETE_TRANSACTION_LINK',
QUERY_TRANSACTION_LINK = 'QUERY_TRANSACTION_LINK',
REDEEM_TRANSACTION_LINK = 'REDEEM_TRANSACTION_LINK',
LIST_TRANSACTION_LINKS = 'LIST_TRANSACTION_LINKS',
// Admin
SEARCH_USERS = 'SEARCH_USERS',
CREATE_PENDING_CREATION = 'CREATE_PENDING_CREATION',
@ -26,4 +31,8 @@ export enum RIGHTS {
SEARCH_PENDING_CREATION = 'SEARCH_PENDING_CREATION',
DELETE_PENDING_CREATION = 'DELETE_PENDING_CREATION',
CONFIRM_PENDING_CREATION = 'CONFIRM_PENDING_CREATION',
SEND_ACTIVATION_EMAIL = 'SEND_ACTIVATION_EMAIL',
DELETE_USER = 'DELETE_USER',
UNDELETE_USER = 'UNDELETE_USER',
CREATION_TRANSACTION_LIST = 'CREATION_TRANSACTION_LIST',
}

View File

@ -18,6 +18,10 @@ export const ROLE_USER = new Role('user', [
RIGHTS.LOGOUT,
RIGHTS.UPDATE_USER_INFOS,
RIGHTS.HAS_ELOPAGE,
RIGHTS.CREATE_TRANSACTION_LINK,
RIGHTS.DELETE_TRANSACTION_LINK,
RIGHTS.REDEEM_TRANSACTION_LINK,
RIGHTS.LIST_TRANSACTION_LINKS,
])
export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights

View File

@ -0,0 +1,9 @@
import CONFIG from './index'
describe('config/index', () => {
describe('decay start block', () => {
it('has the correct date set', () => {
expect(CONFIG.DECAY_START_TIME).toEqual(new Date('2021-05-13 17:46:31'))
})
})
})

View File

@ -1,8 +1,24 @@
// 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: '0033-add_referrer_id',
DECAY_START_TIME: new Date('2021-05-13 17:46:31'), // GMT+0
CONFIG_VERSION: {
DEFAULT: 'DEFAULT',
EXPECTED: 'v1.2022-03-18',
CURRENT: '',
},
}
const server = {
PORT: process.env.PORT || 4000,
JWT_SECRET: process.env.JWT_SECRET || 'secret123',
@ -18,6 +34,7 @@ const database = {
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 = {
@ -31,8 +48,8 @@ const klicktipp = {
const community = {
COMMUNITY_NAME: process.env.COMMUNITY_NAME || 'Gradido Entwicklung',
COMMUNITY_URL: process.env.COMMUNITY_URL || 'http://localhost/vue/',
COMMUNITY_REGISTER_URL: process.env.COMMUNITY_REGISTER_URL || 'http://localhost/vue/register',
COMMUNITY_URL: process.env.COMMUNITY_URL || 'http://localhost/',
COMMUNITY_REGISTER_URL: process.env.COMMUNITY_REGISTER_URL || 'http://localhost/register',
COMMUNITY_DESCRIPTION:
process.env.COMMUNITY_DESCRIPTION || 'Die lokale Entwicklungsumgebung von Gradido.',
}
@ -42,7 +59,6 @@ const loginServer = {
LOGIN_SERVER_KEY: process.env.LOGIN_SERVER_KEY || 'a51ef8ac7ef1abf162fb7a65261acd7a',
}
const resendTime = parseInt(process.env.RESEND_TIME ? process.env.RESEND_TIME : 'null')
const email = {
EMAIL: process.env.EMAIL === 'true' || false,
EMAIL_USERNAME: process.env.EMAIL_USERNAME || 'gradido_email',
@ -51,19 +67,36 @@ const email = {
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/vue/checkEmail/$1',
EMAIL_LINK_SETPASSWORD: process.env.EMAIL_LINK_SETPASSWORD || 'http://localhost/vue/reset/$1',
RESEND_TIME: isNaN(resendTime) ? 10 : resendTime,
process.env.EMAIL_LINK_VERIFICATION || 'http://localhost/checkEmail/{optin}{code}',
EMAIL_LINK_SETPASSWORD:
process.env.EMAIL_LINK_SETPASSWORD || 'http://localhost/reset-password/{optin}',
EMAIL_CODE_VALID_TIME: process.env.EMAIL_CODE_VALID_TIME
? parseInt(process.env.EMAIL_CODE_VALID_TIME) || 10
: 10,
}
const webhook = {
// Elopage
WEBHOOK_ELOPAGE_SECRET: process.env.WEBHOOK_ELOPAGE_SECRET || 'secret',
}
// 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,

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +0,0 @@
import { ArgsType, Field } from 'type-graphql'
@ArgsType()
export default class CheckUsernameArgs {
@Field(() => String)
username: string
}

View File

@ -16,4 +16,7 @@ export default class CreateUserArgs {
@Field(() => Int, { nullable: true })
publisherId: number
@Field(() => String, { nullable: true })
redeemCode?: string | null
}

View File

@ -1,5 +1,5 @@
import { ArgsType, Field, Int } from 'type-graphql'
import { Order } from '../enum/Order'
import { Order } from '@enum/Order'
@ArgsType()
export default class Paginated {
@ -11,10 +11,4 @@ export default class Paginated {
@Field(() => Order, { nullable: true })
order?: Order
@Field(() => Boolean, { nullable: true })
onlyCreations?: boolean
@Field(() => Int, { nullable: true })
userId?: number
}

View File

@ -0,0 +1,19 @@
import { ArgsType, Field, Int } from 'type-graphql'
@ArgsType()
export default class SearchUsersArgs {
@Field(() => String)
searchText: string
@Field(() => Int, { nullable: true })
currentPage?: number
@Field(() => Int, { nullable: true })
pageSize?: number
@Field(() => Boolean, { nullable: true })
notActivated?: boolean
@Field(() => Boolean, { nullable: true })
isDeleted?: boolean
}

View File

@ -0,0 +1,11 @@
import { ArgsType, Field } from 'type-graphql'
import Decimal from 'decimal.js-light'
@ArgsType()
export default class TransactionLinkArgs {
@Field(() => Decimal)
amount: Decimal
@Field(() => String)
memo: string
}

View File

@ -1,12 +1,13 @@
import { ArgsType, Field } from 'type-graphql'
import Decimal from 'decimal.js-light'
@ArgsType()
export default class TransactionSendArgs {
@Field(() => String)
email: string
@Field(() => Number)
amount: number
@Field(() => Decimal)
amount: Decimal
@Field(() => String)
memo: string

View File

@ -8,12 +8,6 @@ export default class UpdateUserInfosArgs {
@Field({ nullable: true })
lastName?: string
@Field({ nullable: true })
description?: string
@Field({ nullable: true })
username?: string
@Field({ nullable: true })
language?: string

View File

@ -2,35 +2,45 @@
import { AuthChecker } from 'type-graphql'
import { decode, encode } from '../../auth/JWT'
import { ROLE_UNAUTHORIZED, ROLE_USER, ROLE_ADMIN } from '../../auth/ROLES'
import { RIGHTS } from '../../auth/RIGHTS'
import { ServerUserRepository } from '../../typeorm/repository/ServerUser'
import { getCustomRepository } from 'typeorm'
import { UserRepository } from '../../typeorm/repository/User'
import { decode, encode } from '@/auth/JWT'
import { ROLE_UNAUTHORIZED, ROLE_USER, ROLE_ADMIN } from '@/auth/ROLES'
import { RIGHTS } from '@/auth/RIGHTS'
import { getCustomRepository } from '@dbTools/typeorm'
import { UserRepository } from '@repository/User'
import { INALIENABLE_RIGHTS } from '@/auth/INALIENABLE_RIGHTS'
import { ServerUser } from '@entity/ServerUser'
const isAuthorized: AuthChecker<any> = async ({ context }, rights) => {
context.role = ROLE_UNAUTHORIZED // unauthorized user
// Do we have a token?
if (context.token) {
const decoded = decode(context.token)
if (!decoded) {
// we always throw on an invalid token
throw new Error('403.13 - Client certificate revoked')
}
// Set context pubKey
context.pubKey = Buffer.from(decoded.pubKey).toString('hex')
// set new header token
// TODO - load from database dynamically & admin - maybe encode this in the token to prevent many database requests
// TODO this implementation is bullshit - two database queries cause our user identifiers are not aligned and vary between email, id and pubKey
const userRepository = await getCustomRepository(UserRepository)
const user = await userRepository.findByPubkeyHex(context.pubKey)
const serverUserRepository = await getCustomRepository(ServerUserRepository)
const countServerUsers = await serverUserRepository.count({ email: user.email })
context.role = countServerUsers > 0 ? ROLE_ADMIN : ROLE_USER
// is rights an inalienable right?
if ((<RIGHTS[]>rights).reduce((acc, right) => acc && INALIENABLE_RIGHTS.includes(right), true))
return true
context.setHeaders.push({ key: 'token', value: encode(decoded.pubKey) })
// Do we have a token?
if (!context.token) {
throw new Error('401 Unauthorized')
}
// Decode the token
const decoded = decode(context.token)
if (!decoded) {
throw new Error('403.13 - Client certificate revoked')
}
// Set context pubKey
context.pubKey = Buffer.from(decoded.pubKey).toString('hex')
// TODO - load from database dynamically & admin - maybe encode this in the token to prevent many database requests
// TODO this implementation is bullshit - two database queries cause our user identifiers are not aligned and vary between email, id and pubKey
const userRepository = await getCustomRepository(UserRepository)
try {
const user = await userRepository.findByPubkeyHex(context.pubKey)
context.user = user
const countServerUsers = await ServerUser.count({ email: user.email })
context.role = countServerUsers > 0 ? ROLE_ADMIN : ROLE_USER
} catch {
// in case the database query fails (user deleted)
throw new Error('401 Unauthorized')
}
// check for correct rights
@ -39,6 +49,8 @@ const isAuthorized: AuthChecker<any> = async ({ context }, rights) => {
throw new Error('401 Unauthorized')
}
// set new header token
context.setHeaders.push({ key: 'token', value: encode(decoded.pubKey) })
return true
}

View File

@ -3,6 +3,10 @@ import { registerEnumType } from 'type-graphql'
export enum TransactionTypeId {
CREATION = 1,
SEND = 2,
RECEIVE = 3,
// This is a virtual property, never occurring on the database
DECAY = 4,
LINK_SUMMARY = 5,
}
registerEnumType(TransactionTypeId, {

Some files were not shown because too many files have changed in this diff Show More