mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-01 03:27:52 +00:00
feat: Revamp basic profile, avatar and message signature (#9310)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
This commit is contained in:
31
app/javascript/dashboard/components/FormSection.vue
Normal file
31
app/javascript/dashboard/components/FormSection.vue
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col items-start w-full gap-6">
|
||||||
|
<div class="flex flex-col w-full gap-4">
|
||||||
|
<h4 v-if="title" class="text-lg font-medium text-ash-900">
|
||||||
|
{{ title }}
|
||||||
|
</h4>
|
||||||
|
<div class="flex flex-row items-center justify-between">
|
||||||
|
<div class="flex-grow h-px bg-ash-200" />
|
||||||
|
</div>
|
||||||
|
<p v-if="description" class="mb-0 text-sm font-normal text-ash-900">
|
||||||
|
{{ description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col w-full gap-6">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
"PASSWORD_UPDATE_SUCCESS": "Your password has been changed successfully",
|
"PASSWORD_UPDATE_SUCCESS": "Your password has been changed successfully",
|
||||||
"AFTER_EMAIL_CHANGED": "Your profile has been updated successfully, please login again as your login credentials are changed",
|
"AFTER_EMAIL_CHANGED": "Your profile has been updated successfully, please login again as your login credentials are changed",
|
||||||
"FORM": {
|
"FORM": {
|
||||||
|
"PICTURE": "Profile Picture",
|
||||||
"AVATAR": "Profile Image",
|
"AVATAR": "Profile Image",
|
||||||
"ERROR": "Please fix form errors",
|
"ERROR": "Please fix form errors",
|
||||||
"REMOVE_IMAGE": "Remove",
|
"REMOVE_IMAGE": "Remove",
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
<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 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>
|
||||||
|
</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 UserProfilePicture from './UserProfilePicture.vue';
|
||||||
|
import UserBasicDetails from './UserBasicDetails.vue';
|
||||||
|
import MessageSignature from './MessageSignature.vue';
|
||||||
|
import FormSection from 'dashboard/components/FormSection.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
MessageSignature,
|
||||||
|
FormSection,
|
||||||
|
UserProfilePicture,
|
||||||
|
UserBasicDetails,
|
||||||
|
},
|
||||||
|
mixins: [alertMixin, globalConfigMixin, uiSettingsMixin],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
avatarFile: '',
|
||||||
|
avatarUrl: '',
|
||||||
|
name: '',
|
||||||
|
displayName: '',
|
||||||
|
email: '',
|
||||||
|
messageSignature: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
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'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
<template>
|
||||||
|
<form class="flex flex-col gap-4" @submit.prevent="updateUser('profile')">
|
||||||
|
<woot-input
|
||||||
|
v-model="userName"
|
||||||
|
:styles="inputStyles"
|
||||||
|
:class="{ error: $v.userName.$error }"
|
||||||
|
:label="$t('PROFILE_SETTINGS.FORM.NAME.LABEL')"
|
||||||
|
:placeholder="$t('PROFILE_SETTINGS.FORM.NAME.PLACEHOLDER')"
|
||||||
|
:error="`${
|
||||||
|
$v.userName.$error ? $t('PROFILE_SETTINGS.FORM.NAME.ERROR') : ''
|
||||||
|
}`"
|
||||||
|
@input="$v.userName.$touch"
|
||||||
|
/>
|
||||||
|
<woot-input
|
||||||
|
v-model="userDisplayName"
|
||||||
|
:styles="inputStyles"
|
||||||
|
:class="{ error: $v.userDisplayName.$error }"
|
||||||
|
:label="$t('PROFILE_SETTINGS.FORM.DISPLAY_NAME.LABEL')"
|
||||||
|
:placeholder="$t('PROFILE_SETTINGS.FORM.DISPLAY_NAME.PLACEHOLDER')"
|
||||||
|
:error="`${
|
||||||
|
$v.userDisplayName.$error
|
||||||
|
? $t('PROFILE_SETTINGS.FORM.DISPLAY_NAME.ERROR')
|
||||||
|
: ''
|
||||||
|
}`"
|
||||||
|
@input="$v.userDisplayName.$touch"
|
||||||
|
/>
|
||||||
|
<woot-input
|
||||||
|
v-if="emailEnabled"
|
||||||
|
v-model="userEmail"
|
||||||
|
:styles="inputStyles"
|
||||||
|
:class="{ error: $v.userEmail.$error }"
|
||||||
|
:label="$t('PROFILE_SETTINGS.FORM.EMAIL.LABEL')"
|
||||||
|
:placeholder="$t('PROFILE_SETTINGS.FORM.EMAIL.PLACEHOLDER')"
|
||||||
|
:error="`${
|
||||||
|
$v.userEmail.$error ? $t('PROFILE_SETTINGS.FORM.EMAIL.ERROR') : ''
|
||||||
|
}`"
|
||||||
|
@input="$v.userEmail.$touch"
|
||||||
|
/>
|
||||||
|
<form-button
|
||||||
|
type="submit"
|
||||||
|
color-scheme="primary"
|
||||||
|
variant="solid"
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
{{ $t('PROFILE_SETTINGS.BTN_TEXT') }}
|
||||||
|
</form-button>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import FormButton from 'v3/components/Form/Button.vue';
|
||||||
|
import { required, minLength, email } from 'vuelidate/lib/validators';
|
||||||
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
FormButton,
|
||||||
|
},
|
||||||
|
mixins: [alertMixin],
|
||||||
|
props: {
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
displayName: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
emailEnabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
userName: this.name,
|
||||||
|
userDisplayName: this.displayName,
|
||||||
|
userEmail: this.email,
|
||||||
|
inputStyles: {
|
||||||
|
borderRadius: '12px',
|
||||||
|
padding: '6px 12px',
|
||||||
|
fontSize: '14px',
|
||||||
|
marginBottom: '2px',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
validations: {
|
||||||
|
userName: {
|
||||||
|
required,
|
||||||
|
minLength: minLength(1),
|
||||||
|
},
|
||||||
|
userDisplayName: {},
|
||||||
|
userEmail: {
|
||||||
|
required,
|
||||||
|
email,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
name: {
|
||||||
|
handler(value) {
|
||||||
|
this.userName = value;
|
||||||
|
},
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
|
displayName: {
|
||||||
|
handler(value) {
|
||||||
|
this.userDisplayName = value;
|
||||||
|
},
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
handler(value) {
|
||||||
|
this.userEmail = value;
|
||||||
|
},
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async updateUser() {
|
||||||
|
this.$v.$touch();
|
||||||
|
if (this.$v.$invalid) {
|
||||||
|
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.ERROR'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$emit('update-user', {
|
||||||
|
name: this.userName,
|
||||||
|
displayName: this.userDisplayName,
|
||||||
|
email: this.userEmail,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<span class="text-sm font-medium text-ash-900">
|
||||||
|
{{ $t('PROFILE_SETTINGS.FORM.PICTURE') }}
|
||||||
|
</span>
|
||||||
|
<profile-avatar
|
||||||
|
:src="src"
|
||||||
|
:name="userNameWithoutEmoji"
|
||||||
|
@change="updateProfilePicture"
|
||||||
|
@delete="deleteProfilePicture"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import ProfileAvatar from 'v3/components/Form/ProfileAvatar.vue';
|
||||||
|
import { removeEmoji } from 'shared/helpers/emoji';
|
||||||
|
const props = defineProps({
|
||||||
|
src: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emits = defineEmits(['change', 'delete']);
|
||||||
|
|
||||||
|
const userNameWithoutEmoji = computed(() => removeEmoji(props.name));
|
||||||
|
|
||||||
|
const updateProfilePicture = e => {
|
||||||
|
emits('change', e);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteProfilePicture = () => {
|
||||||
|
emits('delete');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -18,6 +18,7 @@ import reports from './reports/reports.routes';
|
|||||||
import store from '../../../store';
|
import store from '../../../store';
|
||||||
import sla from './sla/sla.routes';
|
import sla from './sla/sla.routes';
|
||||||
import teams from './teams/teams.routes';
|
import teams from './teams/teams.routes';
|
||||||
|
import personal from './personal/personal.routes';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
routes: [
|
routes: [
|
||||||
@@ -50,5 +51,6 @@ export default {
|
|||||||
...reports.routes,
|
...reports.routes,
|
||||||
...sla.routes,
|
...sla.routes,
|
||||||
...teams.routes,
|
...teams.routes,
|
||||||
|
...personal.routes,
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -273,5 +273,6 @@
|
|||||||
"chevrons-left-outline": ["m11 17-5-5 5-5", "m18 17-5-5 5-5"],
|
"chevrons-left-outline": ["m11 17-5-5 5-5", "m18 17-5-5 5-5"],
|
||||||
"chevron-left-single-outline": "m15 18-6-6 6-6",
|
"chevron-left-single-outline": "m15 18-6-6 6-6",
|
||||||
"chevrons-right-outline": ["m6 17 5-5-5-5", "m13 17 5-5-5-5"],
|
"chevrons-right-outline": ["m6 17 5-5-5-5", "m13 17 5-5-5-5"],
|
||||||
"chevron-right-single-outline": "m9 18 6-6-6-6"
|
"chevron-right-single-outline": "m9 18 6-6-6-6",
|
||||||
|
"avatar-upload-outline": "M19.754 11a.75.75 0 0 1 .743.648l.007.102v7a3.25 3.25 0 0 1-3.065 3.246l-.185.005h-11a3.25 3.25 0 0 1-3.244-3.066l-.006-.184V11.75a.75.75 0 0 1 1.494-.102l.006.102v7a1.75 1.75 0 0 0 1.607 1.745l.143.006h11A1.75 1.75 0 0 0 19 18.894l.005-.143V11.75a.75.75 0 0 1 .75-.75ZM6.22 7.216l4.996-4.996a.75.75 0 0 1 .976-.073l.084.072l5.005 4.997a.75.75 0 0 1-.976 1.134l-.084-.073l-3.723-3.716l.001 11.694a.75.75 0 0 1-.648.743l-.102.007a.75.75 0 0 1-.743-.648L11 16.255V4.558L7.28 8.277a.75.75 0 0 1-.976.073l-.084-.073a.75.75 0 0 1-.073-.977l.073-.084l4.996-4.996L6.22 7.216Z"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user