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:
commit
4bf8c8d0b8
3
frontend/components.d.ts
vendored
3
frontend/components.d.ts
vendored
@ -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']
|
||||||
|
|||||||
69
frontend/src/components/PrimaryDropdown.vue
Normal file
69
frontend/src/components/PrimaryDropdown.vue
Normal 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>
|
||||||
@ -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({
|
||||||
@ -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'
|
||||||
|
|
||||||
|
|||||||
@ -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', {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user