Merge branch 'main-hotfix' into mergify/bp/main-hotfix/pr-971
This commit is contained in:
commit
aff26a5ea2
@ -11,6 +11,7 @@ from pypika import Criterion
|
|||||||
|
|
||||||
from crm.api.views import get_views
|
from crm.api.views import get_views
|
||||||
from crm.fcrm.doctype.crm_form_script.crm_form_script import get_form_script
|
from crm.fcrm.doctype.crm_form_script.crm_form_script import get_form_script
|
||||||
|
from crm.utils import get_dynamic_linked_docs, get_linked_docs
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@ -676,6 +677,7 @@ def remove_assignments(doctype, name, assignees, ignore_permissions=False):
|
|||||||
ignore_permissions=ignore_permissions,
|
ignore_permissions=ignore_permissions,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_assigned_users(doctype, name, default_assigned_to=None):
|
def get_assigned_users(doctype, name, default_assigned_to=None):
|
||||||
assigned_users = frappe.get_all(
|
assigned_users = frappe.get_all(
|
||||||
@ -744,3 +746,98 @@ def getCounts(d, doctype):
|
|||||||
"FCRM Note", filters={"reference_doctype": doctype, "reference_docname": d.get("name")}
|
"FCRM Note", filters={"reference_doctype": doctype, "reference_docname": d.get("name")}
|
||||||
)
|
)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_linked_docs_of_document(doctype, docname):
|
||||||
|
doc = frappe.get_doc(doctype, docname)
|
||||||
|
linked_docs = get_linked_docs(doc)
|
||||||
|
dynamic_linked_docs = get_dynamic_linked_docs(doc)
|
||||||
|
|
||||||
|
linked_docs.extend(dynamic_linked_docs)
|
||||||
|
linked_docs = list({doc["reference_docname"]: doc for doc in linked_docs}.values())
|
||||||
|
|
||||||
|
docs_data = []
|
||||||
|
for doc in linked_docs:
|
||||||
|
data = frappe.get_doc(doc["reference_doctype"], doc["reference_docname"])
|
||||||
|
title = data.get("title")
|
||||||
|
if data.doctype == "CRM Call Log":
|
||||||
|
title = f"Call from {data.get('from')} to {data.get('to')}"
|
||||||
|
|
||||||
|
if data.doctype == "CRM Deal":
|
||||||
|
title = data.get("organization")
|
||||||
|
|
||||||
|
docs_data.append(
|
||||||
|
{
|
||||||
|
"doc": data.doctype,
|
||||||
|
"title": title or data.get("name"),
|
||||||
|
"reference_docname": doc["reference_docname"],
|
||||||
|
"reference_doctype": doc["reference_doctype"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return docs_data
|
||||||
|
|
||||||
|
|
||||||
|
def remove_doc_link(doctype, docname):
|
||||||
|
linked_doc_data = frappe.get_doc(doctype, docname)
|
||||||
|
linked_doc_data.update(
|
||||||
|
{
|
||||||
|
"reference_doctype": None,
|
||||||
|
"reference_docname": None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
linked_doc_data.save(ignore_permissions=True)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_contact_link(doctype, docname):
|
||||||
|
linked_doc_data = frappe.get_doc(doctype, docname)
|
||||||
|
linked_doc_data.update(
|
||||||
|
{
|
||||||
|
"contact": None,
|
||||||
|
"contacts": [],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
linked_doc_data.save(ignore_permissions=True)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def remove_linked_doc_reference(items, remove_contact=None, delete=False):
|
||||||
|
if isinstance(items, str):
|
||||||
|
items = frappe.parse_json(items)
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
if remove_contact:
|
||||||
|
remove_contact_link(item["doctype"], item["docname"])
|
||||||
|
else:
|
||||||
|
remove_doc_link(item["doctype"], item["docname"])
|
||||||
|
|
||||||
|
if delete:
|
||||||
|
frappe.delete_doc(item["doctype"], item["docname"])
|
||||||
|
|
||||||
|
return "success"
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def delete_bulk_docs(doctype, items, delete_linked=False):
|
||||||
|
from frappe.desk.reportview import delete_bulk
|
||||||
|
|
||||||
|
items = frappe.parse_json(items)
|
||||||
|
for doc in items:
|
||||||
|
linked_docs = get_linked_docs_of_document(doctype, doc)
|
||||||
|
for linked_doc in linked_docs:
|
||||||
|
remove_linked_doc_reference(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"doctype": linked_doc["reference_doctype"],
|
||||||
|
"docname": linked_doc["reference_docname"],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
remove_contact=doctype == "Contact",
|
||||||
|
delete=delete_linked,
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(items) > 10:
|
||||||
|
frappe.enqueue("frappe.desk.reportview.delete_bulk", doctype=doctype, items=items)
|
||||||
|
else:
|
||||||
|
delete_bulk(doctype, items)
|
||||||
|
return "success"
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
|
from frappe import frappe
|
||||||
import phonenumbers
|
import phonenumbers
|
||||||
from frappe.utils import floor
|
from frappe.utils import floor
|
||||||
from phonenumbers import NumberParseException
|
from phonenumbers import NumberParseException
|
||||||
from phonenumbers import PhoneNumberFormat as PNF
|
from phonenumbers import PhoneNumberFormat as PNF
|
||||||
|
from frappe.model.docstatus import DocStatus
|
||||||
|
from frappe.model.dynamic_links import get_dynamic_link_map
|
||||||
|
|
||||||
|
|
||||||
def parse_phone_number(phone_number, default_country="IN"):
|
def parse_phone_number(phone_number, default_country="IN"):
|
||||||
@ -93,3 +96,129 @@ def seconds_to_duration(seconds):
|
|||||||
return f"{seconds}s"
|
return f"{seconds}s"
|
||||||
else:
|
else:
|
||||||
return "0s"
|
return "0s"
|
||||||
|
|
||||||
|
# Extracted from frappe core frappe/model/delete_doc.py/check_if_doc_is_linked
|
||||||
|
def get_linked_docs(doc, method="Delete"):
|
||||||
|
from frappe.model.rename_doc import get_link_fields
|
||||||
|
|
||||||
|
link_fields = get_link_fields(doc.doctype)
|
||||||
|
ignored_doctypes = set()
|
||||||
|
|
||||||
|
if method == "Cancel" and (doc_ignore_flags := doc.get("ignore_linked_doctypes")):
|
||||||
|
ignored_doctypes.update(doc_ignore_flags)
|
||||||
|
if method == "Delete":
|
||||||
|
ignored_doctypes.update(frappe.get_hooks("ignore_links_on_delete"))
|
||||||
|
|
||||||
|
docs = []
|
||||||
|
|
||||||
|
for lf in link_fields:
|
||||||
|
link_dt, link_field, issingle = lf["parent"], lf["fieldname"], lf["issingle"]
|
||||||
|
if link_dt in ignored_doctypes or (link_field == "amended_from" and method == "Cancel"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
meta = frappe.get_meta(link_dt)
|
||||||
|
except frappe.DoesNotExistError:
|
||||||
|
frappe.clear_last_message()
|
||||||
|
# This mostly happens when app do not remove their customizations, we shouldn't
|
||||||
|
# prevent link checks from failing in those cases
|
||||||
|
continue
|
||||||
|
|
||||||
|
if issingle:
|
||||||
|
if frappe.db.get_single_value(link_dt, link_field) == doc.name:
|
||||||
|
docs.append({"doc": doc.name, "link_dt": link_dt, "link_field": link_field})
|
||||||
|
continue
|
||||||
|
|
||||||
|
fields = ["name", "docstatus"]
|
||||||
|
|
||||||
|
if meta.istable:
|
||||||
|
fields.extend(["parent", "parenttype"])
|
||||||
|
|
||||||
|
for item in frappe.db.get_values(link_dt, {link_field: doc.name}, fields, as_dict=True):
|
||||||
|
# available only in child table cases
|
||||||
|
item_parent = getattr(item, "parent", None)
|
||||||
|
linked_parent_doctype = item.parenttype if item_parent else link_dt
|
||||||
|
|
||||||
|
if linked_parent_doctype in ignored_doctypes:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if method != "Delete" and (method != "Cancel" or not DocStatus(item.docstatus).is_submitted()):
|
||||||
|
# don't raise exception if not
|
||||||
|
# linked to a non-cancelled doc when deleting or to a submitted doc when cancelling
|
||||||
|
continue
|
||||||
|
elif link_dt == doc.doctype and (item_parent or item.name) == doc.name:
|
||||||
|
# don't raise exception if not
|
||||||
|
# linked to same item or doc having same name as the item
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
reference_docname = item_parent or item.name
|
||||||
|
docs.append(
|
||||||
|
{
|
||||||
|
"doc": doc.name,
|
||||||
|
"reference_doctype": linked_parent_doctype,
|
||||||
|
"reference_docname": reference_docname,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return docs
|
||||||
|
|
||||||
|
# Extracted from frappe core frappe/model/delete_doc.py/check_if_doc_is_dynamically_linked
|
||||||
|
def get_dynamic_linked_docs(doc, method="Delete"):
|
||||||
|
docs = []
|
||||||
|
for df in get_dynamic_link_map().get(doc.doctype, []):
|
||||||
|
ignore_linked_doctypes = doc.get("ignore_linked_doctypes") or []
|
||||||
|
|
||||||
|
if df.parent in frappe.get_hooks("ignore_links_on_delete") or (
|
||||||
|
df.parent in ignore_linked_doctypes and method == "Cancel"
|
||||||
|
):
|
||||||
|
# don't check for communication and todo!
|
||||||
|
continue
|
||||||
|
|
||||||
|
meta = frappe.get_meta(df.parent)
|
||||||
|
if meta.issingle:
|
||||||
|
# dynamic link in single doc
|
||||||
|
refdoc = frappe.db.get_singles_dict(df.parent)
|
||||||
|
if (
|
||||||
|
refdoc.get(df.options) == doc.doctype
|
||||||
|
and refdoc.get(df.fieldname) == doc.name
|
||||||
|
and (
|
||||||
|
# linked to an non-cancelled doc when deleting
|
||||||
|
(method == "Delete" and not DocStatus(refdoc.docstatus).is_cancelled())
|
||||||
|
# linked to a submitted doc when cancelling
|
||||||
|
or (method == "Cancel" and DocStatus(refdoc.docstatus).is_submitted())
|
||||||
|
)
|
||||||
|
):
|
||||||
|
docs.append({"doc": doc.name, "reference_doctype": df.parent, "reference_docname": df.parent})
|
||||||
|
else:
|
||||||
|
# dynamic link in table
|
||||||
|
df["table"] = ", `parent`, `parenttype`, `idx`" if meta.istable else ""
|
||||||
|
for refdoc in frappe.db.sql(
|
||||||
|
"""select `name`, `docstatus` {table} from `tab{parent}` where
|
||||||
|
`{options}`=%s and `{fieldname}`=%s""".format(**df),
|
||||||
|
(doc.doctype, doc.name),
|
||||||
|
as_dict=True,
|
||||||
|
):
|
||||||
|
# linked to an non-cancelled doc when deleting
|
||||||
|
# or linked to a submitted doc when cancelling
|
||||||
|
if (method == "Delete" and not DocStatus(refdoc.docstatus).is_cancelled()) or (
|
||||||
|
method == "Cancel" and DocStatus(refdoc.docstatus).is_submitted()
|
||||||
|
):
|
||||||
|
reference_doctype = refdoc.parenttype if meta.istable else df.parent
|
||||||
|
reference_docname = refdoc.parent if meta.istable else refdoc.name
|
||||||
|
|
||||||
|
if reference_doctype in frappe.get_hooks("ignore_links_on_delete") or (
|
||||||
|
reference_doctype in ignore_linked_doctypes and method == "Cancel"
|
||||||
|
):
|
||||||
|
# don't check for communication and todo!
|
||||||
|
continue
|
||||||
|
|
||||||
|
at_position = f"at Row: {refdoc.idx}" if meta.istable else ""
|
||||||
|
|
||||||
|
docs.append(
|
||||||
|
{
|
||||||
|
"doc": doc.name,
|
||||||
|
"reference_doctype": reference_doctype,
|
||||||
|
"reference_docname": reference_docname,
|
||||||
|
"at_position": at_position,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return docs
|
||||||
|
|||||||
4
frontend/components.d.ts
vendored
4
frontend/components.d.ts
vendored
@ -31,6 +31,7 @@ declare module 'vue' {
|
|||||||
Autocomplete: typeof import('./src/components/frappe-ui/Autocomplete.vue')['default']
|
Autocomplete: typeof import('./src/components/frappe-ui/Autocomplete.vue')['default']
|
||||||
AvatarIcon: typeof import('./src/components/Icons/AvatarIcon.vue')['default']
|
AvatarIcon: typeof import('./src/components/Icons/AvatarIcon.vue')['default']
|
||||||
BrandLogo: typeof import('./src/components/BrandLogo.vue')['default']
|
BrandLogo: typeof import('./src/components/BrandLogo.vue')['default']
|
||||||
|
BulkDeleteLinkedDocModal: typeof import('./src/components/BulkDeleteLinkedDocModal.vue')['default']
|
||||||
CalendarIcon: typeof import('./src/components/Icons/CalendarIcon.vue')['default']
|
CalendarIcon: typeof import('./src/components/Icons/CalendarIcon.vue')['default']
|
||||||
CallArea: typeof import('./src/components/Activities/CallArea.vue')['default']
|
CallArea: typeof import('./src/components/Activities/CallArea.vue')['default']
|
||||||
CallLogDetailModal: typeof import('./src/components/Modals/CallLogDetailModal.vue')['default']
|
CallLogDetailModal: typeof import('./src/components/Modals/CallLogDetailModal.vue')['default']
|
||||||
@ -66,6 +67,7 @@ declare module 'vue' {
|
|||||||
DealsIcon: typeof import('./src/components/Icons/DealsIcon.vue')['default']
|
DealsIcon: typeof import('./src/components/Icons/DealsIcon.vue')['default']
|
||||||
DealsListView: typeof import('./src/components/ListViews/DealsListView.vue')['default']
|
DealsListView: typeof import('./src/components/ListViews/DealsListView.vue')['default']
|
||||||
DeclinedCallIcon: typeof import('./src/components/Icons/DeclinedCallIcon.vue')['default']
|
DeclinedCallIcon: typeof import('./src/components/Icons/DeclinedCallIcon.vue')['default']
|
||||||
|
DeleteLinkedDocModal: typeof import('./src/components/DeleteLinkedDocModal.vue')['default']
|
||||||
DesendingIcon: typeof import('./src/components/Icons/DesendingIcon.vue')['default']
|
DesendingIcon: typeof import('./src/components/Icons/DesendingIcon.vue')['default']
|
||||||
DesktopLayout: typeof import('./src/components/Layouts/DesktopLayout.vue')['default']
|
DesktopLayout: typeof import('./src/components/Layouts/DesktopLayout.vue')['default']
|
||||||
DetailsIcon: typeof import('./src/components/Icons/DetailsIcon.vue')['default']
|
DetailsIcon: typeof import('./src/components/Icons/DetailsIcon.vue')['default']
|
||||||
@ -153,11 +155,13 @@ declare module 'vue' {
|
|||||||
LeadsListView: typeof import('./src/components/ListViews/LeadsListView.vue')['default']
|
LeadsListView: typeof import('./src/components/ListViews/LeadsListView.vue')['default']
|
||||||
LightningIcon: typeof import('./src/components/Icons/LightningIcon.vue')['default']
|
LightningIcon: typeof import('./src/components/Icons/LightningIcon.vue')['default']
|
||||||
Link: typeof import('./src/components/Controls/Link.vue')['default']
|
Link: typeof import('./src/components/Controls/Link.vue')['default']
|
||||||
|
LinkedDocsListView: typeof import('./src/components/ListViews/LinkedDocsListView.vue')['default']
|
||||||
LinkIcon: typeof import('./src/components/Icons/LinkIcon.vue')['default']
|
LinkIcon: typeof import('./src/components/Icons/LinkIcon.vue')['default']
|
||||||
ListBulkActions: typeof import('./src/components/ListBulkActions.vue')['default']
|
ListBulkActions: typeof import('./src/components/ListBulkActions.vue')['default']
|
||||||
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']
|
||||||
|
LucideCalendar: typeof import('~icons/lucide/calendar')['default']
|
||||||
LucideInfo: typeof import('~icons/lucide/info')['default']
|
LucideInfo: typeof import('~icons/lucide/info')['default']
|
||||||
LucideMoreHorizontal: typeof import('~icons/lucide/more-horizontal')['default']
|
LucideMoreHorizontal: typeof import('~icons/lucide/more-horizontal')['default']
|
||||||
LucidePlus: typeof import('~icons/lucide/plus')['default']
|
LucidePlus: typeof import('~icons/lucide/plus')['default']
|
||||||
|
|||||||
@ -14,6 +14,20 @@
|
|||||||
:doctype="doctype"
|
:doctype="doctype"
|
||||||
@reload="reload"
|
@reload="reload"
|
||||||
/>
|
/>
|
||||||
|
<DeleteLinkedDocModal
|
||||||
|
v-if="showDeleteDocModal.showLinkedDocsModal"
|
||||||
|
v-model="showDeleteDocModal.showLinkedDocsModal"
|
||||||
|
:doctype="props.doctype"
|
||||||
|
:docname="showDeleteDocModal.docname"
|
||||||
|
:reload="reload"
|
||||||
|
/>
|
||||||
|
<BulkDeleteLinkedDocModal
|
||||||
|
v-if="showDeleteDocModal.showDeleteModal"
|
||||||
|
v-model="showDeleteDocModal.showDeleteModal"
|
||||||
|
:doctype="props.doctype"
|
||||||
|
:items="showDeleteDocModal.items"
|
||||||
|
:reload="reload"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@ -50,7 +64,11 @@ const { $dialog, $socket } = globalStore()
|
|||||||
const showEditModal = ref(false)
|
const showEditModal = ref(false)
|
||||||
const selectedValues = ref([])
|
const selectedValues = ref([])
|
||||||
const unselectAllAction = ref(() => {})
|
const unselectAllAction = ref(() => {})
|
||||||
|
const showDeleteDocModal = ref({
|
||||||
|
showLinkedDocsModal: false,
|
||||||
|
showDeleteModal: false,
|
||||||
|
docname: null,
|
||||||
|
})
|
||||||
function editValues(selections, unselectAll) {
|
function editValues(selections, unselectAll) {
|
||||||
selectedValues.value = selections
|
selectedValues.value = selections
|
||||||
showEditModal.value = true
|
showEditModal.value = true
|
||||||
@ -88,33 +106,18 @@ function convertToDeal(selections, unselectAll) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function deleteValues(selections, unselectAll) {
|
function deleteValues(selections, unselectAll) {
|
||||||
$dialog({
|
const selectedDocs = Array.from(selections)
|
||||||
title: __('Delete'),
|
if (selectedDocs.length == 1) {
|
||||||
message: __('Are you sure you want to delete {0} item(s)?', [
|
showDeleteDocModal.value = {
|
||||||
selections.size,
|
showLinkedDocsModal: true,
|
||||||
]),
|
docname: selectedDocs[0],
|
||||||
variant: 'solid',
|
}
|
||||||
theme: 'red',
|
} else {
|
||||||
actions: [
|
showDeleteDocModal.value = {
|
||||||
{
|
showDeleteModal: true,
|
||||||
label: __('Delete'),
|
items: selectedDocs,
|
||||||
variant: 'solid',
|
}
|
||||||
theme: 'red',
|
}
|
||||||
onClick: (close) => {
|
|
||||||
capture('bulk_delete')
|
|
||||||
call('frappe.desk.reportview.delete_items', {
|
|
||||||
items: JSON.stringify(Array.from(selections)),
|
|
||||||
doctype: props.doctype,
|
|
||||||
}).then(() => {
|
|
||||||
toast.success(__('Deleted successfully'))
|
|
||||||
unselectAll()
|
|
||||||
list.value.reload()
|
|
||||||
close()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const showAssignmentModal = ref(false)
|
const showAssignmentModal = ref(false)
|
||||||
|
|||||||
@ -80,7 +80,7 @@ import CallLogDetailModal from '@/components/Modals/CallLogDetailModal.vue'
|
|||||||
import CallLogModal from '@/components/Modals/CallLogModal.vue'
|
import CallLogModal from '@/components/Modals/CallLogModal.vue'
|
||||||
import { getCallLogDetail } from '@/utils/callLog'
|
import { getCallLogDetail } from '@/utils/callLog'
|
||||||
import { createResource } from 'frappe-ui'
|
import { createResource } from 'frappe-ui'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref, onMounted } from 'vue'
|
||||||
|
|
||||||
const callLogsListView = ref(null)
|
const callLogsListView = ref(null)
|
||||||
const showCallLogModal = ref(false)
|
const showCallLogModal = ref(false)
|
||||||
@ -124,4 +124,19 @@ function createCallLog() {
|
|||||||
callLog.value = {}
|
callLog.value = {}
|
||||||
showCallLogModal.value = true
|
showCallLogModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openCallLogFromURL = () => {
|
||||||
|
const searchParams = new URLSearchParams(window.location.search)
|
||||||
|
const callLogName = searchParams.get('open')
|
||||||
|
|
||||||
|
if (callLogName) {
|
||||||
|
showCallLog(callLogName)
|
||||||
|
searchParams.delete('open')
|
||||||
|
window.history.replaceState(null, '', window.location.pathname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
openCallLogFromURL()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -105,7 +105,7 @@
|
|||||||
:label="__('Delete')"
|
:label="__('Delete')"
|
||||||
theme="red"
|
theme="red"
|
||||||
size="sm"
|
size="sm"
|
||||||
@click="deleteContact"
|
@click="deleteContact()"
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<FeatherIcon name="trash-2" class="h-4 w-4" />
|
<FeatherIcon name="trash-2" class="h-4 w-4" />
|
||||||
@ -172,6 +172,13 @@
|
|||||||
:errorTitle="errorTitle"
|
:errorTitle="errorTitle"
|
||||||
:errorMessage="errorMessage"
|
:errorMessage="errorMessage"
|
||||||
/>
|
/>
|
||||||
|
<DeleteLinkedDocModal
|
||||||
|
v-if="showDeleteLinkedDocModal"
|
||||||
|
v-model="showDeleteLinkedDocModal"
|
||||||
|
:doctype="'Contact'"
|
||||||
|
:docname="contact.data.name"
|
||||||
|
name="Contacts"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@ -293,6 +300,11 @@ usePageMeta(() => {
|
|||||||
icon: brand.favicon,
|
icon: brand.favicon,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
const showDeleteLinkedDocModal = ref(false)
|
||||||
|
|
||||||
|
async function deleteContact() {
|
||||||
|
showDeleteLinkedDocModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
async function changeContactImage(file) {
|
async function changeContactImage(file) {
|
||||||
await call('frappe.client.set_value', {
|
await call('frappe.client.set_value', {
|
||||||
@ -304,28 +316,6 @@ async function changeContactImage(file) {
|
|||||||
contact.reload()
|
contact.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteContact() {
|
|
||||||
$dialog({
|
|
||||||
title: __('Delete contact'),
|
|
||||||
message: __('Are you sure you want to delete this contact?'),
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
label: __('Delete'),
|
|
||||||
theme: 'red',
|
|
||||||
variant: 'solid',
|
|
||||||
async onClick(close) {
|
|
||||||
await call('frappe.client.delete', {
|
|
||||||
doctype: 'Contact',
|
|
||||||
name: props.contactId,
|
|
||||||
})
|
|
||||||
close()
|
|
||||||
router.push({ name: 'Contacts' })
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const tabIndex = ref(0)
|
const tabIndex = ref(0)
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
@ -389,13 +379,13 @@ function getParsedSections(_sections) {
|
|||||||
'Contact Email',
|
'Contact Email',
|
||||||
option.name,
|
option.name,
|
||||||
'email_id',
|
'email_id',
|
||||||
option.value,
|
option.value
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDelete: async (option, isNew) => {
|
onDelete: async (option, isNew) => {
|
||||||
contact.data.email_ids = contact.data.email_ids.filter(
|
contact.data.email_ids = contact.data.email_ids.filter(
|
||||||
(email) => email.name !== option.name,
|
(email) => email.name !== option.name
|
||||||
)
|
)
|
||||||
!isNew && (await deleteOption('Contact Email', option.name))
|
!isNew && (await deleteOption('Contact Email', option.name))
|
||||||
if (_contact.value.email_id === option.value) {
|
if (_contact.value.email_id === option.value) {
|
||||||
@ -403,7 +393,7 @@ function getParsedSections(_sections) {
|
|||||||
_contact.value.email_id = ''
|
_contact.value.email_id = ''
|
||||||
} else {
|
} else {
|
||||||
_contact.value.email_id = contact.data.email_ids.find(
|
_contact.value.email_id = contact.data.email_ids.find(
|
||||||
(email) => email.is_primary,
|
(email) => email.is_primary
|
||||||
)?.email_id
|
)?.email_id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -446,13 +436,13 @@ function getParsedSections(_sections) {
|
|||||||
'Contact Phone',
|
'Contact Phone',
|
||||||
option.name,
|
option.name,
|
||||||
'phone',
|
'phone',
|
||||||
option.value,
|
option.value
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDelete: async (option, isNew) => {
|
onDelete: async (option, isNew) => {
|
||||||
contact.data.phone_nos = contact.data.phone_nos.filter(
|
contact.data.phone_nos = contact.data.phone_nos.filter(
|
||||||
(phone) => phone.name !== option.name,
|
(phone) => phone.name !== option.name
|
||||||
)
|
)
|
||||||
!isNew && (await deleteOption('Contact Phone', option.name))
|
!isNew && (await deleteOption('Contact Phone', option.name))
|
||||||
if (_contact.value.actual_mobile_no === option.value) {
|
if (_contact.value.actual_mobile_no === option.value) {
|
||||||
@ -461,7 +451,7 @@ function getParsedSections(_sections) {
|
|||||||
} else {
|
} else {
|
||||||
_contact.value.actual_mobile_no =
|
_contact.value.actual_mobile_no =
|
||||||
contact.data.phone_nos.find(
|
contact.data.phone_nos.find(
|
||||||
(phone) => phone.is_primary_mobile_no,
|
(phone) => phone.is_primary_mobile_no
|
||||||
)?.phone
|
)?.phone
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -129,19 +129,6 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<<<<<<< HEAD
|
|
||||||
=======
|
|
||||||
<Tooltip :text="__('Delete')">
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
@click="deleteDealWithModal(deal.data.name)"
|
|
||||||
variant="subtle"
|
|
||||||
icon="trash-2"
|
|
||||||
theme="red"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
>>>>>>> 65435cf2 (fix: delete icon issue & more cleanup)
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -332,6 +319,13 @@
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
|
<DeleteLinkedDocModal
|
||||||
|
v-if="showDeleteLinkedDocModal"
|
||||||
|
v-model="showDeleteLinkedDocModal"
|
||||||
|
:doctype="'CRM Deal'"
|
||||||
|
:docname="props.dealId"
|
||||||
|
name="Deals"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import ErrorPage from '@/components/ErrorPage.vue'
|
import ErrorPage from '@/components/ErrorPage.vue'
|
||||||
@ -475,7 +469,11 @@ const reload = ref(false)
|
|||||||
const showOrganizationModal = ref(false)
|
const showOrganizationModal = ref(false)
|
||||||
const showFilesUploader = ref(false)
|
const showFilesUploader = ref(false)
|
||||||
const _organization = ref({})
|
const _organization = ref({})
|
||||||
|
const showDeleteLinkedDocModal = ref(false)
|
||||||
|
|
||||||
|
async function deleteDealWithModal() {
|
||||||
|
showDeleteLinkedDocModal.value = true
|
||||||
|
}
|
||||||
function updateDeal(fieldname, value, callback) {
|
function updateDeal(fieldname, value, callback) {
|
||||||
value = Array.isArray(fieldname) ? '' : value
|
value = Array.isArray(fieldname) ? '' : value
|
||||||
|
|
||||||
|
|||||||
@ -186,19 +186,6 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<<<<<<< HEAD
|
|
||||||
=======
|
|
||||||
<Tooltip :text="__('Delete')">
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
@click="deleteLeadWithModal(lead.data.name)"
|
|
||||||
variant="subtle"
|
|
||||||
theme="red"
|
|
||||||
icon="trash-2"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
>>>>>>> 65435cf2 (fix: delete icon issue & more cleanup)
|
|
||||||
</div>
|
</div>
|
||||||
<ErrorMessage :message="__(error)" />
|
<ErrorMessage :message="__(error)" />
|
||||||
</div>
|
</div>
|
||||||
@ -343,6 +330,13 @@
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
|
<DeleteLinkedDocModal
|
||||||
|
v-if="showDeleteLinkedDocModal"
|
||||||
|
v-model="showDeleteLinkedDocModal"
|
||||||
|
:doctype="'CRM Lead'"
|
||||||
|
:docname="props.leadId"
|
||||||
|
name="Leads"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import ErrorPage from '@/components/ErrorPage.vue'
|
import ErrorPage from '@/components/ErrorPage.vue'
|
||||||
@ -433,6 +427,7 @@ const props = defineProps({
|
|||||||
|
|
||||||
const errorTitle = ref('')
|
const errorTitle = ref('')
|
||||||
const errorMessage = ref('')
|
const errorMessage = ref('')
|
||||||
|
const showDeleteLinkedDocModal = ref(false)
|
||||||
|
|
||||||
const lead = createResource({
|
const lead = createResource({
|
||||||
url: 'crm.fcrm.doctype.crm_lead.api.get_lead',
|
url: 'crm.fcrm.doctype.crm_lead.api.get_lead',
|
||||||
@ -631,6 +626,10 @@ async function deleteLead(name) {
|
|||||||
router.push({ name: 'Leads' })
|
router.push({ name: 'Leads' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteLeadWithModal(name) {
|
||||||
|
showDeleteLinkedDocModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
// Convert to Deal
|
// Convert to Deal
|
||||||
const showConvertToDealModal = ref(false)
|
const showConvertToDealModal = ref(false)
|
||||||
const existingContactChecked = ref(false)
|
const existingContactChecked = ref(false)
|
||||||
|
|||||||
@ -127,6 +127,7 @@ const viewControls = ref(null)
|
|||||||
watch(
|
watch(
|
||||||
() => notes.value?.data?.page_length_count,
|
() => notes.value?.data?.page_length_count,
|
||||||
(val, old_value) => {
|
(val, old_value) => {
|
||||||
|
openNoteFromURL()
|
||||||
if (!val || val === old_value) return
|
if (!val || val === old_value) return
|
||||||
updatedPageCount.value = val
|
updatedPageCount.value = val
|
||||||
},
|
},
|
||||||
@ -152,4 +153,20 @@ async function deleteNote(name) {
|
|||||||
})
|
})
|
||||||
notes.value.reload()
|
notes.value.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openNoteFromURL = () => {
|
||||||
|
const searchParams = new URLSearchParams(window.location.search)
|
||||||
|
const noteName = searchParams.get('open')
|
||||||
|
|
||||||
|
if (noteName && notes.value?.data?.data) {
|
||||||
|
const foundNote = notes.value.data.data.find(
|
||||||
|
(note) => note.name === noteName,
|
||||||
|
)
|
||||||
|
if (foundNote) {
|
||||||
|
editNote(foundNote)
|
||||||
|
}
|
||||||
|
searchParams.delete('open')
|
||||||
|
window.history.replaceState(null, '', window.location.pathname)
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -83,7 +83,7 @@
|
|||||||
:label="__('Delete')"
|
:label="__('Delete')"
|
||||||
theme="red"
|
theme="red"
|
||||||
size="sm"
|
size="sm"
|
||||||
@click="deleteOrganization"
|
@click="deleteOrganization()"
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<FeatherIcon name="trash-2" class="h-4 w-4" />
|
<FeatherIcon name="trash-2" class="h-4 w-4" />
|
||||||
@ -164,6 +164,13 @@
|
|||||||
:errorTitle="errorTitle"
|
:errorTitle="errorTitle"
|
||||||
:errorMessage="errorMessage"
|
:errorMessage="errorMessage"
|
||||||
/>
|
/>
|
||||||
|
<DeleteLinkedDocModal
|
||||||
|
v-if="showDeleteLinkedDocModal"
|
||||||
|
v-model="showDeleteLinkedDocModal"
|
||||||
|
:doctype="'CRM Organization'"
|
||||||
|
:docname="props.organizationId"
|
||||||
|
name="Organizations"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@ -202,6 +209,7 @@ import {
|
|||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { h, computed, ref } from 'vue'
|
import { h, computed, ref } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import DeleteLinkedDocModal from '@/components/DeleteLinkedDocModal.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
organizationId: {
|
organizationId: {
|
||||||
@ -222,6 +230,8 @@ const router = useRouter()
|
|||||||
const errorTitle = ref('')
|
const errorTitle = ref('')
|
||||||
const errorMessage = ref('')
|
const errorMessage = ref('')
|
||||||
|
|
||||||
|
const showDeleteLinkedDocModal = ref(false)
|
||||||
|
|
||||||
const organization = createDocumentResource({
|
const organization = createDocumentResource({
|
||||||
doctype: 'CRM Organization',
|
doctype: 'CRM Organization',
|
||||||
name: props.organizationId,
|
name: props.organizationId,
|
||||||
@ -249,7 +259,7 @@ const breadcrumbs = computed(() => {
|
|||||||
let view = getView(
|
let view = getView(
|
||||||
route.query.view,
|
route.query.view,
|
||||||
route.query.viewType,
|
route.query.viewType,
|
||||||
'CRM Organization',
|
'CRM Organization'
|
||||||
)
|
)
|
||||||
if (view) {
|
if (view) {
|
||||||
items.push({
|
items.push({
|
||||||
@ -286,6 +296,10 @@ usePageMeta(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
async function deleteOrganization() {
|
||||||
|
showDeleteLinkedDocModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
async function changeOrganizationImage(file) {
|
async function changeOrganizationImage(file) {
|
||||||
await call('frappe.client.set_value', {
|
await call('frappe.client.set_value', {
|
||||||
doctype: 'CRM Organization',
|
doctype: 'CRM Organization',
|
||||||
@ -296,28 +310,6 @@ async function changeOrganizationImage(file) {
|
|||||||
organization.reload()
|
organization.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteOrganization() {
|
|
||||||
$dialog({
|
|
||||||
title: __('Delete organization'),
|
|
||||||
message: __('Are you sure you want to delete this organization?'),
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
label: __('Delete'),
|
|
||||||
theme: 'red',
|
|
||||||
variant: 'solid',
|
|
||||||
async onClick(close) {
|
|
||||||
await call('frappe.client.delete', {
|
|
||||||
doctype: 'CRM Organization',
|
|
||||||
name: props.organizationId,
|
|
||||||
})
|
|
||||||
close()
|
|
||||||
router.push({ name: 'Organizations' })
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function website(url) {
|
function website(url) {
|
||||||
return url && url.replace(/^(?:https?:\/\/)?(?:www\.)?/i, '')
|
return url && url.replace(/^(?:https?:\/\/)?(?:www\.)?/i, '')
|
||||||
}
|
}
|
||||||
|
|||||||
@ -211,7 +211,7 @@ import { getMeta } from '@/stores/meta'
|
|||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
import { formatDate, timeAgo } from '@/utils'
|
import { formatDate, timeAgo } from '@/utils'
|
||||||
import { Tooltip, Avatar, TextEditor, Dropdown, call } from 'frappe-ui'
|
import { Tooltip, Avatar, TextEditor, Dropdown, call } from 'frappe-ui'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
const { getFormattedPercent, getFormattedFloat, getFormattedCurrency } =
|
const { getFormattedPercent, getFormattedFloat, getFormattedCurrency } =
|
||||||
@ -246,6 +246,7 @@ const rows = computed(() => {
|
|||||||
return getKanbanRows(tasks.value.data.data, tasks.value.data.fields)
|
return getKanbanRows(tasks.value.data.data, tasks.value.data.fields)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openTaskFromURL()
|
||||||
return parseRows(tasks.value?.data.data, tasks.value?.data.columns)
|
return parseRows(tasks.value?.data.data, tasks.value?.data.columns)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -391,4 +392,15 @@ function redirect(doctype, docname) {
|
|||||||
}
|
}
|
||||||
router.push({ name: name, params: params })
|
router.push({ name: name, params: params })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openTaskFromURL = () => {
|
||||||
|
const searchParams = new URLSearchParams(window.location.search)
|
||||||
|
const taskName = searchParams.get('open')
|
||||||
|
|
||||||
|
if (taskName && rows.value?.length) {
|
||||||
|
showTask(parseInt(taskName))
|
||||||
|
searchParams.delete('open')
|
||||||
|
window.history.replaceState(null, '', window.location.pathname)
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -2,11 +2,72 @@ import { defineConfig } from 'vite'
|
|||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import vueJsx from '@vitejs/plugin-vue-jsx'
|
import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
import fs from 'fs'
|
||||||
import frappeui from 'frappe-ui/vite'
|
import frappeui from 'frappe-ui/vite'
|
||||||
import { VitePWA } from 'vite-plugin-pwa'
|
import { VitePWA } from 'vite-plugin-pwa'
|
||||||
|
|
||||||
|
function appPath(app) {
|
||||||
|
const root = path.resolve(__dirname, '../..') // points to apps
|
||||||
|
const frontendPaths = [
|
||||||
|
// Standard frontend structure: appname/frontend/src
|
||||||
|
path.join(root, app, 'frontend', 'src'),
|
||||||
|
// Desk-based apps: appname/desk/src
|
||||||
|
path.join(root, app, 'desk', 'src'),
|
||||||
|
// Alternative frontend structures
|
||||||
|
path.join(root, app, 'client', 'src'),
|
||||||
|
path.join(root, app, 'ui', 'src'),
|
||||||
|
// Direct src structure: appname/src
|
||||||
|
path.join(root, app, 'src'),
|
||||||
|
]
|
||||||
|
return frontendPaths.find((srcPath) => fs.existsSync(srcPath)) || null
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasApp(app) {
|
||||||
|
return fs.existsSync(appPath(app))
|
||||||
|
}
|
||||||
|
|
||||||
|
// List of frontend apps used in this project
|
||||||
|
let apps = []
|
||||||
|
|
||||||
|
const alias = [
|
||||||
|
// Default "@" for this app
|
||||||
|
{
|
||||||
|
find: '@',
|
||||||
|
replacement: path.resolve(__dirname, 'src'),
|
||||||
|
},
|
||||||
|
|
||||||
|
// App-specific aliases like @helpdesk, @hrms, etc.
|
||||||
|
...apps.map((app) =>
|
||||||
|
hasApp(app)
|
||||||
|
? { find: `@${app}`, replacement: appPath(app) }
|
||||||
|
: { find: `@${app}`, replacement: `virtual:${app}` },
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
const defineFlags = Object.fromEntries(
|
||||||
|
apps.map((app) => [
|
||||||
|
`__HAS_${app.toUpperCase()}__`,
|
||||||
|
JSON.stringify(hasApp(app)),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
|
||||||
|
const virtualStubPlugin = {
|
||||||
|
name: 'virtual-empty-modules',
|
||||||
|
resolveId(id) {
|
||||||
|
if (id.startsWith('virtual:')) return '\0' + id
|
||||||
|
},
|
||||||
|
load(id) {
|
||||||
|
if (id.startsWith('\0virtual:')) {
|
||||||
|
return 'export default {}; export const missing = true;'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Generated app aliases:', alias)
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
define: defineFlags,
|
||||||
plugins: [
|
plugins: [
|
||||||
frappeui({
|
frappeui({
|
||||||
frappeProxy: true,
|
frappeProxy: true,
|
||||||
@ -60,12 +121,9 @@ export default defineConfig({
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
virtualStubPlugin,
|
||||||
],
|
],
|
||||||
resolve: {
|
resolve: { alias },
|
||||||
alias: {
|
|
||||||
'@': path.resolve(__dirname, 'src'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
include: [
|
include: [
|
||||||
'feather-icons',
|
'feather-icons',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user