Merge pull request #846 from pratikb64/delete-from-record-view
This commit is contained in:
commit
58d4691354
@ -11,6 +11,7 @@ from pypika import Criterion
|
||||
|
||||
from crm.api.views import get_views
|
||||
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()
|
||||
@ -677,6 +678,7 @@ def remove_assignments(doctype, name, assignees, ignore_permissions=False):
|
||||
ignore_permissions=ignore_permissions,
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_assigned_users(doctype, name, default_assigned_to=None):
|
||||
assigned_users = frappe.get_all(
|
||||
@ -745,3 +747,98 @@ def getCounts(d, doctype):
|
||||
"FCRM Note", filters={"reference_doctype": doctype, "reference_docname": d.get("name")}
|
||||
)
|
||||
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
|
||||
from frappe.utils import floor
|
||||
from phonenumbers import NumberParseException
|
||||
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"):
|
||||
@ -93,3 +96,129 @@ def seconds_to_duration(seconds):
|
||||
return f"{seconds}s"
|
||||
else:
|
||||
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
|
||||
|
||||
3
frontend/components.d.ts
vendored
3
frontend/components.d.ts
vendored
@ -31,6 +31,7 @@ declare module 'vue' {
|
||||
Autocomplete: typeof import('./src/components/frappe-ui/Autocomplete.vue')['default']
|
||||
AvatarIcon: typeof import('./src/components/Icons/AvatarIcon.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']
|
||||
CallArea: typeof import('./src/components/Activities/CallArea.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']
|
||||
DealsListView: typeof import('./src/components/ListViews/DealsListView.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']
|
||||
DesktopLayout: typeof import('./src/components/Layouts/DesktopLayout.vue')['default']
|
||||
DetailsIcon: typeof import('./src/components/Icons/DetailsIcon.vue')['default']
|
||||
@ -149,6 +151,7 @@ declare module 'vue' {
|
||||
LeadsListView: typeof import('./src/components/ListViews/LeadsListView.vue')['default']
|
||||
LightningIcon: typeof import('./src/components/Icons/LightningIcon.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']
|
||||
ListBulkActions: typeof import('./src/components/ListBulkActions.vue')['default']
|
||||
ListIcon: typeof import('./src/components/Icons/ListIcon.vue')['default']
|
||||
|
||||
165
frontend/src/components/BulkDeleteLinkedDocModal.vue
Normal file
165
frontend/src/components/BulkDeleteLinkedDocModal.vue
Normal file
@ -0,0 +1,165 @@
|
||||
<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 class="text-ink-gray-5">
|
||||
{{
|
||||
__('Are you sure you want to delete {0} items?', [
|
||||
props.items?.length,
|
||||
])
|
||||
}}
|
||||
</div>
|
||||
</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 class="text-ink-gray-5">
|
||||
{{
|
||||
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>
|
||||
</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.delete_bulk_docs', {
|
||||
items: props.items,
|
||||
doctype: props.doctype,
|
||||
delete_linked: confirmDeleteInfo.value.delete,
|
||||
}).then(() => {
|
||||
confirmDeleteInfo.value = {
|
||||
show: false,
|
||||
title: '',
|
||||
}
|
||||
show.value = false
|
||||
props.reload()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
280
frontend/src/components/DeleteLinkedDocModal.vue
Normal file
280
frontend/src/components/DeleteLinkedDocModal.vue
Normal file
@ -0,0 +1,280 @@
|
||||
<template>
|
||||
<Dialog v-model="show" :options="{ size: 'xl' }">
|
||||
<template #body v-if="!confirmDeleteInfo.show">
|
||||
<div class="bg-surface-modal px-4 pb-6 pt-5 sm:px-6">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="text-2xl leading-6 text-ink-gray-9 font-semibold">
|
||||
{{
|
||||
linkedDocs?.length == 0
|
||||
? __('Delete')
|
||||
: __('Delete or unlink linked documents')
|
||||
}}
|
||||
</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 v-if="linkedDocs?.length > 0">
|
||||
<span class="text-ink-gray-5 text-base">
|
||||
{{
|
||||
__(
|
||||
'Delete or unlink these linked documents before deleting this document',
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<LinkedDocsListView
|
||||
class="mt-4"
|
||||
:rows="linkedDocs"
|
||||
:columns="[
|
||||
{
|
||||
label: 'Document',
|
||||
key: 'title',
|
||||
},
|
||||
{
|
||||
label: 'Master',
|
||||
key: 'reference_doctype',
|
||||
width: '30%',
|
||||
},
|
||||
]"
|
||||
@selectionsChanged="
|
||||
(selections) => viewControls.updateSelections(selections)
|
||||
"
|
||||
:linkedDocsResource="linkedDocsResource"
|
||||
:unlinkLinkedDoc="unlinkLinkedDoc"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="linkedDocs?.length == 0" class="text-ink-gray-5 text-base">
|
||||
{{
|
||||
__('Are you sure you want to delete {0} - {1}?', [
|
||||
props.doctype,
|
||||
props.docname,
|
||||
])
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-4 pb-7 pt-0 sm:px-6">
|
||||
<div class="flex flex-row-reverse gap-2">
|
||||
<Button
|
||||
v-if="linkedDocs?.length > 0"
|
||||
theme="red"
|
||||
variant="solid"
|
||||
@click="confirmDelete()"
|
||||
>
|
||||
<div class="flex gap-1">
|
||||
<FeatherIcon name="trash" class="h-4 w-4" />
|
||||
<span>
|
||||
{{ __('Delete') }}
|
||||
{{
|
||||
viewControls?.selections?.length == 0
|
||||
? __('all')
|
||||
: `${viewControls?.selections?.length} item(s)`
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</Button>
|
||||
<Button
|
||||
v-if="linkedDocs?.length > 0"
|
||||
variant="subtle"
|
||||
theme="gray"
|
||||
@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"
|
||||
:label="__('Delete')"
|
||||
:loading="isDealCreating"
|
||||
@click="deleteDoc()"
|
||||
theme="red"
|
||||
/>
|
||||
</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">
|
||||
{{ 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 class="text-ink-gray-5 text-base">
|
||||
{{ confirmDeleteInfo.message }}
|
||||
</div>
|
||||
<div class="flex justify-end gap-2 mt-6">
|
||||
<Button variant="ghost" @click="cancel()">
|
||||
{{ __('Cancel') }}
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
:label="confirmDeleteInfo.title"
|
||||
@click="removeDocLinks()"
|
||||
theme="red"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { createResource, call } from 'frappe-ui'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
const show = defineModel()
|
||||
const router = useRouter()
|
||||
const props = defineProps({
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
doctype: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
docname: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
reload: {
|
||||
type: Function,
|
||||
},
|
||||
})
|
||||
const viewControls = ref({
|
||||
selections: [],
|
||||
updateSelections: (selections) => {
|
||||
viewControls.value.selections = Array.from(selections || [])
|
||||
},
|
||||
})
|
||||
|
||||
const confirmDeleteInfo = ref({
|
||||
show: false,
|
||||
title: '',
|
||||
})
|
||||
|
||||
const linkedDocsResource = createResource({
|
||||
url: 'crm.api.doc.get_linked_docs_of_document',
|
||||
params: {
|
||||
doctype: props.doctype,
|
||||
docname: props.docname,
|
||||
},
|
||||
auto: true,
|
||||
validate(params) {
|
||||
if (!params?.doctype || !params?.docname) {
|
||||
return false
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const linkedDocs = computed(() => {
|
||||
return (
|
||||
linkedDocsResource.data?.map((doc) => ({
|
||||
id: doc.reference_docname,
|
||||
...doc,
|
||||
})) || []
|
||||
)
|
||||
})
|
||||
|
||||
const cancel = () => {
|
||||
confirmDeleteInfo.value.show = false
|
||||
viewControls.value.updateSelections([])
|
||||
}
|
||||
|
||||
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.remove_linked_doc_reference', {
|
||||
items: selectedDocs,
|
||||
remove_contact: props.doctype == 'Contact',
|
||||
delete: doc.delete,
|
||||
}).then(() => {
|
||||
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 linked item'),
|
||||
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 linked item'),
|
||||
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,
|
||||
})
|
||||
viewControls.value.updateSelections([])
|
||||
}
|
||||
|
||||
const deleteDoc = async () => {
|
||||
await call('frappe.client.delete', {
|
||||
doctype: props.doctype,
|
||||
name: props.docname,
|
||||
})
|
||||
router.push({ name: props.name })
|
||||
props?.reload?.()
|
||||
}
|
||||
</script>
|
||||
@ -14,6 +14,20 @@
|
||||
:doctype="doctype"
|
||||
@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>
|
||||
|
||||
<script setup>
|
||||
@ -50,7 +64,11 @@ const { $dialog, $socket } = globalStore()
|
||||
const showEditModal = ref(false)
|
||||
const selectedValues = ref([])
|
||||
const unselectAllAction = ref(() => {})
|
||||
|
||||
const showDeleteDocModal = ref({
|
||||
showLinkedDocsModal: false,
|
||||
showDeleteModal: false,
|
||||
docname: null,
|
||||
})
|
||||
function editValues(selections, unselectAll) {
|
||||
selectedValues.value = selections
|
||||
showEditModal.value = true
|
||||
@ -88,33 +106,18 @@ function convertToDeal(selections, unselectAll) {
|
||||
}
|
||||
|
||||
function deleteValues(selections, unselectAll) {
|
||||
$dialog({
|
||||
title: __('Delete'),
|
||||
message: __('Are you sure you want to delete {0} item(s)?', [
|
||||
selections.size,
|
||||
]),
|
||||
variant: 'solid',
|
||||
theme: 'red',
|
||||
actions: [
|
||||
{
|
||||
label: __('Delete'),
|
||||
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 selectedDocs = Array.from(selections)
|
||||
if (selectedDocs.length == 1) {
|
||||
showDeleteDocModal.value = {
|
||||
showLinkedDocsModal: true,
|
||||
docname: selectedDocs[0],
|
||||
}
|
||||
} else {
|
||||
showDeleteDocModal.value = {
|
||||
showDeleteModal: true,
|
||||
items: selectedDocs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const showAssignmentModal = ref(false)
|
||||
|
||||
139
frontend/src/components/ListViews/LinkedDocsListView.vue
Normal file
139
frontend/src/components/ListViews/LinkedDocsListView.vue
Normal 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)"
|
||||
>
|
||||
</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>
|
||||
<span
|
||||
v-if="column.key === 'reference_doctype'"
|
||||
class="truncate text-base flex gap-2"
|
||||
>
|
||||
{{ getDoctypeName(row.reference_doctype) }}
|
||||
</span>
|
||||
</template>
|
||||
</ListRowItem>
|
||||
</ListRows>
|
||||
</div>
|
||||
</ListView>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
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 'CRM 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}`)
|
||||
}
|
||||
|
||||
const getDoctypeName = (doctype) => {
|
||||
return doctype.replace(/^(CRM|FCRM)\s*/, '')
|
||||
}
|
||||
</script>
|
||||
@ -80,7 +80,7 @@ import CallLogDetailModal from '@/components/Modals/CallLogDetailModal.vue'
|
||||
import CallLogModal from '@/components/Modals/CallLogModal.vue'
|
||||
import { getCallLogDetail } from '@/utils/callLog'
|
||||
import { createResource } from 'frappe-ui'
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
|
||||
const callLogsListView = ref(null)
|
||||
const showCallLogModal = ref(false)
|
||||
@ -124,4 +124,19 @@ function createCallLog() {
|
||||
callLog.value = {}
|
||||
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>
|
||||
|
||||
@ -105,7 +105,7 @@
|
||||
:label="__('Delete')"
|
||||
theme="red"
|
||||
size="sm"
|
||||
@click="deleteContact"
|
||||
@click="deleteContact()"
|
||||
>
|
||||
<template #prefix>
|
||||
<FeatherIcon name="trash-2" class="h-4 w-4" />
|
||||
@ -172,6 +172,13 @@
|
||||
:errorTitle="errorTitle"
|
||||
:errorMessage="errorMessage"
|
||||
/>
|
||||
<DeleteLinkedDocModal
|
||||
v-if="showDeleteLinkedDocModal"
|
||||
v-model="showDeleteLinkedDocModal"
|
||||
:doctype="'Contact'"
|
||||
:docname="contact.data.name"
|
||||
name="Contacts"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@ -293,6 +300,11 @@ usePageMeta(() => {
|
||||
icon: brand.favicon,
|
||||
}
|
||||
})
|
||||
const showDeleteLinkedDocModal = ref(false)
|
||||
|
||||
async function deleteContact() {
|
||||
showDeleteLinkedDocModal.value = true
|
||||
}
|
||||
|
||||
async function changeContactImage(file) {
|
||||
await call('frappe.client.set_value', {
|
||||
@ -304,28 +316,6 @@ async function changeContactImage(file) {
|
||||
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 tabs = [
|
||||
{
|
||||
|
||||
@ -139,6 +139,18 @@
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip :text="__('Delete')">
|
||||
<div>
|
||||
<Button
|
||||
class="h-7 w-7"
|
||||
@click="deleteDealWithModal(deal.data.name)"
|
||||
variant="subtle"
|
||||
theme="red"
|
||||
>
|
||||
<FeatherIcon name="trash" class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -329,6 +341,13 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<DeleteLinkedDocModal
|
||||
v-if="showDeleteLinkedDocModal"
|
||||
v-model="showDeleteLinkedDocModal"
|
||||
:doctype="'CRM Deal'"
|
||||
:docname="props.dealId"
|
||||
name="Deals"
|
||||
/>
|
||||
</template>
|
||||
<script setup>
|
||||
import ErrorPage from '@/components/ErrorPage.vue'
|
||||
@ -472,7 +491,11 @@ const reload = ref(false)
|
||||
const showOrganizationModal = ref(false)
|
||||
const showFilesUploader = ref(false)
|
||||
const _organization = ref({})
|
||||
const showDeleteLinkedDocModal = ref(false)
|
||||
|
||||
async function deleteDealWithModal() {
|
||||
showDeleteLinkedDocModal.value = true
|
||||
}
|
||||
function updateDeal(fieldname, value, callback) {
|
||||
value = Array.isArray(fieldname) ? '' : value
|
||||
|
||||
|
||||
@ -189,6 +189,18 @@
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip :text="__('Delete')">
|
||||
<div>
|
||||
<Button
|
||||
class="h-7 w-7"
|
||||
@click="deleteLeadWithModal(lead.data.name)"
|
||||
variant="subtle"
|
||||
theme="red"
|
||||
>
|
||||
<FeatherIcon name="trash" class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<ErrorMessage :message="__(error)" />
|
||||
</div>
|
||||
@ -333,6 +345,13 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<DeleteLinkedDocModal
|
||||
v-if="showDeleteLinkedDocModal"
|
||||
v-model="showDeleteLinkedDocModal"
|
||||
:doctype="'CRM Lead'"
|
||||
:docname="props.leadId"
|
||||
name="Leads"
|
||||
/>
|
||||
</template>
|
||||
<script setup>
|
||||
import ErrorPage from '@/components/ErrorPage.vue'
|
||||
@ -423,6 +442,7 @@ const props = defineProps({
|
||||
|
||||
const errorTitle = ref('')
|
||||
const errorMessage = ref('')
|
||||
const showDeleteLinkedDocModal = ref(false)
|
||||
|
||||
const lead = createResource({
|
||||
url: 'crm.fcrm.doctype.crm_lead.api.get_lead',
|
||||
@ -621,6 +641,10 @@ async function deleteLead(name) {
|
||||
router.push({ name: 'Leads' })
|
||||
}
|
||||
|
||||
async function deleteLeadWithModal(name) {
|
||||
showDeleteLinkedDocModal.value = true
|
||||
}
|
||||
|
||||
// Convert to Deal
|
||||
const showConvertToDealModal = ref(false)
|
||||
const existingContactChecked = ref(false)
|
||||
|
||||
@ -127,6 +127,7 @@ const viewControls = ref(null)
|
||||
watch(
|
||||
() => notes.value?.data?.page_length_count,
|
||||
(val, old_value) => {
|
||||
openNoteFromURL()
|
||||
if (!val || val === old_value) return
|
||||
updatedPageCount.value = val
|
||||
},
|
||||
@ -152,4 +153,20 @@ async function deleteNote(name) {
|
||||
})
|
||||
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>
|
||||
|
||||
@ -83,7 +83,7 @@
|
||||
:label="__('Delete')"
|
||||
theme="red"
|
||||
size="sm"
|
||||
@click="deleteOrganization"
|
||||
@click="deleteOrganization()"
|
||||
>
|
||||
<template #prefix>
|
||||
<FeatherIcon name="trash-2" class="h-4 w-4" />
|
||||
@ -164,6 +164,13 @@
|
||||
:errorTitle="errorTitle"
|
||||
:errorMessage="errorMessage"
|
||||
/>
|
||||
<DeleteLinkedDocModal
|
||||
v-if="showDeleteLinkedDocModal"
|
||||
v-model="showDeleteLinkedDocModal"
|
||||
:doctype="'CRM Organization'"
|
||||
:docname="props.organizationId"
|
||||
name="Organizations"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@ -202,6 +209,7 @@ import {
|
||||
} from 'frappe-ui'
|
||||
import { h, computed, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import DeleteLinkedDocModal from '@/components/DeleteLinkedDocModal.vue'
|
||||
|
||||
const props = defineProps({
|
||||
organizationId: {
|
||||
@ -222,6 +230,8 @@ const router = useRouter()
|
||||
const errorTitle = ref('')
|
||||
const errorMessage = ref('')
|
||||
|
||||
const showDeleteLinkedDocModal = ref(false)
|
||||
|
||||
const organization = createDocumentResource({
|
||||
doctype: 'CRM Organization',
|
||||
name: props.organizationId,
|
||||
@ -286,6 +296,10 @@ usePageMeta(() => {
|
||||
}
|
||||
})
|
||||
|
||||
async function deleteOrganization() {
|
||||
showDeleteLinkedDocModal.value = true
|
||||
}
|
||||
|
||||
async function changeOrganizationImage(file) {
|
||||
await call('frappe.client.set_value', {
|
||||
doctype: 'CRM Organization',
|
||||
@ -296,28 +310,6 @@ async function changeOrganizationImage(file) {
|
||||
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) {
|
||||
return url && url.replace(/^(?:https?:\/\/)?(?:www\.)?/i, '')
|
||||
}
|
||||
|
||||
@ -211,7 +211,7 @@ import { getMeta } from '@/stores/meta'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { formatDate, timeAgo } from '@/utils'
|
||||
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'
|
||||
|
||||
const { getFormattedPercent, getFormattedFloat, getFormattedCurrency } =
|
||||
@ -246,6 +246,7 @@ const rows = computed(() => {
|
||||
return getKanbanRows(tasks.value.data.data, tasks.value.data.fields)
|
||||
}
|
||||
|
||||
openTaskFromURL()
|
||||
return parseRows(tasks.value?.data.data, tasks.value?.data.columns)
|
||||
})
|
||||
|
||||
@ -391,4 +392,15 @@ function redirect(doctype, docname) {
|
||||
}
|
||||
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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user