feat: split basic profile component
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -6,66 +6,22 @@
|
||||
<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">
|
||||
Profile Settings
|
||||
{{ $t('PROFILE_SETTINGS.TITLE') }}
|
||||
</h2>
|
||||
<user-basic-profile
|
||||
<user-profile-picture
|
||||
:src="avatarUrl"
|
||||
:name="name"
|
||||
size="72px"
|
||||
@change="handleImageUpload"
|
||||
@update="updateUser"
|
||||
@delete="deleteAvatar"
|
||||
/>
|
||||
|
||||
<form
|
||||
class="flex flex-col gap-6"
|
||||
@submit.prevent="updateUser('profile')"
|
||||
>
|
||||
<form-input
|
||||
v-model="name"
|
||||
type="text"
|
||||
name="name"
|
||||
:class="{ error: $v.name.$error }"
|
||||
:label="$t('PROFILE_SETTINGS.FORM.NAME.LABEL')"
|
||||
:placeholder="$t('PROFILE_SETTINGS.FORM.NAME.PLACEHOLDER')"
|
||||
:has-error="$v.name.$error"
|
||||
:error-message="$t('PROFILE_SETTINGS.FORM.NAME.ERROR')"
|
||||
@blur="$v.name.$touch"
|
||||
/>
|
||||
<form-input
|
||||
v-model="displayName"
|
||||
type="text"
|
||||
name="displayName"
|
||||
:class="{ error: $v.displayName.$error }"
|
||||
:label="$t('PROFILE_SETTINGS.FORM.DISPLAY_NAME.LABEL')"
|
||||
:placeholder="
|
||||
$t('PROFILE_SETTINGS.FORM.DISPLAY_NAME.PLACEHOLDER')
|
||||
"
|
||||
:has-error="$v.displayName.$error"
|
||||
:error-message="$t('PROFILE_SETTINGS.FORM.DISPLAY_NAME.ERROR')"
|
||||
@blur="$v.displayName.$touch"
|
||||
/>
|
||||
<form-input
|
||||
v-if="!globalConfig.disableUserProfileUpdate"
|
||||
v-model="email"
|
||||
type="email"
|
||||
name="email"
|
||||
:class="{ error: $v.email.$error }"
|
||||
:label="$t('PROFILE_SETTINGS.FORM.EMAIL.LABEL')"
|
||||
:placeholder="$t('PROFILE_SETTINGS.FORM.EMAIL.PLACEHOLDER')"
|
||||
:has-error="$v.email.$error"
|
||||
:error-message="$t('PROFILE_SETTINGS.FORM.EMAIL.ERROR')"
|
||||
@blur="$v.email.$touch"
|
||||
/>
|
||||
<v3-button
|
||||
type="submit"
|
||||
color-scheme="primary"
|
||||
variant="solid"
|
||||
size="large"
|
||||
>
|
||||
{{ $t('PROFILE_SETTINGS.BTN_TEXT') }}
|
||||
</v3-button>
|
||||
</form>
|
||||
<user-basic-details
|
||||
:name="name"
|
||||
:display-name="displayName"
|
||||
:email="email"
|
||||
:email-enabled="!globalConfig.disableUserProfileUpdate"
|
||||
@update-user="updateUser"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<personal-wrapper
|
||||
@@ -142,7 +98,6 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import UserBasicProfile from './UserBasicProfile.vue';
|
||||
import MessageSignature from './MessageSignature.vue';
|
||||
import PersonalWrapper from './PersonalWrapper.vue';
|
||||
import HotKeyCard from './HotKeyCard.vue';
|
||||
@@ -153,27 +108,25 @@ import uiSettingsMixin, {
|
||||
isEditorHotKeyEnabled,
|
||||
} from 'dashboard/mixins/uiSettings';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
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 NotificationPreferences from './NotificationPreferences.vue';
|
||||
import FormInput from 'v3/components/Form/Input.vue';
|
||||
import V3Button from 'v3/components/Form/Button.vue';
|
||||
import UserProfilePicture from './UserProfilePicture.vue';
|
||||
import UserBasicDetails from './UserBasicDetails.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
UserBasicProfile,
|
||||
MessageSignature,
|
||||
PersonalWrapper,
|
||||
HotKeyCard,
|
||||
ChangePassword,
|
||||
NotificationSettings,
|
||||
AccessToken,
|
||||
FormInput,
|
||||
V3Button,
|
||||
NotificationPreferences,
|
||||
UserProfilePicture,
|
||||
UserBasicDetails,
|
||||
},
|
||||
mixins: [alertMixin, globalConfigMixin, uiSettingsMixin],
|
||||
data() {
|
||||
@@ -203,17 +156,6 @@ export default {
|
||||
],
|
||||
};
|
||||
},
|
||||
validations: {
|
||||
name: {
|
||||
required,
|
||||
minLength: minLength(1),
|
||||
},
|
||||
displayName: {},
|
||||
email: {
|
||||
required,
|
||||
email,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentUser: 'getCurrentUser',
|
||||
@@ -245,12 +187,6 @@ export default {
|
||||
},
|
||||
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;
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<form class="flex flex-col gap-6" @submit.prevent="updateUser('profile')">
|
||||
<form-input
|
||||
v-model="userName"
|
||||
type="text"
|
||||
name="name"
|
||||
:class="{ error: $v.userName.$error }"
|
||||
:label="$t('PROFILE_SETTINGS.FORM.NAME.LABEL')"
|
||||
:placeholder="$t('PROFILE_SETTINGS.FORM.NAME.PLACEHOLDER')"
|
||||
:has-error="$v.userName.$error"
|
||||
:error-message="$t('PROFILE_SETTINGS.FORM.NAME.ERROR')"
|
||||
@blur="$v.userName.$touch"
|
||||
/>
|
||||
<form-input
|
||||
v-model="userDisplayName"
|
||||
type="text"
|
||||
name="displayName"
|
||||
:class="{ error: $v.userDisplayName.$error }"
|
||||
:label="$t('PROFILE_SETTINGS.FORM.DISPLAY_NAME.LABEL')"
|
||||
:placeholder="$t('PROFILE_SETTINGS.FORM.DISPLAY_NAME.PLACEHOLDER')"
|
||||
:has-error="$v.userDisplayName.$error"
|
||||
:error-message="$t('PROFILE_SETTINGS.FORM.DISPLAY_NAME.ERROR')"
|
||||
@blur="$v.userDisplayName.$touch"
|
||||
/>
|
||||
<form-input
|
||||
v-if="emailEnabled"
|
||||
v-model="userEmail"
|
||||
type="email"
|
||||
name="email"
|
||||
:class="{ error: $v.userEmail.$error }"
|
||||
:label="$t('PROFILE_SETTINGS.FORM.EMAIL.LABEL')"
|
||||
:placeholder="$t('PROFILE_SETTINGS.FORM.EMAIL.PLACEHOLDER')"
|
||||
:has-error="$v.userEmail.$error"
|
||||
:error-message="$t('PROFILE_SETTINGS.FORM.EMAIL.ERROR')"
|
||||
@blur="$v.userEmail.$touch"
|
||||
/>
|
||||
<v3-button
|
||||
type="submit"
|
||||
color-scheme="primary"
|
||||
variant="solid"
|
||||
size="large"
|
||||
>
|
||||
{{ $t('PROFILE_SETTINGS.BTN_TEXT') }}
|
||||
</v3-button>
|
||||
</form>
|
||||
</template>
|
||||
<script>
|
||||
import V3Button from 'v3/components/Form/Button.vue';
|
||||
import FormInput from 'v3/components/Form/Input.vue';
|
||||
import { required, minLength, email } from 'vuelidate/lib/validators';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
export default {
|
||||
components: {
|
||||
V3Button,
|
||||
FormInput,
|
||||
},
|
||||
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,
|
||||
};
|
||||
},
|
||||
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>
|
||||
@@ -1,127 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-sm font-medium text-ash-900"> Profile picture </span>
|
||||
<div
|
||||
class="relative rounded-xl h-[72px] w-[72px] cursor-pointer"
|
||||
:title="name"
|
||||
@mouseover="showUpload"
|
||||
@mouseleave="hideUpload"
|
||||
>
|
||||
<img
|
||||
v-if="shouldShowImage"
|
||||
class="rounded-xl h-[72px] w-[72px]"
|
||||
:src="src"
|
||||
draggable="false"
|
||||
@load="onImgLoad"
|
||||
@error="onImgError"
|
||||
/>
|
||||
<Avatar
|
||||
v-show="!shouldShowImage"
|
||||
:username="userNameWithoutEmoji"
|
||||
:size="72"
|
||||
/>
|
||||
<input
|
||||
ref="file"
|
||||
type="file"
|
||||
accept="image/png, image/jpeg, image/jpg, image/gif, image/webp"
|
||||
style="display: none"
|
||||
@change="handleImageUpload"
|
||||
/>
|
||||
<button
|
||||
v-if="src && showUploadIcon"
|
||||
class="absolute z-10 flex items-center justify-center w-6 h-6 p-1 border border-white rounded-full select-none reset-base -top-2 -right-2 dark:border-white bg-slate-200 dark:bg-slate-700"
|
||||
@click="onAvatarDelete"
|
||||
>
|
||||
<fluent-icon
|
||||
icon="dismiss"
|
||||
size="16"
|
||||
class="text-slate-800 dark:text-slate-200"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
v-if="showUploadIcon"
|
||||
class="reset-base absolute h-[72px] w-[72px] top-0 left-0 rounded-xl select-none flex items-center justify-center bg-modal-backdrop-dark dark:bg-modal-backdrop-dark"
|
||||
@click="openFileInput"
|
||||
>
|
||||
<fluent-icon icon="avatar-upload" size="32" class="text-white" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Avatar from 'v3/components/Form/Avatar.vue';
|
||||
import { removeEmoji } from 'shared/helpers/emoji';
|
||||
export default {
|
||||
components: {
|
||||
Avatar,
|
||||
},
|
||||
props: {
|
||||
src: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hasImageLoaded: false,
|
||||
imgError: false,
|
||||
showUploadIcon: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
userNameWithoutEmoji() {
|
||||
return removeEmoji(this.name);
|
||||
},
|
||||
shouldShowImage() {
|
||||
if (!this.src) {
|
||||
return false;
|
||||
}
|
||||
if (this.hasImageLoaded) {
|
||||
return !this.imgError;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
src(value, oldValue) {
|
||||
if (value !== oldValue && this.imgError) {
|
||||
this.imgError = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onImgError() {
|
||||
this.imgError = true;
|
||||
},
|
||||
onImgLoad() {
|
||||
this.hasImageLoaded = true;
|
||||
},
|
||||
showUpload() {
|
||||
this.showUploadIcon = true;
|
||||
},
|
||||
hideUpload() {
|
||||
this.showUploadIcon = false;
|
||||
},
|
||||
openFileInput() {
|
||||
// Trigger click event on the hidden file input
|
||||
this.$refs.file.click();
|
||||
},
|
||||
handleImageUpload(event) {
|
||||
const [file] = event.target.files;
|
||||
this.$emit('change', {
|
||||
file,
|
||||
url: file ? URL.createObjectURL(file) : null,
|
||||
});
|
||||
},
|
||||
onAvatarDelete() {
|
||||
this.$emit('delete');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-sm font-medium text-ash-900">
|
||||
{{ $t('PROFILE_SETTINGS.FORM.PICTURE') }}
|
||||
</span>
|
||||
<div
|
||||
class="relative rounded-xl h-[72px] w-[72px] cursor-pointer"
|
||||
@mouseover="onImageOver"
|
||||
@mouseleave="onImageOut"
|
||||
>
|
||||
<img
|
||||
v-if="shouldShowImage"
|
||||
class="rounded-xl h-[72px] w-[72px]"
|
||||
:src="src"
|
||||
draggable="false"
|
||||
@load="onImageLoad"
|
||||
@error="onImageLoadError"
|
||||
/>
|
||||
<Avatar
|
||||
v-show="!shouldShowImage"
|
||||
:username="userNameWithoutEmoji"
|
||||
:size="72"
|
||||
/>
|
||||
<input
|
||||
ref="fileInputRef"
|
||||
type="file"
|
||||
accept="image/png, image/jpeg, image/jpg, image/gif, image/webp"
|
||||
style="display: none"
|
||||
@change="onImageUpload"
|
||||
/>
|
||||
<button
|
||||
v-if="src && shouldShowUploadIcon"
|
||||
class="absolute z-10 flex items-center justify-center w-6 h-6 p-1 border border-white rounded-full select-none reset-base -top-2 -right-2 dark:border-white bg-slate-200 dark:bg-slate-700"
|
||||
@click="onAvatarDelete"
|
||||
>
|
||||
<fluent-icon
|
||||
icon="dismiss"
|
||||
size="16"
|
||||
class="text-slate-800 dark:text-slate-200"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
v-if="shouldShowUploadIcon"
|
||||
class="reset-base absolute h-[72px] w-[72px] top-0 left-0 rounded-xl select-none flex items-center justify-center bg-modal-backdrop-dark dark:bg-modal-backdrop-dark"
|
||||
@click="openFileInput"
|
||||
>
|
||||
<fluent-icon icon="avatar-upload" size="32" class="text-white" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import Avatar from 'v3/components/Form/Avatar.vue';
|
||||
import { removeEmoji } from 'shared/helpers/emoji';
|
||||
const props = defineProps({
|
||||
src: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(['change', 'delete']);
|
||||
|
||||
const hasImageLoaded = ref(false);
|
||||
const imageLoadedError = ref(false);
|
||||
const shouldShowUploadIcon = ref(false);
|
||||
const fileInputRef = ref(null);
|
||||
const userNameWithoutEmoji = computed(() => removeEmoji(props.name));
|
||||
|
||||
const shouldShowImage = computed(() => {
|
||||
if (!props.src) {
|
||||
return false;
|
||||
}
|
||||
if (hasImageLoaded.value) {
|
||||
return !imageLoadedError.value;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const onImageLoadError = () => {
|
||||
imageLoadedError.value = true;
|
||||
};
|
||||
|
||||
const onImageLoad = () => {
|
||||
hasImageLoaded.value = true;
|
||||
};
|
||||
|
||||
const onImageOver = () => {
|
||||
shouldShowUploadIcon.value = true;
|
||||
};
|
||||
|
||||
const onImageOut = () => {
|
||||
shouldShowUploadIcon.value = false;
|
||||
};
|
||||
|
||||
const openFileInput = () => {
|
||||
fileInputRef.value.click();
|
||||
};
|
||||
|
||||
const onImageUpload = event => {
|
||||
const [file] = event.target.files;
|
||||
emits('change', {
|
||||
file,
|
||||
url: file ? URL.createObjectURL(file) : null,
|
||||
});
|
||||
};
|
||||
|
||||
const onAvatarDelete = () => {
|
||||
emits('delete');
|
||||
};
|
||||
</script>
|
||||
Reference in New Issue
Block a user