Merge pull request #45 from shariquerik/custom-actions
feat: Custom Actions on Lead/Deal Page
This commit is contained in:
commit
d7796c040c
@ -2,6 +2,7 @@ import frappe
|
||||
from frappe import _
|
||||
|
||||
from crm.api.doc import get_doctype_fields
|
||||
from crm.fcrm.doctype.crm_form_script.crm_form_script import get_form_script
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_deal(name):
|
||||
@ -27,5 +28,6 @@ def get_deal(name):
|
||||
)
|
||||
|
||||
deal["doctype_fields"] = get_doctype_fields("CRM Deal")
|
||||
deal["doctype"] = "CRM Deal"
|
||||
deal["doctype"] = "CRM Deal"
|
||||
deal["_form_script"] = get_form_script('CRM Deal')
|
||||
return deal
|
||||
|
||||
0
crm/fcrm/doctype/crm_form_script/__init__.py
Normal file
0
crm/fcrm/doctype/crm_form_script/__init__.py
Normal file
8
crm/fcrm/doctype/crm_form_script/crm_form_script.js
Normal file
8
crm/fcrm/doctype/crm_form_script/crm_form_script.js
Normal file
@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("CRM Form Script", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
70
crm/fcrm/doctype/crm_form_script/crm_form_script.json
Normal file
70
crm/fcrm/doctype/crm_form_script/crm_form_script.json
Normal file
@ -0,0 +1,70 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "prompt",
|
||||
"creation": "2023-12-28 14:18:09.329868",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"dt",
|
||||
"column_break_gboh",
|
||||
"enabled",
|
||||
"section_break_xeox",
|
||||
"script"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "column_break_gboh",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_xeox",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "dt",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "DocType",
|
||||
"options": "DocType",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "enabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enabled"
|
||||
},
|
||||
{
|
||||
"fieldname": "script",
|
||||
"fieldtype": "Code",
|
||||
"label": "Script",
|
||||
"options": "JS"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-12-28 16:26:39.967069",
|
||||
"modified_by": "Administrator",
|
||||
"module": "FCRM",
|
||||
"name": "CRM Form Script",
|
||||
"naming_rule": "Set by user",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
45
crm/fcrm/doctype/crm_form_script/crm_form_script.py
Normal file
45
crm/fcrm/doctype/crm_form_script/crm_form_script.py
Normal file
@ -0,0 +1,45 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class CRMFormScript(Document):
|
||||
def validate(self):
|
||||
self.check_if_duplicate()
|
||||
|
||||
def check_if_duplicate(self):
|
||||
"""Check if there is already a script for this doctype"""
|
||||
if self.dt and self.enabled:
|
||||
filters = {
|
||||
"dt": self.dt,
|
||||
"enabled": 1,
|
||||
}
|
||||
if self.name:
|
||||
filters["name"] = ["!=", self.name]
|
||||
|
||||
if frappe.db.exists("CRM Form Script", filters):
|
||||
frappe.throw(
|
||||
frappe._(
|
||||
"Script already exists for this doctype and is enabled"
|
||||
),
|
||||
frappe.DuplicateEntryError,
|
||||
)
|
||||
|
||||
def get_form_script(dt):
|
||||
"""Returns the script for the given doctype"""
|
||||
FormScript = frappe.qb.DocType("CRM Form Script")
|
||||
query = (
|
||||
frappe.qb.from_(FormScript)
|
||||
.select("script")
|
||||
.where(FormScript.dt == dt)
|
||||
.where(FormScript.enabled == 1)
|
||||
.limit(1)
|
||||
)
|
||||
|
||||
doc = query.run(as_dict=True)
|
||||
if doc:
|
||||
return doc[0].script
|
||||
else:
|
||||
return None
|
||||
9
crm/fcrm/doctype/crm_form_script/test_crm_form_script.py
Normal file
9
crm/fcrm/doctype/crm_form_script/test_crm_form_script.py
Normal file
@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestCRMFormScript(FrappeTestCase):
|
||||
pass
|
||||
@ -2,6 +2,7 @@ import frappe
|
||||
from frappe import _
|
||||
|
||||
from crm.api.doc import get_doctype_fields
|
||||
from crm.fcrm.doctype.crm_form_script.crm_form_script import get_form_script
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_lead(name):
|
||||
@ -16,4 +17,5 @@ def get_lead(name):
|
||||
|
||||
lead["doctype_fields"] = get_doctype_fields("CRM Lead")
|
||||
lead["doctype"] = "CRM Lead"
|
||||
return lead
|
||||
lead["_form_script"] = get_form_script('CRM Lead')
|
||||
return lead
|
||||
|
||||
55
frontend/src/components/CustomActions.vue
Normal file
55
frontend/src/components/CustomActions.vue
Normal file
@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<Button
|
||||
v-for="action in normalActions"
|
||||
:label="action.label"
|
||||
@click="action.onClick()"
|
||||
>
|
||||
<template v-if="action.icon" #prefix>
|
||||
<FeatherIcon :name="action.icon" class="h-4 w-4" />
|
||||
</template>
|
||||
</Button>
|
||||
<Dropdown v-if="groupedActions.length" :options="groupedActions">
|
||||
<Button>
|
||||
<template #icon>
|
||||
<FeatherIcon name="more-horizontal" class="h-4 w-4" />
|
||||
</template>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { Dropdown, FeatherIcon } from 'frappe-ui'
|
||||
|
||||
const props = defineProps({
|
||||
actions: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const groupedActions = computed(() => {
|
||||
const _actions = props.actions.filter((action) => action.group)
|
||||
const groupedActions = {}
|
||||
|
||||
for (const action of _actions) {
|
||||
if (!groupedActions[action.group]) {
|
||||
groupedActions[action.group] = []
|
||||
}
|
||||
|
||||
groupedActions[action.group].push(action)
|
||||
}
|
||||
|
||||
let _groupedActions = [
|
||||
...Object.keys(groupedActions).map((group) => ({
|
||||
group,
|
||||
items: groupedActions[group],
|
||||
})),
|
||||
]
|
||||
return _groupedActions
|
||||
})
|
||||
|
||||
const normalActions = computed(() => {
|
||||
return props.actions.filter((action) => !action.group)
|
||||
})
|
||||
</script>
|
||||
@ -4,33 +4,10 @@
|
||||
<Breadcrumbs :items="breadcrumbs" />
|
||||
</template>
|
||||
<template #right-header>
|
||||
<Dropdown
|
||||
:options="[
|
||||
{
|
||||
icon: 'trash-2',
|
||||
label: 'Delete',
|
||||
onClick: () =>
|
||||
$dialog({
|
||||
title: 'Delete Deal',
|
||||
message: 'Are you sure you want to delete this deal?',
|
||||
actions: [
|
||||
{
|
||||
label: 'Delete',
|
||||
theme: 'red',
|
||||
variant: 'solid',
|
||||
onClick(close) {
|
||||
deleteDeal(deal.data.name)
|
||||
close()
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
]"
|
||||
@click.stop
|
||||
>
|
||||
<Button icon="more-horizontal" />
|
||||
</Dropdown>
|
||||
<CustomActions
|
||||
v-if="deal.data._customActions"
|
||||
:actions="deal.data._customActions"
|
||||
/>
|
||||
<component :is="deal.data._assignedTo?.length == 1 ? 'Button' : 'div'">
|
||||
<MultipleAvatar
|
||||
:avatars="deal.data._assignedTo"
|
||||
@ -310,8 +287,13 @@ import Link from '@/components/Controls/Link.vue'
|
||||
import Section from '@/components/Section.vue'
|
||||
import SectionFields from '@/components/SectionFields.vue'
|
||||
import SLASection from '@/components/SLASection.vue'
|
||||
import { openWebsite, createToast } from '@/utils'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import CustomActions from '@/components/CustomActions.vue'
|
||||
import {
|
||||
openWebsite,
|
||||
createToast,
|
||||
setupAssignees,
|
||||
setupCustomActions,
|
||||
} from '@/utils'
|
||||
import { contactsStore } from '@/stores/contacts'
|
||||
import { organizationsStore } from '@/stores/organizations'
|
||||
import { statusesStore } from '@/stores/statuses'
|
||||
@ -329,7 +311,6 @@ import {
|
||||
import { ref, computed, h } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const { getUser } = usersStore()
|
||||
const { getContactByName, contacts } = contactsStore()
|
||||
const { organizations, getOrganization } = organizationsStore()
|
||||
const { statusOptions, getDealStatus } = statusesStore()
|
||||
@ -348,12 +329,15 @@ const deal = createResource({
|
||||
cache: ['deal', props.dealId],
|
||||
auto: true,
|
||||
onSuccess: (data) => {
|
||||
let assignees = JSON.parse(data._assign) || []
|
||||
data._assignedTo = assignees.map((user) => ({
|
||||
name: user,
|
||||
image: getUser(user).user_image,
|
||||
label: getUser(user).full_name,
|
||||
}))
|
||||
setupAssignees(data)
|
||||
setupCustomActions(data, {
|
||||
doc: data,
|
||||
$dialog,
|
||||
router,
|
||||
updateField,
|
||||
deleteDoc: deleteDeal,
|
||||
call,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@ -4,33 +4,10 @@
|
||||
<Breadcrumbs :items="breadcrumbs" />
|
||||
</template>
|
||||
<template #right-header>
|
||||
<Dropdown
|
||||
:options="[
|
||||
{
|
||||
icon: 'trash-2',
|
||||
label: 'Delete',
|
||||
onClick: () =>
|
||||
$dialog({
|
||||
title: 'Delete Lead',
|
||||
message: 'Are you sure you want to delete this lead?',
|
||||
actions: [
|
||||
{
|
||||
label: 'Delete',
|
||||
theme: 'red',
|
||||
variant: 'solid',
|
||||
onClick(close) {
|
||||
deleteLead(lead.data.name)
|
||||
close()
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
]"
|
||||
@click.stop
|
||||
>
|
||||
<Button icon="more-horizontal" />
|
||||
</Dropdown>
|
||||
<CustomActions
|
||||
v-if="lead.data._customActions"
|
||||
:actions="lead.data._customActions"
|
||||
/>
|
||||
<component :is="lead.data._assignedTo?.length == 1 ? 'Button' : 'div'">
|
||||
<MultipleAvatar
|
||||
:avatars="lead.data._assignedTo"
|
||||
@ -216,8 +193,13 @@ import MultipleAvatar from '@/components/MultipleAvatar.vue'
|
||||
import Section from '@/components/Section.vue'
|
||||
import SectionFields from '@/components/SectionFields.vue'
|
||||
import SLASection from '@/components/SLASection.vue'
|
||||
import { openWebsite, createToast } from '@/utils'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import CustomActions from '@/components/CustomActions.vue'
|
||||
import {
|
||||
openWebsite,
|
||||
createToast,
|
||||
setupAssignees,
|
||||
setupCustomActions,
|
||||
} from '@/utils'
|
||||
import { contactsStore } from '@/stores/contacts'
|
||||
import { organizationsStore } from '@/stores/organizations'
|
||||
import { statusesStore } from '@/stores/statuses'
|
||||
@ -236,7 +218,6 @@ import {
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const { getUser } = usersStore()
|
||||
const { contacts } = contactsStore()
|
||||
const { organizations, getOrganization } = organizationsStore()
|
||||
const { statusOptions, getLeadStatus } = statusesStore()
|
||||
@ -255,12 +236,15 @@ const lead = createResource({
|
||||
cache: ['lead', props.leadId],
|
||||
auto: true,
|
||||
onSuccess: (data) => {
|
||||
let assignees = JSON.parse(data._assign) || []
|
||||
data._assignedTo = assignees.map((user) => ({
|
||||
name: user,
|
||||
image: getUser(user).user_image,
|
||||
label: getUser(user).full_name,
|
||||
}))
|
||||
setupAssignees(data)
|
||||
setupCustomActions(data, {
|
||||
doc: data,
|
||||
$dialog,
|
||||
router,
|
||||
updateField,
|
||||
deleteDoc: deleteLead,
|
||||
call,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import TaskStatusIcon from '@/components/Icons/TaskStatusIcon.vue'
|
||||
import TaskPriorityIcon from '@/components/Icons/TaskPriorityIcon.vue'
|
||||
import { useDateFormat, useTimeAgo } from '@vueuse/core'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { toast } from 'frappe-ui'
|
||||
import { h, computed } from 'vue'
|
||||
import { h } from 'vue'
|
||||
|
||||
export function createToast(options) {
|
||||
toast({
|
||||
@ -112,3 +113,19 @@ export function validateEmail(email) {
|
||||
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||
return regExp.test(email)
|
||||
}
|
||||
|
||||
export function setupAssignees(data) {
|
||||
let { getUser } = usersStore()
|
||||
let assignees = JSON.parse(data._assign) || []
|
||||
data._assignedTo = assignees.map((user) => ({
|
||||
name: user,
|
||||
image: getUser(user).user_image,
|
||||
label: getUser(user).full_name,
|
||||
}))
|
||||
}
|
||||
|
||||
export function setupCustomActions(data, obj) {
|
||||
let script = new Function(data._form_script + '\nreturn setupForm')()
|
||||
let formScript = script(obj)
|
||||
data._customActions = formScript?.actions || []
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user