commit
b4c766f513
176
crm/api/doc.py
176
crm/api/doc.py
@ -78,12 +78,7 @@ def get_filterable_fields(doctype: str):
|
|||||||
# append standard fields (getting error when using frappe.model.std_fields)
|
# append standard fields (getting error when using frappe.model.std_fields)
|
||||||
standard_fields = [
|
standard_fields = [
|
||||||
{"fieldname": "name", "fieldtype": "Link", "label": "ID", "options": doctype},
|
{"fieldname": "name", "fieldtype": "Link", "label": "ID", "options": doctype},
|
||||||
{
|
{"fieldname": "owner", "fieldtype": "Link", "label": "Created By", "options": "User"},
|
||||||
"fieldname": "owner",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"label": "Created By",
|
|
||||||
"options": "User"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "modified_by",
|
"fieldname": "modified_by",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
@ -98,10 +93,7 @@ def get_filterable_fields(doctype: str):
|
|||||||
{"fieldname": "modified", "fieldtype": "Datetime", "label": "Last Updated On"},
|
{"fieldname": "modified", "fieldtype": "Datetime", "label": "Last Updated On"},
|
||||||
]
|
]
|
||||||
for field in standard_fields:
|
for field in standard_fields:
|
||||||
if (
|
if field.get("fieldname") not in restricted_fields and field.get("fieldtype") in allowed_fieldtypes:
|
||||||
field.get("fieldname") not in restricted_fields and
|
|
||||||
field.get("fieldtype") in allowed_fieldtypes
|
|
||||||
):
|
|
||||||
field["name"] = field.get("fieldname")
|
field["name"] = field.get("fieldname")
|
||||||
res.append(field)
|
res.append(field)
|
||||||
|
|
||||||
@ -128,7 +120,11 @@ def get_group_by_fields(doctype: str):
|
|||||||
]
|
]
|
||||||
|
|
||||||
fields = frappe.get_meta(doctype).fields
|
fields = frappe.get_meta(doctype).fields
|
||||||
fields = [field for field in fields if field.fieldtype not in no_value_fields and field.fieldtype in allowed_fieldtypes]
|
fields = [
|
||||||
|
field
|
||||||
|
for field in fields
|
||||||
|
if field.fieldtype not in no_value_fields and field.fieldtype in allowed_fieldtypes
|
||||||
|
]
|
||||||
fields = [
|
fields = [
|
||||||
{
|
{
|
||||||
"label": _(field.label),
|
"label": _(field.label),
|
||||||
@ -176,6 +172,7 @@ def get_doctype_fields_meta(DocField, doctype, allowed_fieldtypes, restricted_fi
|
|||||||
.run(as_dict=True)
|
.run(as_dict=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_quick_filters(doctype: str):
|
def get_quick_filters(doctype: str):
|
||||||
meta = frappe.get_meta(doctype)
|
meta = frappe.get_meta(doctype)
|
||||||
@ -183,23 +180,25 @@ def get_quick_filters(doctype: str):
|
|||||||
quick_filters = []
|
quick_filters = []
|
||||||
|
|
||||||
for field in fields:
|
for field in fields:
|
||||||
|
|
||||||
if field.fieldtype == "Select":
|
if field.fieldtype == "Select":
|
||||||
field.options = field.options.split("\n")
|
field.options = field.options.split("\n")
|
||||||
field.options = [{"label": option, "value": option} for option in field.options]
|
field.options = [{"label": option, "value": option} for option in field.options]
|
||||||
field.options.insert(0, {"label": "", "value": ""})
|
field.options.insert(0, {"label": "", "value": ""})
|
||||||
quick_filters.append({
|
quick_filters.append(
|
||||||
"label": _(field.label),
|
{
|
||||||
"name": field.fieldname,
|
"label": _(field.label),
|
||||||
"type": field.fieldtype,
|
"name": field.fieldname,
|
||||||
"options": field.options,
|
"type": field.fieldtype,
|
||||||
})
|
"options": field.options,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if doctype == "CRM Lead":
|
if doctype == "CRM Lead":
|
||||||
quick_filters = [filter for filter in quick_filters if filter.get("name") != "converted"]
|
quick_filters = [filter for filter in quick_filters if filter.get("name") != "converted"]
|
||||||
|
|
||||||
return quick_filters
|
return quick_filters
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_data(
|
def get_data(
|
||||||
doctype: str,
|
doctype: str,
|
||||||
@ -223,9 +222,9 @@ def get_data(
|
|||||||
kanban_fields = frappe.parse_json(kanban_fields or "[]")
|
kanban_fields = frappe.parse_json(kanban_fields or "[]")
|
||||||
kanban_columns = frappe.parse_json(kanban_columns or "[]")
|
kanban_columns = frappe.parse_json(kanban_columns or "[]")
|
||||||
|
|
||||||
custom_view_name = view.get('custom_view_name') if view else None
|
custom_view_name = view.get("custom_view_name") if view else None
|
||||||
view_type = view.get('view_type') if view else None
|
view_type = view.get("view_type") if view else None
|
||||||
group_by_field = view.get('group_by_field') if view else None
|
group_by_field = view.get("group_by_field") if view else None
|
||||||
|
|
||||||
for key in filters:
|
for key in filters:
|
||||||
value = filters[key]
|
value = filters[key]
|
||||||
@ -268,7 +267,7 @@ def get_data(
|
|||||||
|
|
||||||
default_view_filters = {
|
default_view_filters = {
|
||||||
"dt": doctype,
|
"dt": doctype,
|
||||||
"type": view_type or 'list',
|
"type": view_type or "list",
|
||||||
"is_default": 1,
|
"is_default": 1,
|
||||||
"user": frappe.session.user,
|
"user": frappe.session.user,
|
||||||
}
|
}
|
||||||
@ -295,13 +294,16 @@ def get_data(
|
|||||||
if group_by_field and group_by_field not in rows:
|
if group_by_field and group_by_field not in rows:
|
||||||
rows.append(group_by_field)
|
rows.append(group_by_field)
|
||||||
|
|
||||||
data = frappe.get_list(
|
data = (
|
||||||
doctype,
|
frappe.get_list(
|
||||||
fields=rows,
|
doctype,
|
||||||
filters=filters,
|
fields=rows,
|
||||||
order_by=order_by,
|
filters=filters,
|
||||||
page_length=page_length,
|
order_by=order_by,
|
||||||
) or []
|
page_length=page_length,
|
||||||
|
)
|
||||||
|
or []
|
||||||
|
)
|
||||||
|
|
||||||
if view_type == "kanban":
|
if view_type == "kanban":
|
||||||
if not rows:
|
if not rows:
|
||||||
@ -336,9 +338,9 @@ def get_data(
|
|||||||
rows.append(field)
|
rows.append(field)
|
||||||
|
|
||||||
for kc in kanban_columns:
|
for kc in kanban_columns:
|
||||||
column_filters = { column_field: kc.get('name') }
|
column_filters = {column_field: kc.get("name")}
|
||||||
order = kc.get("order")
|
order = kc.get("order")
|
||||||
if column_field in filters and filters.get(column_field) != kc.name or kc.get('delete'):
|
if column_field in filters and filters.get(column_field) != kc.name or kc.get("delete"):
|
||||||
column_data = []
|
column_data = []
|
||||||
else:
|
else:
|
||||||
column_filters.update(filters.copy())
|
column_filters.update(filters.copy())
|
||||||
@ -348,7 +350,9 @@ def get_data(
|
|||||||
page_length = kc.get("page_length")
|
page_length = kc.get("page_length")
|
||||||
|
|
||||||
if order:
|
if order:
|
||||||
column_data = get_records_based_on_order(doctype, rows, column_filters, page_length, order)
|
column_data = get_records_based_on_order(
|
||||||
|
doctype, rows, column_filters, page_length, order
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
column_data = frappe.get_list(
|
column_data = frappe.get_list(
|
||||||
doctype,
|
doctype,
|
||||||
@ -359,9 +363,11 @@ def get_data(
|
|||||||
)
|
)
|
||||||
|
|
||||||
new_filters = filters.copy()
|
new_filters = filters.copy()
|
||||||
new_filters.update({ column_field: kc.get('name') })
|
new_filters.update({column_field: kc.get("name")})
|
||||||
|
|
||||||
all_count = len(frappe.get_list(doctype, filters=convert_filter_to_tuple(doctype, new_filters)))
|
all_count = len(
|
||||||
|
frappe.get_list(doctype, filters=convert_filter_to_tuple(doctype, new_filters))
|
||||||
|
)
|
||||||
|
|
||||||
kc["all_count"] = all_count
|
kc["all_count"] = all_count
|
||||||
kc["count"] = len(column_data)
|
kc["count"] = len(column_data)
|
||||||
@ -371,8 +377,8 @@ def get_data(
|
|||||||
|
|
||||||
if order:
|
if order:
|
||||||
column_data = sorted(
|
column_data = sorted(
|
||||||
column_data, key=lambda x: order.index(x.get("name"))
|
column_data,
|
||||||
if x.get("name") in order else len(order)
|
key=lambda x: order.index(x.get("name")) if x.get("name") in order else len(order),
|
||||||
)
|
)
|
||||||
|
|
||||||
data.append({"column": kc, "fields": kanban_fields, "data": column_data})
|
data.append({"column": kc, "fields": kanban_fields, "data": column_data})
|
||||||
@ -406,8 +412,8 @@ def get_data(
|
|||||||
]
|
]
|
||||||
|
|
||||||
for field in std_fields:
|
for field in std_fields:
|
||||||
if field.get('value') not in rows:
|
if field.get("value") not in rows:
|
||||||
rows.append(field.get('value'))
|
rows.append(field.get("value"))
|
||||||
if field not in fields:
|
if field not in fields:
|
||||||
field["label"] = _(field["label"])
|
field["label"] = _(field["label"])
|
||||||
fields.append(field)
|
fields.append(field)
|
||||||
@ -416,6 +422,7 @@ def get_data(
|
|||||||
is_default = frappe.db.get_value("CRM View Settings", custom_view_name, "load_default_columns")
|
is_default = frappe.db.get_value("CRM View Settings", custom_view_name, "load_default_columns")
|
||||||
|
|
||||||
if group_by_field and view_type == "group_by":
|
if group_by_field and view_type == "group_by":
|
||||||
|
|
||||||
def get_options(type, options):
|
def get_options(type, options):
|
||||||
if type == "Select":
|
if type == "Select":
|
||||||
return [option for option in options.split("\n")]
|
return [option for option in options.split("\n")]
|
||||||
@ -428,7 +435,9 @@ def get_data(
|
|||||||
|
|
||||||
if order_by and group_by_field in order_by:
|
if order_by and group_by_field in order_by:
|
||||||
order_by_fields = order_by.split(",")
|
order_by_fields = order_by.split(",")
|
||||||
order_by_fields = [(field.split(" ")[0], field.split(" ")[1]) for field in order_by_fields]
|
order_by_fields = [
|
||||||
|
(field.split(" ")[0], field.split(" ")[1]) for field in order_by_fields
|
||||||
|
]
|
||||||
if (group_by_field, "asc") in order_by_fields:
|
if (group_by_field, "asc") in order_by_fields:
|
||||||
options.sort()
|
options.sort()
|
||||||
elif (group_by_field, "desc") in order_by_fields:
|
elif (group_by_field, "desc") in order_by_fields:
|
||||||
@ -467,6 +476,7 @@ def get_data(
|
|||||||
"view_type": view_type,
|
"view_type": view_type,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def convert_filter_to_tuple(doctype, filters):
|
def convert_filter_to_tuple(doctype, filters):
|
||||||
if isinstance(filters, dict):
|
if isinstance(filters, dict):
|
||||||
filters_items = filters.items()
|
filters_items = filters.items()
|
||||||
@ -504,6 +514,7 @@ def get_records_based_on_order(doctype, rows, filters, page_length, order):
|
|||||||
|
|
||||||
return records
|
return records
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_fields_meta(doctype, restricted_fieldtypes=None, as_array=False):
|
def get_fields_meta(doctype, restricted_fieldtypes=None, as_array=False):
|
||||||
not_allowed_fieldtypes = [
|
not_allowed_fieldtypes = [
|
||||||
@ -521,12 +532,7 @@ def get_fields_meta(doctype, restricted_fieldtypes=None, as_array=False):
|
|||||||
|
|
||||||
standard_fields = [
|
standard_fields = [
|
||||||
{"fieldname": "name", "fieldtype": "Link", "label": "ID", "options": doctype},
|
{"fieldname": "name", "fieldtype": "Link", "label": "ID", "options": doctype},
|
||||||
{
|
{"fieldname": "owner", "fieldtype": "Link", "label": "Created By", "options": "User"},
|
||||||
"fieldname": "owner",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"label": "Created By",
|
|
||||||
"options": "User"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "modified_by",
|
"fieldname": "modified_by",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
@ -542,7 +548,7 @@ def get_fields_meta(doctype, restricted_fieldtypes=None, as_array=False):
|
|||||||
]
|
]
|
||||||
|
|
||||||
for field in standard_fields:
|
for field in standard_fields:
|
||||||
if not restricted_fieldtypes or field.get('fieldtype') not in restricted_fieldtypes:
|
if not restricted_fieldtypes or field.get("fieldtype") not in restricted_fieldtypes:
|
||||||
fields.append(field)
|
fields.append(field)
|
||||||
|
|
||||||
if as_array:
|
if as_array:
|
||||||
@ -550,10 +556,11 @@ 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
|
||||||
|
|
||||||
return fields_meta
|
return fields_meta
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_sidebar_fields(doctype, name):
|
def get_sidebar_fields(doctype, name):
|
||||||
if not frappe.db.exists("CRM Fields Layout", {"dt": doctype, "type": "Side Panel"}):
|
if not frappe.db.exists("CRM Fields Layout", {"dt": doctype, "type": "Side Panel"}):
|
||||||
@ -562,7 +569,7 @@ def get_sidebar_fields(doctype, name):
|
|||||||
|
|
||||||
if not layout:
|
if not layout:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
layout = json.loads(layout)
|
layout = json.loads(layout)
|
||||||
|
|
||||||
not_allowed_fieldtypes = [
|
not_allowed_fieldtypes = [
|
||||||
@ -600,6 +607,7 @@ def get_sidebar_fields(doctype, name):
|
|||||||
|
|
||||||
return layout
|
return layout
|
||||||
|
|
||||||
|
|
||||||
def get_field_obj(field):
|
def get_field_obj(field):
|
||||||
obj = {
|
obj = {
|
||||||
"label": field.label,
|
"label": field.label,
|
||||||
@ -641,6 +649,7 @@ def get_type(field):
|
|||||||
return "read_only"
|
return "read_only"
|
||||||
return field.fieldtype.lower()
|
return field.fieldtype.lower()
|
||||||
|
|
||||||
|
|
||||||
def get_assigned_users(doctype, name, default_assigned_to=None):
|
def get_assigned_users(doctype, name, default_assigned_to=None):
|
||||||
assigned_users = frappe.get_all(
|
assigned_users = frappe.get_all(
|
||||||
"ToDo",
|
"ToDo",
|
||||||
@ -671,32 +680,55 @@ def get_fields(doctype: str, allow_all_fieldtypes: bool = False):
|
|||||||
_fields = []
|
_fields = []
|
||||||
|
|
||||||
for field in fields:
|
for field in fields:
|
||||||
if (
|
if field.fieldtype not in not_allowed_fieldtypes and field.fieldname:
|
||||||
field.fieldtype not in not_allowed_fieldtypes
|
_fields.append(
|
||||||
and field.fieldname
|
{
|
||||||
):
|
"label": field.label,
|
||||||
_fields.append({
|
"type": field.fieldtype,
|
||||||
"label": field.label,
|
"value": field.fieldname,
|
||||||
"type": field.fieldtype,
|
"options": field.options,
|
||||||
"value": field.fieldname,
|
"mandatory": field.reqd,
|
||||||
"options": field.options,
|
"read_only": field.read_only,
|
||||||
"mandatory": field.reqd,
|
"hidden": field.hidden,
|
||||||
"read_only": field.read_only,
|
"depends_on": field.depends_on,
|
||||||
"hidden": field.hidden,
|
"mandatory_depends_on": field.mandatory_depends_on,
|
||||||
"depends_on": field.depends_on,
|
"read_only_depends_on": field.read_only_depends_on,
|
||||||
"mandatory_depends_on": field.mandatory_depends_on,
|
"link_filters": field.get("link_filters"),
|
||||||
"read_only_depends_on": field.read_only_depends_on,
|
"placeholder": field.get("placeholder"),
|
||||||
"link_filters": field.get("link_filters"),
|
}
|
||||||
"placeholder": field.get("placeholder"),
|
)
|
||||||
})
|
|
||||||
|
|
||||||
return _fields
|
return _fields
|
||||||
|
|
||||||
|
|
||||||
def getCounts(d, doctype):
|
def getCounts(d, doctype):
|
||||||
d["_email_count"] = frappe.db.count("Communication", filters={"reference_doctype": doctype, "reference_name": d.get("name"), "communication_type": "Communication"}) or 0
|
d["_email_count"] = (
|
||||||
d["_email_count"] = d["_email_count"] + frappe.db.count("Communication", filters={"reference_doctype": doctype, "reference_name": d.get("name"), "communication_type": "Automated Message"})
|
frappe.db.count(
|
||||||
d["_comment_count"] = frappe.db.count("Comment", filters={"reference_doctype": doctype, "reference_name": d.get("name"), "comment_type": "Comment"})
|
"Communication",
|
||||||
d["_task_count"] = frappe.db.count("CRM Task", filters={"reference_doctype": doctype, "reference_docname": d.get("name")})
|
filters={
|
||||||
d["_note_count"] = frappe.db.count("FCRM Note", filters={"reference_doctype": doctype, "reference_docname": d.get("name")})
|
"reference_doctype": doctype,
|
||||||
return d
|
"reference_name": d.get("name"),
|
||||||
|
"communication_type": "Communication",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
or 0
|
||||||
|
)
|
||||||
|
d["_email_count"] = d["_email_count"] + frappe.db.count(
|
||||||
|
"Communication",
|
||||||
|
filters={
|
||||||
|
"reference_doctype": doctype,
|
||||||
|
"reference_name": d.get("name"),
|
||||||
|
"communication_type": "Automated Message",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
d["_comment_count"] = frappe.db.count(
|
||||||
|
"Comment",
|
||||||
|
filters={"reference_doctype": doctype, "reference_name": d.get("name"), "comment_type": "Comment"},
|
||||||
|
)
|
||||||
|
d["_task_count"] = frappe.db.count(
|
||||||
|
"CRM Task", filters={"reference_doctype": doctype, "reference_docname": d.get("name")}
|
||||||
|
)
|
||||||
|
d["_note_count"] = frappe.db.count(
|
||||||
|
"FCRM Note", filters={"reference_doctype": doctype, "reference_docname": d.get("name")}
|
||||||
|
)
|
||||||
|
return d
|
||||||
|
|||||||
@ -1,38 +1,19 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
|
||||||
|
|
||||||
from crm.api.doc import get_fields_meta, get_assigned_users
|
from crm.api.doc import get_assigned_users, get_fields_meta
|
||||||
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()
|
||||||
def get_deal(name):
|
def get_deal(name):
|
||||||
Deal = frappe.qb.DocType("CRM Deal")
|
deal = frappe.get_doc("CRM Deal", name).as_dict()
|
||||||
|
|
||||||
query = (
|
deal["fields_meta"] = get_fields_meta("CRM Deal")
|
||||||
frappe.qb.from_(Deal)
|
deal["_form_script"] = get_form_script("CRM Deal")
|
||||||
.select("*")
|
|
||||||
.where(Deal.name == name)
|
|
||||||
.limit(1)
|
|
||||||
)
|
|
||||||
|
|
||||||
deal = query.run(as_dict=True)
|
|
||||||
if not len(deal):
|
|
||||||
frappe.throw(_("Deal not found"), frappe.DoesNotExistError)
|
|
||||||
deal = deal.pop()
|
|
||||||
|
|
||||||
|
|
||||||
deal["contacts"] = frappe.get_all(
|
|
||||||
"CRM Contacts",
|
|
||||||
filters={"parenttype": "CRM Deal", "parent": deal.name},
|
|
||||||
fields=["contact", "is_primary"],
|
|
||||||
)
|
|
||||||
|
|
||||||
deal["doctype"] = "CRM Deal"
|
|
||||||
deal["fields_meta"] = get_fields_meta("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
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_deal_contacts(name):
|
def get_deal_contacts(name):
|
||||||
contacts = frappe.get_all(
|
contacts = frappe.get_all(
|
||||||
@ -44,16 +25,19 @@ def get_deal_contacts(name):
|
|||||||
for contact in contacts:
|
for contact in contacts:
|
||||||
is_primary = contact.is_primary
|
is_primary = contact.is_primary
|
||||||
contact = frappe.get_doc("Contact", contact.contact).as_dict()
|
contact = frappe.get_doc("Contact", contact.contact).as_dict()
|
||||||
|
|
||||||
def get_primary_email(contact):
|
def get_primary_email(contact):
|
||||||
for email in contact.email_ids:
|
for email in contact.email_ids:
|
||||||
if email.is_primary:
|
if email.is_primary:
|
||||||
return email.email_id
|
return email.email_id
|
||||||
return contact.email_ids[0].email_id if contact.email_ids else ""
|
return contact.email_ids[0].email_id if contact.email_ids else ""
|
||||||
|
|
||||||
def get_primary_mobile_no(contact):
|
def get_primary_mobile_no(contact):
|
||||||
for phone in contact.phone_nos:
|
for phone in contact.phone_nos:
|
||||||
if phone.is_primary:
|
if phone.is_primary:
|
||||||
return phone.phone
|
return phone.phone
|
||||||
return contact.phone_nos[0].phone if contact.phone_nos else ""
|
return contact.phone_nos[0].phone if contact.phone_nos else ""
|
||||||
|
|
||||||
_contact = {
|
_contact = {
|
||||||
"name": contact.name,
|
"name": contact.name,
|
||||||
"image": contact.image,
|
"image": contact.image,
|
||||||
@ -63,4 +47,4 @@ def get_deal_contacts(name):
|
|||||||
"is_primary": is_primary,
|
"is_primary": is_primary,
|
||||||
}
|
}
|
||||||
deal_contacts.append(_contact)
|
deal_contacts.append(_contact)
|
||||||
return deal_contacts
|
return deal_contacts
|
||||||
|
|||||||
@ -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"
|
"options": "Quick Entry\nSide Panel\nData Fields"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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-06-13 15:10:01.612851",
|
"modified": "2024-12-05 13:29:37.021412",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "FCRM",
|
"module": "FCRM",
|
||||||
"name": "CRM Fields Layout",
|
"name": "CRM Fields Layout",
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
@ -10,46 +11,54 @@ from frappe.model.document import Document
|
|||||||
class CRMFieldsLayout(Document):
|
class CRMFieldsLayout(Document):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_fields_layout(doctype: str, type: str):
|
def get_fields_layout(doctype: str, type: str):
|
||||||
sections = []
|
tabs = []
|
||||||
if frappe.db.exists("CRM Fields Layout", {"dt": doctype, "type": type}):
|
if frappe.db.exists("CRM Fields Layout", {"dt": doctype, "type": type}):
|
||||||
layout = frappe.get_doc("CRM Fields Layout", {"dt": doctype, "type": type})
|
layout = frappe.get_doc("CRM Fields Layout", {"dt": doctype, "type": type})
|
||||||
else:
|
else:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
if layout.layout:
|
if layout.layout:
|
||||||
sections = json.loads(layout.layout)
|
tabs = json.loads(layout.layout)
|
||||||
|
|
||||||
|
has_tabs = tabs[0].get("sections") if tabs and tabs[0] else False
|
||||||
|
|
||||||
|
if not has_tabs:
|
||||||
|
tabs = [{"no_tabs": True, "sections": tabs}]
|
||||||
|
|
||||||
allowed_fields = []
|
allowed_fields = []
|
||||||
for section in sections:
|
for tab in tabs:
|
||||||
if not section.get("fields"):
|
for section in tab.get("sections"):
|
||||||
continue
|
if not section.get("fields"):
|
||||||
allowed_fields.extend(section.get("fields"))
|
continue
|
||||||
|
allowed_fields.extend(section.get("fields"))
|
||||||
|
|
||||||
fields = frappe.get_meta(doctype).fields
|
fields = frappe.get_meta(doctype).fields
|
||||||
fields = [field for field in fields if field.fieldname in allowed_fields]
|
fields = [field for field in fields if field.fieldname in allowed_fields]
|
||||||
|
|
||||||
for section in sections:
|
for tab in tabs:
|
||||||
for field in section.get("fields") if section.get("fields") else []:
|
for section in tab.get("sections"):
|
||||||
field = next((f for f in fields if f.fieldname == field), None)
|
for field in section.get("fields") if section.get("fields") else []:
|
||||||
if field:
|
field = next((f for f in fields if f.fieldname == field), None)
|
||||||
if field.fieldtype == "Select" and field.options:
|
if field:
|
||||||
field.options = field.options.split("\n")
|
if field.fieldtype == "Select" and field.options:
|
||||||
field.options = [{"label": _(option), "value": option} for option in field.options]
|
field.options = field.options.split("\n")
|
||||||
field.options.insert(0, {"label": "", "value": ""})
|
field.options = [{"label": _(option), "value": option} for option in field.options]
|
||||||
field = {
|
field.options.insert(0, {"label": "", "value": ""})
|
||||||
"label": _(field.label),
|
field = {
|
||||||
"name": field.fieldname,
|
"label": _(field.label),
|
||||||
"type": field.fieldtype,
|
"name": field.fieldname,
|
||||||
"options": field.options,
|
"type": field.fieldtype,
|
||||||
"mandatory": field.reqd,
|
"options": field.options,
|
||||||
"placeholder": field.get("placeholder"),
|
"mandatory": field.reqd,
|
||||||
"filters": field.get("link_filters")
|
"placeholder": field.get("placeholder"),
|
||||||
}
|
"filters": field.get("link_filters"),
|
||||||
section["fields"][section.get("fields").index(field["name"])] = field
|
}
|
||||||
|
section["fields"][section.get("fields").index(field["name"])] = field
|
||||||
|
|
||||||
return sections or []
|
return tabs or []
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@ -59,11 +68,13 @@ def save_fields_layout(doctype: str, type: str, layout: str):
|
|||||||
else:
|
else:
|
||||||
doc = frappe.new_doc("CRM Fields Layout")
|
doc = frappe.new_doc("CRM Fields Layout")
|
||||||
|
|
||||||
doc.update({
|
doc.update(
|
||||||
"dt": doctype,
|
{
|
||||||
"type": type,
|
"dt": doctype,
|
||||||
"layout": layout,
|
"type": type,
|
||||||
})
|
"layout": layout,
|
||||||
|
}
|
||||||
|
)
|
||||||
doc.save(ignore_permissions=True)
|
doc.save(ignore_permissions=True)
|
||||||
|
|
||||||
return doc.layout
|
return doc.layout
|
||||||
|
|||||||
@ -1,22 +1,14 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
|
||||||
|
|
||||||
from crm.api.doc import get_fields_meta, get_assigned_users
|
from crm.api.doc import get_assigned_users, get_fields_meta
|
||||||
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()
|
||||||
def get_lead(name):
|
def get_lead(name):
|
||||||
Lead = frappe.qb.DocType("CRM Lead")
|
lead = frappe.get_doc("CRM Lead", name).as_dict()
|
||||||
|
|
||||||
query = frappe.qb.from_(Lead).select("*").where(Lead.name == name).limit(1)
|
|
||||||
|
|
||||||
lead = query.run(as_dict=True)
|
|
||||||
if not len(lead):
|
|
||||||
frappe.throw(_("Lead not found"), frappe.DoesNotExistError)
|
|
||||||
lead = lead.pop()
|
|
||||||
|
|
||||||
lead["doctype"] = "CRM Lead"
|
|
||||||
lead["fields_meta"] = get_fields_meta("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
|
||||||
|
|||||||
@ -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.90",
|
"frappe-ui": "^0.1.91",
|
||||||
"gemoji": "^8.1.0",
|
"gemoji": "^8.1.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mime": "^4.0.1",
|
"mime": "^4.0.1",
|
||||||
|
|||||||
@ -364,6 +364,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else-if="title == 'Data'" class="h-full flex flex-col px-3 sm:px-10">
|
||||||
|
<DataFields :doctype="doctype" :docname="doc.data.name" />
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="flex flex-1 flex-col items-center justify-center gap-3 text-xl font-medium text-ink-gray-4"
|
class="flex flex-1 flex-col items-center justify-center gap-3 text-xl font-medium text-ink-gray-4"
|
||||||
@ -454,9 +457,11 @@ import CallArea from '@/components/Activities/CallArea.vue'
|
|||||||
import NoteArea from '@/components/Activities/NoteArea.vue'
|
import NoteArea from '@/components/Activities/NoteArea.vue'
|
||||||
import TaskArea from '@/components/Activities/TaskArea.vue'
|
import TaskArea from '@/components/Activities/TaskArea.vue'
|
||||||
import AttachmentArea from '@/components/Activities/AttachmentArea.vue'
|
import AttachmentArea from '@/components/Activities/AttachmentArea.vue'
|
||||||
|
import DataFields from '@/components/Activities/DataFields.vue'
|
||||||
import UserAvatar from '@/components/UserAvatar.vue'
|
import UserAvatar from '@/components/UserAvatar.vue'
|
||||||
import ActivityIcon from '@/components/Icons/ActivityIcon.vue'
|
import ActivityIcon from '@/components/Icons/ActivityIcon.vue'
|
||||||
import Email2Icon from '@/components/Icons/Email2Icon.vue'
|
import Email2Icon from '@/components/Icons/Email2Icon.vue'
|
||||||
|
import DetailsIcon from '@/components/Icons/DetailsIcon.vue'
|
||||||
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
||||||
import NoteIcon from '@/components/Icons/NoteIcon.vue'
|
import NoteIcon from '@/components/Icons/NoteIcon.vue'
|
||||||
import TaskIcon from '@/components/Icons/TaskIcon.vue'
|
import TaskIcon from '@/components/Icons/TaskIcon.vue'
|
||||||
@ -719,6 +724,8 @@ const emptyText = computed(() => {
|
|||||||
text = 'No Email Communications'
|
text = 'No Email Communications'
|
||||||
} else if (title.value == 'Comments') {
|
} else if (title.value == 'Comments') {
|
||||||
text = 'No Comments'
|
text = 'No Comments'
|
||||||
|
} else if (title.value == 'Data') {
|
||||||
|
text = 'No Data'
|
||||||
} else if (title.value == 'Calls') {
|
} else if (title.value == 'Calls') {
|
||||||
text = 'No Call Logs'
|
text = 'No Call Logs'
|
||||||
} else if (title.value == 'Notes') {
|
} else if (title.value == 'Notes') {
|
||||||
@ -739,6 +746,8 @@ const emptyTextIcon = computed(() => {
|
|||||||
icon = Email2Icon
|
icon = Email2Icon
|
||||||
} else if (title.value == 'Comments') {
|
} else if (title.value == 'Comments') {
|
||||||
icon = CommentIcon
|
icon = CommentIcon
|
||||||
|
} else if (title.value == 'Data') {
|
||||||
|
icon = DetailsIcon
|
||||||
} else if (title.value == 'Calls') {
|
} else if (title.value == 'Calls') {
|
||||||
icon = PhoneIcon
|
icon = PhoneIcon
|
||||||
} else if (title.value == 'Notes') {
|
} else if (title.value == 'Notes') {
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
|
v-if="title !== 'Data'"
|
||||||
class="mx-4 my-3 flex items-center justify-between text-lg font-medium sm:mx-10 sm:mb-4 sm:mt-8"
|
class="mx-4 my-3 flex items-center justify-between text-lg font-medium sm:mx-10 sm:mb-4 sm:mt-8"
|
||||||
>
|
>
|
||||||
<div class="flex h-8 items-center text-xl font-semibold text-ink-gray-8">
|
<div class="flex h-8 items-center text-xl font-semibold text-ink-gray-8">
|
||||||
|
|||||||
108
frontend/src/components/Activities/DataFields.vue
Normal file
108
frontend/src/components/Activities/DataFields.vue
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="my-3 flex items-center justify-between text-lg font-medium sm:mb-4 sm:mt-8"
|
||||||
|
>
|
||||||
|
<div class="flex h-8 items-center text-xl font-semibold text-ink-gray-8">
|
||||||
|
{{ __('Data') }}
|
||||||
|
<Badge
|
||||||
|
v-if="data.isDirty"
|
||||||
|
class="ml-3"
|
||||||
|
:label="'Not Saved'"
|
||||||
|
theme="orange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<Button v-if="isManager()" @click="showDataFieldsModal = true">
|
||||||
|
<EditIcon class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
label="Save"
|
||||||
|
:disabled="!data.isDirty"
|
||||||
|
variant="solid"
|
||||||
|
:loading="data.save.loading"
|
||||||
|
@click="saveChanges"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="data.get.loading"
|
||||||
|
class="flex flex-1 flex-col items-center justify-center gap-3 text-xl font-medium text-gray-500"
|
||||||
|
>
|
||||||
|
<LoadingIndicator class="h-6 w-6" />
|
||||||
|
<span>{{ __('Loading...') }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<FieldLayout v-if="tabs.data" :tabs="tabs.data" :data="data.doc" />
|
||||||
|
</div>
|
||||||
|
<DataFieldsModal
|
||||||
|
v-if="showDataFieldsModal"
|
||||||
|
v-model="showDataFieldsModal"
|
||||||
|
:doctype="doctype"
|
||||||
|
@reload="
|
||||||
|
() => {
|
||||||
|
tabs.reload()
|
||||||
|
data.reload()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||||
|
import DataFieldsModal from '@/components/Modals/DataFieldsModal.vue'
|
||||||
|
import FieldLayout from '@/components/FieldLayout.vue'
|
||||||
|
import { Badge, createResource, createDocumentResource } from 'frappe-ui'
|
||||||
|
import LoadingIndicator from '@/components/Icons/LoadingIndicator.vue'
|
||||||
|
import { createToast } from '@/utils'
|
||||||
|
import { usersStore } from '@/stores/users'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
doctype: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
docname: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { isManager } = usersStore()
|
||||||
|
|
||||||
|
const showDataFieldsModal = ref(false)
|
||||||
|
|
||||||
|
const data = createDocumentResource({
|
||||||
|
doctype: props.doctype,
|
||||||
|
name: props.docname,
|
||||||
|
setValue: {
|
||||||
|
onSuccess: () => {
|
||||||
|
data.reload()
|
||||||
|
createToast({
|
||||||
|
title: 'Data Updated',
|
||||||
|
icon: 'check',
|
||||||
|
iconClasses: 'text-green-600',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
createToast({
|
||||||
|
title: 'Error',
|
||||||
|
text: err.messages[0],
|
||||||
|
icon: 'x',
|
||||||
|
iconClasses: 'text-red-600',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const tabs = createResource({
|
||||||
|
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
||||||
|
cache: ['DataFields', props.doctype],
|
||||||
|
params: { doctype: props.doctype, type: 'Data Fields' },
|
||||||
|
auto: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
function saveChanges() {
|
||||||
|
data.save.submit()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5 p-[2px] -m-[2px]">
|
||||||
<label class="block" :class="labelClasses" v-if="attrs.label">
|
<label class="block" :class="labelClasses" v-if="attrs.label">
|
||||||
{{ __(attrs.label) }}
|
{{ __(attrs.label) }}
|
||||||
</label>
|
</label>
|
||||||
@ -34,7 +34,7 @@
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="w-full !justify-start"
|
class="w-full !justify-start"
|
||||||
:label="__('Create New')"
|
:label="__('Create New')"
|
||||||
@click="attrs.onCreate(value, close)"
|
@click="() => attrs.onCreate(value, close)"
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<FeatherIcon name="plus" class="h-4" />
|
<FeatherIcon name="plus" class="h-4" />
|
||||||
|
|||||||
271
frontend/src/components/FieldLayout.vue
Normal file
271
frontend/src/components/FieldLayout.vue
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex flex-col"
|
||||||
|
:class="{
|
||||||
|
'border border-outline-gray-1 rounded-lg': hasTabs,
|
||||||
|
'border-outline-gray-modals': modal && hasTabs,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<Tabs
|
||||||
|
v-model="tabIndex"
|
||||||
|
class="!h-full"
|
||||||
|
:tabs="tabs"
|
||||||
|
v-slot="{ tab }"
|
||||||
|
:tablistClass="
|
||||||
|
!hasTabs ? 'hidden' : modal ? 'border-outline-gray-modals' : ''
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div :class="{ 'my-4 sm:my-6': hasTabs }">
|
||||||
|
<div
|
||||||
|
v-for="(section, i) in tab.sections"
|
||||||
|
:key="section.label"
|
||||||
|
class="section"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="i != 0"
|
||||||
|
class="w-full"
|
||||||
|
:class="[section.hideBorder ? 'mt-4' : 'h-px border-t my-5']"
|
||||||
|
/>
|
||||||
|
<Section
|
||||||
|
class="text-lg font-medium"
|
||||||
|
:class="{ 'px-3 sm:px-5': hasTabs }"
|
||||||
|
:label="section.label"
|
||||||
|
:hideLabel="section.hideLabel"
|
||||||
|
:opened="section.opened"
|
||||||
|
:collapsible="section.collapsible"
|
||||||
|
collapseIconPosition="right"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="grid gap-4"
|
||||||
|
:class="[
|
||||||
|
gridClass(section.columns),
|
||||||
|
{ 'px-3 sm:px-5': hasTabs },
|
||||||
|
{ 'mt-6': !section.hideLabel },
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div v-for="field in section.fields" :key="field.name">
|
||||||
|
<div
|
||||||
|
class="settings-field"
|
||||||
|
v-if="
|
||||||
|
(field.type == 'Check' ||
|
||||||
|
(field.read_only && data[field.name]) ||
|
||||||
|
!field.read_only ||
|
||||||
|
!field.hidden) &&
|
||||||
|
(!field.depends_on || field.display_via_depends_on)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="field.type != 'Check'"
|
||||||
|
class="mb-2 text-sm text-ink-gray-5"
|
||||||
|
>
|
||||||
|
{{ __(field.label) }}
|
||||||
|
<span
|
||||||
|
class="text-ink-red-3"
|
||||||
|
v-if="
|
||||||
|
field.mandatory ||
|
||||||
|
(field.mandatory_depends_on &&
|
||||||
|
field.mandatory_via_depends_on)
|
||||||
|
"
|
||||||
|
>*</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<FormControl
|
||||||
|
v-if="field.read_only && field.type !== 'Check'"
|
||||||
|
type="text"
|
||||||
|
:placeholder="getPlaceholder(field)"
|
||||||
|
v-model="data[field.name]"
|
||||||
|
:disabled="true"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-else-if="field.type === 'Select'"
|
||||||
|
type="select"
|
||||||
|
class="form-control"
|
||||||
|
:class="field.prefix ? 'prefix' : ''"
|
||||||
|
:options="field.options"
|
||||||
|
v-model="data[field.name]"
|
||||||
|
:placeholder="getPlaceholder(field)"
|
||||||
|
>
|
||||||
|
<template v-if="field.prefix" #prefix>
|
||||||
|
<IndicatorIcon :class="field.prefix" />
|
||||||
|
</template>
|
||||||
|
</FormControl>
|
||||||
|
<div
|
||||||
|
v-else-if="field.type == 'Check'"
|
||||||
|
class="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<FormControl
|
||||||
|
class="form-control"
|
||||||
|
type="checkbox"
|
||||||
|
v-model="data[field.name]"
|
||||||
|
@change="(e) => (data[field.name] = e.target.checked)"
|
||||||
|
:disabled="Boolean(field.read_only)"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
class="text-sm text-ink-gray-5"
|
||||||
|
@click="data[field.name] = !data[field.name]"
|
||||||
|
>
|
||||||
|
{{ __(field.label) }}
|
||||||
|
<span class="text-ink-red-3" v-if="field.mandatory"
|
||||||
|
>*</span
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-1" v-else-if="field.type === 'Link'">
|
||||||
|
<Link
|
||||||
|
class="form-control flex-1 truncate"
|
||||||
|
:value="data[field.name]"
|
||||||
|
:doctype="field.options"
|
||||||
|
:filters="field.filters"
|
||||||
|
@change="(v) => (data[field.name] = v)"
|
||||||
|
:placeholder="getPlaceholder(field)"
|
||||||
|
:onCreate="field.create"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
v-if="data[field.name] && field.edit"
|
||||||
|
class="shrink-0"
|
||||||
|
:label="__('Edit')"
|
||||||
|
@click="field.edit(data[field.name])"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<EditIcon class="h-4 w-4" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
v-else-if="field.type === 'User'"
|
||||||
|
class="form-control"
|
||||||
|
:value="getUser(data[field.name]).full_name"
|
||||||
|
:doctype="field.options"
|
||||||
|
:filters="field.filters"
|
||||||
|
@change="(v) => (data[field.name] = v)"
|
||||||
|
:placeholder="getPlaceholder(field)"
|
||||||
|
:hideMe="true"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<UserAvatar
|
||||||
|
class="mr-2"
|
||||||
|
:user="data[field.name]"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #item-prefix="{ option }">
|
||||||
|
<UserAvatar class="mr-2" :user="option.value" size="sm" />
|
||||||
|
</template>
|
||||||
|
<template #item-label="{ option }">
|
||||||
|
<Tooltip :text="option.value">
|
||||||
|
<div class="cursor-pointer">
|
||||||
|
{{ getUser(option.value).full_name }}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</template>
|
||||||
|
</Link>
|
||||||
|
<DateTimePicker
|
||||||
|
v-else-if="field.type === 'Datetime'"
|
||||||
|
v-model="data[field.name]"
|
||||||
|
icon-left=""
|
||||||
|
:formatter="(date) => getFormat(date, '', true, true)"
|
||||||
|
:placeholder="getPlaceholder(field)"
|
||||||
|
input-class="border-none"
|
||||||
|
/>
|
||||||
|
<DatePicker
|
||||||
|
v-else-if="field.type === 'Date'"
|
||||||
|
icon-left=""
|
||||||
|
v-model="data[field.name]"
|
||||||
|
:formatter="(date) => getFormat(date, '', true)"
|
||||||
|
:placeholder="getPlaceholder(field)"
|
||||||
|
input-class="border-none"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-else-if="
|
||||||
|
['Small Text', 'Text', 'Long Text'].includes(field.type)
|
||||||
|
"
|
||||||
|
type="textarea"
|
||||||
|
:placeholder="getPlaceholder(field)"
|
||||||
|
v-model="data[field.name]"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-else-if="['Int'].includes(field.type)"
|
||||||
|
type="number"
|
||||||
|
:placeholder="getPlaceholder(field)"
|
||||||
|
v-model="data[field.name]"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-else
|
||||||
|
type="text"
|
||||||
|
:placeholder="getPlaceholder(field)"
|
||||||
|
v-model="data[field.name]"
|
||||||
|
:disabled="Boolean(field.read_only)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import Section from '@/components/Section.vue'
|
||||||
|
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||||
|
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
|
||||||
|
import UserAvatar from '@/components/UserAvatar.vue'
|
||||||
|
import Link from '@/components/Controls/Link.vue'
|
||||||
|
import { usersStore } from '@/stores/users'
|
||||||
|
import { getFormat } from '@/utils'
|
||||||
|
import { Tabs, Tooltip, DatePicker, DateTimePicker } from 'frappe-ui'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
|
const { getUser } = usersStore()
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
tabs: Array,
|
||||||
|
data: Object,
|
||||||
|
modal: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasTabs = computed(() => !props.tabs[0].no_tabs)
|
||||||
|
|
||||||
|
const tabIndex = ref(0)
|
||||||
|
|
||||||
|
function gridClass(columns) {
|
||||||
|
columns = columns || 3
|
||||||
|
let griColsMap = {
|
||||||
|
1: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-1',
|
||||||
|
2: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-2',
|
||||||
|
3: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3',
|
||||||
|
4: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-4',
|
||||||
|
}
|
||||||
|
return griColsMap[columns]
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPlaceholder = (field) => {
|
||||||
|
if (field.placeholder) {
|
||||||
|
return __(field.placeholder)
|
||||||
|
}
|
||||||
|
if (['Select', 'Link'].includes(field.type)) {
|
||||||
|
return __('Select {0}', [__(field.label)])
|
||||||
|
} else {
|
||||||
|
return __('Enter {0}', [__(field.label)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:deep(.form-control.prefix select) {
|
||||||
|
padding-left: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section:has(.settings-field) {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
366
frontend/src/components/FieldLayoutEditor.vue
Normal file
366
frontend/src/components/FieldLayoutEditor.vue
Normal file
@ -0,0 +1,366 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-5.5">
|
||||||
|
<div
|
||||||
|
class="flex justify-between items-center gap-1 text-base bg-surface-gray-2 rounded py-2 px-2.5"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<Draggable
|
||||||
|
v-if="tabs.length && !tabs[tabIndex].no_tabs"
|
||||||
|
:list="tabs"
|
||||||
|
item-key="label"
|
||||||
|
class="flex items-center gap-1"
|
||||||
|
@end="(e) => (tabIndex = e.newIndex)"
|
||||||
|
>
|
||||||
|
<template #item="{ element: tab, index: i }">
|
||||||
|
<div
|
||||||
|
class="cursor-pointer rounded"
|
||||||
|
:class="[
|
||||||
|
tabIndex == i
|
||||||
|
? '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',
|
||||||
|
tab.editingLabel ? 'p-1' : 'px-2 py-1',
|
||||||
|
]"
|
||||||
|
@click="tabIndex = i"
|
||||||
|
>
|
||||||
|
<div @dblclick="() => (tab.editingLabel = true)">
|
||||||
|
<div v-if="!tab.editingLabel" class="flex items-center gap-2">
|
||||||
|
{{ __(tab.label) || __('Untitled') }}
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex gap-1 items-center">
|
||||||
|
<Input
|
||||||
|
v-model="tab.label"
|
||||||
|
@keydown.enter="tab.editingLabel = false"
|
||||||
|
@blur="tab.editingLabel = false"
|
||||||
|
@click.stop
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
v-if="tab.editingLabel"
|
||||||
|
icon="check"
|
||||||
|
variant="ghost"
|
||||||
|
@click="tab.editingLabel = false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Draggable>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
class="!h-6.5 !text-ink-gray-5 hover:!text-ink-gray-9"
|
||||||
|
@click="addTab"
|
||||||
|
:label="__('Add Tab')"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<FeatherIcon name="plus" class="h-4" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</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>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
<div v-show="tabIndex == i" v-for="(tab, i) in tabs" :key="tab.label">
|
||||||
|
<Draggable
|
||||||
|
:list="tab.sections"
|
||||||
|
item-key="label"
|
||||||
|
class="flex flex-col gap-5.5"
|
||||||
|
>
|
||||||
|
<template #item="{ element: section }">
|
||||||
|
<div class="flex flex-col gap-1.5 p-2.5 bg-surface-gray-2 rounded">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div
|
||||||
|
class="flex h-7 max-w-fit cursor-pointer items-center gap-2 text-base font-medium leading-4 text-ink-gray-9"
|
||||||
|
@dblclick="() => (section.editingLabel = true)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="!section.editingLabel"
|
||||||
|
class="flex items-center gap-2"
|
||||||
|
:class="{ 'text-ink-gray-3': section.hideLabel }"
|
||||||
|
>
|
||||||
|
{{ __(section.label) || __('Untitled') }}
|
||||||
|
<FeatherIcon
|
||||||
|
v-if="section.collapsible"
|
||||||
|
name="chevron-down"
|
||||||
|
class="h-4 transition-all duration-300 ease-in-out"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex gap-2 items-center">
|
||||||
|
<Input
|
||||||
|
v-model="section.label"
|
||||||
|
@keydown.enter="section.editingLabel = false"
|
||||||
|
@blur="section.editingLabel = false"
|
||||||
|
@click.stop
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
v-if="section.editingLabel"
|
||||||
|
icon="check"
|
||||||
|
variant="ghost"
|
||||||
|
@click="section.editingLabel = false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Dropdown :options="getSectionOptions(section)">
|
||||||
|
<template #default>
|
||||||
|
<Button variant="ghost">
|
||||||
|
<FeatherIcon name="more-horizontal" class="h-4" />
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
<Draggable
|
||||||
|
:list="section.fields"
|
||||||
|
group="fields"
|
||||||
|
item-key="label"
|
||||||
|
class="grid gap-1.5"
|
||||||
|
:class="gridClass(section.columns)"
|
||||||
|
handle=".cursor-grab"
|
||||||
|
>
|
||||||
|
<template #item="{ element: field }">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<DragVerticalIcon class="h-3.5 cursor-grab" />
|
||||||
|
<div>{{ field.label }}</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
class="!size-4 rounded-sm"
|
||||||
|
icon="x"
|
||||||
|
@click="
|
||||||
|
section.fields.splice(section.fields.indexOf(field), 1)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Draggable>
|
||||||
|
<Autocomplete
|
||||||
|
v-if="fields.data"
|
||||||
|
value=""
|
||||||
|
:options="fields.data"
|
||||||
|
@change="(e) => addField(section, e)"
|
||||||
|
>
|
||||||
|
<template #target="{ togglePopover }">
|
||||||
|
<div class="gap-2 w-full">
|
||||||
|
<Button
|
||||||
|
class="w-full !h-8 !bg-surface-modal"
|
||||||
|
variant="outline"
|
||||||
|
@click="togglePopover()"
|
||||||
|
:label="__('Add Field')"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<FeatherIcon name="plus" class="h-4" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Draggable>
|
||||||
|
<div class="mt-5.5">
|
||||||
|
<Button
|
||||||
|
class="w-full h-8"
|
||||||
|
variant="subtle"
|
||||||
|
:label="__('Add Section')"
|
||||||
|
@click="
|
||||||
|
tabs[tabIndex].sections.push({
|
||||||
|
label: __('New Section'),
|
||||||
|
opened: true,
|
||||||
|
fields: [],
|
||||||
|
})
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<FeatherIcon name="plus" class="h-4" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</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 { Dropdown, createResource } from 'frappe-ui'
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
tabs: Object,
|
||||||
|
doctype: String,
|
||||||
|
})
|
||||||
|
|
||||||
|
const tabIndex = ref(0)
|
||||||
|
|
||||||
|
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 addTab() {
|
||||||
|
if (props.tabs.length == 1 && props.tabs[0].no_tabs) {
|
||||||
|
delete props.tabs[0].no_tabs
|
||||||
|
return
|
||||||
|
}
|
||||||
|
props.tabs.push({ label: __('New Tab'), sections: [] })
|
||||||
|
tabIndex.value = props.tabs.length ? props.tabs.length - 1 : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function addField(section, field) {
|
||||||
|
if (!field) return
|
||||||
|
section.fields.push(field)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTabOptions(tab) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: 'Edit',
|
||||||
|
icon: 'edit',
|
||||||
|
onClick: () => (tab.editingLabel = true),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Remove tab',
|
||||||
|
icon: 'trash-2',
|
||||||
|
onClick: () => {
|
||||||
|
if (props.tabs.length == 1) {
|
||||||
|
props.tabs[0].no_tabs = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
props.tabs.splice(tabIndex.value, 1)
|
||||||
|
tabIndex.value = tabIndex.value ? tabIndex.value - 1 : 0
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSectionOptions(section) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: 'Edit',
|
||||||
|
icon: 'edit',
|
||||||
|
onClick: () => (section.editingLabel = true),
|
||||||
|
condition: () => section.editable !== false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: section.collapsible ? 'Uncollapsible' : 'Collapsible',
|
||||||
|
icon: section.collapsible ? 'chevron-up' : 'chevron-down',
|
||||||
|
onClick: () => (section.collapsible = !section.collapsible),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: section.hideLabel ? 'Show label' : 'Hide label',
|
||||||
|
icon: section.hideLabel ? 'eye' : 'eye-off',
|
||||||
|
onClick: () => (section.hideLabel = !section.hideLabel),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: section.hideBorder ? 'Show border' : 'Hide border',
|
||||||
|
icon: 'minus',
|
||||||
|
onClick: () => (section.hideBorder = !section.hideBorder),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Add column',
|
||||||
|
icon: 'columns',
|
||||||
|
onClick: () =>
|
||||||
|
(section.columns = section.columns ? section.columns + 1 : 4),
|
||||||
|
condition: () => !section.columns || section.columns < 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Remove column',
|
||||||
|
icon: 'columns',
|
||||||
|
onClick: () =>
|
||||||
|
(section.columns = section.columns ? section.columns - 1 : 2),
|
||||||
|
condition: () => !section.columns || section.columns > 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Remove section',
|
||||||
|
icon: 'trash-2',
|
||||||
|
onClick: () => {
|
||||||
|
let currentTab = props.tabs[tabIndex.value]
|
||||||
|
currentTab.sections.splice(currentTab.sections.indexOf(section), 1)
|
||||||
|
},
|
||||||
|
condition: () => section.editable !== false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Move to previous tab',
|
||||||
|
icon: 'trash-2',
|
||||||
|
onClick: () => {
|
||||||
|
let previousTab = props.tabs[tabIndex.value - 1]
|
||||||
|
previousTab.sections.push(section)
|
||||||
|
props.tabs[tabIndex.value].sections.splice(
|
||||||
|
props.tabs[tabIndex.value].sections.indexOf(section),
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
tabIndex.value -= 1
|
||||||
|
},
|
||||||
|
condition: () =>
|
||||||
|
section.editable !== false && props.tabs[tabIndex.value - 1],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Move to next tab',
|
||||||
|
icon: 'trash-2',
|
||||||
|
onClick: () => {
|
||||||
|
let nextTab = props.tabs[tabIndex.value + 1]
|
||||||
|
nextTab.sections.push(section)
|
||||||
|
props.tabs[tabIndex.value].sections.splice(
|
||||||
|
props.tabs[tabIndex.value].sections.indexOf(section),
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
tabIndex.value += 1
|
||||||
|
},
|
||||||
|
condition: () =>
|
||||||
|
section.editable !== false && props.tabs[tabIndex.value + 1],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
function gridClass(columns) {
|
||||||
|
columns = columns || 3
|
||||||
|
let griColsMap = {
|
||||||
|
1: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-1',
|
||||||
|
2: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-2',
|
||||||
|
3: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3',
|
||||||
|
4: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-4',
|
||||||
|
}
|
||||||
|
return griColsMap[columns]
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.doctype,
|
||||||
|
() => fields.fetch(params.value),
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
</script>
|
||||||
@ -1,217 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex flex-col gap-4">
|
|
||||||
<div
|
|
||||||
v-for="section in sections"
|
|
||||||
:key="section.label"
|
|
||||||
class="section first:border-t-0 border-outline-gray-modals first:pt-0"
|
|
||||||
:class="section.hideBorder ? '' : 'border-t pt-4'"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="!section.hideLabel"
|
|
||||||
class="flex h-7 mb-3 max-w-fit cursor-pointer items-center gap-2 text-base font-semibold leading-5"
|
|
||||||
>
|
|
||||||
{{ section.label }}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="grid gap-4"
|
|
||||||
:class="
|
|
||||||
section.columns
|
|
||||||
? 'grid-cols-' + section.columns
|
|
||||||
: 'grid-cols-2 sm:grid-cols-3'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<div v-for="field in section.fields" :key="field.name">
|
|
||||||
<div
|
|
||||||
class="settings-field"
|
|
||||||
v-if="
|
|
||||||
(field.type == 'Check' ||
|
|
||||||
(field.read_only && data[field.name]) ||
|
|
||||||
!field.read_only ||
|
|
||||||
!field.hidden) &&
|
|
||||||
(!field.depends_on || field.display_via_depends_on)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="field.type != 'Check'"
|
|
||||||
class="mb-2 text-sm text-ink-gray-5"
|
|
||||||
>
|
|
||||||
{{ __(field.label) }}
|
|
||||||
<span
|
|
||||||
class="text-ink-red-3"
|
|
||||||
v-if="
|
|
||||||
field.mandatory ||
|
|
||||||
(field.mandatory_depends_on && field.mandatory_via_depends_on)
|
|
||||||
"
|
|
||||||
>*</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<FormControl
|
|
||||||
v-if="field.read_only && field.type !== 'Check'"
|
|
||||||
type="text"
|
|
||||||
:placeholder="getPlaceholder(field)"
|
|
||||||
v-model="data[field.name]"
|
|
||||||
:disabled="true"
|
|
||||||
/>
|
|
||||||
<FormControl
|
|
||||||
v-else-if="field.type === 'Select'"
|
|
||||||
type="select"
|
|
||||||
class="form-control"
|
|
||||||
:class="field.prefix ? 'prefix' : ''"
|
|
||||||
:options="field.options"
|
|
||||||
v-model="data[field.name]"
|
|
||||||
:placeholder="getPlaceholder(field)"
|
|
||||||
>
|
|
||||||
<template v-if="field.prefix" #prefix>
|
|
||||||
<IndicatorIcon :class="field.prefix" />
|
|
||||||
</template>
|
|
||||||
</FormControl>
|
|
||||||
<div
|
|
||||||
v-else-if="field.type == 'Check'"
|
|
||||||
class="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<FormControl
|
|
||||||
class="form-control"
|
|
||||||
type="checkbox"
|
|
||||||
v-model="data[field.name]"
|
|
||||||
@change="(e) => (data[field.name] = e.target.checked)"
|
|
||||||
:disabled="Boolean(field.read_only)"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
class="text-sm text-ink-gray-5"
|
|
||||||
@click="data[field.name] = !data[field.name]"
|
|
||||||
>
|
|
||||||
{{ __(field.label) }}
|
|
||||||
<span class="text-ink-red-3" v-if="field.mandatory">*</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-1" v-else-if="field.type === 'Link'">
|
|
||||||
<Link
|
|
||||||
class="form-control flex-1"
|
|
||||||
:value="data[field.name]"
|
|
||||||
:doctype="field.options"
|
|
||||||
:filters="field.filters"
|
|
||||||
@change="(v) => (data[field.name] = v)"
|
|
||||||
:placeholder="getPlaceholder(field)"
|
|
||||||
:onCreate="field.create"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
v-if="data[field.name] && field.edit"
|
|
||||||
class="shrink-0"
|
|
||||||
:label="__('Edit')"
|
|
||||||
@click="field.edit(data[field.name])"
|
|
||||||
>
|
|
||||||
<template #prefix>
|
|
||||||
<EditIcon class="h-4 w-4" />
|
|
||||||
</template>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Link
|
|
||||||
v-else-if="field.type === 'User'"
|
|
||||||
class="form-control"
|
|
||||||
:value="getUser(data[field.name]).full_name"
|
|
||||||
:doctype="field.options"
|
|
||||||
:filters="field.filters"
|
|
||||||
@change="(v) => (data[field.name] = v)"
|
|
||||||
:placeholder="getPlaceholder(field)"
|
|
||||||
:hideMe="true"
|
|
||||||
>
|
|
||||||
<template #prefix>
|
|
||||||
<UserAvatar class="mr-2" :user="data[field.name]" size="sm" />
|
|
||||||
</template>
|
|
||||||
<template #item-prefix="{ option }">
|
|
||||||
<UserAvatar class="mr-2" :user="option.value" size="sm" />
|
|
||||||
</template>
|
|
||||||
<template #item-label="{ option }">
|
|
||||||
<Tooltip :text="option.value">
|
|
||||||
<div class="cursor-pointer">
|
|
||||||
{{ getUser(option.value).full_name }}
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</template>
|
|
||||||
</Link>
|
|
||||||
<DateTimePicker
|
|
||||||
v-else-if="field.type === 'Datetime'"
|
|
||||||
v-model="data[field.name]"
|
|
||||||
icon-left=""
|
|
||||||
:formatter="(date) => getFormat(date, '', true, true)"
|
|
||||||
:placeholder="getPlaceholder(field)"
|
|
||||||
input-class="border-none"
|
|
||||||
/>
|
|
||||||
<DatePicker
|
|
||||||
v-else-if="field.type === 'Date'"
|
|
||||||
icon-left=""
|
|
||||||
v-model="data[field.name]"
|
|
||||||
:formatter="(date) => getFormat(date, '', true)"
|
|
||||||
:placeholder="getPlaceholder(field)"
|
|
||||||
input-class="border-none"
|
|
||||||
/>
|
|
||||||
<FormControl
|
|
||||||
v-else-if="
|
|
||||||
['Small Text', 'Text', 'Long Text'].includes(field.type)
|
|
||||||
"
|
|
||||||
type="textarea"
|
|
||||||
:placeholder="getPlaceholder(field)"
|
|
||||||
v-model="data[field.name]"
|
|
||||||
/>
|
|
||||||
<FormControl
|
|
||||||
v-else-if="['Int'].includes(field.type)"
|
|
||||||
type="number"
|
|
||||||
:placeholder="getPlaceholder(field)"
|
|
||||||
v-model="data[field.name]"
|
|
||||||
/>
|
|
||||||
<FormControl
|
|
||||||
v-else
|
|
||||||
type="text"
|
|
||||||
:placeholder="getPlaceholder(field)"
|
|
||||||
v-model="data[field.name]"
|
|
||||||
:disabled="Boolean(field.read_only)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
|
||||||
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
|
|
||||||
import UserAvatar from '@/components/UserAvatar.vue'
|
|
||||||
import Link from '@/components/Controls/Link.vue'
|
|
||||||
import { usersStore } from '@/stores/users'
|
|
||||||
import { getFormat } from '@/utils'
|
|
||||||
import { Tooltip, DatePicker, DateTimePicker } from 'frappe-ui'
|
|
||||||
|
|
||||||
const { getUser } = usersStore()
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
sections: Array,
|
|
||||||
data: Object,
|
|
||||||
})
|
|
||||||
|
|
||||||
const getPlaceholder = (field) => {
|
|
||||||
if (field.placeholder) {
|
|
||||||
return __(field.placeholder)
|
|
||||||
}
|
|
||||||
if (['Select', 'Link'].includes(field.type)) {
|
|
||||||
return __('Select {0}', [__(field.label)])
|
|
||||||
} else {
|
|
||||||
return __('Enter {0}', [__(field.label)])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
:deep(.form-control.prefix select) {
|
|
||||||
padding-left: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section:has(.settings-field) {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -37,7 +37,7 @@
|
|||||||
<Section
|
<Section
|
||||||
:label="view.name"
|
:label="view.name"
|
||||||
:hideLabel="view.hideLabel"
|
:hideLabel="view.hideLabel"
|
||||||
:isOpened="view.opened"
|
:opened="view.opened"
|
||||||
>
|
>
|
||||||
<template #header="{ opened, hide, toggle }">
|
<template #header="{ opened, hide, toggle }">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -38,7 +38,7 @@
|
|||||||
<Section
|
<Section
|
||||||
:label="view.name"
|
:label="view.name"
|
||||||
:hideLabel="view.hideLabel"
|
:hideLabel="view.hideLabel"
|
||||||
:isOpened="view.opened"
|
:opened="view.opened"
|
||||||
>
|
>
|
||||||
<template #header="{ opened, hide, toggle }">
|
<template #header="{ opened, hide, toggle }">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -22,8 +22,8 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="sections.data">
|
<div v-if="tabs.data">
|
||||||
<Fields :sections="sections.data" :data="_address" />
|
<FieldLayout :tabs="tabs.data" :data="_address" />
|
||||||
<ErrorMessage class="mt-2" :message="error" />
|
<ErrorMessage class="mt-2" :message="error" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -50,7 +50,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import QuickEntryModal from '@/components/Modals/QuickEntryModal.vue'
|
import QuickEntryModal from '@/components/Modals/QuickEntryModal.vue'
|
||||||
import Fields from '@/components/Fields.vue'
|
import FieldLayout from '@/components/FieldLayout.vue'
|
||||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
import { capture } from '@/telemetry'
|
import { capture } from '@/telemetry'
|
||||||
@ -106,9 +106,9 @@ const dialogOptions = computed(() => {
|
|||||||
return { title, size, actions }
|
return { title, size, actions }
|
||||||
})
|
})
|
||||||
|
|
||||||
const sections = createResource({
|
const tabs = createResource({
|
||||||
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
||||||
cache: ['quickEntryFields', 'Address'],
|
cache: ['QuickEntry', 'Address'],
|
||||||
params: { doctype: 'Address', type: 'Quick Entry' },
|
params: { doctype: 'Address', type: 'Quick Entry' },
|
||||||
auto: true,
|
auto: true,
|
||||||
})
|
})
|
||||||
|
|||||||
@ -10,10 +10,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<Button
|
<Button
|
||||||
v-if="isManager() || detailMode"
|
v-if="isManager()"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="w-7"
|
class="w-7"
|
||||||
@click="detailMode ? (detailMode = false) : openQuickEntryModal()"
|
@click="openQuickEntryModal"
|
||||||
>
|
>
|
||||||
<EditIcon class="h-4 w-4" />
|
<EditIcon class="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@ -22,77 +22,36 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div v-if="filteredSections.length">
|
||||||
<div v-if="detailMode" class="flex flex-col gap-3.5">
|
<FieldLayout :tabs="filteredSections" :data="_contact" />
|
||||||
<div
|
|
||||||
v-for="field in detailFields"
|
|
||||||
:key="field.name"
|
|
||||||
class="flex h-7 items-center gap-2 text-base text-ink-gray-8"
|
|
||||||
>
|
|
||||||
<div class="grid w-7 place-content-center">
|
|
||||||
<component :is="field.icon" />
|
|
||||||
</div>
|
|
||||||
<div v-if="field.type == 'dropdown'">
|
|
||||||
<Dropdown
|
|
||||||
:options="field.options"
|
|
||||||
class="form-control -ml-2 mr-2 w-full flex-1"
|
|
||||||
>
|
|
||||||
<template #default="{ open }">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
:label="contact.data[field.name]"
|
|
||||||
class="dropdown-button w-full justify-between truncate hover:bg-surface-white"
|
|
||||||
>
|
|
||||||
<div class="truncate">{{ contact.data[field.name] }}</div>
|
|
||||||
<template #suffix>
|
|
||||||
<FeatherIcon
|
|
||||||
:name="open ? 'chevron-up' : 'chevron-down'"
|
|
||||||
class="h-4 text-ink-gray-5"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</Button>
|
|
||||||
</template>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
<div v-else>{{ field.value }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Fields
|
|
||||||
v-else-if="filteredSections"
|
|
||||||
:sections="filteredSections"
|
|
||||||
:data="_contact"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!detailMode" class="px-4 pb-7 pt-4 sm:px-6">
|
<div class="px-4 pb-7 pt-4 sm:px-6">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Button
|
<Button
|
||||||
class="w-full"
|
class="w-full"
|
||||||
v-for="action in dialogOptions.actions"
|
v-for="action in dialogOptions.actions"
|
||||||
:key="action.label"
|
:key="action.label"
|
||||||
v-bind="action"
|
v-bind="action"
|
||||||
>
|
:label="__(action.label)"
|
||||||
{{ __(action.label) }}
|
/>
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
<AddressModal v-model="showAddressModal" v-model:address="_address" />
|
<AddressModal v-model="showAddressModal" v-model:address="_address" />
|
||||||
|
<QuickEntryModal
|
||||||
|
v-if="showQuickEntryModal"
|
||||||
|
v-model="showQuickEntryModal"
|
||||||
|
doctype="Contact"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import Fields from '@/components/Fields.vue'
|
import QuickEntryModal from '@/components/Modals/QuickEntryModal.vue'
|
||||||
|
import FieldLayout from '@/components/FieldLayout.vue'
|
||||||
import AddressModal from '@/components/Modals/AddressModal.vue'
|
import AddressModal from '@/components/Modals/AddressModal.vue'
|
||||||
import ContactIcon from '@/components/Icons/ContactIcon.vue'
|
|
||||||
import GenderIcon from '@/components/Icons/GenderIcon.vue'
|
|
||||||
import Email2Icon from '@/components/Icons/Email2Icon.vue'
|
|
||||||
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
|
||||||
import OrganizationsIcon from '@/components/Icons/OrganizationsIcon.vue'
|
|
||||||
import AddressIcon from '@/components/Icons/AddressIcon.vue'
|
|
||||||
import CertificateIcon from '@/components/Icons/CertificateIcon.vue'
|
|
||||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||||
import Dropdown from '@/components/frappe-ui/Dropdown.vue'
|
|
||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
import { capture } from '@/telemetry'
|
import { capture } from '@/telemetry'
|
||||||
import { call, createResource } from 'frappe-ui'
|
import { call, createResource } from 'frappe-ui'
|
||||||
@ -108,7 +67,6 @@ const props = defineProps({
|
|||||||
type: Object,
|
type: Object,
|
||||||
default: {
|
default: {
|
||||||
redirect: true,
|
redirect: true,
|
||||||
detailMode: false,
|
|
||||||
afterInsert: () => {},
|
afterInsert: () => {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -119,7 +77,6 @@ const { isManager } = usersStore()
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const show = defineModel()
|
const show = defineModel()
|
||||||
|
|
||||||
const detailMode = ref(false)
|
|
||||||
const editMode = ref(false)
|
const editMode = ref(false)
|
||||||
let _contact = ref({})
|
let _contact = ref({})
|
||||||
let _address = ref({})
|
let _address = ref({})
|
||||||
@ -186,74 +143,28 @@ function handleContactUpdate(doc) {
|
|||||||
const dialogOptions = computed(() => {
|
const dialogOptions = computed(() => {
|
||||||
let title = !editMode.value ? 'New Contact' : _contact.value.full_name
|
let title = !editMode.value ? 'New Contact' : _contact.value.full_name
|
||||||
|
|
||||||
let size = detailMode.value ? '' : 'xl'
|
let size = 'xl'
|
||||||
let actions = detailMode.value
|
let actions = [
|
||||||
? []
|
{
|
||||||
: [
|
label: editMode.value ? 'Save' : 'Create',
|
||||||
{
|
variant: 'solid',
|
||||||
label: editMode.value ? 'Save' : 'Create',
|
disabled: !dirty.value,
|
||||||
variant: 'solid',
|
onClick: () => (editMode.value ? updateContact() : callInsertDoc()),
|
||||||
disabled: !dirty.value,
|
},
|
||||||
onClick: () => (editMode.value ? updateContact() : callInsertDoc()),
|
]
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
return { title, size, actions }
|
return { title, size, actions }
|
||||||
})
|
})
|
||||||
|
|
||||||
const detailFields = computed(() => {
|
const tabs = createResource({
|
||||||
let details = [
|
|
||||||
{
|
|
||||||
icon: ContactIcon,
|
|
||||||
name: 'full_name',
|
|
||||||
value:
|
|
||||||
(_contact.value.salutation ? _contact.value.salutation + '. ' : '') +
|
|
||||||
_contact.value.full_name,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: GenderIcon,
|
|
||||||
name: 'gender',
|
|
||||||
value: _contact.value.gender,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: Email2Icon,
|
|
||||||
name: 'email_id',
|
|
||||||
value: _contact.value.email_id,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: PhoneIcon,
|
|
||||||
name: 'mobile_no',
|
|
||||||
value: _contact.value.actual_mobile_no,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: OrganizationsIcon,
|
|
||||||
name: 'company_name',
|
|
||||||
value: _contact.value.company_name,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: CertificateIcon,
|
|
||||||
name: 'designation',
|
|
||||||
value: _contact.value.designation,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: AddressIcon,
|
|
||||||
name: 'address',
|
|
||||||
value: _contact.value.address,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
return details.filter((detail) => detail.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
const sections = createResource({
|
|
||||||
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
||||||
cache: ['quickEntryFields', 'Contact'],
|
cache: ['QuickEntry', 'Contact'],
|
||||||
params: { doctype: 'Contact', type: 'Quick Entry' },
|
params: { doctype: 'Contact', type: 'Quick Entry' },
|
||||||
auto: true,
|
auto: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const filteredSections = computed(() => {
|
const filteredSections = computed(() => {
|
||||||
let allSections = sections.data || []
|
let allSections = tabs.data?.[0]?.sections || []
|
||||||
if (!allSections.length) return []
|
if (!allSections.length) return []
|
||||||
|
|
||||||
allSections.forEach((s) => {
|
allSections.forEach((s) => {
|
||||||
@ -276,7 +187,7 @@ const filteredSections = computed(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return allSections
|
return [{ no_tabs: true, sections: allSections }]
|
||||||
})
|
})
|
||||||
|
|
||||||
const dirty = computed(() => {
|
const dirty = computed(() => {
|
||||||
@ -287,7 +198,6 @@ watch(
|
|||||||
() => show.value,
|
() => show.value,
|
||||||
(value) => {
|
(value) => {
|
||||||
if (!value) return
|
if (!value) return
|
||||||
detailMode.value = props.options.detailMode
|
|
||||||
editMode.value = false
|
editMode.value = false
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
_contact.value = { ...props.contact.data }
|
_contact.value = { ...props.contact.data }
|
||||||
@ -298,13 +208,11 @@ watch(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
const showQuickEntryModal = defineModel('quickEntry')
|
const showQuickEntryModal = ref(false)
|
||||||
|
|
||||||
function openQuickEntryModal() {
|
function openQuickEntryModal() {
|
||||||
showQuickEntryModal.value = true
|
showQuickEntryModal.value = true
|
||||||
nextTick(() => {
|
nextTick(() => (show.value = false))
|
||||||
show.value = false
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
135
frontend/src/components/Modals/DataFieldsModal.vue
Normal file
135
frontend/src/components/Modals/DataFieldsModal.vue
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
<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 Data 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">
|
||||||
|
<FormControl
|
||||||
|
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')"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
<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>
|
||||||
|
</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, Switch, 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: 'Data Fields' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabs = createResource({
|
||||||
|
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
||||||
|
cache: ['DataFieldsModal', _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: 'Data Fields',
|
||||||
|
layout: JSON.stringify(_tabs),
|
||||||
|
},
|
||||||
|
).then(() => {
|
||||||
|
loading.value = false
|
||||||
|
show.value = false
|
||||||
|
capture('data_fields_layout_builder', { doctype: _doctype.value })
|
||||||
|
emit('reload')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -33,10 +33,10 @@
|
|||||||
<Switch v-model="chooseExistingContact" />
|
<Switch v-model="chooseExistingContact" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Fields
|
<div class="h-px w-full border-t my-5" />
|
||||||
v-if="filteredSections"
|
<FieldLayout
|
||||||
class="border-t pt-4"
|
v-if="filteredSections.length"
|
||||||
:sections="filteredSections"
|
:tabs="filteredSections"
|
||||||
:data="deal"
|
:data="deal"
|
||||||
/>
|
/>
|
||||||
<ErrorMessage class="mt-4" v-if="error" :message="__(error)" />
|
<ErrorMessage class="mt-4" v-if="error" :message="__(error)" />
|
||||||
@ -58,7 +58,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||||
import Fields from '@/components/Fields.vue'
|
import FieldLayout from '@/components/FieldLayout.vue'
|
||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
import { statusesStore } from '@/stores/statuses'
|
import { statusesStore } from '@/stores/statuses'
|
||||||
import { capture } from '@/telemetry'
|
import { capture } from '@/telemetry'
|
||||||
@ -100,28 +100,30 @@ const isDealCreating = ref(false)
|
|||||||
const chooseExistingContact = ref(false)
|
const chooseExistingContact = ref(false)
|
||||||
const chooseExistingOrganization = ref(false)
|
const chooseExistingOrganization = ref(false)
|
||||||
|
|
||||||
const sections = createResource({
|
const tabs = createResource({
|
||||||
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
||||||
cache: ['quickEntryFields', 'CRM Deal'],
|
cache: ['QuickEntry', 'CRM Deal'],
|
||||||
params: { doctype: 'CRM Deal', type: 'Quick Entry' },
|
params: { doctype: 'CRM Deal', type: 'Quick Entry' },
|
||||||
auto: true,
|
auto: true,
|
||||||
transform: (data) => {
|
transform: (_tabs) => {
|
||||||
return data.forEach((section) => {
|
return _tabs.forEach((tab) => {
|
||||||
section.fields.forEach((field) => {
|
tab.sections.forEach((section) => {
|
||||||
if (field.name == 'status') {
|
section.fields.forEach((field) => {
|
||||||
field.type = 'Select'
|
if (field.name == 'status') {
|
||||||
field.options = dealStatuses.value
|
field.type = 'Select'
|
||||||
field.prefix = getDealStatus(deal.status).iconColorClass
|
field.options = dealStatuses.value
|
||||||
} else if (field.name == 'deal_owner') {
|
field.prefix = getDealStatus(deal.status).iconColorClass
|
||||||
field.type = 'User'
|
} else if (field.name == 'deal_owner') {
|
||||||
}
|
field.type = 'User'
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const filteredSections = computed(() => {
|
const filteredSections = computed(() => {
|
||||||
let allSections = sections.data || []
|
let allSections = tabs.data?.[0]?.sections || []
|
||||||
if (!allSections.length) return []
|
if (!allSections.length) return []
|
||||||
|
|
||||||
let _filteredSections = []
|
let _filteredSections = []
|
||||||
@ -159,7 +161,7 @@ const filteredSections = computed(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return _filteredSections
|
return [{ no_tabs: true, sections: _filteredSections }]
|
||||||
})
|
})
|
||||||
|
|
||||||
const dealStatuses = computed(() => {
|
const dealStatuses = computed(() => {
|
||||||
|
|||||||
@ -23,7 +23,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Fields v-if="sections.data" :sections="sections.data" :data="lead" />
|
<FieldLayout v-if="tabs.data" :tabs="tabs.data" :data="lead" />
|
||||||
<ErrorMessage class="mt-4" v-if="error" :message="__(error)" />
|
<ErrorMessage class="mt-4" v-if="error" :message="__(error)" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -43,7 +43,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||||
import Fields from '@/components/Fields.vue'
|
import FieldLayout from '@/components/FieldLayout.vue'
|
||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
import { statusesStore } from '@/stores/statuses'
|
import { statusesStore } from '@/stores/statuses'
|
||||||
import { capture } from '@/telemetry'
|
import { capture } from '@/telemetry'
|
||||||
@ -63,21 +63,23 @@ const router = useRouter()
|
|||||||
const error = ref(null)
|
const error = ref(null)
|
||||||
const isLeadCreating = ref(false)
|
const isLeadCreating = ref(false)
|
||||||
|
|
||||||
const sections = createResource({
|
const tabs = createResource({
|
||||||
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
||||||
cache: ['quickEntryFields', 'CRM Lead'],
|
cache: ['QuickEntry', 'CRM Lead'],
|
||||||
params: { doctype: 'CRM Lead', type: 'Quick Entry' },
|
params: { doctype: 'CRM Lead', type: 'Quick Entry' },
|
||||||
auto: true,
|
auto: true,
|
||||||
transform: (data) => {
|
transform: (_tabs) => {
|
||||||
return data.forEach((section) => {
|
return _tabs.forEach((tab) => {
|
||||||
section.fields.forEach((field) => {
|
tab.sections.forEach((section) => {
|
||||||
if (field.name == 'status') {
|
section.fields.forEach((field) => {
|
||||||
field.type = 'Select'
|
if (field.name == 'status') {
|
||||||
field.options = leadStatuses.value
|
field.type = 'Select'
|
||||||
field.prefix = getLeadStatus(lead.status).iconColorClass
|
field.options = leadStatuses.value
|
||||||
} else if (field.name == 'lead_owner') {
|
field.prefix = getLeadStatus(lead.status).iconColorClass
|
||||||
field.type = 'User'
|
} else if (field.name == 'lead_owner') {
|
||||||
}
|
field.type = 'User'
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@ -10,10 +10,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<Button
|
<Button
|
||||||
v-if="isManager() || detailMode"
|
v-if="isManager()"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="w-7"
|
class="w-7"
|
||||||
@click="detailMode ? (detailMode = false) : openQuickEntryModal()"
|
@click="openQuickEntryModal"
|
||||||
>
|
>
|
||||||
<EditIcon class="h-4 w-4" />
|
<EditIcon class="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@ -22,27 +22,11 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div v-if="filteredSections.length">
|
||||||
<div v-if="detailMode" class="flex flex-col gap-3.5">
|
<FieldLayout :tabs="filteredSections" :data="_organization" />
|
||||||
<div
|
|
||||||
class="flex h-7 items-center gap-2 text-base text-ink-gray-8"
|
|
||||||
v-for="field in fields"
|
|
||||||
:key="field.name"
|
|
||||||
>
|
|
||||||
<div class="grid w-7 place-content-center">
|
|
||||||
<component :is="field.icon" />
|
|
||||||
</div>
|
|
||||||
<div>{{ field.value }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Fields
|
|
||||||
v-else-if="filteredSections"
|
|
||||||
:sections="filteredSections"
|
|
||||||
:data="_organization"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!detailMode" class="px-4 pb-7 pt-4 sm:px-6">
|
<div class="px-4 pb-7 pt-4 sm:px-6">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Button
|
<Button
|
||||||
class="w-full"
|
class="w-full"
|
||||||
@ -57,21 +41,22 @@
|
|||||||
</template>
|
</template>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
<AddressModal v-model="showAddressModal" v-model:address="_address" />
|
<AddressModal v-model="showAddressModal" v-model:address="_address" />
|
||||||
|
<QuickEntryModal
|
||||||
|
v-if="showQuickEntryModal"
|
||||||
|
v-model="showQuickEntryModal"
|
||||||
|
doctype="CRM Organization"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import Fields from '@/components/Fields.vue'
|
import QuickEntryModal from '@/components/Modals/QuickEntryModal.vue'
|
||||||
|
import FieldLayout from '@/components/FieldLayout.vue'
|
||||||
import AddressModal from '@/components/Modals/AddressModal.vue'
|
import AddressModal from '@/components/Modals/AddressModal.vue'
|
||||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||||
import MoneyIcon from '@/components/Icons/MoneyIcon.vue'
|
|
||||||
import WebsiteIcon from '@/components/Icons/WebsiteIcon.vue'
|
|
||||||
import OrganizationsIcon from '@/components/Icons/OrganizationsIcon.vue'
|
|
||||||
import TerritoryIcon from '@/components/Icons/TerritoryIcon.vue'
|
|
||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
import { formatNumberIntoCurrency } from '@/utils'
|
|
||||||
import { capture } from '@/telemetry'
|
import { capture } from '@/telemetry'
|
||||||
import { call, FeatherIcon, createResource } from 'frappe-ui'
|
import { call, FeatherIcon, createResource } from 'frappe-ui'
|
||||||
import { ref, nextTick, watch, computed, h } from 'vue'
|
import { ref, nextTick, watch, computed } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -79,7 +64,6 @@ const props = defineProps({
|
|||||||
type: Object,
|
type: Object,
|
||||||
default: {
|
default: {
|
||||||
redirect: true,
|
redirect: true,
|
||||||
detailMode: false,
|
|
||||||
afterInsert: () => {},
|
afterInsert: () => {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -93,7 +77,6 @@ const organization = defineModel('organization')
|
|||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const title = ref(null)
|
const title = ref(null)
|
||||||
const detailMode = ref(false)
|
|
||||||
const editMode = ref(false)
|
const editMode = ref(false)
|
||||||
let _address = ref({})
|
let _address = ref({})
|
||||||
let _organization = ref({
|
let _organization = ref({
|
||||||
@ -186,70 +169,27 @@ const dialogOptions = computed(() => {
|
|||||||
let title = !editMode.value
|
let title = !editMode.value
|
||||||
? __('New Organization')
|
? __('New Organization')
|
||||||
: __(_organization.value.organization_name)
|
: __(_organization.value.organization_name)
|
||||||
let size = detailMode.value ? '' : 'xl'
|
let size = 'xl'
|
||||||
let actions = detailMode.value
|
let actions = [
|
||||||
? []
|
{
|
||||||
: [
|
label: editMode.value ? __('Save') : __('Create'),
|
||||||
{
|
variant: 'solid',
|
||||||
label: editMode.value ? __('Save') : __('Create'),
|
onClick: () => (editMode.value ? updateOrganization() : callInsertDoc()),
|
||||||
variant: 'solid',
|
},
|
||||||
onClick: () =>
|
]
|
||||||
editMode.value ? updateOrganization() : callInsertDoc(),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
return { title, size, actions }
|
return { title, size, actions }
|
||||||
})
|
})
|
||||||
|
|
||||||
const fields = computed(() => {
|
const tabs = createResource({
|
||||||
let details = [
|
|
||||||
{
|
|
||||||
icon: OrganizationsIcon,
|
|
||||||
name: 'organization_name',
|
|
||||||
value: _organization.value.organization_name,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: WebsiteIcon,
|
|
||||||
name: 'website',
|
|
||||||
value: _organization.value.website,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: TerritoryIcon,
|
|
||||||
name: 'territory',
|
|
||||||
value: _organization.value.territory,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: MoneyIcon,
|
|
||||||
name: 'annual_revenue',
|
|
||||||
value: formatNumberIntoCurrency(
|
|
||||||
_organization.value.annual_revenue,
|
|
||||||
_organization.value.currency,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: h(FeatherIcon, { name: 'hash', class: 'h-4 w-4' }),
|
|
||||||
name: 'no_of_employees',
|
|
||||||
value: _organization.value.no_of_employees,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: h(FeatherIcon, { name: 'briefcase', class: 'h-4 w-4' }),
|
|
||||||
name: 'industry',
|
|
||||||
value: _organization.value.industry,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
return details.filter((field) => field.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
const sections = createResource({
|
|
||||||
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
||||||
cache: ['quickEntryFields', 'CRM Organization'],
|
cache: ['QuickEntry', 'CRM Organization'],
|
||||||
params: { doctype: 'CRM Organization', type: 'Quick Entry' },
|
params: { doctype: 'CRM Organization', type: 'Quick Entry' },
|
||||||
auto: true,
|
auto: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const filteredSections = computed(() => {
|
const filteredSections = computed(() => {
|
||||||
let allSections = sections.data || []
|
let allSections = tabs.data?.[0]?.sections || []
|
||||||
if (!allSections.length) return []
|
if (!allSections.length) return []
|
||||||
|
|
||||||
allSections.forEach((s) => {
|
allSections.forEach((s) => {
|
||||||
@ -272,7 +212,7 @@ const filteredSections = computed(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return allSections
|
return [{ no_tabs: true, sections: allSections }]
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@ -280,7 +220,6 @@ watch(
|
|||||||
(value) => {
|
(value) => {
|
||||||
if (!value) return
|
if (!value) return
|
||||||
editMode.value = false
|
editMode.value = false
|
||||||
detailMode.value = props.options.detailMode
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
// TODO: Issue with FormControl
|
// TODO: Issue with FormControl
|
||||||
// title.value.el.focus()
|
// title.value.el.focus()
|
||||||
@ -293,12 +232,10 @@ watch(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
const showQuickEntryModal = defineModel('quickEntry')
|
const showQuickEntryModal = ref(false)
|
||||||
|
|
||||||
function openQuickEntryModal() {
|
function openQuickEntryModal() {
|
||||||
showQuickEntryModal.value = true
|
showQuickEntryModal.value = true
|
||||||
nextTick(() => {
|
nextTick(() => (show.value = false))
|
||||||
show.value = false
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -35,13 +35,13 @@
|
|||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="sections?.data">
|
<div v-if="tabs?.data">
|
||||||
<QuickEntryLayoutBuilder
|
<FieldLayoutEditor
|
||||||
v-if="!preview"
|
v-if="!preview"
|
||||||
:sections="sections.data"
|
:tabs="tabs.data"
|
||||||
:doctype="_doctype"
|
:doctype="_doctype"
|
||||||
/>
|
/>
|
||||||
<Fields v-else :sections="sections.data" :data="{}" />
|
<FieldLayout v-else :tabs="tabs.data" :data="{}" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -59,8 +59,8 @@
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import Fields from '@/components/Fields.vue'
|
import FieldLayout from '@/components/FieldLayout.vue'
|
||||||
import QuickEntryLayoutBuilder from '@/components/QuickEntryLayoutBuilder.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, Switch, call, createResource } from 'frappe-ui'
|
||||||
@ -83,20 +83,20 @@ function getParams() {
|
|||||||
return { doctype: _doctype.value, type: 'Quick Entry' }
|
return { doctype: _doctype.value, type: 'Quick Entry' }
|
||||||
}
|
}
|
||||||
|
|
||||||
const sections = createResource({
|
const tabs = createResource({
|
||||||
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
||||||
cache: ['quick-entry-sections', _doctype.value],
|
cache: ['QuickEntryModal', _doctype.value],
|
||||||
params: getParams(),
|
params: getParams(),
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
sections.originalData = JSON.parse(JSON.stringify(data))
|
tabs.originalData = JSON.parse(JSON.stringify(data))
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => sections?.data,
|
() => tabs?.data,
|
||||||
() => {
|
() => {
|
||||||
dirty.value =
|
dirty.value =
|
||||||
JSON.stringify(sections?.data) !== JSON.stringify(sections?.originalData)
|
JSON.stringify(tabs?.data) !== JSON.stringify(tabs?.originalData)
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
)
|
)
|
||||||
@ -105,18 +105,21 @@ onMounted(() => useDebounceFn(reload, 100)())
|
|||||||
|
|
||||||
function reload() {
|
function reload() {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
sections.params = getParams()
|
tabs.params = getParams()
|
||||||
sections.reload()
|
tabs.reload()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveChanges() {
|
function saveChanges() {
|
||||||
let _sections = JSON.parse(JSON.stringify(sections.data))
|
let _tabs = JSON.parse(JSON.stringify(tabs.data))
|
||||||
_sections.forEach((section) => {
|
_tabs.forEach((tab) => {
|
||||||
if (!section.fields) return
|
if (!tab.sections) return
|
||||||
section.fields = section.fields.map(
|
tab.sections.forEach((section) => {
|
||||||
(field) => field.fieldname || field.name,
|
if (!section.fields) return
|
||||||
)
|
section.fields = section.fields.map(
|
||||||
|
(field) => field.fieldname || field.name,
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
loading.value = true
|
loading.value = true
|
||||||
call(
|
call(
|
||||||
@ -124,7 +127,7 @@ function saveChanges() {
|
|||||||
{
|
{
|
||||||
doctype: _doctype.value,
|
doctype: _doctype.value,
|
||||||
type: 'Quick Entry',
|
type: 'Quick Entry',
|
||||||
layout: JSON.stringify(_sections),
|
layout: JSON.stringify(_tabs),
|
||||||
},
|
},
|
||||||
).then(() => {
|
).then(() => {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
|
|||||||
@ -29,21 +29,27 @@
|
|||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="sections.data" class="flex gap-4">
|
<div v-if="tabs.data?.[0]?.sections" class="flex gap-4">
|
||||||
<SidePanelLayoutBuilder
|
<SidePanelLayoutEditor
|
||||||
class="flex flex-1 flex-col pr-2"
|
class="flex flex-1 flex-col pr-2"
|
||||||
:sections="sections.data"
|
:sections="tabs.data[0].sections"
|
||||||
:doctype="_doctype"
|
:doctype="_doctype"
|
||||||
/>
|
/>
|
||||||
<div v-if="preview" class="flex flex-1 flex-col border rounded">
|
<div v-if="preview" class="flex flex-1 flex-col border rounded">
|
||||||
<div
|
<div
|
||||||
v-for="(section, i) in sections.data"
|
v-for="(section, i) in tabs.data[0].sections"
|
||||||
:key="section.label"
|
:key="section.label"
|
||||||
class="flex flex-col py-1.5 px-1"
|
class="flex flex-col py-1.5 px-1"
|
||||||
:class="{ 'border-b': i !== sections.data?.length - 1 }"
|
:class="{
|
||||||
|
'border-b': i !== tabs.data[0].sections?.length - 1,
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<Section :is-opened="section.opened" :label="section.label">
|
<Section
|
||||||
<SectionFields
|
class="p-2"
|
||||||
|
:label="section.label"
|
||||||
|
:opened="section.opened"
|
||||||
|
>
|
||||||
|
<SidePanelLayout
|
||||||
:fields="section.fields"
|
:fields="section.fields"
|
||||||
:isLastSection="i == section.data?.length - 1"
|
:isLastSection="i == section.data?.length - 1"
|
||||||
v-model="data"
|
v-model="data"
|
||||||
@ -75,8 +81,8 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import Section from '@/components/Section.vue'
|
import Section from '@/components/Section.vue'
|
||||||
import SectionFields from '@/components/SectionFields.vue'
|
import SidePanelLayout from '@/components/SidePanelLayout.vue'
|
||||||
import SidePanelLayoutBuilder from '@/components/Settings/SidePanelLayoutBuilder.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, Switch, call, createResource } from 'frappe-ui'
|
||||||
@ -102,20 +108,20 @@ function getParams() {
|
|||||||
return { doctype: _doctype.value, type: 'Side Panel' }
|
return { doctype: _doctype.value, type: 'Side Panel' }
|
||||||
}
|
}
|
||||||
|
|
||||||
const sections = createResource({
|
const tabs = createResource({
|
||||||
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
||||||
cache: ['sidebar-sections', _doctype.value],
|
cache: ['SidePanel', _doctype.value],
|
||||||
params: getParams(),
|
params: getParams(),
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
sections.originalData = JSON.parse(JSON.stringify(data))
|
tabs.originalData = JSON.parse(JSON.stringify(data))
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => sections?.data,
|
() => tabs?.data,
|
||||||
() => {
|
() => {
|
||||||
dirty.value =
|
dirty.value =
|
||||||
JSON.stringify(sections?.data) !== JSON.stringify(sections?.originalData)
|
JSON.stringify(tabs?.data) !== JSON.stringify(tabs?.originalData)
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
)
|
)
|
||||||
@ -124,18 +130,20 @@ onMounted(() => useDebounceFn(reload, 100)())
|
|||||||
|
|
||||||
function reload() {
|
function reload() {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
sections.params = getParams()
|
tabs.params = getParams()
|
||||||
sections.reload()
|
tabs.reload()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveChanges() {
|
function saveChanges() {
|
||||||
let _sections = JSON.parse(JSON.stringify(sections.data))
|
let _tabs = JSON.parse(JSON.stringify(tabs.data))
|
||||||
_sections.forEach((section) => {
|
_tabs.forEach((tab) => {
|
||||||
if (!section.fields) return
|
tab.sections.forEach((section) => {
|
||||||
section.fields = section.fields
|
if (!section.fields) return
|
||||||
.map((field) => field.fieldname || field.name)
|
section.fields = section.fields
|
||||||
.filter(Boolean)
|
.map((field) => field.fieldname || field.name)
|
||||||
|
.filter(Boolean)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
loading.value = true
|
loading.value = true
|
||||||
call(
|
call(
|
||||||
@ -143,7 +151,7 @@ function saveChanges() {
|
|||||||
{
|
{
|
||||||
doctype: _doctype.value,
|
doctype: _doctype.value,
|
||||||
type: 'Side Panel',
|
type: 'Side Panel',
|
||||||
layout: JSON.stringify(_sections),
|
layout: JSON.stringify(_tabs),
|
||||||
},
|
},
|
||||||
).then(() => {
|
).then(() => {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
@ -1,207 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<Draggable :list="sections" item-key="label" class="flex flex-col gap-5.5">
|
|
||||||
<template #item="{ element: section }">
|
|
||||||
<div class="flex flex-col gap-1.5 p-2.5 bg-surface-gray-2 rounded">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div
|
|
||||||
class="flex h-7 max-w-fit cursor-pointer items-center gap-2 text-base font-medium leading-4 text-ink-gray-9"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="!section.editingLabel"
|
|
||||||
:class="{ 'text-ink-gray-3': section.hideLabel }"
|
|
||||||
>
|
|
||||||
{{ __(section.label) || __('Untitled') }}
|
|
||||||
</div>
|
|
||||||
<div v-else class="flex gap-2 items-center">
|
|
||||||
<Input
|
|
||||||
v-model="section.label"
|
|
||||||
@keydown.enter="section.editingLabel = false"
|
|
||||||
@blur="section.editingLabel = false"
|
|
||||||
@click.stop
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
v-if="section.editingLabel"
|
|
||||||
icon="check"
|
|
||||||
variant="ghost"
|
|
||||||
@click="section.editingLabel = false"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Dropdown :options="getOptions(section)">
|
|
||||||
<template #default>
|
|
||||||
<Button variant="ghost">
|
|
||||||
<FeatherIcon name="more-horizontal" class="h-4" />
|
|
||||||
</Button>
|
|
||||||
</template>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
<Draggable
|
|
||||||
:list="section.fields"
|
|
||||||
group="fields"
|
|
||||||
item-key="label"
|
|
||||||
class="grid gap-1.5"
|
|
||||||
:class="
|
|
||||||
section.columns ? 'grid-cols-' + section.columns : 'grid-cols-3'
|
|
||||||
"
|
|
||||||
handle=".cursor-grab"
|
|
||||||
>
|
|
||||||
<template #item="{ element: field }">
|
|
||||||
<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"
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<DragVerticalIcon class="h-3.5 cursor-grab" />
|
|
||||||
<div>{{ field.label }}</div>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
class="!size-4 rounded-sm"
|
|
||||||
icon="x"
|
|
||||||
@click="
|
|
||||||
section.fields.splice(section.fields.indexOf(field), 1)
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Draggable>
|
|
||||||
<Autocomplete
|
|
||||||
v-if="fields.data"
|
|
||||||
value=""
|
|
||||||
:options="fields.data"
|
|
||||||
@change="(e) => addField(section, e)"
|
|
||||||
>
|
|
||||||
<template #target="{ togglePopover }">
|
|
||||||
<div class="gap-2 w-full">
|
|
||||||
<Button
|
|
||||||
class="w-full !h-8 !bg-surface-modal"
|
|
||||||
variant="outline"
|
|
||||||
@click="togglePopover()"
|
|
||||||
:label="__('Add Field')"
|
|
||||||
>
|
|
||||||
<template #prefix>
|
|
||||||
<FeatherIcon name="plus" class="h-4" />
|
|
||||||
</template>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</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>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Draggable>
|
|
||||||
<div class="mt-5.5">
|
|
||||||
<Button
|
|
||||||
class="w-full h-8"
|
|
||||||
variant="subtle"
|
|
||||||
:label="__('Add Section')"
|
|
||||||
@click="
|
|
||||||
sections.push({
|
|
||||||
label: __('New Section'),
|
|
||||||
opened: true,
|
|
||||||
fields: [],
|
|
||||||
})
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<template #prefix>
|
|
||||||
<FeatherIcon name="plus" class="h-4" />
|
|
||||||
</template>
|
|
||||||
</Button>
|
|
||||||
</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 { Dropdown, 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getOptions(section) {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
label: 'Edit',
|
|
||||||
icon: 'edit',
|
|
||||||
onClick: () => (section.editingLabel = true),
|
|
||||||
condition: () => section.editable !== false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: section.hideLabel ? 'Show Label' : 'Hide Label',
|
|
||||||
icon: section.hideLabel ? 'eye' : 'eye-off',
|
|
||||||
onClick: () => (section.hideLabel = !section.hideLabel),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: section.hideBorder ? 'Show Border' : 'Hide Border',
|
|
||||||
icon: 'minus',
|
|
||||||
onClick: () => (section.hideBorder = !section.hideBorder),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Add Column',
|
|
||||||
icon: 'columns',
|
|
||||||
onClick: () =>
|
|
||||||
(section.columns = section.columns ? section.columns + 1 : 4),
|
|
||||||
condition: () => !section.columns || section.columns < 4,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Remove Column',
|
|
||||||
icon: 'columns',
|
|
||||||
onClick: () =>
|
|
||||||
(section.columns = section.columns ? section.columns - 1 : 2),
|
|
||||||
condition: () => !section.columns || section.columns > 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Remove Section',
|
|
||||||
icon: 'trash-2',
|
|
||||||
onClick: () => props.sections.splice(props.sections.indexOf(section), 1),
|
|
||||||
condition: () => section.editable !== false,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.doctype,
|
|
||||||
() => fields.fetch(params.value),
|
|
||||||
{ immediate: true },
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
@ -2,15 +2,25 @@
|
|||||||
<slot name="header" v-bind="{ opened, hide, open, close, toggle }">
|
<slot name="header" v-bind="{ opened, hide, open, close, toggle }">
|
||||||
<div v-if="!hide" class="flex items-center justify-between">
|
<div v-if="!hide" class="flex items-center justify-between">
|
||||||
<div
|
<div
|
||||||
class="flex h-7 text-ink-gray-9 max-w-fit cursor-pointer items-center gap-2 pl-2 pr-3 text-base font-semibold leading-5"
|
class="flex text-ink-gray-9 max-w-fit cursor-pointer items-center gap-2 text-base"
|
||||||
@click="toggle()"
|
v-bind="$attrs"
|
||||||
|
@click="collapsible && toggle()"
|
||||||
>
|
>
|
||||||
<FeatherIcon
|
<FeatherIcon
|
||||||
|
v-if="collapsible && collapseIconPosition === 'left'"
|
||||||
|
name="chevron-right"
|
||||||
|
class="h-4 transition-all duration-300 ease-in-out"
|
||||||
|
:class="{ 'rotate-90': opened }"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{{ __(label) || __('Untitled') }}
|
||||||
|
</span>
|
||||||
|
<FeatherIcon
|
||||||
|
v-if="collapsible && collapseIconPosition === 'right'"
|
||||||
name="chevron-right"
|
name="chevron-right"
|
||||||
class="h-4 transition-all duration-300 ease-in-out"
|
class="h-4 transition-all duration-300 ease-in-out"
|
||||||
:class="{ 'rotate-90': opened }"
|
:class="{ 'rotate-90': opened }"
|
||||||
/>
|
/>
|
||||||
{{ __(label) || __('Untitled') }}
|
|
||||||
</div>
|
</div>
|
||||||
<slot name="actions"></slot>
|
<slot name="actions"></slot>
|
||||||
</div>
|
</div>
|
||||||
@ -23,13 +33,14 @@
|
|||||||
enter-from-class="max-h-0 overflow-hidden"
|
enter-from-class="max-h-0 overflow-hidden"
|
||||||
leave-to-class="max-h-0 overflow-hidden"
|
leave-to-class="max-h-0 overflow-hidden"
|
||||||
>
|
>
|
||||||
<div v-if="opened">
|
<div v-show="opened">
|
||||||
<slot v-bind="{ opened, open, close, toggle }" />
|
<slot v-bind="{ opened, open, close, toggle }" />
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
label: {
|
label: {
|
||||||
type: String,
|
type: String,
|
||||||
@ -39,11 +50,23 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
isOpened: {
|
opened: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
collapsible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
collapseIconPosition: {
|
||||||
|
type: String,
|
||||||
|
default: 'left',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const hide = ref(props.hideLabel)
|
||||||
|
const opened = ref(props.opened)
|
||||||
|
|
||||||
function toggle() {
|
function toggle() {
|
||||||
opened.value = !opened.value
|
opened.value = !opened.value
|
||||||
}
|
}
|
||||||
@ -55,7 +78,4 @@ function open() {
|
|||||||
function close() {
|
function close() {
|
||||||
opened.value = false
|
opened.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
let opened = ref(props.isOpened)
|
|
||||||
let hide = ref(props.hideLabel)
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
<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 h-5 text-ink-gray-9">
|
<h2
|
||||||
|
class="flex gap-2 text-xl font-semibold leading-none h-5 text-ink-gray-9"
|
||||||
|
>
|
||||||
<div>{{ title || __(doctype) }}</div>
|
<div>{{ title || __(doctype) }}</div>
|
||||||
<Badge
|
<Badge
|
||||||
v-if="data.isDirty"
|
v-if="data.isDirty"
|
||||||
@ -10,11 +12,7 @@
|
|||||||
/>
|
/>
|
||||||
</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">
|
||||||
<Fields
|
<FieldLayout v-if="data?.doc && tabs" :tabs="tabs" :data="data.doc" />
|
||||||
v-if="data?.doc && sections"
|
|
||||||
:sections="sections"
|
|
||||||
:data="data.doc"
|
|
||||||
/>
|
|
||||||
<ErrorMessage class="mt-2" :message="error" />
|
<ErrorMessage class="mt-2" :message="error" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="flex flex-1 items-center justify-center">
|
<div v-else class="flex flex-1 items-center justify-center">
|
||||||
@ -31,7 +29,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import Fields from '@/components/Fields.vue'
|
import FieldLayout from '@/components/FieldLayout.vue'
|
||||||
import {
|
import {
|
||||||
createDocumentResource,
|
createDocumentResource,
|
||||||
createResource,
|
createResource,
|
||||||
@ -96,7 +94,7 @@ const data = createDocumentResource({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const sections = computed(() => {
|
const tabs = computed(() => {
|
||||||
if (!fields.data) return []
|
if (!fields.data) return []
|
||||||
let _sections = []
|
let _sections = []
|
||||||
let fieldsData = fields.data
|
let fieldsData = fields.data
|
||||||
@ -136,7 +134,7 @@ const sections = computed(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return _sections
|
return [{ no_tabs: true, sections: _sections }]
|
||||||
})
|
})
|
||||||
|
|
||||||
function update() {
|
function update() {
|
||||||
@ -146,7 +144,8 @@ function update() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function validateMandatoryFields() {
|
function validateMandatoryFields() {
|
||||||
for (let section of sections.value) {
|
if (!tabs.value) return false
|
||||||
|
for (let section of tabs.value[0].sections) {
|
||||||
for (let field of section.fields) {
|
for (let field of section.fields) {
|
||||||
if (
|
if (
|
||||||
(field.mandatory ||
|
(field.mandatory ||
|
||||||
|
|||||||
@ -18,15 +18,12 @@
|
|||||||
:class="inputClasses"
|
:class="inputClasses"
|
||||||
@click="() => togglePopover()"
|
@click="() => togglePopover()"
|
||||||
>
|
>
|
||||||
<div class="flex items-center">
|
<div class="flex text-base leading-5 items-center truncate">
|
||||||
<slot name="prefix" />
|
<slot name="prefix" />
|
||||||
<span
|
<span v-if="selectedValue" class="truncate">
|
||||||
class="overflow-hidden text-ellipsis whitespace-nowrap text-base leading-5"
|
|
||||||
v-if="selectedValue"
|
|
||||||
>
|
|
||||||
{{ displayValue(selectedValue) }}
|
{{ displayValue(selectedValue) }}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-base leading-5 text-ink-gray-4" v-else>
|
<span v-else class="text-ink-gray-4 truncate">
|
||||||
{{ placeholder || '' }}
|
{{ placeholder || '' }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -66,7 +63,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<ComboboxOptions
|
<ComboboxOptions
|
||||||
class="my-1 max-h-[12rem] overflow-y-auto px-1.5"
|
class="my-1 max-h-[12rem] overflow-y-auto p-1.5 pt-0"
|
||||||
static
|
static
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -60,9 +60,7 @@
|
|||||||
clip-path: inset(22px 0 0 0);
|
clip-path: inset(22px 0 0 0);
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<CameraIcon
|
<CameraIcon class="h-6 w-6 cursor-pointer text-white" />
|
||||||
class="h-6 w-6 cursor-pointer text-white"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</component>
|
</component>
|
||||||
</div>
|
</div>
|
||||||
@ -129,7 +127,7 @@
|
|||||||
class="flex flex-col p-3"
|
class="flex flex-col p-3"
|
||||||
:class="{ 'border-b': i !== fieldsLayout.data.length - 1 }"
|
:class="{ 'border-b': i !== fieldsLayout.data.length - 1 }"
|
||||||
>
|
>
|
||||||
<Section :is-opened="section.opened" :label="section.label">
|
<Section :label="section.label" :opened="section.opened">
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<Button
|
<Button
|
||||||
v-if="i == 0 && isManager()"
|
v-if="i == 0 && isManager()"
|
||||||
@ -140,7 +138,7 @@
|
|||||||
<EditIcon class="h-4 w-4" />
|
<EditIcon class="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
<SectionFields
|
<SidePanelLayout
|
||||||
v-if="section.fields"
|
v-if="section.fields"
|
||||||
:fields="section.fields"
|
:fields="section.fields"
|
||||||
:isLastSection="i == fieldsLayout.data.length - 1"
|
:isLastSection="i == fieldsLayout.data.length - 1"
|
||||||
@ -204,14 +202,14 @@
|
|||||||
import Resizer from '@/components/Resizer.vue'
|
import Resizer from '@/components/Resizer.vue'
|
||||||
import Icon from '@/components/Icon.vue'
|
import Icon from '@/components/Icon.vue'
|
||||||
import Section from '@/components/Section.vue'
|
import Section from '@/components/Section.vue'
|
||||||
import SectionFields from '@/components/SectionFields.vue'
|
import SidePanelLayout from '@/components/SidePanelLayout.vue'
|
||||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||||
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
||||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||||
import CameraIcon from '@/components/Icons/CameraIcon.vue'
|
import CameraIcon from '@/components/Icons/CameraIcon.vue'
|
||||||
import DealsIcon from '@/components/Icons/DealsIcon.vue'
|
import DealsIcon from '@/components/Icons/DealsIcon.vue'
|
||||||
import DealsListView from '@/components/ListViews/DealsListView.vue'
|
import DealsListView from '@/components/ListViews/DealsListView.vue'
|
||||||
import SidePanelModal from '@/components/Settings/SidePanelModal.vue'
|
import SidePanelModal from '@/components/Modals/SidePanelModal.vue'
|
||||||
import AddressModal from '@/components/Modals/AddressModal.vue'
|
import AddressModal from '@/components/Modals/AddressModal.vue'
|
||||||
import {
|
import {
|
||||||
formatDate,
|
formatDate,
|
||||||
|
|||||||
@ -59,16 +59,7 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ContactModal
|
<ContactModal v-model="showContactModal" :contact="{}" />
|
||||||
v-model="showContactModal"
|
|
||||||
v-model:quickEntry="showQuickEntryModal"
|
|
||||||
:contact="{}"
|
|
||||||
/>
|
|
||||||
<QuickEntryModal
|
|
||||||
v-if="showQuickEntryModal"
|
|
||||||
v-model="showQuickEntryModal"
|
|
||||||
doctype="Contact"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@ -77,7 +68,6 @@ import CustomActions from '@/components/CustomActions.vue'
|
|||||||
import ContactsIcon from '@/components/Icons/ContactsIcon.vue'
|
import ContactsIcon from '@/components/Icons/ContactsIcon.vue'
|
||||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||||
import ContactModal from '@/components/Modals/ContactModal.vue'
|
import ContactModal from '@/components/Modals/ContactModal.vue'
|
||||||
import QuickEntryModal from '@/components/Modals/QuickEntryModal.vue'
|
|
||||||
import ContactsListView from '@/components/ListViews/ContactsListView.vue'
|
import ContactsListView from '@/components/ListViews/ContactsListView.vue'
|
||||||
import ViewControls from '@/components/ViewControls.vue'
|
import ViewControls from '@/components/ViewControls.vue'
|
||||||
import { organizationsStore } from '@/stores/organizations.js'
|
import { organizationsStore } from '@/stores/organizations.js'
|
||||||
@ -87,7 +77,6 @@ import { ref, computed } from 'vue'
|
|||||||
const { getOrganization } = organizationsStore()
|
const { getOrganization } = organizationsStore()
|
||||||
|
|
||||||
const showContactModal = ref(false)
|
const showContactModal = ref(false)
|
||||||
const showQuickEntryModal = ref(false)
|
|
||||||
|
|
||||||
const contactsListView = ref(null)
|
const contactsListView = ref(null)
|
||||||
|
|
||||||
|
|||||||
@ -124,7 +124,11 @@
|
|||||||
class="section flex flex-col p-3"
|
class="section flex flex-col p-3"
|
||||||
:class="{ 'border-b': i !== fieldsLayout.data.length - 1 }"
|
:class="{ 'border-b': i !== fieldsLayout.data.length - 1 }"
|
||||||
>
|
>
|
||||||
<Section :is-opened="section.opened" :label="section.label">
|
<Section
|
||||||
|
class="px-2 font-semibold"
|
||||||
|
:label="section.label"
|
||||||
|
:opened="section.opened"
|
||||||
|
>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<div v-if="section.contacts" class="pr-2">
|
<div v-if="section.contacts" class="pr-2">
|
||||||
<Link
|
<Link
|
||||||
@ -163,7 +167,7 @@
|
|||||||
<EditIcon class="h-4 w-4" />
|
<EditIcon class="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
<SectionFields
|
<SidePanelLayout
|
||||||
v-if="section.fields"
|
v-if="section.fields"
|
||||||
:fields="section.fields"
|
:fields="section.fields"
|
||||||
:isLastSection="i == fieldsLayout.data.length - 1"
|
:isLastSection="i == fieldsLayout.data.length - 1"
|
||||||
@ -189,7 +193,7 @@
|
|||||||
class="px-2 pb-2.5"
|
class="px-2 pb-2.5"
|
||||||
:class="[i == 0 ? 'pt-5' : 'pt-2.5']"
|
:class="[i == 0 ? 'pt-5' : 'pt-2.5']"
|
||||||
>
|
>
|
||||||
<Section :is-opened="contact.opened">
|
<Section :opened="contact.opened">
|
||||||
<template #header="{ opened, toggle }">
|
<template #header="{ opened, toggle }">
|
||||||
<div
|
<div
|
||||||
class="flex cursor-pointer items-center justify-between gap-2 pr-1 text-base leading-5 text-ink-gray-7"
|
class="flex cursor-pointer items-center justify-between gap-2 pr-1 text-base leading-5 text-ink-gray-7"
|
||||||
@ -326,6 +330,7 @@ import ActivityIcon from '@/components/Icons/ActivityIcon.vue'
|
|||||||
import EmailIcon from '@/components/Icons/EmailIcon.vue'
|
import EmailIcon from '@/components/Icons/EmailIcon.vue'
|
||||||
import Email2Icon from '@/components/Icons/Email2Icon.vue'
|
import Email2Icon from '@/components/Icons/Email2Icon.vue'
|
||||||
import CommentIcon from '@/components/Icons/CommentIcon.vue'
|
import CommentIcon from '@/components/Icons/CommentIcon.vue'
|
||||||
|
import DetailsIcon from '@/components/Icons/DetailsIcon.vue'
|
||||||
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
||||||
import TaskIcon from '@/components/Icons/TaskIcon.vue'
|
import TaskIcon from '@/components/Icons/TaskIcon.vue'
|
||||||
import NoteIcon from '@/components/Icons/NoteIcon.vue'
|
import NoteIcon from '@/components/Icons/NoteIcon.vue'
|
||||||
@ -342,10 +347,10 @@ import AssignmentModal from '@/components/Modals/AssignmentModal.vue'
|
|||||||
import FilesUploader from '@/components/FilesUploader/FilesUploader.vue'
|
import FilesUploader from '@/components/FilesUploader/FilesUploader.vue'
|
||||||
import MultipleAvatar from '@/components/MultipleAvatar.vue'
|
import MultipleAvatar from '@/components/MultipleAvatar.vue'
|
||||||
import ContactModal from '@/components/Modals/ContactModal.vue'
|
import ContactModal from '@/components/Modals/ContactModal.vue'
|
||||||
import SidePanelModal from '@/components/Settings/SidePanelModal.vue'
|
import SidePanelModal from '@/components/Modals/SidePanelModal.vue'
|
||||||
import Link from '@/components/Controls/Link.vue'
|
import Link from '@/components/Controls/Link.vue'
|
||||||
import Section from '@/components/Section.vue'
|
import Section from '@/components/Section.vue'
|
||||||
import SectionFields from '@/components/SectionFields.vue'
|
import SidePanelLayout from '@/components/SidePanelLayout.vue'
|
||||||
import SLASection from '@/components/SLASection.vue'
|
import SLASection from '@/components/SLASection.vue'
|
||||||
import CustomActions from '@/components/CustomActions.vue'
|
import CustomActions from '@/components/CustomActions.vue'
|
||||||
import {
|
import {
|
||||||
@ -554,6 +559,11 @@ const tabs = computed(() => {
|
|||||||
label: __('Comments'),
|
label: __('Comments'),
|
||||||
icon: CommentIcon,
|
icon: CommentIcon,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Data',
|
||||||
|
label: __('Data'),
|
||||||
|
icon: DetailsIcon,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Calls',
|
name: 'Calls',
|
||||||
label: __('Calls'),
|
label: __('Calls'),
|
||||||
|
|||||||
@ -177,8 +177,12 @@
|
|||||||
class="flex flex-col p-3"
|
class="flex flex-col p-3"
|
||||||
:class="{ 'border-b': i !== fieldsLayout.data.length - 1 }"
|
:class="{ 'border-b': i !== fieldsLayout.data.length - 1 }"
|
||||||
>
|
>
|
||||||
<Section :is-opened="section.opened" :label="section.label">
|
<Section
|
||||||
<SectionFields
|
class="px-2 font-semibold"
|
||||||
|
:label="section.label"
|
||||||
|
:opened="section.opened"
|
||||||
|
>
|
||||||
|
<SidePanelLayout
|
||||||
:fields="section.fields"
|
:fields="section.fields"
|
||||||
:isLastSection="i == fieldsLayout.data.length - 1"
|
:isLastSection="i == fieldsLayout.data.length - 1"
|
||||||
v-model="lead.data"
|
v-model="lead.data"
|
||||||
@ -298,6 +302,7 @@ import ActivityIcon from '@/components/Icons/ActivityIcon.vue'
|
|||||||
import EmailIcon from '@/components/Icons/EmailIcon.vue'
|
import EmailIcon from '@/components/Icons/EmailIcon.vue'
|
||||||
import Email2Icon from '@/components/Icons/Email2Icon.vue'
|
import Email2Icon from '@/components/Icons/Email2Icon.vue'
|
||||||
import CommentIcon from '@/components/Icons/CommentIcon.vue'
|
import CommentIcon from '@/components/Icons/CommentIcon.vue'
|
||||||
|
import DetailsIcon from '@/components/Icons/DetailsIcon.vue'
|
||||||
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
||||||
import TaskIcon from '@/components/Icons/TaskIcon.vue'
|
import TaskIcon from '@/components/Icons/TaskIcon.vue'
|
||||||
import NoteIcon from '@/components/Icons/NoteIcon.vue'
|
import NoteIcon from '@/components/Icons/NoteIcon.vue'
|
||||||
@ -312,11 +317,11 @@ import LayoutHeader from '@/components/LayoutHeader.vue'
|
|||||||
import Activities from '@/components/Activities/Activities.vue'
|
import Activities from '@/components/Activities/Activities.vue'
|
||||||
import AssignmentModal from '@/components/Modals/AssignmentModal.vue'
|
import AssignmentModal from '@/components/Modals/AssignmentModal.vue'
|
||||||
import FilesUploader from '@/components/FilesUploader/FilesUploader.vue'
|
import FilesUploader from '@/components/FilesUploader/FilesUploader.vue'
|
||||||
import SidePanelModal from '@/components/Settings/SidePanelModal.vue'
|
import SidePanelModal from '@/components/Modals/SidePanelModal.vue'
|
||||||
import MultipleAvatar from '@/components/MultipleAvatar.vue'
|
import MultipleAvatar from '@/components/MultipleAvatar.vue'
|
||||||
import Link from '@/components/Controls/Link.vue'
|
import Link from '@/components/Controls/Link.vue'
|
||||||
import Section from '@/components/Section.vue'
|
import Section from '@/components/Section.vue'
|
||||||
import SectionFields from '@/components/SectionFields.vue'
|
import SidePanelLayout from '@/components/SidePanelLayout.vue'
|
||||||
import SLASection from '@/components/SLASection.vue'
|
import SLASection from '@/components/SLASection.vue'
|
||||||
import CustomActions from '@/components/CustomActions.vue'
|
import CustomActions from '@/components/CustomActions.vue'
|
||||||
import {
|
import {
|
||||||
@ -500,6 +505,11 @@ const tabs = computed(() => {
|
|||||||
label: __('Comments'),
|
label: __('Comments'),
|
||||||
icon: CommentIcon,
|
icon: CommentIcon,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Data',
|
||||||
|
label: __('Data'),
|
||||||
|
icon: DetailsIcon,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Calls',
|
name: 'Calls',
|
||||||
label: __('Calls'),
|
label: __('Calls'),
|
||||||
|
|||||||
@ -141,8 +141,8 @@
|
|||||||
class="flex flex-col px-2 py-3 sm:p-3"
|
class="flex flex-col px-2 py-3 sm:p-3"
|
||||||
:class="{ 'border-b': i !== fieldsLayout.data.length - 1 }"
|
:class="{ 'border-b': i !== fieldsLayout.data.length - 1 }"
|
||||||
>
|
>
|
||||||
<Section :is-opened="section.opened" :label="section.label">
|
<Section :label="section.label" :opened="section.opened">
|
||||||
<SectionFields
|
<SidePanelLayout
|
||||||
:fields="section.fields"
|
:fields="section.fields"
|
||||||
:isLastSection="i == fieldsLayout.data.length - 1"
|
:isLastSection="i == fieldsLayout.data.length - 1"
|
||||||
v-model="contact.data"
|
v-model="contact.data"
|
||||||
@ -178,7 +178,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import Icon from '@/components/Icon.vue'
|
import Icon from '@/components/Icon.vue'
|
||||||
import Section from '@/components/Section.vue'
|
import Section from '@/components/Section.vue'
|
||||||
import SectionFields from '@/components/SectionFields.vue'
|
import SidePanelLayout from '@/components/SidePanelLayout.vue'
|
||||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||||
import DetailsIcon from '@/components/Icons/DetailsIcon.vue'
|
import DetailsIcon from '@/components/Icons/DetailsIcon.vue'
|
||||||
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
||||||
|
|||||||
@ -69,7 +69,7 @@
|
|||||||
class="flex flex-col px-2 py-3 sm:p-3"
|
class="flex flex-col px-2 py-3 sm:p-3"
|
||||||
:class="{ 'border-b': i !== fieldsLayout.data.length - 1 }"
|
:class="{ 'border-b': i !== fieldsLayout.data.length - 1 }"
|
||||||
>
|
>
|
||||||
<Section :is-opened="section.opened" :label="section.label">
|
<Section :label="section.label" :opened="section.opened">
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<div v-if="section.contacts" class="pr-2">
|
<div v-if="section.contacts" class="pr-2">
|
||||||
<Link
|
<Link
|
||||||
@ -98,7 +98,7 @@
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<SectionFields
|
<SidePanelLayout
|
||||||
v-if="section.fields"
|
v-if="section.fields"
|
||||||
:fields="section.fields"
|
:fields="section.fields"
|
||||||
:isLastSection="i == fieldsLayout.data.length - 1"
|
:isLastSection="i == fieldsLayout.data.length - 1"
|
||||||
@ -124,7 +124,7 @@
|
|||||||
class="px-2 pb-2.5"
|
class="px-2 pb-2.5"
|
||||||
:class="[i == 0 ? 'pt-5' : 'pt-2.5']"
|
:class="[i == 0 ? 'pt-5' : 'pt-2.5']"
|
||||||
>
|
>
|
||||||
<Section :is-opened="contact.opened">
|
<Section :opened="contact.opened">
|
||||||
<template #header="{ opened, toggle }">
|
<template #header="{ opened, toggle }">
|
||||||
<div
|
<div
|
||||||
class="flex cursor-pointer items-center justify-between gap-2 pr-1 text-base leading-5 text-ink-gray-7"
|
class="flex cursor-pointer items-center justify-between gap-2 pr-1 text-base leading-5 text-ink-gray-7"
|
||||||
@ -267,7 +267,7 @@ import MultipleAvatar from '@/components/MultipleAvatar.vue'
|
|||||||
import ContactModal from '@/components/Modals/ContactModal.vue'
|
import ContactModal from '@/components/Modals/ContactModal.vue'
|
||||||
import Link from '@/components/Controls/Link.vue'
|
import Link from '@/components/Controls/Link.vue'
|
||||||
import Section from '@/components/Section.vue'
|
import Section from '@/components/Section.vue'
|
||||||
import SectionFields from '@/components/SectionFields.vue'
|
import SidePanelLayout from '@/components/SidePanelLayout.vue'
|
||||||
import SLASection from '@/components/SLASection.vue'
|
import SLASection from '@/components/SLASection.vue'
|
||||||
import CustomActions from '@/components/CustomActions.vue'
|
import CustomActions from '@/components/CustomActions.vue'
|
||||||
import { createToast, setupAssignees, setupCustomizations } from '@/utils'
|
import { createToast, setupAssignees, setupCustomizations } from '@/utils'
|
||||||
@ -452,6 +452,11 @@ const tabs = computed(() => {
|
|||||||
label: __('Comments'),
|
label: __('Comments'),
|
||||||
icon: CommentIcon,
|
icon: CommentIcon,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Data',
|
||||||
|
label: __('Data'),
|
||||||
|
icon: DetailsIcon,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Calls',
|
name: 'Calls',
|
||||||
label: __('Calls'),
|
label: __('Calls'),
|
||||||
|
|||||||
@ -74,8 +74,8 @@
|
|||||||
class="flex flex-col px-2 py-3 sm:p-3"
|
class="flex flex-col px-2 py-3 sm:p-3"
|
||||||
:class="{ 'border-b': i !== fieldsLayout.data.length - 1 }"
|
:class="{ 'border-b': i !== fieldsLayout.data.length - 1 }"
|
||||||
>
|
>
|
||||||
<Section :is-opened="section.opened" :label="section.label">
|
<Section :label="section.label" :opened="section.opened">
|
||||||
<SectionFields
|
<SidePanelLayout
|
||||||
:fields="section.fields"
|
:fields="section.fields"
|
||||||
:isLastSection="i == fieldsLayout.data.length - 1"
|
:isLastSection="i == fieldsLayout.data.length - 1"
|
||||||
v-model="lead.data"
|
v-model="lead.data"
|
||||||
@ -190,7 +190,7 @@ import AssignmentModal from '@/components/Modals/AssignmentModal.vue'
|
|||||||
import MultipleAvatar from '@/components/MultipleAvatar.vue'
|
import MultipleAvatar from '@/components/MultipleAvatar.vue'
|
||||||
import Link from '@/components/Controls/Link.vue'
|
import Link from '@/components/Controls/Link.vue'
|
||||||
import Section from '@/components/Section.vue'
|
import Section from '@/components/Section.vue'
|
||||||
import SectionFields from '@/components/SectionFields.vue'
|
import SidePanelLayout from '@/components/SidePanelLayout.vue'
|
||||||
import SLASection from '@/components/SLASection.vue'
|
import SLASection from '@/components/SLASection.vue'
|
||||||
import CustomActions from '@/components/CustomActions.vue'
|
import CustomActions from '@/components/CustomActions.vue'
|
||||||
import { createToast, setupAssignees, setupCustomizations } from '@/utils'
|
import { createToast, setupAssignees, setupCustomizations } from '@/utils'
|
||||||
@ -362,6 +362,11 @@ const tabs = computed(() => {
|
|||||||
label: __('Comments'),
|
label: __('Comments'),
|
||||||
icon: CommentIcon,
|
icon: CommentIcon,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Data',
|
||||||
|
label: __('Data'),
|
||||||
|
icon: DetailsIcon,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Calls',
|
name: 'Calls',
|
||||||
label: __('Calls'),
|
label: __('Calls'),
|
||||||
|
|||||||
@ -123,8 +123,8 @@
|
|||||||
class="flex flex-col px-2 py-3 sm:p-3"
|
class="flex flex-col px-2 py-3 sm:p-3"
|
||||||
:class="{ 'border-b': i !== fieldsLayout.data.length - 1 }"
|
:class="{ 'border-b': i !== fieldsLayout.data.length - 1 }"
|
||||||
>
|
>
|
||||||
<Section :is-opened="section.opened" :label="section.label">
|
<Section :label="section.label" :opened="section.opened">
|
||||||
<SectionFields
|
<SidePanelLayout
|
||||||
:fields="section.fields"
|
:fields="section.fields"
|
||||||
:isLastSection="i == fieldsLayout.data.length - 1"
|
:isLastSection="i == fieldsLayout.data.length - 1"
|
||||||
v-model="organization.doc"
|
v-model="organization.doc"
|
||||||
@ -166,7 +166,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import Section from '@/components/Section.vue'
|
import Section from '@/components/Section.vue'
|
||||||
import SectionFields from '@/components/SectionFields.vue'
|
import SidePanelLayout from '@/components/SidePanelLayout.vue'
|
||||||
import Icon from '@/components/Icon.vue'
|
import Icon from '@/components/Icon.vue'
|
||||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||||
import AddressModal from '@/components/Modals/AddressModal.vue'
|
import AddressModal from '@/components/Modals/AddressModal.vue'
|
||||||
|
|||||||
@ -112,7 +112,7 @@
|
|||||||
class="flex flex-col p-3"
|
class="flex flex-col p-3"
|
||||||
:class="{ 'border-b': i !== fieldsLayout.data.length - 1 }"
|
:class="{ 'border-b': i !== fieldsLayout.data.length - 1 }"
|
||||||
>
|
>
|
||||||
<Section :is-opened="section.opened" :label="section.label">
|
<Section :label="section.label" :opened="section.opened">
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<Button
|
<Button
|
||||||
v-if="i == 0 && isManager()"
|
v-if="i == 0 && isManager()"
|
||||||
@ -123,7 +123,7 @@
|
|||||||
<EditIcon class="h-4 w-4" />
|
<EditIcon class="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
<SectionFields
|
<SidePanelLayout
|
||||||
v-if="section.fields"
|
v-if="section.fields"
|
||||||
:fields="section.fields"
|
:fields="section.fields"
|
||||||
:isLastSection="i == fieldsLayout.data.length - 1"
|
:isLastSection="i == fieldsLayout.data.length - 1"
|
||||||
@ -198,8 +198,8 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import Resizer from '@/components/Resizer.vue'
|
import Resizer from '@/components/Resizer.vue'
|
||||||
import Section from '@/components/Section.vue'
|
import Section from '@/components/Section.vue'
|
||||||
import SectionFields from '@/components/SectionFields.vue'
|
import SidePanelLayout from '@/components/SidePanelLayout.vue'
|
||||||
import SidePanelModal from '@/components/Settings/SidePanelModal.vue'
|
import SidePanelModal from '@/components/Modals/SidePanelModal.vue'
|
||||||
import Icon from '@/components/Icon.vue'
|
import Icon from '@/components/Icon.vue'
|
||||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||||
import QuickEntryModal from '@/components/Modals/QuickEntryModal.vue'
|
import QuickEntryModal from '@/components/Modals/QuickEntryModal.vue'
|
||||||
|
|||||||
@ -59,15 +59,7 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<OrganizationModal
|
<OrganizationModal v-model="showOrganizationModal" />
|
||||||
v-model="showOrganizationModal"
|
|
||||||
v-model:quickEntry="showQuickEntryModal"
|
|
||||||
/>
|
|
||||||
<QuickEntryModal
|
|
||||||
v-if="showQuickEntryModal"
|
|
||||||
v-model="showQuickEntryModal"
|
|
||||||
doctype="CRM Organization"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import ViewBreadcrumbs from '@/components/ViewBreadcrumbs.vue'
|
import ViewBreadcrumbs from '@/components/ViewBreadcrumbs.vue'
|
||||||
@ -75,7 +67,6 @@ import CustomActions from '@/components/CustomActions.vue'
|
|||||||
import OrganizationsIcon from '@/components/Icons/OrganizationsIcon.vue'
|
import OrganizationsIcon from '@/components/Icons/OrganizationsIcon.vue'
|
||||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||||
import OrganizationModal from '@/components/Modals/OrganizationModal.vue'
|
import OrganizationModal from '@/components/Modals/OrganizationModal.vue'
|
||||||
import QuickEntryModal from '@/components/Modals/QuickEntryModal.vue'
|
|
||||||
import OrganizationsListView from '@/components/ListViews/OrganizationsListView.vue'
|
import OrganizationsListView from '@/components/ListViews/OrganizationsListView.vue'
|
||||||
import ViewControls from '@/components/ViewControls.vue'
|
import ViewControls from '@/components/ViewControls.vue'
|
||||||
import { formatDate, timeAgo, website, formatNumberIntoCurrency } from '@/utils'
|
import { formatDate, timeAgo, website, formatNumberIntoCurrency } from '@/utils'
|
||||||
@ -83,7 +74,6 @@ import { ref, computed } from 'vue'
|
|||||||
|
|
||||||
const organizationsListView = ref(null)
|
const organizationsListView = ref(null)
|
||||||
const showOrganizationModal = ref(false)
|
const showOrganizationModal = ref(false)
|
||||||
const showQuickEntryModal = ref(false)
|
|
||||||
|
|
||||||
// organizations data is loaded in the ViewControls component
|
// organizations data is loaded in the ViewControls component
|
||||||
const organizations = ref({})
|
const organizations = ref({})
|
||||||
|
|||||||
@ -6,10 +6,7 @@ module.exports = {
|
|||||||
'./node_modules/frappe-ui/src/components/**/*.{vue,js,ts,jsx,tsx}',
|
'./node_modules/frappe-ui/src/components/**/*.{vue,js,ts,jsx,tsx}',
|
||||||
'../node_modules/frappe-ui/src/components/**/*.{vue,js,ts,jsx,tsx}',
|
'../node_modules/frappe-ui/src/components/**/*.{vue,js,ts,jsx,tsx}',
|
||||||
],
|
],
|
||||||
safelist: [
|
safelist: [{ pattern: /!(text|bg)-/, variants: ['hover', 'active'] }],
|
||||||
{ pattern: /!(text|bg)-/, variants: ['hover', 'active'] },
|
|
||||||
{ pattern: /^grid-cols-/ },
|
|
||||||
],
|
|
||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -19,3 +19,46 @@ build-backend = "flit_core.buildapi"
|
|||||||
# These dependencies are only installed when developer mode is enabled
|
# These dependencies are only installed when developer mode is enabled
|
||||||
[tool.bench.dev-dependencies]
|
[tool.bench.dev-dependencies]
|
||||||
# package_name = "~=1.1.0"
|
# package_name = "~=1.1.0"
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
line-length = 110
|
||||||
|
target-version = "py310"
|
||||||
|
exclude = [
|
||||||
|
"**/doctype/*/boilerplate/*.py" # boilerplate are template strings, not valid python
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = [
|
||||||
|
"F",
|
||||||
|
"E",
|
||||||
|
"W",
|
||||||
|
"I",
|
||||||
|
"UP",
|
||||||
|
"B",
|
||||||
|
"RUF",
|
||||||
|
]
|
||||||
|
ignore = [
|
||||||
|
"B017", # assertRaises(Exception) - should be more specific
|
||||||
|
"B018", # useless expression, not assigned to anything
|
||||||
|
"B023", # function doesn't bind loop variable - will have last iteration's value
|
||||||
|
"B904", # raise inside except without from
|
||||||
|
"E101", # indentation contains mixed spaces and tabs
|
||||||
|
"E402", # module level import not at top of file
|
||||||
|
"E501", # line too long
|
||||||
|
"E741", # ambiguous variable name
|
||||||
|
"F401", # "unused" imports
|
||||||
|
"F403", # can't detect undefined names from * import
|
||||||
|
"F405", # can't detect undefined names from * import
|
||||||
|
"F722", # syntax error in forward type annotation
|
||||||
|
"W191", # indentation contains tabs
|
||||||
|
"RUF001", # string contains ambiguous unicode character
|
||||||
|
"UP030", # Use implicit references for positional format fields (translations)
|
||||||
|
"UP031", # Use format specifiers instead of percent format
|
||||||
|
"UP032", # Use f-string instead of `format` call (translations)
|
||||||
|
]
|
||||||
|
typing-modules = ["frappe.types.DF"]
|
||||||
|
|
||||||
|
[tool.ruff.format]
|
||||||
|
quote-style = "double"
|
||||||
|
indent-style = "tab"
|
||||||
|
docstring-code-format = true
|
||||||
Loading…
x
Reference in New Issue
Block a user