feat: Revamp profile settings screen (#9352)

Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
This commit is contained in:
Muhsin Keloth
2024-05-06 22:33:39 +05:30
committed by GitHub
parent 046c434103
commit 2af0d58deb
23 changed files with 302 additions and 1515 deletions

View File

@@ -1,137 +0,0 @@
<template>
<form @submit.prevent="changePassword()">
<div class="flex flex-col w-full gap-4">
<woot-input
v-model="currentPassword"
type="password"
:styles="inputStyles"
:class="{ error: $v.currentPassword.$error }"
:label="$t('PROFILE_SETTINGS.FORM.CURRENT_PASSWORD.LABEL')"
:placeholder="$t('PROFILE_SETTINGS.FORM.CURRENT_PASSWORD.PLACEHOLDER')"
:error="`${
$v.currentPassword.$error
? $t('PROFILE_SETTINGS.FORM.CURRENT_PASSWORD.ERROR')
: ''
}`"
@input="$v.currentPassword.$touch"
/>
<woot-input
v-model="password"
type="password"
:styles="inputStyles"
:class="{ error: $v.password.$error }"
:label="$t('PROFILE_SETTINGS.FORM.PASSWORD.LABEL')"
:placeholder="$t('PROFILE_SETTINGS.FORM.PASSWORD.PLACEHOLDER')"
:error="`${
$v.password.$error ? $t('PROFILE_SETTINGS.FORM.PASSWORD.ERROR') : ''
}`"
@input="$v.password.$touch"
/>
<woot-input
v-model="passwordConfirmation"
type="password"
:styles="inputStyles"
:class="{ error: $v.passwordConfirmation.$error }"
:label="$t('PROFILE_SETTINGS.FORM.PASSWORD_CONFIRMATION.LABEL')"
:placeholder="
$t('PROFILE_SETTINGS.FORM.PASSWORD_CONFIRMATION.PLACEHOLDER')
"
:error="`${
$v.passwordConfirmation.$error
? $t('PROFILE_SETTINGS.FORM.PASSWORD_CONFIRMATION.ERROR')
: ''
}`"
@input="$v.passwordConfirmation.$touch"
/>
<form-button
type="submit"
color-scheme="primary"
variant="solid"
size="large"
:disabled="isButtonDisabled"
>
{{ $t('PROFILE_SETTINGS.FORM.PASSWORD_SECTION.BTN_TEXT') }}
</form-button>
</div>
</form>
</template>
<script>
import { required, minLength } from 'vuelidate/lib/validators';
import alertMixin from 'shared/mixins/alertMixin';
import { parseAPIErrorResponse } from 'dashboard/store/utils/api';
import FormButton from 'v3/components/Form/Button.vue';
export default {
components: {
FormButton,
},
mixins: [alertMixin],
data() {
return {
currentPassword: '',
password: '',
passwordConfirmation: '',
isPasswordChanging: false,
errorMessage: '',
inputStyles: {
borderRadius: '12px',
padding: '6px 12px',
fontSize: '14px',
marginBottom: '2px',
},
};
},
validations: {
currentPassword: {
required,
},
password: {
minLength: minLength(6),
},
passwordConfirmation: {
minLength: minLength(6),
isEqPassword(value) {
if (value !== this.password) {
return false;
}
return true;
},
},
},
computed: {
isButtonDisabled() {
return (
!this.currentPassword ||
!this.passwordConfirmation ||
!this.$v.passwordConfirmation.isEqPassword
);
},
},
methods: {
async changePassword() {
this.$v.$touch();
if (this.$v.$invalid) {
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.ERROR'));
return;
}
let alertMessage = this.$t('PROFILE_SETTINGS.PASSWORD_UPDATE_SUCCESS');
try {
await this.$store.dispatch('updateProfile', {
password: this.password,
password_confirmation: this.passwordConfirmation,
current_password: this.currentPassword,
});
} catch (error) {
alertMessage =
parseAPIErrorResponse(error) ||
this.$t('RESET_PASSWORD.API.ERROR_MESSAGE');
} finally {
this.showAlert(alertMessage);
}
},
},
};
</script>

View File

@@ -1,264 +0,0 @@
<template>
<div class="flex items-center w-full overflow-y-auto">
<div class="flex flex-col h-full p-5 pt-16 mx-auto my-0 font-inter">
<div class="flex flex-col gap-16 pb-8 sm:max-w-[720px]">
<div class="flex flex-col gap-6">
<h2 class="mt-4 text-2xl font-medium text-ash-900">
{{ $t('PROFILE_SETTINGS.TITLE') }}
</h2>
<user-profile-picture
:src="avatarUrl"
:name="name"
size="72px"
@change="updateProfilePicture"
@delete="deleteProfilePicture"
/>
<user-basic-details
:name="name"
:display-name="displayName"
:email="email"
:email-enabled="!globalConfig.disableUserProfileUpdate"
@update-user="updateProfile"
/>
</div>
<form-section
:title="$t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.TITLE')"
:description="
$t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.NOTE')
"
>
<message-signature
:message-signature="messageSignature"
@update-signature="updateSignature"
/>
</form-section>
<form-section
:title="$t('PROFILE_SETTINGS.FORM.SEND_MESSAGE.TITLE')"
:description="$t('PROFILE_SETTINGS.FORM.SEND_MESSAGE.NOTE')"
>
<div
class="flex flex-col justify-between w-full gap-5 sm:gap-4 sm:flex-row"
>
<button
v-for="hotKey in hotKeys"
:key="hotKey.key"
class="px-0 reset-base"
>
<hot-key-card
:key="hotKey.title"
:title="hotKey.title"
:description="hotKey.description"
:light-image="hotKey.lightImage"
:dark-image="hotKey.darkImage"
:active="isEditorHotKeyEnabled(uiSettings, hotKey.key)"
@click="toggleHotKey(hotKey.key)"
/>
</button>
</div>
</form-section>
<form-section
:title="$t('PROFILE_SETTINGS.FORM.PASSWORD_SECTION.TITLE')"
>
<change-password v-if="!globalConfig.disableUserProfileUpdate" />
</form-section>
<form-section
:title="$t('PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.TITLE')"
:description="
$t('PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.NOTE')
"
>
<audio-notifications />
</form-section>
<form-section :title="$t('PROFILE_SETTINGS.FORM.NOTIFICATIONS.TITLE')">
<notification-preferences />
</form-section>
<form-section
:title="$t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.TITLE')"
:description="
useInstallationName(
$t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.NOTE'),
globalConfig.installationName
)
"
>
<access-token
:value="currentUser.access_token"
@on-copy="onCopyToken"
/>
</form-section>
</div>
</div>
</div>
</template>
<script>
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
import uiSettingsMixin, {
isEditorHotKeyEnabled,
} from 'dashboard/mixins/uiSettings';
import alertMixin from 'shared/mixins/alertMixin';
import { mapGetters } from 'vuex';
import { clearCookiesOnLogout } from 'dashboard/store/utils/api.js';
import { copyTextToClipboard } from 'shared/helpers/clipboard';
import UserProfilePicture from './UserProfilePicture.vue';
import UserBasicDetails from './UserBasicDetails.vue';
import MessageSignature from './MessageSignature.vue';
import HotKeyCard from './HotKeyCard.vue';
import ChangePassword from './ChangePassword.vue';
import NotificationPreferences from './NotificationPreferences.vue';
import AudioNotifications from './AudioNotifications.vue';
import FormSection from 'dashboard/components/FormSection.vue';
import AccessToken from './AccessToken.vue';
export default {
components: {
MessageSignature,
FormSection,
UserProfilePicture,
UserBasicDetails,
HotKeyCard,
ChangePassword,
NotificationPreferences,
AudioNotifications,
AccessToken,
},
mixins: [alertMixin, globalConfigMixin, uiSettingsMixin],
data() {
return {
avatarFile: '',
avatarUrl: '',
name: '',
displayName: '',
email: '',
messageSignature: '',
hotKeys: [
{
key: 'enter',
title: this.$t(
'PROFILE_SETTINGS.FORM.SEND_MESSAGE.CARD.ENTER_KEY.HEADING'
),
description: this.$t(
'PROFILE_SETTINGS.FORM.SEND_MESSAGE.CARD.ENTER_KEY.CONTENT'
),
lightImage: '/assets/images/dashboard/profile/hot-key-enter.svg',
darkImage: '/assets/images/dashboard/profile/hot-key-enter-dark.svg',
},
{
key: 'cmd_enter',
title: this.$t(
'PROFILE_SETTINGS.FORM.SEND_MESSAGE.CARD.CMD_ENTER_KEY.HEADING'
),
description: this.$t(
'PROFILE_SETTINGS.FORM.SEND_MESSAGE.CARD.CMD_ENTER_KEY.CONTENT'
),
lightImage: '/assets/images/dashboard/profile/hot-key-ctrl-enter.svg',
darkImage:
'/assets/images/dashboard/profile/hot-key-ctrl-enter-dark.svg',
},
],
};
},
computed: {
...mapGetters({
currentUser: 'getCurrentUser',
currentUserId: 'getCurrentUserID',
globalConfig: 'globalConfig/get',
}),
},
mounted() {
if (this.currentUserId) {
this.initializeUser();
}
},
methods: {
initializeUser() {
this.name = this.currentUser.name;
this.email = this.currentUser.email;
this.avatarUrl = this.currentUser.avatar_url;
this.displayName = this.currentUser.display_name;
this.messageSignature = this.currentUser.message_signature;
},
isEditorHotKeyEnabled,
async dispatchUpdate(payload, successMessage, errorMessage) {
let alertMessage = '';
try {
await this.$store.dispatch('updateProfile', payload);
alertMessage = successMessage;
return true; // return the value so that the status can be known
} catch (error) {
alertMessage = error?.response?.data?.error
? error.response.data.error
: errorMessage;
return false; // return the value so that the status can be known
} finally {
this.showAlert(alertMessage);
}
},
async updateProfile(userAttributes) {
const { name, email, displayName } = userAttributes;
const hasEmailChanged = this.currentUser.email !== email;
this.name = name || this.name;
this.email = email || this.email;
this.displayName = displayName || this.displayName;
const updatePayload = {
name: this.name,
email: this.email,
displayName: this.displayName,
avatar: this.avatarFile,
};
const success = await this.dispatchUpdate(
updatePayload,
hasEmailChanged
? this.$t('PROFILE_SETTINGS.AFTER_EMAIL_CHANGED')
: this.$t('PROFILE_SETTINGS.UPDATE_SUCCESS'),
this.$t('RESET_PASSWORD.API.ERROR_MESSAGE')
);
if (hasEmailChanged && success) clearCookiesOnLogout();
},
async updateSignature(signature) {
const payload = { message_signature: signature };
let successMessage = this.$t(
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.API_SUCCESS'
);
let errorMessage = this.$t(
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.API_ERROR'
);
await this.dispatchUpdate(payload, successMessage, errorMessage);
},
updateProfilePicture({ file, url }) {
this.avatarFile = file;
this.avatarUrl = url;
},
async deleteProfilePicture() {
try {
await this.$store.dispatch('deleteAvatar');
this.avatarUrl = '';
this.avatarFile = '';
this.showAlert(this.$t('PROFILE_SETTINGS.AVATAR_DELETE_SUCCESS'));
} catch (error) {
this.showAlert(this.$t('PROFILE_SETTINGS.AVATAR_DELETE_FAILED'));
}
},
toggleHotKey(key) {
this.hotKeys = this.hotKeys.map(hotKey =>
hotKey.key === key ? { ...hotKey, active: !hotKey.active } : hotKey
);
this.updateUISettings({ editor_message_key: key });
this.showAlert(
this.$t('PROFILE_SETTINGS.FORM.SEND_MESSAGE.UPDATE_SUCCESS')
);
},
async onCopyToken(value) {
await copyTextToClipboard(value);
this.showAlert(this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
},
},
};
</script>

View File

@@ -1,50 +0,0 @@
<template>
<form class="flex flex-col gap-6" @submit.prevent="updateSignature()">
<woot-message-editor
id="message-signature-input"
v-model="signature"
class="message-editor h-[10rem] !px-3"
:is-format-mode="true"
:placeholder="$t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE.PLACEHOLDER')"
:enabled-menu-options="customEditorMenuList"
:enable-suggestions="false"
:show-image-resize-toolbar="true"
/>
<form-button
type="submit"
color-scheme="primary"
variant="solid"
size="large"
>
{{ $t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.BTN_TEXT') }}
</form-button>
</form>
</template>
<script setup>
import { ref, watch } from 'vue';
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
import { MESSAGE_SIGNATURE_EDITOR_MENU_OPTIONS } from 'dashboard/constants/editor';
import FormButton from 'v3/components/Form/Button.vue';
const props = defineProps({
messageSignature: {
type: String,
default: '',
},
});
const customEditorMenuList = MESSAGE_SIGNATURE_EDITOR_MENU_OPTIONS;
const signature = ref(props.messageSignature);
const emit = defineEmits(['update-signature']);
watch(
() => props.messageSignature,
newValue => {
signature.value = newValue;
}
);
const updateSignature = () => {
emit('update-signature', signature.value);
};
</script>

View File

@@ -1,20 +0,0 @@
import { frontendURL } from 'dashboard/helper/URLHelper';
const Index = () => import('./Index.vue');
export default {
routes: [
{
path: frontendURL('accounts/:accountId/personal'),
name: 'personal_settings',
roles: ['administrator', 'agent'],
component: Index,
props: {
headerTitle: 'PROFILE_SETTINGS.TITLE',
icon: 'edit',
showNewButton: false,
showSidemenuIcon: false,
},
},
],
};

View File

@@ -16,7 +16,7 @@
<input
:id="`radio-${option.value}`"
v-model="selectedValue"
class="shadow cursor-pointer grid place-items-center border-2 border-ash-200 appearance-none rounded-full w-4 h-4 checked:bg-primary-600 before:content-[''] before:bg-primary-600 before:border-4 before:rounded-full before:border-ash-25 checked:before:w-[14px] checked:before:h-[14px] checked:border checked:border-primary-600"
class="shadow-sm cursor-pointer grid place-items-center border-2 border-ash-200 appearance-none rounded-full w-4 h-4 checked:bg-primary-600 before:content-[''] before:bg-primary-600 before:border-4 before:rounded-full before:border-ash-25 checked:before:w-[14px] checked:before:h-[14px] checked:border checked:border-primary-600"
type="radio"
:value="option.value"
/>

View File

@@ -1,82 +1,73 @@
<template>
<form @submit.prevent="changePassword()">
<div
class="profile--settings--row text-black-900 dark:text-slate-300 flex items-center"
>
<div class="w-1/4">
<h4 class="text-lg text-black-900 dark:text-slate-200">
{{ $t('PROFILE_SETTINGS.FORM.PASSWORD_SECTION.TITLE') }}
</h4>
<p>{{ $t('PROFILE_SETTINGS.FORM.PASSWORD_SECTION.NOTE') }}</p>
</div>
<div class="w-[45%] p-4">
<woot-input
v-model="currentPassword"
type="password"
:class="{ error: $v.currentPassword.$error }"
:label="$t('PROFILE_SETTINGS.FORM.CURRENT_PASSWORD.LABEL')"
:placeholder="
$t('PROFILE_SETTINGS.FORM.CURRENT_PASSWORD.PLACEHOLDER')
"
:error="
$v.currentPassword.$error
? $t('PROFILE_SETTINGS.FORM.CURRENT_PASSWORD.ERROR')
: ''
"
@blur="$v.currentPassword.$touch"
/>
<div class="flex flex-col w-full gap-4">
<woot-input
v-model="currentPassword"
type="password"
:styles="inputStyles"
:class="{ error: $v.currentPassword.$error }"
:label="$t('PROFILE_SETTINGS.FORM.CURRENT_PASSWORD.LABEL')"
:placeholder="$t('PROFILE_SETTINGS.FORM.CURRENT_PASSWORD.PLACEHOLDER')"
:error="`${
$v.currentPassword.$error
? $t('PROFILE_SETTINGS.FORM.CURRENT_PASSWORD.ERROR')
: ''
}`"
@input="$v.currentPassword.$touch"
/>
<woot-input
v-model="password"
type="password"
:class="{ error: $v.password.$error }"
:label="$t('PROFILE_SETTINGS.FORM.PASSWORD.LABEL')"
:placeholder="$t('PROFILE_SETTINGS.FORM.PASSWORD.PLACEHOLDER')"
:error="
$v.password.$error ? $t('PROFILE_SETTINGS.FORM.PASSWORD.ERROR') : ''
"
@blur="$v.password.$touch"
/>
<woot-input
v-model="password"
type="password"
:styles="inputStyles"
:class="{ error: $v.password.$error }"
:label="$t('PROFILE_SETTINGS.FORM.PASSWORD.LABEL')"
:placeholder="$t('PROFILE_SETTINGS.FORM.PASSWORD.PLACEHOLDER')"
:error="`${
$v.password.$error ? $t('PROFILE_SETTINGS.FORM.PASSWORD.ERROR') : ''
}`"
@input="$v.password.$touch"
/>
<woot-input
v-model="passwordConfirmation"
type="password"
:class="{ error: $v.passwordConfirmation.$error }"
:label="$t('PROFILE_SETTINGS.FORM.PASSWORD_CONFIRMATION.LABEL')"
:placeholder="
$t('PROFILE_SETTINGS.FORM.PASSWORD_CONFIRMATION.PLACEHOLDER')
"
:error="
$v.passwordConfirmation.$error
? $t('PROFILE_SETTINGS.FORM.PASSWORD_CONFIRMATION.ERROR')
: ''
"
@blur="$v.passwordConfirmation.$touch"
/>
<woot-input
v-model="passwordConfirmation"
type="password"
:styles="inputStyles"
:class="{ error: $v.passwordConfirmation.$error }"
:label="$t('PROFILE_SETTINGS.FORM.PASSWORD_CONFIRMATION.LABEL')"
:placeholder="
$t('PROFILE_SETTINGS.FORM.PASSWORD_CONFIRMATION.PLACEHOLDER')
"
:error="`${
$v.passwordConfirmation.$error
? $t('PROFILE_SETTINGS.FORM.PASSWORD_CONFIRMATION.ERROR')
: ''
}`"
@input="$v.passwordConfirmation.$touch"
/>
<woot-button
:is-loading="isPasswordChanging"
type="submit"
:disabled="
!currentPassword ||
!passwordConfirmation ||
!$v.passwordConfirmation.isEqPassword
"
>
{{ $t('PROFILE_SETTINGS.FORM.PASSWORD_SECTION.BTN_TEXT') }}
</woot-button>
</div>
<form-button
type="submit"
color-scheme="primary"
variant="solid"
size="large"
:disabled="isButtonDisabled"
>
{{ $t('PROFILE_SETTINGS.FORM.PASSWORD_SECTION.BTN_TEXT') }}
</form-button>
</div>
</form>
</template>
<script>
import { required, minLength } from 'vuelidate/lib/validators';
import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin';
import { parseAPIErrorResponse } from 'dashboard/store/utils/api';
import FormButton from 'v3/components/Form/Button.vue';
export default {
components: {
FormButton,
},
mixins: [alertMixin],
data() {
return {
@@ -85,6 +76,12 @@ export default {
passwordConfirmation: '',
isPasswordChanging: false,
errorMessage: '',
inputStyles: {
borderRadius: '12px',
padding: '6px 12px',
fontSize: '14px',
marginBottom: '2px',
},
};
},
validations: {
@@ -105,10 +102,13 @@ export default {
},
},
computed: {
...mapGetters({
currentUser: 'getCurrentUser',
currentUserId: 'getCurrentUserID',
}),
isButtonDisabled() {
return (
!this.currentPassword ||
!this.passwordConfirmation ||
!this.$v.passwordConfirmation.isEqPassword
);
},
},
methods: {
async changePassword() {
@@ -117,38 +117,21 @@ export default {
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.ERROR'));
return;
}
let alertMessage = this.$t('PROFILE_SETTINGS.PASSWORD_UPDATE_SUCCESS');
try {
await this.$store.dispatch('updateProfile', {
password: this.password,
password_confirmation: this.passwordConfirmation,
current_password: this.currentPassword,
});
this.errorMessage = this.$t('PROFILE_SETTINGS.PASSWORD_UPDATE_SUCCESS');
} catch (error) {
this.errorMessage =
alertMessage =
parseAPIErrorResponse(error) ||
this.$t('RESET_PASSWORD.API.ERROR_MESSAGE');
} finally {
this.isPasswordChanging = false;
this.showAlert(this.errorMessage);
this.showAlert(alertMessage);
}
},
},
};
</script>
<style lang="scss">
@import '~dashboard/assets/scss/mixins.scss';
.profile--settings--row {
@include border-normal-bottom;
padding: var(--space-normal);
.small-3 {
padding: var(--space-normal) var(--space-medium) var(--space-normal) 0;
}
.small-9 {
padding: var(--space-normal);
}
}
</style>

View File

@@ -1,152 +1,116 @@
<template>
<div class="overflow-auto p-6">
<form @submit.prevent="updateUser('profile')">
<div
class="flex flex-row border-b border-slate-50 dark:border-slate-700 items-center flex p-4"
>
<div class="w-1/4 py-4 pr-6 ml-0">
<h4 class="text-lg text-black-900 dark:text-slate-200">
{{ $t('PROFILE_SETTINGS.FORM.PROFILE_SECTION.TITLE') }}
</h4>
<p>{{ $t('PROFILE_SETTINGS.FORM.PROFILE_SECTION.NOTE') }}</p>
</div>
<div class="p-4 w-[45%]">
<woot-avatar-uploader
:label="$t('PROFILE_SETTINGS.FORM.PROFILE_IMAGE.LABEL')"
:src="avatarUrl"
@change="handleImageUpload"
/>
<div v-if="showDeleteButton" class="avatar-delete-btn">
<woot-button
type="button"
color-scheme="alert"
variant="hollow"
size="small"
@click="deleteAvatar"
>
{{ $t('PROFILE_SETTINGS.DELETE_AVATAR') }}
</woot-button>
</div>
<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.displayName.$error }">
{{ $t('PROFILE_SETTINGS.FORM.DISPLAY_NAME.LABEL') }}
<input
v-model="displayName"
type="text"
:placeholder="
$t('PROFILE_SETTINGS.FORM.DISPLAY_NAME.PLACEHOLDER')
"
@input="$v.displayName.$touch"
/>
</label>
<label
v-if="!globalConfig.disableUserProfileUpdate"
: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>
<woot-button type="submit" :is-loading="isProfileUpdating">
{{ $t('PROFILE_SETTINGS.BTN_TEXT') }}
</woot-button>
</div>
</div>
</form>
<message-signature />
<div
class="border-b border-slate-50 dark:border-slate-700 items-center flex p-4 text-black-900 dark:text-slate-300 row"
<div class="grid py-16 px-5 font-inter mx-auto gap-16 sm:max-w-[720px]">
<div class="flex flex-col gap-6">
<h2 class="text-2xl font-medium text-ash-900">
{{ $t('PROFILE_SETTINGS.TITLE') }}
</h2>
<user-profile-picture
:src="avatarUrl"
:name="name"
size="72px"
@change="updateProfilePicture"
@delete="deleteProfilePicture"
/>
<user-basic-details
:name="name"
:display-name="displayName"
:email="email"
:email-enabled="!globalConfig.disableUserProfileUpdate"
@update-user="updateProfile"
/>
</div>
<form-section
:title="$t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.TITLE')"
:description="$t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.NOTE')"
>
<div class="w-1/4 py-4 pr-6 ml-0">
<h4 class="text-lg text-black-900 dark:text-slate-200">
{{ $t('PROFILE_SETTINGS.FORM.SEND_MESSAGE.TITLE') }}
</h4>
<p>
{{ $t('PROFILE_SETTINGS.FORM.SEND_MESSAGE.NOTE') }}
</p>
</div>
<div class="p-4 w-[45%] flex gap-4 flex-row">
<message-signature
:message-signature="messageSignature"
@update-signature="updateSignature"
/>
</form-section>
<form-section
:title="$t('PROFILE_SETTINGS.FORM.SEND_MESSAGE.TITLE')"
:description="$t('PROFILE_SETTINGS.FORM.SEND_MESSAGE.NOTE')"
>
<div
class="flex flex-col justify-between w-full gap-5 sm:gap-4 sm:flex-row"
>
<button
v-for="keyOption in keyOptions"
:key="keyOption.key"
class="cursor-pointer p-0"
@click="toggleEditorMessageKey(keyOption.key)"
v-for="hotKey in hotKeys"
:key="hotKey.key"
class="px-0 reset-base"
>
<preview-card
:heading="keyOption.heading"
:content="keyOption.content"
:src="keyOption.src"
:active="isEditorHotKeyEnabled(uiSettings, keyOption.key)"
<hot-key-card
:key="hotKey.title"
:title="hotKey.title"
:description="hotKey.description"
:light-image="hotKey.lightImage"
:dark-image="hotKey.darkImage"
:active="isEditorHotKeyEnabled(uiSettings, hotKey.key)"
@click="toggleHotKey(hotKey.key)"
/>
</button>
</div>
</div>
<change-password v-if="!globalConfig.disableUserProfileUpdate" />
<notification-settings />
<div
class="border-b border-slate-50 dark:border-slate-700 items-center flex p-4 text-black-900 dark:text-slate-300 row"
</form-section>
<form-section :title="$t('PROFILE_SETTINGS.FORM.PASSWORD_SECTION.TITLE')">
<change-password v-if="!globalConfig.disableUserProfileUpdate" />
</form-section>
<form-section
:title="$t('PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.TITLE')"
:description="
$t('PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.NOTE')
"
>
<div class="w-1/4 py-4 pr-6 ml-0">
<h4 class="text-lg text-black-900 dark:text-slate-200">
{{ $t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.TITLE') }}
</h4>
<p>
{{
useInstallationName(
$t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.NOTE'),
globalConfig.installationName
)
}}
</p>
</div>
<div class="p-4 w-[45%]">
<masked-text :value="currentUser.access_token" />
</div>
</div>
<audio-notifications />
</form-section>
<form-section :title="$t('PROFILE_SETTINGS.FORM.NOTIFICATIONS.TITLE')">
<notification-preferences />
</form-section>
<form-section
:title="$t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.TITLE')"
:description="
useInstallationName(
$t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.NOTE'),
globalConfig.installationName
)
"
>
<access-token :value="currentUser.access_token" @on-copy="onCopyToken" />
</form-section>
</div>
</template>
<script>
import { required, minLength, email } from 'vuelidate/lib/validators';
import { mapGetters } from 'vuex';
import { clearCookiesOnLogout } from '../../../../store/utils/api';
import { hasValidAvatarUrl } from 'dashboard/helper/URLHelper';
import NotificationSettings from './NotificationSettings.vue';
import alertMixin from 'shared/mixins/alertMixin';
import ChangePassword from './ChangePassword.vue';
import MessageSignature from './MessageSignature.vue';
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
import uiSettingsMixin, {
isEditorHotKeyEnabled,
} from 'dashboard/mixins/uiSettings';
import MaskedText from 'dashboard/components/MaskedText.vue';
import PreviewCard from 'dashboard/components/ui/PreviewCard.vue';
import alertMixin from 'shared/mixins/alertMixin';
import { mapGetters } from 'vuex';
import { clearCookiesOnLogout } from 'dashboard/store/utils/api.js';
import { copyTextToClipboard } from 'shared/helpers/clipboard';
import UserProfilePicture from './UserProfilePicture.vue';
import UserBasicDetails from './UserBasicDetails.vue';
import MessageSignature from './MessageSignature.vue';
import HotKeyCard from './HotKeyCard.vue';
import ChangePassword from './ChangePassword.vue';
import NotificationPreferences from './NotificationPreferences.vue';
import AudioNotifications from './AudioNotifications.vue';
import FormSection from 'dashboard/components/FormSection.vue';
import AccessToken from './AccessToken.vue';
export default {
components: {
NotificationSettings,
ChangePassword,
MessageSignature,
PreviewCard,
MaskedText,
FormSection,
UserProfilePicture,
UserBasicDetails,
HotKeyCard,
ChangePassword,
NotificationPreferences,
AudioNotifications,
AccessToken,
},
mixins: [alertMixin, globalConfigMixin, uiSettingsMixin],
data() {
@@ -156,59 +120,40 @@ export default {
name: '',
displayName: '',
email: '',
isProfileUpdating: false,
errorMessage: '',
keyOptions: [
messageSignature: '',
hotKeys: [
{
key: 'enter',
src: '/assets/images/dashboard/editor/enter-editor.png',
heading: this.$t(
title: this.$t(
'PROFILE_SETTINGS.FORM.SEND_MESSAGE.CARD.ENTER_KEY.HEADING'
),
content: this.$t(
description: this.$t(
'PROFILE_SETTINGS.FORM.SEND_MESSAGE.CARD.ENTER_KEY.CONTENT'
),
lightImage: '/assets/images/dashboard/profile/hot-key-enter.svg',
darkImage: '/assets/images/dashboard/profile/hot-key-enter-dark.svg',
},
{
key: 'cmd_enter',
src: '/assets/images/dashboard/editor/cmd-editor.png',
heading: this.$t(
title: this.$t(
'PROFILE_SETTINGS.FORM.SEND_MESSAGE.CARD.CMD_ENTER_KEY.HEADING'
),
content: this.$t(
description: this.$t(
'PROFILE_SETTINGS.FORM.SEND_MESSAGE.CARD.CMD_ENTER_KEY.CONTENT'
),
lightImage: '/assets/images/dashboard/profile/hot-key-ctrl-enter.svg',
darkImage:
'/assets/images/dashboard/profile/hot-key-ctrl-enter-dark.svg',
},
],
};
},
validations: {
name: {
required,
minLength: minLength(1),
},
displayName: {},
email: {
required,
email,
},
},
computed: {
...mapGetters({
currentUser: 'getCurrentUser',
currentUserId: 'getCurrentUserID',
globalConfig: 'globalConfig/get',
}),
showDeleteButton() {
return hasValidAvatarUrl(this.avatarUrl);
},
},
watch: {
currentUserId(newCurrentUserId, prevCurrentUserId) {
if (prevCurrentUserId !== newCurrentUserId) {
this.initializeUser();
}
},
},
mounted() {
if (this.currentUserId) {
@@ -221,45 +166,66 @@ export default {
this.email = this.currentUser.email;
this.avatarUrl = this.currentUser.avatar_url;
this.displayName = this.currentUser.display_name;
this.messageSignature = this.currentUser.message_signature;
},
isEditorHotKeyEnabled,
async updateUser() {
this.$v.$touch();
if (this.$v.$invalid) {
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.ERROR'));
return;
}
this.isProfileUpdating = true;
const hasEmailChanged = this.currentUser.email !== this.email;
async dispatchUpdate(payload, successMessage, errorMessage) {
let alertMessage = '';
try {
await this.$store.dispatch('updateProfile', {
name: this.name,
email: this.email,
avatar: this.avatarFile,
displayName: this.displayName,
});
this.isProfileUpdating = false;
if (hasEmailChanged) {
clearCookiesOnLogout();
this.errorMessage = this.$t('PROFILE_SETTINGS.AFTER_EMAIL_CHANGED');
}
this.errorMessage = this.$t('PROFILE_SETTINGS.UPDATE_SUCCESS');
await this.$store.dispatch('updateProfile', payload);
alertMessage = successMessage;
return true; // return the value so that the status can be known
} catch (error) {
this.errorMessage = this.$t('RESET_PASSWORD.API.ERROR_MESSAGE');
if (error?.response?.data?.error) {
this.errorMessage = error.response.data.error;
}
alertMessage = error?.response?.data?.error
? error.response.data.error
: errorMessage;
return false; // return the value so that the status can be known
} finally {
this.isProfileUpdating = false;
this.showAlert(this.errorMessage);
this.showAlert(alertMessage);
}
},
handleImageUpload({ file, url }) {
async updateProfile(userAttributes) {
const { name, email, displayName } = userAttributes;
const hasEmailChanged = this.currentUser.email !== email;
this.name = name || this.name;
this.email = email || this.email;
this.displayName = displayName || this.displayName;
const updatePayload = {
name: this.name,
email: this.email,
displayName: this.displayName,
avatar: this.avatarFile,
};
const success = await this.dispatchUpdate(
updatePayload,
hasEmailChanged
? this.$t('PROFILE_SETTINGS.AFTER_EMAIL_CHANGED')
: this.$t('PROFILE_SETTINGS.UPDATE_SUCCESS'),
this.$t('RESET_PASSWORD.API.ERROR_MESSAGE')
);
if (hasEmailChanged && success) clearCookiesOnLogout();
},
async updateSignature(signature) {
const payload = { message_signature: signature };
let successMessage = this.$t(
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.API_SUCCESS'
);
let errorMessage = this.$t(
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.API_ERROR'
);
await this.dispatchUpdate(payload, successMessage, errorMessage);
},
updateProfilePicture({ file, url }) {
this.avatarFile = file;
this.avatarUrl = url;
},
async deleteAvatar() {
async deleteProfilePicture() {
try {
await this.$store.dispatch('deleteAvatar');
this.avatarUrl = '';
@@ -269,12 +235,19 @@ export default {
this.showAlert(this.$t('PROFILE_SETTINGS.AVATAR_DELETE_FAILED'));
}
},
toggleEditorMessageKey(key) {
toggleHotKey(key) {
this.hotKeys = this.hotKeys.map(hotKey =>
hotKey.key === key ? { ...hotKey, active: !hotKey.active } : hotKey
);
this.updateUISettings({ editor_message_key: key });
this.showAlert(
this.$t('PROFILE_SETTINGS.FORM.SEND_MESSAGE.UPDATE_SUCCESS')
);
},
async onCopyToken(value) {
await copyTextToClipboard(value);
this.showAlert(this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
},
},
};
</script>

View File

@@ -1,110 +1,50 @@
<template>
<div
class="profile--settings--row text-black-900 dark:text-slate-300 flex items-center"
>
<div class="w-1/4 py-4 pr-6 ml-0">
<h4 class="text-lg text-black-900 dark:text-slate-200">
{{ $t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.TITLE') }}
</h4>
<p>{{ $t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.NOTE') }}</p>
</div>
<div class="p-4 w-[45%]">
<div>
<label for="message-signature-input">{{
$t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE.LABEL')
}}</label>
<woot-message-editor
id="message-signature-input"
v-model="messageSignature"
class="message-editor h-[10rem]"
:is-format-mode="true"
:placeholder="
$t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE.PLACEHOLDER')
"
:enabled-menu-options="customEditorMenuList"
:enable-suggestions="false"
:show-image-resize-toolbar="true"
/>
</div>
<woot-button
:is-loading="isUpdating"
type="button"
@click.prevent="updateSignature"
>
{{ $t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.BTN_TEXT') }}
</woot-button>
</div>
</div>
<form class="flex flex-col gap-6" @submit.prevent="updateSignature()">
<woot-message-editor
id="message-signature-input"
v-model="signature"
class="message-editor h-[10rem] !px-3"
:is-format-mode="true"
:placeholder="$t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE.PLACEHOLDER')"
:enabled-menu-options="customEditorMenuList"
:enable-suggestions="false"
:show-image-resize-toolbar="true"
/>
<form-button
type="submit"
color-scheme="primary"
variant="solid"
size="large"
>
{{ $t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.BTN_TEXT') }}
</form-button>
</form>
</template>
<script>
import { mapGetters } from 'vuex';
<script setup>
import { ref, watch } from 'vue';
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
import alertMixin from 'shared/mixins/alertMixin';
import { MESSAGE_SIGNATURE_EDITOR_MENU_OPTIONS } from 'dashboard/constants/editor';
import FormButton from 'v3/components/Form/Button.vue';
export default {
components: {
WootMessageEditor,
},
mixins: [alertMixin],
data() {
return {
messageSignature: '',
enableMessageSignature: false,
isUpdating: false,
errorMessage: '',
customEditorMenuList: MESSAGE_SIGNATURE_EDITOR_MENU_OPTIONS,
};
},
computed: {
...mapGetters({
currentUser: 'getCurrentUser',
currentUserId: 'getCurrentUserID',
}),
},
mounted() {
this.initValues();
},
methods: {
initValues() {
const { message_signature: messageSignature } = this.currentUser;
this.messageSignature = messageSignature || '';
},
async updateSignature() {
try {
await this.$store.dispatch('updateProfile', {
message_signature: this.messageSignature || '',
});
this.errorMessage = this.$t(
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.API_SUCCESS'
);
} catch (error) {
this.errorMessage = this.$t(
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.API_ERROR'
);
if (error?.response?.data?.message) {
this.errorMessage = error.response.data.message;
}
} finally {
this.isUpdating = false;
this.initValues();
this.showAlert(this.errorMessage);
}
},
const props = defineProps({
messageSignature: {
type: String,
default: '',
},
});
const customEditorMenuList = MESSAGE_SIGNATURE_EDITOR_MENU_OPTIONS;
const signature = ref(props.messageSignature);
const emit = defineEmits(['update-signature']);
watch(
() => props.messageSignature,
newValue => {
signature.value = newValue;
}
);
const updateSignature = () => {
emit('update-signature', signature.value);
};
</script>
<style lang="scss" scoped>
.message-editor {
@apply px-3 mb-4;
::v-deep {
.ProseMirror-menubar {
@apply left-2;
}
}
}
</style>

View File

@@ -110,7 +110,7 @@
</div>
<div
class="flex items-center justify-between w-full gap-2 p-4 border border-solid border-ash-200 rounded-xl dark:bg-ash-25"
class="flex items-center justify-between w-full gap-2 p-4 border border-solid border-ash-200 rounded-xl"
>
<div class="flex flex-row items-center gap-2">
<fluent-icon

View File

@@ -1,649 +0,0 @@
<template>
<div id="profile-settings-notifications">
<div
class="profile--settings--row text-black-900 dark:text-slate-300 flex items-center"
>
<div class="w-1/4">
<h4 class="text-lg text-black-900 dark:text-slate-200">
{{ $t('PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.TITLE') }}
</h4>
<p>
{{ $t('PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.NOTE') }}
</p>
</div>
<div class="w-[45%] p-4">
<div class="mb-4">
<span class="text-sm notification-label">
{{
$t(
'PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.ALERT_TYPE.TITLE'
)
}}
</span>
<div class="flex items-center gap-2 mb-1">
<input
id="audio_enable_alert_none"
v-model="enableAudioAlerts"
class="notification--checkbox"
type="radio"
value="none"
@input="handleAudioInput"
/>
<label for="audio_enable_alert_none">
{{
$t(
'PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.ALERT_TYPE.NONE'
)
}}
</label>
</div>
<div class="flex items-center gap-2 mb-1">
<input
id="audio_enable_alert_mine"
v-model="enableAudioAlerts"
class="notification--checkbox"
type="radio"
value="mine"
@input="handleAudioInput"
/>
<label for="audio_enable_alert_mine">
{{
$t(
'PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.ALERT_TYPE.ASSIGNED'
)
}}
</label>
</div>
<div class="flex items-center gap-2 mb-1">
<input
id="audio_enable_alert_all"
v-model="enableAudioAlerts"
class="notification--checkbox"
type="radio"
value="all"
@input="handleAudioInput"
/>
<label for="audio_enable_alert_all">
{{
$t(
'PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.ALERT_TYPE.ALL_CONVERSATIONS'
)
}}
</label>
</div>
</div>
<div class="mb-4">
<span class="text-sm notification-label">
{{
$t(
'PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.DEFAULT_TONE.TITLE'
)
}}
</span>
<div>
<select
v-model="notificationTone"
class="tone-selector mb-0"
@change="handleAudioToneChange"
>
<option
v-for="tone in notificationAlertTones"
:key="tone.value"
:value="tone.value"
>
{{ tone.label }}
</option>
</select>
</div>
</div>
<div class="mb-1">
<span class="text-sm notification-label">
{{
$t(
'PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.CONDITIONS.TITLE'
)
}}
</span>
<div class="flex items-center gap-2 mb-1">
<input
id="audio_alert_when_tab_is_inactive"
v-model="playAudioWhenTabIsInactive"
class="notification--checkbox"
type="checkbox"
value="tab_is_inactive"
@input="handleAudioAlertConditions"
/>
<label for="audio_alert_when_tab_is_inactive">
{{
$t(
'PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.CONDITIONS.CONDITION_ONE'
)
}}
</label>
</div>
<div class="flex items-center gap-2 mb-1">
<input
id="audio_alert_until_all_conversations_are_read"
v-model="alertIfUnreadConversationExist"
class="notification--checkbox"
type="checkbox"
value="conversations_are_read"
@input="handleAudioAlertConditions"
/>
<label for="audio_alert_until_all_conversations_are_read">
{{
$t(
'PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.CONDITIONS.CONDITION_TWO'
)
}}
</label>
</div>
</div>
</div>
</div>
<div
class="profile--settings--row text-black-900 dark:text-slate-300 flex items-center"
>
<div class="w-1/4">
<h4 class="text-lg text-black-900 dark:text-slate-200">
{{ $t('PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.TITLE') }}
</h4>
<p>
{{ $t('PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.NOTE') }}
</p>
</div>
<div class="w-[45%] p-4">
<div class="flex items-center gap-2 mb-1">
<input
v-model="selectedEmailFlags"
class="notification--checkbox"
type="checkbox"
value="email_conversation_creation"
@input="handleEmailInput"
/>
<label for="conversation_creation">
{{
$t(
'PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.CONVERSATION_CREATION'
)
}}
</label>
</div>
<div class="flex items-center gap-2 mb-1">
<input
v-model="selectedEmailFlags"
class="notification--checkbox"
type="checkbox"
value="email_conversation_assignment"
@input="handleEmailInput"
/>
<label for="conversation_assignment">
{{
$t(
'PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.CONVERSATION_ASSIGNMENT'
)
}}
</label>
</div>
<div class="flex items-center gap-2 mb-1">
<input
v-model="selectedEmailFlags"
class="notification--checkbox"
type="checkbox"
value="email_conversation_mention"
@input="handleEmailInput"
/>
<label for="conversation_mention">
{{
$t(
'PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.CONVERSATION_MENTION'
)
}}
</label>
</div>
<div class="flex items-center gap-2 mb-1">
<input
v-model="selectedEmailFlags"
class="notification--checkbox"
type="checkbox"
value="email_assigned_conversation_new_message"
@input="handleEmailInput"
/>
<label for="assigned_conversation_new_message">
{{
$t(
'PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.ASSIGNED_CONVERSATION_NEW_MESSAGE'
)
}}
</label>
</div>
<div class="flex items-center gap-2 mb-1">
<input
v-model="selectedEmailFlags"
class="notification--checkbox"
type="checkbox"
value="email_participating_conversation_new_message"
@input="handleEmailInput"
/>
<label for="assigned_conversation_new_message">
{{
$t(
'PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.PARTICIPATING_CONVERSATION_NEW_MESSAGE'
)
}}
</label>
</div>
<div v-if="isSLAEnabled" class="flex items-center gap-2 mb-1">
<input
v-model="selectedEmailFlags"
class="notification--checkbox"
type="checkbox"
value="email_sla_missed_first_response"
@input="handleEmailInput"
/>
<label for="sla_missed_first_response">
{{
$t(
'PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.SLA_MISSED_FIRST_RESPONSE'
)
}}
</label>
</div>
<div v-if="isSLAEnabled" class="flex items-center gap-2 mb-1">
<input
v-model="selectedEmailFlags"
class="notification--checkbox"
type="checkbox"
value="email_sla_missed_next_response"
@input="handleEmailInput"
/>
<label for="sla_missed_next_response">
{{
$t(
'PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.SLA_MISSED_NEXT_RESPONSE'
)
}}
</label>
</div>
<div v-if="isSLAEnabled" class="flex items-center gap-2 mb-1">
<input
v-model="selectedEmailFlags"
class="notification--checkbox"
type="checkbox"
value="email_sla_missed_resolution"
@input="handleEmailInput"
/>
<label for="sla_missed_resolution">
{{
$t(
'PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.SLA_MISSED_RESOLUTION'
)
}}
</label>
</div>
</div>
</div>
<div
v-if="vapidPublicKey && hasPushAPISupport"
class="profile--settings--row text-black-900 dark:text-slate-300 flex items-center push-row"
>
<div class="w-1/4">
<h4 class="text-lg text-black-900 dark:text-slate-200">
{{ $t('PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.TITLE') }}
</h4>
<p>{{ $t('PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.NOTE') }}</p>
</div>
<div class="w-[45%] p-4">
<p v-if="hasEnabledPushPermissions">
{{
$t(
'PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.HAS_ENABLED_PUSH'
)
}}
</p>
<div v-else class="push-notification--button">
<woot-submit-button
:button-text="
$t(
'PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.REQUEST_PUSH'
)
"
class="button nice small"
type="button"
@click="onRequestPermissions"
/>
</div>
<div class="flex items-center gap-2 mb-1">
<input
v-model="selectedPushFlags"
class="notification--checkbox"
type="checkbox"
value="push_conversation_creation"
@input="handlePushInput"
/>
<label for="conversation_creation">
{{
$t(
'PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.CONVERSATION_CREATION'
)
}}
</label>
</div>
<div class="flex items-center gap-2 mb-1">
<input
v-model="selectedPushFlags"
class="notification--checkbox"
type="checkbox"
value="push_conversation_assignment"
@input="handlePushInput"
/>
<label for="conversation_assignment">
{{
$t(
'PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.CONVERSATION_ASSIGNMENT'
)
}}
</label>
</div>
<div class="flex items-center gap-2 mb-1">
<input
v-model="selectedPushFlags"
class="notification--checkbox"
type="checkbox"
value="push_conversation_mention"
@input="handlePushInput"
/>
<label for="conversation_mention">
{{
$t(
'PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.CONVERSATION_MENTION'
)
}}
</label>
</div>
<div class="flex items-center gap-2 mb-1">
<input
v-model="selectedPushFlags"
class="notification--checkbox"
type="checkbox"
value="push_assigned_conversation_new_message"
@input="handlePushInput"
/>
<label for="assigned_conversation_new_message">
{{
$t(
'PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.ASSIGNED_CONVERSATION_NEW_MESSAGE'
)
}}
</label>
</div>
<div class="flex items-center gap-2 mb-1">
<input
v-model="selectedPushFlags"
class="notification--checkbox"
type="checkbox"
value="push_participating_conversation_new_message"
@input="handlePushInput"
/>
<label for="assigned_conversation_new_message">
{{
$t(
'PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.PARTICIPATING_CONVERSATION_NEW_MESSAGE'
)
}}
</label>
</div>
<div v-if="isSLAEnabled" class="flex items-center gap-2 mb-1">
<input
v-model="selectedPushFlags"
class="notification--checkbox"
type="checkbox"
value="push_sla_missed_first_response"
@input="handlePushInput"
/>
<label for="sla_missed_first_response">
{{
$t(
'PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.SLA_MISSED_FIRST_RESPONSE'
)
}}
</label>
</div>
<div v-if="isSLAEnabled" class="flex items-center gap-2 mb-1">
<input
v-model="selectedPushFlags"
class="notification--checkbox"
type="checkbox"
value="push_sla_missed_next_response"
@input="handlePushInput"
/>
<label for="sla_missed_next_response">
{{
$t(
'PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.SLA_MISSED_NEXT_RESPONSE'
)
}}
</label>
</div>
<div v-if="isSLAEnabled" class="flex items-center gap-2 mb-1">
<input
v-model="selectedPushFlags"
class="notification--checkbox"
type="checkbox"
value="push_sla_missed_resolution"
@input="handlePushInput"
/>
<label for="sla_missed_resolution">
{{
$t(
'PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.SLA_MISSED_RESOLUTION'
)
}}
</label>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin';
import configMixin from 'shared/mixins/configMixin';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import {
hasPushPermissions,
requestPushPermissions,
verifyServiceWorkerExistence,
} from '../../../../helper/pushHelper';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
export default {
mixins: [alertMixin, configMixin, uiSettingsMixin],
data() {
return {
selectedEmailFlags: [],
selectedPushFlags: [],
enableAudioAlerts: false,
hasEnabledPushPermissions: false,
playAudioWhenTabIsInactive: false,
alertIfUnreadConversationExist: false,
notificationTone: 'ding',
notificationAlertTones: [
{
value: 'ding',
label: 'Ding',
},
{
value: 'bell',
label: 'Bell',
},
],
};
},
computed: {
...mapGetters({
accountId: 'getCurrentAccountId',
emailFlags: 'userNotificationSettings/getSelectedEmailFlags',
pushFlags: 'userNotificationSettings/getSelectedPushFlags',
uiSettings: 'getUISettings',
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
}),
hasPushAPISupport() {
return !!('Notification' in window);
},
isSLAEnabled() {
return this.isFeatureEnabledonAccount(this.accountId, FEATURE_FLAGS.SLA);
},
},
watch: {
emailFlags(value) {
this.selectedEmailFlags = value;
},
pushFlags(value) {
this.selectedPushFlags = value;
},
uiSettings(value) {
this.notificationUISettings(value);
},
},
mounted() {
if (hasPushPermissions()) {
this.getPushSubscription();
}
this.notificationUISettings(this.uiSettings);
this.$store.dispatch('userNotificationSettings/get');
},
methods: {
notificationUISettings(uiSettings) {
const {
enable_audio_alerts: enableAudio = false,
always_play_audio_alert: alwaysPlayAudioAlert,
alert_if_unread_assigned_conversation_exist:
alertIfUnreadConversationExist,
notification_tone: notificationTone,
} = uiSettings;
this.enableAudioAlerts = enableAudio;
this.playAudioWhenTabIsInactive = !alwaysPlayAudioAlert;
this.alertIfUnreadConversationExist = alertIfUnreadConversationExist;
this.notificationTone = notificationTone || 'ding';
},
onRegistrationSuccess() {
this.hasEnabledPushPermissions = true;
},
onRequestPermissions() {
requestPushPermissions({
onSuccess: this.onRegistrationSuccess,
});
},
getPushSubscription() {
verifyServiceWorkerExistence(registration =>
registration.pushManager
.getSubscription()
.then(subscription => {
if (!subscription) {
this.hasEnabledPushPermissions = false;
} else {
this.hasEnabledPushPermissions = true;
}
})
// eslint-disable-next-line no-console
.catch(error => console.log(error))
);
},
async updateNotificationSettings() {
try {
this.$store.dispatch('userNotificationSettings/update', {
selectedEmailFlags: this.selectedEmailFlags,
selectedPushFlags: this.selectedPushFlags,
});
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_SUCCESS'));
} catch (error) {
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_ERROR'));
}
},
handleEmailInput(e) {
this.selectedEmailFlags = this.toggleInput(
this.selectedEmailFlags,
e.target.value
);
this.updateNotificationSettings();
},
handlePushInput(e) {
this.selectedPushFlags = this.toggleInput(
this.selectedPushFlags,
e.target.value
);
this.updateNotificationSettings();
},
handleAudioInput(e) {
this.enableAudioAlerts = e.target.value;
this.updateUISettings({
enable_audio_alerts: this.enableAudioAlerts,
});
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_SUCCESS'));
},
handleAudioAlertConditions(e) {
let condition = e.target.value;
if (condition === 'tab_is_inactive') {
this.updateUISettings({
always_play_audio_alert: !e.target.checked,
});
} else if (condition === 'conversations_are_read') {
this.updateUISettings({
alert_if_unread_assigned_conversation_exist: e.target.checked,
});
}
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_SUCCESS'));
},
handleAudioToneChange(e) {
this.updateUISettings({ notification_tone: e.target.value });
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_SUCCESS'));
},
toggleInput(selected, current) {
if (selected.includes(current)) {
const newSelectedFlags = selected.filter(flag => flag !== current);
return newSelectedFlags;
}
return [...selected, current];
},
},
};
</script>
<style lang="scss" scoped>
@import '~dashboard/assets/scss/variables.scss';
.notification--checkbox {
font-size: $font-size-large;
}
.push-notification--button {
margin-bottom: var(--space-one);
}
.notification-label {
display: flex;
font-weight: var(--font-weight-bold);
margin-bottom: var(--space-small);
}
.tone-selector {
height: var(--space-large);
padding-bottom: var(--space-micro);
padding-top: var(--space-micro);
width: var(--space-mega);
}
</style>

View File

@@ -0,0 +1,19 @@
<template>
<div
class="flex flex-col justify-between flex-1 h-full m-0 overflow-auto bg-white dark:bg-slate-900"
>
<keep-alive v-if="keepAlive">
<router-view />
</keep-alive>
<router-view v-else />
</div>
</template>
<script setup>
defineProps({
keepAlive: {
type: Boolean,
default: true,
},
});
</script>

View File

@@ -1,6 +1,6 @@
import { frontendURL } from '../../../../helper/URLHelper';
const SettingsContent = () => import('../Wrapper.vue');
const SettingsContent = () => import('./Wrapper.vue');
const Index = () => import('./Index.vue');
export default {
@@ -10,12 +10,6 @@ export default {
name: 'profile_settings',
roles: ['administrator', 'agent'],
component: SettingsContent,
props: {
headerTitle: 'PROFILE_SETTINGS.TITLE',
icon: 'edit',
showNewButton: false,
showSidemenuIcon: false,
},
children: [
{
path: 'settings',

View File

@@ -13,12 +13,11 @@ import integrationapps from './integrationapps/integrations.routes';
import integrations from './integrations/integrations.routes';
import labels from './labels/labels.routes';
import macros from './macros/macros.routes';
import profile from './profile/profile.routes';
import reports from './reports/reports.routes';
import store from '../../../store';
import sla from './sla/sla.routes';
import teams from './teams/teams.routes';
import personal from './personal/personal.routes';
import profile from './profile/profile.routes';
export default {
routes: [
@@ -47,10 +46,9 @@ export default {
...integrations.routes,
...labels.routes,
...macros.routes,
...profile.routes,
...reports.routes,
...sla.routes,
...teams.routes,
...personal.routes,
...profile.routes,
],
};

View File

@@ -4,7 +4,7 @@
v-model="checked"
type="checkbox"
:value="value"
class="flex-shrink-0 mt-0.5 border-ash-200 border checked:border-none checked:bg-primary-600 dark:checked:bg-primary-600 shadow appearance-none rounded-[4px] w-4 h-4 focus:ring-1 after:content-[''] after:text-white checked:after:content-['✓'] after:flex after:items-center after:justify-center after:text-center after:text-xs after:font-bold after:relative"
class="flex-shrink-0 mt-0.5 border-ash-200 border bg-ash-50 checked:border-none checked:bg-primary-600 dark:checked:bg-primary-600 shadow-sm appearance-none rounded-[4px] w-4 h-4 focus:ring-1 after:content-[''] after:text-white checked:after:content-['✓'] after:flex after:items-center after:justify-center after:text-center after:text-xs after:font-bold after:relative"
/>
</template>