Merge pull request #485 from shariquerik/child-table-support
This commit is contained in:
commit
2be808b511
@ -1,10 +1,11 @@
|
|||||||
import frappe
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import get_controller
|
|
||||||
from frappe.model import no_value_fields
|
from frappe.model import no_value_fields
|
||||||
from pypika import Criterion
|
from frappe.model.document import get_controller
|
||||||
from frappe.utils import make_filter_tuple
|
from frappe.utils import make_filter_tuple
|
||||||
|
from pypika import Criterion
|
||||||
|
|
||||||
from crm.api.views import get_views
|
from crm.api.views import get_views
|
||||||
from crm.fcrm.doctype.crm_form_script.crm_form_script import get_form_script
|
from crm.fcrm.doctype.crm_form_script.crm_form_script import get_form_script
|
||||||
@ -557,6 +558,9 @@ def get_fields_meta(doctype, restricted_fieldtypes=None, as_array=False):
|
|||||||
fields_meta = {}
|
fields_meta = {}
|
||||||
for field in fields:
|
for field in fields:
|
||||||
fields_meta[field.get("fieldname")] = field
|
fields_meta[field.get("fieldname")] = field
|
||||||
|
if field.get("fieldtype") == "Table":
|
||||||
|
_fields = frappe.get_meta(field.get("options")).fields
|
||||||
|
fields_meta[field.get("fieldname")] = {"df": field, "fields": _fields}
|
||||||
|
|
||||||
return fields_meta
|
return fields_meta
|
||||||
|
|
||||||
@ -672,7 +676,7 @@ def get_assigned_users(doctype, name, default_assigned_to=None):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_fields(doctype: str, allow_all_fieldtypes: bool = False):
|
def get_fields(doctype: str, allow_all_fieldtypes: bool = False):
|
||||||
not_allowed_fieldtypes = list(frappe.model.no_value_fields) + ["Read Only"]
|
not_allowed_fieldtypes = [*list(frappe.model.no_value_fields), "Read Only"]
|
||||||
if allow_all_fieldtypes:
|
if allow_all_fieldtypes:
|
||||||
not_allowed_fieldtypes = []
|
not_allowed_fieldtypes = []
|
||||||
fields = frappe.get_meta(doctype).fields
|
fields = frappe.get_meta(doctype).fields
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
import json
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
@ -8,7 +7,9 @@ from frappe.desk.form.assign_to import add as assign
|
|||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
|
||||||
from crm.fcrm.doctype.crm_service_level_agreement.utils import get_sla
|
from crm.fcrm.doctype.crm_service_level_agreement.utils import get_sla
|
||||||
from crm.fcrm.doctype.crm_status_change_log.crm_status_change_log import add_status_change_log
|
from crm.fcrm.doctype.crm_status_change_log.crm_status_change_log import (
|
||||||
|
add_status_change_log,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CRMDeal(Document):
|
class CRMDeal(Document):
|
||||||
@ -94,19 +95,26 @@ class CRMDeal(Document):
|
|||||||
shared_with = [d.user for d in docshares] + [agent]
|
shared_with = [d.user for d in docshares] + [agent]
|
||||||
|
|
||||||
for user in shared_with:
|
for user in shared_with:
|
||||||
if user == agent and not frappe.db.exists("DocShare", {"user": agent, "share_name": self.name, "share_doctype": self.doctype}):
|
if user == agent and not frappe.db.exists(
|
||||||
|
"DocShare",
|
||||||
|
{"user": agent, "share_name": self.name, "share_doctype": self.doctype},
|
||||||
|
):
|
||||||
frappe.share.add_docshare(
|
frappe.share.add_docshare(
|
||||||
self.doctype, self.name, agent, write=1, flags={"ignore_share_permission": True}
|
self.doctype,
|
||||||
|
self.name,
|
||||||
|
agent,
|
||||||
|
write=1,
|
||||||
|
flags={"ignore_share_permission": True},
|
||||||
)
|
)
|
||||||
elif user != agent:
|
elif user != agent:
|
||||||
frappe.share.remove(self.doctype, self.name, user)
|
frappe.share.remove(self.doctype, self.name, user)
|
||||||
|
|
||||||
|
|
||||||
def set_sla(self):
|
def set_sla(self):
|
||||||
"""
|
"""
|
||||||
Find an SLA to apply to the deal.
|
Find an SLA to apply to the deal.
|
||||||
"""
|
"""
|
||||||
if self.sla: return
|
if self.sla:
|
||||||
|
return
|
||||||
|
|
||||||
sla = get_sla(self)
|
sla = get_sla(self)
|
||||||
if not sla:
|
if not sla:
|
||||||
@ -129,48 +137,48 @@ class CRMDeal(Document):
|
|||||||
def default_list_data():
|
def default_list_data():
|
||||||
columns = [
|
columns = [
|
||||||
{
|
{
|
||||||
'label': 'Organization',
|
"label": "Organization",
|
||||||
'type': 'Link',
|
"type": "Link",
|
||||||
'key': 'organization',
|
"key": "organization",
|
||||||
'options': 'CRM Organization',
|
"options": "CRM Organization",
|
||||||
'width': '11rem',
|
"width": "11rem",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'label': 'Amount',
|
"label": "Annual Revenue",
|
||||||
'type': 'Currency',
|
"type": "Currency",
|
||||||
'key': 'annual_revenue',
|
"key": "annual_revenue",
|
||||||
'align': 'right',
|
"align": "right",
|
||||||
'width': '9rem',
|
"width": "9rem",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'label': 'Status',
|
"label": "Status",
|
||||||
'type': 'Select',
|
"type": "Select",
|
||||||
'key': 'status',
|
"key": "status",
|
||||||
'width': '10rem',
|
"width": "10rem",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'label': 'Email',
|
"label": "Email",
|
||||||
'type': 'Data',
|
"type": "Data",
|
||||||
'key': 'email',
|
"key": "email",
|
||||||
'width': '12rem',
|
"width": "12rem",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'label': 'Mobile No',
|
"label": "Mobile No",
|
||||||
'type': 'Data',
|
"type": "Data",
|
||||||
'key': 'mobile_no',
|
"key": "mobile_no",
|
||||||
'width': '11rem',
|
"width": "11rem",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'label': 'Assigned To',
|
"label": "Assigned To",
|
||||||
'type': 'Text',
|
"type": "Text",
|
||||||
'key': '_assign',
|
"key": "_assign",
|
||||||
'width': '10rem',
|
"width": "10rem",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'label': 'Last Modified',
|
"label": "Last Modified",
|
||||||
'type': 'Datetime',
|
"type": "Datetime",
|
||||||
'key': 'modified',
|
"key": "modified",
|
||||||
'width': '8rem',
|
"width": "8rem",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
rows = [
|
rows = [
|
||||||
@ -189,16 +197,17 @@ class CRMDeal(Document):
|
|||||||
"modified",
|
"modified",
|
||||||
"_assign",
|
"_assign",
|
||||||
]
|
]
|
||||||
return {'columns': columns, 'rows': rows}
|
return {"columns": columns, "rows": rows}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def default_kanban_settings():
|
def default_kanban_settings():
|
||||||
return {
|
return {
|
||||||
"column_field": "status",
|
"column_field": "status",
|
||||||
"title_field": "organization",
|
"title_field": "organization",
|
||||||
"kanban_fields": '["annual_revenue", "email", "mobile_no", "_assign", "modified"]'
|
"kanban_fields": '["annual_revenue", "email", "mobile_no", "_assign", "modified"]',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def add_contact(deal, contact):
|
def add_contact(deal, contact):
|
||||||
if not frappe.has_permission("CRM Deal", "write", deal):
|
if not frappe.has_permission("CRM Deal", "write", deal):
|
||||||
@ -209,6 +218,7 @@ def add_contact(deal, contact):
|
|||||||
deal.save()
|
deal.save()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def remove_contact(deal, contact):
|
def remove_contact(deal, contact):
|
||||||
if not frappe.has_permission("CRM Deal", "write", deal):
|
if not frappe.has_permission("CRM Deal", "write", deal):
|
||||||
@ -219,6 +229,7 @@ def remove_contact(deal, contact):
|
|||||||
deal.save()
|
deal.save()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def set_primary_contact(deal, contact):
|
def set_primary_contact(deal, contact):
|
||||||
if not frappe.has_permission("CRM Deal", "write", deal):
|
if not frappe.has_permission("CRM Deal", "write", deal):
|
||||||
@ -229,11 +240,14 @@ def set_primary_contact(deal, contact):
|
|||||||
deal.save()
|
deal.save()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def create_organization(doc):
|
def create_organization(doc):
|
||||||
if not doc.get("organization_name"):
|
if not doc.get("organization_name"):
|
||||||
return
|
return
|
||||||
|
|
||||||
existing_organization = frappe.db.exists("CRM Organization", {"organization_name": doc.get("organization_name")})
|
existing_organization = frappe.db.exists(
|
||||||
|
"CRM Organization", {"organization_name": doc.get("organization_name")}
|
||||||
|
)
|
||||||
if existing_organization:
|
if existing_organization:
|
||||||
return existing_organization
|
return existing_organization
|
||||||
|
|
||||||
@ -250,6 +264,7 @@ def create_organization(doc):
|
|||||||
organization.insert(ignore_permissions=True)
|
organization.insert(ignore_permissions=True)
|
||||||
return organization.name
|
return organization.name
|
||||||
|
|
||||||
|
|
||||||
def contact_exists(doc):
|
def contact_exists(doc):
|
||||||
email_exist = frappe.db.exists("Contact Email", {"email_id": doc.get("email")})
|
email_exist = frappe.db.exists("Contact Email", {"email_id": doc.get("email")})
|
||||||
mobile_exist = frappe.db.exists("Contact Phone", {"phone": doc.get("mobile_no")})
|
mobile_exist = frappe.db.exists("Contact Phone", {"phone": doc.get("mobile_no")})
|
||||||
@ -262,6 +277,7 @@ def contact_exists(doc):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def create_contact(doc):
|
def create_contact(doc):
|
||||||
existing_contact = contact_exists(doc)
|
existing_contact = contact_exists(doc)
|
||||||
if existing_contact:
|
if existing_contact:
|
||||||
@ -288,18 +304,23 @@ def create_contact(doc):
|
|||||||
|
|
||||||
return contact.name
|
return contact.name
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def create_deal(args):
|
def create_deal(args):
|
||||||
deal = frappe.new_doc("CRM Deal")
|
deal = frappe.new_doc("CRM Deal")
|
||||||
|
|
||||||
contact = args.get("contact")
|
contact = args.get("contact")
|
||||||
if not contact and (args.get("first_name") or args.get("last_name") or args.get("email") or args.get("mobile_no")):
|
if not contact and (
|
||||||
|
args.get("first_name") or args.get("last_name") or args.get("email") or args.get("mobile_no")
|
||||||
|
):
|
||||||
contact = create_contact(args)
|
contact = create_contact(args)
|
||||||
|
|
||||||
deal.update({
|
deal.update(
|
||||||
"organization": args.get("organization") or create_organization(args),
|
{
|
||||||
"contacts": [{"contact": contact, "is_primary": 1}] if contact else [],
|
"organization": args.get("organization") or create_organization(args),
|
||||||
})
|
"contacts": [{"contact": contact, "is_primary": 1}] if contact else [],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
args.pop("organization", None)
|
args.pop("organization", None)
|
||||||
|
|
||||||
|
|||||||
@ -27,7 +27,7 @@
|
|||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Type",
|
"label": "Type",
|
||||||
"options": "Quick Entry\nSide Panel\nData Fields"
|
"options": "Quick Entry\nSide Panel\nData Fields\nGrid Row"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "section_break_ttpm",
|
"fieldname": "section_break_ttpm",
|
||||||
@ -46,7 +46,7 @@
|
|||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-12-05 13:29:37.021412",
|
"modified": "2024-12-29 12:58:54.280569",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "FCRM",
|
"module": "FCRM",
|
||||||
"name": "CRM Fields Layout",
|
"name": "CRM Fields Layout",
|
||||||
|
|||||||
@ -1,15 +1,16 @@
|
|||||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
import json
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.desk.form.assign_to import add as assign
|
from frappe.desk.form.assign_to import add as assign
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
|
||||||
from frappe.utils import has_gravatar, validate_email_address
|
from frappe.utils import has_gravatar, validate_email_address
|
||||||
|
|
||||||
from crm.fcrm.doctype.crm_service_level_agreement.utils import get_sla
|
from crm.fcrm.doctype.crm_service_level_agreement.utils import get_sla
|
||||||
from crm.fcrm.doctype.crm_status_change_log.crm_status_change_log import add_status_change_log
|
from crm.fcrm.doctype.crm_status_change_log.crm_status_change_log import (
|
||||||
|
add_status_change_log,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CRMLead(Document):
|
class CRMLead(Document):
|
||||||
@ -37,7 +38,15 @@ class CRMLead(Document):
|
|||||||
def set_full_name(self):
|
def set_full_name(self):
|
||||||
if self.first_name:
|
if self.first_name:
|
||||||
self.lead_name = " ".join(
|
self.lead_name = " ".join(
|
||||||
filter(None, [self.salutation, self.first_name, self.middle_name, self.last_name])
|
filter(
|
||||||
|
None,
|
||||||
|
[
|
||||||
|
self.salutation,
|
||||||
|
self.first_name,
|
||||||
|
self.middle_name,
|
||||||
|
self.last_name,
|
||||||
|
],
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_lead_name(self):
|
def set_lead_name(self):
|
||||||
@ -92,9 +101,16 @@ class CRMLead(Document):
|
|||||||
shared_with = [d.user for d in docshares] + [agent]
|
shared_with = [d.user for d in docshares] + [agent]
|
||||||
|
|
||||||
for user in shared_with:
|
for user in shared_with:
|
||||||
if user == agent and not frappe.db.exists("DocShare", {"user": agent, "share_name": self.name, "share_doctype": self.doctype}):
|
if user == agent and not frappe.db.exists(
|
||||||
|
"DocShare",
|
||||||
|
{"user": agent, "share_name": self.name, "share_doctype": self.doctype},
|
||||||
|
):
|
||||||
frappe.share.add_docshare(
|
frappe.share.add_docshare(
|
||||||
self.doctype, self.name, agent, write=1, flags={"ignore_share_permission": True}
|
self.doctype,
|
||||||
|
self.name,
|
||||||
|
agent,
|
||||||
|
write=1,
|
||||||
|
flags={"ignore_share_permission": True},
|
||||||
)
|
)
|
||||||
elif user != agent:
|
elif user != agent:
|
||||||
frappe.share.remove(self.doctype, self.name, user)
|
frappe.share.remove(self.doctype, self.name, user)
|
||||||
@ -188,8 +204,36 @@ class CRMLead(Document):
|
|||||||
"lead_owner": "deal_owner",
|
"lead_owner": "deal_owner",
|
||||||
}
|
}
|
||||||
|
|
||||||
restricted_fieldtypes = ["Tab Break", "Section Break", "Column Break", "HTML", "Button", "Attach", "Table"]
|
restricted_fieldtypes = [
|
||||||
restricted_map_fields = ["name", "naming_series", "creation", "owner", "modified", "modified_by", "idx", "docstatus", "status", "email", "mobile_no", "phone", "sla", "sla_status", "response_by", "first_response_time", "first_responded_on", "communication_status", "sla_creation"]
|
"Tab Break",
|
||||||
|
"Section Break",
|
||||||
|
"Column Break",
|
||||||
|
"HTML",
|
||||||
|
"Button",
|
||||||
|
"Attach",
|
||||||
|
"Table",
|
||||||
|
]
|
||||||
|
restricted_map_fields = [
|
||||||
|
"name",
|
||||||
|
"naming_series",
|
||||||
|
"creation",
|
||||||
|
"owner",
|
||||||
|
"modified",
|
||||||
|
"modified_by",
|
||||||
|
"idx",
|
||||||
|
"docstatus",
|
||||||
|
"status",
|
||||||
|
"email",
|
||||||
|
"mobile_no",
|
||||||
|
"phone",
|
||||||
|
"sla",
|
||||||
|
"sla_status",
|
||||||
|
"response_by",
|
||||||
|
"first_response_time",
|
||||||
|
"first_responded_on",
|
||||||
|
"communication_status",
|
||||||
|
"sla_creation",
|
||||||
|
]
|
||||||
|
|
||||||
for field in self.meta.fields:
|
for field in self.meta.fields:
|
||||||
if field.fieldtype in restricted_fieldtypes:
|
if field.fieldtype in restricted_fieldtypes:
|
||||||
@ -222,7 +266,7 @@ class CRMLead(Document):
|
|||||||
"sla_status": self.sla_status,
|
"sla_status": self.sla_status,
|
||||||
"communication_status": self.communication_status,
|
"communication_status": self.communication_status,
|
||||||
"first_response_time": self.first_response_time,
|
"first_response_time": self.first_response_time,
|
||||||
"first_responded_on": self.first_responded_on
|
"first_responded_on": self.first_responded_on,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -233,7 +277,8 @@ class CRMLead(Document):
|
|||||||
"""
|
"""
|
||||||
Find an SLA to apply to the lead.
|
Find an SLA to apply to the lead.
|
||||||
"""
|
"""
|
||||||
if self.sla: return
|
if self.sla:
|
||||||
|
return
|
||||||
|
|
||||||
sla = get_sla(self)
|
sla = get_sla(self)
|
||||||
if not sla:
|
if not sla:
|
||||||
@ -263,47 +308,47 @@ class CRMLead(Document):
|
|||||||
def default_list_data():
|
def default_list_data():
|
||||||
columns = [
|
columns = [
|
||||||
{
|
{
|
||||||
'label': 'Name',
|
"label": "Name",
|
||||||
'type': 'Data',
|
"type": "Data",
|
||||||
'key': 'lead_name',
|
"key": "lead_name",
|
||||||
'width': '12rem',
|
"width": "12rem",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'label': 'Organization',
|
"label": "Organization",
|
||||||
'type': 'Link',
|
"type": "Link",
|
||||||
'key': 'organization',
|
"key": "organization",
|
||||||
'options': 'CRM Organization',
|
"options": "CRM Organization",
|
||||||
'width': '10rem',
|
"width": "10rem",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'label': 'Status',
|
"label": "Status",
|
||||||
'type': 'Select',
|
"type": "Select",
|
||||||
'key': 'status',
|
"key": "status",
|
||||||
'width': '8rem',
|
"width": "8rem",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'label': 'Email',
|
"label": "Email",
|
||||||
'type': 'Data',
|
"type": "Data",
|
||||||
'key': 'email',
|
"key": "email",
|
||||||
'width': '12rem',
|
"width": "12rem",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'label': 'Mobile No',
|
"label": "Mobile No",
|
||||||
'type': 'Data',
|
"type": "Data",
|
||||||
'key': 'mobile_no',
|
"key": "mobile_no",
|
||||||
'width': '11rem',
|
"width": "11rem",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'label': 'Assigned To',
|
"label": "Assigned To",
|
||||||
'type': 'Text',
|
"type": "Text",
|
||||||
'key': '_assign',
|
"key": "_assign",
|
||||||
'width': '10rem',
|
"width": "10rem",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'label': 'Last Modified',
|
"label": "Last Modified",
|
||||||
'type': 'Datetime',
|
"type": "Datetime",
|
||||||
'key': 'modified',
|
"key": "modified",
|
||||||
'width': '8rem',
|
"width": "8rem",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
rows = [
|
rows = [
|
||||||
@ -323,20 +368,22 @@ class CRMLead(Document):
|
|||||||
"_assign",
|
"_assign",
|
||||||
"image",
|
"image",
|
||||||
]
|
]
|
||||||
return {'columns': columns, 'rows': rows}
|
return {"columns": columns, "rows": rows}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def default_kanban_settings():
|
def default_kanban_settings():
|
||||||
return {
|
return {
|
||||||
"column_field": "status",
|
"column_field": "status",
|
||||||
"title_field": "lead_name",
|
"title_field": "lead_name",
|
||||||
"kanban_fields": '["organization", "email", "mobile_no", "_assign", "modified"]'
|
"kanban_fields": '["organization", "email", "mobile_no", "_assign", "modified"]',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def convert_to_deal(lead, doc=None):
|
def convert_to_deal(lead, doc=None):
|
||||||
if not (doc and doc.flags.get("ignore_permissions")) and not frappe.has_permission("CRM Lead", "write", lead):
|
if not (doc and doc.flags.get("ignore_permissions")) and not frappe.has_permission(
|
||||||
|
"CRM Lead", "write", lead
|
||||||
|
):
|
||||||
frappe.throw(_("Not allowed to convert Lead to Deal"), frappe.PermissionError)
|
frappe.throw(_("Not allowed to convert Lead to Deal"), frappe.PermissionError)
|
||||||
|
|
||||||
lead = frappe.get_cached_doc("CRM Lead", lead)
|
lead = frappe.get_cached_doc("CRM Lead", lead)
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
Subproject commit 5a4f3c8d4f12efba37b9a83a51a59b53fa758be0
|
Subproject commit 46086c524bc218d989c68ca54cd13a37e693fab9
|
||||||
@ -14,7 +14,7 @@
|
|||||||
"@vueuse/core": "^10.3.0",
|
"@vueuse/core": "^10.3.0",
|
||||||
"@vueuse/integrations": "^10.3.0",
|
"@vueuse/integrations": "^10.3.0",
|
||||||
"feather-icons": "^4.28.0",
|
"feather-icons": "^4.28.0",
|
||||||
"frappe-ui": "^0.1.91",
|
"frappe-ui": "^0.1.94",
|
||||||
"gemoji": "^8.1.0",
|
"gemoji": "^8.1.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mime": "^4.0.1",
|
"mime": "^4.0.1",
|
||||||
|
|||||||
@ -31,7 +31,7 @@
|
|||||||
<LoadingIndicator class="h-6 w-6" />
|
<LoadingIndicator class="h-6 w-6" />
|
||||||
<span>{{ __('Loading...') }}</span>
|
<span>{{ __('Loading...') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else class="pb-8">
|
||||||
<FieldLayout
|
<FieldLayout
|
||||||
v-if="tabs.data"
|
v-if="tabs.data"
|
||||||
:tabs="tabs.data"
|
:tabs="tabs.data"
|
||||||
@ -86,7 +86,7 @@ const data = createDocumentResource({
|
|||||||
createToast({
|
createToast({
|
||||||
title: 'Data Updated',
|
title: 'Data Updated',
|
||||||
icon: 'check',
|
icon: 'check',
|
||||||
iconClasses: 'text-green-600',
|
iconClasses: 'text-ink-green-3',
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
|
|||||||
366
frontend/src/components/Controls/Grid.vue
Normal file
366
frontend/src/components/Controls/Grid.vue
Normal file
@ -0,0 +1,366 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col text-base">
|
||||||
|
<div v-if="label" class="mb-1.5 text-sm text-ink-gray-5">
|
||||||
|
{{ __(label) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="fields?.length"
|
||||||
|
class="rounded border border-outline-gray-modals"
|
||||||
|
>
|
||||||
|
<!-- Header -->
|
||||||
|
<div
|
||||||
|
class="grid-header flex items-center rounded-t-[7px] bg-surface-gray-2 text-ink-gray-5 truncate"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="inline-flex items-center justify-center border-r border-outline-gray-2 h-8 p-2 w-12"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
class="cursor-pointer duration-300"
|
||||||
|
:modelValue="allRowsSelected"
|
||||||
|
@click.stop="toggleSelectAllRows($event.target.checked)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="inline-flex items-center justify-center border-r border-outline-gray-2 py-2 px-1 w-12"
|
||||||
|
>
|
||||||
|
{{ __('No') }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="grid w-full truncate"
|
||||||
|
:style="{ gridTemplateColumns: gridTemplateColumns }"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="field in fields"
|
||||||
|
class="border-r border-outline-gray-2 p-2 truncate"
|
||||||
|
:key="field.name"
|
||||||
|
:title="field.label"
|
||||||
|
>
|
||||||
|
{{ __(field.label) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-12">
|
||||||
|
<Button
|
||||||
|
class="flex w-full items-center justify-center rounded !bg-surface-gray-2 border-0"
|
||||||
|
variant="outline"
|
||||||
|
@click="showGridFieldsEditorModal = true"
|
||||||
|
>
|
||||||
|
<FeatherIcon name="settings" class="h-4 w-4 text-ink-gray-7" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Rows -->
|
||||||
|
<template v-if="rows.length">
|
||||||
|
<Draggable class="w-full" v-model="rows" group="rows" item-key="name">
|
||||||
|
<template #item="{ element: row, index }">
|
||||||
|
<div
|
||||||
|
class="grid-row flex cursor-pointer items-center border-b border-outline-gray-modals bg-surface-modals last:rounded-b last:border-b-0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="inline-flex h-9.5 items-center justify-center border-r border-outline-gray-modals p-2 w-12"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
class="cursor-pointer duration-300"
|
||||||
|
:modelValue="selectedRows.has(row.name)"
|
||||||
|
@click.stop="toggleSelectRow(row)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex h-9.5 items-center justify-center border-r border-outline-gray-modals py-2 px-1 text-sm text-ink-gray-8 w-12"
|
||||||
|
>
|
||||||
|
{{ index + 1 }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="grid w-full h-9.5"
|
||||||
|
:style="{ gridTemplateColumns: gridTemplateColumns }"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="border-r border-outline-gray-modals h-full"
|
||||||
|
v-for="field in fields"
|
||||||
|
:key="field.name"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
v-if="field.type === 'Link'"
|
||||||
|
class="text-sm text-ink-gray-8"
|
||||||
|
v-model="row[field.name]"
|
||||||
|
:doctype="field.options"
|
||||||
|
:filters="field.filters"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-else-if="field.type === 'Check'"
|
||||||
|
class="flex h-full justify-center items-center"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
class="cursor-pointer duration-300"
|
||||||
|
v-model="row[field.name]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<DatePicker
|
||||||
|
v-else-if="field.type === 'Date'"
|
||||||
|
v-model="row[field.name]"
|
||||||
|
icon-left=""
|
||||||
|
variant="outline"
|
||||||
|
:formatter="(date) => getFormat(date, '', true)"
|
||||||
|
input-class="border-none text-sm text-ink-gray-8"
|
||||||
|
/>
|
||||||
|
<DateTimePicker
|
||||||
|
v-else-if="field.type === 'Datetime'"
|
||||||
|
v-model="row[field.name]"
|
||||||
|
icon-left=""
|
||||||
|
variant="outline"
|
||||||
|
:formatter="(date) => getFormat(date, '', true, true)"
|
||||||
|
input-class="border-none text-sm text-ink-gray-8"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-else-if="
|
||||||
|
['Small Text', 'Text', 'Long Text', 'Code'].includes(
|
||||||
|
field.type,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
rows="1"
|
||||||
|
type="textarea"
|
||||||
|
variant="outline"
|
||||||
|
v-model="row[field.name]"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-else-if="['Int'].includes(field.type)"
|
||||||
|
type="number"
|
||||||
|
variant="outline"
|
||||||
|
v-model="row[field.name]"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-else-if="field.type === 'Select'"
|
||||||
|
class="text-sm text-ink-gray-8"
|
||||||
|
type="select"
|
||||||
|
variant="outline"
|
||||||
|
v-model="row[field.name]"
|
||||||
|
:options="field.options"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-else
|
||||||
|
class="text-sm text-ink-gray-8"
|
||||||
|
type="text"
|
||||||
|
variant="outline"
|
||||||
|
v-model="row[field.name]"
|
||||||
|
:options="field.options"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="edit-row w-12">
|
||||||
|
<Button
|
||||||
|
class="flex w-full items-center justify-center rounded border-0"
|
||||||
|
variant="outline"
|
||||||
|
@click="showRowList[index] = true"
|
||||||
|
>
|
||||||
|
<EditIcon class="h-4 w-4 text-ink-gray-7" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<GridRowModal
|
||||||
|
v-if="showRowList[index]"
|
||||||
|
v-model="showRowList[index]"
|
||||||
|
v-model:showGridRowFieldsModal="showGridRowFieldsModal"
|
||||||
|
:index="index"
|
||||||
|
:data="row"
|
||||||
|
:doctype="doctype"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Draggable>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="flex flex-col items-center rounded p-5 text-sm text-ink-gray-5"
|
||||||
|
>
|
||||||
|
{{ __('No Data') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="fields?.length" class="mt-2 flex flex-row gap-2">
|
||||||
|
<Button
|
||||||
|
v-if="showDeleteBtn"
|
||||||
|
:label="__('Delete')"
|
||||||
|
variant="solid"
|
||||||
|
theme="red"
|
||||||
|
@click="deleteRows"
|
||||||
|
/>
|
||||||
|
<Button :label="__('Add Row')" @click="addRow" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<GridRowFieldsModal
|
||||||
|
v-if="showGridRowFieldsModal"
|
||||||
|
v-model="showGridRowFieldsModal"
|
||||||
|
:doctype="doctype"
|
||||||
|
/>
|
||||||
|
<GridFieldsEditorModal
|
||||||
|
v-if="showGridFieldsEditorModal"
|
||||||
|
v-model="showGridFieldsEditorModal"
|
||||||
|
:doctype="doctype"
|
||||||
|
:parentDoctype="parentDoctype"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import GridFieldsEditorModal from '@/components/Controls/GridFieldsEditorModal.vue'
|
||||||
|
import GridRowFieldsModal from '@/components/Controls/GridRowFieldsModal.vue'
|
||||||
|
import GridRowModal from '@/components/Controls/GridRowModal.vue'
|
||||||
|
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||||
|
import Link from '@/components/Controls/Link.vue'
|
||||||
|
import { getRandom, getFormat } from '@/utils'
|
||||||
|
import { getMeta } from '@/stores/meta'
|
||||||
|
import {
|
||||||
|
FeatherIcon,
|
||||||
|
FormControl,
|
||||||
|
Checkbox,
|
||||||
|
DateTimePicker,
|
||||||
|
DatePicker,
|
||||||
|
} from 'frappe-ui'
|
||||||
|
import Draggable from 'vuedraggable'
|
||||||
|
import { ref, reactive, computed } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
doctype: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
parentDoctype: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { getGridSettings, getFields } = getMeta(props.doctype)
|
||||||
|
|
||||||
|
const rows = defineModel()
|
||||||
|
const showRowList = ref(new Array(rows.value.length).fill(false))
|
||||||
|
const selectedRows = reactive(new Set())
|
||||||
|
|
||||||
|
const showGridFieldsEditorModal = ref(false)
|
||||||
|
const showGridRowFieldsModal = ref(false)
|
||||||
|
|
||||||
|
const fields = computed(() => {
|
||||||
|
let gridSettings = getGridSettings(props.parentDoctype)
|
||||||
|
let gridFields = getFields()
|
||||||
|
if (gridSettings.length) {
|
||||||
|
let d = gridSettings.map((gs) =>
|
||||||
|
getFieldObj(gridFields.find((f) => f.fieldname === gs.fieldname)),
|
||||||
|
)
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
return gridFields?.map((f) => getFieldObj(f)) || []
|
||||||
|
})
|
||||||
|
|
||||||
|
function getFieldObj(field) {
|
||||||
|
return {
|
||||||
|
label: field.label,
|
||||||
|
name: field.fieldname,
|
||||||
|
type: field.fieldtype,
|
||||||
|
options: field.options,
|
||||||
|
in_list_view: field.in_list_view,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const gridTemplateColumns = computed(() => {
|
||||||
|
if (!fields.value?.length) return '1fr'
|
||||||
|
// for the checkbox & sr no. columns
|
||||||
|
let gridSettings = getGridSettings(props.parentDoctype)
|
||||||
|
if (gridSettings.length) {
|
||||||
|
return gridSettings.map((gs) => `minmax(0, ${gs.columns || 2}fr)`).join(' ')
|
||||||
|
}
|
||||||
|
return fields.value.map(() => `minmax(0, 2fr)`).join(' ')
|
||||||
|
})
|
||||||
|
|
||||||
|
const allRowsSelected = computed(() => {
|
||||||
|
if (!rows.value.length) return false
|
||||||
|
return rows.value.length === selectedRows.size
|
||||||
|
})
|
||||||
|
|
||||||
|
const showDeleteBtn = computed(() => selectedRows.size > 0)
|
||||||
|
|
||||||
|
const toggleSelectAllRows = (iSelected) => {
|
||||||
|
if (iSelected) {
|
||||||
|
rows.value.forEach((row) => selectedRows.add(row.name))
|
||||||
|
} else {
|
||||||
|
selectedRows.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleSelectRow = (row) => {
|
||||||
|
if (selectedRows.has(row.name)) {
|
||||||
|
selectedRows.delete(row.name)
|
||||||
|
} else {
|
||||||
|
selectedRows.add(row.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addRow = () => {
|
||||||
|
const newRow = {}
|
||||||
|
fields.value?.forEach((field) => {
|
||||||
|
if (field.type === 'Check') newRow[field.name] = false
|
||||||
|
else newRow[field.name] = ''
|
||||||
|
})
|
||||||
|
newRow.name = getRandom(10)
|
||||||
|
showRowList.value.push(false)
|
||||||
|
newRow['__islocal'] = true
|
||||||
|
rows.value.push(newRow)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteRows = () => {
|
||||||
|
rows.value = rows.value.filter((row) => !selectedRows.has(row.name))
|
||||||
|
showRowList.value.pop()
|
||||||
|
selectedRows.clear()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* For Input fields */
|
||||||
|
:deep(.grid-row input:not([type='checkbox'])),
|
||||||
|
:deep(.grid-row textarea) {
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
height: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.grid-row input:focus),
|
||||||
|
:deep(.grid-row input:hover),
|
||||||
|
:deep(.grid-row textarea:focus),
|
||||||
|
:deep(.grid-row textarea:hover) {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.grid-row input:focus-within) :deep(.grid-row textarea:focus-within) {
|
||||||
|
border: 1px solid var(--outline-gray-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* For select field */
|
||||||
|
:deep(.grid-row select) {
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
height: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* For Autocomplete */
|
||||||
|
:deep(.grid-row button) {
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
background-color: var(--surface-white);
|
||||||
|
height: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.grid-row .edit-row button) {
|
||||||
|
border-bottom-right-radius: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.grid-row button:focus) :deep(.grid-row button:hover) {
|
||||||
|
box-shadow: none;
|
||||||
|
background-color: var(--surface-white);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.grid-row button:focus-within) {
|
||||||
|
border: 1px solid var(--outline-gray-2);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
193
frontend/src/components/Controls/GridFieldsEditorModal.vue
Normal file
193
frontend/src/components/Controls/GridFieldsEditorModal.vue
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog v-model="show">
|
||||||
|
<template #body-title>
|
||||||
|
<h3
|
||||||
|
class="flex items-center gap-2 text-2xl font-semibold leading-6 text-ink-gray-9"
|
||||||
|
>
|
||||||
|
<div>{{ __('Edit Grid Fields Layout') }}</div>
|
||||||
|
<Badge
|
||||||
|
v-if="dirty"
|
||||||
|
:label="__('Not Saved')"
|
||||||
|
variant="subtle"
|
||||||
|
theme="orange"
|
||||||
|
/>
|
||||||
|
</h3>
|
||||||
|
</template>
|
||||||
|
<template #body-content>
|
||||||
|
<div class="mt-4">
|
||||||
|
<div class="text-base text-ink-gray-8 mb-2">
|
||||||
|
{{ __('Fields Order') }}
|
||||||
|
</div>
|
||||||
|
<Draggable
|
||||||
|
v-if="oldFields?.length"
|
||||||
|
:list="fields"
|
||||||
|
@end="reorder"
|
||||||
|
group="fields"
|
||||||
|
item-key="fieldname"
|
||||||
|
class="flex flex-col gap-1"
|
||||||
|
>
|
||||||
|
<template #item="{ element: field }">
|
||||||
|
<div
|
||||||
|
class="px-1 py-0.5 bg-surface-gray-2 border border-outline-gray-modals rounded text-base text-ink-gray-8 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 class="flex items-center gap-2">
|
||||||
|
<TextInput
|
||||||
|
variant="outline"
|
||||||
|
type="number"
|
||||||
|
v-model="field.columns"
|
||||||
|
class="w-20"
|
||||||
|
/>
|
||||||
|
<Button variant="ghost" icon="x" @click="removeField(field)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Draggable>
|
||||||
|
<Autocomplete
|
||||||
|
v-if="dropdownFields?.length"
|
||||||
|
value=""
|
||||||
|
:options="dropdownFields"
|
||||||
|
@change="(e) => addField(e)"
|
||||||
|
>
|
||||||
|
<template #target="{ togglePopover }">
|
||||||
|
<Button
|
||||||
|
class="w-full mt-2"
|
||||||
|
@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 text-ink-gray-9">
|
||||||
|
<div>{{ option.label }}</div>
|
||||||
|
<div class="text-ink-gray-4 text-sm">
|
||||||
|
{{ `${option.fieldname} - ${option.fieldtype}` }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Autocomplete>
|
||||||
|
<ErrorMessage class="mt-3" v-if="error" :message="error" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #actions>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<Button
|
||||||
|
v-if="dirty"
|
||||||
|
class="w-full"
|
||||||
|
:label="__('Reset')"
|
||||||
|
@click="reset"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
class="w-full"
|
||||||
|
:label="__('Save')"
|
||||||
|
variant="solid"
|
||||||
|
@click="update"
|
||||||
|
:loading="loading"
|
||||||
|
:disabled="!dirty"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import DragVerticalIcon from '@/components/Icons/DragVerticalIcon.vue'
|
||||||
|
import Autocomplete from '@/components/frappe-ui/Autocomplete.vue'
|
||||||
|
import { getMeta } from '@/stores/meta'
|
||||||
|
import Draggable from 'vuedraggable'
|
||||||
|
import { Dialog, ErrorMessage } from 'frappe-ui'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
doctype: String,
|
||||||
|
parentDoctype: String,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { userSettings, getFields, getGridSettings, saveUserSettings } = getMeta(
|
||||||
|
props.doctype,
|
||||||
|
)
|
||||||
|
|
||||||
|
const show = defineModel()
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref(null)
|
||||||
|
|
||||||
|
const dirty = computed(() => {
|
||||||
|
return JSON.stringify(fields.value) !== JSON.stringify(oldFields.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const oldFields = computed(() => {
|
||||||
|
let _fields = getFields()
|
||||||
|
let gridSettings = getGridSettings(props.parentDoctype)
|
||||||
|
|
||||||
|
if (gridSettings.length) {
|
||||||
|
return gridSettings.map((field) => {
|
||||||
|
let f = _fields.find((f) => f.fieldname === field.fieldname)
|
||||||
|
if (f) {
|
||||||
|
f.columns = field.columns
|
||||||
|
return fieldObj(f)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return _fields?.filter((field) => field.in_list_view).map((f) => fieldObj(f))
|
||||||
|
})
|
||||||
|
|
||||||
|
const fields = ref(JSON.parse(JSON.stringify(oldFields.value || [])))
|
||||||
|
|
||||||
|
const dropdownFields = computed(() => {
|
||||||
|
return getFields()?.filter(
|
||||||
|
(field) => !fields.value.find((f) => f.fieldname === field.fieldname),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
fields.value = JSON.parse(JSON.stringify(oldFields.value || []))
|
||||||
|
}
|
||||||
|
|
||||||
|
function addField(field) {
|
||||||
|
fields.value.push(fieldObj(field))
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeField(field) {
|
||||||
|
const index = fields.value.findIndex((f) => f.fieldname === field.fieldname)
|
||||||
|
fields.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
let updateFields = fields.value.map((field) => {
|
||||||
|
return {
|
||||||
|
fieldname: field.fieldname,
|
||||||
|
columns: field.columns,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (updateFields.length === 0) {
|
||||||
|
error.value = __('At least one field is required')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
saveUserSettings(props.parentDoctype, 'GridView', updateFields, () => {
|
||||||
|
loading.value = false
|
||||||
|
show.value = false
|
||||||
|
userSettings[props.parentDoctype]['GridView'][props.doctype] = updateFields
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function fieldObj(field) {
|
||||||
|
return {
|
||||||
|
label: field.label,
|
||||||
|
fieldname: field.fieldname,
|
||||||
|
fieldtype: field.fieldtype,
|
||||||
|
options: field.options,
|
||||||
|
in_list_view: field.in_list_view,
|
||||||
|
columns: field.columns || 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
125
frontend/src/components/Controls/GridRowFieldsModal.vue
Normal file
125
frontend/src/components/Controls/GridRowFieldsModal.vue
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog v-model="show" :options="{ size: '3xl' }">
|
||||||
|
<template #body-title>
|
||||||
|
<h3
|
||||||
|
class="flex items-center gap-2 text-2xl font-semibold leading-6 text-ink-gray-9"
|
||||||
|
>
|
||||||
|
<div>{{ __('Edit Grid Row Fields Layout') }}</div>
|
||||||
|
<Badge
|
||||||
|
v-if="dirty"
|
||||||
|
:label="__('Not Saved')"
|
||||||
|
variant="subtle"
|
||||||
|
theme="orange"
|
||||||
|
/>
|
||||||
|
</h3>
|
||||||
|
</template>
|
||||||
|
<template #body-content>
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<div class="flex justify-between gap-2">
|
||||||
|
<Button
|
||||||
|
:label="preview ? __('Hide preview') : __('Show preview')"
|
||||||
|
@click="preview = !preview"
|
||||||
|
/>
|
||||||
|
<div class="flex flex-row-reverse gap-2">
|
||||||
|
<Button
|
||||||
|
:loading="loading"
|
||||||
|
:label="__('Save')"
|
||||||
|
variant="solid"
|
||||||
|
@click="saveChanges"
|
||||||
|
/>
|
||||||
|
<Button :label="__('Reset')" @click="reload" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="tabs?.data">
|
||||||
|
<FieldLayoutEditor
|
||||||
|
v-if="!preview"
|
||||||
|
:tabs="tabs.data"
|
||||||
|
:doctype="_doctype"
|
||||||
|
/>
|
||||||
|
<FieldLayout v-else :tabs="tabs.data" :data="{}" :modal="true" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import FieldLayout from '@/components/FieldLayout.vue'
|
||||||
|
import FieldLayoutEditor from '@/components/FieldLayoutEditor.vue'
|
||||||
|
import { useDebounceFn } from '@vueuse/core'
|
||||||
|
import { capture } from '@/telemetry'
|
||||||
|
import { Dialog, Badge, call, createResource } from 'frappe-ui'
|
||||||
|
import { ref, watch, onMounted, nextTick } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
doctype: {
|
||||||
|
type: String,
|
||||||
|
default: 'CRM Lead',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['reload'])
|
||||||
|
|
||||||
|
const show = defineModel()
|
||||||
|
const _doctype = ref(props.doctype)
|
||||||
|
const loading = ref(false)
|
||||||
|
const dirty = ref(false)
|
||||||
|
const preview = ref(false)
|
||||||
|
|
||||||
|
function getParams() {
|
||||||
|
return { doctype: _doctype.value, type: 'Grid Row' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabs = createResource({
|
||||||
|
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
||||||
|
cache: ['GridRowFieldsModal', _doctype.value],
|
||||||
|
params: getParams(),
|
||||||
|
onSuccess(data) {
|
||||||
|
tabs.originalData = JSON.parse(JSON.stringify(data))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => tabs?.data,
|
||||||
|
() => {
|
||||||
|
dirty.value =
|
||||||
|
JSON.stringify(tabs?.data) !== JSON.stringify(tabs?.originalData)
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
onMounted(() => useDebounceFn(reload, 100)())
|
||||||
|
|
||||||
|
function reload() {
|
||||||
|
nextTick(() => {
|
||||||
|
tabs.params = getParams()
|
||||||
|
tabs.reload()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveChanges() {
|
||||||
|
let _tabs = JSON.parse(JSON.stringify(tabs.data))
|
||||||
|
_tabs.forEach((tab) => {
|
||||||
|
if (!tab.sections) return
|
||||||
|
tab.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: 'Grid Row',
|
||||||
|
layout: JSON.stringify(_tabs),
|
||||||
|
},
|
||||||
|
).then(() => {
|
||||||
|
loading.value = false
|
||||||
|
show.value = false
|
||||||
|
capture('data_fields_layout_builder', { doctype: _doctype.value })
|
||||||
|
emit('reload')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
62
frontend/src/components/Controls/GridRowModal.vue
Normal file
62
frontend/src/components/Controls/GridRowModal.vue
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog v-model="show" :options="{ size: '4xl' }">
|
||||||
|
<template #body>
|
||||||
|
<div class="bg-surface-modal px-4 pb-6 pt-5 sm:px-6">
|
||||||
|
<div class="mb-5 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-2xl font-semibold leading-6 text-ink-gray-9">
|
||||||
|
{{ __('Editing Row {0}', [index + 1]) }}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<Button
|
||||||
|
v-if="isManager()"
|
||||||
|
variant="ghost"
|
||||||
|
class="w-7"
|
||||||
|
@click="openGridRowFieldsModal"
|
||||||
|
>
|
||||||
|
<EditIcon class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" class="w-7" @click="show = false">
|
||||||
|
<FeatherIcon name="x" class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<FieldLayout v-if="tabs.data" :tabs="tabs.data" :data="data" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||||
|
import FieldLayout from '@/components/FieldLayout.vue'
|
||||||
|
import { usersStore } from '@/stores/users'
|
||||||
|
import { createResource } from 'frappe-ui'
|
||||||
|
import { nextTick } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
index: Number,
|
||||||
|
data: Object,
|
||||||
|
doctype: String,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { isManager } = usersStore()
|
||||||
|
|
||||||
|
const show = defineModel()
|
||||||
|
const showGridRowFieldsModal = defineModel('showGridRowFieldsModal')
|
||||||
|
|
||||||
|
const tabs = createResource({
|
||||||
|
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
||||||
|
cache: ['GridRow', props.doctype],
|
||||||
|
params: { doctype: props.doctype, type: 'Grid Row' },
|
||||||
|
auto: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
function openGridRowFieldsModal() {
|
||||||
|
showGridRowFieldsModal.value = true
|
||||||
|
nextTick(() => (show.value = false))
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -15,7 +15,7 @@
|
|||||||
!hasTabs ? 'hidden' : modal ? 'border-outline-gray-modals' : ''
|
!hasTabs ? 'hidden' : modal ? 'border-outline-gray-modals' : ''
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<div :class="{ 'my-4 sm:my-6': hasTabs }">
|
<div class="overflow-hidden" :class="{ 'my-4 sm:my-6': hasTabs }">
|
||||||
<div
|
<div
|
||||||
v-for="(section, i) in tab.sections"
|
v-for="(section, i) in tab.sections"
|
||||||
:key="section.label"
|
:key="section.label"
|
||||||
@ -76,6 +76,13 @@
|
|||||||
v-model="data[field.name]"
|
v-model="data[field.name]"
|
||||||
:disabled="true"
|
:disabled="true"
|
||||||
/>
|
/>
|
||||||
|
<Grid
|
||||||
|
v-else-if="field.type === 'Table'"
|
||||||
|
v-model="data[field.name]"
|
||||||
|
:fields="field.fields"
|
||||||
|
:doctype="field.options"
|
||||||
|
:parentDoctype="doctype"
|
||||||
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-else-if="field.type === 'Select'"
|
v-else-if="field.type === 'Select'"
|
||||||
type="select"
|
type="select"
|
||||||
@ -178,7 +185,9 @@
|
|||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-else-if="
|
v-else-if="
|
||||||
['Small Text', 'Text', 'Long Text'].includes(field.type)
|
['Small Text', 'Text', 'Long Text', 'Code'].includes(
|
||||||
|
field.type,
|
||||||
|
)
|
||||||
"
|
"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
:placeholder="getPlaceholder(field)"
|
:placeholder="getPlaceholder(field)"
|
||||||
@ -237,7 +246,8 @@ import EditIcon from '@/components/Icons/EditIcon.vue'
|
|||||||
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
|
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
|
||||||
import UserAvatar from '@/components/UserAvatar.vue'
|
import UserAvatar from '@/components/UserAvatar.vue'
|
||||||
import Link from '@/components/Controls/Link.vue'
|
import Link from '@/components/Controls/Link.vue'
|
||||||
import { getMeta } from '../stores/meta'
|
import Grid from '@/components/Controls/Grid.vue'
|
||||||
|
import { getMeta } from '@/stores/meta'
|
||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
import { getFormat } from '@/utils'
|
import { getFormat } from '@/utils'
|
||||||
import { flt } from '@/utils/numberFormat.js'
|
import { flt } from '@/utils/numberFormat.js'
|
||||||
|
|||||||
@ -1,70 +1,70 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-5.5">
|
<div class="flex flex-col gap-5.5">
|
||||||
<div
|
<div
|
||||||
class="flex justify-between items-center gap-1 text-base bg-surface-gray-2 rounded py-2 px-2.5"
|
class="flex items-center gap-2 text-base bg-surface-gray-2 rounded py-2 px-2.5"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-1">
|
<Draggable
|
||||||
<Draggable
|
v-if="tabs.length && !tabs[tabIndex].no_tabs"
|
||||||
v-if="tabs.length && !tabs[tabIndex].no_tabs"
|
:list="tabs"
|
||||||
:list="tabs"
|
item-key="label"
|
||||||
item-key="label"
|
class="flex items-center gap-2"
|
||||||
class="flex items-center gap-1"
|
@end="(e) => (tabIndex = e.newIndex)"
|
||||||
@end="(e) => (tabIndex = e.newIndex)"
|
>
|
||||||
>
|
<template #item="{ element: tab, index: i }">
|
||||||
<template #item="{ element: tab, index: i }">
|
<div
|
||||||
<div
|
class="flex items-center gap-2 cursor-pointer rounded"
|
||||||
class="cursor-pointer rounded"
|
:class="[
|
||||||
:class="[
|
tabIndex == i
|
||||||
tabIndex == i
|
? 'text-ink-gray-9 bg-surface-white shadow-sm'
|
||||||
? 'text-ink-gray-9 bg-surface-white shadow-sm'
|
: 'text-ink-gray-5 hover:text-ink-gray-9 hover:bg-surface-white hover:shadow-sm',
|
||||||
: 'text-ink-gray-5 hover:text-ink-gray-9 hover:bg-surface-white hover:shadow-sm',
|
tab.editingLabel ? 'p-1' : 'px-2 py-1',
|
||||||
tab.editingLabel ? 'p-1' : 'px-2 py-1',
|
]"
|
||||||
]"
|
@click="tabIndex = i"
|
||||||
@click="tabIndex = i"
|
>
|
||||||
>
|
<div @dblclick="() => (tab.editingLabel = true)">
|
||||||
<div @dblclick="() => (tab.editingLabel = true)">
|
<div v-if="!tab.editingLabel" class="flex items-center gap-2">
|
||||||
<div v-if="!tab.editingLabel" class="flex items-center gap-2">
|
{{ __(tab.label) || __('Untitled') }}
|
||||||
{{ __(tab.label) || __('Untitled') }}
|
</div>
|
||||||
</div>
|
<div v-else class="flex gap-1 items-center">
|
||||||
<div v-else class="flex gap-1 items-center">
|
<Input
|
||||||
<Input
|
v-model="tab.label"
|
||||||
v-model="tab.label"
|
@keydown.enter="tab.editingLabel = false"
|
||||||
@keydown.enter="tab.editingLabel = false"
|
@blur="tab.editingLabel = false"
|
||||||
@blur="tab.editingLabel = false"
|
@click.stop
|
||||||
@click.stop
|
/>
|
||||||
/>
|
<Button
|
||||||
<Button
|
v-if="tab.editingLabel"
|
||||||
v-if="tab.editingLabel"
|
icon="check"
|
||||||
icon="check"
|
variant="ghost"
|
||||||
variant="ghost"
|
@click="tab.editingLabel = false"
|
||||||
@click="tab.editingLabel = false"
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
<Dropdown
|
||||||
</Draggable>
|
v-if="!tab.no_tabs && tabIndex == i"
|
||||||
<Button
|
:options="getTabOptions(tab)"
|
||||||
variant="ghost"
|
class="!h-4"
|
||||||
class="!h-6.5 !text-ink-gray-5 hover:!text-ink-gray-9"
|
@click.stop
|
||||||
@click="addTab"
|
>
|
||||||
:label="__('Add Tab')"
|
<template #default>
|
||||||
>
|
<Button variant="ghost" class="!p-1 !h-4">
|
||||||
<template #prefix>
|
<FeatherIcon name="more-horizontal" class="h-4" />
|
||||||
<FeatherIcon name="plus" class="h-4" />
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
</Button>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
<Dropdown
|
|
||||||
v-if="tabs.length && !tabs[tabIndex].no_tabs"
|
|
||||||
:options="getTabOptions(tabs[tabIndex])"
|
|
||||||
>
|
|
||||||
<template #default>
|
|
||||||
<Button variant="ghost">
|
|
||||||
<FeatherIcon name="more-horizontal" class="h-4" />
|
|
||||||
</Button>
|
|
||||||
</template>
|
</template>
|
||||||
</Dropdown>
|
</Draggable>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
class="!h-6.5 !text-ink-gray-5 hover:!text-ink-gray-9"
|
||||||
|
@click="addTab"
|
||||||
|
:label="__('Add Tab')"
|
||||||
|
>
|
||||||
|
<template v-slot:[slotName]>
|
||||||
|
<FeatherIcon name="plus" class="h-4" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="tabIndex == i" v-for="(tab, i) in tabs" :key="tab.label">
|
<div v-show="tabIndex == i" v-for="(tab, i) in tabs" :key="tab.label">
|
||||||
<Draggable
|
<Draggable
|
||||||
@ -126,9 +126,9 @@
|
|||||||
<div
|
<div
|
||||||
class="px-2.5 py-2 border border-outline-gray-2 rounded text-base bg-surface-modal text-ink-gray-8 flex items-center leading-4 justify-between gap-2"
|
class="px-2.5 py-2 border border-outline-gray-2 rounded text-base bg-surface-modal text-ink-gray-8 flex items-center leading-4 justify-between gap-2"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2 truncate">
|
||||||
<DragVerticalIcon class="h-3.5 cursor-grab" />
|
<DragVerticalIcon class="h-3.5 cursor-grab" />
|
||||||
<div>{{ field.label }}</div>
|
<div class="truncate">{{ field.label }}</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@ -207,9 +207,14 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const tabIndex = ref(0)
|
const tabIndex = ref(0)
|
||||||
|
const slotName = computed(() => {
|
||||||
|
if (props.tabs.length == 1 && props.tabs[0].no_tabs) {
|
||||||
|
return 'prefix'
|
||||||
|
}
|
||||||
|
return 'default'
|
||||||
|
})
|
||||||
|
|
||||||
const restrictedFieldTypes = [
|
const restrictedFieldTypes = [
|
||||||
'Table',
|
|
||||||
'Geolocation',
|
'Geolocation',
|
||||||
'Attach',
|
'Attach',
|
||||||
'Attach Image',
|
'Attach Image',
|
||||||
@ -230,6 +235,34 @@ const fields = createResource({
|
|||||||
params: params.value,
|
params: params.value,
|
||||||
cache: ['fieldsMeta', props.doctype],
|
cache: ['fieldsMeta', props.doctype],
|
||||||
auto: true,
|
auto: true,
|
||||||
|
transform: (data) => {
|
||||||
|
let restrictedFields = [
|
||||||
|
'name',
|
||||||
|
'owner',
|
||||||
|
'creation',
|
||||||
|
'modified',
|
||||||
|
'modified_by',
|
||||||
|
'docstatus',
|
||||||
|
'_comments',
|
||||||
|
'_user_tags',
|
||||||
|
'_assign',
|
||||||
|
'_liked_by',
|
||||||
|
]
|
||||||
|
let existingFields = []
|
||||||
|
|
||||||
|
for (let tab of props.tabs) {
|
||||||
|
for (let section of tab.sections) {
|
||||||
|
existingFields = existingFields.concat(section.fields)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.filter((field) => {
|
||||||
|
return (
|
||||||
|
!existingFields.find((f) => f.name === field.fieldname) &&
|
||||||
|
!restrictedFields.includes(field.fieldname)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
function addTab() {
|
function addTab() {
|
||||||
@ -243,7 +276,12 @@ function addTab() {
|
|||||||
|
|
||||||
function addField(section, field) {
|
function addField(section, field) {
|
||||||
if (!field) return
|
if (!field) return
|
||||||
section.fields.push(field)
|
let newFieldObj = {
|
||||||
|
...field,
|
||||||
|
name: field.fieldname,
|
||||||
|
type: field.fieldtype,
|
||||||
|
}
|
||||||
|
section.fields.push(newFieldObj)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTabOptions(tab) {
|
function getTabOptions(tab) {
|
||||||
|
|||||||
@ -89,7 +89,7 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
<template #item-label="{ option }">
|
<template #item-label="{ option }">
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1 text-ink-gray-9">
|
||||||
<div>{{ option.label }}</div>
|
<div>{{ option.label }}</div>
|
||||||
<div class="text-ink-gray-4 text-sm">
|
<div class="text-ink-gray-4 text-sm">
|
||||||
{{ `${option.fieldname} - ${option.fieldtype}` }}
|
{{ `${option.fieldname} - ${option.fieldtype}` }}
|
||||||
@ -166,9 +166,6 @@ const fields = createResource({
|
|||||||
params: { doctype: props.doctype, as_array: true },
|
params: { doctype: props.doctype, as_array: true },
|
||||||
cache: ['kanban_fields', props.doctype],
|
cache: ['kanban_fields', props.doctype],
|
||||||
auto: true,
|
auto: true,
|
||||||
onSuccess: (data) => {
|
|
||||||
data
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const allFields = computed({
|
const allFields = computed({
|
||||||
|
|||||||
@ -16,18 +16,19 @@
|
|||||||
<template #body-content>
|
<template #body-content>
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<div class="flex justify-between gap-2">
|
<div class="flex justify-between gap-2">
|
||||||
<FormControl
|
<Button
|
||||||
type="select"
|
|
||||||
class="w-1/4"
|
|
||||||
v-model="_doctype"
|
|
||||||
:options="['CRM Lead', 'CRM Deal']"
|
|
||||||
@change="reload"
|
|
||||||
/>
|
|
||||||
<Switch
|
|
||||||
v-model="preview"
|
|
||||||
:label="preview ? __('Hide preview') : __('Show preview')"
|
:label="preview ? __('Hide preview') : __('Show preview')"
|
||||||
size="sm"
|
@click="preview = !preview"
|
||||||
/>
|
/>
|
||||||
|
<div class="flex flex-row-reverse gap-2">
|
||||||
|
<Button
|
||||||
|
:loading="loading"
|
||||||
|
:label="__('Save')"
|
||||||
|
variant="solid"
|
||||||
|
@click="saveChanges"
|
||||||
|
/>
|
||||||
|
<Button :label="__('Reset')" @click="reload" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="tabs?.data">
|
<div v-if="tabs?.data">
|
||||||
<FieldLayoutEditor
|
<FieldLayoutEditor
|
||||||
@ -39,17 +40,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #actions>
|
|
||||||
<div class="flex flex-row-reverse gap-2">
|
|
||||||
<Button
|
|
||||||
:loading="loading"
|
|
||||||
:label="__('Save')"
|
|
||||||
variant="solid"
|
|
||||||
@click="saveChanges"
|
|
||||||
/>
|
|
||||||
<Button :label="__('Reset')" @click="reload" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
@ -57,7 +47,7 @@ import FieldLayout from '@/components/FieldLayout.vue'
|
|||||||
import FieldLayoutEditor from '@/components/FieldLayoutEditor.vue'
|
import FieldLayoutEditor from '@/components/FieldLayoutEditor.vue'
|
||||||
import { useDebounceFn } from '@vueuse/core'
|
import { useDebounceFn } from '@vueuse/core'
|
||||||
import { capture } from '@/telemetry'
|
import { capture } from '@/telemetry'
|
||||||
import { Dialog, Badge, Switch, call, createResource } from 'frappe-ui'
|
import { Dialog, Badge, call, createResource } from 'frappe-ui'
|
||||||
import { ref, watch, onMounted, nextTick } from 'vue'
|
import { ref, watch, onMounted, nextTick } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|||||||
@ -16,24 +16,19 @@
|
|||||||
<template #body-content>
|
<template #body-content>
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<div class="flex justify-between gap-2">
|
<div class="flex justify-between gap-2">
|
||||||
<FormControl
|
<Button
|
||||||
type="select"
|
|
||||||
class="w-1/4"
|
|
||||||
v-model="_doctype"
|
|
||||||
:options="[
|
|
||||||
'CRM Lead',
|
|
||||||
'CRM Deal',
|
|
||||||
'Contact',
|
|
||||||
'CRM Organization',
|
|
||||||
'Address',
|
|
||||||
]"
|
|
||||||
@change="reload"
|
|
||||||
/>
|
|
||||||
<Switch
|
|
||||||
v-model="preview"
|
|
||||||
:label="preview ? __('Hide preview') : __('Show preview')"
|
:label="preview ? __('Hide preview') : __('Show preview')"
|
||||||
size="sm"
|
@click="preview = !preview"
|
||||||
/>
|
/>
|
||||||
|
<div class="flex flex-row-reverse gap-2">
|
||||||
|
<Button
|
||||||
|
:loading="loading"
|
||||||
|
:label="__('Save')"
|
||||||
|
variant="solid"
|
||||||
|
@click="saveChanges"
|
||||||
|
/>
|
||||||
|
<Button :label="__('Reset')" @click="reload" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="tabs?.data">
|
<div v-if="tabs?.data">
|
||||||
<FieldLayoutEditor
|
<FieldLayoutEditor
|
||||||
@ -45,17 +40,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #actions>
|
|
||||||
<div class="flex flex-row-reverse gap-2">
|
|
||||||
<Button
|
|
||||||
:loading="loading"
|
|
||||||
:label="__('Save')"
|
|
||||||
variant="solid"
|
|
||||||
@click="saveChanges"
|
|
||||||
/>
|
|
||||||
<Button :label="__('Reset')" @click="reload" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
|
|||||||
@ -16,18 +16,19 @@
|
|||||||
<template #body-content>
|
<template #body-content>
|
||||||
<div class="flex flex-col gap-5.5">
|
<div class="flex flex-col gap-5.5">
|
||||||
<div class="flex justify-between gap-2">
|
<div class="flex justify-between gap-2">
|
||||||
<FormControl
|
<Button
|
||||||
type="select"
|
|
||||||
class="w-1/4"
|
|
||||||
v-model="_doctype"
|
|
||||||
:options="['CRM Lead', 'CRM Deal', 'Contact', 'CRM Organization']"
|
|
||||||
@change="reload"
|
|
||||||
/>
|
|
||||||
<Switch
|
|
||||||
v-model="preview"
|
|
||||||
:label="preview ? __('Hide preview') : __('Show preview')"
|
:label="preview ? __('Hide preview') : __('Show preview')"
|
||||||
size="sm"
|
@click="preview = !preview"
|
||||||
/>
|
/>
|
||||||
|
<div class="flex flex-row-reverse gap-2">
|
||||||
|
<Button
|
||||||
|
:loading="loading"
|
||||||
|
:label="__('Save')"
|
||||||
|
variant="solid"
|
||||||
|
@click="saveChanges"
|
||||||
|
/>
|
||||||
|
<Button :label="__('Reset')" @click="reload" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="tabs.data?.[0]?.sections" class="flex gap-4">
|
<div v-if="tabs.data?.[0]?.sections" class="flex gap-4">
|
||||||
<SidePanelLayoutEditor
|
<SidePanelLayoutEditor
|
||||||
@ -66,17 +67,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #actions>
|
|
||||||
<div class="flex flex-row-reverse gap-2">
|
|
||||||
<Button
|
|
||||||
:loading="loading"
|
|
||||||
:label="__('Save')"
|
|
||||||
variant="solid"
|
|
||||||
@click="saveChanges"
|
|
||||||
/>
|
|
||||||
<Button :label="__('Reset')" @click="reload" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
@ -85,7 +75,7 @@ import SidePanelLayout from '@/components/SidePanelLayout.vue'
|
|||||||
import SidePanelLayoutEditor from '@/components/SidePanelLayoutEditor.vue'
|
import SidePanelLayoutEditor from '@/components/SidePanelLayoutEditor.vue'
|
||||||
import { useDebounceFn } from '@vueuse/core'
|
import { useDebounceFn } from '@vueuse/core'
|
||||||
import { capture } from '@/telemetry'
|
import { capture } from '@/telemetry'
|
||||||
import { Dialog, Badge, Switch, call, createResource } from 'frappe-ui'
|
import { Dialog, Badge, call, createResource } from 'frappe-ui'
|
||||||
import { ref, watch, onMounted, nextTick } from 'vue'
|
import { ref, watch, onMounted, nextTick } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import { createResource } from 'frappe-ui'
|
import { createResource } from 'frappe-ui'
|
||||||
import { formatCurrency, formatNumber } from '@/utils/numberFormat.js'
|
import { formatCurrency, formatNumber } from '@/utils/numberFormat.js'
|
||||||
import { reactive } from 'vue'
|
import { ref, reactive } from 'vue'
|
||||||
|
|
||||||
const doctypeMeta = reactive({})
|
const doctypeMeta = reactive({})
|
||||||
|
const userSettings = reactive({})
|
||||||
|
|
||||||
export function getMeta(doctype) {
|
export function getMeta(doctype) {
|
||||||
const meta = createResource({
|
const meta = createResource({
|
||||||
@ -18,6 +19,8 @@ export function getMeta(doctype) {
|
|||||||
for (let dtMeta of dtMetas) {
|
for (let dtMeta of dtMetas) {
|
||||||
doctypeMeta[dtMeta.name] = dtMeta
|
doctypeMeta[dtMeta.name] = dtMeta
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userSettings[doctype] = JSON.parse(res.user_settings)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -52,9 +55,54 @@ export function getMeta(doctype) {
|
|||||||
return formatCurrency(doc[fieldname], '', currency, precision)
|
return formatCurrency(doc[fieldname], '', currency, precision)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getGridSettings(parentDoctype, dt = null) {
|
||||||
|
dt = dt || doctype
|
||||||
|
if (!userSettings[parentDoctype]['GridView']?.[doctype]) return {}
|
||||||
|
return userSettings[parentDoctype]['GridView'][doctype]
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFields(dt = null) {
|
||||||
|
dt = dt || doctype
|
||||||
|
return doctypeMeta[dt]?.fields.map((f) => {
|
||||||
|
if (f.fieldtype === 'Select' && typeof f.options === 'string') {
|
||||||
|
f.options = f.options.split('\n').map((option) => {
|
||||||
|
return {
|
||||||
|
label: option,
|
||||||
|
value: option,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return f
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveUserSettings(parentDoctype, key, value, callback) {
|
||||||
|
let oldUserSettings = userSettings[parentDoctype]
|
||||||
|
let newUserSettings = JSON.parse(JSON.stringify(oldUserSettings))
|
||||||
|
|
||||||
|
newUserSettings[key][doctype] = value
|
||||||
|
|
||||||
|
if (JSON.stringify(oldUserSettings) !== JSON.stringify(newUserSettings)) {
|
||||||
|
return createResource({
|
||||||
|
url: 'frappe.model.utils.user_settings.save',
|
||||||
|
params: {
|
||||||
|
doctype: parentDoctype,
|
||||||
|
user_settings: JSON.stringify(newUserSettings),
|
||||||
|
},
|
||||||
|
auto: true,
|
||||||
|
onSuccess: () => callback?.(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return callback?.()
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
meta,
|
meta,
|
||||||
doctypeMeta,
|
doctypeMeta,
|
||||||
|
userSettings,
|
||||||
|
getFields,
|
||||||
|
getGridSettings,
|
||||||
|
saveUserSettings,
|
||||||
getFormattedFloat,
|
getFormattedFloat,
|
||||||
getFormattedPercent,
|
getFormattedPercent,
|
||||||
getFormattedCurrency,
|
getFormattedCurrency,
|
||||||
|
|||||||
@ -305,3 +305,14 @@ export function isImage(extention) {
|
|||||||
extention.toLowerCase(),
|
extention.toLowerCase(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getRandom(len) {
|
||||||
|
let text = ''
|
||||||
|
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
|
||||||
|
|
||||||
|
Array.from({ length: len }).forEach(() => {
|
||||||
|
text += possible.charAt(Math.floor(Math.random() * possible.length))
|
||||||
|
})
|
||||||
|
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|||||||
@ -2388,10 +2388,10 @@ fraction.js@^4.3.7:
|
|||||||
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7"
|
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7"
|
||||||
integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==
|
integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==
|
||||||
|
|
||||||
frappe-ui@^0.1.91:
|
frappe-ui@^0.1.94:
|
||||||
version "0.1.93"
|
version "0.1.94"
|
||||||
resolved "https://registry.yarnpkg.com/frappe-ui/-/frappe-ui-0.1.93.tgz#0443800e195cddcff88ba875989148a92a270206"
|
resolved "https://registry.yarnpkg.com/frappe-ui/-/frappe-ui-0.1.94.tgz#574a193ce51b47cb2747a5d5f9e9fe91b164e3a6"
|
||||||
integrity sha512-21VUcBBB9g1o7Iv/TW8ng3SdCxsm9G/g0rEdC9Y1Vqx1A1Ucf8VtVFECX2MgoDrLAwVOBKayU5BMaS1fuphcnA==
|
integrity sha512-WV7nApCrDBqtKPPiVVxqFgA0JdWQz9kATZylfmMJQrSWszHCx0k0eriTSvT9+0vmtT0T9UYqLfXnhNj0lnqYNA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@headlessui/vue" "^1.7.14"
|
"@headlessui/vue" "^1.7.14"
|
||||||
"@popperjs/core" "^2.11.2"
|
"@popperjs/core" "^2.11.2"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user