Compare commits

..

No commits in common. "main" and "v1.52.2" have entirely different histories.

154 changed files with 22333 additions and 55910 deletions

1
.gitignore vendored
View File

@ -7,5 +7,6 @@ dev-dist
tags tags
node_modules node_modules
crm/public/frontend crm/public/frontend
frontend/yarn.lock
crm/www/crm.html crm/www/crm.html
build build

View File

@ -1,4 +1,4 @@
__version__ = "1.53.1" __version__ = "1.52.2"
__title__ = "Frappe CRM" __title__ = "Frappe CRM"

View File

@ -1,32 +0,0 @@
import frappe
@frappe.whitelist()
def get_assignment_rules_list():
assignment_rules = []
for docname in frappe.get_all(
"Assignment Rule", filters={"document_type": ["in", ["CRM Lead", "CRM Deal"]]}
):
doc = frappe.get_value(
"Assignment Rule",
docname,
fieldname=[
"name",
"description",
"disabled",
"priority",
],
as_dict=True,
)
users_exists = bool(frappe.db.exists("Assignment Rule User", {"parent": docname.name}))
assignment_rules.append({**doc, "users_exists": users_exists})
return assignment_rules
@frappe.whitelist()
def duplicate_assignment_rule(docname, new_name):
doc = frappe.get_doc("Assignment Rule", docname)
doc.name = new_name
doc.assignment_rule_name = new_name
doc.insert()
return doc

View File

