Feature: Agent Profile Update with avatar (#449)
* Feature: Agent Profile Update with avatar * Add Update Profile with name, avatar, email and password
This commit is contained in:
parent
e61ba95cf7
commit
c4e2a84f65
25 changed files with 584 additions and 133 deletions
1
__mocks__/fileMock.js
Normal file
1
__mocks__/fileMock.js
Normal file
|
@ -0,0 +1 @@
|
|||
module.exports = '';
|
|
@ -1,5 +1,5 @@
|
|||
class Api::V1::ProfilesController < Api::BaseController
|
||||
before_action :fetch_user
|
||||
before_action :set_user
|
||||
|
||||
def show
|
||||
render json: @user
|
||||
|
@ -7,12 +7,11 @@ class Api::V1::ProfilesController < Api::BaseController
|
|||
|
||||
def update
|
||||
@user.update!(profile_params)
|
||||
render json: @user
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_user
|
||||
def set_user
|
||||
@user = current_user
|
||||
end
|
||||
|
||||
|
|
|
@ -18,8 +18,7 @@ export default {
|
|||
},
|
||||
|
||||
mounted() {
|
||||
this.$store.dispatch('set_user');
|
||||
this.$store.dispatch('validityCheck');
|
||||
this.$store.dispatch('setUser');
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -1,29 +1,10 @@
|
|||
/* eslint no-console: 0 */
|
||||
/* global axios */
|
||||
/* eslint no-undef: "error" */
|
||||
/* eslint-env browser */
|
||||
/* eslint no-unused-expressions: ["error", { "allowShortCircuit": true }] */
|
||||
|
||||
import moment from 'moment';
|
||||
import Cookies from 'js-cookie';
|
||||
import endPoints from './endPoints';
|
||||
import { frontendURL } from '../helper/URLHelper';
|
||||
|
||||
const setAuthCredentials = response => {
|
||||
const expiryDate = moment.unix(response.headers.expiry);
|
||||
Cookies.set('auth_data', response.headers, {
|
||||
expires: expiryDate.diff(moment(), 'days'),
|
||||
});
|
||||
Cookies.set('user', response.data.data, {
|
||||
expires: expiryDate.diff(moment(), 'days'),
|
||||
});
|
||||
};
|
||||
|
||||
const clearCookiesOnLogout = () => {
|
||||
Cookies.remove('auth_data');
|
||||
Cookies.remove('user');
|
||||
window.location = frontendURL('login');
|
||||
};
|
||||
import { setAuthCredentials, clearCookiesOnLogout } from '../store/utils/api';
|
||||
|
||||
export default {
|
||||
login(creds) {
|
||||
|
@ -60,20 +41,7 @@ export default {
|
|||
},
|
||||
validityCheck() {
|
||||
const urlData = endPoints('validityCheck');
|
||||
const fetchPromise = new Promise((resolve, reject) => {
|
||||
axios
|
||||
.get(urlData.url)
|
||||
.then(response => {
|
||||
resolve(response);
|
||||
})
|
||||
.catch(error => {
|
||||
if (error.response.status === 401) {
|
||||
clearCookiesOnLogout();
|
||||
}
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
return fetchPromise;
|
||||
return axios.get(urlData.url);
|
||||
},
|
||||
logout() {
|
||||
const urlData = endPoints('logout');
|
||||
|
@ -136,13 +104,7 @@ export default {
|
|||
password,
|
||||
})
|
||||
.then(response => {
|
||||
const expiryDate = moment.unix(response.headers.expiry);
|
||||
Cookies.set('auth_data', response.headers, {
|
||||
expires: expiryDate.diff(moment(), 'days'),
|
||||
});
|
||||
Cookies.set('user', response.data.data, {
|
||||
expires: expiryDate.diff(moment(), 'days'),
|
||||
});
|
||||
setAuthCredentials(response);
|
||||
resolve(response);
|
||||
})
|
||||
.catch(error => {
|
||||
|
@ -155,4 +117,22 @@ export default {
|
|||
const urlData = endPoints('resetPassword');
|
||||
return axios.post(urlData.url, { email });
|
||||
},
|
||||
|
||||
profileUpdate({ name, email, password, password_confirmation, avatar }) {
|
||||
const formData = new FormData();
|
||||
if (name) {
|
||||
formData.append('profile[name]', name);
|
||||
}
|
||||
if (email) {
|
||||
formData.append('profile[email]', email);
|
||||
}
|
||||
if (password && password_confirmation) {
|
||||
formData.append('profile[password]', password);
|
||||
formData.append('profile[password_confirmation]', password_confirmation);
|
||||
}
|
||||
if (avatar) {
|
||||
formData.append('profile[avatar]', avatar);
|
||||
}
|
||||
return axios.put(endPoints('profileUpdate').url, formData);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -10,6 +10,9 @@ const endPoints = {
|
|||
validityCheck: {
|
||||
url: '/auth/validate_token',
|
||||
},
|
||||
profileUpdate: {
|
||||
url: '/api/v1/profile',
|
||||
},
|
||||
logout: {
|
||||
url: 'auth/sign_out',
|
||||
},
|
||||
|
|
|
@ -42,12 +42,21 @@
|
|||
class="dropdown-pane top"
|
||||
>
|
||||
<ul class="vertical dropdown menu">
|
||||
<li><a href="#" @click.prevent="logout()">Logout</a></li>
|
||||
<li>
|
||||
<router-link to="/app/profile/settings">
|
||||
{{ $t('SIDEBAR.PROFILE_SETTINGS') }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" @click.prevent="logout()">
|
||||
{{ $t('SIDEBAR.LOGOUT') }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</transition>
|
||||
<div class="current-user" @click.prevent="showOptions()">
|
||||
<thumbnail :src="gravatarUrl()" :username="currentUser.name" />
|
||||
<thumbnail :src="currentUser.avatar_url" :username="currentUser.name" />
|
||||
<div class="current-user--data">
|
||||
<h3 class="current-user--name">
|
||||
{{ currentUser.name }}
|
||||
|
@ -65,7 +74,6 @@
|
|||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import md5 from 'md5';
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
|
||||
import adminMixin from '../../mixins/isAdmin';
|
||||
|
@ -99,6 +107,7 @@ export default {
|
|||
daysLeft: 'getTrialLeft',
|
||||
subscriptionData: 'getSubscription',
|
||||
inboxes: 'inboxes/getInboxes',
|
||||
currentUser: 'getCurrentUser',
|
||||
}),
|
||||
accessibleMenuItems() {
|
||||
// get all keys in menuGroup
|
||||
|
@ -144,9 +153,6 @@ export default {
|
|||
})),
|
||||
};
|
||||
},
|
||||
currentUser() {
|
||||
return Auth.getCurrentUser();
|
||||
},
|
||||
dashboardPath() {
|
||||
return frontendURL('dashboard');
|
||||
},
|
||||
|
@ -174,10 +180,6 @@ export default {
|
|||
this.$store.dispatch('inboxes/get');
|
||||
},
|
||||
methods: {
|
||||
gravatarUrl() {
|
||||
const hash = md5(this.currentUser.email);
|
||||
return `${window.WootConstants.GRAVATAR_URL}${hash}?default=404`;
|
||||
},
|
||||
filterBillingRoutes(menuItems) {
|
||||
return menuItems.filter(
|
||||
menuItem => !menuItem.toState.includes('billing')
|
||||
|
@ -185,6 +187,9 @@ export default {
|
|||
},
|
||||
filterMenuItemsByRole(menuItems) {
|
||||
const { role } = this.currentUser;
|
||||
if (!role) {
|
||||
return [];
|
||||
}
|
||||
return menuItems.filter(
|
||||
menuItem =>
|
||||
window.roleWiseRoutes[role].indexOf(menuItem.toStateName) > -1
|
||||
|
|
|
@ -8,6 +8,8 @@ export default {
|
|||
'inbox_conversation',
|
||||
'settings_account_reports',
|
||||
'billing_deactivated',
|
||||
'profile_settings',
|
||||
'profile_settings_index',
|
||||
],
|
||||
menuItems: {
|
||||
assignedToMe: {
|
||||
|
|
|
@ -37,11 +37,11 @@
|
|||
},
|
||||
"AUTH": {
|
||||
"TITLE": "Channels",
|
||||
"DESC": "Currently we support website live chat widgets and Facebook Pages as platforms. We have more platforms like Twitter, Telegram and Line in the works, which will be out soon."
|
||||
"DESC": "Currently we support Website live chat widgets, Facebook Pages and Twitter profiles as platforms. We have more platforms like Whatsapp, Email, Telegram and Line in the works, which will be out soon."
|
||||
},
|
||||
"AGENTS": {
|
||||
"TITLE": "Agents",
|
||||
"DESC": "Here you can add agents to manage your newly created inbox. Only these selected agents will have access to your inbox. Agents whcih are not part of this inbox will not be able to see or respond to messages in this inbox when they login. <br> <b>PS:</b> As an administrator, if you need access to all inboxes, you should add yourself as agent to all inboxes that you create."
|
||||
"DESC": "Here you can add agents to manage your newly created inbox. Only these selected agents will have access to your inbox. Agents which are not part of this inbox will not be able to see or respond to messages in this inbox when they login. <br> <b>PS:</b> As an administrator, if you need access to all inboxes, you should add yourself as agent to all inboxes that you create."
|
||||
},
|
||||
"DETAILS": {
|
||||
"TITLE": "Inbox Details",
|
||||
|
|
|
@ -10,6 +10,7 @@ import { default as _login } from './login.json';
|
|||
import { default as _report } from './report.json';
|
||||
import { default as _resetPassword } from './resetPassword.json';
|
||||
import { default as _setNewPassword } from './setNewPassword.json';
|
||||
import { default as _settings } from './settings.json';
|
||||
import { default as _signup } from './signup.json';
|
||||
|
||||
export default {
|
||||
|
@ -24,5 +25,6 @@ export default {
|
|||
..._report,
|
||||
..._resetPassword,
|
||||
..._setNewPassword,
|
||||
..._settings,
|
||||
..._signup,
|
||||
};
|
||||
|
|
50
app/javascript/dashboard/i18n/locale/en/settings.json
Normal file
50
app/javascript/dashboard/i18n/locale/en/settings.json
Normal file
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"PROFILE_SETTINGS": {
|
||||
"LINK": "Profile Settings",
|
||||
"TITLE": "Profile Settings",
|
||||
"BTN_TEXT": "Update Profile",
|
||||
"AFTER_EMAIL_CHANGED": "Your profile has been updated successfully, please login again as your login credentials are changed",
|
||||
"FORM": {
|
||||
"AVATAR": "Profile Image",
|
||||
"ERROR": "Please fix form errors",
|
||||
"REMOVE_IMAGE": "Remove",
|
||||
"UPLOAD_IMAGE": "Upload image",
|
||||
"UPDATE_IMAGE": "Update image",
|
||||
"PROFILE_SECTION" : {
|
||||
"TITLE": "Profile",
|
||||
"NOTE": "Your email address is your identity and is used to log in."
|
||||
},
|
||||
"PASSWORD_SECTION" : {
|
||||
"TITLE": "Password",
|
||||
"NOTE": "Updating your password would reset your logins in multiple devices."
|
||||
},
|
||||
"PROFILE_IMAGE":{
|
||||
"LABEL": "Profile Image"
|
||||
},
|
||||
"NAME": {
|
||||
"LABEL": "Your name",
|
||||
"ERROR": "Please enter a valid name",
|
||||
"PLACEHOLDER": "Please enter your name, this would be displayed in conversations"
|
||||
},
|
||||
"EMAIL": {
|
||||
"LABEL": "Your email address",
|
||||
"ERROR": "Please enter a valid email address",
|
||||
"PLACEHOLDER": "Please enter your email address, this would be displayed in conversations"
|
||||
},
|
||||
"PASSWORD": {
|
||||
"LABEL": "Password",
|
||||
"ERROR": "Please enter a password of length 6 or more",
|
||||
"PLACEHOLDER": "Please enter a new password"
|
||||
},
|
||||
"PASSWORD_CONFIRMATION": {
|
||||
"LABEL": "Confirm new password",
|
||||
"ERROR": "Confirm password should match the password",
|
||||
"PLACEHOLDER": "Please re-enter your password"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SIDEBAR": {
|
||||
"PROFILE_SETTINGS": "Profile Settings",
|
||||
"LOGOUT": "Logout"
|
||||
}
|
||||
}
|
|
@ -1,23 +1,40 @@
|
|||
<template>
|
||||
<form class="login-box medium-4 column align-self-middle" v-on:submit.prevent="login()">
|
||||
<form
|
||||
class="login-box medium-4 column align-self-middle"
|
||||
@submit.prevent="login()"
|
||||
>
|
||||
<div class="column log-in-form">
|
||||
<h4>{{$t('SET_NEW_PASSWORD.TITLE')}}</h4>
|
||||
<label :class="{ 'error': $v.credentials.password.$error }">
|
||||
{{$t('LOGIN.PASSWORD.LABEL')}}
|
||||
<input type="password" v-bind:placeholder="$t('SET_NEW_PASSWORD.PASSWORD.PLACEHOLDER')" v-model.trim="credentials.password" @input="$v.credentials.password.$touch">
|
||||
<span class="message" v-if="$v.credentials.password.$error">
|
||||
{{$t('SET_NEW_PASSWORD.PASSWORD.ERROR')}}
|
||||
<h4>{{ $t('SET_NEW_PASSWORD.TITLE') }}</h4>
|
||||
<label :class="{ error: $v.credentials.password.$error }">
|
||||
{{ $t('LOGIN.PASSWORD.LABEL') }}
|
||||
<input
|
||||
v-model.trim="credentials.password"
|
||||
type="password"
|
||||
:placeholder="$t('SET_NEW_PASSWORD.PASSWORD.PLACEHOLDER')"
|
||||
@input="$v.credentials.password.$touch"
|
||||
/>
|
||||
<span v-if="$v.credentials.password.$error" class="message">
|
||||
{{ $t('SET_NEW_PASSWORD.PASSWORD.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
<label :class="{ 'error': $v.credentials.confirmPassword.$error }">
|
||||
{{$t('SET_NEW_PASSWORD.CONFIRM_PASSWORD.LABEL')}}
|
||||
<input type="password" v-bind:placeholder="$t('SET_NEW_PASSWORD.CONFIRM_PASSWORD.PLACEHOLDER')" v-model.trim="credentials.confirmPassword" @input="$v.credentials.confirmPassword.$touch">
|
||||
<span class="message" v-if="$v.credentials.confirmPassword.$error">
|
||||
{{$t('SET_NEW_PASSWORD.CONFIRM_PASSWORD.ERROR')}}
|
||||
<label :class="{ error: $v.credentials.confirmPassword.$error }">
|
||||
{{ $t('SET_NEW_PASSWORD.CONFIRM_PASSWORD.LABEL') }}
|
||||
<input
|
||||
v-model.trim="credentials.confirmPassword"
|
||||
type="password"
|
||||
:placeholder="$t('SET_NEW_PASSWORD.CONFIRM_PASSWORD.PLACEHOLDER')"
|
||||
@input="$v.credentials.confirmPassword.$touch"
|
||||
/>
|
||||
<span v-if="$v.credentials.confirmPassword.$error" class="message">
|
||||
{{ $t('SET_NEW_PASSWORD.CONFIRM_PASSWORD.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
<woot-submit-button
|
||||
:disabled="$v.credentials.password.$invalid || $v.credentials.confirmPassword.$invalid || newPasswordAPI.showLoading"
|
||||
:disabled="
|
||||
$v.credentials.password.$invalid ||
|
||||
$v.credentials.confirmPassword.$invalid ||
|
||||
newPasswordAPI.showLoading
|
||||
"
|
||||
:button-text="$t('SET_NEW_PASSWORD.SUBMIT')"
|
||||
:loading="newPasswordAPI.showLoading"
|
||||
button-class="expanded"
|
||||
|
@ -99,7 +116,7 @@ export default {
|
|||
resetPasswordToken: this.resetPasswordToken,
|
||||
};
|
||||
Auth.setNewPassword(credentials)
|
||||
.then((res) => {
|
||||
.then(res => {
|
||||
if (res.status === 200) {
|
||||
window.location = res.data.redirect_url;
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<span>{{ headerTitle }}</span>
|
||||
</h1>
|
||||
<router-link
|
||||
v-if="showNewButton && showButton && currentRole"
|
||||
v-if="showNewButton && showButton && isAdmin"
|
||||
:to="buttonRoute"
|
||||
class="button icon success nice button--fixed-right-top"
|
||||
>
|
||||
|
@ -17,18 +17,30 @@
|
|||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import BackButton from '../../../components/widgets/BackButton';
|
||||
import Auth from '../../../api/auth';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BackButton,
|
||||
},
|
||||
props: {
|
||||
headerTitle: String,
|
||||
buttonRoute: String,
|
||||
buttonText: String,
|
||||
icon: String,
|
||||
headerTitle: {
|
||||
default: '',
|
||||
type: String,
|
||||
},
|
||||
buttonRoute: {
|
||||
default: '',
|
||||
type: String,
|
||||
},
|
||||
buttonText: {
|
||||
default: '',
|
||||
type: String,
|
||||
},
|
||||
icon: {
|
||||
default: '',
|
||||
type: String,
|
||||
},
|
||||
showButton: Boolean,
|
||||
showNewButton: Boolean,
|
||||
hideButtonRoutes: {
|
||||
|
@ -39,11 +51,14 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentUser: 'getCurrentUser',
|
||||
}),
|
||||
iconClass() {
|
||||
return `icon ${this.icon} header--icon`;
|
||||
},
|
||||
currentRole() {
|
||||
const { role } = Auth.getCurrentUser();
|
||||
isAdmin() {
|
||||
const { role } = this.currentUser;
|
||||
return role === 'administrator';
|
||||
},
|
||||
},
|
||||
|
|
|
@ -108,7 +108,6 @@
|
|||
/* global bus */
|
||||
|
||||
import { mapGetters } from 'vuex';
|
||||
import md5 from 'md5';
|
||||
import Thumbnail from '../../../../components/widgets/Thumbnail';
|
||||
|
||||
import AddAgent from './AddAgent';
|
||||
|
@ -182,10 +181,6 @@ export default {
|
|||
agent => agent.role === 'administrator' && agent.confirmed
|
||||
);
|
||||
},
|
||||
gravatarUrl(email) {
|
||||
const hash = md5(email);
|
||||
return `${window.WootConstants.GRAVATAR_URL}${hash}?default=404`;
|
||||
},
|
||||
// Edit Function
|
||||
openAddPopup() {
|
||||
this.showAddPopup = true;
|
||||
|
|
|
@ -0,0 +1,224 @@
|
|||
<template>
|
||||
<div class="columns profile--settings ">
|
||||
<form @submit.prevent="updateUser">
|
||||
<div class="small-12 row profile--settings--row">
|
||||
<div class="columns small-3 ">
|
||||
<p class="section--title">
|
||||
{{ $t('PROFILE_SETTINGS.FORM.PROFILE_SECTION.TITLE') }}
|
||||
</p>
|
||||
<p>{{ $t('PROFILE_SETTINGS.FORM.PROFILE_SECTION.NOTE') }}</p>
|
||||
</div>
|
||||
<div class="columns small-9">
|
||||
<label>
|
||||
{{ $t('PROFILE_SETTINGS.FORM.PROFILE_IMAGE.LABEL') }}
|
||||
<thumbnail size="80px" :src="avatarUrl"></thumbnail>
|
||||
<input
|
||||
id="file"
|
||||
ref="file"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
@change="handleImageUpload"
|
||||
/>
|
||||
</label>
|
||||
<label :class="{ error: $v.name.$error }">
|
||||
{{ $t('PROFILE_SETTINGS.FORM.NAME.LABEL') }}
|
||||
<input
|
||||
v-model="name"
|
||||
type="text"
|
||||
:placeholder="$t('PROFILE_SETTINGS.FORM.NAME.PLACEHOLDER')"
|
||||
@input="$v.name.$touch"
|
||||
/>
|
||||
<span v-if="$v.name.$error" class="message">
|
||||
{{ $t('PROFILE_SETTINGS.FORM.NAME.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
<label :class="{ error: $v.email.$error }">
|
||||
{{ $t('PROFILE_SETTINGS.FORM.EMAIL.LABEL') }}
|
||||
<input
|
||||
v-model.trim="email"
|
||||
type="email"
|
||||
:placeholder="$t('PROFILE_SETTINGS.FORM.EMAIL.PLACEHOLDER')"
|
||||
@input="$v.email.$touch"
|
||||
/>
|
||||
<span v-if="$v.email.$error" class="message">
|
||||
{{ $t('PROFILE_SETTINGS.FORM.EMAIL.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="profile--settings--row row">
|
||||
<div class="columns small-3 ">
|
||||
<p class="section--title">
|
||||
{{ $t('PROFILE_SETTINGS.FORM.PASSWORD_SECTION.TITLE') }}
|
||||
</p>
|
||||
<p>{{ $t('PROFILE_SETTINGS.FORM.PASSWORD_SECTION.NOTE') }}</p>
|
||||
</div>
|
||||
<div class="columns small-9">
|
||||
<label :class="{ error: $v.password.$error }">
|
||||
{{ $t('PROFILE_SETTINGS.FORM.PASSWORD.LABEL') }}
|
||||
<input
|
||||
v-model.trim="password"
|
||||
type="password"
|
||||
:placeholder="$t('PROFILE_SETTINGS.FORM.PASSWORD.PLACEHOLDER')"
|
||||
@input="$v.password.$touch"
|
||||
/>
|
||||
<span v-if="$v.password.$error" class="message">
|
||||
{{ $t('PROFILE_SETTINGS.FORM.PASSWORD.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
<label :class="{ error: $v.passwordConfirmation.$error }">
|
||||
{{ $t('PROFILE_SETTINGS.FORM.PASSWORD_CONFIRMATION.LABEL') }}
|
||||
<input
|
||||
v-model.trim="passwordConfirmation"
|
||||
type="password"
|
||||
:placeholder="
|
||||
$t('PROFILE_SETTINGS.FORM.PASSWORD_CONFIRMATION.PLACEHOLDER')
|
||||
"
|
||||
@input="$v.passwordConfirmation.$touch"
|
||||
/>
|
||||
<span v-if="$v.passwordConfirmation.$error" class="message">
|
||||
{{ $t('PROFILE_SETTINGS.FORM.PASSWORD_CONFIRMATION.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<woot-submit-button
|
||||
class="button nice success button--fixed-right-top"
|
||||
:button-text="$t('PROFILE_SETTINGS.BTN_TEXT')"
|
||||
:loading="isUpdating"
|
||||
>
|
||||
</woot-submit-button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* global bus */
|
||||
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import { required, minLength, email } from 'vuelidate/lib/validators';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { clearCookiesOnLogout } from '../../../../api/auth';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
avatarFile: '',
|
||||
avatarUrl: '',
|
||||
name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
passwordConfirmation: '',
|
||||
isUpdating: false,
|
||||
};
|
||||
},
|
||||
validations: {
|
||||
name: {
|
||||
required,
|
||||
},
|
||||
email: {
|
||||
required,
|
||||
email,
|
||||
},
|
||||
password: {
|
||||
minLength: minLength(6),
|
||||
},
|
||||
passwordConfirmation: {
|
||||
minLength: minLength(6),
|
||||
isEqPassword(value) {
|
||||
if (value !== this.password) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentUser: 'getCurrentUser',
|
||||
currentUserId: 'getCurrentUserID',
|
||||
}),
|
||||
},
|
||||
watch: {
|
||||
currentUserId(newCurrentUserId, prevCurrentUserId) {
|
||||
if (prevCurrentUserId !== newCurrentUserId) {
|
||||
this.initializeUser();
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.currentUserId) {
|
||||
this.initializeUser();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initializeUser() {
|
||||
this.name = this.currentUser.name;
|
||||
this.email = this.currentUser.email;
|
||||
this.avatarUrl = this.currentUser.avatar_url;
|
||||
},
|
||||
async updateUser() {
|
||||
this.$v.$touch();
|
||||
if (this.$v.$invalid) {
|
||||
bus.$emit('newToastMessage', this.$t('PROFILE_SETTINGS.FORM.ERROR'));
|
||||
return;
|
||||
}
|
||||
this.isUpdating = true;
|
||||
const hasEmailChanged = this.currentUser.email !== this.email;
|
||||
try {
|
||||
await this.$store.dispatch('updateProfile', {
|
||||
name: this.name,
|
||||
email: this.email,
|
||||
avatar: this.avatarFile,
|
||||
password: this.password,
|
||||
password_confirmation: this.passwordConfirmation,
|
||||
});
|
||||
this.isUpdating = false;
|
||||
if (hasEmailChanged) {
|
||||
clearCookiesOnLogout();
|
||||
bus.$emit(
|
||||
'newToastMessage',
|
||||
this.$t('PROFILE_SETTINGS.AFTER_EMAIL_CHANGED')
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
this.isUpdating = false;
|
||||
}
|
||||
},
|
||||
handleImageUpload(event) {
|
||||
const [file] = event.target.files;
|
||||
this.avatarFile = file;
|
||||
this.avatarUrl = URL.createObjectURL(file);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~dashboard/assets/scss/variables.scss';
|
||||
@import '~dashboard/assets/scss/mixins.scss';
|
||||
|
||||
.profile--settings {
|
||||
padding: 24px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.profile--settings--row {
|
||||
@include border-normal-bottom;
|
||||
padding: 16px;
|
||||
.small-3 {
|
||||
padding: 16px 16px 16px 0;
|
||||
}
|
||||
.small-9 {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.section--title {
|
||||
color: $color-woot;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,27 @@
|
|||
import SettingsContent from '../Wrapper';
|
||||
import Index from './Index.vue';
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
|
||||
export default {
|
||||
routes: [
|
||||
{
|
||||
path: frontendURL('profile'),
|
||||
name: 'profile_settings',
|
||||
roles: ['administrator', 'agent'],
|
||||
component: SettingsContent,
|
||||
props: {
|
||||
headerTitle: 'PROFILE_SETTINGS.TITLE',
|
||||
icon: 'ion-compose',
|
||||
showNewButton: false,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'settings',
|
||||
name: 'profile_settings_index',
|
||||
component: Index,
|
||||
roles: ['administrator', 'agent'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
|
@ -1,10 +1,11 @@
|
|||
import agent from './agents/agent.routes';
|
||||
import inbox from './inbox/inbox.routes';
|
||||
import canned from './canned/canned.routes';
|
||||
import reports from './reports/reports.routes';
|
||||
import billing from './billing/billing.routes';
|
||||
import Auth from '../../../api/auth';
|
||||
import { frontendURL } from '../../../helper/URLHelper';
|
||||
import agent from './agents/agent.routes';
|
||||
import Auth from '../../../api/auth';
|
||||
import billing from './billing/billing.routes';
|
||||
import canned from './canned/canned.routes';
|
||||
import inbox from './inbox/inbox.routes';
|
||||
import profile from './profile/profile.routes';
|
||||
import reports from './reports/reports.routes';
|
||||
|
||||
export default {
|
||||
routes: [
|
||||
|
@ -19,10 +20,11 @@ export default {
|
|||
return frontendURL('settings/canned-response');
|
||||
},
|
||||
},
|
||||
...inbox.routes,
|
||||
...agent.routes,
|
||||
...canned.routes,
|
||||
...reports.routes,
|
||||
...billing.routes,
|
||||
...canned.routes,
|
||||
...inbox.routes,
|
||||
...profile.routes,
|
||||
...reports.routes,
|
||||
],
|
||||
};
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
/* eslint no-console: 0 */
|
||||
/* eslint-env browser */
|
||||
/* eslint no-param-reassign: 0 */
|
||||
import axios from 'axios';
|
||||
import moment from 'moment';
|
||||
|
@ -9,7 +7,8 @@ import router from '../../routes';
|
|||
import authAPI from '../../api/auth';
|
||||
import createAxios from '../../helper/APIHelper';
|
||||
import actionCable from '../../helper/actionCable';
|
||||
// initial state
|
||||
import { setUser, getHeaderExpiry, clearCookiesOnLogout } from '../utils/api';
|
||||
|
||||
const state = {
|
||||
currentUser: {
|
||||
id: null,
|
||||
|
@ -27,15 +26,19 @@ const state = {
|
|||
};
|
||||
|
||||
// getters
|
||||
const getters = {
|
||||
isLoggedIn(_state) {
|
||||
return _state.currentUser.id !== null;
|
||||
export const getters = {
|
||||
isLoggedIn($state) {
|
||||
return !!$state.currentUser.id;
|
||||
},
|
||||
|
||||
getCurrentUserID(_state) {
|
||||
return _state.currentUser.id;
|
||||
},
|
||||
|
||||
getCurrentUser(_state) {
|
||||
return _state.currentUser;
|
||||
},
|
||||
|
||||
getSubscription(_state) {
|
||||
return _state.currentUser.subscription === undefined
|
||||
? null
|
||||
|
@ -53,7 +56,7 @@ const getters = {
|
|||
};
|
||||
|
||||
// actions
|
||||
const actions = {
|
||||
export const actions = {
|
||||
login({ commit }, credentials) {
|
||||
return new Promise((resolve, reject) => {
|
||||
authAPI
|
||||
|
@ -70,14 +73,21 @@ const actions = {
|
|||
});
|
||||
});
|
||||
},
|
||||
validityCheck(context) {
|
||||
if (context.getters.isLoggedIn) {
|
||||
authAPI.validityCheck();
|
||||
async validityCheck(context) {
|
||||
try {
|
||||
const response = await authAPI.validityCheck();
|
||||
setUser(response.data.payload.data, getHeaderExpiry(response));
|
||||
context.commit(types.default.SET_CURRENT_USER);
|
||||
} catch (error) {
|
||||
if (error.response.status === 401) {
|
||||
clearCookiesOnLogout();
|
||||
}
|
||||
}
|
||||
},
|
||||
set_user({ commit }) {
|
||||
setUser({ commit, dispatch }) {
|
||||
if (authAPI.isLoggedIn()) {
|
||||
commit(types.default.SET_CURRENT_USER);
|
||||
dispatch('validityCheck');
|
||||
} else {
|
||||
commit(types.default.CLEAR_USER);
|
||||
}
|
||||
|
@ -85,6 +95,15 @@ const actions = {
|
|||
logout({ commit }) {
|
||||
commit(types.default.CLEAR_USER);
|
||||
},
|
||||
updateProfile: async ({ commit }, params) => {
|
||||
try {
|
||||
const response = await authAPI.profileUpdate(params);
|
||||
setUser(response.data, getHeaderExpiry(response));
|
||||
commit(types.default.SET_CURRENT_USER);
|
||||
} catch (error) {
|
||||
// Ignore error
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// mutations
|
||||
|
@ -93,8 +112,12 @@ const mutations = {
|
|||
_state.currentUser.id = null;
|
||||
},
|
||||
[types.default.SET_CURRENT_USER](_state) {
|
||||
Object.assign(_state.currentUser, authAPI.getAuthData());
|
||||
Object.assign(_state.currentUser, authAPI.getCurrentUser());
|
||||
const currentUser = {
|
||||
...authAPI.getAuthData(),
|
||||
...authAPI.getCurrentUser(),
|
||||
};
|
||||
|
||||
Vue.set(_state, 'currentUser', currentUser);
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
import axios from 'axios';
|
||||
import Cookies from 'js-cookie';
|
||||
import { actions } from '../../auth';
|
||||
import * as types from '../../../mutation-types';
|
||||
import { setUser, clearCookiesOnLogout } from '../../../utils/api';
|
||||
import '../../../../routes';
|
||||
|
||||
jest.mock('../../../../routes', () => {});
|
||||
jest.mock('../../../utils/api', () => ({
|
||||
setUser: jest.fn(),
|
||||
clearCookiesOnLogout: jest.fn(),
|
||||
getHeaderExpiry: jest.fn(),
|
||||
}));
|
||||
jest.mock('js-cookie', () => ({
|
||||
getJSON: jest.fn(),
|
||||
}));
|
||||
|
||||
const commit = jest.fn();
|
||||
const dispatch = jest.fn();
|
||||
global.axios = axios;
|
||||
jest.mock('axios');
|
||||
|
||||
describe('#actions', () => {
|
||||
describe('#validityCheck', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
axios.get.mockResolvedValue({
|
||||
data: { payload: { data: { id: 1, name: 'John' } } },
|
||||
headers: { expiry: 581842904 },
|
||||
});
|
||||
await actions.validityCheck({ commit });
|
||||
expect(setUser).toHaveBeenCalledTimes(1);
|
||||
expect(commit.mock.calls).toEqual([[types.default.SET_CURRENT_USER]]);
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.get.mockRejectedValue({
|
||||
response: { status: 401 },
|
||||
});
|
||||
await actions.validityCheck({ commit });
|
||||
expect(clearCookiesOnLogout);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#updateProfile', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
axios.put.mockResolvedValue({
|
||||
data: { id: 1, name: 'John' },
|
||||
headers: { expiry: 581842904 },
|
||||
});
|
||||
await actions.updateProfile({ commit }, { name: 'Pranav' });
|
||||
expect(setUser).toHaveBeenCalledTimes(1);
|
||||
expect(commit.mock.calls).toEqual([[types.default.SET_CURRENT_USER]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setUser', () => {
|
||||
it('sends correct actions if user is logged in', async () => {
|
||||
Cookies.getJSON.mockImplementation(() => true);
|
||||
actions.setUser({ commit, dispatch });
|
||||
expect(commit.mock.calls).toEqual([[types.default.SET_CURRENT_USER]]);
|
||||
expect(dispatch.mock.calls).toEqual([['validityCheck']]);
|
||||
});
|
||||
|
||||
it('sends correct actions if user is not logged in', async () => {
|
||||
Cookies.getJSON.mockImplementation(() => false);
|
||||
actions.setUser({ commit, dispatch });
|
||||
expect(commit.mock.calls).toEqual([[types.default.CLEAR_USER]]);
|
||||
expect(dispatch).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,20 @@
|
|||
import { getters } from '../../auth';
|
||||
|
||||
import '../../../../routes';
|
||||
|
||||
jest.mock('../../../../routes', () => {});
|
||||
describe('#getters', () => {
|
||||
it('isLoggedIn', () => {
|
||||
expect(getters.isLoggedIn({ currentUser: { id: null } })).toEqual(false);
|
||||
expect(getters.isLoggedIn({ currentUser: { id: 1 } })).toEqual(true);
|
||||
});
|
||||
|
||||
it('getCurrentUserID', () => {
|
||||
expect(getters.getCurrentUserID({ currentUser: { id: 1 } })).toEqual(1);
|
||||
});
|
||||
it('getCurrentUser', () => {
|
||||
expect(
|
||||
getters.getCurrentUser({ currentUser: { id: 1, name: 'Pranav' } })
|
||||
).toEqual({ id: 1, name: 'Pranav' });
|
||||
});
|
||||
});
|
|
@ -1,6 +1,30 @@
|
|||
/* eslint no-param-reassign: 0 */
|
||||
import moment from 'moment';
|
||||
import Cookies from 'js-cookie';
|
||||
import { frontendURL } from '../../helper/URLHelper';
|
||||
|
||||
export const getLoadingStatus = state => state.fetchAPIloadingStatus;
|
||||
export const setLoadingStatus = (state, status) => {
|
||||
state.fetchAPIloadingStatus = status;
|
||||
};
|
||||
|
||||
export const setUser = (userData, expiryDate) =>
|
||||
Cookies.set('user', userData, {
|
||||
expires: expiryDate.diff(moment(), 'days'),
|
||||
});
|
||||
|
||||
export const getHeaderExpiry = response => moment.unix(response.headers.expiry);
|
||||
|
||||
export const setAuthCredentials = response => {
|
||||
const expiryDate = getHeaderExpiry(response);
|
||||
Cookies.set('auth_data', response.headers, {
|
||||
expires: expiryDate.diff(moment(), 'days'),
|
||||
});
|
||||
setUser(response.data.data, expiryDate);
|
||||
};
|
||||
|
||||
export const clearCookiesOnLogout = () => {
|
||||
Cookies.remove('auth_data');
|
||||
Cookies.remove('user');
|
||||
window.location = frontendURL('login');
|
||||
};
|
||||
|
|
11
app/views/api/v1/profiles/update.json.jbuilder
Normal file
11
app/views/api/v1/profiles/update.json.jbuilder
Normal file
|
@ -0,0 +1,11 @@
|
|||
json.id @user.id
|
||||
json.provider @user.provider
|
||||
json.uid @user.uid
|
||||
json.name @user.name
|
||||
json.nickname @user.nickname
|
||||
json.email @user.email
|
||||
json.account_id @user.account_id
|
||||
json.pubsub_token @user.pubsub_token
|
||||
json.role @user.role
|
||||
json.confirmed @user.confirmed?
|
||||
json.avatar_url @user.avatar_url
|
|
@ -21,6 +21,8 @@ module.exports = {
|
|||
transformIgnorePatterns: ['node_modules/*'],
|
||||
moduleNameMapper: {
|
||||
'^@/(.*)$': '<rootDir>/app/javascript/$1',
|
||||
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
|
||||
'<rootDir>/__mocks__/fileMock.js',
|
||||
},
|
||||
roots: ['<rootDir>/app/javascript'],
|
||||
snapshotSerializers: ['jest-serializer-vue'],
|
||||
|
|
|
@ -30,7 +30,6 @@
|
|||
"ionicons": "~2.0.1",
|
||||
"js-cookie": "^2.2.1",
|
||||
"lodash.groupby": "^4.6.0",
|
||||
"md5": "~2.2.1",
|
||||
"moment": "~2.19.3",
|
||||
"query-string": "5",
|
||||
"spinkit": "~1.2.5",
|
||||
|
|
21
yarn.lock
21
yarn.lock
|
@ -2375,11 +2375,6 @@ chardet@^0.7.0:
|
|||
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
|
||||
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
|
||||
|
||||
charenc@~0.0.1:
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667"
|
||||
integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=
|
||||
|
||||
chart.js@~2.5.0:
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.5.0.tgz#fe6e751a893769f56e72bee5ad91207e1c592957"
|
||||
|
@ -2857,11 +2852,6 @@ cross-spawn@^3.0.0:
|
|||
lru-cache "^4.0.1"
|
||||
which "^1.2.9"
|
||||
|
||||
crypt@~0.0.1:
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b"
|
||||
integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=
|
||||
|
||||
crypto-browserify@^3.11.0:
|
||||
version "3.12.0"
|
||||
resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec"
|
||||
|
@ -5131,7 +5121,7 @@ is-binary-path@^1.0.0:
|
|||
dependencies:
|
||||
binary-extensions "^1.0.0"
|
||||
|
||||
is-buffer@^1.1.5, is-buffer@~1.1.1:
|
||||
is-buffer@^1.1.5:
|
||||
version "1.1.6"
|
||||
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
|
||||
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
|
||||
|
@ -6391,15 +6381,6 @@ md5.js@^1.3.4:
|
|||
inherits "^2.0.1"
|
||||
safe-buffer "^5.1.2"
|
||||
|
||||
md5@~2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/md5/-/md5-2.2.1.tgz#53ab38d5fe3c8891ba465329ea23fac0540126f9"
|
||||
integrity sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=
|
||||
dependencies:
|
||||
charenc "~0.0.1"
|
||||
crypt "~0.0.1"
|
||||
is-buffer "~1.1.1"
|
||||
|
||||
mdn-data@2.0.4:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b"
|
||||
|
|
Loading…
Reference in a new issue