feat: add list view & handle bulk delete, unlink - cherry-picked

This commit is contained in:
Pratik 2025-05-26 15:24:24 +00:00
parent f66de4cc72
commit 0bc1ee4c4e
9 changed files with 602 additions and 111 deletions

View File

@ -752,37 +752,28 @@ def getLinkedDocs(doctype, docname):
doc = frappe.get_doc(doctype, docname) doc = frappe.get_doc(doctype, docname)
linked_docs = get_linked_docs(doc) linked_docs = get_linked_docs(doc)
dynamic_linked_docs = get_dynamic_linked_docs(doc) dynamic_linked_docs = get_dynamic_linked_docs(doc)
linked_docs.extend(dynamic_linked_docs) linked_docs.extend(dynamic_linked_docs)
linked_docs = list({doc["reference_docname"]: doc for doc in linked_docs}.values())
return 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"CRM Call Log - from {data.get('from')} to {data.get('to')}"
if data.doctype == "CRM Deal":
title = data.get("organization")
@frappe.whitelist() docs_data.append({
def removeLinkedDocReference(doctype=None, docname=None,removeAll=False,removeContact=None): "doc": data.doctype,
"title": title or data.get("name"),
"reference_docname": doc["reference_docname"],
"reference_doctype": doc["reference_doctype"],
})
return docs_data
if (not doctype or not docname) and not removeAll:
return "Invalid doctype or docname"
if removeAll:
if removeContact:
ref_doc = getLinkedDocs(doctype, docname)
for linked_doc in ref_doc:
removeContactLink(linked_doc["reference_doctype"], linked_doc["reference_docname"])
return "success"
linked_docs = getLinkedDocs(doctype, docname)
for linked_doc in linked_docs:
removeDocLink(linked_doc["reference_doctype"], linked_doc["reference_docname"])
return "success"
else:
if removeContact:
removeContactLink(doctype, docname)
return "success"
removeDocLink(doctype, docname)
return "success"
def removeDocLink(doctype, docname): def removeDocLink(doctype, docname):
linked_doc_data = frappe.get_doc(doctype, docname) linked_doc_data = frappe.get_doc(doctype, docname)
@ -801,12 +792,40 @@ def removeContactLink(doctype, docname):
linked_doc_data.save(ignore_permissions=True) linked_doc_data.save(ignore_permissions=True)
@frappe.whitelist() @frappe.whitelist()
def deleteBulkDocs(doctype, items): def removeLinkedDocReference(items, removeContact=None, delete=False):
if isinstance(items, str):
items = frappe.parse_json(items)
for item in items:
if removeContact:
removeContactLink(item["doctype"], item["docname"])
else:
removeDocLink(item["doctype"], item["docname"])
if delete:
frappe.delete_doc(item["doctype"], item["docname"])
return "success"
@frappe.whitelist()
def deleteBulkDocs(doctype, items, deleteLinked=False):
from frappe.desk.reportview import delete_bulk from frappe.desk.reportview import delete_bulk
items = frappe.parse_json(items) items = frappe.parse_json(items)
for doc in items: for doc in items:
removeLinkedDocReference(doctype, doc, removeAll=True,removeContact=doctype=="Contact") linked_docs = getLinkedDocs(doctype, doc)
for linked_doc in linked_docs:
removeLinkedDocReference([
{
"doctype": linked_doc["reference_doctype"],
"docname": linked_doc["reference_docname"],
}
] ,
removeContact=doctype=="Contact",
delete=deleteLinked
)
if len(items) > 10: if len(items) > 10:
frappe.enqueue("frappe.desk.reportview.delete_bulk", doctype=doctype, items=items) frappe.enqueue("frappe.desk.reportview.delete_bulk", doctype=doctype, items=items)

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']
@ -55,6 +56,7 @@ declare module 'vue' {
ContactsIcon: typeof import('./src/components/Icons/ContactsIcon.vue')['default'] ContactsIcon: typeof import('./src/components/Icons/ContactsIcon.vue')['default']
ContactsListView: typeof import('./src/components/ListViews/ContactsListView.vue')['default'] ContactsListView: typeof import('./src/components/ListViews/ContactsListView.vue')['default']
ConvertIcon: typeof import('./src/components/Icons/ConvertIcon.vue')['default'] ConvertIcon: typeof import('./src/components/Icons/ConvertIcon.vue')['default']
copy: typeof import('./src/components/DeleteLinkedDocModal copy.vue')['default']
CountUpTimer: typeof import('./src/components/CountUpTimer.vue')['default'] CountUpTimer: typeof import('./src/components/CountUpTimer.vue')['default']
CreateDocumentModal: typeof import('./src/components/Modals/CreateDocumentModal.vue')['default'] CreateDocumentModal: typeof import('./src/components/Modals/CreateDocumentModal.vue')['default']
CRMLogo: typeof import('./src/components/Icons/CRMLogo.vue')['default'] CRMLogo: typeof import('./src/components/Icons/CRMLogo.vue')['default']
@ -150,11 +152,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']
LucidePlus: typeof import('~icons/lucide/plus')['default'] LucidePlus: typeof import('~icons/lucide/plus')['default']
LucideSearch: typeof import('~icons/lucide/search')['default'] LucideSearch: typeof import('~icons/lucide/search')['default']

View File

@ -0,0 +1,167 @@
<template>
<Dialog v-model="show" :options="{ size: 'xl' }">
<template #body>
<div class="bg-surface-modal px-4 pb-6 pt-5 sm:px-6">
<div class="mb-6 flex items-center justify-between">
<div>
<h3 class="text-2xl leading-6 text-ink-gray-9 font-semibold">
{{ __('Delete') }}
</h3>
</div>
<div class="flex items-center gap-1">
<Button variant="ghost" class="w-7" @click="show = false">
<FeatherIcon name="x" class="h-4 w-4" />
</Button>
</div>
</div>
<div>
<div>
{{
__('Are you sure you want to delete {0} items?', [
props.items?.length,
])
}}
</div>
<ErrorMessage class="mt-4" v-if="error" :message="__(error)" />
</div>
</div>
<div class="px-4 pb-7 pt-0 sm:px-6">
<div class="flex flex-row-reverse gap-2">
<Button theme="red" variant="solid" @click="confirmDelete()">
<div class="flex gap-1">
<FeatherIcon name="trash" class="h-4 w-4" />
<span>
{{ __('Delete {0} items', [props.items.length]) }}
</span>
</div>
</Button>
<Button variant="solid" @click="confirmUnlink()">
<div class="flex gap-1">
<FeatherIcon name="unlock" class="h-4 w-4" />
<span>
{{ __('Unlink and delete {0} items', [props.items.length]) }}
</span>
</div>
</Button>
</div>
</div>
</template>
<template #body v-if="confirmDeleteInfo.show">
<div class="bg-surface-modal px-4 pb-6 pt-5 sm:px-6">
<div class="mb-6 flex items-center justify-between">
<div>
<h3 class="text-2xl leading-6 text-ink-gray-9 font-semibold">
{{ __('Delete') }}
</h3>
</div>
<div class="flex items-center gap-1">
<Button variant="ghost" class="w-7" @click="show = false">
<FeatherIcon name="x" class="h-4 w-4" />
</Button>
</div>
</div>
<div>
<div>
{{
confirmDeleteInfo.delete
? __(
'This will delete selected items and items linked to it, are you sure?',
)
: __(
'This will delete selected items and unlink linked items to it, are you sure?',
)
}}
</div>
<ErrorMessage class="mt-4" v-if="error" :message="__(error)" />
</div>
</div>
<div class="px-4 pb-7 pt-0 sm:px-6">
<div class="flex flex-row-reverse gap-2">
<Button variant="solid" theme="red" @click="deleteDocs()">
<div class="flex gap-1">
<span>
{{
confirmDeleteInfo.delete
? __('Delete')
: __('Unlink and delete')
}}
</span>
</div>
</Button>
<Button variant="subtle" @click="confirmDeleteInfo.show = false">
<div class="flex gap-1">
<span>
{{ __('Cancel') }}
</span>
</div>
</Button>
</div>
</div>
</template>
</Dialog>
</template>
<script setup>
import { call } from 'frappe-ui'
import { ref } from 'vue'
const show = defineModel()
const props = defineProps({
doctype: {
type: String,
required: true,
},
items: {
type: Array,
required: true,
},
reload: {
type: Function,
required: true,
},
})
const confirmDeleteInfo = ref({
show: false,
title: '',
message: '',
delete: false,
})
const confirmDelete = () => {
confirmDeleteInfo.value = {
show: true,
title: __('Delete'),
message: __('Are you sure you want to delete {0} linked doc(s)?', [
props.items.length,
]),
delete: true,
}
}
const confirmUnlink = () => {
confirmDeleteInfo.value = {
show: true,
title: __('Unlink'),
message: __('Are you sure you want to unlink {0} linked doc(s)?', [
props.items.length,
]),
delete: false,
}
}
const deleteDocs = () => {
call('crm.api.doc.deleteBulkDocs', {
items: props.items,
doctype: props.doctype,
deleteLinked: confirmDeleteInfo.value.delete,
}).then(() => {
confirmDeleteInfo.value = {
show: false,
title: '',
}
show.value = false
props.reload()
})
}
</script>

View File

@ -1,6 +1,6 @@
<template> <template>
<Dialog v-model="show" :options="{ size: 'xl' }"> <Dialog v-model="show" :options="{ size: 'xl' }">
<template #body> <template #body v-if="!confirmDeleteInfo.show">
<div class="bg-surface-modal px-4 pb-6 pt-5 sm:px-6"> <div class="bg-surface-modal px-4 pb-6 pt-5 sm:px-6">
<div class="mb-6 flex items-center justify-between"> <div class="mb-6 flex items-center justify-between">
<div> <div>
@ -15,44 +15,36 @@
</div> </div>
</div> </div>
<div> <div>
<div v-if="linkedDocs.data?.length > 0"> <div v-if="linkedDocs?.length > 0">
<span> <span>
{{ {{
__( __(
'Unlink these linked documents before deleting this document', 'Delete or unlink these linked documents before deleting this document',
) )
}} }}
</span> </span>
<ul class="mt-5 space-y-1"> <LinkedDocsListView
<hr /> class="mt-4"
<li v-for="doc in linkedDocs.data" :key="doc.name"> :rows="linkedDocs"
<div class="flex justify-between items-center"> :columns="[
<span {
class="text-lg font-medium text-ellipsis overflow-hidden whitespace-nowrap w-full" label: 'Document',
>{{ doc.reference_doctype }} ({{ key: 'title',
doc.reference_docname },
}})</span {
> label: 'Doctype',
<div class="flex gap-2"> key: 'reference_doctype',
<Button variant="ghost" @click="viewLinkedDoc(doc)"> width: '30%',
<div class="flex gap-1"> },
<FeatherIcon name="external-link" class="h-4 w-4" /> ]"
<span> View </span> @selectionsChanged="
</div> (selections) => viewControls.updateSelections(selections)
</Button> "
<Button variant="ghost" @click="unlinkLinkedDoc(doc)"> :linkedDocsResource="linkedDocsResource"
<div class="flex gap-1"> :unlinkLinkedDoc="unlinkLinkedDoc"
<FeatherIcon name="unlock" class="h-4 w-4" /> />
<span> Unlink </span>
</div>
</Button>
</div>
</div>
<hr class="my-2 w-full" />
</li>
</ul>
</div> </div>
<div v-if="linkedDocs.data?.length == 0"> <div v-if="linkedDocs?.length == 0">
{{ {{
__('Are you sure you want to delete {0} - {1}?', [ __('Are you sure you want to delete {0} - {1}?', [
props.doctype, props.doctype,
@ -66,23 +58,42 @@
<div class="px-4 pb-7 pt-0 sm:px-6"> <div class="px-4 pb-7 pt-0 sm:px-6">
<div class="flex flex-row-reverse gap-2"> <div class="flex flex-row-reverse gap-2">
<Button <Button
v-if="linkedDocs.data?.length > 0" v-if="linkedDocs?.length > 0"
theme="red"
variant="solid" variant="solid"
@click=" @click="confirmDelete()"
unlinkLinkedDoc({
reference_doctype: props.doctype,
reference_docname: props.docname,
removeAll: true,
})
"
> >
<div class="flex gap-1"> <div class="flex gap-1">
<FeatherIcon name="unlock" class="h-4 w-4" /> <FeatherIcon name="trash" class="h-4 w-4" />
<span> Unlink all </span> <span>
Delete and unlink
{{
viewControls?.selections?.length == 0
? 'all'
: `${viewControls?.selections?.length} item(s)`
}}
</span>
</div> </div>
</Button> </Button>
<Button <Button
v-if="linkedDocs.data?.length == 0" v-if="linkedDocs?.length > 0"
variant="solid"
@click="confirmUnlink()"
>
<div class="flex gap-1">
<FeatherIcon name="unlock" class="h-4 w-4" />
<span>
Unlink
{{
viewControls?.selections?.length == 0
? 'all'
: `${viewControls?.selections?.length} item(s)`
}}
</span>
</div>
</Button>
<Button
v-if="linkedDocs?.length == 0"
variant="solid" variant="solid"
:label="__('Delete')" :label="__('Delete')"
:loading="isDealCreating" :loading="isDealCreating"
@ -92,12 +103,43 @@
</div> </div>
</div> </div>
</template> </template>
<template #body v-if="confirmDeleteInfo.show">
<div class="bg-surface-modal px-4 pb-6 pt-5 sm:px-6">
<div class="mb-6 flex items-center justify-between">
<div>
<h3 class="text-2xl leading-6 text-ink-gray-9 font-semibold">
{{ confirmDeleteInfo.title }}
</h3>
</div>
<div class="flex items-center gap-1">
<Button variant="ghost" class="w-7" @click="show = false">
<FeatherIcon name="x" class="h-4 w-4" />
</Button>
</div>
</div>
<div>
{{ confirmDeleteInfo.message }}
</div>
<div class="flex flex-row-reverse gap-2 mt-6">
<Button variant="ghost" @click="confirmDeleteInfo.show = false">
{{ __('Cancel') }}
</Button>
<Button
variant="solid"
:label="confirmDeleteInfo.title"
@click="removeDocLinks()"
theme="red"
/>
</div>
</div>
</template>
</Dialog> </Dialog>
</template> </template>
<script setup> <script setup>
import { createResource, call } from 'frappe-ui' import { createResource, call } from 'frappe-ui'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { computed, ref } from 'vue'
const show = defineModel() const show = defineModel()
const router = useRouter() const router = useRouter()
@ -114,9 +156,23 @@ const props = defineProps({
type: String, type: String,
required: true, required: true,
}, },
reload: {
type: Function,
},
})
const viewControls = ref({
selections: [],
updateSelections: (selections) => {
viewControls.value.selections = Array.from(selections || [])
},
}) })
const linkedDocs = createResource({ const confirmDeleteInfo = ref({
show: false,
title: '',
})
const linkedDocsResource = createResource({
url: 'crm.api.doc.getLinkedDocs', url: 'crm.api.doc.getLinkedDocs',
params: { params: {
doctype: props.doctype, doctype: props.doctype,
@ -130,18 +186,76 @@ const linkedDocs = createResource({
}, },
}) })
const viewLinkedDoc = (doc) => { const linkedDocs = computed(() => {
window.open(`/app/Form/${doc.reference_doctype}/${doc.reference_docname}`) return (
} linkedDocsResource.data?.map((doc) => ({
id: doc.reference_docname,
...doc,
})) || []
)
})
const unlinkLinkedDoc = (doc) => { const unlinkLinkedDoc = (doc) => {
let selectedDocs = []
if (viewControls.value.selections.length > 0) {
Array.from(viewControls.value.selections).forEach((selection) => {
const docData = linkedDocs.value.find((d) => d.id == selection)
selectedDocs.push({
doctype: docData.reference_doctype,
docname: docData.reference_docname,
})
})
} else {
selectedDocs = linkedDocs.value.map((doc) => ({
doctype: doc.reference_doctype,
docname: doc.reference_docname,
}))
}
call('crm.api.doc.removeLinkedDocReference', { call('crm.api.doc.removeLinkedDocReference', {
doctype: doc.reference_doctype, items: selectedDocs,
docname: doc.reference_docname,
removeAll: doc.removeAll,
removeContact: props.doctype == 'Contact', removeContact: props.doctype == 'Contact',
delete: doc.delete,
}).then(() => { }).then(() => {
linkedDocs.reload() linkedDocsResource.reload()
confirmDeleteInfo.value = {
show: false,
title: '',
}
})
}
const confirmDelete = () => {
const items =
viewControls.value.selections.length == 0
? 'all'
: viewControls.value.selections.length
confirmDeleteInfo.value = {
show: true,
title: __('Delete'),
message: __('Are you sure you want to delete {0} linked item(s)?', [items]),
delete: true,
}
}
const confirmUnlink = () => {
const items =
viewControls.value.selections.length == 0
? 'all'
: viewControls.value.selections.length
confirmDeleteInfo.value = {
show: true,
title: __('Unlink'),
message: __('Are you sure you want to unlink {0} linked item(s)?', [items]),
delete: false,
}
}
const removeDocLinks = () => {
unlinkLinkedDoc({
reference_doctype: props.doctype,
reference_docname: props.docname,
delete: confirmDeleteInfo.value.delete,
}) })
} }
@ -151,5 +265,6 @@ const deleteDoc = async () => {
name: props.docname, name: props.docname,
}) })
router.push({ name: props.name }) router.push({ name: props.name })
props?.reload?.()
} }
</script> </script>

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('crm.api.doc.deleteBulkDocs', {
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

@ -0,0 +1,139 @@
<template>
<ListView
:class="$attrs.class"
:columns="columns"
:rows="rows"
:options="{
selectable: true,
showTooltip: true,
resizeColumn: true,
}"
row-key="reference_docname"
@update:selections="(selections) => emit('selectionsChanged', selections)"
ref="listViewRef"
>
<ListHeader @columnWidthUpdated="emit('columnWidthUpdated')">
<ListHeaderItem
v-for="column in columns"
:key="column.key"
:item="column"
@columnWidthUpdated="emit('columnWidthUpdated', column)"
>
<Button
v-if="column.key == 'reference_docname'"
variant="ghosted"
class="!h-4"
:class="isLikeFilterApplied ? 'fill-red-500' : 'fill-white'"
@click="() => emit('applyLikeFilter')"
>
<HeartIcon class="h-4 w-4" />
</Button>
</ListHeaderItem>
</ListHeader>
<div class="*:mx-0 *:sm:mx-0">
<ListRows :rows="rows" v-slot="{ idx, column, item, row }">
<ListRowItem
:item="item"
@click="listViewRef.toggleRow(row['reference_docname'])"
>
<template #default="{ label }">
<div
v-if="column.key === 'title'"
class="truncate text-base flex gap-2"
>
<span>
{{ label }}
</span>
<FeatherIcon
name="external-link"
class="h-4 w-4"
@click.stop="viewLinkedDoc(row)"
/>
</div>
</template>
</ListRowItem>
</ListRows>
</div>
</ListView>
</template>
<script setup>
import HeartIcon from '@/components/Icons/HeartIcon.vue'
import ListRows from '@/components/ListViews/ListRows.vue'
import { ListView, ListHeader, ListHeaderItem, ListRowItem } from 'frappe-ui'
import { ref } from 'vue'
const props = defineProps({
rows: {
type: Array,
required: true,
},
columns: {
type: Array,
required: true,
},
linkedDocsResource: {
type: Object,
required: true,
},
unlinkLinkedDoc: {
type: Function,
required: true,
},
options: {
type: Object,
default: () => ({
selectable: true,
showTooltip: true,
resizeColumn: false,
totalCount: 0,
rowCount: 0,
}),
},
})
const emit = defineEmits([
'loadMore',
'updatePageCount',
'columnWidthUpdated',
'applyFilter',
'applyLikeFilter',
'likeDoc',
'selectionsChanged',
])
const listViewRef = ref(null)
const viewLinkedDoc = (doc) => {
let page = ''
let id = ''
switch (doc.reference_doctype) {
case 'CRM Lead':
page = 'leads'
id = doc.reference_docname
break
case 'CRM Call Log':
page = 'call-logs'
id = `view?open=${doc.reference_docname}`
break
case 'CRM Task':
page = 'tasks'
id = `view?open=${doc.reference_docname}`
break
case 'Contact':
page = 'contacts'
id = doc.reference_docname
break
case 'Organization':
page = 'organizations'
id = doc.reference_docname
break
case 'FCRM Note':
page = 'notes'
id = `view?open=${doc.reference_docname}`
break
default:
break
}
window.open(`/crm/${page}/${id}`)
}
</script>

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

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

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