mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-01 19:48:08 +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",
|
||||
"AFTER_EMAIL_CHANGED": "Your profile has been updated successfully, please login again as your login credentials are changed",
|
||||
"FORM": {
|
||||
"PICTURE": "Profile Picture",
|
||||
"AVATAR": "Profile Image",
|
||||
"ERROR": "Please fix form errors",
|
||||
"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 sla from './sla/sla.routes';
|
||||
import teams from './teams/teams.routes';
|
||||
import personal from './personal/personal.routes';
|
||||
|
||||
export default {
|
||||
routes: [
|
||||
@@ -50,5 +51,6 @@ export default {
|
||||
...reports.routes,
|
||||
...sla.routes,
|
||||
...teams.routes,
|
||||
...personal.routes,
|
||||
],
|
||||
};
|
||||
|
||||
@@ -273,5 +273,6 @@
|
||||
"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",
|
||||
"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