1
0
forked from test/crm

refactor: profile page

(cherry picked from commit fad7c5985c05a87386b9d1ea1286398a3ea642e5)

# Conflicts:
#	frontend/components.d.ts
#	frontend/src/components/Settings/ProfileImageEditor.vue
This commit is contained in:
Shariq Ansari 2025-06-17 23:13:04 +05:30 committed by Mergify
parent b047dab16d
commit bce5eef2c9
2 changed files with 117 additions and 82 deletions

View File

@ -15,7 +15,6 @@ declare module 'vue' {
AddExistingUserModal: typeof import('./src/components/Modals/AddExistingUserModal.vue')['default']
AddressIcon: typeof import('./src/components/Icons/AddressIcon.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']
AppHeader: typeof import('./src/components/Layouts/AppHeader.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']
InboxIcon: typeof import('./src/components/Icons/InboxIcon.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']
InviteUserPage: typeof import('./src/components/Settings/InviteUserPage.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']
ListRows: typeof import('./src/components/ListViews/ListRows.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']
MarkAsDoneIcon: typeof import('./src/components/Icons/MarkAsDoneIcon.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']
PlayIcon: typeof import('./src/components/Icons/PlayIcon.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']
QuickEntryModal: typeof import('./src/components/Modals/QuickEntryModal.vue')['default']
QuickFilterField: typeof import('./src/components/QuickFilterField.vue')['default']

View File

@ -2,19 +2,65 @@
<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 v-if="profile" class="flex w-full items-center justify-between">
<div class="flex items-center gap-4">
<Avatar
class="!size-16"
:image="profile.user_image"
:label="profile.full_name"
/>
<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>
</div>
</div>
<FileUploader
@success="(file) => updateImage(file.file_url)"
:validateFile="validateFile"
>
<template #default="{ openFileSelector, error: _error }">
<div class="flex items-center gap-4">
<div class="group relative !size-[66px]">
<Avatar
class="!size-16"
:image="profile.user_image"
:label="profile.full_name"
/>
<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
:label="__('Change Password')"
icon-left="lock"
@ -24,113 +70,101 @@
v-if="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 class="flex flex-col gap-4">
<div class="flex justify-between gap-4">
<FormControl
class="w-full"
label="First name"
:label="__('First name')"
v-model="profile.first_name"
/>
<FormControl
class="w-full"
label="Last name"
:label="__('Last name')"
v-model="profile.last_name"
/>
</div>
</div>
</div>
<div class="flex justify-between flex-row-reverse">
<div class="flex justify-between items-center">
<div>
<ErrorMessage :message="error" />
</div>
<Button
variant="solid"
:label="__('Update')"
:loading="loading"
@click="updateUser"
:disabled="!dirty"
:loading="setUser.loading"
@click="setUser.submit()"
/>
<ErrorMessage :message="error" />
</div>
</div>
</template>
<script setup>
import CameraIcon from '@/components/Icons/CameraIcon.vue'
import ChangePasswordModal from '@/components/Modals/ChangePasswordModal.vue'
import ProfileImageEditor from '@/components/Settings/ProfileImageEditor.vue'
import { usersStore } from '@/stores/users'
import { Dialog, Avatar, createResource, ErrorMessage, toast } from 'frappe-ui'
import { useOnboarding } from 'frappe-ui/frappe'
import {
Dropdown,
FileUploader,
Avatar,
createResource,
toast,
} from 'frappe-ui'
import { ref, computed, onMounted } from 'vue'
const { getUser, users } = usersStore()
const { updateOnboardingStep } = useOnboarding('frappecrm')
const user = computed(() => getUser() || {})
const showEditProfilePhotoModal = ref(false)
const showChangePasswordModal = ref(false)
const profile = ref({})
const loading = ref(false)
const error = ref('')
function updateUser() {
loading.value = true
const dirty = computed(() => {
return (
profile.value.first_name !== user.value.first_name ||
profile.value.last_name !== user.value.last_name
)
})
let passwordUpdated = false
if (profile.value.new_password) {
passwordUpdated = true
}
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: {
const setUser = createResource({
url: 'frappe.client.set_value',
makeParams() {
return {
doctype: 'User',
name: user.value.name,
fieldname,
},
auto: true,
onSuccess: () => {
if (passwordUpdated) {
updateOnboardingStep('setup_your_password')
}
loading.value = false
error.value = ''
profile.value.new_password = ''
showEditProfilePhotoModal.value = false
toast.success(__('Profile updated successfully'))
users.reload()
},
onError: (err) => {
loading.value = false
error.value = err.message
},
})
fieldname: {
first_name: profile.value.first_name,
last_name: profile.value.last_name,
user_image: profile.value.user_image,
},
}
},
onSuccess: () => {
error.value = ''
toast.success(__('Profile updated successfully'))
users.reload()
},
onError: (err) => {
error.value = err.messages[0] || __('Failed to update profile')
},
})
function updateImage(fileUrl = '') {
profile.value.user_image = fileUrl
setUser.submit()
}
onMounted(() => {
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>