@ -750,11 +750,7 @@ def getCounts(d, doctype):
@frappe.whitelist() @frappe.whitelist()
def get_linked_docs_of_document(doctype, docname): def get_linked_docs_of_document(doctype, docname):
try:
doc = frappe.get_doc(doctype, docname) doc = frappe.get_doc(doctype, docname)
except frappe.DoesNotExistError:
return []
linked_docs = get_linked_docs(doc) linked_docs = get_linked_docs(doc)
dynamic_linked_docs = get_dynamic_linked_docs(doc) dynamic_linked_docs = get_dynamic_linked_docs(doc)
@ -763,14 +759,7 @@ def get_linked_docs_of_document(doctype, docname):
docs_data = [] docs_data = []
for doc in linked_docs: for doc in linked_docs:
if not doc.get("reference_doctype") or not doc.get("reference_docname"):
continue
try:
data = frappe.get_doc(doc["reference_doctype"], doc["reference_docname"]) data = frappe.get_doc(doc["reference_doctype"], doc["reference_docname"])
except (frappe.DoesNotExistError, frappe.ValidationError):
continue
title = data.get("title") title = data.get("title")
if data.doctype == "CRM Call Log": if data.doctype == "CRM Call Log":
title = f"Call from {data.get('from')} to {data.get('to')}" title = f"Call from {data.get('from')} to {data.get('to')}"
@ -778,9 +767,6 @@ def get_linked_docs_of_document(doctype, docname):
if data.doctype == "CRM Deal": if data.doctype == "CRM Deal":
title = data.get("organization") title = data.get("organization")
if data.doctype == "CRM Notification":
title = data.get("message")
docs_data.append( docs_data.append(
{ {
"doc": data.doctype, "doc": data.doctype,
@ -793,41 +779,17 @@ def get_linked_docs_of_document(doctype, docname):
def remove_doc_link(doctype, docname): def remove_doc_link(doctype, docname):
if not doctype or not docname:
return
try:
linked_doc_data = frappe.get_doc(doctype, docname) linked_doc_data = frappe.get_doc(doctype, docname)
if doctype == "CRM Notification":
delete_notification_type = {
"notification_type_doctype": "",
"notification_type_doc": "",
}
delete_references = {
"reference_doctype": "",
"reference_name": "",
}
if linked_doc_data.get("notification_type_doctype") == linked_doc_data.get("reference_doctype"):
delete_references.update(delete_notification_type)
linked_doc_data.update(delete_references)
else:
linked_doc_data.update( linked_doc_data.update(
{ {
"reference_doctype": "", "reference_doctype": None,
"reference_docname": "", "reference_docname": None,
} }
) )
linked_doc_data.save(ignore_permissions=True) linked_doc_data.save(ignore_permissions=True)
except (frappe.DoesNotExistError, frappe.ValidationError):
pass
def remove_contact_link(doctype, docname): def remove_contact_link(doctype, docname):
if not doctype or not docname:
return
try:
linked_doc_data = frappe.get_doc(doctype, docname) linked_doc_data = frappe.get_doc(doctype, docname)
linked_doc_data.update( linked_doc_data.update(
{ {
@ -836,8 +798,6 @@ def remove_contact_link(doctype, docname):
} }
) )
linked_doc_data.save(ignore_permissions=True) linked_doc_data.save(ignore_permissions=True)
except (frappe.DoesNotExistError, frappe.ValidationError):
pass
@frappe.whitelist() @frappe.whitelist()
@ -846,19 +806,13 @@ def remove_linked_doc_reference(items, remove_contact=None, delete=False):
items = frappe.parse_json(items) items = frappe.parse_json(items)
for item in items: for item in items:
if not item.get("doctype") or not item.get("docname"):
continue
try:
if remove_contact: if remove_contact:
remove_contact_link(item["doctype"], item["docname"]) remove_contact_link(item["doctype"], item["docname"])
else: else:
remove_doc_link(item["doctype"], item["docname"]) remove_doc_link(item["doctype"], item["docname"])
if delete: if delete:
frappe.delete_doc(item["doctype"], item["docname"]) frappe.delete_doc(item["doctype"], item["docname"])
except (frappe.DoesNotExistError, frappe.ValidationError):
# Skip if document doesn't exist or has validation errors
continue
return "success" return "success"
@ -867,27 +821,10 @@ def remove_linked_doc_reference(items, remove_contact=None, delete=False):
def delete_bulk_docs(doctype, items, delete_linked=False): def delete_bulk_docs(doctype, items, delete_linked=False):
from frappe.desk.reportview import delete_bulk from frappe.desk.reportview import delete_bulk
if not doctype:
frappe.throw("Doctype is required")
if not items:
frappe.throw("Items are required")
items = frappe.parse_json(items) items = frappe.parse_json(items)
if not isinstance(items, list):
frappe.throw("Items must be a list")
for doc in items: for doc in items:
try:
if not frappe.db.exists(doctype, doc):
frappe.log_error(f"Document {doctype} {doc} does not exist", "Bulk Delete Error")
continue
linked_docs = get_linked_docs_of_document(doctype, doc) linked_docs = get_linked_docs_of_document(doctype, doc)
for linked_doc in linked_docs: for linked_doc in linked_docs:
if not linked_doc.get("reference_doctype") or not linked_doc.get("reference_docname"):
continue
remove_linked_doc_reference( remove_linked_doc_reference(
[ [
{ {
@ -898,10 +835,6 @@ def delete_bulk_docs(doctype, items, delete_linked=False):
remove_contact=doctype == "Contact", remove_contact=doctype == "Contact",
delete=delete_linked, delete=delete_linked,
) )
except Exception as e:
frappe.log_error(
f"Error processing linked docs for {doctype} {doc}: {str(e)}", "Bulk Delete Error"
)
if len(items) > 10: if len(items) > 10:
frappe.enqueue("frappe.desk.reportview.delete_bulk", doctype=doctype, items=items) frappe.enqueue("frappe.desk.reportview.delete_bulk", doctype=doctype, items=items)

View File

@ -10,13 +10,6 @@ from crm.fcrm.doctype.crm_notification.crm_notification import notify_user
def validate(doc, method): def validate(doc, method):
if doc.type == "Incoming" and doc.get("from"): if doc.type == "Incoming" and doc.get("from"):
name, doctype = get_lead_or_deal_from_number(doc.get("from")) name, doctype = get_lead_or_deal_from_number(doc.get("from"))
if name != None:
doc.reference_doctype = doctype
doc.reference_name = name
if doc.type == "Outgoing" and doc.get("to"):
name, doctype = get_lead_or_deal_from_number(doc.get("to"))
if name != None:
doc.reference_doctype = doctype doc.reference_doctype = doctype
doc.reference_name = name doc.reference_name = name
@ -36,7 +29,7 @@ def on_update(doc, method):
def notify_agent(doc): def notify_agent(doc):
if doc.type == "Incoming": if doc.type == "Incoming":
doctype = doc.reference_doctype doctype = doc.reference_doctype
if doctype and doctype.startswith("CRM "): if doctype.startswith("CRM "):
doctype = doctype[4:].lower() doctype = doctype[4:].lower()
notification_text = f""" notification_text = f"""
<div class="mb-2 leading-5 text-ink-gray-5"> <div class="mb-2 leading-5 text-ink-gray-5">

View File

@ -26,9 +26,8 @@ def create_default_manager_dashboard(force=False):
doc.title = "Manager Dashboard" doc.title = "Manager Dashboard"
doc.layout = default_manager_dashboard_layout() doc.layout = default_manager_dashboard_layout()
doc.insert(ignore_permissions=True) doc.insert(ignore_permissions=True)
else: elif force:
doc = frappe.get_doc("CRM Dashboard", "Manager Dashboard") doc = frappe.get_doc("CRM Dashboard", "Manager Dashboard")
if force:
doc.layout = default_manager_dashboard_layout() doc.layout = default_manager_dashboard_layout()
doc.save(ignore_permissions=True) doc.save(ignore_permissions=True)
return doc.layout return doc.layout

View File

@ -129,13 +129,15 @@
"fieldname": "email", "fieldname": "email",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Primary Email", "label": "Primary Email",
"options": "Email" "options": "Email",
"read_only": 1
}, },
{ {
"fieldname": "mobile_no", "fieldname": "mobile_no",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Primary Mobile No", "label": "Primary Mobile No",
"options": "Phone" "options": "Phone",
"read_only": 1
}, },
{ {
"default": "Qualification", "default": "Qualification",
@ -249,7 +251,8 @@
"fieldname": "phone", "fieldname": "phone",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Primary Phone", "label": "Primary Phone",
"options": "Phone" "options": "Phone",
"read_only": 1
}, },
{ {
"fieldname": "log_tab", "fieldname": "log_tab",
@ -432,7 +435,7 @@
"grid_page_length": 50, "grid_page_length": 50,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2025-08-26 12:12:56.324245", "modified": "2025-07-13 11:54:20.608489",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "FCRM", "module": "FCRM",
"name": "CRM Deal", "name": "CRM Deal",

View File

@ -25,7 +25,7 @@ class CRMDeal(Document):
add_status_change_log(self) add_status_change_log(self)
if frappe.db.get_value("CRM Deal Status", self.status, "type") == "Won": if frappe.db.get_value("CRM Deal Status", self.status, "type") == "Won":
self.closed_date = frappe.utils.nowdate() self.closed_date = frappe.utils.nowdate()
self.validate_forecasting_fields() self.validate_forcasting_fields()
self.validate_lost_reason() self.validate_lost_reason()
self.update_exchange_rate() self.update_exchange_rate()
@ -151,21 +151,9 @@ class CRMDeal(Document):
if not self.probability or self.probability == 0: if not self.probability or self.probability == 0:
self.probability = frappe.db.get_value("CRM Deal Status", self.status, "probability") or 0 self.probability = frappe.db.get_value("CRM Deal Status", self.status, "probability") or 0
def update_expected_deal_value(self): def validate_forcasting_fields(self):
"""
Update the expected deal value based on the net total or total.
"""
if (
frappe.db.get_single_value("FCRM Settings", "auto_update_expected_deal_value")
and (self.net_total or self.total)
and self.expected_deal_value
):
self.expected_deal_value = self.net_total or self.total
def validate_forecasting_fields(self):
self.update_closed_date() self.update_closed_date()
self.update_default_probability() self.update_default_probability()
self.update_expected_deal_value()
if frappe.db.get_single_value("FCRM Settings", "enable_forecasting"): if frappe.db.get_single_value("FCRM Settings", "enable_forecasting"):
if not self.expected_deal_value or self.expected_deal_value == 0: if not self.expected_deal_value or self.expected_deal_value == 0:
frappe.throw(_("Expected Deal Value is required."), frappe.MandatoryError) frappe.throw(_("Expected Deal Value is required."), frappe.MandatoryError)

View File

@ -63,7 +63,8 @@
"fieldname": "twiml_sid", "fieldname": "twiml_sid",
"fieldtype": "Data", "fieldtype": "Data",
"label": "TwiML SID", "label": "TwiML SID",
"permlevel": 1 "permlevel": 1,
"read_only": 1
}, },
{ {
"fieldname": "section_break_ssqj", "fieldname": "section_break_ssqj",
@ -104,7 +105,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2025-08-19 13:36:19.823197", "modified": "2025-01-15 19:35:13.406254",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "FCRM", "module": "FCRM",
"name": "CRM Twilio Settings", "name": "CRM Twilio Settings",
@ -151,7 +152,6 @@
"write": 1 "write": 1
} }
], ],
"row_format": "Dynamic",
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],

View File

@ -8,7 +8,6 @@
"defaults_tab", "defaults_tab",
"restore_defaults", "restore_defaults",
"enable_forecasting", "enable_forecasting",
"auto_update_expected_deal_value",
"currency_tab", "currency_tab",
"currency", "currency",
"exchange_rate_provider_section", "exchange_rate_provider_section",
@ -106,19 +105,12 @@
{ {
"fieldname": "column_break_vqck", "fieldname": "column_break_vqck",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"default": "1",
"description": "Automatically update \"Expected Deal Value\" based on the total value of associated products in a deal",
"fieldname": "auto_update_expected_deal_value",
"fieldtype": "Check",
"label": "Auto Update Expected Deal Value"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2025-09-16 17:33:26.406549", "modified": "2025-07-29 11:26:50.420614",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "FCRM", "module": "FCRM",
"name": "FCRM Settings", "name": "FCRM Settings",

View File

@ -25,8 +25,6 @@ def after_install(force=False):
add_standard_dropdown_items() add_standard_dropdown_items()
add_default_scripts() add_default_scripts()
create_default_manager_dashboard(force) create_default_manager_dashboard(force)
create_assignment_rule_custom_fields()
add_assignment_rule_property_setters()
frappe.db.commit() frappe.db.commit()
@ -194,7 +192,7 @@ def add_default_fields_layout(force=False):
}, },
"CRM Deal-Data Fields": { "CRM Deal-Data Fields": {
"doctype": "CRM Deal", "doctype": "CRM Deal",
"layout": '[{"name":"first_tab","sections":[{"label":"Details","name":"details_section","opened":true,"columns":[{"name":"column_z9XL","fields":["organization","annual_revenue","next_step"]},{"name":"column_gM4w","fields":["website","closed_date","deal_owner"]},{"name":"column_gWmE","fields":["territory","probability"]}]},{"label":"Products","name":"section_jHhQ","opened":true,"columns":[{"name":"column_xiNF","fields":["products"]}],"editingLabel":false,"hideLabel":true},{"label":"New Section","name":"section_WNOQ","opened":true,"columns":[{"name":"column_ziBW","fields":["total"]},{"label":"","name":"column_wuwA","fields":["net_total"]}],"hideBorder":true,"hideLabel":true}]}]', "layout": '[{"label": "Details", "name": "details_section", "opened": true, "columns": [{"name": "column_z9XL", "fields": ["organization", "annual_revenue", "next_step"]}, {"name": "column_gM4w", "fields": ["website", "close_date", "deal_owner"]}, {"name": "column_gWmE", "fields": ["territory", "probability"]}]}]',
}, },
} }
@ -423,80 +421,3 @@ def add_default_scripts():
for doctype in ["CRM Lead", "CRM Deal"]: for doctype in ["CRM Lead", "CRM Deal"]:
create_product_details_script(doctype) create_product_details_script(doctype)
create_forecasting_script() create_forecasting_script()
def add_assignment_rule_property_setters():
"""Add a property setter to the Assignment Rule DocType for assign_condition and unassign_condition."""
default_fields = {
"doctype": "Property Setter",
"doctype_or_field": "DocField",
"doc_type": "Assignment Rule",
"property_type": "Data",
"is_system_generated": 1,
}
if not frappe.db.exists("Property Setter", {"name": "Assignment Rule-assign_condition-depends_on"}):
frappe.get_doc(
{
**default_fields,
"name": "Assignment Rule-assign_condition-depends_on",
"field_name": "assign_condition",
"property": "depends_on",
"value": "eval: !doc.assign_condition_json",
}
).insert()
else:
frappe.db.set_value(
"Property Setter",
{"name": "Assignment Rule-assign_condition-depends_on"},
"value",
"eval: !doc.assign_condition_json",
)
if not frappe.db.exists("Property Setter", {"name": "Assignment Rule-unassign_condition-depends_on"}):
frappe.get_doc(
{
**default_fields,
"name": "Assignment Rule-unassign_condition-depends_on",
"field_name": "unassign_condition",
"property": "depends_on",
"value": "eval: !doc.unassign_condition_json",
}
).insert()
else:
frappe.db.set_value(
"Property Setter",
{"name": "Assignment Rule-unassign_condition-depends_on"},
"value",
"eval: !doc.unassign_condition_json",
)
def create_assignment_rule_custom_fields():
if not frappe.get_meta("Assignment Rule").has_field("assign_condition_json"):
click.secho("* Installing Custom Fields in Assignment Rule")
create_custom_fields(
{
"Assignment Rule": [
{
"description": "Autogenerated field by CRM App",
"fieldname": "assign_condition_json",
"fieldtype": "Code",
"label": "Assign Condition JSON",
"insert_after": "assign_condition",
"depends_on": "eval: doc.assign_condition_json",
},
{
"description": "Autogenerated field by CRM App",
"fieldname": "unassign_condition_json",
"fieldtype": "Code",
"label": "Unassign Condition JSON",
"insert_after": "unassign_condition",
"depends_on": "eval: doc.unassign_condition_json",
},
],
}
)
frappe.clear_cache(doctype="Assignment Rule")

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -15,5 +15,3 @@ crm.patches.v1_0.move_twilio_agent_to_telephony_agent
crm.patches.v1_0.create_default_scripts # 13-06-2025 crm.patches.v1_0.create_default_scripts # 13-06-2025
crm.patches.v1_0.update_deal_status_probabilities crm.patches.v1_0.update_deal_status_probabilities
crm.patches.v1_0.update_deal_status_type crm.patches.v1_0.update_deal_status_type
crm.patches.v1_0.create_default_lost_reasons
crm.patches.v1_0.add_fields_in_assignment_rule

View File

@ -1,9 +0,0 @@
from crm.install import (
add_assignment_rule_property_setters,
create_assignment_rule_custom_fields,
)
def execute():
create_assignment_rule_custom_fields()
add_assignment_rule_property_setters()

View File

@ -1,5 +0,0 @@
from crm.install import add_default_lost_reasons
def execute():
add_default_lost_reasons()

@ -1 +1 @@
Subproject commit c9a0fc937cc897864857271b3708a0c675379015 Subproject commit b295b54aaa3a2a22f455df819d87433dc6b9ff6a

1
frontend/.gitignore vendored
View File

@ -3,4 +3,3 @@ node_modules
dist dist
dist-ssr dist-ssr
*.local *.local
components.d.ts

View File

@ -33,7 +33,7 @@ declare module 'vue' {
Autocomplete: typeof import('./src/components/frappe-ui/Autocomplete.vue')['default'] Autocomplete: typeof import('./src/components/frappe-ui/Autocomplete.vue')['default']
AvatarIcon: typeof import('./src/components/Icons/AvatarIcon.vue')['default'] AvatarIcon: typeof import('./src/components/Icons/AvatarIcon.vue')['default']
BrandLogo: typeof import('./src/components/BrandLogo.vue')['default'] BrandLogo: typeof import('./src/components/BrandLogo.vue')['default']
BrandSettings: typeof import('./src/components/Settings/BrandSettings.vue')['default'] BrandSettings: typeof import('./src/components/Settings/General/BrandSettings.vue')['default']
BulkDeleteLinkedDocModal: typeof import('./src/components/BulkDeleteLinkedDocModal.vue')['default'] BulkDeleteLinkedDocModal: typeof import('./src/components/BulkDeleteLinkedDocModal.vue')['default']
CalendarIcon: typeof import('./src/components/Icons/CalendarIcon.vue')['default'] CalendarIcon: typeof import('./src/components/Icons/CalendarIcon.vue')['default']
CallArea: typeof import('./src/components/Activities/CallArea.vue')['default'] CallArea: typeof import('./src/components/Activities/CallArea.vue')['default']
@ -63,7 +63,7 @@ declare module 'vue' {
CountUpTimer: typeof import('./src/components/CountUpTimer.vue')['default'] CountUpTimer: typeof import('./src/components/CountUpTimer.vue')['default']
CreateDocumentModal: typeof import('./src/components/Modals/CreateDocumentModal.vue')['default'] CreateDocumentModal: typeof import('./src/components/Modals/CreateDocumentModal.vue')['default']
CRMLogo: typeof import('./src/components/Icons/CRMLogo.vue')['default'] CRMLogo: typeof import('./src/components/Icons/CRMLogo.vue')['default']
CurrencySettings: typeof import('./src/components/Settings/CurrencySettings.vue')['default'] CurrencySettings: typeof import('./src/components/Settings/General/CurrencySettings.vue')['default']
CustomActions: typeof import('./src/components/CustomActions.vue')['default'] CustomActions: typeof import('./src/components/CustomActions.vue')['default']
DashboardGrid: typeof import('./src/components/Dashboard/DashboardGrid.vue')['default'] DashboardGrid: typeof import('./src/components/Dashboard/DashboardGrid.vue')['default']
DashboardIcon: typeof import('./src/components/Icons/DashboardIcon.vue')['default'] DashboardIcon: typeof import('./src/components/Icons/DashboardIcon.vue')['default']
@ -85,6 +85,7 @@ declare module 'vue' {
DragIcon: typeof import('./src/components/Icons/DragIcon.vue')['default'] DragIcon: typeof import('./src/components/Icons/DragIcon.vue')['default']
DragVerticalIcon: typeof import('./src/components/Icons/DragVerticalIcon.vue')['default'] DragVerticalIcon: typeof import('./src/components/Icons/DragVerticalIcon.vue')['default']
Dropdown: typeof import('./src/components/frappe-ui/Dropdown.vue')['default'] Dropdown: typeof import('./src/components/frappe-ui/Dropdown.vue')['default']
DropdownItem: typeof import('./src/components/DropdownItem.vue')['default']
DuplicateIcon: typeof import('./src/components/Icons/DuplicateIcon.vue')['default'] DuplicateIcon: typeof import('./src/components/Icons/DuplicateIcon.vue')['default']
DurationIcon: typeof import('./src/components/Icons/DurationIcon.vue')['default'] DurationIcon: typeof import('./src/components/Icons/DurationIcon.vue')['default']
EditEmailTemplate: typeof import('./src/components/Settings/EmailTemplate/EditEmailTemplate.vue')['default'] EditEmailTemplate: typeof import('./src/components/Settings/EmailTemplate/EditEmailTemplate.vue')['default']
@ -127,10 +128,11 @@ declare module 'vue' {
FileVideoIcon: typeof import('./src/components/Icons/FileVideoIcon.vue')['default'] FileVideoIcon: typeof import('./src/components/Icons/FileVideoIcon.vue')['default']
Filter: typeof import('./src/components/Filter.vue')['default'] Filter: typeof import('./src/components/Filter.vue')['default']
FilterIcon: typeof import('./src/components/Icons/FilterIcon.vue')['default'] FilterIcon: typeof import('./src/components/Icons/FilterIcon.vue')['default']
ForecastingSettings: typeof import('./src/components/Settings/ForecastingSettings.vue')['default']
FormattedInput: typeof import('./src/components/Controls/FormattedInput.vue')['default'] FormattedInput: typeof import('./src/components/Controls/FormattedInput.vue')['default']
FrappeCloudIcon: typeof import('./src/components/Icons/FrappeCloudIcon.vue')['default'] FrappeCloudIcon: typeof import('./src/components/Icons/FrappeCloudIcon.vue')['default']
GenderIcon: typeof import('./src/components/Icons/GenderIcon.vue')['default'] GenderIcon: typeof import('./src/components/Icons/GenderIcon.vue')['default']
GeneralSettings: typeof import('./src/components/Settings/General/GeneralSettings.vue')['default']
GeneralSettingsPage: typeof import('./src/components/Settings/General/GeneralSettingsPage.vue')['default']
GlobalModals: typeof import('./src/components/Modals/GlobalModals.vue')['default'] GlobalModals: typeof import('./src/components/Modals/GlobalModals.vue')['default']
GoogleIcon: typeof import('./src/components/Icons/GoogleIcon.vue')['default'] GoogleIcon: typeof import('./src/components/Icons/GoogleIcon.vue')['default']
Grid: typeof import('./src/components/Controls/Grid.vue')['default'] Grid: typeof import('./src/components/Controls/Grid.vue')['default']
@ -141,7 +143,7 @@ declare module 'vue' {
GroupByIcon: typeof import('./src/components/Icons/GroupByIcon.vue')['default'] GroupByIcon: typeof import('./src/components/Icons/GroupByIcon.vue')['default']
HeartIcon: typeof import('./src/components/Icons/HeartIcon.vue')['default'] HeartIcon: typeof import('./src/components/Icons/HeartIcon.vue')['default']
HelpIcon: typeof import('./src/components/Icons/HelpIcon.vue')['default'] HelpIcon: typeof import('./src/components/Icons/HelpIcon.vue')['default']
HomeActions: typeof import('./src/components/Settings/HomeActions.vue')['default'] HomeActions: typeof import('./src/components/Settings/General/HomeActions.vue')['default']
Icon: typeof import('./src/components/Icon.vue')['default'] Icon: typeof import('./src/components/Icon.vue')['default']
IconPicker: typeof import('./src/components/IconPicker.vue')['default'] IconPicker: typeof import('./src/components/IconPicker.vue')['default']
ImageUploader: typeof import('./src/components/Controls/ImageUploader.vue')['default'] ImageUploader: typeof import('./src/components/Controls/ImageUploader.vue')['default']
@ -168,6 +170,10 @@ declare module 'vue' {
LoadingIndicator: typeof import('./src/components/Icons/LoadingIndicator.vue')['default'] LoadingIndicator: typeof import('./src/components/Icons/LoadingIndicator.vue')['default']
LostReasonModal: typeof import('./src/components/Modals/LostReasonModal.vue')['default'] LostReasonModal: typeof import('./src/components/Modals/LostReasonModal.vue')['default']
LucideCalendar: typeof import('~icons/lucide/calendar')['default'] LucideCalendar: typeof import('~icons/lucide/calendar')['default']
LucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
LucidePenLine: typeof import('~icons/lucide/pen-line')['default']
LucideRefreshCcw: typeof import('~icons/lucide/refresh-ccw')['default']
LucideX: typeof import('~icons/lucide/x')['default']
MarkAsDoneIcon: typeof import('./src/components/Icons/MarkAsDoneIcon.vue')['default'] MarkAsDoneIcon: typeof import('./src/components/Icons/MarkAsDoneIcon.vue')['default']
MaximizeIcon: typeof import('./src/components/Icons/MaximizeIcon.vue')['default'] MaximizeIcon: typeof import('./src/components/Icons/MaximizeIcon.vue')['default']
MenuIcon: typeof import('./src/components/Icons/MenuIcon.vue')['default'] MenuIcon: typeof import('./src/components/Icons/MenuIcon.vue')['default']
@ -182,6 +188,7 @@ declare module 'vue' {
MultiSelectEmailInput: typeof import('./src/components/Controls/MultiSelectEmailInput.vue')['default'] MultiSelectEmailInput: typeof import('./src/components/Controls/MultiSelectEmailInput.vue')['default']
MultiSelectUserInput: typeof import('./src/components/Controls/MultiSelectUserInput.vue')['default'] MultiSelectUserInput: typeof import('./src/components/Controls/MultiSelectUserInput.vue')['default']
MuteIcon: typeof import('./src/components/Icons/MuteIcon.vue')['default'] MuteIcon: typeof import('./src/components/Icons/MuteIcon.vue')['default']
NestedPopover: typeof import('./src/components/NestedPopover.vue')['default']
NewEmailTemplate: typeof import('./src/components/Settings/EmailTemplate/NewEmailTemplate.vue')['default'] NewEmailTemplate: typeof import('./src/components/Settings/EmailTemplate/NewEmailTemplate.vue')['default']
NoteArea: typeof import('./src/components/Activities/NoteArea.vue')['default'] NoteArea: typeof import('./src/components/Activities/NoteArea.vue')['default']
NoteIcon: typeof import('./src/components/Icons/NoteIcon.vue')['default'] NoteIcon: typeof import('./src/components/Icons/NoteIcon.vue')['default']
@ -200,8 +207,6 @@ declare module 'vue' {
PlaybackSpeedOption: typeof import('./src/components/Activities/PlaybackSpeedOption.vue')['default'] PlaybackSpeedOption: typeof import('./src/components/Activities/PlaybackSpeedOption.vue')['default']
PlayIcon: typeof import('./src/components/Icons/PlayIcon.vue')['default'] PlayIcon: typeof import('./src/components/Icons/PlayIcon.vue')['default']
Popover: typeof import('./src/components/frappe-ui/Popover.vue')['default'] Popover: typeof import('./src/components/frappe-ui/Popover.vue')['default']
PrimaryDropdown: typeof import('./src/components/PrimaryDropdown.vue')['default']
PrimaryDropdownItem: typeof import('./src/components/PrimaryDropdownItem.vue')['default']
ProfileSettings: typeof import('./src/components/Settings/ProfileSettings.vue')['default'] ProfileSettings: typeof import('./src/components/Settings/ProfileSettings.vue')['default']
QuickEntryModal: typeof import('./src/components/Modals/QuickEntryModal.vue')['default'] QuickEntryModal: typeof import('./src/components/Modals/QuickEntryModal.vue')['default']
QuickFilterField: typeof import('./src/components/QuickFilterField.vue')['default'] QuickFilterField: typeof import('./src/components/QuickFilterField.vue')['default']
@ -228,7 +233,6 @@ declare module 'vue' {
SmileIcon: typeof import('./src/components/Icons/SmileIcon.vue')['default'] SmileIcon: typeof import('./src/components/Icons/SmileIcon.vue')['default']
SortBy: typeof import('./src/components/SortBy.vue')['default'] SortBy: typeof import('./src/components/SortBy.vue')['default']
SortIcon: typeof import('./src/components/Icons/SortIcon.vue')['default'] SortIcon: typeof import('./src/components/Icons/SortIcon.vue')['default']
SparkleIcon: typeof import('./src/components/Icons/SparkleIcon.vue')['default']
SquareAsterisk: typeof import('./src/components/Icons/SquareAsterisk.vue')['default'] SquareAsterisk: typeof import('./src/components/Icons/SquareAsterisk.vue')['default']
StepsIcon: typeof import('./src/components/Icons/StepsIcon.vue')['default'] StepsIcon: typeof import('./src/components/Icons/StepsIcon.vue')['default']
SuccessIcon: typeof import('./src/components/Icons/SuccessIcon.vue')['default'] SuccessIcon: typeof import('./src/components/Icons/SuccessIcon.vue')['default']

View File

@ -13,7 +13,7 @@
"@tiptap/extension-paragraph": "^2.12.0", "@tiptap/extension-paragraph": "^2.12.0",
"@twilio/voice-sdk": "^2.10.2", "@twilio/voice-sdk": "^2.10.2",
"@vueuse/integrations": "^10.3.0", "@vueuse/integrations": "^10.3.0",
"frappe-ui": "^0.1.201", "frappe-ui": "^0.1.171",
"gemoji": "^8.1.0", "gemoji": "^8.1.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mime": "^4.0.1", "mime": "^4.0.1",

View File

@ -238,9 +238,12 @@
<Button <Button
class="!size-4" class="!size-4"
variant="ghost" variant="ghost"
:icon="SelectIcon"
@click="activity.show_others = !activity.show_others" @click="activity.show_others = !activity.show_others"
/> >
<template #icon>
<SelectIcon />
</template>
</Button>
</div> </div>
<div <div
v-else v-else

View File

@ -9,17 +9,23 @@
<Button <Button
v-if="title == 'Emails'" v-if="title == 'Emails'"
variant="solid" variant="solid"
:label="__('New Email')"
iconLeft="plus"
@click="emailBox.show = true" @click="emailBox.show = true"
/> >
<template #prefix>
<FeatherIcon name="plus" class="h-4 w-4" />
</template>
<span>{{ __('New Email') }}</span>
</Button>
<Button <Button
v-else-if="title == 'Comments'" v-else-if="title == 'Comments'"
variant="solid" variant="solid"
:label="__('New Comment')"
iconLeft="plus"
@click="emailBox.showComment = true" @click="emailBox.showComment = true"
/> >
<template #prefix>
<FeatherIcon name="plus" class="h-4 w-4" />
</template>
<span>{{ __('New Comment') }}</span>
</Button>
<MultiActionButton <MultiActionButton
v-else-if="title == 'Calls'" v-else-if="title == 'Calls'"
variant="solid" variant="solid"
@ -28,46 +34,60 @@
<Button <Button
v-else-if="title == 'Notes'" v-else-if="title == 'Notes'"
variant="solid" variant="solid"
:label="__('New Note')"
iconLeft="plus"
@click="modalRef.showNote()" @click="modalRef.showNote()"
/> >
<template #prefix>
<FeatherIcon name="plus" class="h-4 w-4" />
</template>
<span>{{ __('New Note') }}</span>
</Button>
<Button <Button
v-else-if="title == 'Tasks'" v-else-if="title == 'Tasks'"
variant="solid" variant="solid"
:label="__('New Task')"
iconLeft="plus"
@click="modalRef.showTask()" @click="modalRef.showTask()"
/> >
<template #prefix>
<FeatherIcon name="plus" class="h-4 w-4" />
</template>
<span>{{ __('New Task') }}</span>
</Button>
<Button <Button
v-else-if="title == 'Attachments'" v-else-if="title == 'Attachments'"
variant="solid" variant="solid"
:label="__('Upload Attachment')"
iconLeft="plus"
@click="showFilesUploader = true" @click="showFilesUploader = true"
/> >
<template #prefix>
<FeatherIcon name="plus" class="h-4 w-4" />
</template>
<span>{{ __('Upload Attachment') }}</span>
</Button>
<div class="flex gap-2 shrink-0" v-else-if="title == 'WhatsApp'"> <div class="flex gap-2 shrink-0" v-else-if="title == 'WhatsApp'">
<Button <Button
:label="__('Send Template')" :label="__('Send Template')"
@click="showWhatsappTemplates = true" @click="showWhatsappTemplates = true"
/> />
<Button <Button variant="solid" @click="whatsappBox.show()">
variant="solid" <template #prefix>
:label="__('New Message')" <FeatherIcon name="plus" class="h-4 w-4" />
iconLeft="plus" </template>
@click="whatsappBox.show()" <span>{{ __('New Message') }}</span>
/> </Button>
</div> </div>
<Dropdown v-else :options="defaultActions" @click.stop> <Dropdown v-else :options="defaultActions" @click.stop>
<template v-slot="{ open }"> <template v-slot="{ open }">
<Button <Button variant="solid" class="flex items-center gap-1">
variant="solid" <template #prefix>
class="flex items-center gap-1" <FeatherIcon name="plus" class="h-4 w-4" />
:label="__('New')" </template>
iconLeft="plus" <span>{{ __('New') }}</span>
:iconRight="open ? 'chevron-up' : 'chevron-down'" <template #suffix>
<FeatherIcon
:name="open ? 'chevron-up' : 'chevron-down'"
class="h-4 w-4"
/> />
</template> </template>
</Button>
</template>
</Dropdown> </Dropdown>
</div> </div>
</template> </template>

View File

@ -38,10 +38,13 @@
</div> </div>
</Tooltip> </Tooltip>
<div class="flex gap-1"> <div class="flex gap-1">
<Button <Tooltip
:tooltip=" :text="
attachment.is_private ? __('Make public') : __('Make private') attachment.is_private ? __('Make public') : __('Make private')
" "
>
<div>
<Button
class="!size-5" class="!size-5"
@click.stop=" @click.stop="
togglePrivate(attachment.name, attachment.is_private) togglePrivate(attachment.name, attachment.is_private)
@ -54,16 +57,24 @@
/> />
</template> </template>
</Button> </Button>
</div>
</Tooltip>
<Tooltip :text="__('Delete attachment')">
<div>
<Button <Button
:tooltip="__('Delete attachment')"
class="!size-5" class="!size-5"
@click.stop="() => deleteAttachment(attachment.name)" @click.stop="() => deleteAttachment(attachment.name)"
> >
<template #icon> <template #icon>
<FeatherIcon name="trash-2" class="size-3 text-ink-gray-7" /> <FeatherIcon
name="trash-2"
class="size-3 text-ink-gray-7"
/>
</template> </template>
</Button> </Button>
</div> </div>
</Tooltip>
</div>
</div> </div>
</div> </div>
<div <div

View File

@ -1,12 +1,12 @@
<template> <template>
<div class="w-full text-sm text-ink-gray-5"> <div class="w-full text-sm text-ink-gray-5">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Button <Button variant="ghost" @click="playPause">
variant="ghost" <template #icon>
class="text-ink-gray-5" <PlayIcon v-if="isPaused" class="size-4 text-ink-gray-5" />
:icon="isPaused ? PlayIcon : PauseIcon" <PauseIcon v-else class="size-4 text-ink-gray-5" />
@click="playPause" </template>
/> </Button>
<div class="flex gap-2 items-center justify-between flex-1"> <div class="flex gap-2 items-center justify-between flex-1">
<input <input
class="w-full slider !h-[0.5] bg-surface-gray-3 [&::-webkit-slider-thumb]:shadow [&::-webkit-slider-thumb:hover]:outline [&::-webkit-slider-thumb:hover]:outline-[0.5px]" class="w-full slider !h-[0.5] bg-surface-gray-3 [&::-webkit-slider-thumb]:shadow [&::-webkit-slider-thumb:hover]:outline [&::-webkit-slider-thumb:hover]:outline-[0.5px]"
@ -61,11 +61,11 @@
</Button> </Button>
</div> </div>
<Dropdown :options="options"> <Dropdown :options="options">
<Button <Button variant="ghost" @click="showPlaybackSpeed = false">
icon="more-horizontal" <template #icon>
variant="ghost" <FeatherIcon class="size-4" name="more-horizontal" />
@click="showPlaybackSpeed = false" </template>
/> </Button>
</Dropdown> </Dropdown>
</div> </div>
</div> </div>

View File

@ -14,10 +14,12 @@
<div class="flex gap-1"> <div class="flex gap-1">
<Button <Button
v-if="isManager() && !isMobileView" v-if="isManager() && !isMobileView"
:tooltip="__('Edit fields layout')"
:icon="EditIcon"
@click="showDataFieldsModal = true" @click="showDataFieldsModal = true"
/> >
<template #icon>
<EditIcon />
</template>
</Button>
<Button <Button
label="Save" label="Save"
:disabled="!document.isDirty" :disabled="!document.isDirty"

View File

@ -2,9 +2,7 @@
<div <div
class="cursor-pointer flex flex-col rounded-md shadow bg-surface-cards px-3 py-1.5 text-base transition-all duration-300 ease-in-out" class="cursor-pointer flex flex-col rounded-md shadow bg-surface-cards px-3 py-1.5 text-base transition-all duration-300 ease-in-out"
> >
<div <div class="-mb-0.5 flex items-center justify-between gap-2 truncate text-ink-gray-9">
class="-mb-0.5 flex items-center justify-between gap-2 truncate text-ink-gray-9"
>
<div class="flex items-center gap-2 truncate"> <div class="flex items-center gap-2 truncate">
<span>{{ activity.data.sender_full_name }}</span> <span>{{ activity.data.sender_full_name }}</span>
<span class="sm:flex hidden text-sm text-ink-gray-5"> <span class="sm:flex hidden text-sm text-ink-gray-5">
@ -30,20 +28,32 @@
</div> </div>
</Tooltip> </Tooltip>
<div class="flex gap-0.5"> <div class="flex gap-0.5">
<Tooltip :text="__('Reply')">
<div>
<Button <Button
:tooltip="__('Reply')"
variant="ghost" variant="ghost"
class="text-ink-gray-7" class="text-ink-gray-7"
:icon="ReplyIcon"
@click="reply(activity.data)" @click="reply(activity.data)"
/> >
<template #icon>
<ReplyIcon />
</template>
</Button>
</div>
</Tooltip>
<Tooltip :text="__('Reply All')">
<div>
<Button <Button
:tooltip="__('Reply All')"
variant="ghost" variant="ghost"
:icon="ReplyAllIcon"
class="text-ink-gray-7" class="text-ink-gray-7"
@click="reply(activity.data, true)" @click="reply(activity.data, true)"
/> >
<template #icon>
<ReplyAllIcon />
</template>
</Button>
</div>
</Tooltip>
</div> </div>
</div> </div>
</div> </div>

View File

@ -41,13 +41,13 @@
:options="taskStatusOptions(modalRef.updateTaskStatus, task)" :options="taskStatusOptions(modalRef.updateTaskStatus, task)"
@click.stop @click.stop
> >
<Button <Tooltip :text="__('Change Status')">
:tooltip="__('Change status')" <div>
variant="ghosted" <Button variant="ghosted" class="hover:bg-surface-gray-4">
class="hover:bg-surface-gray-4"
>
<TaskStatusIcon :status="task.status" /> <TaskStatusIcon :status="task.status" />
</Button> </Button>
</div>
</Tooltip>
</Dropdown> </Dropdown>
<Dropdown <Dropdown
:options="[ :options="[

View File

@ -1,7 +1,7 @@
<template> <template>
<Popover placement="bottom-end"> <NestedPopover>
<template #target="{ togglePopover }"> <template #target>
<div class="flex items-center" @click="togglePopover"> <div class="flex items-center">
<component <component
v-if="assignees?.length" v-if="assignees?.length"
:is="assignees?.length == 1 ? 'Button' : 'div'" :is="assignees?.length == 1 ? 'Button' : 'div'"
@ -11,23 +11,24 @@
<Button v-else :label="__('Assign to')" /> <Button v-else :label="__('Assign to')" />
</div> </div>
</template> </template>
<template #body="{ isOpen }"> <template #body="{ open }">
<AssignToBody <AssignToBody
v-show="isOpen" v-show="open"
v-model="assignees" v-model="assignees"
:docname="docname" :docname="docname"
:doctype="doctype" :doctype="doctype"
:open="isOpen" :open="open"
:onUpdate="ownerField && saveAssignees" :onUpdate="ownerField && saveAssignees"
/> />
</template> </template>
</Popover> </NestedPopover>
</template> </template>
<script setup> <script setup>
import NestedPopover from '@/components/NestedPopover.vue'
import MultipleAvatar from '@/components/MultipleAvatar.vue' import MultipleAvatar from '@/components/MultipleAvatar.vue'
import AssignToBody from '@/components/AssignToBody.vue' import AssignToBody from '@/components/AssignToBody.vue'
import { useDocument } from '@/data/document' import { useDocument } from '@/data/document'
import { toast, Popover } from 'frappe-ui' import { toast } from 'frappe-ui'
import { computed } from 'vue' import { computed } from 'vue'
const props = defineProps({ const props = defineProps({

View File

@ -25,6 +25,7 @@
:key="assignee.name" :key="assignee.name"
@click.stop @click.stop
> >
<div>
<div <div
class="flex items-center text-sm p-0.5 text-ink-gray-6 border border-outline-gray-1 bg-surface-modal rounded-full cursor-pointer" class="flex items-center text-sm p-0.5 text-ink-gray-6 border border-outline-gray-1 bg-surface-modal rounded-full cursor-pointer"
@click.stop @click.stop
@ -41,6 +42,7 @@
</template> </template>
</Button> </Button>
</div> </div>
</div>
</Tooltip> </Tooltip>
</div> </div>
</template> </template>
@ -72,7 +74,7 @@ import UserAvatar from '@/components/UserAvatar.vue'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
import { usersStore } from '@/stores/users' import { usersStore } from '@/stores/users'
import { capture } from '@/telemetry' import { capture } from '@/telemetry'
import { Tooltip, Switch, createResource } from 'frappe-ui' import { Tooltip, Switch, toast, createResource } from 'frappe-ui'
import { ref, watch } from 'vue' import { ref, watch } from 'vue'
const props = defineProps({ const props = defineProps({
@ -152,7 +154,6 @@ watch(
updateAssignees() updateAssignees()
} }
}, },
{ immediate: true },
) )
async function updateAssignees() { async function updateAssignees() {

View File

@ -5,9 +5,11 @@
:label="label" :label="label"
theme="gray" theme="gray"
variant="outline" variant="outline"
:iconLeft="getIcon()"
@click="toggleDialog()" @click="toggleDialog()"
> >
<template #prefix>
<component :is="getIcon()" class="h-4 w-4" />
</template>
<template #suffix> <template #suffix>
<slot name="suffix" /> <slot name="suffix" />
</template> </template>

View File

@ -13,7 +13,7 @@
</div> </div>
</div> </div>
<div> <div>
<div class="text-ink-gray-5 text-base"> <div class="text-ink-gray-5">
{{ {{
__('Are you sure you want to delete {0} items?', [ __('Are you sure you want to delete {0} items?', [
props.items?.length, props.items?.length,
@ -53,7 +53,7 @@
</div> </div>
</div> </div>
<div> <div>
<div class="text-ink-gray-5 text-base"> <div class="text-ink-gray-5">
{{ {{
confirmDeleteInfo.delete confirmDeleteInfo.delete
? __( ? __(

View File

@ -1,7 +1,7 @@
<template> <template>
<Popover placement="bottom-end"> <NestedPopover>
<template #target="{ togglePopover }"> <template #target>
<Button :label="__('Columns')" @click="togglePopover"> <Button :label="__('Columns')">
<template v-if="hideLabel"> <template v-if="hideLabel">
<ColumnsIcon class="h-4" /> <ColumnsIcon class="h-4" />
</template> </template>
@ -65,28 +65,37 @@
<Button <Button
class="w-full !justify-start !text-ink-gray-5" class="w-full !justify-start !text-ink-gray-5"
variant="ghost" variant="ghost"
@click="togglePopover()"
:label="__('Add Column')" :label="__('Add Column')"
iconLeft="plus" >
@click="togglePopover" <template #prefix>
/> <FeatherIcon name="plus" class="h-4" />
</template>
</Button>
</template> </template>
</Autocomplete> </Autocomplete>
<Button <Button
v-if="columnsUpdated" v-if="columnsUpdated"
class="w-full !justify-start !text-ink-gray-5" class="w-full !justify-start !text-ink-gray-5"
variant="ghost" variant="ghost"
:label="__('Reset Changes')"
:iconLeft="ReloadIcon"
@click="reset(close)" @click="reset(close)"
/> :label="__('Reset Changes')"
>
<template #prefix>
<ReloadIcon class="h-4" />
</template>
</Button>
<Button <Button
v-if="!is_default" v-if="!is_default"
class="w-full !justify-start !text-ink-gray-5" class="w-full !justify-start !text-ink-gray-5"
variant="ghost" variant="ghost"
:label="__('Reset to Default')"
:iconLeft="ReloadIcon"
@click="resetToDefault(close)" @click="resetToDefault(close)"
/> :label="__('Reset to Default')"
>
<template #prefix>
<ReloadIcon class="h-4" />
</template>
</Button>
</div> </div>
</div> </div>
<div v-else> <div v-else>
@ -135,7 +144,7 @@
</div> </div>
</div> </div>
</template> </template>
</Popover> </NestedPopover>
</template> </template>
<script setup> <script setup>
@ -143,9 +152,9 @@ import ColumnsIcon from '@/components/Icons/ColumnsIcon.vue'
import EditIcon from '@/components/Icons/EditIcon.vue' import EditIcon from '@/components/Icons/EditIcon.vue'
import DragIcon from '@/components/Icons/DragIcon.vue' import DragIcon from '@/components/Icons/DragIcon.vue'
import ReloadIcon from '@/components/Icons/ReloadIcon.vue' import ReloadIcon from '@/components/Icons/ReloadIcon.vue'
import NestedPopover from '@/components/NestedPopover.vue'
import Autocomplete from '@/components/frappe-ui/Autocomplete.vue' import Autocomplete from '@/components/frappe-ui/Autocomplete.vue'
import { isTouchScreenDevice } from '@/utils' import { isTouchScreenDevice } from '@/utils'
import { Popover } from 'frappe-ui'
import Draggable from 'vuedraggable' import Draggable from 'vuedraggable'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { watchOnce } from '@vueuse/core' import { watchOnce } from '@vueuse/core'
@ -210,7 +219,6 @@ const fields = computed(() => {
}) })
function addColumn(c) { function addColumn(c) {
if (!c) return
let align = ['Float', 'Int', 'Percent', 'Currency'].includes(c.type) let align = ['Float', 'Int', 'Percent', 'Currency'].includes(c.type)
? 'right' ? 'right'
: 'left' : 'left'

View File

@ -45,12 +45,11 @@
v-slot="{ togglePopover }" v-slot="{ togglePopover }"
@update:modelValue="() => appendEmoji()" @update:modelValue="() => appendEmoji()"
> >
<Button <Button variant="ghost" @click="togglePopover()">
:tooltip="__('Insert Emoji')" <template #icon>
:icon="SmileIcon" <SmileIcon class="h-4" />
variant="ghost" </template>
@click="togglePopover()" </Button>
/>
</IconPicker> </IconPicker>
<FileUploader <FileUploader
:upload-args="{ :upload-args="{
@ -62,11 +61,14 @@
> >
<template #default="{ openFileSelector }"> <template #default="{ openFileSelector }">
<Button <Button
:tooltip="__('Attach a file')" theme="gray"
variant="ghost" variant="ghost"
:icon="AttachmentIcon"
@click="openFileSelector()" @click="openFileSelector()"
/> >
<template #icon>
<AttachmentIcon class="h-4" />
</template>
</Button>
</template> </template>
</FileUploader> </FileUploader>
</div> </div>

View File

@ -8,18 +8,24 @@
showEmailBox ? '!bg-surface-gray-4 hover:!bg-surface-gray-3' : '', showEmailBox ? '!bg-surface-gray-4 hover:!bg-surface-gray-3' : '',
]" ]"
:label="__('Reply')" :label="__('Reply')"
:iconLeft="Email2Icon"
@click="toggleEmailBox()" @click="toggleEmailBox()"
/> >
<template #prefix>
<Email2Icon class="h-4" />
</template>
</Button>
<Button <Button
variant="ghost" variant="ghost"
:label="__('Comment')" :label="__('Comment')"
:class="[ :class="[
showCommentBox ? '!bg-surface-gray-4 hover:!bg-surface-gray-3' : '', showCommentBox ? '!bg-surface-gray-4 hover:!bg-surface-gray-3' : '',
]" ]"
:iconLeft="CommentIcon"
@click="toggleCommentBox()" @click="toggleCommentBox()"
/> >
<template #prefix>
<CommentIcon class="h-4" />
</template>
</Button>
</div> </div>
</div> </div>
<div <div

View File

@ -1,454 +0,0 @@
<template>
<div
class="flex gap-2"
:class="[
{
'items-center': !props.isGroup,
},
]"
>
<div
class="flex gap-2 w-full"
:class="[
{
'items-center justify-between': !props.isGroup,
},
]"
>
<div :class="'text-end text-base text-gray-600'">
<div v-if="props.itemIndex == 0" class="min-w-[66px] text-start">
{{ __('Where') }}
</div>
<div v-else class="min-w-[66px] flex items-start">
<Button
variant="subtle"
class="w-max"
@click="toggleConjunction"
icon-right="refresh-cw"
:disabled="props.itemIndex > 2"
:label="conjunction"
/>
</div>
</div>
<div v-if="!props.isGroup" class="flex items-center gap-2 w-full">
<div id="fieldname" class="w-full">
<Autocomplete
:options="filterableFields.data"
v-model="props.condition[0]"
:placeholder="__('Field')"
@update:modelValue="updateField"
/>
</div>
<div id="operator">
<FormControl
v-if="!props.condition[0]"
disabled
type="text"
:placeholder="__('operator')"
class="w-[100px]"
/>
<FormControl
v-else
:disabled="!props.condition[0]"
type="select"
v-model="props.condition[1]"
@change="updateOperator"
:options="getOperators()"
class="w-max min-w-[100px]"
/>
</div>
<div id="value" class="w-full">
<FormControl
v-if="!props.condition[0]"
disabled
type="text"
:placeholder="__('condition')"
class="w-full"
/>
<component
v-else
:is="getValueControl()"
v-model="props.condition[2]"
@change="updateValue"
:placeholder="__('condition')"
/>
</div>
</div>
<CFConditions
v-if="props.isGroup && !(props.level == 2 || props.level == 4)"
:conditions="props.condition"
:isChild="true"
:level="props.level"
:disableAddCondition="props.disableAddCondition"
/>
<Button
variant="outline"
v-if="props.isGroup && (props.level == 2 || props.level == 4)"
@click="show = true"
:label="__('Open nested conditions')"
/>
</div>
<div :class="'w-max'">
<Dropdown placement="right" :options="dropdownOptions">
<Button variant="ghost" icon="more-horizontal" />
</Dropdown>
</div>
</div>
<Dialog
v-model="show"
:options="{ size: '3xl', title: __('Nested conditions') }"
>
<template #body-content>
<CFConditions
:conditions="props.condition"
:isChild="true"
:level="props.level"
:disableAddCondition="props.disableAddCondition"
/>
</template>
</Dialog>
</template>
<script setup>
import {
Autocomplete,
Button,
DatePicker,
DateRangePicker,
DateTimePicker,
Dialog,
Dropdown,
FormControl,
Rating,
} from 'frappe-ui'
import { computed, defineEmits, h, ref } from 'vue'
import GroupIcon from '~icons/lucide/group'
import UnGroupIcon from '~icons/lucide/ungroup'
import CFConditions from './CFConditions.vue'
import { filterableFields } from './filterableFields'
import Link from '@/components/Controls/Link.vue'
const show = ref(false)
const emit = defineEmits([
'remove',
'unGroupConditions',
'toggleConjunction',
'turnIntoGroup',
])
const props = defineProps({
condition: {
type: Array,
required: true,
},
isChild: {
type: Boolean,
default: false,
},
itemIndex: {
type: Number,
},
level: {
type: Number,
default: 0,
},
isGroup: {
type: Boolean,
default: false,
},
conjunction: {
type: String,
},
disableAddCondition: {
type: Boolean,
default: false,
},
})
const dropdownOptions = computed(() => {
const options = []
if (!props.isGroup && props.level < 4) {
options.push({
label: __('Turn into a group'),
icon: () => h(GroupIcon),
onClick: () => {
emit('turnIntoGroup')
},
})
}
if (props.isGroup) {
options.push({
label: __('Ungroup conditions'),
icon: () => h(UnGroupIcon),
onClick: () => {
emit('unGroupConditions')
},
})
}
options.push({
label: __('Remove'),
icon: 'trash-2',
variant: 'red',
onClick: () => emit('remove'),
condition: () => !props.isGroup,
})
options.push({
label: __('Remove group'),
icon: 'trash-2',
variant: 'red',
onClick: () => emit('remove'),
condition: () => props.isGroup,
})
return options
})
const typeCheck = ['Check']
const typeLink = ['Link', 'Dynamic Link']
const typeNumber = ['Float', 'Int', 'Currency', 'Percent']
const typeSelect = ['Select']
const typeString = ['Data', 'Long Text', 'Small Text', 'Text Editor', 'Text']
const typeDate = ['Date', 'Datetime']
const typeRating = ['Rating']
function toggleConjunction() {
emit('toggleConjunction', props.conjunction)
}
const updateField = (field) => {
props.condition[0] = field?.fieldname
resetConditionValue()
}
const resetConditionValue = () => {
props.condition[2] = ''
}
function getValueControl() {
const [field, operator] = props.condition
if (!field) return null
const fieldData = filterableFields.data?.find((f) => f.fieldname == field)
if (!fieldData) return null
const { fieldtype, options } = fieldData
if (operator == 'is') {
return h(FormControl, {
type: 'select',
options: [
{
label: 'Set',
value: 'set',
},
{
label: 'Not Set',
value: 'not set',
},
],
})
} else if (['like', 'not like', 'in', 'not in'].includes(operator)) {
return h(FormControl, { type: 'text' })
} else if (typeSelect.includes(fieldtype) || typeCheck.includes(fieldtype)) {
const _options =
fieldtype == 'Check' ? ['Yes', 'No'] : getSelectOptions(options)
return h(FormControl, {
type: 'select',
options: _options.map((o) => ({
label: o,
value: o,
})),
})
} else if (typeLink.includes(fieldtype)) {
if (fieldtype == 'Dynamic Link') {
return h(FormControl, { type: 'text' })
}
return h(Link, {
class: 'form-control',
doctype: options,
value: props.condition[2],
})
} else if (typeNumber.includes(fieldtype)) {
return h(FormControl, { type: 'number' })
} else if (typeDate.includes(fieldtype) && operator == 'between') {
return h(DateRangePicker, { value: props.condition[2], iconLeft: '' })
} else if (typeDate.includes(fieldtype)) {
return h(fieldtype == 'Date' ? DatePicker : DateTimePicker, {
value: props.condition[2],
iconLeft: '',
})
} else if (typeRating.includes(fieldtype)) {
return h(Rating, {
modelValue: props.condition[2] || 0,
class: 'truncate',
'update:modelValue': (v) => updateValue(v),
})
} else {
return h(FormControl, { type: 'text' })
}
}
function updateValue(value) {
value = value.target ? value.target.value : value
if (props.condition[1] === 'between') {
props.condition[2] = [value.split(',')[0], value.split(',')[1]]
} else {
props.condition[2] = value + ''
}
}
function getSelectOptions(options) {
return options.split('\n')
}
function updateOperator(event) {
let oldOperatorValue = event.target._value
let newOperatorValue = event.target.value
props.condition[1] = event.target.value
if (!isSameTypeOperator(oldOperatorValue, newOperatorValue)) {
props.condition[2] = getDefaultValue(props.condition[0])
}
resetConditionValue()
}
function getOperators() {
let options = []
const field = props.condition[0]
if (!field) return options
const fieldData = filterableFields.data?.find((f) => f.fieldname == field)
if (!fieldData) return options
const { fieldtype, fieldname } = fieldData
if (typeString.includes(fieldtype)) {
options.push(
...[
{ label: 'Equals', value: '==' },
{ label: 'Not Equals', value: '!=' },
{ label: 'Like', value: 'like' },
{ label: 'Not Like', value: 'not like' },
{ label: 'In', value: 'in' },
{ label: 'Not In', value: 'not in' },
{ label: 'Is', value: 'is' },
],
)
}
if (fieldname === '_assign') {
options = [
{ label: 'Like', value: 'like' },
{ label: 'Not Like', value: 'not like' },
{ label: 'Is', value: 'is' },
]
}
if (typeNumber.includes(fieldtype)) {
options.push(
...[
{ label: 'Equals', value: '==' },
{ label: 'Not Equals', value: '!=' },
{ label: 'Like', value: 'like' },
{ label: 'Not Like', value: 'not like' },
{ label: 'In', value: 'in' },
{ label: 'Not In', value: 'not in' },
{ label: 'Is', value: 'is' },
{ label: '<', value: '<' },
{ label: '>', value: '>' },
{ label: '<=', value: '<=' },
{ label: '>=', value: '>=' },
],
)
}
if (typeSelect.includes(fieldtype)) {
options.push(
...[
{ label: 'Equals', value: '==' },
{ label: 'Not Equals', value: '!=' },
{ label: 'In', value: 'in' },
{ label: 'Not In', value: 'not in' },
{ label: 'Is', value: 'is' },
],
)
}
if (typeLink.includes(fieldtype)) {
options.push(
...[
{ label: 'Equals', value: '==' },
{ label: 'Not Equals', value: '!=' },
{ label: 'Like', value: 'like' },
{ label: 'Not Like', value: 'not like' },
{ label: 'In', value: 'in' },
{ label: 'Not In', value: 'not in' },
{ label: 'Is', value: 'is' },
],
)
}
if (typeCheck.includes(fieldtype)) {
options.push(...[{ label: 'Equals', value: '==' }])
}
if (['Duration'].includes(fieldtype)) {
options.push(
...[
{ label: 'Like', value: 'like' },
{ label: 'Not Like', value: 'not like' },
{ label: 'In', value: 'in' },
{ label: 'Not In', value: 'not in' },
{ label: 'Is', value: 'is' },
],
)
}
if (typeDate.includes(fieldtype)) {
options.push(
...[
{ label: 'Equals', value: '==' },
{ label: 'Not Equals', value: '!=' },
{ label: 'Is', value: 'is' },
{ label: '>', value: '>' },
{ label: '<', value: '<' },
{ label: '>=', value: '>=' },
{ label: '<=', value: '<=' },
{ label: 'Between', value: 'between' },
],
)
}
if (typeRating.includes(fieldtype)) {
options.push(
...[
{ label: 'Equals', value: '==' },
{ label: 'Not Equals', value: '!=' },
{ label: 'Is', value: 'is' },
{ label: '>', value: '>' },
{ label: '<', value: '<' },
{ label: '>=', value: '>=' },
{ label: '<=', value: '<=' },
],
)
}
const op = options.find((o) => o.value == props.condition[1])
props.condition[1] = op?.value || options[0].value
return options
}
function getDefaultValue(field) {
if (typeSelect.includes(field.fieldtype)) {
return getSelectOptions(field.options)[0]
}
if (typeCheck.includes(field.fieldtype)) {
return 'Yes'
}
if (typeDate.includes(field.fieldtype)) {
return null
}
if (typeRating.includes(field.fieldtype)) {
return 0
}
return ''
}
function isSameTypeOperator(oldOperator, newOperator) {
let textOperators = ['==', '!=', 'in', 'not in', '>', '<', '>=', '<=']
if (
textOperators.includes(oldOperator) &&
textOperators.includes(newOperator)
)
return true
return false
}
</script>

View File

@ -1,142 +0,0 @@
<template>
<div class="rounded-lg border border-outline-gray-2 p-3 flex flex-col gap-4 w-full">
<template v-for="(condition, i) in props.conditions" :key="condition.field">
<CFCondition
v-if="Array.isArray(condition)"
:condition="condition"
:isChild="props.isChild"
:itemIndex="i"
@remove="removeCondition(condition)"
@unGroupConditions="unGroupConditions(condition)"
:level="props.level + 1"
@toggleConjunction="toggleConjunction"
:isGroup="isGroupCondition(condition[0])"
:conjunction="getConjunction()"
@turnIntoGroup="turnIntoGroup(condition)"
:disableAddCondition="props.disableAddCondition"
/>
</template>
<div v-if="props.isChild" class="flex">
<Dropdown v-slot="{ open }" :options="dropdownOptions">
<Button
:disabled="props.disableAddCondition"
:label="__('Add condition')"
icon-left="plus"
:icon-right="open ? 'chevron-up' : 'chevron-down'"
/>
</Dropdown>
</div>
</div>
</template>
<script setup>
import { Button, Dropdown } from 'frappe-ui'
import { computed, watch } from 'vue'
import CFCondition from './CFCondition.vue'
import { filterableFields } from './filterableFields'
const props = defineProps({
conditions: {
type: Array,
required: true,
},
isChild: {
type: Boolean,
default: false,
},
level: {
type: Number,
default: 0,
},
disableAddCondition: {
type: Boolean,
default: false,
},
doctype: {
type: String,
required: true,
},
})
const getConjunction = () => {
let conjunction = 'and'
props.conditions.forEach((condition) => {
if (typeof condition == 'string') {
conjunction = condition
}
})
return conjunction
}
const turnIntoGroup = (condition) => {
props.conditions.splice(props.conditions.indexOf(condition), 1, [condition])
}
const isGroupCondition = (condition) => {
return Array.isArray(condition)
}
const dropdownOptions = computed(() => {
const options = [
{
label: __('Add condition'),
onClick: () => {
const conjunction = getConjunction()
props.conditions.push(conjunction, ['', '', ''])
},
},
]
if (props.level < 3) {
options.push({
label: __('Add condition group'),
onClick: () => {
const conjunction = getConjunction()
props.conditions.push(conjunction, [[]])
},
})
}
return options
})
function removeCondition(condition) {
const conditionIndex = props.conditions.indexOf(condition)
if (conditionIndex == 0) {
props.conditions.splice(conditionIndex, 2)
} else {
props.conditions.splice(conditionIndex - 1, 2)
}
}
function unGroupConditions(condition) {
const conjunction = getConjunction()
const newConditions = condition.map((c) => {
if (typeof c == 'string') {
return conjunction
}
return c
})
const index = props.conditions.indexOf(condition)
if (index !== -1) {
props.conditions.splice(index, 1, ...newConditions)
}
}
function toggleConjunction(conjunction) {
for (let i = 0; i < props.conditions.length; i++) {
if (typeof props.conditions[i] == 'string') {
props.conditions[i] = conjunction == 'and' ? 'or' : 'and'
}
}
}
watch(
() => props.doctype,
(doctype) => {
filterableFields.submit({
doctype,
})
},
{ immediate: true },
)
</script>

View File

@ -1,17 +0,0 @@
import { createResource } from 'frappe-ui'
export const filterableFields = createResource({
url: 'crm.api.doc.get_filterable_fields',
transform: (data) => {
data = data
.filter((field) => !field.fieldname.startsWith('_'))
.map((field) => {
return {
label: field.label,
value: field.fieldname,
...field,
}
})
return data
},
})

View File

@ -52,14 +52,16 @@
> >
</div> </div>
</div> </div>
<div class="flex items-center justify-center w-12"> <div class="w-12">
<Button <Button
:tooltip="__('Edit grid fields')" class="flex w-full items-center justify-center rounded !bg-surface-gray-2 border-0"
class="rounded !bg-surface-gray-2 border-0 !text-ink-gray-5"
variant="outline" variant="outline"
icon="settings"
@click="showGridFieldsEditorModal = true" @click="showGridFieldsEditorModal = true"
/> >
<template #icon>
<FeatherIcon name="settings" class="size-4 text-ink-gray-7" />
</template>
</Button>
</div> </div>
</div> </div>
<!-- Rows --> <!-- Rows -->
@ -70,7 +72,6 @@
:delay="isTouchScreenDevice() ? 200 : 0" :delay="isTouchScreenDevice() ? 200 : 0"
group="rows" group="rows"
item-key="name" item-key="name"
@end="reorder"
> >
<template #item="{ element: row, index }"> <template #item="{ element: row, index }">
<div <div
@ -276,14 +277,16 @@
/> />
</div> </div>
</div> </div>
<div class="edit-row flex items-center justify-center w-12"> <div class="edit-row w-12">
<Button <Button
:tooltip="__('Edit row')" class="flex w-full items-center justify-center rounded border-0"
class="rounded border-0 !text-ink-gray-7"
variant="outline" variant="outline"
:icon="EditIcon"
@click="showRowList[index] = true" @click="showRowList[index] = true"
/> >
<template #icon>
<EditIcon class="text-ink-gray-7" />
</template>
</Button>
</div> </div>
<GridRowModal <GridRowModal
v-if="showRowList[index]" v-if="showRowList[index]"
@ -347,6 +350,7 @@ import { usersStore } from '@/stores/users'
import { getMeta } from '@/stores/meta' import { getMeta } from '@/stores/meta'
import { createDocument } from '@/composables/document' import { createDocument } from '@/composables/document'
import { import {
FeatherIcon,
FormControl, FormControl,
Checkbox, Checkbox,
DateTimePicker, DateTimePicker,
@ -516,13 +520,6 @@ const deleteRows = () => {
selectedRows.clear() selectedRows.clear()
} }
const reorder = () => {
rows.value.forEach((row, index) => {
row.idx = index + 1
})
}
function fieldChange(value, field, row) { function fieldChange(value, field, row) {
triggerOnChange(field.fieldname, value, row) triggerOnChange(field.fieldname, value, row)
} }

View File

@ -54,10 +54,13 @@
<template #target="{ togglePopover }"> <template #target="{ togglePopover }">
<Button <Button
class="w-full mt-2" class="w-full mt-2"
:label="__('Add Field')"
iconLeft="plus"
@click="togglePopover()" @click="togglePopover()"
/> :label="__('Add Field')"
>
<template #prefix>
<FeatherIcon name="plus" class="h-4" />
</template>
</Button>
</template> </template>
<template #item-label="{ option }"> <template #item-label="{ option }">
<div class="flex flex-col gap-1 text-ink-gray-9"> <div class="flex flex-col gap-1 text-ink-gray-9">
@ -72,7 +75,7 @@
</div> </div>
</template> </template>
<template #actions> <template #actions>
<div class="flex items-center gap-2 justify-end"> <div class="flex flex-col gap-2">
<Button <Button
v-if="dirty" v-if="dirty"
class="w-full" class="w-full"

View File

@ -11,18 +11,19 @@
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<Button <Button
v-if="isManager()" v-if="isManager()"
:tooltip="__('Edit fields layout')"
variant="ghost" variant="ghost"
class="w-7" class="w-7"
:icon="EditIcon"
@click="openGridRowFieldsModal" @click="openGridRowFieldsModal"
/> >
<Button <template #icon>
icon="x" <EditIcon />
variant="ghost" </template>
class="w-7" </Button>
@click="show = false" <Button variant="ghost" class="w-7" @click="show = false">
/> <template #icon>
<FeatherIcon name="x" class="size-4" />
</template>
</Button>
</div> </div>
</div> </div>
<div> <div>

View File

@ -1,6 +1,7 @@
<template> <template>
<FileUploader <FileUploader
:file-types="image_type" :file-types="image_type"
class="text-base"
@success=" @success="
(file) => { (file) => {
$emit('upload', file.file_url) $emit('upload', file.file_url)
@ -9,28 +10,21 @@
> >
<template v-slot="{ progress, uploading, openFileSelector }"> <template v-slot="{ progress, uploading, openFileSelector }">
<div class="flex items-end space-x-1"> <div class="flex items-end space-x-1">
<Button <Button @click="openFileSelector">
@click="openFileSelector" {{
:iconLeft="uploading ? 'cloud-upload' : ImageUpIcon"
:label="
uploading uploading
? __('Uploading {0}%', [progress]) ? `Uploading ${progress}%`
: image_url : image_url
? __('Change') ? 'Change'
: __('Upload') : 'Upload'
" }}
/> </Button>
<Button <Button v-if="image_url" @click="$emit('remove')">Remove</Button>
v-if="image_url"
:label="__('Remove')"
@click="$emit('remove')"
/>
</div> </div>
</template> </template>
</FileUploader> </FileUploader>
</template> </template>
<script setup> <script setup>
import ImageUpIcon from '~icons/lucide/image-up'
import { FileUploader, Button } from 'frappe-ui' import { FileUploader, Button } from 'frappe-ui'
const prop = defineProps({ const prop = defineProps({
@ -39,6 +33,10 @@ const prop = defineProps({
type: String, type: String,
default: 'image/*', default: 'image/*',
}, },
label: {
type: String,
default: '',
},
}) })
const emit = defineEmits(['upload', 'remove']) const emit = defineEmits(['upload', 'remove'])
</script> </script>

View File

@ -48,18 +48,24 @@
variant="ghost" variant="ghost"
class="w-full !justify-start" class="w-full !justify-start"
:label="__('Create New')" :label="__('Create New')"
iconLeft="plus"
@click="() => attrs.onCreate(value, close)" @click="() => attrs.onCreate(value, close)"
/> >
<template #prefix>
<FeatherIcon name="plus" class="h-4" />
</template>
</Button>
</div> </div>
<div> <div>
<Button <Button
variant="ghost" variant="ghost"
class="w-full !justify-start" class="w-full !justify-start"
:label="__('Clear')" :label="__('Clear')"
iconLeft="x"
@click="() => clearValue(close)" @click="() => clearValue(close)"
/> >
<template #prefix>
<FeatherIcon name="x" class="h-4" />
</template>
</Button>
</div> </div>
</template> </template>
</Autocomplete> </Autocomplete>

View File

@ -18,10 +18,14 @@
:key="g.label" :key="g.label"
> >
<Dropdown :options="g.action" v-slot="{ open }"> <Dropdown :options="g.action" v-slot="{ open }">
<Button <Button :label="g.label">
:label="g.label" <template #suffix>
:iconRight="open ? 'chevron-up' : 'chevron-down'" <FeatherIcon
:name="open ? 'chevron-up' : 'chevron-down'"
class="h-4"
/> />
</template>
</Button>
</Dropdown> </Dropdown>
</div> </div>
</template> </template>

View File

@ -2,7 +2,7 @@
<Dialog v-model="show" :options="{ size: 'xl' }"> <Dialog v-model="show" :options="{ size: 'xl' }">
<template #body v-if="!confirmDeleteInfo.show"> <template #body v-if="!confirmDeleteInfo.show">
<div class="bg-surface-modal px-4 pb-6 pt-5 sm:px-6"> <div class="bg-surface-modal px-4 pb-6 pt-5 sm:px-6">
<div class="mb-6 flex items-center justify-between"> <div class="mb-4 flex items-center justify-between">
<div> <div>
<h3 class="text-2xl leading-6 text-ink-gray-9 font-semibold"> <h3 class="text-2xl leading-6 text-ink-gray-9 font-semibold">
{{ {{
@ -32,12 +32,11 @@
{ {
label: 'Document', label: 'Document',
key: 'title', key: 'title',
width: '19rem',
}, },
{ {
label: 'Master', label: 'Master',
key: 'reference_doctype', key: 'reference_doctype',
width: '12rem', width: '30%',
}, },
]" ]"
@selectionsChanged=" @selectionsChanged="

View File

@ -19,36 +19,53 @@
v-if="editMode" v-if="editMode"
variant="ghost" variant="ghost"
:label="__('Save')" :label="__('Save')"
size="sm"
class="opacity-0 hover:bg-surface-gray-4 group-hover:opacity-100" class="opacity-0 hover:bg-surface-gray-4 group-hover:opacity-100"
@click="saveOption" @click="saveOption"
/> />
<Tooltip text="Set As Primary" v-if="!isNew && !option.selected">
<div>
<Button <Button
v-if="!isNew && !option.selected"
:tooltip="__('Set As Primary')"
variant="ghost" variant="ghost"
:icon="SuccessIcon" size="sm"
class="opacity-0 hover:bg-surface-gray-4 group-hover:opacity-100" class="opacity-0 hover:bg-surface-gray-4 group-hover:opacity-100"
@click="option.onClick" @click="option.onClick"
/> >
<template #icon>
<SuccessIcon />
</template>
</Button>
</div>
</Tooltip>
<Tooltip v-if="!editMode" text="Edit">
<div>
<Button <Button
v-if="!editMode"
:tooltip="__('Edit')"
variant="ghost" variant="ghost"
:icon="EditIcon" size="sm"
class="opacity-0 hover:bg-surface-gray-4 group-hover:opacity-100" class="opacity-0 hover:bg-surface-gray-4 group-hover:opacity-100"
@click="toggleEditMode" @click="toggleEditMode"
/> >
<template #icon>
<EditIcon />
</template>
</Button>
</div>
</Tooltip>
<Tooltip text="Delete">
<div>
<Button <Button
:tooltip="__('Delete')"
variant="ghost" variant="ghost"
icon="x" icon="x"
size="sm"
class="opacity-0 hover:bg-surface-gray-4 group-hover:opacity-100" class="opacity-0 hover:bg-surface-gray-4 group-hover:opacity-100"
@click="() => option.onDelete(option, isNew)" @click="() => option.onDelete(option, isNew)"
/> />
</div> </div>
</Tooltip>
</div>
</div> </div>
<div v-if="option.selected"> <div v-if="option.selected">
<FeatherIcon name="check" class="text-ink-gray-5 h-4 w-6" /> <FeatherIcon name="check" class="text-ink-gray-5 h-4 w-6" size="sm" />
</div> </div>
</div> </div>
</template> </template>
@ -56,7 +73,7 @@
<script setup> <script setup>
import SuccessIcon from '@/components/Icons/SuccessIcon.vue' import SuccessIcon from '@/components/Icons/SuccessIcon.vue'
import EditIcon from '@/components/Icons/EditIcon.vue' import EditIcon from '@/components/Icons/EditIcon.vue'
import { TextInput } from 'frappe-ui' import { TextInput, Tooltip } from 'frappe-ui'
import { nextTick, ref, onMounted } from 'vue' import { nextTick, ref, onMounted } from 'vue'
const props = defineProps({ const props = defineProps({

View File

@ -123,12 +123,11 @@
v-slot="{ togglePopover }" v-slot="{ togglePopover }"
@update:modelValue="() => appendEmoji()" @update:modelValue="() => appendEmoji()"
> >
<Button <Button variant="ghost" @click="togglePopover()">
:tooltip="__('Insert Emoji')" <template #icon>
:icon="SmileIcon" <SmileIcon class="h-4" />
variant="ghost" </template>
@click="togglePopover()" </Button>
/>
</IconPicker> </IconPicker>
<FileUploader <FileUploader
:upload-args="{ :upload-args="{
@ -139,20 +138,21 @@
@success="(f) => attachments.push(f)" @success="(f) => attachments.push(f)"
> >
<template #default="{ openFileSelector }"> <template #default="{ openFileSelector }">
<Button <Button variant="ghost" @click="openFileSelector()">
:tooltip="__('Attach a file')" <template #icon>
:icon="AttachmentIcon" <AttachmentIcon class="h-4" />
variant="ghost" </template>
@click="openFileSelector()" </Button>
/>
</template> </template>
</FileUploader> </FileUploader>
<Button <Button
:tooltip="__('Insert Email Template')"
variant="ghost" variant="ghost"
:icon="EmailTemplateIcon"
@click="showEmailTemplateSelectorModal = true" @click="showEmailTemplateSelectorModal = true"
/> >
<template #icon>
<EmailTemplateIcon class="h-4" />
</template>
</Button>
</div> </div>
<div class="mt-2 flex items-center justify-end space-x-2 sm:mt-0"> <div class="mt-2 flex items-center justify-end space-x-2 sm:mt-0">
<Button v-bind="discardButtonProps || {}" :label="__('Discard')" /> <Button v-bind="discardButtonProps || {}" :label="__('Discard')" />

View File

@ -89,9 +89,12 @@
v-if="data[field.fieldname] && field.edit" v-if="data[field.fieldname] && field.edit"
class="shrink-0" class="shrink-0"
:label="__('Edit')" :label="__('Edit')"
:iconLeft="EditIcon"
@click="field.edit(data[field.fieldname])" @click="field.edit(data[field.fieldname])"
/> >
<template #prefix>
<EditIcon class="h-4 w-4" />
</template>
</Button>
</div> </div>
<TableMultiselectInput <TableMultiselectInput

View File

@ -169,10 +169,13 @@
<Button <Button
class="w-full !h-8 !bg-surface-modal" class="w-full !h-8 !bg-surface-modal"
variant="outline" variant="outline"
:label="__('Add Field')"
iconLeft="plus"
@click="togglePopover()" @click="togglePopover()"
/> :label="__('Add Field')"
>
<template #prefix>
<FeatherIcon name="plus" class="h-4" />
</template>
</Button>
</div> </div>
</template> </template>
<template #item-label="{ option }"> <template #item-label="{ option }">
@ -195,7 +198,6 @@
class="w-full h-8" class="w-full h-8"
variant="subtle" variant="subtle"
:label="__('Add Section')" :label="__('Add Section')"
iconLeft="plus"
@click=" @click="
tabs[tabIndex].sections.push({ tabs[tabIndex].sections.push({
label: __('New Section'), label: __('New Section'),
@ -204,7 +206,11 @@
columns: [{ name: 'column_' + getRandom(), fields: [] }], columns: [{ name: 'column_' + getRandom(), fields: [] }],
}) })
" "
/> >
<template #prefix>
<FeatherIcon name="plus" class="h-4" />
</template>
</Button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -29,7 +29,6 @@
filesUploaderArea?.showWebLink || filesUploaderArea?.showCamera filesUploaderArea?.showWebLink || filesUploaderArea?.showCamera
" "
:label="isMobileView ? __('Back') : __('Back to file upload')" :label="isMobileView ? __('Back') : __('Back to file upload')"
iconLeft="arrow-left"
@click=" @click="
() => { () => {
filesUploaderArea.showWebLink = false filesUploaderArea.showWebLink = false
@ -38,7 +37,11 @@
filesUploaderArea.cameraImage = null filesUploaderArea.cameraImage = null
} }
" "
/> >
<template #prefix>
<FeatherIcon name="arrow-left" class="size-4" />
</template>
</Button>
<Button <Button
v-if=" v-if="
filesUploaderArea?.showCamera && !filesUploaderArea?.cameraImage filesUploaderArea?.showCamera && !filesUploaderArea?.cameraImage

View File

@ -1,13 +1,12 @@
<template> <template>
<Popover placement="bottom-end"> <NestedPopover>
<template #target="{ togglePopover, close }"> <template #target>
<div class="flex items-center"> <div class="flex items-center">
<Button <Button
:label="__('Filter')" :label="__('Filter')"
:class="filters?.size ? 'rounded-r-none' : ''" :class="filters?.size ? 'rounded-r-none' : ''"
:iconLeft="FilterIcon"
@click="togglePopover"
> >
<template #prefix><FilterIcon class="h-4" /></template>
<template v-if="filters?.size" #suffix> <template v-if="filters?.size" #suffix>
<div <div
class="flex h-5 w-5 items-center justify-center rounded-[5px] bg-surface-white pt-px text-xs font-medium text-ink-gray-8 shadow-sm" class="flex h-5 w-5 items-center justify-center rounded-[5px] bg-surface-white pt-px text-xs font-medium text-ink-gray-8 shadow-sm"
@ -16,14 +15,16 @@
</div> </div>
</template> </template>
</Button> </Button>
<Tooltip v-if="filters?.size" :text="__('Clear all Filter')">
<div>
<Button <Button
v-if="filters?.size"
:tooltip="__('Clear all Filter')"
class="rounded-l-none border-l" class="rounded-l-none border-l"
icon="x" icon="x"
@click.stop="clearfilter(close)" @click.stop="clearfilter(false)"
/> />
</div> </div>
</Tooltip>
</div>
</template> </template>
<template #body="{ close }"> <template #body="{ close }">
<div <div
@ -133,10 +134,13 @@
<Button <Button
class="!text-ink-gray-5" class="!text-ink-gray-5"
variant="ghost" variant="ghost"
:label="__('Add Filter')"
iconLeft="plus"
@click="togglePopover()" @click="togglePopover()"
/> :label="__('Add Filter')"
>
<template #prefix>
<FeatherIcon name="plus" class="h-4" />
</template>
</Button>
</template> </template>
</Autocomplete> </Autocomplete>
<Button <Button
@ -150,16 +154,17 @@
</div> </div>
</div> </div>
</template> </template>
</Popover> </NestedPopover>
</template> </template>
<script setup> <script setup>
import NestedPopover from '@/components/NestedPopover.vue'
import FilterIcon from '@/components/Icons/FilterIcon.vue' import FilterIcon from '@/components/Icons/FilterIcon.vue'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
import Autocomplete from '@/components/frappe-ui/Autocomplete.vue' import Autocomplete from '@/components/frappe-ui/Autocomplete.vue'
import { import {
FormControl, FormControl,
createResource, createResource,
Popover, Tooltip,
DatePicker, DatePicker,
DateTimePicker, DateTimePicker,
DateRangePicker, DateRangePicker,
@ -480,7 +485,7 @@ function removeFilter(index) {
function clearfilter(close) { function clearfilter(close) {
filters.value.clear() filters.value.clear()
apply() apply()
close() close && close()
} }
function updateValue(value, filter) { function updateValue(value, filter) {

View File

@ -7,11 +7,19 @@
? groupByValue?.label ? groupByValue?.label
: __('Group By: ') + groupByValue?.label : __('Group By: ') + groupByValue?.label
" "
:iconLeft="DetailsIcon"
:iconRight="isOpen ? 'chevron-up' : 'chevron-down'"
@click="togglePopover()" @click="togglePopover()"
>
<template #prefix>
<DetailsIcon />
</template>
<template #suffix>
<FeatherIcon
:name="isOpen ? 'chevron-up' : 'chevron-down'"
class="h-4"
/> />
</template> </template>
</Button>
</template>
</Autocomplete> </Autocomplete>
</template> </template>
<script setup> <script setup>

View File

@ -69,7 +69,7 @@
</Popover> </Popover>
</template> </template>
<script setup> <script setup>
import { Popover } from 'frappe-ui' import Popover from '@/components/frappe-ui/Popover.vue'
import { gemoji } from 'gemoji' import { gemoji } from 'gemoji'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'

View File

@ -1,19 +0,0 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-settings2-icon lucide-settings-2"
>
<path d="M14 17H5" />
<path d="M19 7h-9" />
<circle cx="17" cy="17" r="3" />
<circle cx="7" cy="7" r="3" />
</svg>
</template>

View File

@ -1,16 +0,0 @@
<template>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M3.37543 1.93494L4.21632 2.21494C4.35232 2.26027 4.44388 2.38738 4.44388 2.53138C4.44388 2.67538 4.35143 2.80249 4.21543 2.84783L3.37454 3.12783L3.09365 3.9696C3.04921 4.10472 2.92121 4.19716 2.7781 4.19716C2.63499 4.19716 2.50787 4.1056 2.46254 3.9696L2.18165 3.12783L1.34076 2.84783C1.20476 2.80249 1.11232 2.67538 1.11232 2.53138C1.11232 2.38738 1.20476 2.26027 1.34076 2.21494L2.18165 1.93494L2.46254 1.09316C2.55321 0.82116 3.00387 0.82116 3.09454 1.09316L3.37543 1.93494ZM8.44852 1.33394C8.3643 1.16325 8.19046 1.05518 8.00012 1.05518C7.80978 1.05518 7.63595 1.16325 7.55173 1.33394L5.67697 5.13368L1.48388 5.74214C1.29552 5.76947 1.13901 5.90137 1.08017 6.08238C1.02133 6.26339 1.07036 6.46211 1.20665 6.59497L4.24065 9.55281L3.52421 13.7284C3.49203 13.916 3.56913 14.1056 3.7231 14.2174C3.87706 14.3293 4.08119 14.3441 4.24966 14.2555L8.11188 12.2253C8.35631 12.0968 8.4503 11.7945 8.32181 11.5501C8.19333 11.3057 7.89102 11.2117 7.64659 11.3402L4.68114 12.899L5.2707 9.46284C5.29853 9.30065 5.24477 9.13514 5.12693 9.02027L2.63025 6.58626L6.08082 6.08555C6.24373 6.06191 6.38457 5.95959 6.45741 5.81196L8.00012 2.6852L9.54284 5.81196C9.61568 5.95959 9.75652 6.06191 9.91943 6.08555L13.37 6.58625L11.6235 8.2887C11.4258 8.48146 11.4218 8.79802 11.6145 8.99575C11.8073 9.19349 12.1239 9.19752 12.3216 9.00476L14.7936 6.59498C14.9299 6.46212 14.9789 6.2634 14.9201 6.08239C14.8612 5.90138 14.7047 5.76947 14.5164 5.74214L10.3233 5.13368L8.44852 1.33394ZM13.4744 11.9911L12.3517 11.6168L11.9775 10.4942C11.8557 10.1315 11.2557 10.1315 11.1339 10.4942L10.7597 11.6168L9.63702 11.9911C9.45569 12.0515 9.33302 12.2213 9.33302 12.4124C9.33302 12.6035 9.45569 12.7733 9.63702 12.8337L10.7597 13.2079L11.1339 14.3306C11.1944 14.5119 11.365 14.6346 11.5561 14.6346C11.7472 14.6346 11.917 14.5119 11.9784 14.3306L12.3526 13.2079L13.4752 12.8337C13.6566 12.7733 13.7792 12.6035 13.7792 12.4124C13.7792 12.2213 13.6566 12.0515 13.4752 11.9911H13.4744ZM13.3333 2.88883C13.3333 3.25702 13.0349 3.5555 12.6667 3.5555C12.2985 3.5555 12 3.25702 12 2.88883C12 2.52064 12.2985 2.22217 12.6667 2.22217C13.0349 2.22217 13.3333 2.52064 13.3333 2.88883Z"
fill="currentColor"
/>
</svg>
</template>

View File

@ -3,8 +3,11 @@
:label="__('Kanban Settings')" :label="__('Kanban Settings')"
@click="showDialog = true" @click="showDialog = true"
v-bind="$attrs" v-bind="$attrs"
:iconLeft="KanbanIcon" >
/> <template #prefix>
<KanbanIcon class="h-4" />
</template>
</Button>
<Dialog v-model="showDialog" :options="{ title: __('Kanban Settings') }"> <Dialog v-model="showDialog" :options="{ title: __('Kanban Settings') }">
<template #body-content> <template #body-content>
<div> <div>
@ -20,8 +23,8 @@
<template #target="{ togglePopover }"> <template #target="{ togglePopover }">
<Button <Button
class="w-full !justify-start" class="w-full !justify-start"
:label="columnField.label"
@click="togglePopover()" @click="togglePopover()"
:label="columnField.label"
/> />
</template> </template>
</Autocomplete> </Autocomplete>
@ -77,10 +80,13 @@
<template #target="{ togglePopover }"> <template #target="{ togglePopover }">
<Button <Button
class="w-full mt-2" class="w-full mt-2"
:label="__('Add Field')"
iconLeft="plus"
@click="togglePopover()" @click="togglePopover()"
/> :label="__('Add Field')"
>
<template #prefix>
<FeatherIcon name="plus" class="h-4" />
</template>
</Button>
</template> </template>
<template #item-label="{ option }"> <template #item-label="{ option }">
<div class="flex flex-col gap-1 text-ink-gray-9"> <div class="flex flex-col gap-1 text-ink-gray-9">

View File

@ -15,18 +15,17 @@
> >
<div class="flex gap-2 items-center group justify-between"> <div class="flex gap-2 items-center group justify-between">
<div class="flex items-center text-base"> <div class="flex items-center text-base">
<Popover> <NestedPopover>
<template #target="{ togglePopover }"> <template #target>
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
class="hover:!bg-surface-gray-2" class="hover:!bg-surface-gray-2"
@click="togglePopover"
> >
<IndicatorIcon :class="parseColor(column.column.color)" /> <IndicatorIcon :class="parseColor(column.column.color)" />
</Button> </Button>
</template> </template>
<template #body> <template #body="{ close }">
<div <div
class="flex flex-col gap-3 px-3 py-2.5 min-w-40 rounded-lg bg-surface-modal shadow-2xl ring-1 ring-black ring-opacity-5 focus:outline-none" class="flex flex-col gap-3 px-3 py-2.5 min-w-40 rounded-lg bg-surface-modal shadow-2xl ring-1 ring-black ring-opacity-5 focus:outline-none"
> >
@ -49,7 +48,7 @@
</div> </div>
</div> </div>
</template> </template>
</Popover> </NestedPopover>
<div class="text-ink-gray-9">{{ column.column.name }}</div> <div class="text-ink-gray-9">{{ column.column.name }}</div>
</div> </div>
<div class="flex"> <div class="flex">
@ -154,10 +153,13 @@
<template #target="{ togglePopover }"> <template #target="{ togglePopover }">
<Button <Button
class="w-full mt-2.5 mb-1 mr-5" class="w-full mt-2.5 mb-1 mr-5"
:label="__('Add Column')"
iconLeft="plus"
@click="togglePopover()" @click="togglePopover()"
/> :label="__('Add Column')"
>
<template #prefix>
<FeatherIcon name="plus" class="h-4" />
</template>
</Button>
</template> </template>
</Autocomplete> </Autocomplete>
</div> </div>
@ -165,10 +167,11 @@
</template> </template>
<script setup> <script setup>
import Autocomplete from '@/components/frappe-ui/Autocomplete.vue' import Autocomplete from '@/components/frappe-ui/Autocomplete.vue'
import NestedPopover from '@/components/NestedPopover.vue'
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue' import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
import { isTouchScreenDevice, colors, parseColor } from '@/utils' import { isTouchScreenDevice, colors, parseColor } from '@/utils'
import Draggable from 'vuedraggable' import Draggable from 'vuedraggable'
import { Dropdown, Popover } from 'frappe-ui' import { Dropdown } from 'frappe-ui'
import { computed } from 'vue' import { computed } from 'vue'
const props = defineProps({ const props = defineProps({

View File

@ -3,8 +3,8 @@
class="relative flex h-full flex-col justify-between transition-all duration-300 ease-in-out" class="relative flex h-full flex-col justify-between transition-all duration-300 ease-in-out"
:class="isSidebarCollapsed ? 'w-12' : 'w-[220px]'" :class="isSidebarCollapsed ? 'w-12' : 'w-[220px]'"
> >
<div class="p-2"> <div>
<UserDropdown :isCollapsed="isSidebarCollapsed" /> <UserDropdown class="p-2" :isCollapsed="isSidebarCollapsed" />
</div> </div>
<div class="flex-1 overflow-y-auto"> <div class="flex-1 overflow-y-auto">
<div class="mb-3 flex flex-col"> <div class="mb-3 flex flex-col">
@ -197,11 +197,13 @@ const isSidebarCollapsed = useStorage('isSidebarCollapsed', false)
const isFCSite = ref(window.is_fc_site) const isFCSite = ref(window.is_fc_site)
const isDemoSite = ref(window.is_demo_site) const isDemoSite = ref(window.is_demo_site)
const allViews = computed(() => {
const links = [ const links = [
{ {
label: 'Dashboard', label: 'Dashboard',
icon: LucideLayoutDashboard, icon: LucideLayoutDashboard,
to: 'Dashboard', to: 'Dashboard',
condition: () => isManager(),
}, },
{ {
label: 'Leads', label: 'Leads',
@ -240,7 +242,6 @@ const links = [
}, },
] ]
const allViews = computed(() => {
let _views = [ let _views = [
{ {
name: 'All Views', name: 'All Views',

View File

@ -106,8 +106,6 @@ function convertToDeal(selections, unselectAll) {
} }
function deleteValues(selections, unselectAll) { function deleteValues(selections, unselectAll) {
unselectAllAction.value = unselectAll
const selectedDocs = Array.from(selections) const selectedDocs = Array.from(selections)
if (selectedDocs.length == 1) { if (selectedDocs.length == 1) {
showDeleteDocModal.value = { showDeleteDocModal.value = {
@ -219,12 +217,6 @@ function bulkActions(selections, unselectAll) {
} }
function reload(unselectAll) { function reload(unselectAll) {
showDeleteDocModal.value = {
showLinkedDocsModal: false,
showDeleteModal: false,
docname: null,
}
unselectAllAction.value?.() unselectAllAction.value?.()
unselectAll?.() unselectAll?.()
list.value?.reload() list.value?.reload()

View File

@ -26,14 +26,13 @@
<ListRowItem <ListRowItem
:item="item" :item="item"
@click="listViewRef.toggleRow(row['reference_docname'])" @click="listViewRef.toggleRow(row['reference_docname'])"
class="!w-full"
> >
<template #default="{ label }"> <template #default="{ label }">
<div <div
v-if="column.key === 'title'" v-if="column.key === 'title'"
class="truncate text-base flex gap-2 w-full" class="truncate text-base flex gap-2"
> >
<span class="max-w-[90%] truncate"> <span>
{{ label }} {{ label }}
</span> </span>
<FeatherIcon <FeatherIcon
@ -103,7 +102,6 @@ const listViewRef = ref(null)
const viewLinkedDoc = (doc) => { const viewLinkedDoc = (doc) => {
let page = '' let page = ''
let id = '' let id = ''
let openDesk = false
switch (doc.reference_doctype) { switch (doc.reference_doctype) {
case 'CRM Lead': case 'CRM Lead':
page = 'leads' page = 'leads'
@ -125,11 +123,6 @@ const viewLinkedDoc = (doc) => {
page = 'organizations' page = 'organizations'
id = doc.reference_docname id = doc.reference_docname
break break
case 'CRM Notification':
page = 'crm-notification'
id = doc.reference_docname
openDesk = true
break
case 'FCRM Note': case 'FCRM Note':
page = 'notes' page = 'notes'
id = `view?open=${doc.reference_docname}` id = `view?open=${doc.reference_docname}`
@ -137,11 +130,7 @@ const viewLinkedDoc = (doc) => {
default: default:
break break
} }
let base = '/crm' window.open(`/crm/${page}/${id}`)
if (openDesk) {
base = '/app'
}
window.open(`${base}/${page}/${id}`)
} }
const getDoctypeName = (doctype) => { const getDoctypeName = (doctype) => {

View File

@ -11,18 +11,19 @@
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<Button <Button
v-if="isManager() && !isMobileView" v-if="isManager() && !isMobileView"
:tooltip="__('Edit fields layout')"
variant="ghost" variant="ghost"
:icon="EditIcon"
class="w-7" class="w-7"
@click="openQuickEntryModal" @click="openQuickEntryModal"
/> >
<Button <template #icon>
icon="x" <EditIcon />
variant="ghost" </template>
class="w-7" </Button>
@click="show = false" <Button variant="ghost" class="w-7" @click="show = false">
/> <template #icon>
<FeatherIcon name="x" class="size-4" />
</template>
</Button>
</div> </div>
</div> </div>
<div v-if="tabs.data && _address.doc"> <div v-if="tabs.data && _address.doc">

View File

@ -36,17 +36,18 @@
<Button <Button
v-if="!isMobileView" v-if="!isMobileView"
variant="ghost" variant="ghost"
:tooltip="__('Edit call log')"
:icon="EditIcon"
class="w-7" class="w-7"
@click="openCallLogModal" @click="openCallLogModal"
/> >
<Button <template #icon>
icon="x" <EditIcon />
variant="ghost" </template>
class="w-7" </Button>
@click="show = false" <Button variant="ghost" class="w-7" @click="show = false">
/> <template #icon>
<FeatherIcon name="x" class="size-4" />
</template>
</Button>
</div> </div>
</div> </div>
<div class="flex flex-col gap-3.5"> <div class="flex flex-col gap-3.5">

View File

@ -13,17 +13,18 @@
<Button <Button
v-if="isManager() && !isMobileView" v-if="isManager() && !isMobileView"
variant="ghost" variant="ghost"
:tooltip="__('Edit fields layout')"
:icon="EditIcon"
class="w-7" class="w-7"
@click="openQuickEntryModal" @click="openQuickEntryModal"
/> >
<Button <template #icon>
variant="ghost" <EditIcon />
class="w-7" </template>
@click="show = false" </Button>
icon="x" <Button variant="ghost" class="w-7" @click="show = false">
/> <template #icon>
<FeatherIcon name="x" class="size-4" />
</template>
</Button>
</div> </div>
</div> </div>
<div v-if="tabs.data"> <div v-if="tabs.data">
@ -36,7 +37,7 @@
</div> </div>
</div> </div>
<div class="px-4 pt-4 pb-7 sm:px-6"> <div class="px-4 pt-4 pb-7 sm:px-6">
<div class="flex justify-end gap-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"
@ -60,7 +61,7 @@ import { showQuickEntryModal, quickEntryProps } from '@/composables/modals'
import { getRandom } from '@/utils' import { getRandom } from '@/utils'
import { capture } from '@/telemetry' import { capture } from '@/telemetry'
import { useDocument } from '@/data/document' import { useDocument } from '@/data/document'
import { createResource, ErrorMessage, Badge } from 'frappe-ui' import { FeatherIcon, createResource, ErrorMessage, Badge } from 'frappe-ui'
import { ref, nextTick, computed, onMounted } from 'vue' import { ref, nextTick, computed, onMounted } from 'vue'
const props = defineProps({ const props = defineProps({

View File

@ -13,16 +13,17 @@
v-if="isManager() && !isMobileView" v-if="isManager() && !isMobileView"
variant="ghost" variant="ghost"
class="w-7" class="w-7"
:tooltip="__('Edit fields layout')"
:icon="EditIcon"
@click="openQuickEntryModal" @click="openQuickEntryModal"
/> >
<Button <template #icon>
variant="ghost" <EditIcon />
class="w-7" </template>
@click="show = false" </Button>
icon="x" <Button variant="ghost" class="w-7" @click="show = false">
/> <template #icon>
<FeatherIcon name="x" class="size-4" />
</template>
</Button>
</div> </div>
</div> </div>
<FieldLayout <FieldLayout
@ -89,16 +90,12 @@ const { document: _contact, triggerOnBeforeCreate } = useDocument('Contact')
async function createContact() { async function createContact() {
if (_contact.doc.email_id) { if (_contact.doc.email_id) {
_contact.doc.email_ids = [ _contact.doc.email_ids = [{ email_id: _contact.doc.email_id, is_primary: 1 }]
{ email_id: _contact.doc.email_id, is_primary: 1 },
]
delete _contact.doc.email_id delete _contact.doc.email_id
} }
if (_contact.doc.mobile_no) { if (_contact.doc.mobile_no) {
_contact.doc.phone_nos = [ _contact.doc.phone_nos = [{ phone: _contact.doc.mobile_no, is_primary_mobile_no: 1 }]
{ phone: _contact.doc.mobile_no, is_primary_mobile_no: 1 },
]
delete _contact.doc.mobile_no delete _contact.doc.mobile_no
} }

View File

@ -1,5 +1,17 @@
<template> <template>
<Dialog v-model="show" :options="{ size: 'xl' }"> <Dialog
v-model="show"
:options="{
size: 'xl',
actions: [
{
label: __('Convert'),
variant: 'solid',
onClick: convertToDeal,
},
],
}"
>
<template #body-header> <template #body-header>
<div class="mb-6 flex items-center justify-between"> <div class="mb-6 flex items-center justify-between">
<div> <div>
@ -11,10 +23,12 @@
<Button <Button
v-if="isManager() && !isMobileView" v-if="isManager() && !isMobileView"
variant="ghost" variant="ghost"
:tooltip="__('Edit deal\'s mandatory fields layout')"
:icon="EditIcon"
@click="openQuickEntryModal" @click="openQuickEntryModal"
/> >
<template #icon>
<EditIcon class="h-4 w-4" />
</template>
</Button>
<Button icon="x" variant="ghost" @click="show = false" /> <Button icon="x" variant="ghost" @click="show = false" />
</div> </div>
</div> </div>
@ -78,11 +92,6 @@
/> />
<ErrorMessage class="mt-4" :message="error" /> <ErrorMessage class="mt-4" :message="error" />
</template> </template>
<template #actions>
<div class="flex justify-end">
<Button :label="__('Convert')" variant="solid" @click="convertToDeal" />
</div>
</template>
</Dialog> </Dialog>
</template> </template>
<script setup> <script setup>

View File

@ -13,16 +13,17 @@
v-if="isManager() && !isMobileView" v-if="isManager() && !isMobileView"
variant="ghost" variant="ghost"
class="w-7" class="w-7"
:tooltip="__('Edit fields layout')"
:icon="EditIcon"
@click="openQuickEntryModal" @click="openQuickEntryModal"
/> >
<Button <template #icon>
variant="ghost" <EditIcon />
class="w-7" </template>
icon="x" </Button>
@click="show = false" <Button variant="ghost" class="w-7" @click="show = false">
/> <template #icon>
<FeatherIcon name="x" class="size-4" />
</template>
</Button>
</div> </div>
</div> </div>
<div v-if="tabs.data"> <div v-if="tabs.data">

View File

@ -13,16 +13,17 @@
v-if="isManager() && !isMobileView" v-if="isManager() && !isMobileView"
variant="ghost" variant="ghost"
class="w-7" class="w-7"
:tooltip="__('Edit fields layout')"
:icon="EditIcon"
@click="openQuickEntryModal" @click="openQuickEntryModal"
/> >
<Button <template #icon>
variant="ghost" <EditIcon />
class="w-7" </template>
icon="x" </Button>
@click="show = false" <Button variant="ghost" class="w-7" @click="show = false">
/> <template #icon>
<FeatherIcon name="x" class="size-4" />
</template>
</Button>
</div> </div>
</div> </div>
<div> <div>

View File

@ -13,16 +13,17 @@
v-if="isManager() && !isMobileView" v-if="isManager() && !isMobileView"
variant="ghost" variant="ghost"
class="w-7" class="w-7"
:tooltip="__('Edit fields layout')"
:icon="EditIcon"
@click="openQuickEntryModal" @click="openQuickEntryModal"
/> >
<Button <template #icon>
variant="ghost" <EditIcon />
class="w-7" </template>
@click="show = false" </Button>
icon="x" <Button variant="ghost" class="w-7" @click="show = false">
/> <template #icon>
<FeatherIcon name="x" class="size-4" />
</template>
</Button>
</div> </div>
</div> </div>
<div> <div>

View File

@ -1,60 +1,45 @@
<template> <template>
<Dialog v-model="show" :options="{ size: 'xl' }"> <Dialog v-model="show" :options="{
size: 'xl',
actions: [
{
label: editMode ? __('Update') : __('Create'),
variant: 'solid',
onClick: () => updateNote(),
},
],
}">
<template #body-title> <template #body-title>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<h3 class="text-2xl font-semibold leading-6 text-ink-gray-9"> <h3 class="text-2xl font-semibold leading-6 text-ink-gray-9">
{{ editMode ? __('Edit Note') : __('Create Note') }} {{ editMode ? __('Edit Note') : __('Create Note') }}
</h3> </h3>
<Button <Button v-if="_note?.reference_docname" size="sm" :label="_note.reference_doctype == 'CRM Deal'
v-if="_note?.reference_docname"
size="sm"
:label="
_note.reference_doctype == 'CRM Deal'
? __('Open Deal') ? __('Open Deal')
: __('Open Lead') : __('Open Lead')
" " @click="redirect()">
:iconRight="ArrowUpRightIcon" <template #suffix>
@click="redirect()" <ArrowUpRightIcon class="w-4 h-4" />
/> </template>
</Button>
</div> </div>
</template> </template>
<template #body-content> <template #body-content>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div> <div>
<FormControl <FormControl ref="title" :label="__('Title')" v-model="_note.title" :placeholder="__('Call with John Doe')"
ref="title" required />
:label="__('Title')"
v-model="_note.title"
:placeholder="__('Call with John Doe')"
required
/>
</div> </div>
<div> <div>
<div class="mb-1.5 text-xs text-ink-gray-5">{{ __('Content') }}</div> <div class="mb-1.5 text-xs text-ink-gray-5">{{ __('Content') }}</div>
<TextEditor <TextEditor variant="outline" ref="content"
variant="outline"
ref="content"
editor-class="!prose-sm overflow-auto min-h-[180px] max-h-80 py-1.5 px-2 rounded border border-[--surface-gray-2] bg-surface-gray-2 placeholder-ink-gray-4 hover:border-outline-gray-modals hover:bg-surface-gray-3 hover:shadow-sm focus:bg-surface-white focus:border-outline-gray-4 focus:shadow-sm focus:ring-0 focus-visible:ring-2 focus-visible:ring-outline-gray-3 text-ink-gray-8 transition-colors" editor-class="!prose-sm overflow-auto min-h-[180px] max-h-80 py-1.5 px-2 rounded border border-[--surface-gray-2] bg-surface-gray-2 placeholder-ink-gray-4 hover:border-outline-gray-modals hover:bg-surface-gray-3 hover:shadow-sm focus:bg-surface-white focus:border-outline-gray-4 focus:shadow-sm focus:ring-0 focus-visible:ring-2 focus-visible:ring-outline-gray-3 text-ink-gray-8 transition-colors"
:bubbleMenu="true" :bubbleMenu="true" :content="_note.content" @change="(val) => (_note.content = val)" :placeholder="__('Took a call with John Doe and discussed the new project.')
:content="_note.content" " />
@change="(val) => (_note.content = val)"
:placeholder="
__('Took a call with John Doe and discussed the new project.')
"
/>
</div> </div>
<ErrorMessage class="mt-4" v-if="error" :message="__(error)" /> <ErrorMessage class="mt-4" v-if="error" :message="__(error)" />
</div> </div>
</template> </template>
<template #actions>
<div class="flex justify-end">
<Button
:label="editMode ? __('Update') : __('Create')"
variant="solid"
@click="updateNote"
/>
</div>
</template>
</Dialog> </Dialog>
</template> </template>
@ -107,9 +92,7 @@ async function updateNote() {
emit('after', d) emit('after', d)
} }
} else { } else {
let d = await call( let d = await call('frappe.client.insert', {
'frappe.client.insert',
{
doc: { doc: {
doctype: 'FCRM Note', doctype: 'FCRM Note',
title: _note.value.title, title: _note.value.title,
@ -117,15 +100,13 @@ async function updateNote() {
reference_doctype: props.doctype, reference_doctype: props.doctype,
reference_docname: props.doc || '', reference_docname: props.doc || '',
}, },
}, }, {
{
onError: (err) => { onError: (err) => {
if (err.error.exc_type == 'MandatoryError') { if (err.error.exc_type == 'MandatoryError') {
error.value = 'Title is mandatory' error.value = "Title is mandatory"
} }
}, }
}, })
)
if (d.name) { if (d.name) {
updateOnboardingStep('create_first_note') updateOnboardingStep('create_first_note')
capture('note_created') capture('note_created')

View File

@ -13,16 +13,17 @@
v-if="isManager() && !isMobileView" v-if="isManager() && !isMobileView"
variant="ghost" variant="ghost"
class="w-7" class="w-7"
:tooltip="__('Edit fields layout')"
:icon="EditIcon"
@click="openQuickEntryModal" @click="openQuickEntryModal"
/> >
<Button <template #icon>
variant="ghost" <EditIcon />
class="w-7" </template>
@click="show = false" </Button>
icon="x" <Button variant="ghost" class="w-7" @click="show = false">
/> <template #icon>
<FeatherIcon name="x" class="size-4" />
</template>
</Button>
</div> </div>
</div> </div>
<FieldLayout <FieldLayout
@ -108,7 +109,6 @@ async function createOrganization() {
onError: (err) => { onError: (err) => {
if (err.error.exc_type == 'ValidationError') { if (err.error.exc_type == 'ValidationError') {
error.value = err.error?.messages?.[0] error.value = err.error?.messages?.[0]
loading.value = false
} }
}, },
}, },
@ -175,5 +175,6 @@ function openAddressModal(_address) {
doctype: 'Address', doctype: 'Address',
address: _address, address: _address,
} }
nextTick(() => (show.value = false))
} }
</script> </script>

View File

@ -1,5 +1,17 @@
<template> <template>
<Dialog v-model="show" :options="{ size: 'xl' }"> <Dialog
v-model="show"
:options="{
size: 'xl',
actions: [
{
label: editMode ? __('Update') : __('Create'),
variant: 'solid',
onClick: () => updateTask(),
},
],
}"
>
<template #body-title> <template #body-title>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<h3 class="text-2xl font-semibold leading-6 text-ink-gray-9"> <h3 class="text-2xl font-semibold leading-6 text-ink-gray-9">
@ -13,9 +25,12 @@
? __('Open Deal') ? __('Open Deal')
: __('Open Lead') : __('Open Lead')
" "
:iconRight="ArrowUpRightIcon"
@click="redirect()" @click="redirect()"
/> >
<template #suffix>
<ArrowUpRightIcon class="w-4 h-4" />
</template>
</Button>
</div> </div>
</template> </template>
<template #body-content> <template #body-content>
@ -47,7 +62,7 @@
</div> </div>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<Dropdown :options="taskStatusOptions(updateTaskStatus)"> <Dropdown :options="taskStatusOptions(updateTaskStatus)">
<Button :label="_task.status"> <Button :label="_task.status" class="justify-between w-full">
<template #prefix> <template #prefix>
<TaskStatusIcon :status="_task.status" /> <TaskStatusIcon :status="_task.status" />
</template> </template>
@ -78,17 +93,15 @@
</Tooltip> </Tooltip>
</template> </template>
</Link> </Link>
<div class="w-36">
<DateTimePicker <DateTimePicker
class="datepicker" class="datepicker w-36"
v-model="_task.due_date" v-model="_task.due_date"
:placeholder="__('01/04/2024 11:30 PM')" :placeholder="__('01/04/2024 11:30 PM')"
:formatter="(date) => getFormat(date, '', true, true)" :formatter="(date) => getFormat(date, '', true, true)"
input-class="border-none" input-class="border-none"
/> />
</div>
<Dropdown :options="taskPriorityOptions(updateTaskPriority)"> <Dropdown :options="taskPriorityOptions(updateTaskPriority)">
<Button :label="_task.priority"> <Button :label="_task.priority" class="justify-between w-full">
<template #prefix> <template #prefix>
<TaskPriorityIcon :priority="_task.priority" /> <TaskPriorityIcon :priority="_task.priority" />
</template> </template>
@ -98,15 +111,6 @@
<ErrorMessage class="mt-4" v-if="error" :message="__(error)" /> <ErrorMessage class="mt-4" v-if="error" :message="__(error)" />
</div> </div>
</template> </template>
<template #actions>
<div class="flex justify-end">
<Button
:label="editMode ? __('Update') : __('Create')"
variant="solid"
@click="updateTask"
/>
</div>
</template>
</Dialog> </Dialog>
</template> </template>

View File

@ -7,6 +7,17 @@
: duplicateMode : duplicateMode
? __('Duplicate View') ? __('Duplicate View')
: __('Create View'), : __('Create View'),
actions: [
{
label: editMode
? __('Save Changes')
: duplicateMode
? __('Duplicate')
: __('Create'),
variant: 'solid',
onClick: () => (editMode ? update() : create()),
},
],
}" }"
> >
<template #body-content> <template #body-content>
@ -31,21 +42,6 @@
/> />
</div> </div>
</template> </template>
<template #actions>
<div class="flex justify-end">
<Button
variant="solid"
:label="
editMode
? __('Save Changes')
: duplicateMode
? __('Duplicate')
: __('Create')
"
@click="() => (editMode ? update() : create())"
/>
</div>
</template>
</Dialog> </Dialog>
</template> </template>

View File

@ -9,9 +9,21 @@
$attrs.class, $attrs.class,
showDropdown ? 'rounded-br-none rounded-tr-none' : '', showDropdown ? 'rounded-br-none rounded-tr-none' : '',
]" ]"
:iconLeft="activeButton.icon"
@click="() => activeButton.onClick()" @click="() => activeButton.onClick()"
>
<template #prefix>
<FeatherIcon
v-if="activeButton.icon && typeof activeButton.icon === 'string'"
:name="activeButton.icon"
class="h-4 w-4"
/> />
<component
v-else-if="activeButton.icon"
:is="activeButton.icon"
class="h-4 w-4"
/>
</template>
</Button>
<Dropdown <Dropdown
v-if="showDropdown" v-if="showDropdown"
:options="parsedOptions" :options="parsedOptions"
@ -42,6 +54,7 @@ const showDropdown = ref(props.options?.length > 1)
const activeButton = ref(props.options?.[0] || {}) const activeButton = ref(props.options?.[0] || {})
const parsedOptions = computed(() => { const parsedOptions = computed(() => {
debugger
return ( return (
props.options?.map((option) => { props.options?.map((option) => {
return { return {

View File

@ -0,0 +1,60 @@
<template>
<Popover v-slot="{ open }">
<PopoverButton
as="div"
ref="reference"
@click="updatePosition"
@focusin="updatePosition"
@keydown="updatePosition"
v-slot="{ open }"
>
<slot name="target" v-bind="{ open }" />
</PopoverButton>
<div v-show="open">
<PopoverPanel
v-slot="{ open, close }"
ref="popover"
static
class="z-[100]"
>
<slot name="body" v-bind="{ open, close }" />
</PopoverPanel>
</div>
</Popover>
</template>
<script setup>
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
import { createPopper } from '@popperjs/core'
import { nextTick, ref, onBeforeUnmount } from 'vue'
const props = defineProps({
placement: {
type: String,
default: 'bottom-start',
},
})
const reference = ref(null)
const popover = ref(null)
let popper = ref(null)
function setupPopper() {
if (!popper.value) {
popper.value = createPopper(reference.value.el, popover.value.el, {
placement: props.placement,
})
} else {
popper.value.update()
}
}
function updatePosition() {
nextTick(() => setupPopper())
}
onBeforeUnmount(() => {
popper.value?.destroy()
})
</script>

View File

@ -16,18 +16,24 @@
> >
<div class="text-base font-medium">{{ __('Notifications') }}</div> <div class="text-base font-medium">{{ __('Notifications') }}</div>
<div class="flex gap-1"> <div class="flex gap-1">
<Button <Tooltip :text="__('Mark all as read')">
:tooltip="__('Mark all as read')" <div>
:icon="MarkAsDoneIcon" <Button variant="ghost" @click="() => markAllAsRead()">
variant="ghost" <template #icon>
@click="markAllAsRead" <MarkAsDoneIcon class="h-4 w-4" />
/> </template>
<Button </Button>
:tooltip="__('Close')" </div>
icon="x" </Tooltip>
variant="ghost" <Tooltip :text="__('Close')">
@click="() => toggle()" <div>
/> <Button variant="ghost" @click="() => toggle()">
<template #icon>
<FeatherIcon name="x" class="h-4 w-4" />
</template>
</Button>
</div>
</Tooltip>
</div> </div>
</div> </div>
<div <div
@ -94,6 +100,7 @@ import { globalStore } from '@/stores/global'
import { timeAgo } from '@/utils' import { timeAgo } from '@/utils'
import { onClickOutside } from '@vueuse/core' import { onClickOutside } from '@vueuse/core'
import { capture } from '@/telemetry' import { capture } from '@/telemetry'
import { Tooltip } from 'frappe-ui'
import { ref, onMounted, onBeforeUnmount } from 'vue' import { ref, onMounted, onBeforeUnmount } from 'vue'
const { $socket } = globalStore() const { $socket } = globalStore()

View File

@ -1,69 +0,0 @@
<template>
<Popover>
<template #target="{ isOpen, togglePopover }">
<Button
:label="value"
class="dropdown-button flex items-center justify-between bg-surface-white !px-2.5 py-1.5 text-base text-ink-gray-8 placeholder-ink-gray-4 transition-colors hover:bg-surface-white focus:bg-surface-white focus:shadow-sm focus:outline-none focus:ring-0"
@click="togglePopover"
>
<div v-if="value" class="truncate">{{ value }}</div>
<div v-else class="text-base leading-5 text-ink-gray-4 truncate">
{{ placeholder }}
</div>
<template #suffix>
<FeatherIcon
:name="isOpen ? 'chevron-up' : 'chevron-down'"
class="h-4 text-ink-gray-5"
/>
</template>
</Button>
</template>
<template #body>
<div
class="my-2 p-1.5 min-w-40 space-y-1.5 divide-y divide-outline-gray-1 rounded-lg bg-surface-modal shadow-2xl ring-1 ring-black ring-opacity-5 focus:outline-none"
>
<div>
<PrimaryDropdownItem
v-for="option in options"
:key="option.name || option.value"
:option="option"
/>
<div v-if="!options?.length">
<div class="p-1.5 pl-3 pr-4 text-base text-ink-gray-4">
{{ __('No {0} Available', [label]) }}
</div>
</div>
</div>
<div class="pt-1.5">
<Button
variant="ghost"
class="w-full !justify-start"
:label="__('Create New')"
iconLeft="plus"
@click="create && create()"
/>
</div>
</div>
</template>
</Popover>
</template>
<script setup>
import PrimaryDropdownItem from '@/components/PrimaryDropdownItem.vue'
import { Popover } from 'frappe-ui'
const props = defineProps({
value: { type: [String, Number], default: '' },
placeholder: { type: String, default: '' },
options: { type: Array, default: [] },
create: { type: Function },
label: { type: String, default: '' },
})
</script>
<style scoped>
.dropdown-button {
border-color: transparent;
background: transparent;
}
</style>

Some files were not shown because too many files have changed in this diff Show More