refactor: profile page
(cherry picked from commit fad7c5985c05a87386b9d1ea1286398a3ea642e5) # Conflicts: # frontend/components.d.ts # frontend/src/components/Settings/ProfileImageEditor.vue
This commit is contained in:
parent
b047dab16d
commit
bce5eef2c9
7
frontend/components.d.ts
vendored
7
frontend/components.d.ts
vendored
@ -15,7 +15,6 @@ declare module 'vue' {
|
|||||||
AddExistingUserModal: typeof import('./src/components/Modals/AddExistingUserModal.vue')['default']
|
AddExistingUserModal: typeof import('./src/components/Modals/AddExistingUserModal.vue')['default']
|
||||||
AddressIcon: typeof import('./src/components/Icons/AddressIcon.vue')['default']
|
AddressIcon: typeof import('./src/components/Icons/AddressIcon.vue')['default']
|
||||||
AddressModal: typeof import('./src/components/Modals/AddressModal.vue')['default']
|
AddressModal: typeof import('./src/components/Modals/AddressModal.vue')['default']
|
||||||
Agents: typeof import('./src/components/Settings/Agents.vue')['default']
|
|
||||||
AllModals: typeof import('./src/components/Activities/AllModals.vue')['default']
|
AllModals: typeof import('./src/components/Activities/AllModals.vue')['default']
|
||||||
AppHeader: typeof import('./src/components/Layouts/AppHeader.vue')['default']
|
AppHeader: typeof import('./src/components/Layouts/AppHeader.vue')['default']
|
||||||
Apps: typeof import('./src/components/Apps.vue')['default']
|
Apps: typeof import('./src/components/Apps.vue')['default']
|
||||||
@ -138,7 +137,6 @@ declare module 'vue' {
|
|||||||
InboundCallIcon: typeof import('./src/components/Icons/InboundCallIcon.vue')['default']
|
InboundCallIcon: typeof import('./src/components/Icons/InboundCallIcon.vue')['default']
|
||||||
InboxIcon: typeof import('./src/components/Icons/InboxIcon.vue')['default']
|
InboxIcon: typeof import('./src/components/Icons/InboxIcon.vue')['default']
|
||||||
IndicatorIcon: typeof import('./src/components/Icons/IndicatorIcon.vue')['default']
|
IndicatorIcon: typeof import('./src/components/Icons/IndicatorIcon.vue')['default']
|
||||||
InviteAgentPage: typeof import('./src/components/Settings/InviteAgentPage.vue')['default']
|
|
||||||
InviteIcon: typeof import('./src/components/Icons/InviteIcon.vue')['default']
|
InviteIcon: typeof import('./src/components/Icons/InviteIcon.vue')['default']
|
||||||
InviteUserPage: typeof import('./src/components/Settings/InviteUserPage.vue')['default']
|
InviteUserPage: typeof import('./src/components/Settings/InviteUserPage.vue')['default']
|
||||||
KanbanIcon: typeof import('./src/components/Icons/KanbanIcon.vue')['default']
|
KanbanIcon: typeof import('./src/components/Icons/KanbanIcon.vue')['default']
|
||||||
@ -155,6 +153,10 @@ declare module 'vue' {
|
|||||||
ListIcon: typeof import('./src/components/Icons/ListIcon.vue')['default']
|
ListIcon: typeof import('./src/components/Icons/ListIcon.vue')['default']
|
||||||
ListRows: typeof import('./src/components/ListViews/ListRows.vue')['default']
|
ListRows: typeof import('./src/components/ListViews/ListRows.vue')['default']
|
||||||
LoadingIndicator: typeof import('./src/components/Icons/LoadingIndicator.vue')['default']
|
LoadingIndicator: typeof import('./src/components/Icons/LoadingIndicator.vue')['default']
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
LucideInfo: typeof import('~icons/lucide/info')['default']
|
||||||
|
>>>>>>> fad7c598 (refactor: profile page)
|
||||||
LucidePlus: typeof import('~icons/lucide/plus')['default']
|
LucidePlus: typeof import('~icons/lucide/plus')['default']
|
||||||
MarkAsDoneIcon: typeof import('./src/components/Icons/MarkAsDoneIcon.vue')['default']
|
MarkAsDoneIcon: typeof import('./src/components/Icons/MarkAsDoneIcon.vue')['default']
|
||||||
MaximizeIcon: typeof import('./src/components/Icons/MaximizeIcon.vue')['default']
|
MaximizeIcon: typeof import('./src/components/Icons/MaximizeIcon.vue')['default']
|
||||||
@ -187,7 +189,6 @@ declare module 'vue' {
|
|||||||
PlaybackSpeedOption: typeof import('./src/components/Activities/PlaybackSpeedOption.vue')['default']
|
PlaybackSpeedOption: typeof import('./src/components/Activities/PlaybackSpeedOption.vue')['default']
|
||||||
PlayIcon: typeof import('./src/components/Icons/PlayIcon.vue')['default']
|
PlayIcon: typeof import('./src/components/Icons/PlayIcon.vue')['default']
|
||||||
Popover: typeof import('./src/components/frappe-ui/Popover.vue')['default']
|
Popover: typeof import('./src/components/frappe-ui/Popover.vue')['default']
|
||||||
ProfileImageEditor: typeof import('./src/components/Settings/ProfileImageEditor.vue')['default']
|
|
||||||
ProfileSettings: typeof import('./src/components/Settings/ProfileSettings.vue')['default']
|
ProfileSettings: typeof import('./src/components/Settings/ProfileSettings.vue')['default']
|
||||||
QuickEntryModal: typeof import('./src/components/Modals/QuickEntryModal.vue')['default']
|
QuickEntryModal: typeof import('./src/components/Modals/QuickEntryModal.vue')['default']
|
||||||
QuickFilterField: typeof import('./src/components/QuickFilterField.vue')['default']
|
QuickFilterField: typeof import('./src/components/QuickFilterField.vue')['default']
|
||||||
|
|||||||
@ -2,19 +2,65 @@
|
|||||||
<div class="flex h-full flex-col gap-8 p-8 text-ink-gray-9">
|
<div class="flex h-full flex-col gap-8 p-8 text-ink-gray-9">
|
||||||
<div class="flex-1 flex flex-col gap-8 mt-2 overflow-y-auto">
|
<div class="flex-1 flex flex-col gap-8 mt-2 overflow-y-auto">
|
||||||
<div v-if="profile" class="flex w-full items-center justify-between">
|
<div v-if="profile" class="flex w-full items-center justify-between">
|
||||||
<div class="flex items-center gap-4">
|
<FileUploader
|
||||||
<Avatar
|
@success="(file) => updateImage(file.file_url)"
|
||||||
class="!size-16"
|
:validateFile="validateFile"
|
||||||
:image="profile.user_image"
|
>
|
||||||
:label="profile.full_name"
|
<template #default="{ openFileSelector, error: _error }">
|
||||||
/>
|
<div class="flex items-center gap-4">
|
||||||
<div class="flex flex-col gap-1">
|
<div class="group relative !size-[66px]">
|
||||||
<span class="text-2xl font-semibold text-ink-gray-9">{{
|
<Avatar
|
||||||
profile.full_name
|
class="!size-16"
|
||||||
}}</span>
|
:image="profile.user_image"
|
||||||
<span class="text-base text-ink-gray-7">{{ profile.email }}</span>
|
:label="profile.full_name"
|
||||||
</div>
|
/>
|
||||||
</div>
|
<component
|
||||||
|
:is="profile.user_image ? Dropdown : 'div'"
|
||||||
|
v-bind="
|
||||||
|
profile.user_image
|
||||||
|
? {
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
icon: 'upload',
|
||||||
|
label: profile.user_image
|
||||||
|
? __('Change image')
|
||||||
|
: __('Upload image'),
|
||||||
|
onClick: openFileSelector,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'trash-2',
|
||||||
|
label: __('Remove image'),
|
||||||
|
onClick: () => updateImage(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
: { onClick: openFileSelector }
|
||||||
|
"
|
||||||
|
class="!absolute bottom-0 left-0 right-0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="z-1 absolute bottom-0.5 left-0 right-0.5 flex h-9 cursor-pointer items-center justify-center rounded-b-full bg-black bg-opacity-40 pt-3 opacity-0 duration-300 ease-in-out group-hover:opacity-100"
|
||||||
|
style="
|
||||||
|
-webkit-clip-path: inset(12px 0 0 0);
|
||||||
|
clip-path: inset(12px 0 0 0);
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<CameraIcon class="size-4 cursor-pointer text-white" />
|
||||||
|
</div>
|
||||||
|
</component>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<span class="text-2xl font-semibold text-ink-gray-9">
|
||||||
|
{{ profile.full_name }}
|
||||||
|
</span>
|
||||||
|
<span class="text-base text-ink-gray-7">
|
||||||
|
{{ profile.email }}
|
||||||
|
</span>
|
||||||
|
<ErrorMessage :message="__(_error)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</FileUploader>
|
||||||
<Button
|
<Button
|
||||||
:label="__('Change Password')"
|
:label="__('Change Password')"
|
||||||
icon-left="lock"
|
icon-left="lock"
|
||||||
@ -24,113 +70,101 @@
|
|||||||
v-if="showChangePasswordModal"
|
v-if="showChangePasswordModal"
|
||||||
v-model="showChangePasswordModal"
|
v-model="showChangePasswordModal"
|
||||||
/>
|
/>
|
||||||
<Dialog
|
|
||||||
:options="{ title: __('Edit profile photo') }"
|
|
||||||
v-model="showEditProfilePhotoModal"
|
|
||||||
>
|
|
||||||
<template #body-content>
|
|
||||||
<ProfileImageEditor v-model="profile" />
|
|
||||||
</template>
|
|
||||||
<template #actions>
|
|
||||||
<Button
|
|
||||||
variant="solid"
|
|
||||||
class="w-full"
|
|
||||||
:loading="loading"
|
|
||||||
@click="updateUser"
|
|
||||||
:label="__('Save')"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<div class="flex justify-between gap-4">
|
<div class="flex justify-between gap-4">
|
||||||
<FormControl
|
<FormControl
|
||||||
class="w-full"
|
class="w-full"
|
||||||
label="First name"
|
:label="__('First name')"
|
||||||
v-model="profile.first_name"
|
v-model="profile.first_name"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
class="w-full"
|
class="w-full"
|
||||||
label="Last name"
|
:label="__('Last name')"
|
||||||
v-model="profile.last_name"
|
v-model="profile.last_name"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between flex-row-reverse">
|
<div class="flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<ErrorMessage :message="error" />
|
||||||
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="solid"
|
variant="solid"
|
||||||
:label="__('Update')"
|
:label="__('Update')"
|
||||||
:loading="loading"
|
:disabled="!dirty"
|
||||||
@click="updateUser"
|
:loading="setUser.loading"
|
||||||
|
@click="setUser.submit()"
|
||||||
/>
|
/>
|
||||||
<ErrorMessage :message="error" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import CameraIcon from '@/components/Icons/CameraIcon.vue'
|
||||||
import ChangePasswordModal from '@/components/Modals/ChangePasswordModal.vue'
|
import ChangePasswordModal from '@/components/Modals/ChangePasswordModal.vue'
|
||||||
import ProfileImageEditor from '@/components/Settings/ProfileImageEditor.vue'
|
|
||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
import { Dialog, Avatar, createResource, ErrorMessage, toast } from 'frappe-ui'
|
import {
|
||||||
import { useOnboarding } from 'frappe-ui/frappe'
|
Dropdown,
|
||||||
|
FileUploader,
|
||||||
|
Avatar,
|
||||||
|
createResource,
|
||||||
|
toast,
|
||||||
|
} from 'frappe-ui'
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
|
||||||
const { getUser, users } = usersStore()
|
const { getUser, users } = usersStore()
|
||||||
const { updateOnboardingStep } = useOnboarding('frappecrm')
|
|
||||||
|
|
||||||
const user = computed(() => getUser() || {})
|
const user = computed(() => getUser() || {})
|
||||||
|
|
||||||
const showEditProfilePhotoModal = ref(false)
|
|
||||||
const showChangePasswordModal = ref(false)
|
const showChangePasswordModal = ref(false)
|
||||||
|
|
||||||
const profile = ref({})
|
const profile = ref({})
|
||||||
const loading = ref(false)
|
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
|
|
||||||
function updateUser() {
|
const dirty = computed(() => {
|
||||||
loading.value = true
|
return (
|
||||||
|
profile.value.first_name !== user.value.first_name ||
|
||||||
|
profile.value.last_name !== user.value.last_name
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
let passwordUpdated = false
|
const setUser = createResource({
|
||||||
|
url: 'frappe.client.set_value',
|
||||||
if (profile.value.new_password) {
|
makeParams() {
|
||||||
passwordUpdated = true
|
return {
|
||||||
}
|
|
||||||
|
|
||||||
const fieldname = {
|
|
||||||
first_name: profile.value.first_name,
|
|
||||||
last_name: profile.value.last_name,
|
|
||||||
user_image: profile.value.user_image,
|
|
||||||
email: profile.value.email,
|
|
||||||
new_password: profile.value.new_password,
|
|
||||||
}
|
|
||||||
createResource({
|
|
||||||
url: 'frappe.client.set_value',
|
|
||||||
params: {
|
|
||||||
doctype: 'User',
|
doctype: 'User',
|
||||||
name: user.value.name,
|
name: user.value.name,
|
||||||
fieldname,
|
fieldname: {
|
||||||
},
|
first_name: profile.value.first_name,
|
||||||
auto: true,
|
last_name: profile.value.last_name,
|
||||||
onSuccess: () => {
|
user_image: profile.value.user_image,
|
||||||
if (passwordUpdated) {
|
},
|
||||||
updateOnboardingStep('setup_your_password')
|
}
|
||||||
}
|
},
|
||||||
loading.value = false
|
onSuccess: () => {
|
||||||
error.value = ''
|
error.value = ''
|
||||||
profile.value.new_password = ''
|
toast.success(__('Profile updated successfully'))
|
||||||
showEditProfilePhotoModal.value = false
|
users.reload()
|
||||||
toast.success(__('Profile updated successfully'))
|
},
|
||||||
users.reload()
|
onError: (err) => {
|
||||||
},
|
error.value = err.messages[0] || __('Failed to update profile')
|
||||||
onError: (err) => {
|
},
|
||||||
loading.value = false
|
})
|
||||||
error.value = err.message
|
|
||||||
},
|
function updateImage(fileUrl = '') {
|
||||||
})
|
profile.value.user_image = fileUrl
|
||||||
|
setUser.submit()
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
profile.value = { ...user.value }
|
profile.value = { ...user.value }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function validateFile(file) {
|
||||||
|
let extn = file.name.split('.').pop().toLowerCase()
|
||||||
|
if (!['png', 'jpg', 'jpeg'].includes(extn)) {
|
||||||
|
return __('Only PNG and JPG images are allowed')
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user