Merge pull request #221 from frappe/develop
chore: Merge develop to main
This commit is contained in:
commit
31aab68ee4
134
crm/api/doc.py
134
crm/api/doc.py
@ -66,12 +66,12 @@ def get_filterable_fields(doctype: str):
|
|||||||
|
|
||||||
# append DocFields
|
# append DocFields
|
||||||
DocField = frappe.qb.DocType("DocField")
|
DocField = frappe.qb.DocType("DocField")
|
||||||
doc_fields = get_fields_meta(DocField, doctype, allowed_fieldtypes, restricted_fields)
|
doc_fields = get_doctype_fields_meta(DocField, doctype, allowed_fieldtypes, restricted_fields)
|
||||||
res.extend(doc_fields)
|
res.extend(doc_fields)
|
||||||
|
|
||||||
# append Custom Fields
|
# append Custom Fields
|
||||||
CustomField = frappe.qb.DocType("Custom Field")
|
CustomField = frappe.qb.DocType("Custom Field")
|
||||||
custom_fields = get_fields_meta(CustomField, doctype, allowed_fieldtypes, restricted_fields)
|
custom_fields = get_doctype_fields_meta(CustomField, doctype, allowed_fieldtypes, restricted_fields)
|
||||||
res.extend(custom_fields)
|
res.extend(custom_fields)
|
||||||
|
|
||||||
# append standard fields (getting error when using frappe.model.std_fields)
|
# append standard fields (getting error when using frappe.model.std_fields)
|
||||||
@ -157,44 +157,7 @@ def get_group_by_fields(doctype: str):
|
|||||||
return fields
|
return fields
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
def get_doctype_fields_meta(DocField, doctype, allowed_fieldtypes, restricted_fields):
|
||||||
def get_quick_entry_fields(doctype: str):
|
|
||||||
sections = []
|
|
||||||
if frappe.db.exists("CRM Fields Layout", {"dt": doctype, "type": "Quick Entry"}):
|
|
||||||
layout = frappe.get_doc("CRM Fields Layout", {"dt": doctype, "type": "Quick Entry"})
|
|
||||||
else:
|
|
||||||
return []
|
|
||||||
|
|
||||||
if layout.layout:
|
|
||||||
sections = json.loads(layout.layout)
|
|
||||||
|
|
||||||
allowed_fields = []
|
|
||||||
for section in sections:
|
|
||||||
allowed_fields.extend(section.get("fields"))
|
|
||||||
|
|
||||||
fields = frappe.get_meta(doctype).fields
|
|
||||||
fields = [field for field in fields if field.fieldname in allowed_fields]
|
|
||||||
|
|
||||||
for section in sections:
|
|
||||||
for field in section.get("fields"):
|
|
||||||
field = next((f for f in fields if f.fieldname == field), None)
|
|
||||||
if field:
|
|
||||||
if field.fieldtype == "Select":
|
|
||||||
field.options = field.options.split("\n")
|
|
||||||
field.options = [{"label": _(option), "value": option} for option in field.options]
|
|
||||||
field.options.insert(0, {"label": "", "value": ""})
|
|
||||||
field = {
|
|
||||||
"label": _(field.label),
|
|
||||||
"name": field.fieldname,
|
|
||||||
"type": field.fieldtype,
|
|
||||||
"options": field.options,
|
|
||||||
"mandatory": field.reqd,
|
|
||||||
}
|
|
||||||
section["fields"][section.get("fields").index(field["name"])] = field
|
|
||||||
|
|
||||||
return sections or []
|
|
||||||
|
|
||||||
def get_fields_meta(DocField, doctype, allowed_fieldtypes, restricted_fields):
|
|
||||||
parent = "parent" if DocField._table_name == "tabDocField" else "dt"
|
parent = "parent" if DocField._table_name == "tabDocField" else "dt"
|
||||||
return (
|
return (
|
||||||
frappe.qb.from_(DocField)
|
frappe.qb.from_(DocField)
|
||||||
@ -412,9 +375,43 @@ def get_list_data(
|
|||||||
"list_script": get_form_script(doctype, "List"),
|
"list_script": get_form_script(doctype, "List"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
def get_doctype_fields(doctype, name):
|
def get_fields_meta(doctype, restricted_fieldtypes=None, as_array=False):
|
||||||
not_allowed_fieldtypes = [
|
not_allowed_fieldtypes = [
|
||||||
|
"Tab Break",
|
||||||
|
"Section Break",
|
||||||
|
"Column Break",
|
||||||
|
]
|
||||||
|
|
||||||
|
if restricted_fieldtypes:
|
||||||
|
restricted_fieldtypes = frappe.parse_json(restricted_fieldtypes)
|
||||||
|
not_allowed_fieldtypes += restricted_fieldtypes
|
||||||
|
|
||||||
|
fields = frappe.get_meta(doctype).fields
|
||||||
|
fields = [field for field in fields if field.fieldtype not in not_allowed_fieldtypes]
|
||||||
|
|
||||||
|
if as_array:
|
||||||
|
return fields
|
||||||
|
|
||||||
|
fields_meta = {}
|
||||||
|
for field in fields:
|
||||||
|
fields_meta[field.fieldname] = field
|
||||||
|
|
||||||
|
return fields_meta
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_sidebar_fields(doctype, name):
|
||||||
|
if not frappe.db.exists("CRM Fields Layout", {"dt": doctype, "type": "Side Panel"}):
|
||||||
|
return []
|
||||||
|
layout = frappe.get_doc("CRM Fields Layout", {"dt": doctype, "type": "Side Panel"}).layout
|
||||||
|
|
||||||
|
if not layout:
|
||||||
|
return []
|
||||||
|
|
||||||
|
layout = json.loads(layout)
|
||||||
|
|
||||||
|
not_allowed_fieldtypes = [
|
||||||
|
"Tab Break",
|
||||||
"Section Break",
|
"Section Break",
|
||||||
"Column Break",
|
"Column Break",
|
||||||
]
|
]
|
||||||
@ -422,56 +419,31 @@ def get_doctype_fields(doctype, name):
|
|||||||
fields = frappe.get_meta(doctype).fields
|
fields = frappe.get_meta(doctype).fields
|
||||||
fields = [field for field in fields if field.fieldtype not in not_allowed_fieldtypes]
|
fields = [field for field in fields if field.fieldtype not in not_allowed_fieldtypes]
|
||||||
|
|
||||||
sections = {}
|
|
||||||
section_fields = []
|
|
||||||
last_section = None
|
|
||||||
doc = frappe.get_cached_doc(doctype, name)
|
doc = frappe.get_cached_doc(doctype, name)
|
||||||
|
|
||||||
has_high_permlevel_fields = any(df.permlevel > 0 for df in fields)
|
has_high_permlevel_fields = any(df.permlevel > 0 for df in fields)
|
||||||
if has_high_permlevel_fields:
|
if has_high_permlevel_fields:
|
||||||
has_read_access_to_permlevels = doc.get_permlevel_access("read")
|
has_read_access_to_permlevels = doc.get_permlevel_access("read")
|
||||||
has_write_access_to_permlevels = doc.get_permlevel_access("write")
|
has_write_access_to_permlevels = doc.get_permlevel_access("write")
|
||||||
|
|
||||||
for field in fields:
|
for section in layout:
|
||||||
if field.fieldtype == "Tab Break" and last_section:
|
section["name"] = section.get("name") or section.get("label")
|
||||||
sections[last_section]["fields"] = section_fields
|
for field in section.get("fields") if section.get("fields") else []:
|
||||||
last_section = None
|
field_obj = next((f for f in fields if f.fieldname == field), None)
|
||||||
if field.read_only:
|
if field_obj:
|
||||||
section_fields = []
|
if field_obj.permlevel > 0:
|
||||||
continue
|
field_has_write_access = field_obj.permlevel in has_write_access_to_permlevels
|
||||||
if field.fieldtype == "Tab Break":
|
field_has_read_access = field_obj.permlevel in has_read_access_to_permlevels
|
||||||
if field.read_only:
|
if not field_has_write_access and field_has_read_access:
|
||||||
section_fields = []
|
field_obj.read_only = 1
|
||||||
continue
|
if not field_has_read_access and not field_has_write_access:
|
||||||
section_fields = []
|
field_obj.hidden = 1
|
||||||
last_section = field.fieldname
|
section["fields"][section.get("fields").index(field)] = get_field_obj(field_obj)
|
||||||
sections[field.fieldname] = {
|
|
||||||
"label": field.label,
|
|
||||||
"name": field.fieldname,
|
|
||||||
"opened": True,
|
|
||||||
"fields": [],
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
if field.permlevel > 0:
|
|
||||||
field_has_write_access = field.permlevel in has_write_access_to_permlevels
|
|
||||||
field_has_read_access = field.permlevel in has_read_access_to_permlevels
|
|
||||||
if not field_has_write_access and field_has_read_access:
|
|
||||||
field.read_only = 1
|
|
||||||
if not field_has_read_access and not field_has_write_access:
|
|
||||||
field.hidden = 1
|
|
||||||
section_fields.append(get_field_obj(field))
|
|
||||||
|
|
||||||
section_fields = []
|
|
||||||
for section in sections:
|
|
||||||
section_fields.append(sections[section])
|
|
||||||
|
|
||||||
fields = [field for field in fields if field.fieldtype not in "Tab Break"]
|
|
||||||
fields_meta = {}
|
fields_meta = {}
|
||||||
for field in fields:
|
for field in fields:
|
||||||
fields_meta[field.fieldname] = field
|
fields_meta[field.fieldname] = field
|
||||||
|
|
||||||
return section_fields, fields_meta
|
return layout
|
||||||
|
|
||||||
|
|
||||||
def get_field_obj(field):
|
def get_field_obj(field):
|
||||||
obj = {
|
obj = {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
|
||||||
from crm.api.doc import get_doctype_fields, get_assigned_users
|
from crm.api.doc import get_fields_meta, get_assigned_users
|
||||||
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
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@ -27,8 +27,8 @@ def get_deal(name):
|
|||||||
fields=["contact", "is_primary"],
|
fields=["contact", "is_primary"],
|
||||||
)
|
)
|
||||||
|
|
||||||
deal["doctype_fields"], deal["all_fields"] = get_doctype_fields("CRM Deal", name)
|
|
||||||
deal["doctype"] = "CRM Deal"
|
deal["doctype"] = "CRM Deal"
|
||||||
|
deal["fields_meta"] = get_fields_meta("CRM Deal")
|
||||||
deal["_form_script"] = get_form_script('CRM Deal')
|
deal["_form_script"] = get_form_script('CRM Deal')
|
||||||
deal["_assign"] = get_assigned_users("CRM Deal", deal.name, deal.owner)
|
deal["_assign"] = get_assigned_users("CRM Deal", deal.name, deal.owner)
|
||||||
return deal
|
return deal
|
||||||
|
|||||||
@ -19,8 +19,7 @@
|
|||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Document Type",
|
"label": "Document Type",
|
||||||
"options": "DocType",
|
"options": "DocType"
|
||||||
"unique": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "type",
|
"fieldname": "type",
|
||||||
@ -47,7 +46,7 @@
|
|||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-06-07 17:01:20.250697",
|
"modified": "2024-06-13 15:10:01.612851",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "FCRM",
|
"module": "FCRM",
|
||||||
"name": "CRM Fields Layout",
|
"name": "CRM Fields Layout",
|
||||||
|
|||||||
@ -1,9 +1,67 @@
|
|||||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
# import frappe
|
import json
|
||||||
|
import frappe
|
||||||
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
class CRMFieldsLayout(Document):
|
class CRMFieldsLayout(Document):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_fields_layout(doctype: str, type: str):
|
||||||
|
sections = []
|
||||||
|
if frappe.db.exists("CRM Fields Layout", {"dt": doctype, "type": type}):
|
||||||
|
layout = frappe.get_doc("CRM Fields Layout", {"dt": doctype, "type": type})
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
|
if layout.layout:
|
||||||
|
sections = json.loads(layout.layout)
|
||||||
|
|
||||||
|
allowed_fields = []
|
||||||
|
for section in sections:
|
||||||
|
if not section.get("fields"):
|
||||||
|
continue
|
||||||
|
allowed_fields.extend(section.get("fields"))
|
||||||
|
|
||||||
|
fields = frappe.get_meta(doctype).fields
|
||||||
|
fields = [field for field in fields if field.fieldname in allowed_fields]
|
||||||
|
|
||||||
|
for section in sections:
|
||||||
|
for field in section.get("fields") if section.get("fields") else []:
|
||||||
|
field = next((f for f in fields if f.fieldname == field), None)
|
||||||
|
if field:
|
||||||
|
if field.fieldtype == "Select":
|
||||||
|
field.options = field.options.split("\n")
|
||||||
|
field.options = [{"label": _(option), "value": option} for option in field.options]
|
||||||
|
field.options.insert(0, {"label": "", "value": ""})
|
||||||
|
field = {
|
||||||
|
"label": _(field.label),
|
||||||
|
"name": field.fieldname,
|
||||||
|
"type": field.fieldtype,
|
||||||
|
"options": field.options,
|
||||||
|
"mandatory": field.reqd,
|
||||||
|
}
|
||||||
|
section["fields"][section.get("fields").index(field["name"])] = field
|
||||||
|
|
||||||
|
return sections or []
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def save_fields_layout(doctype: str, type: str, layout: str):
|
||||||
|
if frappe.db.exists("CRM Fields Layout", {"dt": doctype, "type": type}):
|
||||||
|
doc = frappe.get_doc("CRM Fields Layout", {"dt": doctype, "type": type})
|
||||||
|
else:
|
||||||
|
doc = frappe.new_doc("CRM Fields Layout")
|
||||||
|
|
||||||
|
doc.update({
|
||||||
|
"dt": doctype,
|
||||||
|
"type": type,
|
||||||
|
"layout": layout,
|
||||||
|
})
|
||||||
|
doc.save(ignore_permissions=True)
|
||||||
|
|
||||||
|
return doc.layout
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
|
||||||
from crm.api.doc import get_doctype_fields, get_assigned_users
|
from crm.api.doc import get_fields_meta, get_assigned_users
|
||||||
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
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@ -15,8 +15,8 @@ def get_lead(name):
|
|||||||
frappe.throw(_("Lead not found"), frappe.DoesNotExistError)
|
frappe.throw(_("Lead not found"), frappe.DoesNotExistError)
|
||||||
lead = lead.pop()
|
lead = lead.pop()
|
||||||
|
|
||||||
lead["doctype_fields"], lead["all_fields"] = get_doctype_fields("CRM Lead", name)
|
|
||||||
lead["doctype"] = "CRM Lead"
|
lead["doctype"] = "CRM Lead"
|
||||||
|
lead["fields_meta"] = get_fields_meta("CRM Lead")
|
||||||
lead["_form_script"] = get_form_script('CRM Lead')
|
lead["_form_script"] = get_form_script('CRM Lead')
|
||||||
lead["_assign"] = get_assigned_users("CRM Lead", lead.name, lead.owner)
|
lead["_assign"] = get_assigned_users("CRM Lead", lead.name, lead.owner)
|
||||||
return lead
|
return lead
|
||||||
|
|||||||
@ -6,4 +6,5 @@ crm.patches.v1_0.move_crm_note_data_to_fcrm_note
|
|||||||
[post_model_sync]
|
[post_model_sync]
|
||||||
# Patches added in this section will be executed after doctypes are migrated
|
# Patches added in this section will be executed after doctypes are migrated
|
||||||
crm.patches.v1_0.create_email_template_custom_fields
|
crm.patches.v1_0.create_email_template_custom_fields
|
||||||
crm.patches.v1_0.create_default_fields_layout
|
crm.patches.v1_0.create_default_fields_layout
|
||||||
|
crm.patches.v1_0.create_default_sidebar_fields_layout
|
||||||
63
crm/patches/v1_0/create_default_sidebar_fields_layout.py
Normal file
63
crm/patches/v1_0/create_default_sidebar_fields_layout.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import json
|
||||||
|
import frappe
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
if not frappe.db.exists("CRM Fields Layout", {"dt": "CRM Lead", "type": "Side Panel"}):
|
||||||
|
create_doctype_fields_layout("CRM Lead")
|
||||||
|
|
||||||
|
if not frappe.db.exists("CRM Fields Layout", {"dt": "CRM Deal", "type": "Side Panel"}):
|
||||||
|
create_doctype_fields_layout("CRM Deal")
|
||||||
|
|
||||||
|
def create_doctype_fields_layout(doctype):
|
||||||
|
not_allowed_fieldtypes = [
|
||||||
|
"Section Break",
|
||||||
|
"Column Break",
|
||||||
|
]
|
||||||
|
|
||||||
|
fields = frappe.get_meta(doctype).fields
|
||||||
|
fields = [field for field in fields if field.fieldtype not in not_allowed_fieldtypes]
|
||||||
|
|
||||||
|
sections = {}
|
||||||
|
section_fields = []
|
||||||
|
last_section = None
|
||||||
|
|
||||||
|
for field in fields:
|
||||||
|
if field.fieldtype == "Tab Break" and last_section:
|
||||||
|
sections[last_section]["fields"] = section_fields
|
||||||
|
last_section = None
|
||||||
|
if field.read_only:
|
||||||
|
section_fields = []
|
||||||
|
continue
|
||||||
|
if field.fieldtype == "Tab Break":
|
||||||
|
if field.read_only:
|
||||||
|
section_fields = []
|
||||||
|
continue
|
||||||
|
section_fields = []
|
||||||
|
last_section = field.fieldname
|
||||||
|
sections[field.fieldname] = {
|
||||||
|
"label": field.label,
|
||||||
|
"name": field.fieldname,
|
||||||
|
"opened": True,
|
||||||
|
"fields": [],
|
||||||
|
}
|
||||||
|
if field.fieldname == "contacts_tab":
|
||||||
|
sections[field.fieldname]["editable"] = False
|
||||||
|
sections[field.fieldname]["contacts"] = []
|
||||||
|
else:
|
||||||
|
section_fields.append(field.fieldname)
|
||||||
|
|
||||||
|
section_fields = []
|
||||||
|
for section in sections:
|
||||||
|
if section == "contacts_tab":
|
||||||
|
sections[section]["name"] = "contacts_section"
|
||||||
|
sections[section].pop("fields", None)
|
||||||
|
section_fields.append(sections[section])
|
||||||
|
|
||||||
|
frappe.get_doc({
|
||||||
|
"doctype": "CRM Fields Layout",
|
||||||
|
"dt": doctype,
|
||||||
|
"type": "Side Panel",
|
||||||
|
"layout": json.dumps(section_fields),
|
||||||
|
}).insert(ignore_permissions=True)
|
||||||
|
|
||||||
|
return section_fields
|
||||||
@ -235,9 +235,9 @@ const detailFields = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const sections = createResource({
|
const sections = createResource({
|
||||||
url: 'crm.api.doc.get_quick_entry_fields',
|
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
||||||
cache: ['quickEntryFields', 'Contact'],
|
cache: ['quickEntryFields', 'Contact'],
|
||||||
params: { doctype: 'Contact' },
|
params: { doctype: 'Contact', type: 'Quick Entry'},
|
||||||
auto: true,
|
auto: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -77,9 +77,9 @@ const chooseExistingContact = ref(false)
|
|||||||
const chooseExistingOrganization = ref(false)
|
const chooseExistingOrganization = ref(false)
|
||||||
|
|
||||||
const sections = createResource({
|
const sections = createResource({
|
||||||
url: 'crm.api.doc.get_quick_entry_fields',
|
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
||||||
cache: ['quickEntryFields', 'CRM Deal'],
|
cache: ['quickEntryFields', 'CRM Deal'],
|
||||||
params: { doctype: 'CRM Deal' },
|
params: { doctype: 'CRM Deal', type: 'Quick Entry'},
|
||||||
auto: true,
|
auto: true,
|
||||||
transform: (data) => {
|
transform: (data) => {
|
||||||
return data.forEach((section) => {
|
return data.forEach((section) => {
|
||||||
|
|||||||
@ -40,9 +40,9 @@ const error = ref(null)
|
|||||||
const isLeadCreating = ref(false)
|
const isLeadCreating = ref(false)
|
||||||
|
|
||||||
const sections = createResource({
|
const sections = createResource({
|
||||||
url: 'crm.api.doc.get_quick_entry_fields',
|
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
||||||
cache: ['quickEntryFields', 'CRM Lead'],
|
cache: ['quickEntryFields', 'CRM Lead'],
|
||||||
params: { doctype: 'CRM Lead' },
|
params: { doctype: 'CRM Lead', type: 'Quick Entry' },
|
||||||
auto: true,
|
auto: true,
|
||||||
transform: (data) => {
|
transform: (data) => {
|
||||||
return data.forEach((section) => {
|
return data.forEach((section) => {
|
||||||
|
|||||||
@ -225,9 +225,9 @@ const fields = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const sections = createResource({
|
const sections = createResource({
|
||||||
url: 'crm.api.doc.get_quick_entry_fields',
|
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
||||||
cache: ['quickEntryFields', 'CRM Organization'],
|
cache: ['quickEntryFields', 'CRM Organization'],
|
||||||
params: { doctype: 'CRM Organization' },
|
params: { doctype: 'CRM Organization', type: 'Quick Entry'},
|
||||||
auto: true,
|
auto: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -28,6 +28,10 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
default: 'left',
|
default: 'left',
|
||||||
},
|
},
|
||||||
|
parent: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const sidebarResizing = ref(false)
|
const sidebarResizing = ref(false)
|
||||||
@ -58,6 +62,9 @@ function resize(e) {
|
|||||||
sidebarWidth.value =
|
sidebarWidth.value =
|
||||||
props.side == 'left' ? e.clientX : window.innerWidth - e.clientX
|
props.side == 'left' ? e.clientX : window.innerWidth - e.clientX
|
||||||
|
|
||||||
|
let gap = props.parent ? distance() : 0
|
||||||
|
sidebarWidth.value = sidebarWidth.value - gap
|
||||||
|
|
||||||
// snap to props.defaultWidth
|
// snap to props.defaultWidth
|
||||||
let range = [props.defaultWidth - 10, props.defaultWidth + 10]
|
let range = [props.defaultWidth - 10, props.defaultWidth + 10]
|
||||||
if (sidebarWidth.value > range[0] && sidebarWidth.value < range[1]) {
|
if (sidebarWidth.value > range[0] && sidebarWidth.value < range[1]) {
|
||||||
@ -71,4 +78,9 @@ function resize(e) {
|
|||||||
sidebarWidth.value = props.maxWidth
|
sidebarWidth.value = props.maxWidth
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function distance() {
|
||||||
|
if (!props.parent) return 0
|
||||||
|
const rect = props.parent.getBoundingClientRect()
|
||||||
|
return window.innerWidth - rect[props.side]
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -136,7 +136,7 @@ const _fields = computed(() => {
|
|||||||
let all_fields = []
|
let all_fields = []
|
||||||
props.fields?.forEach((field) => {
|
props.fields?.forEach((field) => {
|
||||||
let df = field.all_properties
|
let df = field.all_properties
|
||||||
if (df.depends_on) evaluate_depends_on(df.depends_on, field)
|
if (df?.depends_on) evaluate_depends_on(df.depends_on, field)
|
||||||
all_fields.push(field)
|
all_fields.push(field)
|
||||||
})
|
})
|
||||||
return all_fields
|
return all_fields
|
||||||
|
|||||||
94
frontend/src/components/Settings/FieldsLayout.vue
Normal file
94
frontend/src/components/Settings/FieldsLayout.vue
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="parentRef" class="flex h-full">
|
||||||
|
<div class="flex-1 flex flex-col justify-between gap-2 p-8">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<h2 class="flex gap-2 text-xl font-semibold leading-none h-5 mb-4">
|
||||||
|
<div>{{ __('Sidebar Fields Layout') }}</div>
|
||||||
|
<Badge
|
||||||
|
v-if="dirty"
|
||||||
|
:label="__('Not Saved')"
|
||||||
|
variant="subtle"
|
||||||
|
theme="orange"
|
||||||
|
/>
|
||||||
|
</h2>
|
||||||
|
<FormControl
|
||||||
|
type="select"
|
||||||
|
v-model="doctype"
|
||||||
|
:label="__('DocType')"
|
||||||
|
:options="['CRM Lead', 'CRM Deal']"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row-reverse gap-2">
|
||||||
|
<Button
|
||||||
|
:loading="loading"
|
||||||
|
:label="__('Save')"
|
||||||
|
variant="solid"
|
||||||
|
@click="saveChanges"
|
||||||
|
/>
|
||||||
|
<Button :label="__('Reset')" @click="sections.reload" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Resizer
|
||||||
|
class="flex flex-col justify-between border-l"
|
||||||
|
:parent="parentRef"
|
||||||
|
side="right"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="sections.data"
|
||||||
|
class="flex flex-1 flex-col justify-between overflow-hidden"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col overflow-y-auto">
|
||||||
|
<SidebarLayoutBuilder :sections="sections.data" :doctype="doctype" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Resizer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import Resizer from '@/components/Resizer.vue'
|
||||||
|
import SidebarLayoutBuilder from '@/components/Settings/SidebarLayoutBuilder.vue'
|
||||||
|
import { Badge, call, createResource } from 'frappe-ui'
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
|
||||||
|
const parentRef = ref(null)
|
||||||
|
const doctype = ref('CRM Lead')
|
||||||
|
|
||||||
|
const oldSections = ref([])
|
||||||
|
|
||||||
|
const sections = createResource({
|
||||||
|
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
||||||
|
cache: ['sidebar-sections', doctype.value],
|
||||||
|
params: { doctype: doctype.value, type: 'Side Panel' },
|
||||||
|
auto: true,
|
||||||
|
onSuccess(data) {
|
||||||
|
oldSections.value = JSON.parse(JSON.stringify(data))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const dirty = computed(() => {
|
||||||
|
if (!sections.data) return false
|
||||||
|
return JSON.stringify(sections.data) !== JSON.stringify(oldSections.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
function saveChanges() {
|
||||||
|
let _sections = JSON.parse(JSON.stringify(sections.data))
|
||||||
|
_sections.forEach((section) => {
|
||||||
|
if (!section.fields) return
|
||||||
|
section.fields = section.fields.map((field) => field.fieldname || field.name)
|
||||||
|
})
|
||||||
|
loading.value = true
|
||||||
|
call('crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.save_fields_layout', {
|
||||||
|
doctype: doctype.value,
|
||||||
|
type: 'Side Panel',
|
||||||
|
layout: JSON.stringify(_sections),
|
||||||
|
}).then(() => {
|
||||||
|
loading.value = false
|
||||||
|
sections.reload()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(doctype, (val) => sections.fetch({ doctype: val, type: 'Side Panel' }), {
|
||||||
|
immediate: true,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="profile" class="flex w-full items-center justify-between">
|
<div v-if="profile" class="flex w-full items-center justify-between p-12 pt-14">
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<Avatar
|
<Avatar
|
||||||
class="!size-16"
|
class="!size-16"
|
||||||
|
|||||||
@ -40,7 +40,7 @@
|
|||||||
/>
|
/>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 flex-col overflow-y-auto p-8">
|
<div class="flex flex-1 flex-col overflow-y-auto">
|
||||||
<component :is="activeTab.component" v-if="activeTab" />
|
<component :is="activeTab.component" v-if="activeTab" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -54,10 +54,11 @@ import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
|||||||
import ProfileSettings from '@/components/Settings/ProfileSettings.vue'
|
import ProfileSettings from '@/components/Settings/ProfileSettings.vue'
|
||||||
import WhatsAppSettings from '@/components/Settings/WhatsAppSettings.vue'
|
import WhatsAppSettings from '@/components/Settings/WhatsAppSettings.vue'
|
||||||
import TwilioSettings from '@/components/Settings/TwilioSettings.vue'
|
import TwilioSettings from '@/components/Settings/TwilioSettings.vue'
|
||||||
|
import FieldsLayout from '@/components/Settings/FieldsLayout.vue'
|
||||||
import SidebarLink from '@/components/SidebarLink.vue'
|
import SidebarLink from '@/components/SidebarLink.vue'
|
||||||
import { isWhatsappInstalled } from '@/composables/settings'
|
import { isWhatsappInstalled } from '@/composables/settings'
|
||||||
import { Dialog } from 'frappe-ui'
|
import { Dialog, FeatherIcon } from 'frappe-ui'
|
||||||
import { ref, markRaw, computed } from 'vue'
|
import { ref, markRaw, computed, h } from 'vue'
|
||||||
|
|
||||||
const show = defineModel()
|
const show = defineModel()
|
||||||
|
|
||||||
@ -67,6 +68,11 @@ let tabs = [
|
|||||||
icon: ContactsIcon,
|
icon: ContactsIcon,
|
||||||
component: markRaw(ProfileSettings),
|
component: markRaw(ProfileSettings),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Fields Layout',
|
||||||
|
icon: h(FeatherIcon, { name: 'grid' }),
|
||||||
|
component: markRaw(FieldsLayout),
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
let integrations = computed(() => {
|
let integrations = computed(() => {
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex h-full flex-col gap-8">
|
<div class="flex h-full flex-col gap-8">
|
||||||
<h2 class="flex gap-2 text-xl font-semibold leading-none">
|
<h2 class="flex gap-2 text-xl font-semibold leading-none h-5">
|
||||||
<div>{{ __(doctype) }}</div>
|
<div>{{ __(doctype) }}</div>
|
||||||
<Badge
|
<Badge
|
||||||
|
v-if="data.isDirty"
|
||||||
:label="__('Not Saved')"
|
:label="__('Not Saved')"
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
theme="orange"
|
theme="orange"
|
||||||
v-if="data.isDirty"
|
|
||||||
/>
|
/>
|
||||||
</h2>
|
</h2>
|
||||||
<div v-if="!data.get.loading" class="flex-1 overflow-y-auto">
|
<div v-if="!data.get.loading" class="flex-1 overflow-y-auto">
|
||||||
@ -17,7 +17,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="flex flex-1 items-center justify-center">
|
<div v-else class="flex flex-1 items-center justify-center">
|
||||||
<Spinner />
|
<Spinner class="size-8" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row-reverse">
|
<div class="flex flex-row-reverse">
|
||||||
<Button
|
<Button
|
||||||
@ -31,7 +31,12 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import Fields from '@/components/Fields.vue'
|
import Fields from '@/components/Fields.vue'
|
||||||
import { createDocumentResource, createResource, Spinner, Badge } from 'frappe-ui'
|
import {
|
||||||
|
createDocumentResource,
|
||||||
|
createResource,
|
||||||
|
Spinner,
|
||||||
|
Badge,
|
||||||
|
} from 'frappe-ui'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|||||||
166
frontend/src/components/Settings/SidebarLayoutBuilder.vue
Normal file
166
frontend/src/components/Settings/SidebarLayoutBuilder.vue
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Draggable :list="sections" item-key="label" class="flex flex-col">
|
||||||
|
<template #item="{ element: section }">
|
||||||
|
<div class="border-b">
|
||||||
|
<div class="flex items-center justify-between p-2">
|
||||||
|
<div
|
||||||
|
class="flex h-7 max-w-fit cursor-pointer items-center gap-2 pl-2 pr-3 text-base font-semibold leading-5"
|
||||||
|
@click="section.opened = !section.opened"
|
||||||
|
>
|
||||||
|
<FeatherIcon
|
||||||
|
name="chevron-right"
|
||||||
|
class="h-4 text-gray-900 transition-all duration-300 ease-in-out"
|
||||||
|
:class="{ 'rotate-90': section.opened }"
|
||||||
|
/>
|
||||||
|
<div v-if="!section.editingLabel">
|
||||||
|
{{ __(section.label) || __('Untitled') }}
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<Input
|
||||||
|
v-model="section.label"
|
||||||
|
@keydown.enter="section.editingLabel = false"
|
||||||
|
@blur="section.editingLabel = false"
|
||||||
|
@click.stop
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
:icon="section.editingLabel ? 'check' : 'edit'"
|
||||||
|
variant="ghost"
|
||||||
|
@click="section.editingLabel = !section.editingLabel"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
v-if="section.editable !== false"
|
||||||
|
icon="x"
|
||||||
|
variant="ghost"
|
||||||
|
@click="sections.splice(sections.indexOf(section), 1)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-show="section.opened" class="p-4 pt-0 pb-2">
|
||||||
|
<Draggable
|
||||||
|
:list="section.fields"
|
||||||
|
item-key="label"
|
||||||
|
class="flex flex-col gap-1"
|
||||||
|
handle=".cursor-grab"
|
||||||
|
>
|
||||||
|
<template #item="{ element: field }">
|
||||||
|
<div
|
||||||
|
class="px-1.5 py-1 border rounded text-base text-gray-800 flex items-center justify-between gap-2"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<DragVerticalIcon class="h-3.5 cursor-grab" />
|
||||||
|
<div>{{ field.label }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
icon="x"
|
||||||
|
@click="
|
||||||
|
section.fields.splice(section.fields.indexOf(field), 1)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Draggable>
|
||||||
|
<Autocomplete
|
||||||
|
v-if="fields.data && section.editable !== false"
|
||||||
|
value=""
|
||||||
|
:options="fields.data"
|
||||||
|
@change="(e) => addField(section, e)"
|
||||||
|
>
|
||||||
|
<template #target="{ togglePopover }">
|
||||||
|
<Button
|
||||||
|
class="w-full mt-2"
|
||||||
|
variant="outline"
|
||||||
|
@click="togglePopover()"
|
||||||
|
:label="__('Add Field')"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<FeatherIcon name="plus" class="h-4" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
<template #item-label="{ option }">
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<div>{{ option.label }}</div>
|
||||||
|
<div class="text-gray-500 text-sm">
|
||||||
|
{{ `${option.fieldname} - ${option.fieldtype}` }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Autocomplete>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="flex justify-center items-center border rounded border-dashed p-3"
|
||||||
|
>
|
||||||
|
<div class="text-sm text-gray-500">
|
||||||
|
{{ __('This section is not editable') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Draggable>
|
||||||
|
<div class="p-2">
|
||||||
|
<Button
|
||||||
|
class="w-full"
|
||||||
|
variant="outline"
|
||||||
|
:label="__('Add Section')"
|
||||||
|
@click="
|
||||||
|
sections.push({ label: 'New Section', opened: true, fields: [] })
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import Autocomplete from '@/components/frappe-ui/Autocomplete.vue'
|
||||||
|
import DragVerticalIcon from '@/components/Icons/DragVerticalIcon.vue'
|
||||||
|
import Draggable from 'vuedraggable'
|
||||||
|
import { Input, createResource } from 'frappe-ui'
|
||||||
|
import { computed, watch } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
sections: Object,
|
||||||
|
doctype: String,
|
||||||
|
})
|
||||||
|
|
||||||
|
const restrictedFieldTypes = [
|
||||||
|
'Table',
|
||||||
|
'Geolocation',
|
||||||
|
'Attach',
|
||||||
|
'Attach Image',
|
||||||
|
'HTML',
|
||||||
|
'Signature',
|
||||||
|
]
|
||||||
|
|
||||||
|
const params = computed(() => {
|
||||||
|
return {
|
||||||
|
doctype: props.doctype,
|
||||||
|
restricted_fieldtypes: restrictedFieldTypes,
|
||||||
|
as_array: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const fields = createResource({
|
||||||
|
url: 'crm.api.doc.get_fields_meta',
|
||||||
|
params: params.value,
|
||||||
|
cache: ['fieldsMeta', props.doctype],
|
||||||
|
auto: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
function addField(section, field) {
|
||||||
|
if (!field) return
|
||||||
|
section.fields.push(field)
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.doctype,
|
||||||
|
() => fields.fetch(params.value),
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
</script>
|
||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<SettingsPage doctype="Twilio Settings" />
|
<SettingsPage doctype="Twilio Settings" class="p-8" />
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import SettingsPage from '@/components/Settings/SettingsPage.vue'
|
import SettingsPage from '@/components/Settings/SettingsPage.vue'
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<SettingsPage doctype="WhatsApp Settings" />
|
<SettingsPage doctype="WhatsApp Settings" class="p-8" />
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import SettingsPage from '@/components/Settings/SettingsPage.vue'
|
import SettingsPage from '@/components/Settings/SettingsPage.vue'
|
||||||
|
|||||||
@ -108,15 +108,15 @@
|
|||||||
@updateField="updateField"
|
@updateField="updateField"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="detailSections.length"
|
v-if="fieldsLayout.data"
|
||||||
class="flex flex-1 flex-col justify-between overflow-hidden"
|
class="flex flex-1 flex-col justify-between overflow-hidden"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col overflow-y-auto">
|
<div class="flex flex-col overflow-y-auto">
|
||||||
<div
|
<div
|
||||||
v-for="(section, i) in detailSections"
|
v-for="(section, i) in fieldsLayout.data"
|
||||||
:key="section.label"
|
:key="section.label"
|
||||||
class="flex flex-col p-3"
|
class="flex flex-col p-3"
|
||||||
:class="{ 'border-b': i !== detailSections.length - 1 }"
|
:class="{ 'border-b': i !== fieldsLayout.data.length - 1 }"
|
||||||
>
|
>
|
||||||
<Section :is-opened="section.opened" :label="section.label">
|
<Section :is-opened="section.opened" :label="section.label">
|
||||||
<template #actions>
|
<template #actions>
|
||||||
@ -414,7 +414,7 @@ function updateDeal(fieldname, value, callback) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function validateRequired(fieldname, value) {
|
function validateRequired(fieldname, value) {
|
||||||
let meta = deal.data.all_fields || {}
|
let meta = deal.data.fields_meta || {}
|
||||||
if (meta[fieldname]?.reqd && !value) {
|
if (meta[fieldname]?.reqd && !value) {
|
||||||
createToast({
|
createToast({
|
||||||
title: __('Error Updating Deal'),
|
title: __('Error Updating Deal'),
|
||||||
@ -480,44 +480,31 @@ const tabs = computed(() => {
|
|||||||
return tabOptions.filter((tab) => (tab.condition ? tab.condition() : true))
|
return tabOptions.filter((tab) => (tab.condition ? tab.condition() : true))
|
||||||
})
|
})
|
||||||
|
|
||||||
const detailSections = computed(() => {
|
const fieldsLayout = createResource({
|
||||||
let data = deal.data
|
url: 'crm.api.doc.get_sidebar_fields',
|
||||||
if (!data) return []
|
cache: ['fieldsLayout', props.dealId],
|
||||||
return getParsedFields(data.doctype_fields, deal_contacts.data)
|
params: { doctype: 'CRM Deal', name: props.dealId },
|
||||||
|
auto: true,
|
||||||
|
transform: (data) => getParsedFields(data),
|
||||||
})
|
})
|
||||||
|
|
||||||
function getParsedFields(sections, contacts) {
|
function getParsedFields(sections, contacts) {
|
||||||
sections.forEach((section) => {
|
sections.forEach((section) => {
|
||||||
if (section.name == 'contacts_tab') {
|
if (section.name == 'contacts_section') return
|
||||||
delete section.fields
|
section.fields.forEach((field) => {
|
||||||
section.contacts =
|
if (field.name == 'organization') {
|
||||||
contacts?.map((contact) => {
|
field.create = (value, close) => {
|
||||||
return {
|
_organization.value.organization_name = value
|
||||||
name: contact.name,
|
showOrganizationModal.value = true
|
||||||
full_name: contact.full_name,
|
close()
|
||||||
email: contact.email,
|
|
||||||
mobile_no: contact.mobile_no,
|
|
||||||
image: contact.image,
|
|
||||||
is_primary: contact.is_primary,
|
|
||||||
opened: false,
|
|
||||||
}
|
|
||||||
}) || []
|
|
||||||
} else {
|
|
||||||
section.fields.forEach((field) => {
|
|
||||||
if (field.name == 'organization') {
|
|
||||||
field.create = (value, close) => {
|
|
||||||
_organization.value.organization_name = value
|
|
||||||
showOrganizationModal.value = true
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
field.link = (org) =>
|
|
||||||
router.push({
|
|
||||||
name: 'Organization',
|
|
||||||
params: { organizationId: org },
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
field.link = (org) =>
|
||||||
}
|
router.push({
|
||||||
|
name: 'Organization',
|
||||||
|
params: { organizationId: org },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
return sections
|
return sections
|
||||||
}
|
}
|
||||||
@ -595,6 +582,23 @@ const deal_contacts = createResource({
|
|||||||
params: { name: props.dealId },
|
params: { name: props.dealId },
|
||||||
cache: ['deal_contacts', props.dealId],
|
cache: ['deal_contacts', props.dealId],
|
||||||
auto: true,
|
auto: true,
|
||||||
|
onSuccess: (data) => {
|
||||||
|
let contactSection = fieldsLayout.data.find(
|
||||||
|
(section) => section.name == 'contacts_section',
|
||||||
|
)
|
||||||
|
if (!contactSection) return
|
||||||
|
contactSection.contacts = data.map((contact) => {
|
||||||
|
return {
|
||||||
|
name: contact.name,
|
||||||
|
full_name: contact.full_name,
|
||||||
|
email: contact.email,
|
||||||
|
mobile_no: contact.mobile_no,
|
||||||
|
image: contact.image,
|
||||||
|
is_primary: contact.is_primary,
|
||||||
|
opened: false,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
function triggerCall() {
|
function triggerCall() {
|
||||||
|
|||||||
@ -162,15 +162,15 @@
|
|||||||
@updateField="updateField"
|
@updateField="updateField"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="detailSections.length"
|
v-if="fieldsLayout.data"
|
||||||
class="flex flex-1 flex-col justify-between overflow-hidden"
|
class="flex flex-1 flex-col justify-between overflow-hidden"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col overflow-y-auto">
|
<div class="flex flex-col overflow-y-auto">
|
||||||
<div
|
<div
|
||||||
v-for="(section, i) in detailSections"
|
v-for="(section, i) in fieldsLayout.data"
|
||||||
:key="section.label"
|
:key="section.label"
|
||||||
class="flex flex-col p-3"
|
class="flex flex-col p-3"
|
||||||
:class="{ 'border-b': i !== detailSections.length - 1 }"
|
:class="{ 'border-b': i !== fieldsLayout.data.length - 1 }"
|
||||||
>
|
>
|
||||||
<Section :is-opened="section.opened" :label="section.label">
|
<Section :is-opened="section.opened" :label="section.label">
|
||||||
<SectionFields
|
<SectionFields
|
||||||
@ -384,7 +384,7 @@ function updateLead(fieldname, value, callback) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function validateRequired(fieldname, value) {
|
function validateRequired(fieldname, value) {
|
||||||
let meta = lead.data.all_fields || {}
|
let meta = lead.data.fields_meta || {}
|
||||||
if (meta[fieldname]?.reqd && !value) {
|
if (meta[fieldname]?.reqd && !value) {
|
||||||
createToast({
|
createToast({
|
||||||
title: __('Error Updating Lead'),
|
title: __('Error Updating Lead'),
|
||||||
@ -469,10 +469,11 @@ function validateFile(file) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const detailSections = computed(() => {
|
const fieldsLayout = createResource({
|
||||||
let data = lead.data
|
url: 'crm.api.doc.get_sidebar_fields',
|
||||||
if (!data) return []
|
cache: ['fieldsLayout', props.leadId],
|
||||||
return data.doctype_fields
|
params: { doctype: 'CRM Lead', name: props.leadId },
|
||||||
|
auto: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
function updateField(name, value, callback) {
|
function updateField(name, value, callback) {
|
||||||
|
|||||||
@ -369,7 +369,7 @@ function updateDeal(fieldname, value, callback) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function validateRequired(fieldname, value) {
|
function validateRequired(fieldname, value) {
|
||||||
let meta = deal.data.all_fields || {}
|
let meta = deal.data.fields_meta || {}
|
||||||
if (meta[fieldname]?.reqd && !value) {
|
if (meta[fieldname]?.reqd && !value) {
|
||||||
createToast({
|
createToast({
|
||||||
title: __('Error Updating Deal'),
|
title: __('Error Updating Deal'),
|
||||||
|
|||||||
@ -283,7 +283,7 @@ function updateLead(fieldname, value, callback) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function validateRequired(fieldname, value) {
|
function validateRequired(fieldname, value) {
|
||||||
let meta = lead.data.all_fields || {}
|
let meta = lead.data.fields_meta || {}
|
||||||
if (meta[fieldname]?.reqd && !value) {
|
if (meta[fieldname]?.reqd && !value) {
|
||||||
createToast({
|
createToast({
|
||||||
title: __('Error Updating Lead'),
|
title: __('Error Updating Lead'),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user