Merge pull request #3040 from gradido/2519-new-design-for-settings-and-profile

feat(frontend): new style for settings page
This commit is contained in:
Hannes Heine 2023-06-30 18:27:17 +02:00 committed by GitHub
commit 0f9184d142
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 560 additions and 246 deletions

View File

@ -2,6 +2,9 @@ import { ArgsType, Field, Int } from 'type-graphql'
@ArgsType()
export class CreateUserArgs {
@Field(() => String, { nullable: true })
alias?: string | null
@Field(() => String)
email: string

View File

@ -195,7 +195,15 @@ export class UserResolver {
@Mutation(() => User)
async createUser(
@Args()
{ email, firstName, lastName, language, publisherId = null, redeemCode = null }: CreateUserArgs,
{
alias = null,
email,
firstName,
lastName,
language,
publisherId = null,
redeemCode = null,
}: CreateUserArgs,
): Promise<User> {
logger.addContext('user', 'unknown')
logger.info(
@ -231,6 +239,9 @@ export class UserResolver {
user.lastName = lastName
user.language = language
user.publisherId = publisherId
if (alias && (await validateAlias(alias))) {
user.alias = alias
}
logger.debug('partly faked user', user)
void sendAccountMultiRegistrationEmail({
@ -264,6 +275,9 @@ export class UserResolver {
dbUser.firstName = firstName
dbUser.lastName = lastName
dbUser.language = language
if (alias && (await validateAlias(alias))) {
dbUser.alias = alias
}
dbUser.publisherId = publisherId ?? 0
dbUser.passwordEncryptionType = PasswordEncryptionType.NO_PASSWORD
logger.debug('new dbUser', dbUser)

View File

@ -50,6 +50,7 @@ export const updateUserInfos = gql`
export const createUser = gql`
mutation (
$alias: String
$firstName: String!
$lastName: String!
$email: String!
@ -58,6 +59,7 @@ export const createUser = gql`
$redeemCode: String
) {
createUser(
alias: $alias
email: $email
firstName: $firstName
lastName: $lastName

View File

@ -1,4 +1,5 @@
export interface UserInterface {
alias?: string
email?: string
firstName?: string
lastName?: string

View File

@ -1,6 +1,7 @@
import { UserInterface } from './UserInterface'
export const bobBaumeister: UserInterface = {
alias: 'MeisterBob',
email: 'bob@baumeister.de',
firstName: 'Bob',
lastName: 'der Baumeister',

View File

@ -7,7 +7,7 @@ const profilePage = new ProfilePage()
When('the user opens the change password menu', () => {
cy.get(profilePage.openChangePassword).click()
cy.get(profilePage.newPasswordRepeatInput).should('be.visible')
cy.get(profilePage.submitNewPasswordBtn).should('be.disabled')
cy.get(profilePage.submitNewPasswordBtn).should('have.class','btn-light')
})
When('the user fills the password form with:', (table: DataTable) => {

View File

@ -56,6 +56,8 @@ module.exports = {
'settings.password.subtitle',
'math.asterisk',
'/pageTitle./',
'error.empty-transactionlist',
'error.no-transactionlist',
],
enableFix: false,
},

View File

@ -1,5 +1,5 @@
<template>
<div id="app">
<div id="app" ref="app" :class="darkMode ? 'dark-mode' : ''">
<div :class="$route.meta.requiresAuth ? 'appContent' : ''">
<component :is="$route.meta.requiresAuth ? 'DashboardLayout' : 'AuthLayout'" />
<div class="goldrand position-fixed fixed-bottom zindex1000"></div>
@ -13,6 +13,11 @@ import AuthLayout from '@/layouts/AuthLayout'
export default {
name: 'App',
computed: {
darkMode() {
return this.$store.state.darkMode
},
},
components: {
DashboardLayout,
AuthLayout,

View File

@ -3,7 +3,7 @@ $mode-toggle-bg: #262626;
#app {
&.dark-mode {
background-color: black;
background-color: $dark;
color: #fff;
}
}

View File

@ -17,6 +17,7 @@
:placeholder="placeholder"
:type="showPassword ? 'text' : 'password'"
:state="validated ? valid : false"
data-test="password-input-field"
></b-form-input>
<b-input-group-append>
<b-button

View File

@ -0,0 +1,47 @@
import { mount } from '@vue/test-utils'
import InputUsername from './InputUsername'
const localVue = global.localVue
describe('UserName Form', () => {
let wrapper
const mocks = {
$t: jest.fn((t) => t),
$store: {
state: {
username: '',
},
},
}
const propsData = {
value: '',
unique: false,
}
const Wrapper = () => {
return mount(InputUsername, { localVue, mocks, propsData })
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('renders the component', () => {
expect(wrapper.find('[data-test="username"]').exists()).toBeTruthy()
})
describe('currentValue', () => {
beforeEach(async () => {
wrapper = Wrapper()
await wrapper.setProps({ value: 'petra' })
await wrapper.find('[data-test="username"]').setValue('petra')
})
it('emits input event with the current value', () => {
expect(wrapper.emitted('input')).toEqual([['petra']])
})
})
})
})

View File

@ -1,42 +1,59 @@
<template>
<validation-provider
tag="div"
:rules="rules"
:name="name"
:bails="!showAllErrors"
:immediate="immediate"
vid="username"
v-slot="{ errors, valid, validated, ariaInput, ariaMsg }"
>
<b-form-group :label-for="labelFor">
<b-form-input
v-model="currentValue"
v-bind="ariaInput"
:id="labelFor"
:name="name"
:placeholder="placeholder"
type="text"
:state="validated ? valid : false"
autocomplete="off"
></b-form-input>
<b-form-invalid-feedback v-bind="ariaMsg">
<div v-if="showAllErrors">
<span v-for="error in errors" :key="error">
{{ error }}
<br />
</span>
</div>
<div v-else>
{{ errors[0] }}
</div>
</b-form-invalid-feedback>
</b-form-group>
</validation-provider>
<div class="input-username">
<validation-provider
tag="div"
:rules="rules"
:name="name"
:bails="!showAllErrors"
:immediate="immediate"
vid="username"
v-slot="{ errors, valid, validated, ariaInput, ariaMsg }"
>
<b-form-group :label="$t('form.username')">
<b-input-group>
<b-form-input
v-model="currentValue"
v-bind="ariaInput"
:id="labelFor"
:name="name"
:placeholder="placeholder"
type="text"
:state="validated ? valid : false"
autocomplete="off"
data-test="username"
></b-form-input>
<b-input-group-append>
<b-button
size="lg"
text="Button"
variant="secondary"
icon="x-lg"
@click="$emit('set-is-edit', false)"
>
<b-icon-x-circle></b-icon-x-circle>
</b-button>
</b-input-group-append>
</b-input-group>
<b-form-invalid-feedback v-bind="ariaMsg">
<div v-if="showAllErrors">
<span v-for="error in errors" :key="error">
{{ error }}
<br />
</span>
</div>
<div v-else>
{{ errors[0] }}
</div>
</b-form-invalid-feedback>
</b-form-group>
</validation-provider>
</div>
</template>
<script>
export default {
name: 'InputUsername',
props: {
isEdit: { type: Boolean, default: false },
rules: {
default: () => {
return {

View File

@ -68,10 +68,10 @@ export default {
},
})
.then(() => {
// toast success message
this.toastSuccess(this.$t('settings.language.success'))
})
.catch(() => {
// toast error message
this.toastSuccess(this.$t('error'))
})
}
},

View File

@ -69,7 +69,7 @@ describe('Sidebar', () => {
})
it('has nav-item "navigation.settings" in navbar', () => {
expect(wrapper.findAll('ul').at(1).findAll('.nav-item').at(0).text()).toEqual(
expect(wrapper.find('[data-test="settings-menu"]').find('span').text()).toBe(
'navigation.settings',
)
})
@ -92,7 +92,7 @@ describe('Sidebar', () => {
})
it('has nav-item "navigation.settings" in navbar', () => {
expect(wrapper.findAll('ul').at(1).findAll('.nav-item').at(0).text()).toEqual(
expect(wrapper.find('[data-test="settings-menu"]').find('span').text()).toBe(
'navigation.settings',
)
})

View File

@ -35,9 +35,17 @@
</b-nav>
<hr class="m-3" />
<b-nav vertical class="w-100">
<b-nav-item to="/settings" class="mb-3" active-class="activeRoute">
<b-nav-item
to="/settings"
class="mb-3"
active-class="activeRoute"
data-test="settings-menu"
>
<b-img src="/img/svg/settings.svg" height="20" class="svg-icon" />
<span class="ml-2">{{ $t('navigation.settings') }}</span>
<b-badge v-if="!$store.state.username" variant="warning">
{{ $t('settings.newSettings') }}
</b-badge>
</b-nav-item>
<b-nav-item
class="mb-3 text-light"

View File

@ -17,7 +17,7 @@ describe('UserName Form', () => {
$t: jest.fn((t) => t),
$store: {
state: {
username: 'peter',
username: null,
},
commit: storeCommitMock,
},
@ -36,121 +36,142 @@ describe('UserName Form', () => {
})
it('renders the component', () => {
expect(wrapper.find('div#username_form').exists()).toBeTruthy()
expect(wrapper.find('div#username_form').exists()).toBe(true)
})
it('has an edit icon', () => {
expect(wrapper.find('svg.bi-pencil').exists()).toBeTruthy()
})
describe('has no username', () => {
// it('renders the username', () => {
// expect(wrapper.find('[data-test="username-input-group"]')).toBe(true)
// })
it('renders the username', () => {
expect(wrapper.findAll('div.col').at(2).text()).toBe('peter')
it('has a component username change ', () => {
expect(wrapper.findComponent({ name: 'InputUsername' }).exists()).toBe(true)
})
})
describe('edit username', () => {
describe('change / edit username', () => {
beforeEach(async () => {
await wrapper.find('svg.bi-pencil').trigger('click')
wrapper.vm.isEdit = true
})
it('shows an cancel icon', () => {
expect(wrapper.find('svg.bi-x-circle').exists()).toBeTruthy()
it('has no the username', () => {
expect(wrapper.find('[data-test="username-input-group"]')).toBeTruthy()
})
it('closes the input when cancel icon is clicked', async () => {
await wrapper.find('svg.bi-x-circle').trigger('click')
expect(wrapper.find('input').exists()).toBeFalsy()
it('has a component username change ', () => {
expect(wrapper.findComponent({ name: 'InputUsername' }).exists()).toBeTruthy()
})
it('does not change the username when cancel is clicked', async () => {
await wrapper.find('input').setValue('petra')
await wrapper.find('svg.bi-x-circle').trigger('click')
expect(wrapper.findAll('div.col').at(2).text()).toBe('peter')
it('first step is username empty ', () => {
expect(wrapper.vm.username).toEqual('')
})
it('has a submit button', () => {
expect(wrapper.find('button[type="submit"]').exists()).toBeTruthy()
})
it('does not enable submit button when data is not changed', async () => {
await wrapper.find('form').trigger('keyup')
expect(wrapper.find('button[type="submit"]').attributes('disabled')).toBe('disabled')
})
describe('successfull submit', () => {
describe('change / edit username', () => {
beforeEach(async () => {
mockAPIcall.mockResolvedValue({
data: {
updateUserInfos: {
validValues: 3,
},
},
})
jest.clearAllMocks()
await wrapper.find('input').setValue('petra')
await wrapper.find('form').trigger('keyup')
await wrapper.find('button[type="submit"]').trigger('click')
await flushPromises()
mocks.$store.state.username = ''
await wrapper.setData({ isEdit: true })
})
it('calls the API', () => {
expect(mockAPIcall).toBeCalledWith(
expect.objectContaining({
variables: {
alias: 'petra',
},
}),
it('first step is isEdit false ', () => {
expect(wrapper.vm.isEdit).toEqual(true)
})
it(' has username-alert text ', () => {
expect(wrapper.find('[data-test="username-alert"]').text()).toBe(
'settings.username.no-username',
)
})
it('commits username to store', () => {
expect(storeCommitMock).toBeCalledWith('username', 'petra')
})
it('toasts a success message', () => {
expect(toastSuccessSpy).toBeCalledWith('settings.username.change-success')
})
it('has an edit button again', () => {
expect(wrapper.find('svg.bi-pencil').exists()).toBeTruthy()
it('has a submit button with disabled true', () => {
expect(wrapper.find('[data-test="submit-username-button"]').exists()).toBe(false)
})
})
describe('submit results in server error', () => {
describe('edit username', () => {
beforeEach(async () => {
mockAPIcall.mockRejectedValue({
message: 'Error',
})
jest.clearAllMocks()
await wrapper.find('input').setValue('petra')
await wrapper.find('form').trigger('keyup')
await wrapper.find('button[type="submit"]').trigger('click')
await flushPromises()
await wrapper.setData({ username: 'petra' })
})
it('calls the API', () => {
expect(mockAPIcall).toBeCalledWith(
expect.objectContaining({
variables: {
alias: 'petra',
it('has a submit button', () => {
expect(wrapper.find('[data-test="submit-username-button"]').exists()).toBe(true)
})
describe('successfull submit', () => {
beforeEach(async () => {
mockAPIcall.mockResolvedValue({
data: {
updateUserInfos: {
validValues: 3,
},
},
}),
)
})
jest.clearAllMocks()
await wrapper.find('input').setValue('petra')
await wrapper.find('form').trigger('keyup')
await wrapper.find('[data-test="submit-username-button"]').trigger('click')
await flushPromises()
})
it('calls the API', () => {
expect(mockAPIcall).toBeCalledWith(
expect.objectContaining({
variables: {
alias: 'petra',
},
}),
)
})
it('commits username to store', () => {
expect(storeCommitMock).toBeCalledWith('username', 'petra')
})
it('toasts a success message', () => {
expect(toastSuccessSpy).toBeCalledWith('settings.username.change-success')
})
})
it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Error')
describe('submit results in server error', () => {
beforeEach(async () => {
mockAPIcall.mockRejectedValue({
message: 'Error',
})
jest.clearAllMocks()
await wrapper.find('input').setValue('petra')
await wrapper.find('form').trigger('keyup')
await wrapper.find('[data-test="submit-username-button"]').trigger('click')
await flushPromises()
})
it('calls the API', () => {
expect(mockAPIcall).toBeCalledWith(
expect.objectContaining({
variables: {
alias: 'petra',
},
}),
)
})
it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Error')
})
})
})
})
describe('no username in store', () => {
beforeEach(() => {
mocks.$store.state.username = null
wrapper = Wrapper()
})
describe('has a username', () => {
beforeEach(async () => {
mocks.$store.state.username = 'petra'
})
it('displays an information why to enter a username', () => {
expect(wrapper.findAll('div.col').at(2).text()).toBe('settings.username.no-username')
it('has isEdit true', () => {
expect(wrapper.vm.isEdit).toBe(true)
})
it(' has no username-alert text ', () => {
expect(wrapper.find('[data-test="username-alert"]').exists()).toBe(false)
})
it('has no component username change ', () => {
expect(wrapper.findComponent({ name: 'InputUsername' }).exists()).toBe(false)
})
})
})
})

View File

@ -1,38 +1,20 @@
<template>
<b-card id="username_form" class="card-border-radius card-background-gray">
<div>
<b-row class="mb-4 text-right">
<b-col class="text-right">
<a
class="cursor-pointer"
@click="showUserData ? (showUserData = !showUserData) : cancelEdit()"
>
<span class="pointer mr-3">{{ $t('settings.username.change-username') }}</span>
<b-icon v-if="showUserData" class="pointer ml-3" icon="pencil"></b-icon>
<b-icon v-else icon="x-circle" class="pointer ml-3" variant="danger"></b-icon>
</a>
</b-col>
</b-row>
<div id="username_form">
<div v-if="$store.state.username">
<label>{{ $t('form.username') }}</label>
<b-input-group class="mb-3" data-test="username-input-group">
<b-form-input
v-model="username"
readonly
data-test="username-input-readonly"
></b-form-input>
</b-input-group>
</div>
<div>
<div v-else>
<validation-observer ref="usernameObserver" v-slot="{ handleSubmit, invalid }">
<b-form @submit.stop.prevent="handleSubmit(onSubmit)">
<b-row class="mb-3">
<b-col class="col-12">
<small>
<b>{{ $t('form.username') }}</b>
</small>
</b-col>
<b-col v-if="showUserData" class="col-12">
<span v-if="username">
{{ username }}
</span>
<div v-else class="alert">
{{ $t('settings.username.no-username') }}
</div>
</b-col>
<b-col v-else class="col-12">
<input-username
v-model="username"
:name="$t('form.username')"
@ -40,10 +22,18 @@
:showAllErrors="true"
:unique="true"
:rules="rules"
:isEdit="isEdit"
@set-is-edit="setIsEdit"
data-test="component-input-username"
/>
</b-col>
<b-col class="col-12">
<div v-if="!username" class="alert" data-test="username-alert">
{{ $t('settings.username.no-username') }}
</div>
</b-col>
</b-row>
<b-row class="text-right" v-if="!showUserData">
<b-row class="text-right" v-if="newUsername">
<b-col>
<div class="text-right" ref="submitButton">
<b-button
@ -51,6 +41,7 @@
@click="onSubmit"
type="submit"
:disabled="disabled(invalid)"
data-test="submit-username-button"
>
{{ $t('form.save') }}
</b-button>
@ -60,7 +51,7 @@
</b-form>
</validation-observer>
</div>
</b-card>
</div>
</template>
<script>
import { updateUserInfos } from '@/graphql/mutations'
@ -73,7 +64,7 @@ export default {
},
data() {
return {
showUserData: true,
isEdit: false,
username: this.$store.state.username || '',
usernameUnique: false,
rules: {
@ -87,10 +78,6 @@ export default {
}
},
methods: {
cancelEdit() {
this.username = this.$store.state.username || ''
this.showUserData = true
},
async onSubmit(event) {
event.preventDefault()
this.$apollo
@ -102,7 +89,6 @@ export default {
})
.then(() => {
this.$store.commit('username', this.username)
this.showUserData = true
this.toastSuccess(this.$t('settings.username.change-success'))
})
.catch((error) => {
@ -112,6 +98,10 @@ export default {
disabled(invalid) {
return !this.newUsername || invalid
},
setIsEdit(bool) {
this.username = this.$store.state.username
this.isEdit = bool
},
},
computed: {
newUsername() {

View File

@ -38,22 +38,22 @@ describe('UserCard_Newsletter', () => {
})
it('renders the component', () => {
expect(wrapper.find('div#formusernewsletter').exists()).toBeTruthy()
expect(wrapper.find('div.formusernewsletter').exists()).toBeTruthy()
})
it('has an edit BFormCheckbox switch', () => {
expect(wrapper.find('.Test-BFormCheckbox').exists()).toBeTruthy()
expect(wrapper.find('[test="BFormCheckbox"]').exists()).toBeTruthy()
})
describe('unsubscribe with success', () => {
beforeEach(async () => {
await wrapper.setData({ newsletterState: true })
wrapper.setData({ newsletterState: true })
mockAPIcall.mockResolvedValue({
data: {
unsubscribeNewsletter: true,
},
})
await wrapper.find('input').setChecked(false)
wrapper.find('input').setChecked(false)
})
it('calls the unsubscribe mutation', () => {

View File

@ -1,30 +1,13 @@
<template>
<b-card id="formusernewsletter" class="card-border-radius card-background-gray">
<div>
<b-row class="mb-3">
<b-col class="mb-2 col-12">
<small>
<b>{{ $t('settings.newsletter.newsletter') }}</b>
</small>
</b-col>
<b-col class="col-12">
<b-form-checkbox
class="Test-BFormCheckbox"
v-model="newsletterState"
name="check-button"
switch
@change="onSubmit"
>
{{
newsletterState
? $t('settings.newsletter.newsletterTrue')
: $t('settings.newsletter.newsletterFalse')
}}
</b-form-checkbox>
</b-col>
</b-row>
</div>
</b-card>
<div class="formusernewsletter">
<b-form-checkbox
test="BFormCheckbox"
v-model="newsletterState"
name="check-button"
switch
@change="onSubmit"
></b-form-checkbox>
</div>
</template>
<script>
import { subscribeNewsletter, unsubscribeNewsletter } from '@/graphql/mutations'

View File

@ -17,7 +17,7 @@
</div>
<div v-if="!showPassword">
<validation-observer ref="observer" v-slot="{ handleSubmit }">
<validation-observer ref="observer" v-slot="{ handleSubmit, invalid }">
<b-form @submit.stop.prevent="handleSubmit(onSubmit)">
<b-row class="mb-2">
<b-col>
@ -34,9 +34,9 @@
<div class="text-right">
<b-button
type="submit"
:variant="disabled ? 'light' : 'success'"
:variant="invalid ? 'light' : 'success'"
class="mt-4"
:disabled="disabled"
:disabled="invalid && disabled"
data-test="submit-new-password-btn"
>
{{ $t('form.save') }}
@ -101,10 +101,7 @@ export default {
},
computed: {
disabled() {
return !(
this.form.newPassword.password !== '' &&
this.form.newPassword.password === this.form.newPassword.passwordRepeat
)
return this.form.newPassword.password !== this.form.newPassword.passwordRepeat
},
},
}

View File

@ -169,8 +169,8 @@
"thx": "Danke",
"to": "bis",
"to1": "an",
"username": "Nutzername",
"username-placeholder": "Gebe einen eindeutigen Nutzernamen ein",
"username": "Benutzername",
"username-placeholder": "Wähle deinen Benutzernamen",
"validation": {
"gddCreationTime": "Das Feld {_field_} muss eine Zahl zwischen {min} und {max} mit höchstens einer Nachkommastelle sein",
"gddSendAmount": "Das Feld {_field_} muss eine Zahl zwischen {min} und {max} mit höchstens zwei Nachkommastellen sein",
@ -280,6 +280,7 @@
"settings": "Einstellungen",
"transactions": "Deine Transaktionen"
},
"PersonalDetails": "Persönliche Angaben",
"qrCode": "QR Code",
"send_gdd": "GDD versenden",
"send_per_link": "GDD versenden per Link",
@ -291,8 +292,10 @@
"warningText": "Bist du noch da?"
},
"settings": {
"emailInfo": "Kann aktuell noch nicht geändert werden.",
"hideAmountGDD": "Dein GDD Betrag ist versteckt.",
"hideAmountGDT": "Dein GDT Betrag ist versteckt.",
"info": "Transaktionen können nun per Benutzername oder E-Mail-Adresse getätigt werden.",
"language": {
"changeLanguage": "Sprache ändern",
"de": "Deutsch",
@ -304,8 +307,11 @@
},
"name": {
"change-name": "Name ändern",
"change-success": "Dein Name wurde erfolgreich geändert."
"change-success": "Dein Name wurde erfolgreich geändert.",
"enterFirstname": "Vorname eingeben",
"enterLastname": "Nachname eingeben"
},
"newSettings": "Neue Einstellungen",
"newsletter": {
"newsletter": "Informationen per E-Mail",
"newsletterFalse": "Du erhältst keine Informationen per E-Mail.",
@ -330,8 +336,7 @@
"showAmountGDT": "Dein GDT Betrag ist sichtbar.",
"username": {
"change-success": "Dein Nutzername wurde erfolgreich geändert.",
"change-username": "Nutzername ändern",
"no-username": "Bitte gebe einen Nutzernamen ein. Damit hilfst du anderen Benutzern dich zu finden, ohne deine Email preisgeben zu müssen."
"no-username": "Bitte gib einen Benutzernamen ein. Damit hilfst du anderen Benutzern dich zu finden, ohne deine E-Mail-Adresse preisgeben zu müssen."
}
},
"signin": "Anmelden",

View File

@ -170,7 +170,7 @@
"to": "to",
"to1": "to",
"username": "Username",
"username-placeholder": "Enter a unique username",
"username-placeholder": "Choose your username",
"validation": {
"gddCreationTime": "The field {_field_} must be a number between {min} and {max} with at most one decimal place.",
"gddSendAmount": "The {_field_} field must be a number between {min} and {max} with at most two digits after the decimal point",
@ -280,6 +280,7 @@
"settings": "Settings",
"transactions": "Your transactions"
},
"PersonalDetails": "Personal details",
"qrCode": "QR Code",
"send_gdd": "Send GDD",
"send_per_link": "Send GDD via Link",
@ -291,8 +292,10 @@
"warningText": "Are you still there?"
},
"settings": {
"emailInfo": "NCannot be changed at this time.",
"hideAmountGDD": "Your GDD amount is hidden.",
"hideAmountGDT": "Your GDT amount is hidden.",
"info": "Transactions can now be made by username or email address.",
"language": {
"changeLanguage": "Change language",
"de": "Deutsch",
@ -304,8 +307,11 @@
},
"name": {
"change-name": "Change name",
"change-success": "Your name has been successfully changed."
"change-success": "Your name has been successfully changed.",
"enterFirstname": "Enter your firstname",
"enterLastname": "Enter your lastname"
},
"newSettings": "New Settings",
"newsletter": {
"newsletter": "Information by email",
"newsletterFalse": "You will not receive any information by email.",
@ -330,8 +336,7 @@
"showAmountGDT": "Your GDT amount is visible.",
"username": {
"change-success": "Your username has been changed successfully.",
"change-username": "Change username",
"no-username": "Please enter a username. This helps other users to find you without exposing your email."
"no-username": "Please enter a username. This will help other users find you without having to reveal your email address."
}
},
"signin": "Sign in",

View File

@ -1,42 +1,140 @@
import { shallowMount } from '@vue/test-utils'
import { mount } from '@vue/test-utils'
import Settings from './Settings'
import flushPromises from 'flush-promises'
import { toastSuccessSpy } from '@test/testSetup'
const localVue = global.localVue
const mockAPIcall = jest.fn()
const storeCommitMock = jest.fn()
describe('Settings', () => {
let wrapper
const mocks = {
$t: jest.fn((t) => t),
$store: {
state: {
darkMode: true,
firstName: 'John',
lastName: 'Doe',
email: 'john.doe@test.com',
language: 'en',
newsletterState: false,
},
commit: storeCommitMock,
},
$apollo: {
mutate: mockAPIcall,
},
}
const Wrapper = () => {
return shallowMount(Settings, { localVue, mocks })
return mount(Settings, { localVue, mocks })
}
describe('shallow Mount', () => {
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('has a user card', () => {
expect(wrapper.findComponent({ name: 'UserCard' }).exists()).toBeTruthy()
})
it('has a user first and last name form', () => {
expect(wrapper.findComponent({ name: 'UserData' }).exists()).toBeTruthy()
})
it('has a user change language form', () => {
expect(wrapper.findComponent({ name: 'UserLanguage' }).exists()).toBeTruthy()
expect(wrapper.findComponent({ name: 'LanguageSwitch' }).exists()).toBeTruthy()
})
it('has a user change password form', () => {
expect(wrapper.findComponent({ name: 'UserPassword' }).exists()).toBeTruthy()
})
it('has a user change newsletter form', () => {
expect(wrapper.findComponent({ name: 'UserNewsletter' }).exists()).toBeTruthy()
describe('isDisabled', () => {
it('returns false when firstName and lastName match the state', async () => {
// wrapper.vm.firstName = 'John'
// wrapper.vm.lastName = 'Doe'
wrapper.find('[data-test="firstname"]').setValue('John')
wrapper.find('[data-test="lastname"]').setValue('Doe')
await wrapper.find('[data-test="firstname"]').trigger('keyup')
const result = wrapper.find('[data-test="submit-userdata"]')
expect(result.exists()).toBe(false)
})
it('returns true when either firstName or lastName do not match the state', async () => {
wrapper.find('[data-test="firstname"]').setValue('Janer')
wrapper.find('[data-test="lastname"]').setValue('Does')
await wrapper.find('[data-test="firstname"]').trigger('keyup')
const result = wrapper.find('[data-test="submit-userdata"]')
expect(result.exists()).toBe(true)
})
})
describe('successfull submit', () => {
beforeEach(async () => {
wrapper.find('[data-test="firstname"]').setValue('Janer')
wrapper.find('[data-test="lastname"]').setValue('Does')
mockAPIcall.mockResolvedValue({
data: {
updateUserInfos: {
validValues: 3,
},
},
})
})
it('Cange first and lastname', async () => {
await wrapper.find('[data-test="submit-userdata"]').trigger('click')
await flushPromises()
expect(mockAPIcall).toBeCalledWith(
expect.objectContaining({
variables: {
firstName: 'Janer',
lastName: 'Does',
},
}),
)
})
it('commits firstname to store', () => {
expect(storeCommitMock).toBeCalledWith('firstName', 'Janer')
})
it('commits lastname to store', () => {
expect(storeCommitMock).toBeCalledWith('lastName', 'Does')
})
it('toasts a success message', () => {
expect(toastSuccessSpy).toBeCalledWith('settings.name.change-success')
})
})
// TODO: describe('darkMode style', () => {
// it('default darkMode is true', () => {
// expect(wrapper.vm.darkMode).toBe(true)
// })
// describe('dark mode is false', () => {
// beforeEach(() => {
// wrapper.vm.darkMode = false
// })
// it('commits darkMode to store', () => {
// expect(storeCommitMock).toBeCalledWith('setDarkMode', false)
// })
// it('toasts a success message', () => {
// expect(toastSuccessSpy).toBeCalledWith('settings.modeLight')
// })
// describe('set dark mode is true', () => {
// beforeEach(() => {
// wrapper.vm.darkMode = true
// })
// // Test case 1: Test setting dark mode
// test('darkMode sets the dark mode', () => {
// expect(storeCommitMock).toBeCalledWith('setDarkMode', true)
// })
// })
// })
// })
})
})

View File

@ -1,30 +1,99 @@
<template>
<div class="container bg-white appBoxShadow p-3 mt--3">
<user-card :balance="balance" :transactionCount="transactionCount"></user-card>
<user-data />
<div class="card bg-white gradido-border-radius appBoxShadow p-4 mt--3">
<div class="h2">{{ $t('PersonalDetails') }}</div>
<div class="m-4 text-small">
{{ $t('settings.info') }}
</div>
<b-row>
<b-col cols="12" md="6" lg="6">
<user-name />
</b-col>
<b-col cols="12" md="6" lg="6">
<b-form-group :label="$t('form.email')" :description="$t('settings.emailInfo')">
<b-form-input v-model="email" readonly></b-form-input>
</b-form-group>
</b-col>
</b-row>
<hr />
<user-name />
<b-form>
<b-row class="mt-3">
<b-col cols="12" md="6" lg="6">
<label>{{ $t('form.firstname') }}</label>
<b-form-input
v-model="firstName"
:placeholder="$t('settings.name.enterFirstname')"
data-test="firstname"
trim
></b-form-input>
</b-col>
<b-col cols="12" md="6" lg="6">
<label>{{ $t('form.lastname') }}</label>
<b-form-input
v-model="lastName"
:placeholder="$t('settings.name.enterLastname')"
data-test="lastname"
trim
></b-form-input>
</b-col>
</b-row>
<div v-if="!isDisabled" class="mt-4 pt-4 text-center">
<b-button
type="submit"
variant="primary"
@click.prevent="onSubmit"
data-test="submit-userdata"
>
{{ $t('form.save') }}
</b-button>
</div>
</b-form>
<hr />
<b-row>
<b-col cols="12" md="6" lg="6">{{ $t('language') }}</b-col>
<b-col cols="12" md="6" lg="6" class="text-right">
<user-language />
</b-col>
</b-row>
<hr />
<div class="h3 mt-5">{{ $t('form.password') }}</div>
<user-password />
<hr />
<user-language />
<hr />
<user-newsletter />
<b-row class="mb-5">
<b-col cols="12" md="6" lg="6">
{{ $t('settings.newsletter.newsletter') }}
<div class="text-small">
{{
newsletterState
? $t('settings.newsletter.newsletterTrue')
: $t('settings.newsletter.newsletterFalse')
}}
</div>
</b-col>
<b-col cols="12" md="6" lg="6" class="text-right">
<user-newsletter />
</b-col>
</b-row>
<!-- TODO<b-row>
<b-col cols="12" md="6" lg="6">{{ $t('settings.darkMode') }}</b-col>
<b-col cols="12" md="6" lg="6" class="text-right">
<b-form-checkbox v-model="darkMode" name="dark-mode" switch aligne></b-form-checkbox>
</b-col>
</b-row> -->
</div>
</template>
<script>
import UserCard from '@/components/UserSettings/UserCard'
import UserData from '@/components/UserSettings/UserData'
import UserName from '@/components/UserSettings/UserName'
import UserName from '@/components/UserSettings/UserName.vue'
import UserPassword from '@/components/UserSettings/UserPassword'
import UserLanguage from '@/components/UserSettings/UserLanguage'
import UserNewsletter from '@/components/UserSettings/UserNewsletter'
import UserLanguage from '@/components/LanguageSwitch2.vue'
import UserNewsletter from '@/components/UserSettings/UserNewsletter.vue'
import { updateUserInfos } from '@/graphql/mutations'
export default {
name: 'Profile',
components: {
UserCard,
UserData,
UserName,
UserPassword,
UserLanguage,
@ -34,13 +103,53 @@ export default {
balance: { type: Number, default: 0 },
transactionCount: { type: Number, default: 0 },
},
methods: {
updateTransactions(pagination) {
this.$emit('update-transactions', pagination)
data() {
const { state } = this.$store
const { darkMode, firstName, lastName, email, newsletterState } = state
return {
darkMode,
username: '',
firstName,
lastName,
email,
newsletterState,
mutation: '',
variables: {},
}
},
computed: {
isDisabled() {
const { firstName, lastName } = this.$store.state
return firstName === this.firstName && lastName === this.lastName
},
},
created() {
this.updateTransactions(0)
// TODO: watch: {
// darkMode(val) {
// this.$store.commit('setDarkMode', this.darkMode)
// this.toastSuccess(
// this.darkMode ? this.$t('settings.modeDark') : this.$t('settings.modeLight'),
// )
// },
// },
methods: {
async onSubmit(key) {
try {
await this.$apollo.mutate({
mutation: updateUserInfos,
variables: {
firstName: this.firstName,
lastName: this.lastName,
},
})
this.$store.commit('firstName', this.firstName)
this.$store.commit('lastName', this.lastName)
this.showUserData = true
this.toastSuccess(this.$t('settings.name.change-success'))
} catch (error) {}
},
},
}
</script>

View File

@ -4,7 +4,6 @@ import createPersistedState from 'vuex-persistedstate'
import { localeChanged } from 'vee-validate'
import i18n from '@/i18n.js'
import jwtDecode from 'jwt-decode'
Vue.use(Vuex)
export const mutations = {
@ -56,6 +55,9 @@ export const mutations = {
email: (state, email) => {
state.email = email || ''
},
setDarkMode: (state, darkMode) => {
state.darkMode = !!darkMode
},
}
export const actions = {
@ -71,6 +73,7 @@ export const actions = {
commit('isAdmin', data.isAdmin)
commit('hideAmountGDD', data.hideAmountGDD)
commit('hideAmountGDT', data.hideAmountGDT)
commit('setDarkMode', data.darkMode)
},
logout: ({ commit, state }) => {
commit('token', null)
@ -85,6 +88,7 @@ export const actions = {
commit('hideAmountGDD', false)
commit('hideAmountGDT', true)
commit('email', '')
commit('setDarkMode', false)
localStorage.clear()
},
}
@ -114,6 +118,7 @@ try {
hideAmountGDD: null,
hideAmountGDT: null,
email: '',
darkMode: false,
},
getters: {},
// Syncronous mutation of the state

View File

@ -199,7 +199,7 @@ describe('Vuex store', () => {
it('calls eleven commits', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenCalledTimes(11)
expect(commit).toHaveBeenCalledTimes(12)
})
it('commits gradidoID', () => {
@ -264,7 +264,7 @@ describe('Vuex store', () => {
it('calls twelve commits', () => {
logout({ commit, state })
expect(commit).toHaveBeenCalledTimes(12)
expect(commit).toHaveBeenCalledTimes(13)
})
it('commits token', () => {