Merge master resolve merge conflict.

This commit is contained in:
elweyn 2021-12-13 11:19:06 +01:00
commit a0f6ec65d8
25 changed files with 428 additions and 821 deletions

View File

@ -6,7 +6,7 @@ import { UpdatePendingCreation } from '../model/UpdatePendingCreation'
import { RIGHTS } from '../../auth/RIGHTS'
import { TransactionRepository } from '../../typeorm/repository/Transaction'
import { TransactionCreationRepository } from '../../typeorm/repository/TransactionCreation'
import { PendingCreationRepository } from '../../typeorm/repository/PendingCreation'
import { LoginPendingTasksAdminRepository } from '../../typeorm/repository/LoginPendingTasksAdmin'
import { UserRepository } from '../../typeorm/repository/User'
import CreatePendingCreationArgs from '../arg/CreatePendingCreationArgs'
import UpdatePendingCreationArgs from '../arg/UpdatePendingCreationArgs'
@ -48,8 +48,8 @@ export class AdminResolver {
const creations = await getUserCreations(user.id)
const creationDateObj = new Date(creationDate)
if (isCreationValid(creations, amount, creationDateObj)) {
const pendingCreationRepository = getCustomRepository(PendingCreationRepository)
const loginPendingTaskAdmin = pendingCreationRepository.create()
const loginPendingTasksAdminRepository = getCustomRepository(LoginPendingTasksAdminRepository)
const loginPendingTaskAdmin = loginPendingTasksAdminRepository.create()
loginPendingTaskAdmin.userId = user.id
loginPendingTaskAdmin.amount = BigInt(amount * 10000)
loginPendingTaskAdmin.created = new Date()
@ -57,7 +57,7 @@ export class AdminResolver {
loginPendingTaskAdmin.memo = memo
loginPendingTaskAdmin.moderator = moderator
pendingCreationRepository.save(loginPendingTaskAdmin)
loginPendingTasksAdminRepository.save(loginPendingTaskAdmin)
}
return await getUserCreations(user.id)
}
@ -70,8 +70,8 @@ export class AdminResolver {
const userRepository = getCustomRepository(UserRepository)
const user = await userRepository.findByEmail(email)
const pendingCreationRepository = getCustomRepository(PendingCreationRepository)
const updatedCreation = await pendingCreationRepository.findOneOrFail({ id })
const loginPendingTasksAdminRepository = getCustomRepository(LoginPendingTasksAdminRepository)
const updatedCreation = await loginPendingTasksAdminRepository.findOneOrFail({ id })
if (updatedCreation.userId !== user.id)
throw new Error('user of the pending creation and send user does not correspond')
@ -81,7 +81,7 @@ export class AdminResolver {
updatedCreation.date = new Date(creationDate)
updatedCreation.moderator = moderator
await pendingCreationRepository.save(updatedCreation)
await loginPendingTasksAdminRepository.save(updatedCreation)
const result = new UpdatePendingCreation()
result.amount = parseInt(amount.toString())
result.memo = updatedCreation.memo
@ -110,8 +110,8 @@ export class AdminResolver {
@Query(() => [PendingCreation])
async getPendingCreations(): Promise<PendingCreation[]> {
const pendingCreationRepository = getCustomRepository(PendingCreationRepository)
const pendingCreations = await pendingCreationRepository.find()
const loginPendingTasksAdminRepository = getCustomRepository(LoginPendingTasksAdminRepository)
const pendingCreations = await loginPendingTasksAdminRepository.find()
const pendingCreationsPromise = await Promise.all(
pendingCreations.map(async (pendingCreation) => {
@ -137,16 +137,16 @@ export class AdminResolver {
@Mutation(() => Boolean)
async deletePendingCreation(@Arg('id') id: number): Promise<boolean> {
const pendingCreationRepository = getCustomRepository(PendingCreationRepository)
const entity = await pendingCreationRepository.findOneOrFail(id)
const res = await pendingCreationRepository.delete(entity)
const loginPendingTasksAdminRepository = getCustomRepository(LoginPendingTasksAdminRepository)
const entity = await loginPendingTasksAdminRepository.findOneOrFail(id)
const res = await loginPendingTasksAdminRepository.delete(entity)
return !!res
}
@Mutation(() => Boolean)
async confirmPendingCreation(@Arg('id') id: number): Promise<boolean> {
const pendingCreationRepository = getCustomRepository(PendingCreationRepository)
const pendingCreation = await pendingCreationRepository.findOneOrFail(id)
const loginPendingTasksAdminRepository = getCustomRepository(LoginPendingTasksAdminRepository)
const pendingCreation = await loginPendingTasksAdminRepository.findOneOrFail(id)
const transactionRepository = getCustomRepository(TransactionRepository)
let transaction = new Transaction()
@ -198,7 +198,7 @@ export class AdminResolver {
userBalance.modified = new Date()
userBalance.recordDate = userBalance.recordDate ? userBalance.recordDate : new Date()
await balanceRepository.save(userBalance)
await pendingCreationRepository.delete(pendingCreation)
await loginPendingTasksAdminRepository.delete(pendingCreation)
return true
}
@ -227,8 +227,8 @@ async function getUserCreations(id: number): Promise<number[]> {
.orderBy('target_month', 'ASC')
.getRawMany()
const pendingCreationRepository = getCustomRepository(PendingCreationRepository)
const pendingAmountsQuery = await pendingCreationRepository
const loginPendingTasksAdminRepository = getCustomRepository(LoginPendingTasksAdminRepository)
const pendingAmountsQuery = await loginPendingTasksAdminRepository
.createQueryBuilder('login_pending_tasks_admin')
.select('MONTH(login_pending_tasks_admin.date)', 'target_month')
.addSelect('SUM(login_pending_tasks_admin.amount)', 'sum')

View File

@ -2,4 +2,4 @@ import { EntityRepository, Repository } from 'typeorm'
import { LoginPendingTasksAdmin } from '@entity/LoginPendingTasksAdmin'
@EntityRepository(LoginPendingTasksAdmin)
export class PendingCreationRepository extends Repository<LoginPendingTasksAdmin> {}
export class LoginPendingTasksAdminRepository extends Repository<LoginPendingTasksAdmin> {}

View File

@ -1,7 +1,7 @@
##################################################################################
# BASE ###########################################################################
##################################################################################
FROM node:12.19.0-alpine3.10 as base
FROM node:17-alpine as base
# ENVs (available in production aswell, can be overwritten by commandline or env file)
## DOCKER_WORKDIR would be a classical ARG, but that is not multi layer persistent - shame
@ -14,8 +14,6 @@ ENV BUILD_VERSION="0.0.0.0"
ENV BUILD_COMMIT="0000000"
## SET NODE_ENV
ENV NODE_ENV="production"
## App relevant Envs
#ENV PORT="4000"
# Labels
LABEL org.label-schema.build-date="${BUILD_DATE}"
@ -34,10 +32,6 @@ LABEL maintainer="support@gradido.net"
## install: git
#RUN apk --no-cache add git
# Settings
## Expose Container Port
# EXPOSE ${PORT}
## Workdir
RUN mkdir -p ${DOCKER_WORKDIR}
WORKDIR ${DOCKER_WORKDIR}
@ -99,7 +93,7 @@ FROM base as production
# Copy "binary"-files from build image
COPY --from=build ${DOCKER_WORKDIR}/build ./build
# We also copy the node_modules express and serve-static for the run script
COPY --from=build ${DOCKER_WORKDIR}/node_modules ./node_modules
COPY --from=build ${DOCKER_WORKDIR}/node_modules ./node_modules
# Copy static files
# COPY --from=build ${DOCKER_WORKDIR}/public ./public
# Copy package.json for script definitions (lock file should not be needed)

View File

@ -68,9 +68,6 @@ services:
image: gradido/database:test_up
build:
target: test_up
#networks:
# - external-net
# - internal-net
environment:
- NODE_ENV="development"
volumes:

View File

@ -116,12 +116,10 @@ services:
- mariadb
networks:
- internal-net
#ports:
# - 4000:4000
- external-net # this is required to fetch the packages
environment:
# Envs used in Dockerfile
# - DOCKER_WORKDIR="/app"
# - PORT=4000
- BUILD_DATE
- BUILD_VERSION
- BUILD_COMMIT

View File

@ -1,6 +1,6 @@
<template>
<div id="app" class="font-sans text-gray-800">
<div class="">
<div>
<particles-bg v-if="$store.state.coinanimation" type="custom" :config="config" :bg="true" />
<component :is="$route.meta.requiresAuth ? 'DashboardLayout' : 'AuthLayoutGDD'" />
</div>

View File

@ -0,0 +1,103 @@
import { mount } from '@vue/test-utils'
import Navbar from './Navbar'
const localVue = global.localVue
const propsData = {
balance: 1234,
visible: false,
elopageUri: 'https://elopage.com',
}
const mocks = {
$i18n: {
locale: 'en',
},
$t: jest.fn((t) => t),
$store: {
state: {
hasElopage: false,
isAdmin: true,
},
},
}
describe('Navbar', () => {
let wrapper
const Wrapper = () => {
return mount(Navbar, { localVue, propsData, mocks })
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('renders the component', () => {
expect(wrapper.find('div.component-navbar').exists()).toBeTruthy()
})
describe('navigation Navbar', () => {
it('has .navbar-brand in the navbar', () => {
expect(wrapper.find('.navbar-brand').exists()).toBeTruthy()
})
it('has b-navbar-toggle in the navbar', () => {
expect(wrapper.find('.navbar-toggler').exists()).toBeTruthy()
})
it('has ten b-nav-item in the navbar', () => {
expect(wrapper.findAll('.nav-item')).toHaveLength(10)
})
it('has first nav-item "amount GDD" in navbar', () => {
expect(wrapper.findAll('.nav-item').at(1).text()).toEqual('1234 GDD')
})
it('has first nav-item "overview" in navbar', () => {
expect(wrapper.findAll('.nav-item').at(3).text()).toEqual('overview')
})
it('has first nav-item "send" in navbar', () => {
expect(wrapper.findAll('.nav-item').at(4).text()).toEqual('send')
})
it('has first nav-item "transactions" in navbar', () => {
expect(wrapper.findAll('.nav-item').at(5).text()).toEqual('transactions')
})
it('has first nav-item "my-profil" in navbar', () => {
expect(wrapper.findAll('.nav-item').at(6).text()).toEqual('site.navbar.my-profil')
})
it('has a link to the members area', () => {
expect(wrapper.findAll('.nav-item').at(7).text()).toContain('members_area')
expect(wrapper.findAll('.nav-item').at(7).find('a').attributes('href')).toBe(
'https://elopage.com',
)
})
it('has first nav-item "admin_area" in navbar', () => {
expect(wrapper.findAll('.nav-item').at(8).text()).toEqual('admin_area')
})
it('has first nav-item "logout" in navbar', () => {
expect(wrapper.findAll('.nav-item').at(9).text()).toEqual('logout')
})
})
})
describe('check watch visible true', () => {
beforeEach(async () => {
await wrapper.setProps({ visible: true })
})
it('has visibleCollapse == visible', () => {
expect(wrapper.vm.visibleCollapse).toBe(true)
})
})
describe('check watch visible false', () => {
beforeEach(async () => {
await wrapper.setProps({ visible: false })
})
it('has visibleCollapse == visible', () => {
expect(wrapper.vm.visibleCollapse).toBe(false)
})
})
})

View File

@ -0,0 +1,111 @@
<template>
<div class="component-navbar" style="background-color: #fff">
<b-navbar toggleable="lg" type="light" variant="faded">
<div class="navbar-brand">
<b-navbar-nav @click="$emit('set-visible', false)">
<b-nav-item to="/overview">
<img :src="logo" class="navbar-brand-img" alt="..." />
</b-nav-item>
</b-navbar-nav>
</div>
<b-navbar-nav class="ml-auto" is-nav>
<b-nav-item>{{ balance }} GDD</b-nav-item>
<b-nav-item to="/profile" right class="d-none d-sm-none d-md-none d-lg-flex shadow-lg">
<small>
{{ $store.state.firstName }} {{ $store.state.lastName }},
<b>{{ $store.state.email }}</b>
<b-icon class="ml-3" icon="gear-fill" aria-hidden="true"></b-icon>
</small>
</b-nav-item>
</b-navbar-nav>
<b-navbar-toggle
target="false"
@click="$emit('set-visible', (visibleCollapse = !visible))"
></b-navbar-toggle>
</b-navbar>
<b-collapse id="collapse-nav" v-model="visibleCollapse" class="p-3 b-collaps-gradido">
<b-nav vertical @click="$emit('set-visible', false)">
<div class="text-right">
<b-link to="/profile">
<small>
{{ $store.state.firstName }}
{{ $store.state.lastName }},
<b>{{ $store.state.email }}</b>
</small>
</b-link>
</div>
<b-nav-item to="/overview" class="mb-3">
{{ $t('overview') }}
</b-nav-item>
<b-nav-item to="/send" class="mb-3">{{ $t('send') }}</b-nav-item>
<b-nav-item to="/transactions" class="mb-3">
{{ $t('transactions') }}
</b-nav-item>
<b-nav-item to="/profile" class="mb-3">
<b-icon icon="gear-fill" aria-hidden="true"></b-icon>
{{ $t('site.navbar.my-profil') }}
</b-nav-item>
<br />
<b-nav-item :href="elopageUri" class="mb-3" target="_blank">
<b-icon icon="link45deg" aria-hidden="true"></b-icon>
{{ $t('members_area') }}
<b-badge v-if="!$store.state.hasElopage" pill variant="danger">!</b-badge>
</b-nav-item>
<b-nav-item class="mb-3" v-if="$store.state.isAdmin" @click="$emit('admin')">
<b-icon icon="link45deg" aria-hidden="true"></b-icon>
{{ $t('admin_area') }}
</b-nav-item>
<b-nav-item class="mb-3" @click="$emit('logout')">
<b-icon icon="power" aria-hidden="true"></b-icon>
{{ $t('logout') }}
</b-nav-item>
</b-nav>
</b-collapse>
</div>
</template>
<script>
export default {
name: 'navbar',
props: {
visible: {
type: Boolean,
required: true,
},
balance: {
type: Number,
required: true,
},
elopageUri: {
type: String,
required: false,
},
},
data() {
return {
logo: 'img/brand/green.png',
visibleCollapse: this.visible,
}
},
watch: {
visible() {
this.visibleCollapse = this.visible
},
},
}
</script>
<style>
.b-collaps-gradido {
position: absolute;
z-index: 100000;
background-color: #dfe0e3f5;
width: 100%;
box-shadow: #b4b4b4 0px 13px 22px;
font-size: large;
}
.b-collaps-gradido li :hover {
background-color: #e9e7e7f5;
}
</style>

View File

@ -0,0 +1,65 @@
import { mount } from '@vue/test-utils'
import Sidebar from './Sidebar.vue'
const localVue = global.localVue
describe('Sidebar', () => {
let wrapper
const mocks = {
$i18n: {
locale: 'en',
},
$t: jest.fn((t) => t),
$store: {
state: {
hasElopage: true,
isAdmin: true,
},
},
}
const Wrapper = () => {
return mount(Sidebar, { localVue, mocks })
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('renders the component', () => {
expect(wrapper.find('div#component-sidebar').exists()).toBeTruthy()
})
describe('navigation Navbar', () => {
it('has seven b-nav-item in the navbar', () => {
expect(wrapper.findAll('.nav-item')).toHaveLength(7)
})
it('has first nav-item "overview" in navbar', () => {
expect(wrapper.findAll('.nav-item').at(0).text()).toEqual('overview')
})
it('has first nav-item "send" in navbar', () => {
expect(wrapper.findAll('.nav-item').at(1).text()).toEqual('send')
})
it('has first nav-item "transactions" in navbar', () => {
expect(wrapper.findAll('.nav-item').at(2).text()).toEqual('transactions')
})
it('has first nav-item "my-profil" in navbar', () => {
expect(wrapper.findAll('.nav-item').at(3).text()).toEqual('site.navbar.my-profil')
})
it('has a link to the members area', () => {
expect(wrapper.findAll('.nav-item').at(4).text()).toContain('members_area')
expect(wrapper.findAll('.nav-item').at(4).find('a').attributes('href')).toBe('#')
})
it('has first nav-item "admin_area" in navbar', () => {
expect(wrapper.findAll('.nav-item').at(5).text()).toEqual('admin_area')
})
it('has first nav-item "logout" in navbar', () => {
expect(wrapper.findAll('.nav-item').at(6).text()).toEqual('logout')
})
})
})
})

View File

@ -0,0 +1,45 @@
<template>
<div id="component-sidebar">
<div class="pl-3">
<p></p>
<div class="mb-6">
<b-nav vertical class="w-200">
<b-nav-item to="/overview" class="mb-3" active>{{ $t('overview') }}</b-nav-item>
<b-nav-item to="/send" class="mb-3">{{ $t('send') }}</b-nav-item>
<b-nav-item to="/transactions" class="mb-3">{{ $t('transactions') }}</b-nav-item>
<b-nav-item to="/profile" class="mb-3">
<b-icon icon="gear-fill" aria-hidden="true"></b-icon>
{{ $t('site.navbar.my-profil') }}
</b-nav-item>
</b-nav>
<hr />
<b-nav vertical class="w-100">
<b-nav-item class="mb-3" :href="elopageUri" target="_blank">
<b-icon icon="link45deg" aria-hidden="true"></b-icon>
{{ $t('members_area') }}
<b-badge v-if="!$store.state.hasElopage" pill variant="danger">!</b-badge>
</b-nav-item>
<b-nav-item class="mb-3" v-if="$store.state.isAdmin" @click="$emit('admin')">
<b-icon icon="link45deg" aria-hidden="true"></b-icon>
{{ $t('admin_area') }}
</b-nav-item>
<b-nav-item class="mb-3" @click="$emit('logout')">
<b-icon icon="power" aria-hidden="true"></b-icon>
{{ $t('logout') }}
</b-nav-item>
</b-nav>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'sidebar',
props: {
elopageUri: {
type: String,
required: false,
},
},
}
</script>

View File

@ -1,21 +0,0 @@
<template>
<button
type="button"
class="navbar-toggler collapsed"
data-toggle="collapse"
data-target="#navbar"
aria-controls="navbarSupportedContent"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-bar bar1"></span>
<span class="navbar-toggler-bar bar2"></span>
<span class="navbar-toggler-bar bar3"></span>
</button>
</template>
<script>
export default {
name: 'navbar-toggle-button',
}
</script>
<style></style>

View File

@ -1,32 +0,0 @@
<template>
<button
class="navbar-toggler"
type="button"
data-toggle="collapse"
:data-target="target"
:aria-controls="target"
:aria-expanded="toggled"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
<slot>
<span></span>
</slot>
</button>
</template>
<script>
export default {
props: {
target: {
type: [String, Number],
description: 'Button target element',
},
toggled: {
type: Boolean,
default: false,
description: 'Whether button is toggled',
},
},
}
</script>
<style></style>

View File

@ -1,200 +0,0 @@
import { mount, RouterLinkStub } from '@vue/test-utils'
import SideBar from './SideBar'
const localVue = global.localVue
const storeDispatchMock = jest.fn()
describe('SideBar', () => {
let wrapper
const stubs = {
RouterLink: RouterLinkStub,
}
const propsData = {
balance: 1234.56,
}
const mocks = {
$store: {
state: {
email: 'test@example.org',
publisherId: 123,
firstName: 'test',
lastName: 'example',
hasElopage: false,
},
dispatch: storeDispatchMock,
},
$i18n: {
locale: 'en',
},
$t: jest.fn((t) => t),
$n: jest.fn((n) => n),
}
const Wrapper = () => {
return mount(SideBar, { localVue, mocks, stubs, propsData })
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('renders the component', () => {
expect(wrapper.find('#sidenav-main').exists()).toBeTruthy()
})
describe('navbar button', () => {
it('has a navbar button', () => {
expect(wrapper.find('button.navbar-toggler').exists()).toBeTruthy()
})
it('calls showSidebar when clicked', async () => {
const spy = jest.spyOn(wrapper.vm.$sidebar, 'displaySidebar')
wrapper.find('button.navbar-toggler').trigger('click')
await wrapper.vm.$nextTick()
expect(spy).toHaveBeenCalledWith(true)
})
})
describe('balance', () => {
it('shows em-dash as balance while loading', () => {
expect(wrapper.find('div.row.text-center').text()).toBe('— GDD')
})
it('shows the when loaded', async () => {
wrapper.setProps({
pending: false,
})
await wrapper.vm.$nextTick()
expect(wrapper.find('div.row.text-center').text()).toBe('1234.56 GDD')
})
})
describe('close siedbar', () => {
it('calls closeSidebar when clicked', async () => {
const spy = jest.spyOn(wrapper.vm.$sidebar, 'displaySidebar')
wrapper.find('#sidenav-collapse-main').find('button.navbar-toggler').trigger('click')
await wrapper.vm.$nextTick()
expect(spy).toHaveBeenCalledWith(false)
})
})
describe('static menu items', () => {
describe("member's area without publisher ID", () => {
it('has a link to the elopage', () => {
expect(wrapper.findAll('li').at(0).text()).toContain('members_area')
})
it('has a badge', () => {
expect(wrapper.findAll('li').at(0).text()).toContain('!')
})
it('links to the elopage registration', () => {
expect(wrapper.findAll('li').at(0).find('a').attributes('href')).toBe(
'https://elopage.com/s/gradido/basic-de/payment?locale=en&prid=111&pid=123&firstName=test&lastName=example&email=test@example.org',
)
})
describe('with locale="de"', () => {
beforeEach(() => {
mocks.$i18n.locale = 'de'
})
it('links to the German elopage registration when locale is set to de', () => {
expect(wrapper.findAll('li').at(0).find('a').attributes('href')).toBe(
'https://elopage.com/s/gradido/basic-de/payment?locale=de&prid=111&pid=123&firstName=test&lastName=example&email=test@example.org',
)
})
})
describe("member's area with publisher ID", () => {
beforeEach(() => {
mocks.$store.state.hasElopage = true
})
it('links to the elopage member area', () => {
expect(wrapper.findAll('li').at(0).find('a').attributes('href')).toBe(
'https://elopage.com/s/gradido/sign_in?locale=de',
)
})
it('has no badge', () => {
expect(wrapper.findAll('li').at(0).text()).not.toContain('!')
})
})
describe("member's area with default publisher ID and no elopage", () => {
beforeEach(() => {
mocks.$store.state.publisherId = null
mocks.$store.state.hasElopage = false
})
it('links to the elopage member area with default publisher ID', () => {
expect(wrapper.findAll('li').at(0).find('a').attributes('href')).toBe(
'https://elopage.com/s/gradido/basic-de/payment?locale=de&prid=111&pid=2896&firstName=test&lastName=example&email=test@example.org',
)
})
it('has a badge', () => {
expect(wrapper.findAll('li').at(0).text()).toContain('!')
})
})
})
describe('logout', () => {
it('has a logout button', () => {
expect(wrapper.findAll('li').at(1).text()).toBe('logout')
})
it('emits logout when logout is clicked', async () => {
wrapper.findAll('li').at(1).find('a').trigger('click')
await wrapper.vm.$nextTick()
expect(wrapper.emitted('logout')).toEqual([[]])
})
})
describe('admin-area', () => {
it('is not visible when not an admin', () => {
expect(wrapper.findAll('li').at(1).text()).not.toBe('admin_area')
})
describe('logged in as admin', () => {
const assignLocationSpy = jest.fn()
beforeEach(async () => {
mocks.$store.state.isAdmin = true
mocks.$store.state.token = 'valid-token'
delete window.location
window.location = {
assign: assignLocationSpy,
}
wrapper = Wrapper()
})
it('is visible', () => {
expect(wrapper.findAll('li').at(1).text()).toBe('admin_area')
})
describe('click on admin area', () => {
beforeEach(async () => {
await wrapper.findAll('li').at(1).find('a').trigger('click')
})
it('opens a new window when clicked', () => {
expect(assignLocationSpy).toHaveBeenCalledWith(
'http://localhost/admin/authenticate?token=valid-token',
)
})
it('dispatches logout to store', () => {
expect(storeDispatchMock).toHaveBeenCalledWith('logout')
})
})
})
})
})
})
})

View File

@ -1,145 +0,0 @@
<template>
<nav
class="navbar navbar-vertical fixed-left navbar-expand-md navbar-light bg-transparent"
id="sidenav-main"
>
<div class="container-fluid">
<!--Toggler-->
<navbar-toggle-button @click.native="showSidebar"></navbar-toggle-button>
<div class="navbar-brand">
<img :src="logo" class="navbar-brand-img" alt="..." />
</div>
<b-row class="text-center">
<b-col>{{ pending ? '—' : $n(balance, 'decimal') }} GDD</b-col>
</b-row>
<slot name="mobile-right">
<ul class="nav align-items-center d-md-none">
<div class="media align-items-center">
<span class="avatar avatar-sm">
<vue-qrcode
v-if="$store.state.email"
:value="$store.state.email"
type="image/png"
></vue-qrcode>
</span>
</div>
</ul>
</slot>
<slot></slot>
<div
v-show="$sidebar.showSidebar"
class="navbar-collapse collapse show"
id="sidenav-collapse-main"
>
<div class="navbar-collapse-header d-md-none">
<div class="row">
<div class="col-6 collapse-brand">
<img :src="logo" />
</div>
<div class="col-6 collapse-close">
<navbar-toggle-button @click.native="closeSidebar"></navbar-toggle-button>
</div>
</div>
</div>
<ul class="navbar-nav">
<slot name="links"></slot>
</ul>
<hr class="my-2" />
<ul class="navbar-nav ml-3">
<li class="nav-item">
<a :href="getElopageLink()" class="nav-link" target="_blank">
{{ $t('members_area') }}&nbsp;
<b-badge v-if="!$store.state.hasElopage" pill variant="danger">!</b-badge>
</a>
</li>
</ul>
<ul class="navbar-nav ml-3" v-if="$store.state.isAdmin">
<li class="nav-item">
<a class="nav-link pointer" @click="admin">
{{ $t('admin_area') }}
</a>
</li>
</ul>
<ul class="navbar-nav ml-3">
<li class="nav-item">
<a class="nav-link pointer" @click="logout">
{{ $t('logout') }}
</a>
</li>
</ul>
</div>
</div>
</nav>
</template>
<script>
import NavbarToggleButton from '@/components/NavbarToggleButton'
import VueQrcode from 'vue-qrcode'
import CONFIG from '../../config'
export default {
name: 'sidebar',
components: {
NavbarToggleButton,
VueQrcode,
},
props: {
logo: {
type: String,
default: 'img/brand/green.png',
description: 'Gradido Sidebar app logo',
},
value: { type: String },
autoClose: {
type: Boolean,
default: true,
description: 'Whether sidebar should autoclose on mobile when clicking an item',
},
balance: {
type: Number,
default: 0,
},
pending: {
type: Boolean,
default: true,
},
},
provide() {
return {
autoClose: this.autoClose,
}
},
methods: {
closeSidebar() {
this.$sidebar.displaySidebar(false)
},
showSidebar() {
this.$sidebar.displaySidebar(true)
},
logout() {
this.$emit('logout')
},
admin() {
window.location.assign(CONFIG.ADMIN_AUTH_URL.replace('$1', this.$store.state.token))
this.$store.dispatch('logout') // logout without redirect
},
getElopageLink() {
const pId = this.$store.state.publisherId
? this.$store.state.publisherId
: CONFIG.DEFAULT_PUBLISHER_ID
return encodeURI(
this.$store.state.hasElopage
? `https://elopage.com/s/gradido/sign_in?locale=${this.$i18n.locale}`
: `https://elopage.com/s/gradido/basic-de/payment?locale=${this.$i18n.locale}&prid=111&pid=${pId}&firstName=${this.$store.state.firstName}&lastName=${this.$store.state.lastName}&email=${this.$store.state.email}`,
)
},
},
}
</script>
<style>
.pointer {
cursor: pointer;
}
</style>

View File

@ -1,189 +0,0 @@
<template>
<b-nav-item
:is="baseComponent"
:to="link.path ? link.path : '/'"
class="nav-item"
:class="{ active: isActive }"
>
<a
v-if="isMenu"
class="sidebar-menu-item nav-link"
:class="{ active: isActive }"
:aria-expanded="!collapsed"
data-toggle="collapse"
@click.prevent="collapseMenu"
>
<template v-if="addLink">
<span class="nav-link-text">
{{ link.name }}
<b class="caret"></b>
</span>
</template>
<template v-else>
<i :class="link.icon"></i>
<span class="nav-link-text">
{{ link.name }}
<b class="caret"></b>
</span>
</template>
</a>
<collapse-transition>
<div v-if="$slots.default || this.isMenu" v-show="!collapsed" class="collapse show">
<ul class="nav nav-sm flex-column">
<slot></slot>
</ul>
</div>
</collapse-transition>
<slot name="title" v-if="children.length === 0 && !$slots.default && link.path">
<component
:to="link.path"
@click.native="linkClick"
:is="elementType(link, false)"
class="nav-link"
:class="{ active: link.active }"
:target="link.target"
:href="link.path"
>
<template v-if="addLink">
<span class="nav-link-text">{{ link.name }}</span>
</template>
<template v-else>
<i :class="link.icon"></i>
<span class="nav-link-text">{{ link.name }}</span>
</template>
</component>
</slot>
</b-nav-item>
</template>
<script>
import { CollapseTransition } from 'vue2-transitions'
export default {
name: 'sidebar-item',
components: {
CollapseTransition,
},
props: {
menu: {
type: Boolean,
default: false,
description:
"Whether the item is a menu. Most of the item it's not used and should be used only if you want to override the default behavior.",
},
link: {
type: Object,
default: () => {
return {
name: '',
path: '',
children: [],
}
},
description:
'Sidebar link. Can contain name, path, icon and other attributes. See examples for more info',
},
},
provide() {
return {
addLink: this.addChild,
removeLink: this.removeChild,
}
},
inject: {
addLink: { default: null },
removeLink: { default: null },
autoClose: {
default: true,
},
},
data() {
return {
children: [],
collapsed: true,
}
},
computed: {
baseComponent() {
return this.isMenu || this.link.isRoute ? 'li' : 'router-link'
},
linkPrefix() {
if (this.link.name) {
const words = this.link.name.split(' ')
return words.map((word) => word.substring(0, 1)).join('')
}
return ''
},
isMenu() {
return this.children.length > 0 || this.menu === true
},
isActive() {
if (this.$route && this.$route.path) {
const matchingRoute = this.children.find((c) => this.$route.path.startsWith(c.link.path))
if (matchingRoute !== undefined) {
return true
}
}
return false
},
},
methods: {
addChild(item) {
const index = this.$slots.default.indexOf(item.$vnode)
this.children.splice(index, 0, item)
},
removeChild(item) {
const tabs = this.children
const index = tabs.indexOf(item)
tabs.splice(index, 1)
},
elementType(link, isParent = true) {
if (link.isRoute === false) {
return isParent ? 'li' : 'a'
} else {
return 'router-link'
}
},
linkAbbreviation(name) {
const matches = name.match(/\b(\w)/g)
return matches.join('')
},
linkClick() {
if (this.autoClose && this.$sidebar && this.$sidebar.showSidebar === true) {
this.$sidebar.displaySidebar(false)
}
},
collapseMenu() {
this.collapsed = !this.collapsed
},
collapseSubMenu(link) {
link.collapsed = !link.collapsed
},
},
mounted() {
if (this.addLink) {
this.addLink(this)
}
if (this.link.collapsed !== undefined) {
this.collapsed = this.link.collapsed
}
if (this.isActive && this.isMenu) {
this.collapsed = false
}
},
destroyed() {
if (this.$el && this.$el.parentNode) {
this.$el.parentNode.removeChild(this.$el)
}
if (this.removeLink) {
this.removeLink(this)
}
},
}
</script>
<style>
.sidebar-menu-item {
cursor: pointer;
}
</style>

View File

@ -1,41 +0,0 @@
import Sidebar from './SideBar.vue'
import SidebarItem from './SidebarItem.vue'
const SidebarStore = {
showSidebar: false,
sidebarLinks: [],
isMinimized: false,
displaySidebar(value) {
this.showSidebar = value
},
toggleMinimize() {
document.body.classList.toggle('sidebar-mini')
const simulateWindowResize = setInterval(() => {
window.dispatchEvent(new Event('resize'))
}, 180)
setTimeout(() => {
clearInterval(simulateWindowResize)
}, 1000)
this.isMinimized = !this.isMinimized
},
}
const SidebarPlugin = {
install(Vue, options) {
if (options && options.sidebarLinks) {
SidebarStore.sidebarLinks = options.sidebarLinks
}
const app = new Vue({
data: {
sidebarStore: SidebarStore,
},
})
Vue.prototype.$sidebar = app.sidebarStore
Vue.component('side-bar', Sidebar)
Vue.component('sidebar-item', SidebarItem)
},
}
export default SidebarPlugin

View File

@ -1,5 +0,0 @@
import NavbarToggleButton from './Navbar/NavbarToggleButton'
import SidebarPlugin from './SidebarPlugin'
export { SidebarPlugin, NavbarToggleButton }

View File

@ -1,6 +1,5 @@
import GlobalComponents from './globalComponents'
import GlobalDirectives from './globalDirectives'
import SideBar from '@/components/SidebarPlugin'
import Toasted from 'vue-toasted'
@ -28,7 +27,6 @@ export default {
install(Vue) {
Vue.use(GlobalComponents)
Vue.use(GlobalDirectives)
Vue.use(SideBar)
Vue.use(BootstrapVue)
Vue.use(IconsPlugin)
Vue.use(VueMoment)

View File

@ -27,7 +27,7 @@ describe('dashboard plugin', () => {
})
describe('vue toasted', () => {
const toastedAction = vueUseMock.mock.calls[11][1].action.onClick
const toastedAction = vueUseMock.mock.calls[10][1].action.onClick
const goAwayMock = jest.fn()
const toastObject = {
goAway: goAwayMock,

View File

@ -72,8 +72,12 @@ describe('DashboardLayoutGdd', () => {
wrapper = Wrapper()
})
it('has a navbar', () => {
expect(wrapper.find('.main-navbar').exists()).toBeTruthy()
})
it('has a sidebar', () => {
expect(wrapper.find('nav#sidenav-main').exists()).toBeTruthy()
expect(wrapper.find('.main-sidebar').exists()).toBeTruthy()
})
it('has a main content div', () => {
@ -81,64 +85,10 @@ describe('DashboardLayoutGdd', () => {
})
it('has a footer inside the main content', () => {
expect(wrapper.find('div.main-content').find('footer.footer').exists()).toBeTruthy()
expect(wrapper.find('div.main-page').find('footer.footer').exists()).toBeTruthy()
})
describe('navigation bar', () => {
let navbar
beforeEach(() => {
navbar = wrapper.findAll('ul.navbar-nav').at(0)
})
it('has four items in the navbar', () => {
expect(navbar.findAll('ul > a')).toHaveLength(4)
})
it('has first item "overview" in navbar', () => {
expect(navbar.findAll('ul > a').at(0).text()).toEqual('overview')
})
it('has first item "overview" linked to overview in navbar', () => {
expect(navbar.findAll('ul > a > a').at(0).attributes('href')).toBe('/overview')
})
it('has second item "send" in navbar', () => {
expect(navbar.findAll('ul > a').at(1).text()).toEqual('send')
})
it('has second item "send" linked to /send in navbar', () => {
expect(wrapper.findAll('ul > a > a').at(1).attributes('href')).toBe('/send')
})
it('has third item "transactions" in navbar', () => {
expect(navbar.findAll('ul > a').at(2).text()).toEqual('transactions')
})
it('has third item "transactions" linked to transactions in navbar', async () => {
expect(wrapper.findAll('ul > a > a').at(2).attributes('href')).toBe('/transactions')
})
it('has fourth item "My profile" in navbar', () => {
expect(navbar.findAll('ul > a').at(3).text()).toEqual('site.navbar.my-profil')
})
it('has fourth item "My profile" linked to profile in navbar', async () => {
expect(wrapper.findAll('ul > a > a').at(3).attributes('href')).toBe('/profile')
})
it('has a link to the members area', () => {
expect(wrapper.findAll('ul').at(2).text()).toContain('members_area')
expect(wrapper.findAll('ul').at(2).text()).toContain('!')
expect(wrapper.findAll('ul').at(2).find('a').attributes('href')).toBe(
'https://elopage.com/s/gradido/basic-de/payment?locale=en&prid=111&pid=123&firstName=User&lastName=Example&email=user@example.org',
)
})
it('has a logout button', () => {
expect(wrapper.findAll('ul').at(3).text()).toBe('logout')
})
describe('logout', () => {
beforeEach(async () => {
await apolloMock.mockResolvedValue({

View File

@ -1,95 +1,64 @@
<template>
<div>
<side-bar @logout="logout" :balance="balance" :pending="pending">
<template slot="links">
<sidebar-item
:link="{
name: $t('overview'),
path: '/overview',
}"
></sidebar-item>
<sidebar-item
:link="{
name: $t('send'),
path: '/send',
}"
></sidebar-item>
<sidebar-item
:link="{
name: $t('transactions'),
path: '/transactions',
}"
></sidebar-item>
<sidebar-item
:link="{
name: $t('site.navbar.my-profil'),
path: '/profile',
}"
></sidebar-item>
</template>
</side-bar>
<div class="main-content" style="max-width: 1000px">
<div class="d-none d-md-block">
<b-navbar>
<b-navbar-nav class="ml-auto">
<b-nav-item>
<b-media no-body class="align-items-center">
<span class="pb-2 text-lg font-weight-bold">
{{ $store.state.email }}
</span>
<b-media-body class="ml-2">
<span class="avatar">
<vue-qrcode
v-if="$store.state.email"
:value="$store.state.email"
type="image/png"
></vue-qrcode>
</span>
</b-media-body>
</b-media>
</b-nav-item>
</b-navbar-nav>
</b-navbar>
<navbar
class="main-navbar"
:balance="balance"
:visible="visible"
:elopageUri="elopageUri"
@set-visible="setVisible"
@admin="admin"
@logout="logout"
/>
<div class="content-gradido">
<div class="d-none d-sm-none d-md-none d-lg-flex shadow-lg" style="width: 300px">
<sidebar class="main-sidebar" :elopageUri="elopageUri" @admin="admin" @logout="logout" />
</div>
<div @click="$sidebar.displaySidebar(false)">
<fade-transition :duration="200" origin="center top" mode="out-in">
<router-view
ref="router-view"
:balance="balance"
:gdt-balance="GdtBalance"
:transactions="transactions"
:transactionCount="transactionCount"
:pending="pending"
@update-balance="updateBalance"
@update-transactions="updateTransactions"
></router-view>
</fade-transition>
<div class="main-page ml-2 mr-2" style="width: 100%" @click="visible = false">
<div class="main-content">
<fade-transition :duration="200" origin="center top" mode="out-in">
<router-view
ref="router-view"
:balance="balance"
:gdt-balance="GdtBalance"
:transactions="transactions"
:transactionCount="transactionCount"
:pending="pending"
@update-balance="updateBalance"
@update-transactions="updateTransactions"
></router-view>
</fade-transition>
</div>
<content-footer v-if="!$route.meta.hideFooter"></content-footer>
</div>
<content-footer v-if="!$route.meta.hideFooter"></content-footer>
</div>
</div>
</template>
<script>
import Navbar from '../../components/Menu/Navbar.vue'
import Sidebar from '../../components/Menu/Sidebar.vue'
import { logout, transactionsQuery } from '../../graphql/queries'
import ContentFooter from './ContentFooter.vue'
import { FadeTransition } from 'vue2-transitions'
import VueQrcode from 'vue-qrcode'
import CONFIG from '../../config'
export default {
components: {
Navbar,
Sidebar,
ContentFooter,
VueQrcode,
FadeTransition,
},
data() {
return {
logo: 'img/brand/green.png',
balance: 0,
GdtBalance: 0,
transactions: [],
bookedBalance: 0,
transactionCount: 0,
pending: true,
visible: false,
}
},
methods: {
@ -99,12 +68,10 @@ export default {
query: logout,
})
.then(() => {
this.$sidebar.displaySidebar(false)
this.$store.dispatch('logout')
this.$router.push('/login')
})
.catch(() => {
this.$sidebar.displaySidebar(false)
this.$store.dispatch('logout')
if (this.$router.currentRoute.path !== '/login') this.$router.push('/login')
})
@ -140,6 +107,40 @@ export default {
updateBalance(ammount) {
this.balance -= ammount
},
admin() {
window.location.assign(CONFIG.ADMIN_AUTH_URL.replace('$1', this.$store.state.token))
this.$store.dispatch('logout') // logout without redirect
},
setVisible(bool) {
this.visible = bool
},
},
computed: {
elopageUri() {
const pId = this.$store.state.publisherId
? this.$store.state.publisherId
: CONFIG.DEFAULT_PUBLISHER_ID
return encodeURI(
this.$store.state.hasElopage
? `https://elopage.com/s/gradido/sign_in?locale=${this.$i18n.locale}`
: `https://elopage.com/s/gradido/basic-de/payment?locale=${this.$i18n.locale}&prid=111&pid=${pId}&firstName=${this.$store.state.firstName}&lastName=${this.$store.state.lastName}&email=${this.$store.state.email}`,
)
},
},
}
</script>
<style>
.content-gradido {
display: inline-flex;
width: 100%;
height: 91%;
position: absolute;
}
.navbar-brand-img {
height: 2rem;
padding-left: 10px;
}
.bg-lightgrey {
background-color: #f0f0f0;
}
</style>

View File

@ -1,10 +1,10 @@
<template>
<div>
<b-container fluid>
<div>
<b-row>
<b-col class="col-6">
<b-row>
<b-col class="col-11 bg-gray text-white p-3">
<b-col class="col-11 ml-2 p-3 bg-lightgrey">
<status
class="gdd-status-gdd"
:pending="pending"
@ -14,9 +14,9 @@
</b-col>
</b-row>
</b-col>
<b-col class="col-6 text-right">
<b-col class="col-6 text-right bg-lightgrey">
<b-row>
<b-col class="bg-white text-gray p-3">
<b-col class="p-3">
<status
class="gdd-status-gdt"
:pending="pending"
@ -36,7 +36,7 @@
@update-transactions="updateTransactions"
/>
<gdd-transaction-list-footer :count="transactionCount" />
</b-container>
</div>
</div>
</template>
<script>

View File

@ -38,10 +38,6 @@ describe('SendOverview', () => {
wrapper = Wrapper()
})
it('has a status GDD line gdd-status-gdd', () => {
expect(wrapper.find('div.gdd-status-gdd').exists()).toBeTruthy()
})
it('has a send field', () => {
expect(wrapper.find('div.gdd-send').exists()).toBeTruthy()
})

View File

@ -1,19 +1,6 @@
<template>
<div>
<b-container fluid>
<b-row>
<b-col class="bg-gray text-white text-center p-3">
<status
class="gdd-status-gdd"
v-if="showContext"
:pending="pending"
:balance="balance"
status-text="GDD"
/>
</b-col>
</b-row>
<br />
<b-container>
<gdd-send :currentTransactionStep="currentTransactionStep">
<template #transaction-form>
<transaction-form :balance="balance" @set-transaction="setTransaction"></transaction-form>
@ -41,9 +28,7 @@
</div>
</template>
<script>
import Status from '../../components/Status.vue'
import GddSend from './SendOverview/GddSend.vue'
import TransactionForm from './SendOverview/GddSend/TransactionForm.vue'
import TransactionConfirmation from './SendOverview/GddSend/TransactionConfirmation.vue'
import TransactionResult from './SendOverview/GddSend/TransactionResult.vue'
@ -58,7 +43,6 @@ const EMPTY_TRANSACTION_DATA = {
export default {
name: 'SendOverview',
components: {
Status,
GddSend,
TransactionForm,

View File

@ -8,7 +8,6 @@ import * as rules from 'vee-validate/dist/rules'
import { messages } from 'vee-validate/dist/locale/en.json'
import RegeneratorRuntime from 'regenerator-runtime'
import SideBar from '@/components/SidebarPlugin'
import VueQrcode from 'vue-qrcode'
import VueMoment from 'vue-moment'
@ -41,7 +40,6 @@ global.localVue.use(BootstrapVue)
global.localVue.use(Vuex)
global.localVue.use(IconsPlugin)
global.localVue.use(RegeneratorRuntime)
global.localVue.use(SideBar)
global.localVue.use(VueQrcode)
global.localVue.use(VueMoment)
global.localVue.component('validation-provider', ValidationProvider)