Merge branch 'master' into 2474-Incorrectly-Email-Label-on-InputEmail

This commit is contained in:
Alexander Friedland 2023-01-10 16:16:20 +01:00 committed by GitHub
commit 675aeb397e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 434 additions and 10 deletions

View File

@ -10,7 +10,7 @@ Decimal.set({
})
const constants = {
DB_VERSION: '0058-add_communities_table',
DB_VERSION: '0059-add_hide_amount_to_users',
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info

View File

@ -19,4 +19,10 @@ export default class UpdateUserInfosArgs {
@Field({ nullable: true })
passwordNew?: string
@Field({ nullable: true })
hideAmountGDD?: boolean
@Field({ nullable: true })
hideAmountGDT?: boolean
}

View File

@ -27,6 +27,8 @@ export class User {
this.klickTipp = null
this.hasElopage = null
this.creation = creation
this.hideAmountGDD = user.hideAmountGDD
this.hideAmountGDT = user.hideAmountGDT
}
@Field(() => Number)
@ -72,6 +74,12 @@ export class User {
@Field(() => String)
language: string
@Field(() => Boolean)
hideAmountGDD: boolean
@Field(() => Boolean)
hideAmountGDT: boolean
// This is not the users publisherId, but the one of the users who recommend him
@Field(() => Number, { nullable: true })
publisherId: number | null

View File

@ -63,6 +63,7 @@ jest.mock('@/emails/sendEmailVariants', () => {
})
/*
jest.mock('@/apis/KlicktippController', () => {
return {
__esModule: true,
@ -132,6 +133,8 @@ describe('UserResolver', () => {
{
id: expect.any(Number),
gradidoID: expect.any(String),
hideAmountGDD: expect.any(Boolean),
hideAmountGDT: expect.any(Boolean),
alias: null,
emailContact: expect.any(UserContact), // 'peter@lustig.de',
emailId: expect.any(Number),

View File

@ -567,7 +567,15 @@ export class UserResolver {
@Mutation(() => Boolean)
async updateUserInfos(
@Args()
{ firstName, lastName, language, password, passwordNew }: UpdateUserInfosArgs,
{
firstName,
lastName,
language,
password,
passwordNew,
hideAmountGDD,
hideAmountGDT,
}: UpdateUserInfosArgs,
@Ctx() context: Context,
): Promise<boolean> {
logger.info(`updateUserInfos(${firstName}, ${lastName}, ${language}, ***, ***)...`)
@ -609,6 +617,15 @@ export class UserResolver {
userEntity.password = encryptPassword(userEntity, passwordNew)
}
// Save hideAmountGDD value
if (hideAmountGDD !== undefined) {
userEntity.hideAmountGDD = hideAmountGDD
}
// Save hideAmountGDT value
if (hideAmountGDT !== undefined) {
userEntity.hideAmountGDT = hideAmountGDT
}
const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect()
await queryRunner.startTransaction('REPEATABLE READ')

View File

@ -31,6 +31,8 @@ export const updateUserInfos = gql`
$password: String
$passwordNew: String
$locale: String
$hideAmountGDD: Boolean
$hideAmountGDT: Boolean
) {
updateUserInfos(
firstName: $firstName
@ -38,6 +40,8 @@ export const updateUserInfos = gql`
password: $password
passwordNew: $passwordNew
language: $locale
hideAmountGDD: $hideAmountGDD
hideAmountGDT: $hideAmountGDT
)
}
`

View File

@ -18,6 +18,8 @@ const communityDbUser: dbUser = {
lastName: 'Akademie',
deletedAt: null,
password: BigInt(0),
hideAmountGDD: false,
hideAmountGDT: false,
// emailHash: Buffer.from(''),
createdAt: new Date(),
// emailChecked: false,

View File

@ -6,7 +6,7 @@ import {
DeleteDateColumn,
OneToOne,
} from 'typeorm'
import { User } from './User'
import { User } from '../User'
@Entity('user_contacts', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
export class UserContact extends BaseEntity {

View File

@ -0,0 +1,118 @@
import {
BaseEntity,
Entity,
PrimaryGeneratedColumn,
Column,
DeleteDateColumn,
OneToMany,
JoinColumn,
OneToOne,
} from 'typeorm'
import { Contribution } from '../Contribution'
import { ContributionMessage } from '../ContributionMessage'
import { UserContact } from '../UserContact'
@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
export class User extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({
name: 'gradido_id',
length: 36,
nullable: false,
collation: 'utf8mb4_unicode_ci',
})
gradidoID: string
@Column({
name: 'alias',
length: 20,
nullable: true,
default: null,
collation: 'utf8mb4_unicode_ci',
})
alias: string
@OneToOne(() => UserContact, (emailContact: UserContact) => emailContact.user)
@JoinColumn({ name: 'email_id' })
emailContact: UserContact
@Column({ name: 'email_id', type: 'int', unsigned: true, nullable: true, default: null })
emailId: number | null
@Column({
name: 'first_name',
length: 255,
nullable: true,
default: null,
collation: 'utf8mb4_unicode_ci',
})
firstName: string
@Column({
name: 'last_name',
length: 255,
nullable: true,
default: null,
collation: 'utf8mb4_unicode_ci',
})
lastName: string
@Column({ name: 'created_at', default: () => 'CURRENT_TIMESTAMP', nullable: false })
createdAt: Date
@DeleteDateColumn({ name: 'deleted_at', nullable: true })
deletedAt: Date | null
@Column({ type: 'bigint', default: 0, unsigned: true })
password: BigInt
@Column({
name: 'password_encryption_type',
type: 'int',
unsigned: true,
nullable: false,
default: 0,
})
passwordEncryptionType: number
@Column({ length: 4, default: 'de', collation: 'utf8mb4_unicode_ci', nullable: false })
language: string
@Column({ type: 'bool', default: false })
hideAmountGDD: boolean
@Column({ type: 'bool', default: false })
hideAmountGDT: boolean
@Column({ name: 'is_admin', type: 'datetime', nullable: true, default: null })
isAdmin: Date | null
@Column({ name: 'referrer_id', type: 'int', unsigned: true, nullable: true, default: null })
referrerId?: number | null
@Column({
name: 'contribution_link_id',
type: 'int',
unsigned: true,
nullable: true,
default: null,
})
contributionLinkId?: number | null
@Column({ name: 'publisher_id', default: 0 })
publisherId: number
@OneToMany(() => Contribution, (contribution) => contribution.user)
@JoinColumn({ name: 'user_id' })
contributions?: Contribution[]
@OneToMany(() => ContributionMessage, (message) => message.user)
@JoinColumn({ name: 'user_id' })
messages?: ContributionMessage[]
@OneToMany(() => UserContact, (userContact: UserContact) => userContact.user)
@JoinColumn({ name: 'user_id' })
userContacts?: UserContact[]
}

View File

@ -1 +1 @@
export { User } from './0057-clear_old_password_junk/User'
export { User } from './0059-add_hide_amount_to_users/User'

View File

@ -0,0 +1,12 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable @typescript-eslint/no-explicit-any */
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn('ALTER TABLE users ADD COLUMN hideAmountGDD bool DEFAULT false;')
await queryFn('ALTER TABLE users ADD COLUMN hideAmountGDT bool DEFAULT false;')
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn('ALTER TABLE users DROP COLUMN hideAmountGDD;')
await queryFn('ALTER TABLE users DROP COLUMN hideAmountGDT;')
}

View File

@ -1,20 +1,31 @@
import { mount } from '@vue/test-utils'
import GddAmount from './GddAmount'
import { updateUserInfos } from '@/graphql/mutations'
import flushPromises from 'flush-promises'
import { toastErrorSpy, toastSuccessSpy } from '@test/testSetup'
const localVue = global.localVue
const mockAPICall = jest.fn()
const storeCommitMock = jest.fn()
const state = {
language: 'en',
hideAmountGDD: false,
}
const mocks = {
$store: {
state,
commit: storeCommitMock,
},
$i18n: {
locale: 'en',
},
$t: jest.fn((t) => t),
$apollo: {
mutate: mockAPICall,
},
}
const propsData = {
@ -38,5 +49,89 @@ describe('GddAmount', () => {
it('renders the component gdd-amount', () => {
expect(wrapper.find('div.gdd-amount').exists()).toBe(true)
})
describe('API throws exception', () => {
beforeEach(async () => {
mockAPICall.mockRejectedValue({
message: 'Ouch',
})
jest.clearAllMocks()
await wrapper.find('div.border-left svg').trigger('click')
await flushPromises()
})
it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Ouch')
})
})
describe('API call successful', () => {
beforeEach(async () => {
mockAPICall.mockResolvedValue({
data: {
updateUserInfos: {
validValues: 1,
},
},
})
jest.clearAllMocks()
await wrapper.find('div.border-left svg').trigger('click')
await flushPromises()
})
it('calls the API', () => {
expect(mockAPICall).toBeCalledWith(
expect.objectContaining({
mutation: updateUserInfos,
variables: {
hideAmountGDD: true,
},
}),
)
})
it('commits hideAmountGDD to store', () => {
expect(storeCommitMock).toBeCalledWith('hideAmountGDD', true)
})
it('toasts a success message', () => {
expect(toastSuccessSpy).toBeCalledWith('settings.showAmountGDD')
})
})
})
describe('second call to API', () => {
beforeEach(async () => {
mockAPICall.mockResolvedValue({
data: {
updateUserInfos: {
validValues: 1,
},
},
})
jest.clearAllMocks()
wrapper.vm.$store.state.hideAmountGDD = true
await wrapper.find('div.border-left svg').trigger('click')
await flushPromises()
})
it('calls the API', () => {
expect(mockAPICall).toBeCalledWith(
expect.objectContaining({
mutation: updateUserInfos,
variables: {
hideAmountGDD: false,
},
}),
)
})
it('commits hideAmountGDD to store', () => {
expect(storeCommitMock).toBeCalledWith('hideAmountGDD', false)
})
it('toasts a success message', () => {
expect(toastSuccessSpy).toBeCalledWith('settings.hideAmountGDD')
})
})
})

