Merge pull request #1227 from frappe/mergify/bp/main-hotfix/pr-1226

fix: if contact email is updated it is updating previously opened contact (backport #1226)
This commit is contained in:
Shariq Ansari 2025-09-05 16:15:01 +05:30 committed by GitHub
commit 4bf8c8d0b8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 164 additions and 160 deletions

View File

@ -85,7 +85,6 @@ declare module 'vue' {
DragIcon: typeof import('./src/components/Icons/DragIcon.vue')['default'] DragIcon: typeof import('./src/components/Icons/DragIcon.vue')['default']
DragVerticalIcon: typeof import('./src/components/Icons/DragVerticalIcon.vue')['default'] DragVerticalIcon: typeof import('./src/components/Icons/DragVerticalIcon.vue')['default']
Dropdown: typeof import('./src/components/frappe-ui/Dropdown.vue')['default'] Dropdown: typeof import('./src/components/frappe-ui/Dropdown.vue')['default']
DropdownItem: typeof import('./src/components/DropdownItem.vue')['default']
DuplicateIcon: typeof import('./src/components/Icons/DuplicateIcon.vue')['default'] DuplicateIcon: typeof import('./src/components/Icons/DuplicateIcon.vue')['default']
DurationIcon: typeof import('./src/components/Icons/DurationIcon.vue')['default'] DurationIcon: typeof import('./src/components/Icons/DurationIcon.vue')['default']
EditEmailTemplate: typeof import('./src/components/Settings/EmailTemplate/EditEmailTemplate.vue')['default'] EditEmailTemplate: typeof import('./src/components/Settings/EmailTemplate/EditEmailTemplate.vue')['default']
@ -202,6 +201,8 @@ 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']
PrimaryDropdown: typeof import('./src/components/PrimaryDropdown.vue')['default']
PrimaryDropdownItem: typeof import('./src/components/PrimaryDropdownItem.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']

View File

@ -0,0 +1,69 @@
<template>
<Popover>
<template #target="{ isOpen, togglePopover }">
<Button
:label="value"
class="dropdown-button flex items-center justify-between bg-surface-white !px-2.5 py-1.5 text-base text-ink-gray-8 placeholder-ink-gray-4 transition-colors hover:bg-surface-white focus:bg-surface-white focus:shadow-sm focus:outline-none focus:ring-0"
@click="togglePopover"
>
<div v-if="value" class="truncate">{{ value }}</div>
<div v-else class="text-base leading-5 text-ink-gray-4 truncate">
{{ placeholder }}
</div>
<template #suffix>
<FeatherIcon
:name="isOpen ? 'chevron-up' : 'chevron-down'"
class="h-4 text-ink-gray-5"
/>
</template>
</Button>
</template>
<template #body>
<div
class="my-2 p-1.5 min-w-40 space-y-1.5 divide-y divide-outline-gray-1 rounded-lg bg-surface-modal shadow-2xl ring-1 ring-black ring-opacity-5 focus:outline-none"
>
<div>
<PrimaryDropdownItem
v-for="option in options"
:key="option.name || option.value"
:option="option"
/>
<div v-if="!options?.length">
<div class="p-1.5 pl-3 pr-4 text-base text-ink-gray-4">
{{ __('No {0} Available', [label]) }}
</div>
</div>
</div>
<div class="pt-1.5">
<Button
variant="ghost"
class="w-full !justify-start"
:label="__('Create New')"
iconLeft="plus"
@click="create && create()"
/>
</div>
</div>
</template>
</Popover>
</template>
<script setup>
import PrimaryDropdownItem from '@/components/PrimaryDropdownItem.vue'
import { Popover } from 'frappe-ui'
const props = defineProps({
value: { type: [String, Number], default: '' },
placeholder: { type: String, default: '' },
options: { type: Array, default: [] },
create: { type: Function },
label: { type: String, default: '' },
})
</script>
<style scoped>
.dropdown-button {
border-color: transparent;
background: transparent;
}
</style>

View File

@ -56,7 +56,7 @@
<script setup> <script setup>
import SuccessIcon from '@/components/Icons/SuccessIcon.vue' import SuccessIcon from '@/components/Icons/SuccessIcon.vue'
import EditIcon from '@/components/Icons/EditIcon.vue' import EditIcon from '@/components/Icons/EditIcon.vue'
import { TextInput, Tooltip } from 'frappe-ui' import { TextInput } from 'frappe-ui'
import { nextTick, ref, onMounted } from 'vue' import { nextTick, ref, onMounted } from 'vue'
const props = defineProps({ const props = defineProps({

View File

@ -79,70 +79,14 @@
<div>{{ doc[field.fieldname] }}</div> <div>{{ doc[field.fieldname] }}</div>
</Tooltip> </Tooltip>
</div> </div>
<div v-else-if="field.fieldtype === 'Dropdown'"> <PrimaryDropdown
<Popover> v-else-if="field.fieldtype === 'Dropdown'"
<template #target="{ isOpen, togglePopover }"> :value="doc[field.fieldname]"
<Button :placeholder="field.placeholder"
:label="doc[field.fieldname]" :options="field.options"
class="dropdown-button flex items-center justify-between bg-surface-white !px-2.5 py-1.5 text-base text-ink-gray-8 placeholder-ink-gray-4 transition-colors hover:bg-surface-white focus:bg-surface-white focus:shadow-sm focus:outline-none focus:ring-0" :create="field.create"
@click="togglePopover" :label="field.label"
> />
<div
v-if="doc[field.fieldname]"
class="truncate"
>
{{ doc[field.fieldname] }}
</div>
<div
v-else
class="text-base leading-5 text-ink-gray-4 truncate"
>
{{ field.placeholder }}
</div>
<template #suffix>
<FeatherIcon
:name="
isOpen ? 'chevron-up' : 'chevron-down'
"
class="h-4 text-ink-gray-5"
/>
</template>
</Button>
</template>
<template #body>
<div
class="my-2 p-1.5 min-w-40 space-y-1.5 divide-y divide-outline-gray-1 rounded-lg bg-surface-modal shadow-2xl ring-1 ring-black ring-opacity-5 focus:outline-none"
>
<div>
<DropdownItem
v-if="field.options?.length"
v-for="option in field.options"
:key="option.name"
:option="option"
/>
<div v-else>
<div
class="p-1.5 pl-3 pr-4 text-base text-ink-gray-4"
>
{{
__('No {0} Available', [field.label])
}}
</div>
</div>
</div>
<div class="pt-1.5">
<Button
variant="ghost"
class="w-full !justify-start"
:label="__('Create New')"
iconLeft="plus"
@click="field.create()"
/>
</div>
</div>
</template>
</Popover>
</div>
<FormControl <FormControl
v-else-if="field.fieldtype == 'Check'" v-else-if="field.fieldtype == 'Check'"
class="form-control" class="form-control"
@ -366,7 +310,7 @@
import Password from '@/components/Controls/Password.vue' import Password from '@/components/Controls/Password.vue'
import FormattedInput from '@/components/Controls/FormattedInput.vue' import FormattedInput from '@/components/Controls/FormattedInput.vue'
import Section from '@/components/Section.vue' import Section from '@/components/Section.vue'
import DropdownItem from '@/components/DropdownItem.vue' import PrimaryDropdown from '@/components/PrimaryDropdown.vue'
import FadedScrollableDiv from '@/components/FadedScrollableDiv.vue' import FadedScrollableDiv from '@/components/FadedScrollableDiv.vue'
import ArrowUpRightIcon from '@/components/Icons/ArrowUpRightIcon.vue' import ArrowUpRightIcon from '@/components/Icons/ArrowUpRightIcon.vue'
import EditIcon from '@/components/Icons/EditIcon.vue' import EditIcon from '@/components/Icons/EditIcon.vue'
@ -378,7 +322,7 @@ import { usersStore } from '@/stores/users'
import { isMobileView } from '@/composables/settings' import { isMobileView } from '@/composables/settings'
import { getFormat, evaluateDependsOnValue } from '@/utils' import { getFormat, evaluateDependsOnValue } from '@/utils'
import { flt } from '@/utils/numberFormat.js' import { flt } from '@/utils/numberFormat.js'
import { Tooltip, DateTimePicker, DatePicker, Popover } from 'frappe-ui' import { Tooltip, DateTimePicker, DatePicker } from 'frappe-ui'
import { useDocument } from '@/data/document' import { useDocument } from '@/data/document'
import { ref, computed, getCurrentInstance } from 'vue' import { ref, computed, getCurrentInstance } from 'vue'

View File

@ -113,7 +113,7 @@
class="flex flex-1 flex-col justify-between overflow-hidden" class="flex flex-1 flex-col justify-between overflow-hidden"
> >
<SidePanelLayout <SidePanelLayout
:sections="sections.data" :sections="parsedSections"
doctype="Contact" doctype="Contact"
:docname="contact.doc.name" :docname="contact.doc.name"
@reload="sections.reload" @reload="sections.reload"
@ -293,9 +293,7 @@ const tabs = [
const deals = createResource({ const deals = createResource({
url: 'crm.api.contact.get_linked_deals', url: 'crm.api.contact.get_linked_deals',
cache: ['deals', props.contactId], cache: ['deals', props.contactId],
params: { params: { contact: props.contactId },
contact: props.contactId,
},
auto: true, auto: true,
}) })
@ -310,118 +308,110 @@ const sections = createResource({
cache: ['sidePanelSections', 'Contact'], cache: ['sidePanelSections', 'Contact'],
params: { doctype: 'Contact' }, params: { doctype: 'Contact' },
auto: true, auto: true,
transform: (data) => computed(() => getParsedSections(data)),
}) })
function getParsedSections(_sections) { const parsedSections = computed(() => {
return _sections.map((section) => { if (!sections.data) return []
section.columns = section.columns.map((column) => { return sections.data.map((section) => ({
column.fields = column.fields.map((field) => { ...section,
columns: section.columns.map((column) => ({
...column,
fields: column.fields.map((field) => {
if (field.fieldname === 'email_id') { if (field.fieldname === 'email_id') {
return { return {
...field, ...field,
read_only: false, read_only: false,
fieldtype: 'Dropdown', fieldtype: 'Dropdown',
options: options: (contact.doc?.email_ids || []).map((email) => ({
contact.doc?.email_ids?.map((email) => { name: email.name,
return { value: email.email_id,
name: email.name, selected: email.email_id === contact.doc.email_id,
value: email.email_id, placeholder: 'john@doe.com',
selected: email.email_id === contact.doc.email_id, onClick: () => setAsPrimary('email', email.email_id),
placeholder: 'john@doe.com', onSave: (option, isNew) =>
onClick: () => { isNew
setAsPrimary('email', email.email_id) ? createNew('email', option.value)
}, : editOption(
onSave: (option, isNew) => { 'Contact Email',
if (isNew) { option.name,
createNew('email', option.value) 'email_id',
} else { option.value,
editOption( ),
'Contact Email', onDelete: async (option, isNew) => {
option.name, contact.doc.email_ids = contact.doc.email_ids.filter(
'email_id', (e) => e.name !== option.name,
option.value )
) if (!isNew) await deleteOption('Contact Email', option.name)
} },
}, })),
onDelete: async (option, isNew) => {
contact.doc.email_ids = contact.doc.email_ids.filter(
(email) => email.name !== option.name,
)
!isNew && (await deleteOption('Contact Email', option.name))
},
}
}) || [],
create: () => { create: () => {
contact.doc?.email_ids?.push({ // Add a temporary new option locally (mirrors original behavior)
name: 'new-1', contact.doc.email_ids = [
value: '', ...(contact.doc.email_ids || []),
selected: false, {
isNew: true, name: 'new-1',
}) value: '',
selected: false,
isNew: true,
},
]
}, },
} }
} else if (field.fieldname === 'mobile_no') { }
if (field.fieldname === 'mobile_no') {
return { return {
...field, ...field,
read_only: false, read_only: false,
fieldtype: 'Dropdown', fieldtype: 'Dropdown',
options: options: (contact.doc?.phone_nos || []).map((phone) => ({
contact.doc?.phone_nos?.map((phone) => { name: phone.name,
return { value: phone.phone,
name: phone.name, selected: phone.phone === contact.doc.mobile_no,
value: phone.phone, onClick: () => setAsPrimary('mobile_no', phone.phone),
selected: phone.phone === contact.doc.mobile_no, onSave: (option, isNew) =>
onClick: () => { isNew
setAsPrimary('mobile_no', phone.phone) ? createNew('phone', option.value)
}, : editOption(
onSave: (option, isNew) => { 'Contact Phone',
if (isNew) { option.name,
createNew('phone', option.value) 'phone',
} else { option.value,
editOption( ),
'Contact Phone', onDelete: async (option, isNew) => {
option.name, contact.doc.phone_nos = contact.doc.phone_nos.filter(
'phone', (p) => p.name !== option.name,
option.value )
) if (!isNew) await deleteOption('Contact Phone', option.name)
} },
}, })),
onDelete: async (option, isNew) => {
contact.doc.phone_nos = contact.doc.phone_nos.filter(
(phone) => phone.name !== option.name,
)
!isNew && (await deleteOption('Contact Phone', option.name))
},
}
}) || [],
create: () => { create: () => {
contact.doc?.phone_nos?.push({ contact.doc.phone_nos = [
name: 'new-1', ...(contact.doc.phone_nos || []),
value: '', {
selected: false, name: 'new-1',
isNew: true, value: '',
}) selected: false,
isNew: true,
},
]
}, },
} }
} else if (field.fieldname === 'address') { }
if (field.fieldname === 'address') {
return { return {
...field, ...field,
create: (value, close) => { create: (_value, close) => {
openAddressModal() openAddressModal()
close() close && close()
}, },
edit: (address) => openAddressModal(address), edit: (address) => openAddressModal(address),
} }
} else {
return field
} }
}) return field
return column }),
}) })),
return section }))
}) })
}
async function setAsPrimary(field, value) { async function setAsPrimary(field, value) {
let d = await call('crm.api.contact.set_as_primary', { let d = await call('crm.api.contact.set_as_primary', {