mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge branch 'master' into apollo-clicktipp-connector
This commit is contained in:
commit
eca81883a0
35
.github/workflows/test.yml
vendored
35
.github/workflows/test.yml
vendored
@ -170,7 +170,7 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
##########################################################################
|
||||
# BUILD NGINX DOCKER IMAGE #############################################
|
||||
# BUILD NGINX DOCKER IMAGE ###############################################
|
||||
##########################################################################
|
||||
- name: nginx | Build `test` image
|
||||
run: |
|
||||
@ -182,6 +182,35 @@ jobs:
|
||||
name: docker-nginx-test
|
||||
path: /tmp/nginx.tar
|
||||
|
||||
##############################################################################
|
||||
# JOB: LOCALES FRONTEND ######################################################
|
||||
##############################################################################
|
||||
locales_frontend:
|
||||
name: Locales - 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
|
||||
##########################################################################
|
||||
# LOCALES FRONTEND #######################################################
|
||||
##########################################################################
|
||||
- name: frontend | Locales
|
||||
run: docker run --rm gradido/frontend:test yarn run locales
|
||||
|
||||
##############################################################################
|
||||
# JOB: LINT FRONTEND #########################################################
|
||||
##############################################################################
|
||||
@ -206,7 +235,7 @@ jobs:
|
||||
- name: Load Docker Image
|
||||
run: docker load < /tmp/frontend.tar
|
||||
##########################################################################
|
||||
# LINT FRONTEND ###########################################################
|
||||
# LINT FRONTEND ##########################################################
|
||||
##########################################################################
|
||||
- name: frontend | Lint
|
||||
run: docker run --rm gradido/frontend:test yarn run lint
|
||||
@ -316,7 +345,7 @@ jobs:
|
||||
report_name: Coverage Frontend
|
||||
type: lcov
|
||||
result_path: ./coverage/lcov.info
|
||||
min_coverage: 60
|
||||
min_coverage: 66
|
||||
token: ${{ github.token }}
|
||||
|
||||
##############################################################################
|
||||
|
||||
@ -39,4 +39,3 @@ git submodule update --recursive --init
|
||||
## Useful Links
|
||||
|
||||
- [Gradido.net](https://gradido.net/)
|
||||
- [Discord](https://discord.gg/kA3zBAKQDC)
|
||||
|
||||
28
docu/Concepts/Snippets/Authorization/concept.md
Normal file
28
docu/Concepts/Snippets/Authorization/concept.md
Normal file
@ -0,0 +1,28 @@
|
||||
# Authorization and Private Keys
|
||||
## Keys
|
||||
For creating transactions ed25519 keys are used for signing.
|
||||
As long the user is the only controlling the private key he is the only one
|
||||
how can sign transactions on his behalf.
|
||||
It is a core concept of all crypto currencies and important for the concept,
|
||||
that the user has full control over his data.
|
||||
|
||||
Usually crypto currencies like bitcoin or iota save the keys on local system,
|
||||
maybe additional protected with a password which is used to encrypt the keys.
|
||||
|
||||
## Gradido
|
||||
Gradido should be easy to use, so we must offer a solution for everyone not that fit
|
||||
with computer, as easy to use like paypal.
|
||||
For that role we have the Login-Server.
|
||||
It stores the private keys of the user encrypted with there email and password.
|
||||
Additional it stores the passphrase which can be used to generate the private key,
|
||||
encryted with server admin public key. So only the server admin can access the keys
|
||||
with his private key. [not done yet]
|
||||
It is needed for passwort reset if a user has forgetten his password.
|
||||
|
||||
But for the entire concept Login-Server isn't the only way to store the private keys.
|
||||
For users which has more experience with computer and especially with crypto currencies
|
||||
it should be a way to keep there private keys by themselfs.
|
||||
|
||||
For example a Desktop- or Handy-App which store the keys locally maybe additional encrypted.
|
||||
Maybe it is possible to use Stronghold from iota for that.
|
||||
With that the user don't need to use the Login-Server.
|
||||
29
docu/Concepts/Snippets/Authorization/jwt.md
Normal file
29
docu/Concepts/Snippets/Authorization/jwt.md
Normal file
@ -0,0 +1,29 @@
|
||||
# How JWT could be used for authorization with and without Login-Server
|
||||
## What we need
|
||||
The only encrypted data in db are the private key.
|
||||
Every other data could be accessed without login, depending on frontend and backend code.
|
||||
So we need only a way to prove the backend that we have access to the private key.
|
||||
|
||||
## JWT
|
||||
JWT is perfect for that.
|
||||
We can use JWT to store the public key of the user as UUID for finding his data in db,
|
||||
signing it with the private key. So even if the backend is running in multiple instances,
|
||||
on every request is it possible to check the JWT token, that the signature is signed with
|
||||
the private key, belonging to the public key.
|
||||
The only thing the backend cannot do with that is signing a transaction.
|
||||
That can only be done by the Login-Server or a Desktop or Handy-App storing the private key locally.
|
||||
With that we have universal way for authorization against the backend.
|
||||
We could additional store if we like to sign transactions local or with Login-Server and the Login-Server url.
|
||||
|
||||
## JWT and Login-Server
|
||||
Login-Server uses Poco version 1.9.4 but unfortunately Poco only introduces jwt from version 1.10.
|
||||
And Updating to 1.10 needs some work because some things have changed in Poco 1.10.
|
||||
|
||||
## JWT signature algorithms
|
||||
In JWT standard ed25519 don't seemd to play a role.
|
||||
We must find out if we can use the ed25519 keys together with one of the signature algorithms
|
||||
in JWT standard or we must use **crypto_sign_verify_detached** from libsodium even it is nonstandard
|
||||
to verify signature created with ed25519 keys and libsodiums **crypto_sign_detached** function.
|
||||
|
||||
|
||||
|
||||
16
docu/Concepts/Snippets/Authorization/session_id.md
Normal file
16
docu/Concepts/Snippets/Authorization/session_id.md
Normal file
@ -0,0 +1,16 @@
|
||||
# Session Id Authorization
|
||||
## Login-Server
|
||||
With every login, the Login-Server creates a session with a random id,
|
||||
storing it in memory. For Login email and password are needed.
|
||||
From email and an additional app-secret (**crypto.app_secret** in Login-Server config) a sha512 hash will be genereted, named **hash512_salt**.
|
||||
With sodium function *crypto_pwhash* with **hash512_salt** and user password a secret encryption key will be calculated.
|
||||
*crypto_pwhash* uses argon2 algorithmus to have a CPU hard calculation. Currently it is configured for < 0.5s.
|
||||
So it is harder to use brute-force attacks to guess the password. Even if someone gets hands on the data saved in db.
|
||||
|
||||
With sodium function *crypto_shorthash* a hash will be calculated from the secret encryption key and server crypto key (**crypto.server_key** in Login-Server config, hex encoded, 16 Bytes, 32 Character hex encoded)
|
||||
and compared against saved hash in db. If they identical user has successfull logged in.
|
||||
The secret encryption key will be stored in memory together with the user session and client ip from which login call came.
|
||||
The session_id will be returned.
|
||||
The session will be hold in memory for 15 minutes default, can be changed in Login-Server config field **session.timeout**
|
||||
|
||||
|
||||
@ -72,6 +72,9 @@ RUN yarn run build
|
||||
##################################################################################
|
||||
FROM build as test
|
||||
|
||||
# Install Additional Software
|
||||
RUN apk add --no-cache bash jq
|
||||
|
||||
# Run command
|
||||
CMD /bin/sh -c "yarn run dev"
|
||||
|
||||
|
||||
@ -1,79 +0,0 @@
|
||||
<template>
|
||||
<div id="accordion" role="tablist" aria-multiselectable="true" class="accordion">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'collapse',
|
||||
props: {
|
||||
animationDuration: {
|
||||
type: Number,
|
||||
default: 250,
|
||||
description: 'Collapse animation duration',
|
||||
},
|
||||
multipleActive: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
description: 'Whether you can have multiple collapse items opened at the same time',
|
||||
},
|
||||
activeIndex: {
|
||||
type: Number,
|
||||
default: -1,
|
||||
description: 'Active collapse item index',
|
||||
},
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
animationDuration: this.animationDuration,
|
||||
multipleActive: this.multipleActive,
|
||||
addItem: this.addItem,
|
||||
removeItem: this.removeItem,
|
||||
deactivateAll: this.deactivateAll,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
items: [],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addItem(item) {
|
||||
const index = this.$slots.default.indexOf(item.$vnode)
|
||||
if (index !== -1) {
|
||||
this.items.splice(index, 0, item)
|
||||
}
|
||||
},
|
||||
removeItem(item) {
|
||||
const items = this.items
|
||||
const index = items.indexOf(item)
|
||||
if (index > -1) {
|
||||
items.splice(index, 1)
|
||||
}
|
||||
},
|
||||
deactivateAll() {
|
||||
this.items.forEach((item) => {
|
||||
item.active = false
|
||||
})
|
||||
},
|
||||
activateItem() {
|
||||
if (this.activeIndex !== -1) {
|
||||
this.items[this.activeIndex].active = true
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.activateItem()
|
||||
})
|
||||
},
|
||||
watch: {
|
||||
activeIndex() {
|
||||
this.activateItem()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@ -1,91 +0,0 @@
|
||||
<template>
|
||||
<b-card no-body>
|
||||
<b-card-header role="tab" class="card-header" :aria-expanded="active">
|
||||
<a
|
||||
data-toggle="collapse"
|
||||
data-parent="#accordion"
|
||||
:href="`#${itemId}`"
|
||||
@click.prevent="activate"
|
||||
:aria-controls="`content-${itemId}`"
|
||||
>
|
||||
<slot name="title">{{ title }}</slot>
|
||||
<i class="tim-icons icon-minimal-down"></i>
|
||||
</a>
|
||||
</b-card-header>
|
||||
<collapse-transition :duration="animationDuration">
|
||||
<div
|
||||
v-show="active"
|
||||
:id="`content-${itemId}`"
|
||||
role="tabpanel"
|
||||
:aria-labelledby="title"
|
||||
class="collapsed"
|
||||
>
|
||||
<div class="card-body"><slot></slot></div>
|
||||
</div>
|
||||
</collapse-transition>
|
||||
</b-card>
|
||||
</template>
|
||||
<script>
|
||||
import { CollapseTransition } from 'vue2-transitions'
|
||||
|
||||
export default {
|
||||
name: 'collapse-item',
|
||||
components: {
|
||||
CollapseTransition,
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
description: 'Collapse item title',
|
||||
},
|
||||
id: String,
|
||||
},
|
||||
inject: {
|
||||
animationDuration: {
|
||||
default: 250,
|
||||
},
|
||||
multipleActive: {
|
||||
default: false,
|
||||
},
|
||||
addItem: {
|
||||
default: () => {},
|
||||
},
|
||||
removeItem: {
|
||||
default: () => {},
|
||||
},
|
||||
deactivateAll: {
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
itemId() {
|
||||
return this.id || this.title
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
active: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
activate() {
|
||||
const wasActive = this.active
|
||||
if (!this.multipleActive) {
|
||||
this.deactivateAll()
|
||||
}
|
||||
this.active = !wasActive
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.addItem(this)
|
||||
},
|
||||
destroyed() {
|
||||
if (this.$el && this.$el.parentNode) {
|
||||
this.$el.parentNode.removeChild(this.$el)
|
||||
}
|
||||
this.removeItem(this)
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style></style>
|
||||
@ -3,11 +3,17 @@ import PaginationButtons from './PaginationButtons'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const propsData = {
|
||||
totalRows: 42,
|
||||
perPage: 12,
|
||||
value: 1,
|
||||
}
|
||||
|
||||
describe('PaginationButtons', () => {
|
||||
let wrapper
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(PaginationButtons, { localVue })
|
||||
return mount(PaginationButtons, { localVue, propsData })
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
@ -19,34 +25,20 @@ describe('PaginationButtons', () => {
|
||||
expect(wrapper.find('div.pagination-buttons').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('has previous page button disabled by default', () => {
|
||||
expect(wrapper.find('button.previous-page').attributes('disabled')).toBe('disabled')
|
||||
})
|
||||
|
||||
it('has bext page button disabled by default', () => {
|
||||
expect(wrapper.find('button.next-page').attributes('disabled')).toBe('disabled')
|
||||
})
|
||||
|
||||
it('shows the text "1 / 1" by default"', () => {
|
||||
expect(wrapper.find('p.text-center').text()).toBe('1 / 1')
|
||||
})
|
||||
|
||||
describe('with active buttons', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.setProps({
|
||||
hasNext: true,
|
||||
hasPrevious: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('emits show-previous when previous page button is clicked', () => {
|
||||
wrapper.find('button.previous-page').trigger('click')
|
||||
expect(wrapper.emitted('show-previous')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('emits show-next when next page button is clicked', () => {
|
||||
it('emits input next page button is clicked', async () => {
|
||||
wrapper.find('button.next-page').trigger('click')
|
||||
expect(wrapper.emitted('show-next')).toBeTruthy()
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.emitted().input[0]).toEqual([2])
|
||||
})
|
||||
|
||||
it('emits input when previous page button is clicked', async () => {
|
||||
wrapper.setProps({ value: 2 })
|
||||
wrapper.setData({ currentValue: 2 })
|
||||
await wrapper.vm.$nextTick()
|
||||
wrapper.find('button.previous-page').trigger('click')
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.emitted().input[0]).toEqual([1])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
<template>
|
||||
<div class="pagination-buttons">
|
||||
<div class="pagination-buttons" v-if="totalRows > perPage">
|
||||
<b-row class="m-4">
|
||||
<b-col class="text-right">
|
||||
<b-button class="previous-page" :disabled="!hasPrevious" @click="$emit('show-previous')">
|
||||
<b-button class="previous-page" :disabled="!hasPrevious" @click="currentValue--">
|
||||
<b-icon icon="chevron-left" variant="primary"></b-icon>
|
||||
</b-button>
|
||||
</b-col>
|
||||
<b-col cols="3">
|
||||
<p class="text-center pt-2">{{ currentPage }} / {{ totalPages }}</p>
|
||||
<p class="text-center pt-2">{{ value }} / {{ totalPages }}</p>
|
||||
</b-col>
|
||||
<b-col>
|
||||
<b-button class="next-page" :disabled="!hasNext" @click="$emit('show-next')">
|
||||
<b-button class="next-page" :disabled="!hasNext" @click="currentValue++">
|
||||
<b-icon icon="chevron-right" variant="primary"></b-icon>
|
||||
</b-button>
|
||||
</b-col>
|
||||
@ -21,10 +21,33 @@
|
||||
export default {
|
||||
name: 'PaginationButtons',
|
||||
props: {
|
||||
hasNext: { type: Boolean, default: false },
|
||||
hasPrevious: { type: Boolean, default: false },
|
||||
totalPages: { type: Number, default: 1 },
|
||||
currentPage: { type: Number, default: 1 },
|
||||
totalRows: { required: true },
|
||||
perPage: { type: Number, required: true },
|
||||
value: { type: Number, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentValue: { type: Number, default: 1 },
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasNext() {
|
||||
return this.value * this.perPage < this.totalRows
|
||||
},
|
||||
hasPrevious() {
|
||||
return this.value > 1
|
||||
},
|
||||
totalPages() {
|
||||
return Math.ceil(this.totalRows / this.perPage)
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.currentValue = this.value
|
||||
},
|
||||
watch: {
|
||||
currentValue() {
|
||||
if (this.currentValue !== this.value) this.$emit('input', this.currentValue)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -1,8 +1,5 @@
|
||||
import NavbarToggleButton from './Navbar/NavbarToggleButton'
|
||||
|
||||
import Collapse from './Collapse/Collapse.vue'
|
||||
import CollapseItem from './Collapse/CollapseItem.vue'
|
||||
|
||||
import SidebarPlugin from './SidebarPlugin'
|
||||
|
||||
export { SidebarPlugin, NavbarToggleButton, Collapse, CollapseItem }
|
||||
export { SidebarPlugin, NavbarToggleButton }
|
||||
|
||||
@ -6,6 +6,8 @@ sendMock.mockResolvedValue('success')
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
window.scrollTo = jest.fn()
|
||||
|
||||
describe('AccountOverview', () => {
|
||||
let wrapper
|
||||
|
||||
|
||||
@ -1,183 +0,0 @@
|
||||
<template>
|
||||
<div class="pt-4 pb-4">
|
||||
<b-tabs content-class="mt-3" class="display-4" fill>
|
||||
<b-tab :title="names.thisMonth" active>
|
||||
<b-row>
|
||||
<b-col lg="3">
|
||||
<b-input :label="$t('communitys.form.hours')">
|
||||
<b-form-input
|
||||
type="number"
|
||||
size="lg"
|
||||
placeholder="23"
|
||||
style="font-size: xx-large; padding-left: 5px"
|
||||
/>
|
||||
</b-input>
|
||||
<b-input :label="$t('communitys.form.date_period')">
|
||||
<flat-pickr
|
||||
class="form-control"
|
||||
v-model="date"
|
||||
:config="config"
|
||||
style="font-size: 0.5em; padding-left: 5px"
|
||||
></flat-pickr>
|
||||
</b-input>
|
||||
</b-col>
|
||||
<b-col lg="9">
|
||||
<b-input :label="$t('communitys.form.hours_report')">
|
||||
<textarea
|
||||
class="form-control"
|
||||
rows="5"
|
||||
@focus="textFocus"
|
||||
style="font-size: x-large; padding-left: 20px"
|
||||
></textarea>
|
||||
</b-input>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<b-row>
|
||||
<div ref="mydiv"></div>
|
||||
</b-row>
|
||||
<b-row>
|
||||
<b-col md="6">
|
||||
<b-button @click.prevent="newWorkForm" variant="warning">
|
||||
+ {{ $t('communitys.form.more_hours') }}
|
||||
</b-button>
|
||||
</b-col>
|
||||
<b-col md="6" class="text-right">
|
||||
<b-button variant="success" @click.prevent="submitForm2">
|
||||
{{ $t('communitys.form.submit') }}
|
||||
</b-button>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-tab>
|
||||
|
||||
<b-tab :title="names.lastMonth"></b-tab>
|
||||
|
||||
<b-tab :title="names.beforLastMonth"></b-tab>
|
||||
</b-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'GDDAddWork2',
|
||||
data() {
|
||||
return {
|
||||
date: null,
|
||||
config: {
|
||||
altInput: false,
|
||||
dateFormat: 'd-m-Y',
|
||||
minDate: this.$moment().startOf('month').format('DD.MM.YYYY'),
|
||||
maxDate: this.$moment().format('DD.MM.YYYY'),
|
||||
mode: 'range',
|
||||
},
|
||||
index: 0,
|
||||
form: [],
|
||||
stundenSumme: 0,
|
||||
messages: [],
|
||||
submitted: false,
|
||||
days: {
|
||||
thisMonth: this.$moment().month(this.$moment().month()).daysInMonth(),
|
||||
lastMonth: this.$moment()
|
||||
.month(this.$moment().month() - 1)
|
||||
.daysInMonth(),
|
||||
beforLastMonth: this.$moment()
|
||||
.month(this.$moment().month() - 2)
|
||||
.daysInMonth(),
|
||||
},
|
||||
names: {
|
||||
thisMonth: this.$moment().month(this.$moment().month()).format('MMMM'),
|
||||
lastMonth: this.$moment()
|
||||
.month(this.$moment().month() - 1)
|
||||
.format('MMMM'),
|
||||
beforLastMonth: this.$moment()
|
||||
.month(this.$moment().month() - 2)
|
||||
.format('MMMM'),
|
||||
},
|
||||
formular: null,
|
||||
}
|
||||
},
|
||||
created() {},
|
||||
watch: {
|
||||
$form: function () {
|
||||
this.stunden(this.form)
|
||||
},
|
||||
},
|
||||
mounted() {},
|
||||
methods: {
|
||||
stunden(hour, i, mon) {
|
||||
let n = 0
|
||||
this.stundenSumme = 0
|
||||
for (n; n < this.form.length; n++) {
|
||||
if (this.form[n] > 0) {
|
||||
this.stundenSumme += parseInt(this.form[n])
|
||||
}
|
||||
}
|
||||
this.messages.push({
|
||||
id: this.index,
|
||||
MonthsNumber: mon,
|
||||
DaysNumber: i,
|
||||
HoursNumber: hour,
|
||||
DestinationText: '',
|
||||
TextDecoded: '',
|
||||
})
|
||||
this.index++
|
||||
},
|
||||
addNewMessage: function () {
|
||||
this.messages.push({
|
||||
DaysNumber: '',
|
||||
TextDecoded: '',
|
||||
})
|
||||
},
|
||||
deleteNewMessage: function (event) {
|
||||
this.form.splice(event, null)
|
||||
this.messages.splice(this.index, 1)
|
||||
this.index--
|
||||
},
|
||||
submitForm: function (e) {
|
||||
// console.log('submitForm')
|
||||
this.messages = [{ DaysNumber: '', TextDecoded: '' }]
|
||||
this.submitted = true
|
||||
},
|
||||
textFocus() {
|
||||
// console.log('textFocus TODO')
|
||||
},
|
||||
newWorkForm() {
|
||||
this.formular = `
|
||||
<b-col lg="3">
|
||||
<b-input label="Stunden">
|
||||
<b-form-input
|
||||
type="number"
|
||||
size="lg"
|
||||
placeholder="0"
|
||||
style="font-size: xx-large; padding-left: 20px"
|
||||
/>
|
||||
</b-input>
|
||||
<b-input label="Datum / Zeitraum">
|
||||
<flat-pickr
|
||||
class="form-control"
|
||||
v-model="date"
|
||||
:config="config"
|
||||
style="font-size: xx-large; padding-left: 20px"
|
||||
></flat-pickr>
|
||||
</b-input>
|
||||
</b-col>
|
||||
<b-col lg="9">
|
||||
<b-input label="Arbeitsreport">
|
||||
<textarea
|
||||
class="form-control"
|
||||
rows="5"
|
||||
@focus="textFocus"
|
||||
style="font-size: x-large; padding-left: 20px"
|
||||
></textarea>
|
||||
</b-input>
|
||||
</b-col>
|
||||
`
|
||||
|
||||
// console.log('newWorkForm TODO')
|
||||
const myElement = this.$refs.mydiv
|
||||
myElement.append(this.formular)
|
||||
this.$compile(myElement)
|
||||
this.formular = null
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -84,13 +84,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<pagination-buttons
|
||||
v-if="showPagination && transactionCount > pageSize"
|
||||
:has-next="hasNext"
|
||||
:has-previous="hasPrevious"
|
||||
:total-pages="totalPages"
|
||||
:current-page="currentPage"
|
||||
@show-next="showNext"
|
||||
@show-previous="showPrevious"
|
||||
v-if="showPagination"
|
||||
v-model="currentPage"
|
||||
:per-page="pageSize"
|
||||
:total-rows="transactionCount"
|
||||
></pagination-buttons>
|
||||
<div v-if="transactions.length === 0" class="mt-4 text-center">
|
||||
<span>{{ $t('transaction.nullTransactions') }}</span>
|
||||
@ -128,29 +125,13 @@ export default {
|
||||
transactionCount: { type: Number, default: 0 },
|
||||
showPagination: { type: Boolean, default: false },
|
||||
},
|
||||
watch: {
|
||||
timestamp: {
|
||||
immediate: true,
|
||||
handler: 'updateTransactions',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
hasNext() {
|
||||
return this.currentPage * this.pageSize < this.transactionCount
|
||||
},
|
||||
hasPrevious() {
|
||||
return this.currentPage > 1
|
||||
},
|
||||
totalPages() {
|
||||
return Math.ceil(this.transactionCount / this.pageSize)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateTransactions() {
|
||||
this.$emit('update-transactions', {
|
||||
firstPage: this.currentPage,
|
||||
items: this.pageSize,
|
||||
})
|
||||
window.scrollTo(0, 0)
|
||||
},
|
||||
getProperties(givenType) {
|
||||
const type = iconsByType[givenType]
|
||||
@ -165,15 +146,14 @@ export default {
|
||||
throwError(msg) {
|
||||
throw new Error(msg)
|
||||
},
|
||||
showNext() {
|
||||
this.currentPage++
|
||||
},
|
||||
watch: {
|
||||
currentPage() {
|
||||
this.updateTransactions()
|
||||
window.scrollTo(0, 0)
|
||||
},
|
||||
showPrevious() {
|
||||
this.currentPage--
|
||||
this.updateTransactions()
|
||||
window.scrollTo(0, 0)
|
||||
timestamp: {
|
||||
immediate: true,
|
||||
handler: 'updateTransactions',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -1,83 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-list-group>
|
||||
<b-list-group-item v-for="item in items" :key="item.id">
|
||||
<div class="d-flex w-100 justify-content-between" @click="toogle(item)">
|
||||
<b-icon
|
||||
v-if="item.status == 'submitted'"
|
||||
icon="clock-history"
|
||||
class="m-1"
|
||||
font-scale="2"
|
||||
style="color: orange"
|
||||
></b-icon>
|
||||
<b-icon v-else icon="check2-all" class="m-1" font-scale="2" style="color: green"></b-icon>
|
||||
<h2 class="text-muted">
|
||||
<small>{{ item.datel }}</small>
|
||||
- {{ item.text }}
|
||||
</h2>
|
||||
</div>
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
<hr />
|
||||
<b-icon icon="clock-history" class="m-1" font-scale="2" style="color: orange"></b-icon>
|
||||
Wartet auf Bestätigung |
|
||||
<b-icon icon="check2-all" class="m-1" font-scale="2" style="color: green"></b-icon>
|
||||
bestätigt
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'GddWorkTable',
|
||||
data() {
|
||||
return {
|
||||
form: [],
|
||||
items: [
|
||||
{
|
||||
id: 1,
|
||||
text: 'Zwei Säcke Plastikmüll im Wald gesammelt',
|
||||
datel: '12.12.2020 14:04',
|
||||
status: 'submitted',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
text: 'Frau Schmidt bei der Gartenarbeit geholfen.',
|
||||
datel: '22.06.2020 22:23',
|
||||
status: 'submitted',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
text: 'Ein online Kurs für nachhaltiges Mülltrennen erstellt',
|
||||
datel: '15.04.2020 12:55',
|
||||
status: 'confirmed',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
text: 'Gradido bei meinen Freunden vorgestellt',
|
||||
datel: '10.03.2020 18:20',
|
||||
status: 'confirmed',
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
rowClass(item, type) {
|
||||
if (!item || type !== 'row') return
|
||||
if (item.status === 'received') return 'table-success'
|
||||
if (item.status === 'sent') return 'table-warning'
|
||||
if (item.status === 'earned') return 'table-primary'
|
||||
},
|
||||
toogle(item) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const temp =
|
||||
'<b-collapse visible v-bind:id="item.id">xxx <small class="text-muted">porta</small></b-collapse>'
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.el-table .cell {
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
</style>
|
||||
@ -46,6 +46,9 @@ const apolloMock = jest.fn().mockResolvedValue({
|
||||
})
|
||||
|
||||
const toastErrorMock = jest.fn()
|
||||
const windowScrollToMock = jest.fn()
|
||||
|
||||
window.scrollTo = windowScrollToMock
|
||||
|
||||
describe('GdtTransactionList', () => {
|
||||
let wrapper
|
||||
@ -90,6 +93,10 @@ describe('GdtTransactionList', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('scrolls to (0, 0) after API call', () => {
|
||||
expect(windowScrollToMock).toBeCalledWith(0, 0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('server returns error', () => {
|
||||
@ -105,5 +112,21 @@ describe('GdtTransactionList', () => {
|
||||
expect(toastErrorMock).toBeCalledWith('Ouch!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('change of currentPage', () => {
|
||||
it('calls the API after currentPage changes', async () => {
|
||||
jest.clearAllMocks()
|
||||
wrapper.setData({ currentPage: 2 })
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(apolloMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
variables: {
|
||||
currentPage: 2,
|
||||
pageSize: 25,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -28,13 +28,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<pagination-buttons
|
||||
v-if="transactionGdtCount > pageSize"
|
||||
:has-next="hasNext"
|
||||
:has-previous="hasPrevious"
|
||||
:total-pages="totalPages"
|
||||
:current-page="currentPage"
|
||||
@show-next="showNext"
|
||||
@show-previous="showPrevious"
|
||||
v-model="currentPage"
|
||||
:per-page="pageSize"
|
||||
:total-rows="transactionGdtCount"
|
||||
></pagination-buttons>
|
||||
</div>
|
||||
</template>
|
||||
@ -52,23 +48,12 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
transactionsGdt: { default: () => [] },
|
||||
transactionsGdt: [],
|
||||
transactionGdtCount: { type: Number, default: 0 },
|
||||
currentPage: 1,
|
||||
pageSize: 25,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasNext() {
|
||||
return this.currentPage * this.pageSize < this.transactionGdtCount
|
||||
},
|
||||
hasPrevious() {
|
||||
return this.currentPage > 1
|
||||
},
|
||||
totalPages() {
|
||||
return Math.ceil(this.transactionGdtCount / this.pageSize)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async updateGdt() {
|
||||
this.$apollo
|
||||
@ -85,25 +70,21 @@ export default {
|
||||
} = result
|
||||
this.transactionsGdt = listGDTEntries.gdtEntries
|
||||
this.transactionGdtCount = listGDTEntries.count
|
||||
window.scrollTo(0, 0)
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toasted.error(error.message)
|
||||
})
|
||||
},
|
||||
showNext() {
|
||||
this.currentPage++
|
||||
this.updateGdt()
|
||||
window.scrollTo(0, 0)
|
||||
},
|
||||
showPrevious() {
|
||||
this.currentPage--
|
||||
this.updateGdt()
|
||||
window.scrollTo(0, 0)
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.updateGdt()
|
||||
},
|
||||
watch: {
|
||||
currentPage() {
|
||||
this.updateGdt()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
|
||||
@ -3,6 +3,8 @@ import UserProfileTransactionList from './UserProfileTransactionList'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
window.scrollTo = jest.fn()
|
||||
|
||||
describe('UserProfileTransactionList', () => {
|
||||
let wrapper
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user