Merge branch 'main-hotfix' into mergify/bp/main-hotfix/pr-971

This commit is contained in:
Shariq Ansari 2025-06-30 12:36:10 +05:30 committed by GitHub
commit aff26a5ea2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 428 additions and 114 deletions

View File

@ -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"

View File

@ -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

View File

@ -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']

View File

@ -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)

View File

@ -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>

View File

@ -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
} }
} }

View File

@ -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

View File

@ -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)

View File

@ -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>

View File

@ -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, '')
} }

View File

@ -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>

View File

@ -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',