diff --git a/CHANGELOG.md b/CHANGELOG.md index 754566658..9ce354b1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,44 @@ 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.14.1](https://github.com/gradido/gradido/compare/1.14.0...1.14.1) + +- fix(frontend): load contributionMessages is fixed [`#2390`](https://github.com/gradido/gradido/pull/2390) + +#### [1.14.0](https://github.com/gradido/gradido/compare/1.13.3...1.14.0) + +> 14 November 2022 + +- chore(release): version 1.14.0 [`#2389`](https://github.com/gradido/gradido/pull/2389) +- fix(frontend): close all open collapse by change tabs in community [`#2388`](https://github.com/gradido/gradido/pull/2388) +- fix(backend): corrected E-Mail texts [`#2386`](https://github.com/gradido/gradido/pull/2386) +- fix(frontend): better history messages [`#2381`](https://github.com/gradido/gradido/pull/2381) +- fix(frontend): mailto link [`#2383`](https://github.com/gradido/gradido/pull/2383) +- fix(admin): fix text in admin area to uppercase [`#2365`](https://github.com/gradido/gradido/pull/2365) +- feat(frontend): move the information about gradido being free to the auth layout [`#2349`](https://github.com/gradido/gradido/pull/2349) +- fix(admin): load error fixed for contribution link [`#2364`](https://github.com/gradido/gradido/pull/2364) +- fix(admin): edit contribution link does not take old values [`#2362`](https://github.com/gradido/gradido/pull/2362) +- fix(other): corrected dockerfile descriptions [`#2346`](https://github.com/gradido/gradido/pull/2346) +- feat(backend): 🍰 Send email for rejected contributions [`#2340`](https://github.com/gradido/gradido/pull/2340) +- feat(admin): edit automatic contribution link [`#2309`](https://github.com/gradido/gradido/pull/2309) +- refactor(backend): fix logger mocks [`#2308`](https://github.com/gradido/gradido/pull/2308) +- fix(admin): update contribution list after admin updates contribution [`#2330`](https://github.com/gradido/gradido/pull/2330) +- fix(frontend): inconsistent labeling on login register [`#2350`](https://github.com/gradido/gradido/pull/2350) +- feat(backend): setup hyperswarm [`#1874`](https://github.com/gradido/gradido/pull/1874) +- feat(other): lint pull request workflow [`#2338`](https://github.com/gradido/gradido/pull/2338) +- Feature: 🍰 add updated at to contributions [`#2237`](https://github.com/gradido/gradido/pull/2237) +- Refactor: GitHub test workflow - disable video recording and reduce wait time [`#2336`](https://github.com/gradido/gradido/pull/2336) +- 2274 feature concept manuel user registration for admins [`#2289`](https://github.com/gradido/gradido/pull/2289) +- 1574 concept to introduce gradidoID and change password encryption [`#2252`](https://github.com/gradido/gradido/pull/2252) +- contributionlink stage-2 and stage-3 of capturing and activation [`#2241`](https://github.com/gradido/gradido/pull/2241) +- Github workflow: update actions to the current API version using Node v 16 [`#2323`](https://github.com/gradido/gradido/pull/2323) +- feature: Fullstack tests in GitHub workflow [`#2319`](https://github.com/gradido/gradido/pull/2319) + #### [1.13.3](https://github.com/gradido/gradido/compare/1.13.2...1.13.3) +> 1 November 2022 + +- release: Version 1.13.3 [`#2322`](https://github.com/gradido/gradido/pull/2322) - 2294 contribution links on its own page [`#2312`](https://github.com/gradido/gradido/pull/2312) - fix: Change Orange Color [`#2302`](https://github.com/gradido/gradido/pull/2302) - fix: Release Statistic Query Runner [`#2320`](https://github.com/gradido/gradido/pull/2320) diff --git a/admin/package.json b/admin/package.json index 82a2413de..7f0e7ffd5 100644 --- a/admin/package.json +++ b/admin/package.json @@ -3,7 +3,7 @@ "description": "Administraion Interface for Gradido", "main": "index.js", "author": "Moriz Wahl", - "version": "1.13.3", + "version": "1.14.1", "license": "Apache-2.0", "private": false, "scripts": { diff --git a/admin/src/plugins/apolloProvider.js b/admin/src/plugins/apolloProvider.js index 95b7aab7e..8b02013f4 100644 --- a/admin/src/plugins/apolloProvider.js +++ b/admin/src/plugins/apolloProvider.js @@ -10,7 +10,7 @@ const authLink = new ApolloLink((operation, forward) => { operation.setContext({ headers: { Authorization: token && token.length > 0 ? `Bearer ${token}` : '', - clientRequestTime: new Date().toString(), + clientTimezoneOffset: new Date().getTimezoneOffset(), }, }) return forward(operation).map((response) => { diff --git a/admin/src/plugins/apolloProvider.test.js b/admin/src/plugins/apolloProvider.test.js index 7889c3318..483862bea 100644 --- a/admin/src/plugins/apolloProvider.test.js +++ b/admin/src/plugins/apolloProvider.test.js @@ -94,7 +94,7 @@ describe('apolloProvider', () => { expect(setContextMock).toBeCalledWith({ headers: { Authorization: 'Bearer some-token', - clientRequestTime: expect.any(String), + clientTimezoneOffset: expect.any(Number), }, }) }) @@ -110,7 +110,7 @@ describe('apolloProvider', () => { expect(setContextMock).toBeCalledWith({ headers: { Authorization: '', - clientRequestTime: expect.any(String), + clientTimezoneOffset: expect.any(Number), }, }) }) diff --git a/backend/package.json b/backend/package.json index 80db9ff9d..3e26225bf 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "gradido-backend", - "version": "1.13.3", + "version": "1.14.1", "description": "Gradido unified backend providing an API-Service for Gradido Transactions", "main": "src/index.ts", "repository": "https://github.com/gradido/gradido/backend", diff --git a/backend/src/graphql/resolver/AdminResolver.test.ts b/backend/src/graphql/resolver/AdminResolver.test.ts index f26fce3d8..503bab472 100644 --- a/backend/src/graphql/resolver/AdminResolver.test.ts +++ b/backend/src/graphql/resolver/AdminResolver.test.ts @@ -2,7 +2,7 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import { objectValuesToArray } from '@/util/utilities' -import { testEnvironment, resetToken, cleanDB } from '@test/helpers' +import { testEnvironment, resetToken, cleanDB, contributionDateFormatter } from '@test/helpers' import { userFactory } from '@/seeds/factory/user' import { creationFactory } from '@/seeds/factory/creation' import { creations } from '@/seeds/creation/index' @@ -83,6 +83,12 @@ let user: User let creation: Contribution | void let result: any +describe('contributionDateFormatter', () => { + it('formats the date correctly', () => { + expect(contributionDateFormatter(new Date('Thu Feb 29 2024 13:12:11'))).toEqual('2/29/2024') + }) +}) + describe('AdminResolver', () => { describe('set user role', () => { describe('unauthenticated', () => { @@ -751,7 +757,7 @@ describe('AdminResolver', () => { email: 'bibi@bloxberg.de', amount: new Decimal(300), memo: 'Danke Bibi!', - creationDate: new Date().toString(), + creationDate: contributionDateFormatter(new Date()), }, }), ).resolves.toEqual( @@ -861,7 +867,7 @@ describe('AdminResolver', () => { email: 'bibi@bloxberg.de', amount: new Decimal(300), memo: 'Danke Bibi!', - creationDate: new Date().toString(), + creationDate: contributionDateFormatter(new Date()), }, }), ).resolves.toEqual( @@ -936,19 +942,25 @@ describe('AdminResolver', () => { }) describe('adminCreateContribution', () => { + const now = new Date() + beforeAll(async () => { - const now = new Date() creation = await creationFactory(testEnv, { email: 'peter@lustig.de', amount: 400, memo: 'Herzlich Willkommen bei Gradido!', - creationDate: new Date(now.getFullYear(), now.getMonth() - 1, 1).toISOString(), + creationDate: contributionDateFormatter( + new Date(now.getFullYear(), now.getMonth() - 1, 1), + ), }) }) describe('user to create for does not exist', () => { it('throws an error', async () => { jest.clearAllMocks() + variables.creationDate = contributionDateFormatter( + new Date(now.getFullYear(), now.getMonth() - 1, 1), + ) await expect( mutate({ mutation: adminCreateContribution, variables }), ).resolves.toEqual( @@ -969,6 +981,9 @@ describe('AdminResolver', () => { beforeAll(async () => { user = await userFactory(testEnv, stephenHawking) variables.email = 'stephen@hawking.uk' + variables.creationDate = contributionDateFormatter( + new Date(now.getFullYear(), now.getMonth() - 1, 1), + ) }) it('throws an error', async () => { @@ -995,6 +1010,9 @@ describe('AdminResolver', () => { beforeAll(async () => { user = await userFactory(testEnv, garrickOllivander) variables.email = 'garrick@ollivander.com' + variables.creationDate = contributionDateFormatter( + new Date(now.getFullYear(), now.getMonth() - 1, 1), + ) }) it('throws an error', async () => { @@ -1021,6 +1039,7 @@ describe('AdminResolver', () => { beforeAll(async () => { user = await userFactory(testEnv, bibiBloxberg) variables.email = 'bibi@bloxberg.de' + variables.creationDate = 'invalid-date' }) describe('date of creation is not a date string', () => { @@ -1030,30 +1049,22 @@ describe('AdminResolver', () => { mutate({ mutation: adminCreateContribution, variables }), ).resolves.toEqual( expect.objectContaining({ - errors: [ - new GraphQLError('No information for available creations for the given date'), - ], + errors: [new GraphQLError(`invalid Date for creationDate=invalid-date`)], }), ) }) it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith( - 'No information for available creations with the given creationDate=', - 'Invalid Date', - ) + expect(logger.error).toBeCalledWith(`invalid Date for creationDate=invalid-date`) }) }) describe('date of creation is four months ago', () => { it('throws an error', async () => { jest.clearAllMocks() - const now = new Date() - variables.creationDate = new Date( - now.getFullYear(), - now.getMonth() - 4, - 1, - ).toString() + variables.creationDate = contributionDateFormatter( + new Date(now.getFullYear(), now.getMonth() - 4, 1), + ) await expect( mutate({ mutation: adminCreateContribution, variables }), ).resolves.toEqual( @@ -1068,7 +1079,7 @@ describe('AdminResolver', () => { it('logs the error thrown', () => { expect(logger.error).toBeCalledWith( 'No information for available creations with the given creationDate=', - variables.creationDate, + new Date(variables.creationDate).toString(), ) }) }) @@ -1076,12 +1087,9 @@ describe('AdminResolver', () => { describe('date of creation is in the future', () => { it('throws an error', async () => { jest.clearAllMocks() - const now = new Date() - variables.creationDate = new Date( - now.getFullYear(), - now.getMonth() + 4, - 1, - ).toString() + variables.creationDate = contributionDateFormatter( + new Date(now.getFullYear(), now.getMonth() + 4, 1), + ) await expect( mutate({ mutation: adminCreateContribution, variables }), ).resolves.toEqual( @@ -1096,7 +1104,7 @@ describe('AdminResolver', () => { it('logs the error thrown', () => { expect(logger.error).toBeCalledWith( 'No information for available creations with the given creationDate=', - variables.creationDate, + new Date(variables.creationDate).toString(), ) }) }) @@ -1104,7 +1112,7 @@ describe('AdminResolver', () => { describe('amount of creation is too high', () => { it('throws an error', async () => { jest.clearAllMocks() - variables.creationDate = new Date().toString() + variables.creationDate = contributionDateFormatter(now) await expect( mutate({ mutation: adminCreateContribution, variables }), ).resolves.toEqual( @@ -1192,7 +1200,7 @@ describe('AdminResolver', () => { email, amount: new Decimal(500), memo: 'Grundeinkommen', - creationDate: new Date().toString(), + creationDate: contributionDateFormatter(new Date()), } }) @@ -1238,7 +1246,7 @@ describe('AdminResolver', () => { email: 'bob@baumeister.de', amount: new Decimal(300), memo: 'Danke Bibi!', - creationDate: new Date().toString(), + creationDate: contributionDateFormatter(new Date()), }, }), ).resolves.toEqual( @@ -1268,7 +1276,7 @@ describe('AdminResolver', () => { email: 'stephen@hawking.uk', amount: new Decimal(300), memo: 'Danke Bibi!', - creationDate: new Date().toString(), + creationDate: contributionDateFormatter(new Date()), }, }), ).resolves.toEqual( @@ -1294,7 +1302,7 @@ describe('AdminResolver', () => { email: 'bibi@bloxberg.de', amount: new Decimal(300), memo: 'Danke Bibi!', - creationDate: new Date().toString(), + creationDate: contributionDateFormatter(new Date()), }, }), ).resolves.toEqual( @@ -1321,8 +1329,8 @@ describe('AdminResolver', () => { amount: new Decimal(300), memo: 'Danke Bibi!', creationDate: creation - ? creation.contributionDate.toString() - : new Date().toString(), + ? contributionDateFormatter(creation.contributionDate) + : contributionDateFormatter(new Date()), }, }), ).resolves.toEqual( @@ -1356,8 +1364,8 @@ describe('AdminResolver', () => { amount: new Decimal(1900), memo: 'Danke Peter!', creationDate: creation - ? creation.contributionDate.toString() - : new Date().toString(), + ? contributionDateFormatter(creation.contributionDate) + : contributionDateFormatter(new Date()), }, }), ).resolves.toEqual( @@ -1390,8 +1398,8 @@ describe('AdminResolver', () => { amount: new Decimal(300), memo: 'Danke Peter!', creationDate: creation - ? creation.contributionDate.toString() - : new Date().toString(), + ? contributionDateFormatter(creation.contributionDate) + : contributionDateFormatter(new Date()), }, }), ).resolves.toEqual( @@ -1430,8 +1438,8 @@ describe('AdminResolver', () => { amount: new Decimal(200), memo: 'Das war leider zu Viel!', creationDate: creation - ? creation.contributionDate.toString() - : new Date().toString(), + ? contributionDateFormatter(creation.contributionDate) + : contributionDateFormatter(new Date()), }, }), ).resolves.toEqual( @@ -1554,7 +1562,7 @@ describe('AdminResolver', () => { variables: { amount: 100.0, memo: 'Test env contribution', - creationDate: new Date().toString(), + creationDate: contributionDateFormatter(new Date()), }, }) }) @@ -1633,7 +1641,9 @@ describe('AdminResolver', () => { email: 'peter@lustig.de', amount: 400, memo: 'Herzlich Willkommen bei Gradido!', - creationDate: new Date(now.getFullYear(), now.getMonth() - 1, 1).toISOString(), + creationDate: contributionDateFormatter( + new Date(now.getFullYear(), now.getMonth() - 1, 1), + ), }) }) @@ -1664,7 +1674,9 @@ describe('AdminResolver', () => { email: 'bibi@bloxberg.de', amount: 450, memo: 'Herzlich Willkommen bei Gradido liebe Bibi!', - creationDate: new Date(now.getFullYear(), now.getMonth() - 2, 1).toISOString(), + creationDate: contributionDateFormatter( + new Date(now.getFullYear(), now.getMonth() - 2, 1), + ), }) }) @@ -1735,13 +1747,17 @@ describe('AdminResolver', () => { email: 'bibi@bloxberg.de', amount: 50, memo: 'Herzlich Willkommen bei Gradido liebe Bibi!', - creationDate: new Date(now.getFullYear(), now.getMonth() - 2, 1).toISOString(), + creationDate: contributionDateFormatter( + new Date(now.getFullYear(), now.getMonth() - 2, 1), + ), }) c2 = await creationFactory(testEnv, { email: 'bibi@bloxberg.de', amount: 50, memo: 'Herzlich Willkommen bei Gradido liebe Bibi!', - creationDate: new Date(now.getFullYear(), now.getMonth() - 2, 1).toISOString(), + creationDate: contributionDateFormatter( + new Date(now.getFullYear(), now.getMonth() - 2, 1), + ), }) }) diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index 479d020ea..80c69a864 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -1,4 +1,4 @@ -import { Context, getUser } from '@/server/context' +import { Context, getUser, getClientTimezoneOffset } from '@/server/context' import { backendLogger as logger } from '@/server/logger' import { Resolver, Query, Arg, Args, Authorized, Mutation, Ctx, Int } from 'type-graphql' import { @@ -49,6 +49,7 @@ import { validateContribution, isStartEndDateValid, updateCreations, + isValidDateString, } from './util/creations' import { CONTRIBUTIONLINK_NAME_MAX_CHARS, @@ -86,7 +87,9 @@ export class AdminResolver { async searchUsers( @Args() { searchText, currentPage = 1, pageSize = 25, filters }: SearchUsersArgs, + @Ctx() context: Context, ): Promise { + const clientTimezoneOffset = getClientTimezoneOffset(context) const userRepository = getCustomRepository(UserRepository) const userFields = [ 'id', @@ -114,7 +117,10 @@ export class AdminResolver { } } - const creations = await getUserCreations(users.map((u) => u.id)) + const creations = await getUserCreations( + users.map((u) => u.id), + clientTimezoneOffset, + ) const adminUsers = await Promise.all( users.map(async (user) => { @@ -237,6 +243,11 @@ export class AdminResolver { logger.info( `adminCreateContribution(email=${email}, amount=${amount}, memo=${memo}, creationDate=${creationDate})`, ) + const clientTimezoneOffset = getClientTimezoneOffset(context) + if (!isValidDateString(creationDate)) { + logger.error(`invalid Date for creationDate=${creationDate}`) + throw new Error(`invalid Date for creationDate=${creationDate}`) + } const emailContact = await UserContact.findOne({ where: { email }, withDeleted: true, @@ -262,11 +273,11 @@ export class AdminResolver { const event = new Event() const moderator = getUser(context) logger.trace('moderator: ', moderator.id) - const creations = await getUserCreation(emailContact.userId) + const creations = await getUserCreation(emailContact.userId, clientTimezoneOffset) logger.trace('creations:', creations) const creationDateObj = new Date(creationDate) logger.trace('creationDateObj:', creationDateObj) - validateContribution(creations, amount, creationDateObj) + validateContribution(creations, amount, creationDateObj, clientTimezoneOffset) const contribution = DbContribution.create() contribution.userId = emailContact.userId contribution.amount = amount @@ -289,7 +300,7 @@ export class AdminResolver { event.setEventAdminContributionCreate(eventAdminCreateContribution), ) - return getUserCreation(emailContact.userId) + return getUserCreation(emailContact.userId, clientTimezoneOffset) } @Authorized([RIGHTS.ADMIN_CREATE_CONTRIBUTIONS]) @@ -325,6 +336,7 @@ export class AdminResolver { @Args() { id, email, amount, memo, creationDate }: AdminUpdateContributionArgs, @Ctx() context: Context, ): Promise { + const clientTimezoneOffset = getClientTimezoneOffset(context) const emailContact = await UserContact.findOne({ where: { email }, withDeleted: true, @@ -365,17 +377,17 @@ export class AdminResolver { } const creationDateObj = new Date(creationDate) - let creations = await getUserCreation(user.id) + let creations = await getUserCreation(user.id, clientTimezoneOffset) if (contributionToUpdate.contributionDate.getMonth() === creationDateObj.getMonth()) { - creations = updateCreations(creations, contributionToUpdate) + creations = updateCreations(creations, contributionToUpdate, clientTimezoneOffset) } else { logger.error('Currently the month of the contribution cannot change.') throw new Error('Currently the month of the contribution cannot change.') } // all possible cases not to be true are thrown in this function - validateContribution(creations, amount, creationDateObj) + validateContribution(creations, amount, creationDateObj, clientTimezoneOffset) contributionToUpdate.amount = amount contributionToUpdate.memo = memo contributionToUpdate.contributionDate = new Date(creationDate) @@ -389,7 +401,7 @@ export class AdminResolver { result.memo = contributionToUpdate.memo result.date = contributionToUpdate.contributionDate - result.creation = await getUserCreation(user.id) + result.creation = await getUserCreation(user.id, clientTimezoneOffset) const event = new Event() const eventAdminContributionUpdate = new EventAdminContributionUpdate() @@ -405,7 +417,8 @@ export class AdminResolver { @Authorized([RIGHTS.LIST_UNCONFIRMED_CONTRIBUTIONS]) @Query(() => [UnconfirmedContribution]) - async listUnconfirmedContributions(): Promise { + async listUnconfirmedContributions(@Ctx() context: Context): Promise { + const clientTimezoneOffset = getClientTimezoneOffset(context) const contributions = await getConnection() .createQueryBuilder() .select('c') @@ -419,7 +432,7 @@ export class AdminResolver { } const userIds = contributions.map((p) => p.userId) - const userCreations = await getUserCreations(userIds) + const userCreations = await getUserCreations(userIds, clientTimezoneOffset) const users = await dbUser.find({ where: { id: In(userIds) }, withDeleted: true, @@ -493,6 +506,7 @@ export class AdminResolver { @Arg('id', () => Int) id: number, @Ctx() context: Context, ): Promise { + const clientTimezoneOffset = getClientTimezoneOffset(context) const contribution = await DbContribution.findOne(id) if (!contribution) { logger.error(`Contribution not found for given id: ${id}`) @@ -511,8 +525,13 @@ export class AdminResolver { logger.error('This user was deleted. Cannot confirm a contribution.') throw new Error('This user was deleted. Cannot confirm a contribution.') } - const creations = await getUserCreation(contribution.userId, false) - validateContribution(creations, contribution.amount, contribution.contributionDate) + const creations = await getUserCreation(contribution.userId, clientTimezoneOffset, false) + validateContribution( + creations, + contribution.amount, + contribution.contributionDate, + clientTimezoneOffset, + ) const receivedCallDate = new Date() diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index a061304b7..15bdbfc2e 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -1,5 +1,5 @@ import { RIGHTS } from '@/auth/RIGHTS' -import { Context, getUser } from '@/server/context' +import { Context, getUser, getClientTimezoneOffset } from '@/server/context' import { backendLogger as logger } from '@/server/logger' import { Contribution as dbContribution } from '@entity/Contribution' import { Arg, Args, Authorized, Ctx, Int, Mutation, Query, Resolver } from 'type-graphql' @@ -31,6 +31,7 @@ export class ContributionResolver { @Args() { amount, memo, creationDate }: ContributionArgs, @Ctx() context: Context, ): Promise { + const clientTimezoneOffset = getClientTimezoneOffset(context) if (memo.length > MEMO_MAX_CHARS) { logger.error(`memo text is too long: memo.length=${memo.length} > ${MEMO_MAX_CHARS}`) throw new Error(`memo text is too long (${MEMO_MAX_CHARS} characters maximum)`) @@ -44,10 +45,10 @@ export class ContributionResolver { const event = new Event() const user = getUser(context) - const creations = await getUserCreation(user.id) + const creations = await getUserCreation(user.id, clientTimezoneOffset) logger.trace('creations', creations) const creationDateObj = new Date(creationDate) - validateContribution(creations, amount, creationDateObj) + validateContribution(creations, amount, creationDateObj, clientTimezoneOffset) const contribution = dbContribution.create() contribution.userId = user.id @@ -171,6 +172,7 @@ export class ContributionResolver { @Args() { amount, memo, creationDate }: ContributionArgs, @Ctx() context: Context, ): Promise { + const clientTimezoneOffset = getClientTimezoneOffset(context) if (memo.length > MEMO_MAX_CHARS) { logger.error(`memo text is too long: memo.length=${memo.length} > ${MEMO_MAX_CHARS}`) throw new Error(`memo text is too long (${MEMO_MAX_CHARS} characters maximum)`) @@ -206,16 +208,16 @@ export class ContributionResolver { ) } const creationDateObj = new Date(creationDate) - let creations = await getUserCreation(user.id) + let creations = await getUserCreation(user.id, clientTimezoneOffset) if (contributionToUpdate.contributionDate.getMonth() === creationDateObj.getMonth()) { - creations = updateCreations(creations, contributionToUpdate) + creations = updateCreations(creations, contributionToUpdate, clientTimezoneOffset) } else { logger.error('Currently the month of the contribution cannot change.') throw new Error('Currently the month of the contribution cannot change.') } // all possible cases not to be true are thrown in this function - validateContribution(creations, amount, creationDateObj) + validateContribution(creations, amount, creationDateObj, clientTimezoneOffset) const contributionMessage = ContributionMessage.create() contributionMessage.contributionId = contributionId diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index 74c531c54..a5c4a5f01 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -1,5 +1,5 @@ import { backendLogger as logger } from '@/server/logger' -import { Context, getUser } from '@/server/context' +import { Context, getUser, getClientTimezoneOffset } from '@/server/context' import { getConnection } from '@dbTools/typeorm' import { Resolver, @@ -169,6 +169,7 @@ export class TransactionLinkResolver { @Arg('code', () => String) code: string, @Ctx() context: Context, ): Promise { + const clientTimezoneOffset = getClientTimezoneOffset(context) const user = getUser(context) const now = new Date() @@ -258,9 +259,9 @@ export class TransactionLinkResolver { } } - const creations = await getUserCreation(user.id) + const creations = await getUserCreation(user.id, clientTimezoneOffset) logger.info('open creations', creations) - validateContribution(creations, contributionLink.amount, now) + validateContribution(creations, contributionLink.amount, now, clientTimezoneOffset) const contribution = new DbContribution() contribution.userId = user.id contribution.createdAt = now diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 81d0bab0f..707b7ac49 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -1,7 +1,7 @@ import fs from 'fs' import { backendLogger as logger } from '@/server/logger' import i18n from 'i18n' -import { Context, getUser } from '@/server/context' +import { Context, getUser, getClientTimezoneOffset } from '@/server/context' import { Resolver, Query, Args, Arg, Authorized, Ctx, UseMiddleware, Mutation } from 'type-graphql' import { getConnection, getCustomRepository, IsNull, Not } from '@dbTools/typeorm' import CONFIG from '@/config' @@ -306,8 +306,9 @@ export class UserResolver { async verifyLogin(@Ctx() context: Context): Promise { logger.info('verifyLogin...') // TODO refactor and do not have duplicate code with login(see below) + const clientTimezoneOffset = getClientTimezoneOffset(context) const userEntity = getUser(context) - const user = new User(userEntity, await getUserCreation(userEntity.id)) + const user = new User(userEntity, await getUserCreation(userEntity.id, clientTimezoneOffset)) // user.pubkey = userEntity.pubKey.toString('hex') // Elopage Status & Stored PublisherId user.hasElopage = await this.hasElopage(context) @@ -324,6 +325,7 @@ export class UserResolver { @Ctx() context: Context, ): Promise { logger.info(`login with ${email}, ***, ${publisherId} ...`) + const clientTimezoneOffset = getClientTimezoneOffset(context) email = email.trim().toLowerCase() const dbUser = await findUserByEmail(email) if (dbUser.deletedAt) { @@ -354,7 +356,7 @@ export class UserResolver { logger.addContext('user', dbUser.id) logger.debug('validation of login credentials successful...') - const user = new User(dbUser, await getUserCreation(dbUser.id)) + const user = new User(dbUser, await getUserCreation(dbUser.id, clientTimezoneOffset)) logger.debug(`user= ${JSON.stringify(user, null, 2)}`) i18n.setLocale(user.language) diff --git a/backend/src/graphql/resolver/util/creations.test.ts b/backend/src/graphql/resolver/util/creations.test.ts new file mode 100644 index 000000000..8d747e989 --- /dev/null +++ b/backend/src/graphql/resolver/util/creations.test.ts @@ -0,0 +1,266 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ + +import { testEnvironment, cleanDB, contributionDateFormatter } from '@test/helpers' +import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' +import { peterLustig } from '@/seeds/users/peter-lustig' +import { User } from '@entity/User' +import { Contribution } from '@entity/Contribution' +import { userFactory } from '@/seeds/factory/user' +import { login, createContribution, adminCreateContribution } from '@/seeds/graphql/mutations' +import { getUserCreation } from './creations' + +let mutate: any, con: any +let testEnv: any + +beforeAll(async () => { + testEnv = await testEnvironment() + mutate = testEnv.mutate + con = testEnv.con + await cleanDB() +}) + +afterAll(async () => { + await cleanDB() + await con.close() +}) + +const setZeroHours = (date: Date): Date => { + return new Date(date.getFullYear(), date.getMonth(), date.getDate()) +} + +describe('util/creation', () => { + let user: User + let admin: User + + const now = new Date() + + beforeAll(async () => { + user = await userFactory(testEnv, bibiBloxberg) + admin = await userFactory(testEnv, peterLustig) + }) + + describe('getUserCreations', () => { + beforeAll(async () => { + await mutate({ + mutation: login, + variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, + }) + await mutate({ + mutation: adminCreateContribution, + variables: { + email: 'bibi@bloxberg.de', + amount: 250.0, + memo: 'Admin contribution for this month', + creationDate: contributionDateFormatter(now), + }, + }) + await mutate({ + mutation: adminCreateContribution, + variables: { + email: 'bibi@bloxberg.de', + amount: 160.0, + memo: 'Admin contribution for the last month', + creationDate: contributionDateFormatter( + new Date(now.getFullYear(), now.getMonth() - 1, now.getDate()), + ), + }, + }) + await mutate({ + mutation: adminCreateContribution, + variables: { + email: 'bibi@bloxberg.de', + amount: 450.0, + memo: 'Admin contribution for two months ago', + creationDate: contributionDateFormatter( + new Date(now.getFullYear(), now.getMonth() - 2, now.getDate()), + ), + }, + }) + await mutate({ + mutation: login, + variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, + }) + await mutate({ + mutation: createContribution, + variables: { + amount: 400.0, + memo: 'Contribution for this month', + creationDate: contributionDateFormatter(now), + }, + }) + await mutate({ + mutation: createContribution, + variables: { + amount: 500.0, + memo: 'Contribution for the last month', + creationDate: contributionDateFormatter( + new Date(now.getFullYear(), now.getMonth() - 1, now.getDate()), + ), + }, + }) + }) + + it('has the correct data setup', async () => { + await expect(Contribution.find()).resolves.toEqual([ + expect.objectContaining({ + userId: user.id, + contributionDate: setZeroHours(now), + amount: expect.decimalEqual(250), + memo: 'Admin contribution for this month', + moderatorId: admin.id, + contributionType: 'ADMIN', + contributionStatus: 'PENDING', + }), + expect.objectContaining({ + userId: user.id, + contributionDate: setZeroHours( + new Date(now.getFullYear(), now.getMonth() - 1, now.getDate()), + ), + amount: expect.decimalEqual(160), + memo: 'Admin contribution for the last month', + moderatorId: admin.id, + contributionType: 'ADMIN', + contributionStatus: 'PENDING', + }), + expect.objectContaining({ + userId: user.id, + contributionDate: setZeroHours( + new Date(now.getFullYear(), now.getMonth() - 2, now.getDate()), + ), + amount: expect.decimalEqual(450), + memo: 'Admin contribution for two months ago', + moderatorId: admin.id, + contributionType: 'ADMIN', + contributionStatus: 'PENDING', + }), + expect.objectContaining({ + userId: user.id, + contributionDate: setZeroHours(now), + amount: expect.decimalEqual(400), + memo: 'Contribution for this month', + moderatorId: null, + contributionType: 'USER', + contributionStatus: 'PENDING', + }), + expect.objectContaining({ + userId: user.id, + contributionDate: setZeroHours( + new Date(now.getFullYear(), now.getMonth() - 1, now.getDate()), + ), + amount: expect.decimalEqual(500), + memo: 'Contribution for the last month', + moderatorId: null, + contributionType: 'USER', + contributionStatus: 'PENDING', + }), + ]) + }) + + describe('call getUserCreation now', () => { + it('returns the expected open contributions', async () => { + await expect(getUserCreation(user.id, 0)).resolves.toEqual([ + expect.decimalEqual(550), + expect.decimalEqual(340), + expect.decimalEqual(350), + ]) + }) + + describe('run forward in time one hour before next month', () => { + const targetDate = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 0, 0) + + beforeAll(() => { + jest.useFakeTimers() + setTimeout(jest.fn(), targetDate.getTime() - now.getTime()) + jest.runAllTimers() + }) + + afterAll(() => { + jest.useRealTimers() + }) + + it('has the clock set correctly', () => { + expect(new Date().toISOString()).toContain( + `${targetDate.getFullYear()}-${targetDate.getMonth() + 1}-${targetDate.getDate()}T23:`, + ) + }) + + describe('call getUserCreation with UTC', () => { + it('returns the expected open contributions', async () => { + await expect(getUserCreation(user.id, 0)).resolves.toEqual([ + expect.decimalEqual(550), + expect.decimalEqual(340), + expect.decimalEqual(350), + ]) + }) + }) + + describe('call getUserCreation with JST (GMT+0900)', () => { + it('returns the expected open contributions', async () => { + await expect(getUserCreation(user.id, -540, true)).resolves.toEqual([ + expect.decimalEqual(340), + expect.decimalEqual(350), + expect.decimalEqual(1000), + ]) + }) + }) + + describe('call getUserCreation with PST (GMT-0800)', () => { + it('returns the expected open contributions', async () => { + await expect(getUserCreation(user.id, 480, true)).resolves.toEqual([ + expect.decimalEqual(550), + expect.decimalEqual(340), + expect.decimalEqual(350), + ]) + }) + }) + + describe('run two hours forward to be in the next month in UTC', () => { + const nextMonthTargetDate = new Date() + nextMonthTargetDate.setTime(targetDate.getTime() + 2 * 60 * 60 * 1000) + + beforeAll(() => { + setTimeout(jest.fn(), 2 * 60 * 60 * 1000) + jest.runAllTimers() + }) + + it('has the clock set correctly', () => { + expect(new Date().toISOString()).toContain( + `${nextMonthTargetDate.getFullYear()}-${nextMonthTargetDate.getMonth() + 1}-01T01:`, + ) + }) + + describe('call getUserCreation with UTC', () => { + it('returns the expected open contributions', async () => { + await expect(getUserCreation(user.id, 0, true)).resolves.toEqual([ + expect.decimalEqual(340), + expect.decimalEqual(350), + expect.decimalEqual(1000), + ]) + }) + }) + + describe('call getUserCreation with JST (GMT+0900)', () => { + it('returns the expected open contributions', async () => { + await expect(getUserCreation(user.id, -540, true)).resolves.toEqual([ + expect.decimalEqual(340), + expect.decimalEqual(350), + expect.decimalEqual(1000), + ]) + }) + }) + + describe('call getUserCreation with PST (GMT-0800)', () => { + it('returns the expected open contributions', async () => { + await expect(getUserCreation(user.id, 450, true)).resolves.toEqual([ + expect.decimalEqual(550), + expect.decimalEqual(340), + expect.decimalEqual(350), + ]) + }) + }) + }) + }) + }) + }) +}) diff --git a/backend/src/graphql/resolver/util/creations.ts b/backend/src/graphql/resolver/util/creations.ts index abf4017cb..eb4b6394d 100644 --- a/backend/src/graphql/resolver/util/creations.ts +++ b/backend/src/graphql/resolver/util/creations.ts @@ -13,9 +13,10 @@ export const validateContribution = ( creations: Decimal[], amount: Decimal, creationDate: Date, + timezoneOffset: number, ): void => { logger.trace('isContributionValid: ', creations, amount, creationDate) - const index = getCreationIndex(creationDate.getMonth()) + const index = getCreationIndex(creationDate.getMonth(), timezoneOffset) if (index < 0) { logger.error( @@ -37,10 +38,11 @@ export const validateContribution = ( export const getUserCreations = async ( ids: number[], + timezoneOffset: number, includePending = true, ): Promise => { logger.trace('getUserCreations:', ids, includePending) - const months = getCreationMonths() + const months = getCreationMonths(timezoneOffset) logger.trace('getUserCreations months', months) const queryRunner = getConnection().createQueryRunner() @@ -87,24 +89,29 @@ export const getUserCreations = async ( }) } -export const getUserCreation = async (id: number, includePending = true): Promise => { - logger.trace('getUserCreation', id, includePending) - const creations = await getUserCreations([id], includePending) +export const getUserCreation = async ( + id: number, + timezoneOffset: number, + includePending = true, +): Promise => { + logger.trace('getUserCreation', id, includePending, timezoneOffset) + const creations = await getUserCreations([id], timezoneOffset, includePending) logger.trace('getUserCreation creations=', creations) return creations[0] ? creations[0].creations : FULL_CREATION_AVAILABLE } -export const getCreationMonths = (): number[] => { - const now = new Date(Date.now()) +const getCreationMonths = (timezoneOffset: number): number[] => { + const clientNow = new Date() + clientNow.setTime(clientNow.getTime() - timezoneOffset * 60 * 1000) return [ - now.getMonth() + 1, - new Date(now.getFullYear(), now.getMonth() - 1, 1).getMonth() + 1, - new Date(now.getFullYear(), now.getMonth() - 2, 1).getMonth() + 1, - ].reverse() + new Date(clientNow.getFullYear(), clientNow.getMonth() - 2, 1).getMonth() + 1, + new Date(clientNow.getFullYear(), clientNow.getMonth() - 1, 1).getMonth() + 1, + clientNow.getMonth() + 1, + ] } -export const getCreationIndex = (month: number): number => { - return getCreationMonths().findIndex((el) => el === month + 1) +const getCreationIndex = (month: number, timezoneOffset: number): number => { + return getCreationMonths(timezoneOffset).findIndex((el) => el === month + 1) } export const isStartEndDateValid = ( @@ -128,8 +135,12 @@ export const isStartEndDateValid = ( } } -export const updateCreations = (creations: Decimal[], contribution: Contribution): Decimal[] => { - const index = getCreationIndex(contribution.contributionDate.getMonth()) +export const updateCreations = ( + creations: Decimal[], + contribution: Contribution, + timezoneOffset: number, +): Decimal[] => { + const index = getCreationIndex(contribution.contributionDate.getMonth(), timezoneOffset) if (index < 0) { throw new Error('You cannot create GDD for a month older than the last three months.') @@ -137,3 +148,7 @@ export const updateCreations = (creations: Decimal[], contribution: Contribution creations[index] = creations[index].plus(contribution.amount.toString()) return creations } + +export const isValidDateString = (dateString: string): boolean => { + return new Date(dateString).toString() !== 'Invalid Date' +} diff --git a/backend/src/seeds/index.ts b/backend/src/seeds/index.ts index c5a55cb84..3675d381d 100644 --- a/backend/src/seeds/index.ts +++ b/backend/src/seeds/index.ts @@ -29,6 +29,7 @@ const context = { // eslint-disable-next-line @typescript-eslint/no-empty-function forEach: (): void => {}, }, + clientTimezoneOffset: 0, } export const cleanDB = async () => { diff --git a/backend/src/server/context.ts b/backend/src/server/context.ts index 5bfc22e72..8ba590dd3 100644 --- a/backend/src/server/context.ts +++ b/backend/src/server/context.ts @@ -9,7 +9,7 @@ export interface Context { setHeaders: { key: string; value: string }[] role?: Role user?: dbUser - clientRequestTime?: string + clientTimezoneOffset?: number // hack to use less DB calls for Balance Resolver lastTransaction?: dbTransaction transactionCount?: number @@ -19,7 +19,7 @@ export interface Context { const context = (args: ExpressContext): Context => { const authorization = args.req.headers.authorization - const clientRequestTime = args.req.headers.clientrequesttime + const clientTimezoneOffset = args.req.headers.clienttimezoneoffset const context: Context = { token: null, setHeaders: [], @@ -27,8 +27,8 @@ const context = (args: ExpressContext): Context => { if (authorization) { context.token = authorization.replace(/^Bearer /, '') } - if (clientRequestTime && typeof clientRequestTime === 'string') { - context.clientRequestTime = clientRequestTime + if (clientTimezoneOffset && typeof clientTimezoneOffset === 'string') { + context.clientTimezoneOffset = Number(clientTimezoneOffset) } return context } @@ -38,4 +38,14 @@ export const getUser = (context: Context): dbUser => { throw new Error('No user given in context!') } +export const getClientTimezoneOffset = (context: Context): number => { + if ( + (context.clientTimezoneOffset || context.clientTimezoneOffset === 0) && + Math.abs(context.clientTimezoneOffset) <= 27 * 60 + ) { + return context.clientTimezoneOffset + } + throw new Error('No valid client time zone offset in context!') +} + export default context diff --git a/backend/test/helpers.ts b/backend/test/helpers.ts index aaf59af86..7ee8e6052 100644 --- a/backend/test/helpers.ts +++ b/backend/test/helpers.ts @@ -16,6 +16,7 @@ const context = { push: headerPushMock, forEach: jest.fn(), }, + clientTimezoneOffset: 0, } export const cleanDB = async () => { @@ -46,3 +47,12 @@ export const resetEntity = async (entity: any) => { export const resetToken = () => { context.token = '' } + +// format date string as it comes from the frontend for the contribution date +export const contributionDateFormatter = (date: Date): string => { + return `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}` +} + +export const setClientTimezoneOffset = (offset: number): void => { + context.clientTimezoneOffset = offset +} diff --git a/database/package.json b/database/package.json index 096c7a9bd..6216a25fb 100644 --- a/database/package.json +++ b/database/package.json @@ -1,6 +1,6 @@ { "name": "gradido-database", - "version": "1.13.3", + "version": "1.14.1", "description": "Gradido Database Tool to execute database migrations", "main": "src/index.ts", "repository": "https://github.com/gradido/gradido/database", diff --git a/frontend/package.json b/frontend/package.json index 4e983d716..cfc12630e 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "bootstrap-vue-gradido-wallet", - "version": "1.13.3", + "version": "1.14.1", "private": true, "scripts": { "start": "node run/server.js", diff --git a/frontend/src/components/ContributionMessages/ContributionMessagesFormular.spec.js b/frontend/src/components/ContributionMessages/ContributionMessagesFormular.spec.js index aba5abc34..42deac9cb 100644 --- a/frontend/src/components/ContributionMessages/ContributionMessagesFormular.spec.js +++ b/frontend/src/components/ContributionMessages/ContributionMessagesFormular.spec.js @@ -67,9 +67,9 @@ describe('ContributionMessagesFormular', () => { await wrapper.find('form').trigger('submit') }) - it('emitted "get-list-contribution-messages" with data', async () => { + it('emitted "get-list-contribution-messages" with false', async () => { expect(wrapper.emitted('get-list-contribution-messages')).toEqual( - expect.arrayContaining([expect.arrayContaining([42])]), + expect.arrayContaining([expect.arrayContaining([false])]), ) }) diff --git a/frontend/src/components/ContributionMessages/ContributionMessagesFormular.vue b/frontend/src/components/ContributionMessages/ContributionMessagesFormular.vue index 1a5928cc3..c601de4f5 100644 --- a/frontend/src/components/ContributionMessages/ContributionMessagesFormular.vue +++ b/frontend/src/components/ContributionMessages/ContributionMessagesFormular.vue @@ -51,7 +51,7 @@ export default { }, }) .then((result) => { - this.$emit('get-list-contribution-messages', this.contributionId) + this.$emit('get-list-contribution-messages', false) this.$emit('update-state', this.contributionId) this.form.text = '' this.toastSuccess(this.$t('message.reply')) diff --git a/frontend/src/components/ContributionMessages/ContributionMessagesList.spec.js b/frontend/src/components/ContributionMessages/ContributionMessagesList.spec.js index 7798532b7..c5c26a2c0 100644 --- a/frontend/src/components/ContributionMessages/ContributionMessagesList.spec.js +++ b/frontend/src/components/ContributionMessages/ContributionMessagesList.spec.js @@ -40,16 +40,6 @@ describe('ContributionMessagesList', () => { expect(wrapper.findComponent({ name: 'ContributionMessagesFormular' }).exists()).toBe(true) }) - describe('get List Contribution Messages', () => { - beforeEach(() => { - wrapper.vm.getListContributionMessages() - }) - - it('emits getListContributionMessages', async () => { - expect(wrapper.vm.$emit('get-list-contribution-messages')).toBeTruthy() - }) - }) - describe('update State', () => { beforeEach(() => { wrapper.vm.updateState() diff --git a/frontend/src/components/ContributionMessages/ContributionMessagesList.vue b/frontend/src/components/ContributionMessages/ContributionMessagesList.vue index 4b7045a40..e9262c073 100644 --- a/frontend/src/components/ContributionMessages/ContributionMessagesList.vue +++ b/frontend/src/components/ContributionMessages/ContributionMessagesList.vue @@ -9,7 +9,7 @@ @@ -50,9 +50,6 @@ export default { }, }, methods: { - getListContributionMessages() { - this.$emit('get-list-contribution-messages', this.contributionId) - }, updateState(id) { this.$emit('update-state', id) }, diff --git a/frontend/src/components/Contributions/ContributionList.vue b/frontend/src/components/Contributions/ContributionList.vue index ca4e7a9a0..e76664e03 100644 --- a/frontend/src/components/Contributions/ContributionList.vue +++ b/frontend/src/components/Contributions/ContributionList.vue @@ -3,6 +3,7 @@
{ const mocks = { $t: jest.fn((t) => t), $d: jest.fn((d) => d), + $apollo: { query: jest.fn().mockResolvedValue() }, } const propsData = { @@ -132,6 +133,27 @@ describe('ContributionListItem', () => { expect(wrapper.emitted('delete-contribution')).toBeFalsy() }) }) + + describe('updateState', () => { + beforeEach(async () => { + await wrapper.vm.updateState() + }) + + it('emit update-state', () => { + expect(wrapper.vm.$emit('update-state')).toBeTruthy() + }) + }) + }) + + describe('getListContributionMessages', () => { + beforeEach(() => { + wrapper + .findComponent({ name: 'ContributionMessagesList' }) + .vm.$emit('get-list-contribution-messages') + }) + it('emits closeAllOpenCollapse', () => { + expect(wrapper.emitted('closeAllOpenCollapse')).toBeTruthy() + }) }) }) }) diff --git a/frontend/src/components/Contributions/ContributionListItem.vue b/frontend/src/components/Contributions/ContributionListItem.vue index 683d234ba..53de8c461 100644 --- a/frontend/src/components/Contributions/ContributionListItem.vue +++ b/frontend/src/components/Contributions/ContributionListItem.vue @@ -32,12 +32,13 @@ v-if="!['CONFIRMED', 'DELETED'].includes(state) && !allContribution" class="pointer ml-5" @click=" - $emit('update-contribution-form', { - id: id, - contributionDate: contributionDate, - memo: memo, - amount: amount, - }) + $emit('closeAllOpenCollapse'), + $emit('update-contribution-form', { + id: id, + contributionDate: contributionDate, + memo: memo, + amount: amount, + }) " > @@ -178,8 +179,10 @@ export default { if (value) this.$emit('delete-contribution', item) }) }, - getListContributionMessages() { - // console.log('getListContributionMessages', this.contributionId) + getListContributionMessages(closeCollapse = true) { + if (closeCollapse) { + this.$emit('closeAllOpenCollapse') + } this.$apollo .query({ query: listContributionMessages, diff --git a/frontend/src/pages/Community.vue b/frontend/src/pages/Community.vue index 786307405..58426207f 100644 --- a/frontend/src/pages/Community.vue +++ b/frontend/src/pages/Community.vue @@ -2,7 +2,7 @@
- +
- +

{{ $t('navigation.community') }}

@@ -112,6 +113,13 @@ export default { } }, methods: { + closeAllOpenCollapse() { + // console.log('Community closeAllOpenCollapse ') + // console.log('closeAllOpenCollapse', this.$el.querySelectorAll('.collapse.show')) + this.$el.querySelectorAll('.collapse.show').forEach((value) => { + this.$root.$emit('bv::toggle::collapse', value.id) + }) + }, setContribution(data) { this.$apollo .mutate({ diff --git a/frontend/src/plugins/apolloProvider.js b/frontend/src/plugins/apolloProvider.js index 05954d36b..7a37bcf62 100644 --- a/frontend/src/plugins/apolloProvider.js +++ b/frontend/src/plugins/apolloProvider.js @@ -12,7 +12,7 @@ const authLink = new ApolloLink((operation, forward) => { operation.setContext({ headers: { Authorization: token && token.length > 0 ? `Bearer ${token}` : '', - clientRequestTime: new Date().toString(), + clientTimezoneOffset: new Date().getTimezoneOffset(), }, }) return forward(operation).map((response) => { diff --git a/frontend/src/plugins/apolloProvider.test.js b/frontend/src/plugins/apolloProvider.test.js index 31e1a664b..584014213 100644 --- a/frontend/src/plugins/apolloProvider.test.js +++ b/frontend/src/plugins/apolloProvider.test.js @@ -98,7 +98,7 @@ describe('apolloProvider', () => { expect(setContextMock).toBeCalledWith({ headers: { Authorization: 'Bearer some-token', - clientRequestTime: expect.any(String), + clientTimezoneOffset: expect.any(Number), }, }) }) @@ -114,7 +114,7 @@ describe('apolloProvider', () => { expect(setContextMock).toBeCalledWith({ headers: { Authorization: '', - clientRequestTime: expect.any(String), + clientTimezoneOffset: expect.any(Number), }, }) }) diff --git a/package.json b/package.json index 8e5fcfc70..72efee984 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gradido", - "version": "1.13.3", + "version": "1.14.1", "description": "Gradido", "main": "index.js", "repository": "git@github.com:gradido/gradido.git",