mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 19:17:48 +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
	 Muhsin Keloth
					Muhsin Keloth