View File

@ -39,7 +39,7 @@
<b-icon
:icon="hideAmount ? 'eye-slash' : 'eye'"
class="mr-3 gradido-global-border-color-accent pointer hover-icon"
@click="$store.commit('hideAmountGDD', !hideAmount)"
@click="updateHideAmountGDD"
></b-icon>
</b-col>
</b-row>
@ -47,6 +47,8 @@
</div>
</template>
<script>
import { updateUserInfos } from '@/graphql/mutations'
export default {
name: 'GddAmount',
props: {
@ -60,5 +62,27 @@ export default {
return this.$store.state.hideAmountGDD
},
},
methods: {
async updateHideAmountGDD() {
this.$apollo
.mutate({
mutation: updateUserInfos,
variables: {
hideAmountGDD: !this.hideAmount,
},
})
.then(() => {
this.$store.commit('hideAmountGDD', !this.hideAmount)
if (!this.hideAmount) {
this.toastSuccess(this.$t('settings.showAmountGDD'))
} else {
this.toastSuccess(this.$t('settings.hideAmountGDD'))
}
})
.catch((error) => {
this.toastError(error.message)
})
},
},
}
</script>

View File

@ -1,19 +1,30 @@
import { mount } from '@vue/test-utils'
import GdtAmount from './GdtAmount'
import { updateUserInfos } from '@/graphql/mutations'
import flushPromises from 'flush-promises'
import { toastErrorSpy, toastSuccessSpy } from '@test/testSetup'
const localVue = global.localVue
const mockAPICall = jest.fn()
const storeCommitMock = jest.fn()
const state = {
language: 'en',
hideAmountGDT: false,
}
const mocks = {
$store: {
state,
commit: storeCommitMock,
},
$i18n: {
locale: 'en',
},
$apollo: {
mutate: mockAPICall,
},
$t: jest.fn((t) => t),
$n: jest.fn((n) => n),
}
@ -39,5 +50,89 @@ describe('GdtAmount', () => {
it('renders the component gdt-amount', () => {
expect(wrapper.find('div.gdt-amount').exists()).toBe(true)
})
describe('API throws exception', () => {
beforeEach(async () => {
mockAPICall.mockRejectedValue({
message: 'Ouch',
})
jest.clearAllMocks()
await wrapper.find('div.border-left svg').trigger('click')
await flushPromises()
})
it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Ouch')
})
})
describe('API call successful', () => {
beforeEach(async () => {
mockAPICall.mockResolvedValue({
data: {
updateUserInfos: {
validValues: 1,
},
},
})
jest.clearAllMocks()
await wrapper.find('div.border-left svg').trigger('click')
await flushPromises()
})
it('calls the API', () => {
expect(mockAPICall).toBeCalledWith(
expect.objectContaining({
mutation: updateUserInfos,
variables: {
hideAmountGDT: true,
},
}),
)
})
it('commits hideAmountGDT to store', () => {
expect(storeCommitMock).toBeCalledWith('hideAmountGDT', true)
})
it('toasts a success message', () => {
expect(toastSuccessSpy).toBeCalledWith('settings.showAmountGDT')
})
})
})
describe('second call to API', () => {
beforeEach(async () => {
mockAPICall.mockResolvedValue({
data: {
updateUserInfos: {
validValues: 1,
},
},
})
jest.clearAllMocks()
wrapper.vm.$store.state.hideAmountGDT = true
await wrapper.find('div.border-left svg').trigger('click')
await flushPromises()
})
it('calls the API', () => {
expect(mockAPICall).toBeCalledWith(
expect.objectContaining({
mutation: updateUserInfos,
variables: {
hideAmountGDT: false,
},
}),
)
})
it('commits hideAmountGDT to store', () => {
expect(storeCommitMock).toBeCalledWith('hideAmountGDT', false)
})
it('toasts a success message', () => {
expect(toastSuccessSpy).toBeCalledWith('settings.hideAmountGDT')
})
})
})

View File

@ -34,7 +34,7 @@
<b-icon
:icon="hideAmount ? 'eye-slash' : 'eye'"
class="mr-3 gradido-global-border-color-accent pointer hover-icon"
@click="$store.commit('hideAmountGDT', !hideAmount)"
@click="updateHideAmountGDT"
></b-icon>
</b-col>
</b-row>
@ -42,6 +42,8 @@
</div>
</template>
<script>
import { updateUserInfos } from '@/graphql/mutations'
export default {
name: 'GdtAmount',
props: {
@ -54,5 +56,27 @@ export default {
return this.$store.state.hideAmountGDT
},
},
methods: {
async updateHideAmountGDT() {
this.$apollo
.mutate({
mutation: updateUserInfos,
variables: {
hideAmountGDT: !this.hideAmount,
},
})
.then(() => {
this.$store.commit('hideAmountGDT', !this.hideAmount)
if (!this.hideAmount) {
this.toastSuccess(this.$t('settings.showAmountGDT'))
} else {
this.toastSuccess(this.$t('settings.hideAmountGDT'))
}
})
.catch((error) => {
this.toastError(error.message)
})
},
},
}
</script>

View File

@ -31,6 +31,8 @@ export const updateUserInfos = gql`
$password: String
$passwordNew: String
$locale: String
$hideAmountGDD: Boolean
$hideAmountGDT: Boolean
) {
updateUserInfos(
firstName: $firstName
@ -38,6 +40,8 @@ export const updateUserInfos = gql`
password: $password
passwordNew: $passwordNew
language: $locale
hideAmountGDD: $hideAmountGDD
hideAmountGDT: $hideAmountGDT
)
}
`
@ -151,6 +155,8 @@ export const login = gql`
publisherId
isAdmin
creation
hideAmountGDD
hideAmountGDT
}
}
`

View File

@ -14,6 +14,8 @@ export const verifyLogin = gql`
publisherId
isAdmin
creation
hideAmountGDD
hideAmountGDT
}
}
`

View File

@ -269,6 +269,8 @@
"warningText": "Bist du noch da?"
},
"settings": {
"hideAmountGDD": "Dein GDD Betrag ist versteckt.",
"hideAmountGDT": "Dein GDT Betrag ist versteckt.",
"language": {
"changeLanguage": "Sprache ändern",
"de": "Deutsch",
@ -301,7 +303,9 @@
"text": "Speichere nun dein neues Passwort, mit dem du dich zukünftig in deinem Gradido-Konto anmelden kannst."
},
"subtitle": "Wenn du dein Passwort vergessen hast, kannst du es hier zurücksetzen."
}
},
"showAmountGDD": "Dein GDD Betrag ist sichtbar.",
"showAmountGDT": "Dein GDT Betrag ist sichtbar."
},
"signin": "Anmelden",
"signup": "Registrieren",

View File

@ -269,6 +269,8 @@
"warningText": "Are you still there?"
},
"settings": {
"hideAmountGDD": "Your GDD amount is hidden.",
"hideAmountGDT": "Your GDT amount is hidden.",
"language": {
"changeLanguage": "Change language",
"de": "Deutsch",
@ -301,7 +303,9 @@
"text": "Now save your new password, which you can use to log in to your Gradido account in the future."
},
"subtitle": "If you have forgotten your password, you can reset it here."
}
},
"showAmountGDD": "Your GDD amount is visible.",
"showAmountGDT": "Your GDT amount is visible."
},
"signin": "Sign in",
"signup": "Sign up",