Compare commits
384 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf849f7dff | ||
|
|
3e9fdb8d20 | ||
|
|
260755fd2e | ||
|
|
85df56b464 | ||
|
|
a9e956b5fc | ||
|
|
905eb63c5f | ||
|
|
3eee437014 | ||
|
|
4498de2041 | ||
|
|
bfb0d25765 | ||
|
|
9dd6ffa1e1 | ||
|
|
05322e805f | ||
|
|
8dc6db3218 | ||
|
|
1b63384eac | ||
|
|
37bbbb6b4b | ||
|
|
6f0244c2b6 | ||
|
|
e08bc9cd20 | ||
|
|
2043157567 | ||
|
|
8093a422cd | ||
|
|
4bef919d38 | ||
|
|
874947b8ae | ||
|
|
ca90c0406e | ||
|
|
6bf27d852b | ||
|
|
793cb76789 | ||
|
|
ff07054ca3 | ||
|
|
2ee5269d3e | ||
|
|
e27954da52 | ||
|
|
2faa0d0f04 | ||
|
|
f1664eec2f | ||
|
|
7f2efea7cb | ||
|
|
d2cc6b7c2e | ||
|
|
1afb6d6827 | ||
|
|
ccaf136830 | ||
|
|
58a41d1b11 | ||
|
|
777f3ac06c | ||
|
|
105c78e264 | ||
|
|
46cc1d2924 | ||
|
|
ff4ca9fe66 | ||
|
|
4989dc0921 | ||
|
|
1e613ebcd1 | ||
|
|
4f8f195d77 | ||
|
|
af64b86a04 | ||
|
|
7e9bc0524e | ||
|
|
9d0a0d1d32 | ||
|
|
9c84a8be7f | ||
|
|
d24537489e | ||
|
|
bd89b3b356 | ||
|
|
988fb90ddb | ||
|
|
8018b1766c | ||
|
|
9c4c2a0aca | ||
|
|
803e639961 | ||
|
|
fabd362b2a | ||
|
|
7dd98733f1 | ||
|
|
ee4b7721b0 | ||
|
|
95bc551254 | ||
|
|
2546bdabb1 | ||
|
|
af248964c6 | ||
|
|
8749f7bfd0 | ||
|
|
d18618b856 | ||
|
|
ce4af4907a | ||
|
|
84d24a384b | ||
|
|
29d86859d4 | ||
|
|
be452fee58 | ||
|
|
100eec0677 | ||
|
|
a4d3852c0e | ||
|
|
0399fc32be | ||
|
|
a79192ef4d | ||
|
|
e10ec543a7 | ||
|
|
1962b9a103 | ||
|
|
a3abaa57ec | ||
|
|
1bf3f7a38c | ||
|
|
af81750388 | ||
|
|
7330a3c2a5 | ||
|
|
9e8a4024a5 | ||
|
|
7ef00965fa | ||
|
|
97925aae15 | ||
|
|
0b75228722 | ||
|
|
a360fa774b | ||
|
|
4601b56ee1 | ||
|
|
d985a44291 | ||
|
|
ce66705e9c | ||
|
|
004923419c | ||
|
|
49ed1ac174 | ||
|
|
a7dd1e9bf6 | ||
|
|
4cfd0022f4 | ||
|
|
fc3d8cd94d | ||
|
|
5473a93b5e | ||
|
|
57d306ea1f | ||
|
|
7c324bd07f | ||
|
|
09421217b4 | ||
|
|
630dfcd0e7 | ||
|
|
b999a375b3 | ||
|
|
b5f5a3b5d5 | ||
|
|
814d39572a | ||
|
|
6fc4c0699a | ||
|
|
50ea06a568 | ||
|
|
daf0ddaab4 | ||
|
|
70107a4836 | ||
|
|
f2e6380d3e | ||
|
|
eed1e23b5b | ||
|
|
1bb75dd911 | ||
|
|
1086ce406f | ||
|
|
39c5497363 | ||
|
|
4bc31431e7 | ||
|
|
fe290877e4 | ||
|
|
e4bdc0586e | ||
|
|
695f9e1303 | ||
|
|
98747bdc2a | ||
|
|
16ed1ad060 | ||
|
|
ff312d964b | ||
|
|
49d7af5548 | ||
|
|
a30d21c346 | ||
|
|
f459bd57ba | ||
|
|
b4d89e1a5a | ||
|
|
25bc9d8acb | ||
|
|
568477f9c7 | ||
|
|
9c04ade20d | ||
|
|
7940211fad | ||
|
|
df0968ed67 | ||
|
|
89462e63cb | ||
|
|
23c53ffa9a | ||
|
|
ae3df8d391 | ||
|
|
2d51933ceb | ||
|
|
a7958dc2a2 | ||
|
|
1c4f78b01c | ||
|
|
2722ef6cad | ||
|
|
ac7d3907c2 | ||
|
|
17b7c6ecef | ||
|
|
8d7a155d78 | ||
|
|
6dc85ad1b2 | ||
|
|
ac98c1a090 | ||
|
|
90a6bde438 | ||
|
|
5fea7bf0e2 | ||
|
|
1031e9c4ec | ||
|
|
c6e4b9b5d3 | ||
|
|
f2a4b9ec56 | ||
|
|
14a1af4455 | ||
|
|
e3ab227124 | ||
|
|
5f6cc26126 | ||
|
|
8051fd1f99 | ||
|
|
1fc98f619c | ||
|
|
5921af8c0b | ||
|
|
fe505d33d2 | ||
|
|
300f0b24e2 | ||
|
|
b7c26e35e9 | ||
|
|
4c7c8b915a | ||
|
|
d80feb1e77 | ||
|
|
2a8c7307f0 | ||
|
|
0683d93621 | ||
|
|
5ce2fcd368 | ||
|
|
5372fdcaf1 | ||
|
|
9f46bbd25d | ||
|
|
87e82ce39e | ||
|
|
443aeddca0 | ||
|
|
dc7d5f57b8 | ||
|
|
1cbd633e5b | ||
|
|
cf58a634e9 | ||
|
|
b118e75bec | ||
|
|
2b1048fd83 | ||
|
|
7468f19b48 | ||
|
|
f994dd36a9 | ||
|
|
dc98613296 | ||
|
|
f132a46206 | ||
|
|
cfeac13c9c | ||
|
|
b82dc0473a | ||
|
|
8f0e8f3f52 | ||
|
|
7a9da275da | ||
|
|
cc25feea09 | ||
|
|
b64058bd10 | ||
|
|
b6af543243 | ||
|
|
7bcee291c7 | ||
|
|
570a45eeaf | ||
|
|
dcadd3d0c1 | ||
|
|
8d4975554d | ||
|
|
4bf8c8d0b8 | ||
|
|
0d9264e5e2 | ||
|
|
068f303448 | ||
|
|
e8e4367eb7 | ||
|
|
afadf9f8f4 | ||
|
|
8ab6da55de | ||
|
|
b066eb8c75 | ||
|
|
c33f2e936a | ||
|
|
2f02b8ccfd | ||
|
|
9e9b0f7266 | ||
|
|
148e6ef8bb | ||
|
|
00398b188c | ||
|
|
80e43864b7 | ||
|
|
9a9b91b76d | ||
|
|
d555222ebb | ||
|
|
222ba09c67 | ||
|
|
0c8a3edf5e | ||
|
|
c44f7d5834 | ||
|
|
f072fc992c | ||
|
|
092310846c | ||
|
|
eb00a7c692 | ||
|
|
8a7ed1e8f1 | ||
|
|
a852e33605 | ||
|
|
7f3732f0e0 | ||
|
|
75a9bd290c | ||
|
|
687c1d362c | ||
|
|
e771a63640 | ||
|
|
e87785fac5 | ||
|
|
c84da0c7b4 | ||
|
|
aeb4590a6f | ||
|
|
d6567c7f01 | ||
|
|
8083fc46a8 | ||
|
|
7f6a20ff17 | ||
|
|
95df85f676 | ||
|
|
728150e324 | ||
|
|
2ac8a06bae | ||
|
|
2f56af0384 | ||
|
|
06d647dd72 | ||
|
|
ee964637ac | ||
|
|
4f113fab91 | ||
|
|
8be5535a2b | ||
|
|
c0050dda71 | ||
|
|
56eecdd260 | ||
|
|
ead86735d4 | ||
|
|
76f6da88f3 | ||
|
|
e252ac8370 | ||
|
|
03b920570e | ||
|
|
da4e84d6d0 | ||
|
|
2f90a075fc | ||
|
|
de85fa5d5b | ||
|
|
e43500887f | ||
|
|
8f69a49e2c | ||
|
|
31cbe78320 | ||
|
|
fa85c61a0a | ||
|
|
e87e1ad61d | ||
|
|
bd75fed217 | ||
|
|
1ede125c80 | ||
|
|
e1e2049b1f | ||
|
|
32d9e1e2ba | ||
|
|
ea5b0fdacd | ||
|
|
a7374bedb2 | ||
|
|
5b7437d36a | ||
|
|
8fca1842c2 | ||
|
|
2473cf88ef | ||
|
|
4de04d6760 | ||
|
|
f4dc6f8338 | ||
|
|
df5ee5a50c | ||
|
|
7b4a3dee6e | ||
|
|
3dd6c5d1ed | ||
|
|
bc7a148999 | ||
|
|
f3f0322003 | ||
|
|
aa128a3411 | ||
|
|
57f74dbbff | ||
|
|
88b9c6af1d | ||
|
|
243832812f | ||
|
|
be09a8fe30 | ||
|
|
f5e54eefef | ||
|
|
867da93a39 | ||
|
|
580f61ab71 | ||
|
|
b55011a58e | ||
|
|
a8504a910e | ||
|
|
ffc048977f | ||
|
|
212709782a | ||
|
|
9fb0d3175d | ||
|
|
4839298ba4 | ||
|
|
b40c72b95c | ||
|
|
5a941e0944 | ||
|
|
4987545595 | ||
|
|
f53e35117a | ||
|
|
de2939d863 | ||
|
|
25a26d3e2c | ||
|
|
efd02c3459 | ||
|
|
13e72bf7f3 | ||
|
|
7b8cc6caa3 | ||
|
|
d1e66cd5bb | ||
|
|
3a9bad7954 | ||
|
|
3f6e69c3f6 | ||
|
|
57fb0e07cf | ||
|
|
4cfab1fbb6 | ||
|
|
c851b9f4b3 | ||
|
|
7d8f980c87 | ||
|
|
2ccae25bfe | ||
|
|
1da242fd37 | ||
|
|
35b7e69426 | ||
|
|
fbaca7f154 | ||
|
|
913094dce3 | ||
|
|
4d619a908c | ||
|
|
6dc8975c08 | ||
|
|
1b0a92c0c5 | ||
|
|
bd3aca85ac | ||
|
|
6d7372240f | ||
|
|
c799d1502b | ||
|
|
acdfe6c82b | ||
|
|
17687690cf | ||
|
|
f28920dd60 | ||
|
|
90f61dffa6 | ||
|
|
556f8e6b11 | ||
|
|
78aea3bbac | ||
|
|
67f75289c7 | ||
|
|
8e86a06858 | ||
|
|
445f5d7ade | ||
|
|
015228488b | ||
|
|
9a0e0ed721 | ||
|
|
89b392d59b | ||
|
|
799dedb582 | ||
|
|
9418df86f9 | ||
|
|
57dd748cae | ||
|
|
06b4e4d544 | ||
|
|
e98d54cdd9 | ||
|
|
a00838b7a0 | ||
|
|
fad4bc994d | ||
|
|
b3752d63db | ||
|
|
4c13c5143f | ||
|
|
9423af5cc1 | ||
|
|
dcc3b88d4f | ||
|
|
a55b69a945 | ||
|
|
89abee30bf | ||
|
|
066371bd76 | ||
|
|
f61a40698d | ||
|
|
0b9c0915c7 | ||
|
|
e6781ea4bb | ||
|
|
277fb85e32 | ||
|
|
4592dfcd13 | ||
|
|
cf0f922f2f | ||
|
|
bc35d6b98f | ||
|
|
3fbd40b591 | ||
|
|
f50b8a78eb | ||
|
|
5212a61388 | ||
|
|
0d1c057cf3 | ||
|
|
5a243d29da | ||
|
|
992b47e531 | ||
|
|
ee8f806f64 | ||
|
|
bc0ca74f88 | ||
|
|
55dabaf877 | ||
|
|
006c7efc06 | ||
|
|
4c809a9166 | ||
|
|
9e19e54f75 | ||
|
|
806f75a2fe | ||
|
|
295b0f4c2a | ||
|
|
7945527fd6 | ||
|
|
696531f392 | ||
|
|
c0d43a9b58 | ||
|
|
584250c4e5 | ||
|
|
ec467ae126 | ||
|
|
579fe78e6f | ||
|
|
a246a5e6e4 | ||
|
|
daaf015462 | ||
|
|
edb68fe08b | ||
|
|
7e736b2892 | ||
|
|
3b18f3a86a | ||
|
|
f6cf935c9c | ||
|
|
1dfbcd1055 | ||
|
|
3326230062 | ||
|
|
de4471876f | ||
|
|
41ce361f04 | ||
|
|
466a2b58ee | ||
|
|
0e71880463 | ||
|
|
1afbc001b5 | ||
|
|
9e3eba8ab2 | ||
|
|
32405f3120 | ||
|
|
d7735d634d | ||
|
|
b97a80249c | ||
|
|
9c3ddeaf7d | ||
|
|
de14eb3ffb | ||
|
|
74cce77dc5 | ||
|
|
2352f51838 | ||
|
|
0e2be93e92 | ||
|
|
903c214c3d | ||
|
|
9c653cf5e7 | ||
|
|
3c296a67a2 | ||
|
|
9196369c17 | ||
|
|
853ef4859d | ||
|
|
c861fbda49 | ||
|
|
a81f3685bc | ||
|
|
73d13351b5 | ||
|
|
43e8f2a48f | ||
|
|
f347919dbf | ||
|
|
14622a00b3 | ||
|
|
4a83b52ab5 | ||
|
|
30fed5f6ad | ||
|
|
96031d2288 | ||
|
|
58a0ef2d0e | ||
|
|
7b2168232e | ||
|
|
d0dc642b12 | ||
|
|
e4722a79cc | ||
|
|
671ce54380 | ||
|
|
d4ac8772ae | ||
|
|
1df71070d0 | ||
|
|
58d4b7afae | ||
|
|
9e08b68f22 | ||
|
|
3a38ccace0 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -7,6 +7,5 @@ dev-dist
|
||||
tags
|
||||
node_modules
|
||||
crm/public/frontend
|
||||
frontend/yarn.lock
|
||||
crm/www/crm.html
|
||||
build
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
|
||||
__version__ = "1.52.2"
|
||||
__version__ = "1.53.1"
|
||||
__title__ = "Frappe CRM"
|
||||
|
||||
|
||||
32
crm/api/assignment_rule.py
Normal file
32
crm/api/assignment_rule.py
Normal file
@ -0,0 +1,32 @@
|
||||
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
|
||||
137
crm/api/doc.py
137
crm/api/doc.py
@ -750,7 +750,11 @@ def getCounts(d, doctype):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_linked_docs_of_document(doctype, docname):
|
||||
doc = frappe.get_doc(doctype, docname)
|
||||
try:
|
||||
doc = frappe.get_doc(doctype, docname)
|
||||
except frappe.DoesNotExistError:
|
||||
return []
|
||||
|
||||
linked_docs = get_linked_docs(doc)
|
||||
dynamic_linked_docs = get_dynamic_linked_docs(doc)
|
||||
|
||||
@ -759,7 +763,14 @@ def get_linked_docs_of_document(doctype, docname):
|
||||
|
||||
docs_data = []
|
||||
for doc in linked_docs:
|
||||
data = frappe.get_doc(doc["reference_doctype"], doc["reference_docname"])
|
||||
if not doc.get("reference_doctype") or not doc.get("reference_docname"):
|
||||
continue
|
||||
|
||||
try:
|
||||
data = frappe.get_doc(doc["reference_doctype"], doc["reference_docname"])
|
||||
except (frappe.DoesNotExistError, frappe.ValidationError):
|
||||
continue
|
||||
|
||||
title = data.get("title")
|
||||
if data.doctype == "CRM Call Log":
|
||||
title = f"Call from {data.get('from')} to {data.get('to')}"
|
||||
@ -767,6 +778,9 @@ def get_linked_docs_of_document(doctype, docname):
|
||||
if data.doctype == "CRM Deal":
|
||||
title = data.get("organization")
|
||||
|
||||
if data.doctype == "CRM Notification":
|
||||
title = data.get("message")
|
||||
|
||||
docs_data.append(
|
||||
{
|
||||
"doc": data.doctype,
|
||||
@ -779,25 +793,51 @@ def get_linked_docs_of_document(doctype, docname):
|
||||
|
||||
|
||||
def remove_doc_link(doctype, docname):
|
||||
linked_doc_data = frappe.get_doc(doctype, docname)
|
||||
linked_doc_data.update(
|
||||
{
|
||||
"reference_doctype": None,
|
||||
"reference_docname": None,
|
||||
}
|
||||
)
|
||||
linked_doc_data.save(ignore_permissions=True)
|
||||
if not doctype or not docname:
|
||||
return
|
||||
|
||||
try:
|
||||
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(
|
||||
{
|
||||
"reference_doctype": "",
|
||||
"reference_docname": "",
|
||||
}
|
||||
)
|
||||
linked_doc_data.save(ignore_permissions=True)
|
||||
except (frappe.DoesNotExistError, frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
def remove_contact_link(doctype, docname):
|
||||
linked_doc_data = frappe.get_doc(doctype, docname)
|
||||
linked_doc_data.update(
|
||||
{
|
||||
"contact": None,
|
||||
"contacts": [],
|
||||
}
|
||||
)
|
||||
linked_doc_data.save(ignore_permissions=True)
|
||||
if not doctype or not docname:
|
||||
return
|
||||
|
||||
try:
|
||||
linked_doc_data = frappe.get_doc(doctype, docname)
|
||||
linked_doc_data.update(
|
||||
{
|
||||
"contact": None,
|
||||
"contacts": [],
|
||||
}
|
||||
)
|
||||
linked_doc_data.save(ignore_permissions=True)
|
||||
except (frappe.DoesNotExistError, frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@ -806,13 +846,19 @@ def remove_linked_doc_reference(items, remove_contact=None, delete=False):
|
||||
items = frappe.parse_json(items)
|
||||
|
||||
for item in items:
|
||||
if remove_contact:
|
||||
remove_contact_link(item["doctype"], item["docname"])
|
||||
else:
|
||||
remove_doc_link(item["doctype"], item["docname"])
|
||||
if not item.get("doctype") or not item.get("docname"):
|
||||
continue
|
||||
|
||||
if delete:
|
||||
frappe.delete_doc(item["doctype"], item["docname"])
|
||||
try:
|
||||
if remove_contact:
|
||||
remove_contact_link(item["doctype"], item["docname"])
|
||||
else:
|
||||
remove_doc_link(item["doctype"], item["docname"])
|
||||
if delete:
|
||||
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"
|
||||
|
||||
@ -821,19 +867,40 @@ def remove_linked_doc_reference(items, remove_contact=None, delete=False):
|
||||
def delete_bulk_docs(doctype, items, delete_linked=False):
|
||||
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)
|
||||
if not isinstance(items, list):
|
||||
frappe.throw("Items must be a list")
|
||||
|
||||
for doc in items:
|
||||
linked_docs = get_linked_docs_of_document(doctype, doc)
|
||||
for linked_doc in linked_docs:
|
||||
remove_linked_doc_reference(
|
||||
[
|
||||
{
|
||||
"doctype": linked_doc["reference_doctype"],
|
||||
"docname": linked_doc["reference_docname"],
|
||||
}
|
||||
],
|
||||
remove_contact=doctype == "Contact",
|
||||
delete=delete_linked,
|
||||
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)
|
||||
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(
|
||||
[
|
||||
{
|
||||
"doctype": linked_doc["reference_doctype"],
|
||||
"docname": linked_doc["reference_docname"],
|
||||
}
|
||||
],
|
||||
remove_contact=doctype == "Contact",
|
||||
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:
|
||||
|
||||
@ -10,8 +10,15 @@ from crm.fcrm.doctype.crm_notification.crm_notification import notify_user
|
||||
def validate(doc, method):
|
||||
if doc.type == "Incoming" and doc.get("from"):
|
||||
name, doctype = get_lead_or_deal_from_number(doc.get("from"))
|
||||
doc.reference_doctype = doctype
|
||||
doc.reference_name = name
|
||||
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_name = name
|
||||
|
||||
|
||||
def on_update(doc, method):
|
||||
@ -29,7 +36,7 @@ def on_update(doc, method):
|
||||
def notify_agent(doc):
|
||||
if doc.type == "Incoming":
|
||||
doctype = doc.reference_doctype
|
||||
if doctype.startswith("CRM "):
|
||||
if doctype and doctype.startswith("CRM "):
|
||||
doctype = doctype[4:].lower()
|
||||
notification_text = f"""
|
||||
<div class="mb-2 leading-5 text-ink-gray-5">
|
||||
|
||||
@ -26,8 +26,9 @@ def create_default_manager_dashboard(force=False):
|
||||
doc.title = "Manager Dashboard"
|
||||
doc.layout = default_manager_dashboard_layout()
|
||||
doc.insert(ignore_permissions=True)
|
||||
elif force:
|
||||
else:
|
||||
doc = frappe.get_doc("CRM Dashboard", "Manager Dashboard")
|
||||
doc.layout = default_manager_dashboard_layout()
|
||||
doc.save(ignore_permissions=True)
|
||||
if force:
|
||||
doc.layout = default_manager_dashboard_layout()
|
||||
doc.save(ignore_permissions=True)
|
||||
return doc.layout
|
||||
|
||||
@ -129,15 +129,13 @@
|
||||
"fieldname": "email",
|
||||
"fieldtype": "Data",
|
||||
"label": "Primary Email",
|
||||
"options": "Email",
|
||||
"read_only": 1
|
||||
"options": "Email"
|
||||
},
|
||||
{
|
||||
"fieldname": "mobile_no",
|
||||
"fieldtype": "Data",
|
||||
"label": "Primary Mobile No",
|
||||
"options": "Phone",
|
||||
"read_only": 1
|
||||
"options": "Phone"
|
||||
},
|
||||
{
|
||||
"default": "Qualification",
|
||||
@ -251,8 +249,7 @@
|
||||
"fieldname": "phone",
|
||||
"fieldtype": "Data",
|
||||
"label": "Primary Phone",
|
||||
"options": "Phone",
|
||||
"read_only": 1
|
||||
"options": "Phone"
|
||||
},
|
||||
{
|
||||
"fieldname": "log_tab",
|
||||
@ -435,7 +432,7 @@
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-07-13 11:54:20.608489",
|
||||
"modified": "2025-08-26 12:12:56.324245",
|
||||
"modified_by": "Administrator",
|
||||
"module": "FCRM",
|
||||
"name": "CRM Deal",
|
||||
|
||||
@ -25,7 +25,7 @@ class CRMDeal(Document):
|
||||
add_status_change_log(self)
|
||||
if frappe.db.get_value("CRM Deal Status", self.status, "type") == "Won":
|
||||
self.closed_date = frappe.utils.nowdate()
|
||||
self.validate_forcasting_fields()
|
||||
self.validate_forecasting_fields()
|
||||
self.validate_lost_reason()
|
||||
self.update_exchange_rate()
|
||||
|
||||
@ -151,9 +151,21 @@ class CRMDeal(Document):
|
||||
if not self.probability or self.probability == 0:
|
||||
self.probability = frappe.db.get_value("CRM Deal Status", self.status, "probability") or 0
|
||||
|
||||
def validate_forcasting_fields(self):
|
||||
def update_expected_deal_value(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_default_probability()
|
||||
self.update_expected_deal_value()
|
||||
if frappe.db.get_single_value("FCRM Settings", "enable_forecasting"):
|
||||
if not self.expected_deal_value or self.expected_deal_value == 0:
|
||||
frappe.throw(_("Expected Deal Value is required."), frappe.MandatoryError)
|
||||
|
||||
@ -63,8 +63,7 @@
|
||||
"fieldname": "twiml_sid",
|
||||
"fieldtype": "Data",
|
||||
"label": "TwiML SID",
|
||||
"permlevel": 1,
|
||||
"read_only": 1
|
||||
"permlevel": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_ssqj",
|
||||
@ -105,7 +104,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-01-15 19:35:13.406254",
|
||||
"modified": "2025-08-19 13:36:19.823197",
|
||||
"modified_by": "Administrator",
|
||||
"module": "FCRM",
|
||||
"name": "CRM Twilio Settings",
|
||||
@ -152,8 +151,9 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
"defaults_tab",
|
||||
"restore_defaults",
|
||||
"enable_forecasting",
|
||||
"auto_update_expected_deal_value",
|
||||
"currency_tab",
|
||||
"currency",
|
||||
"exchange_rate_provider_section",
|
||||
@ -105,12 +106,19 @@
|
||||
{
|
||||
"fieldname": "column_break_vqck",
|
||||
"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,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-07-29 11:26:50.420614",
|
||||
"modified": "2025-09-16 17:33:26.406549",
|
||||
"modified_by": "Administrator",
|
||||
"module": "FCRM",
|
||||
"name": "FCRM Settings",
|
||||
|
||||
@ -25,6 +25,8 @@ def after_install(force=False):
|
||||
add_standard_dropdown_items()
|
||||
add_default_scripts()
|
||||
create_default_manager_dashboard(force)
|
||||
create_assignment_rule_custom_fields()
|
||||
add_assignment_rule_property_setters()
|
||||
frappe.db.commit()
|
||||
|
||||
|
||||
@ -192,7 +194,7 @@ def add_default_fields_layout(force=False):
|
||||
},
|
||||
"CRM Deal-Data Fields": {
|
||||
"doctype": "CRM Deal",
|
||||
"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"]}]}]',
|
||||
"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}]}]',
|
||||
},
|
||||
}
|
||||
|
||||
@ -421,3 +423,80 @@ def add_default_scripts():
|
||||
for doctype in ["CRM Lead", "CRM Deal"]:
|
||||
create_product_details_script(doctype)
|
||||
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")
|
||||
|
||||
2192
crm/locale/ar.po
2192
crm/locale/ar.po
File diff suppressed because it is too large
Load Diff
2198
crm/locale/bs.po
2198
crm/locale/bs.po
File diff suppressed because it is too large
Load Diff
2192
crm/locale/cs.po
2192
crm/locale/cs.po
File diff suppressed because it is too large
Load Diff
6392
crm/locale/da.po
Normal file
6392
crm/locale/da.po
Normal file
File diff suppressed because it is too large
Load Diff
2260
crm/locale/de.po
2260
crm/locale/de.po
File diff suppressed because it is too large
Load Diff
2198
crm/locale/eo.po
2198
crm/locale/eo.po
File diff suppressed because it is too large
Load Diff
2198
crm/locale/es.po
2198
crm/locale/es.po
File diff suppressed because it is too large
Load Diff
2262
crm/locale/fa.po
2262
crm/locale/fa.po
File diff suppressed because it is too large
Load Diff
2196
crm/locale/fr.po
2196
crm/locale/fr.po
File diff suppressed because it is too large
Load Diff
2198
crm/locale/hr.po
2198
crm/locale/hr.po
File diff suppressed because it is too large
Load Diff
2208
crm/locale/hu.po
2208
crm/locale/hu.po
File diff suppressed because it is too large
Load Diff
2240
crm/locale/id.po
2240
crm/locale/id.po
File diff suppressed because it is too large
Load Diff
2582
crm/locale/it.po
2582
crm/locale/it.po
File diff suppressed because it is too large
Load Diff
2192
crm/locale/main.pot
2192
crm/locale/main.pot
File diff suppressed because it is too large
Load Diff
6392
crm/locale/nb.po
Normal file
6392
crm/locale/nb.po
Normal file
File diff suppressed because it is too large
Load Diff
2192
crm/locale/nl.po
2192
crm/locale/nl.po
File diff suppressed because it is too large
Load Diff
2194
crm/locale/pl.po
2194
crm/locale/pl.po
File diff suppressed because it is too large
Load Diff
2256
crm/locale/pt.po
2256
crm/locale/pt.po
File diff suppressed because it is too large
Load Diff
2202
crm/locale/pt_BR.po
2202
crm/locale/pt_BR.po
File diff suppressed because it is too large
Load Diff
2192
crm/locale/ru.po
2192
crm/locale/ru.po
File diff suppressed because it is too large
Load Diff
2204
crm/locale/sr.po
2204
crm/locale/sr.po
File diff suppressed because it is too large
Load Diff
2216
crm/locale/sr_CS.po
2216
crm/locale/sr_CS.po
File diff suppressed because it is too large
Load Diff
2204
crm/locale/sv.po
2204
crm/locale/sv.po
File diff suppressed because it is too large
Load Diff
2234
crm/locale/th.po
2234
crm/locale/th.po
File diff suppressed because it is too large
Load Diff
2198
crm/locale/tr.po
2198
crm/locale/tr.po
File diff suppressed because it is too large
Load Diff
2192
crm/locale/vi.po
2192
crm/locale/vi.po
File diff suppressed because it is too large
Load Diff
2194
crm/locale/zh.po
2194
crm/locale/zh.po
File diff suppressed because it is too large
Load Diff
@ -14,4 +14,6 @@ crm.patches.v1_0.update_layouts_to_new_format
|
||||
crm.patches.v1_0.move_twilio_agent_to_telephony_agent
|
||||
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_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
|
||||
|
||||
9
crm/patches/v1_0/add_fields_in_assignment_rule.py
Normal file
9
crm/patches/v1_0/add_fields_in_assignment_rule.py
Normal file
@ -0,0 +1,9 @@
|
||||
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()
|
||||
5
crm/patches/v1_0/create_default_lost_reasons.py
Normal file
5
crm/patches/v1_0/create_default_lost_reasons.py
Normal file
@ -0,0 +1,5 @@
|
||||
from crm.install import add_default_lost_reasons
|
||||
|
||||
|
||||
def execute():
|
||||
add_default_lost_reasons()
|
||||
@ -1 +1 @@
|
||||
Subproject commit b295b54aaa3a2a22f455df819d87433dc6b9ff6a
|
||||
Subproject commit c9a0fc937cc897864857271b3708a0c675379015
|
||||
3
frontend/.gitignore
vendored
3
frontend/.gitignore
vendored
@ -2,4 +2,5 @@ node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
*.local
|
||||
components.d.ts
|
||||
18
frontend/components.d.ts
vendored
18
frontend/components.d.ts
vendored
@ -33,7 +33,7 @@ declare module 'vue' {
|
||||
Autocomplete: typeof import('./src/components/frappe-ui/Autocomplete.vue')['default']
|
||||
AvatarIcon: typeof import('./src/components/Icons/AvatarIcon.vue')['default']
|
||||
BrandLogo: typeof import('./src/components/BrandLogo.vue')['default']
|
||||
BrandSettings: typeof import('./src/components/Settings/General/BrandSettings.vue')['default']
|
||||
BrandSettings: typeof import('./src/components/Settings/BrandSettings.vue')['default']
|
||||
BulkDeleteLinkedDocModal: typeof import('./src/components/BulkDeleteLinkedDocModal.vue')['default']
|
||||
CalendarIcon: typeof import('./src/components/Icons/CalendarIcon.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']
|
||||
CreateDocumentModal: typeof import('./src/components/Modals/CreateDocumentModal.vue')['default']
|
||||
CRMLogo: typeof import('./src/components/Icons/CRMLogo.vue')['default']
|
||||
CurrencySettings: typeof import('./src/components/Settings/General/CurrencySettings.vue')['default']
|
||||
CurrencySettings: typeof import('./src/components/Settings/CurrencySettings.vue')['default']
|
||||
CustomActions: typeof import('./src/components/CustomActions.vue')['default']
|
||||
DashboardGrid: typeof import('./src/components/Dashboard/DashboardGrid.vue')['default']
|
||||
DashboardIcon: typeof import('./src/components/Icons/DashboardIcon.vue')['default']
|
||||
@ -85,7 +85,6 @@ declare module 'vue' {
|
||||
DragIcon: typeof import('./src/components/Icons/DragIcon.vue')['default']
|
||||
DragVerticalIcon: typeof import('./src/components/Icons/DragVerticalIcon.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']
|
||||
DurationIcon: typeof import('./src/components/Icons/DurationIcon.vue')['default']
|
||||
EditEmailTemplate: typeof import('./src/components/Settings/EmailTemplate/EditEmailTemplate.vue')['default']
|
||||
@ -128,11 +127,10 @@ declare module 'vue' {
|
||||
FileVideoIcon: typeof import('./src/components/Icons/FileVideoIcon.vue')['default']
|
||||
Filter: typeof import('./src/components/Filter.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']
|
||||
FrappeCloudIcon: typeof import('./src/components/Icons/FrappeCloudIcon.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']
|
||||
GoogleIcon: typeof import('./src/components/Icons/GoogleIcon.vue')['default']
|
||||
Grid: typeof import('./src/components/Controls/Grid.vue')['default']
|
||||
@ -143,7 +141,7 @@ declare module 'vue' {
|
||||
GroupByIcon: typeof import('./src/components/Icons/GroupByIcon.vue')['default']
|
||||
HeartIcon: typeof import('./src/components/Icons/HeartIcon.vue')['default']
|
||||
HelpIcon: typeof import('./src/components/Icons/HelpIcon.vue')['default']
|
||||
HomeActions: typeof import('./src/components/Settings/General/HomeActions.vue')['default']
|
||||
HomeActions: typeof import('./src/components/Settings/HomeActions.vue')['default']
|
||||
Icon: typeof import('./src/components/Icon.vue')['default']
|
||||
IconPicker: typeof import('./src/components/IconPicker.vue')['default']
|
||||
ImageUploader: typeof import('./src/components/Controls/ImageUploader.vue')['default']
|
||||
@ -170,10 +168,6 @@ declare module 'vue' {
|
||||
LoadingIndicator: typeof import('./src/components/Icons/LoadingIndicator.vue')['default']
|
||||
LostReasonModal: typeof import('./src/components/Modals/LostReasonModal.vue')['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']
|
||||
MaximizeIcon: typeof import('./src/components/Icons/MaximizeIcon.vue')['default']
|
||||
MenuIcon: typeof import('./src/components/Icons/MenuIcon.vue')['default']
|
||||
@ -188,7 +182,6 @@ declare module 'vue' {
|
||||
MultiSelectEmailInput: typeof import('./src/components/Controls/MultiSelectEmailInput.vue')['default']
|
||||
MultiSelectUserInput: typeof import('./src/components/Controls/MultiSelectUserInput.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']
|
||||
NoteArea: typeof import('./src/components/Activities/NoteArea.vue')['default']
|
||||
NoteIcon: typeof import('./src/components/Icons/NoteIcon.vue')['default']
|
||||
@ -207,6 +200,8 @@ declare module 'vue' {
|
||||
PlaybackSpeedOption: typeof import('./src/components/Activities/PlaybackSpeedOption.vue')['default']
|
||||
PlayIcon: typeof import('./src/components/Icons/PlayIcon.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']
|
||||
QuickEntryModal: typeof import('./src/components/Modals/QuickEntryModal.vue')['default']
|
||||
QuickFilterField: typeof import('./src/components/QuickFilterField.vue')['default']
|
||||
@ -233,6 +228,7 @@ declare module 'vue' {
|
||||
SmileIcon: typeof import('./src/components/Icons/SmileIcon.vue')['default']
|
||||
SortBy: typeof import('./src/components/SortBy.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']
|
||||
StepsIcon: typeof import('./src/components/Icons/StepsIcon.vue')['default']
|
||||
SuccessIcon: typeof import('./src/components/Icons/SuccessIcon.vue')['default']
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
"@tiptap/extension-paragraph": "^2.12.0",
|
||||
"@twilio/voice-sdk": "^2.10.2",
|
||||
"@vueuse/integrations": "^10.3.0",
|
||||
"frappe-ui": "^0.1.171",
|
||||
"frappe-ui": "^0.1.201",
|
||||
"gemoji": "^8.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mime": "^4.0.1",
|
||||
|
||||
@ -238,12 +238,9 @@
|
||||
<Button
|
||||
class="!size-4"
|
||||
variant="ghost"
|
||||
:icon="SelectIcon"
|
||||
@click="activity.show_others = !activity.show_others"
|
||||
>
|
||||
<template #icon>
|
||||
<SelectIcon />
|
||||
</template>
|
||||
</Button>
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
|
||||
@ -9,23 +9,17 @@
|
||||
<Button
|
||||
v-if="title == 'Emails'"
|
||||
variant="solid"
|
||||
:label="__('New Email')"
|
||||
iconLeft="plus"
|
||||
@click="emailBox.show = true"
|
||||
>
|
||||
<template #prefix>
|
||||
<FeatherIcon name="plus" class="h-4 w-4" />
|
||||
</template>
|
||||
<span>{{ __('New Email') }}</span>
|
||||
</Button>
|
||||
/>
|
||||
<Button
|
||||
v-else-if="title == 'Comments'"
|
||||
variant="solid"
|
||||
:label="__('New Comment')"
|
||||
iconLeft="plus"
|
||||
@click="emailBox.showComment = true"
|
||||
>
|
||||
<template #prefix>
|
||||
<FeatherIcon name="plus" class="h-4 w-4" />
|
||||
</template>
|
||||
<span>{{ __('New Comment') }}</span>
|
||||
</Button>
|
||||
/>
|
||||
<MultiActionButton
|
||||
v-else-if="title == 'Calls'"
|
||||
variant="solid"
|
||||
@ -34,59 +28,45 @@
|
||||
<Button
|
||||
v-else-if="title == 'Notes'"
|
||||
variant="solid"
|
||||
:label="__('New Note')"
|
||||
iconLeft="plus"
|
||||
@click="modalRef.showNote()"
|
||||
>
|
||||
<template #prefix>
|
||||
<FeatherIcon name="plus" class="h-4 w-4" />
|
||||
</template>
|
||||
<span>{{ __('New Note') }}</span>
|
||||
</Button>
|
||||
/>
|
||||
<Button
|
||||
v-else-if="title == 'Tasks'"
|
||||
variant="solid"
|
||||
:label="__('New Task')"
|
||||
iconLeft="plus"
|
||||
@click="modalRef.showTask()"
|
||||
>
|
||||
<template #prefix>
|
||||
<FeatherIcon name="plus" class="h-4 w-4" />
|
||||
</template>
|
||||
<span>{{ __('New Task') }}</span>
|
||||
</Button>
|
||||
/>
|
||||
<Button
|
||||
v-else-if="title == 'Attachments'"
|
||||
variant="solid"
|
||||
:label="__('Upload Attachment')"
|
||||
iconLeft="plus"
|
||||
@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'">
|
||||
<Button
|
||||
:label="__('Send Template')"
|
||||
@click="showWhatsappTemplates = true"
|
||||
/>
|
||||
<Button variant="solid" @click="whatsappBox.show()">
|
||||
<template #prefix>
|
||||
<FeatherIcon name="plus" class="h-4 w-4" />
|
||||
</template>
|
||||
<span>{{ __('New Message') }}</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
:label="__('New Message')"
|
||||
iconLeft="plus"
|
||||
@click="whatsappBox.show()"
|
||||
/>
|
||||
</div>
|
||||
<Dropdown v-else :options="defaultActions" @click.stop>
|
||||
<template v-slot="{ open }">
|
||||
<Button variant="solid" class="flex items-center gap-1">
|
||||
<template #prefix>
|
||||
<FeatherIcon name="plus" class="h-4 w-4" />
|
||||
</template>
|
||||
<span>{{ __('New') }}</span>
|
||||
<template #suffix>
|
||||
<FeatherIcon
|
||||
:name="open ? 'chevron-up' : 'chevron-down'"
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
</template>
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
class="flex items-center gap-1"
|
||||
:label="__('New')"
|
||||
iconLeft="plus"
|
||||
:iconRight="open ? 'chevron-up' : 'chevron-down'"
|
||||
/>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
@ -38,42 +38,31 @@
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div class="flex gap-1">
|
||||
<Tooltip
|
||||
:text="
|
||||
<Button
|
||||
:tooltip="
|
||||
attachment.is_private ? __('Make public') : __('Make private')
|
||||
"
|
||||
class="!size-5"
|
||||
@click.stop="
|
||||
togglePrivate(attachment.name, attachment.is_private)
|
||||
"
|
||||
>
|
||||
<div>
|
||||
<Button
|
||||
class="!size-5"
|
||||
@click.stop="
|
||||
togglePrivate(attachment.name, attachment.is_private)
|
||||
"
|
||||
>
|
||||
<template #icon>
|
||||
<FeatherIcon
|
||||
:name="attachment.is_private ? 'lock' : 'unlock'"
|
||||
class="size-3 text-ink-gray-7"
|
||||
/>
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip :text="__('Delete attachment')">
|
||||
<div>
|
||||
<Button
|
||||
class="!size-5"
|
||||
@click.stop="() => deleteAttachment(attachment.name)"
|
||||
>
|
||||
<template #icon>
|
||||
<FeatherIcon
|
||||
name="trash-2"
|
||||
class="size-3 text-ink-gray-7"
|
||||
/>
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<template #icon>
|
||||
<FeatherIcon
|
||||
:name="attachment.is_private ? 'lock' : 'unlock'"
|
||||
class="size-3 text-ink-gray-7"
|
||||
/>
|
||||
</template>
|
||||
</Button>
|
||||
<Button
|
||||
:tooltip="__('Delete attachment')"
|
||||
class="!size-5"
|
||||
@click.stop="() => deleteAttachment(attachment.name)"
|
||||
>
|
||||
<template #icon>
|
||||
<FeatherIcon name="trash-2" class="size-3 text-ink-gray-7" />
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div class="w-full text-sm text-ink-gray-5">
|
||||
<div class="flex items-center gap-2">
|
||||
<Button variant="ghost" @click="playPause">
|
||||
<template #icon>
|
||||
<PlayIcon v-if="isPaused" class="size-4 text-ink-gray-5" />
|
||||
<PauseIcon v-else class="size-4 text-ink-gray-5" />
|
||||
</template>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="text-ink-gray-5"
|
||||
:icon="isPaused ? PlayIcon : PauseIcon"
|
||||
@click="playPause"
|
||||
/>
|
||||
<div class="flex gap-2 items-center justify-between flex-1">
|
||||
<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]"
|
||||
@ -61,11 +61,11 @@
|
||||
</Button>
|
||||
</div>
|
||||
<Dropdown :options="options">
|
||||
<Button variant="ghost" @click="showPlaybackSpeed = false">
|
||||
<template #icon>
|
||||
<FeatherIcon class="size-4" name="more-horizontal" />
|
||||
</template>
|
||||
</Button>
|
||||
<Button
|
||||
icon="more-horizontal"
|
||||
variant="ghost"
|
||||
@click="showPlaybackSpeed = false"
|
||||
/>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -14,12 +14,10 @@
|
||||
<div class="flex gap-1">
|
||||
<Button
|
||||
v-if="isManager() && !isMobileView"
|
||||
:tooltip="__('Edit fields layout')"
|
||||
:icon="EditIcon"
|
||||
@click="showDataFieldsModal = true"
|
||||
>
|
||||
<template #icon>
|
||||
<EditIcon />
|
||||
</template>
|
||||
</Button>
|
||||
/>
|
||||
<Button
|
||||
label="Save"
|
||||
:disabled="!document.isDirty"
|
||||
|
||||
@ -2,7 +2,9 @@
|
||||
<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"
|
||||
>
|
||||
<div class="-mb-0.5 flex items-center justify-between gap-2 truncate text-ink-gray-9">
|
||||
<div
|
||||
class="-mb-0.5 flex items-center justify-between gap-2 truncate text-ink-gray-9"
|
||||
>
|
||||
<div class="flex items-center gap-2 truncate">
|
||||
<span>{{ activity.data.sender_full_name }}</span>
|
||||
<span class="sm:flex hidden text-sm text-ink-gray-5">
|
||||
@ -28,32 +30,20 @@
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div class="flex gap-0.5">
|
||||
<Tooltip :text="__('Reply')">
|
||||
<div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="text-ink-gray-7"
|
||||
@click="reply(activity.data)"
|
||||
>
|
||||
<template #icon>
|
||||
<ReplyIcon />
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip :text="__('Reply All')">
|
||||
<div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="text-ink-gray-7"
|
||||
@click="reply(activity.data, true)"
|
||||
>
|
||||
<template #icon>
|
||||
<ReplyAllIcon />
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Button
|
||||
:tooltip="__('Reply')"
|
||||
variant="ghost"
|
||||
class="text-ink-gray-7"
|
||||
:icon="ReplyIcon"
|
||||
@click="reply(activity.data)"
|
||||
/>
|
||||
<Button
|
||||
:tooltip="__('Reply All')"
|
||||
variant="ghost"
|
||||
:icon="ReplyAllIcon"
|
||||
class="text-ink-gray-7"
|
||||
@click="reply(activity.data, true)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -41,13 +41,13 @@
|
||||
:options="taskStatusOptions(modalRef.updateTaskStatus, task)"
|
||||
@click.stop
|
||||
>
|
||||
<Tooltip :text="__('Change Status')">
|
||||
<div>
|
||||
<Button variant="ghosted" class="hover:bg-surface-gray-4">
|
||||
<TaskStatusIcon :status="task.status" />
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Button
|
||||
:tooltip="__('Change status')"
|
||||
variant="ghosted"
|
||||
class="hover:bg-surface-gray-4"
|
||||
>
|
||||
<TaskStatusIcon :status="task.status" />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
<Dropdown
|
||||
:options="[
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<NestedPopover>
|
||||
<template #target>
|
||||
<div class="flex items-center">
|
||||
<Popover placement="bottom-end">
|
||||
<template #target="{ togglePopover }">
|
||||
<div class="flex items-center" @click="togglePopover">
|
||||
<component
|
||||
v-if="assignees?.length"
|
||||
:is="assignees?.length == 1 ? 'Button' : 'div'"
|
||||
@ -11,24 +11,23 @@
|
||||
<Button v-else :label="__('Assign to')" />
|
||||
</div>
|
||||
</template>
|
||||
<template #body="{ open }">
|
||||
<template #body="{ isOpen }">
|
||||
<AssignToBody
|
||||
v-show="open"
|
||||
v-show="isOpen"
|
||||
v-model="assignees"
|
||||
:docname="docname"
|
||||
:doctype="doctype"
|
||||
:open="open"
|
||||
:open="isOpen"
|
||||
:onUpdate="ownerField && saveAssignees"
|
||||
/>
|
||||
</template>
|
||||
</NestedPopover>
|
||||
</Popover>
|
||||
</template>
|
||||
<script setup>
|
||||
import NestedPopover from '@/components/NestedPopover.vue'
|
||||
import MultipleAvatar from '@/components/MultipleAvatar.vue'
|
||||
import AssignToBody from '@/components/AssignToBody.vue'
|
||||
import { useDocument } from '@/data/document'
|
||||
import { toast } from 'frappe-ui'
|
||||
import { toast, Popover } from 'frappe-ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
|
||||
@ -25,23 +25,21 @@
|
||||
:key="assignee.name"
|
||||
@click.stop
|
||||
>
|
||||
<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"
|
||||
@click.stop
|
||||
<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"
|
||||
@click.stop
|
||||
>
|
||||
<UserAvatar :user="assignee.name" size="sm" />
|
||||
<div class="ml-1">{{ getUser(assignee.name).full_name }}</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="rounded-full !size-4 m-1"
|
||||
@click.stop="removeValue(assignee.name)"
|
||||
>
|
||||
<UserAvatar :user="assignee.name" size="sm" />
|
||||
<div class="ml-1">{{ getUser(assignee.name).full_name }}</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="rounded-full !size-4 m-1"
|
||||
@click.stop="removeValue(assignee.name)"
|
||||
>
|
||||
<template #icon>
|
||||
<FeatherIcon name="x" class="h-3 w-3 text-ink-gray-6" />
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
<template #icon>
|
||||
<FeatherIcon name="x" class="h-3 w-3 text-ink-gray-6" />
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@ -74,7 +72,7 @@ import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { capture } from '@/telemetry'
|
||||
import { Tooltip, Switch, toast, createResource } from 'frappe-ui'
|
||||
import { Tooltip, Switch, createResource } from 'frappe-ui'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
@ -154,6 +152,7 @@ watch(
|
||||
updateAssignees()
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
async function updateAssignees() {
|
||||
|
||||
@ -5,11 +5,9 @@
|
||||
:label="label"
|
||||
theme="gray"
|
||||
variant="outline"
|
||||
:iconLeft="getIcon()"
|
||||
@click="toggleDialog()"
|
||||
>
|
||||
<template #prefix>
|
||||
<component :is="getIcon()" class="h-4 w-4" />
|
||||
</template>
|
||||
<template #suffix>
|
||||
<slot name="suffix" />
|
||||
</template>
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-ink-gray-5">
|
||||
<div class="text-ink-gray-5 text-base">
|
||||
{{
|
||||
__('Are you sure you want to delete {0} items?', [
|
||||
props.items?.length,
|
||||
@ -53,7 +53,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-ink-gray-5">
|
||||
<div class="text-ink-gray-5 text-base">
|
||||
{{
|
||||
confirmDeleteInfo.delete
|
||||
? __(
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<NestedPopover>
|
||||
<template #target>
|
||||
<Button :label="__('Columns')">
|
||||
<Popover placement="bottom-end">
|
||||
<template #target="{ togglePopover }">
|
||||
<Button :label="__('Columns')" @click="togglePopover">
|
||||
<template v-if="hideLabel">
|
||||
<ColumnsIcon class="h-4" />
|
||||
</template>
|
||||
@ -65,37 +65,28 @@
|
||||
<Button
|
||||
class="w-full !justify-start !text-ink-gray-5"
|
||||
variant="ghost"
|
||||
@click="togglePopover()"
|
||||
:label="__('Add Column')"
|
||||
>
|
||||
<template #prefix>
|
||||
<FeatherIcon name="plus" class="h-4" />
|
||||
</template>
|
||||
</Button>
|
||||
iconLeft="plus"
|
||||
@click="togglePopover"
|
||||
/>
|
||||
</template>
|
||||
</Autocomplete>
|
||||
<Button
|
||||
v-if="columnsUpdated"
|
||||
class="w-full !justify-start !text-ink-gray-5"
|
||||
variant="ghost"
|
||||
@click="reset(close)"
|
||||
:label="__('Reset Changes')"
|
||||
>
|
||||
<template #prefix>
|
||||
<ReloadIcon class="h-4" />
|
||||
</template>
|
||||
</Button>
|
||||
:iconLeft="ReloadIcon"
|
||||
@click="reset(close)"
|
||||
/>
|
||||
<Button
|
||||
v-if="!is_default"
|
||||
class="w-full !justify-start !text-ink-gray-5"
|
||||
variant="ghost"
|
||||
@click="resetToDefault(close)"
|
||||
:label="__('Reset to Default')"
|
||||
>
|
||||
<template #prefix>
|
||||
<ReloadIcon class="h-4" />
|
||||
</template>
|
||||
</Button>
|
||||
:iconLeft="ReloadIcon"
|
||||
@click="resetToDefault(close)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
@ -144,7 +135,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</NestedPopover>
|
||||
</Popover>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@ -152,9 +143,9 @@ import ColumnsIcon from '@/components/Icons/ColumnsIcon.vue'
|
||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||
import DragIcon from '@/components/Icons/DragIcon.vue'
|
||||
import ReloadIcon from '@/components/Icons/ReloadIcon.vue'
|
||||
import NestedPopover from '@/components/NestedPopover.vue'
|
||||
import Autocomplete from '@/components/frappe-ui/Autocomplete.vue'
|
||||
import { isTouchScreenDevice } from '@/utils'
|
||||
import { Popover } from 'frappe-ui'
|
||||
import Draggable from 'vuedraggable'
|
||||
import { computed, ref } from 'vue'
|
||||
import { watchOnce } from '@vueuse/core'
|
||||
@ -219,6 +210,7 @@ const fields = computed(() => {
|
||||
})
|
||||
|
||||
function addColumn(c) {
|
||||
if (!c) return
|
||||
let align = ['Float', 'Int', 'Percent', 'Currency'].includes(c.type)
|
||||
? 'right'
|
||||
: 'left'
|
||||
|
||||
@ -45,11 +45,12 @@
|
||||
v-slot="{ togglePopover }"
|
||||
@update:modelValue="() => appendEmoji()"
|
||||
>
|
||||
<Button variant="ghost" @click="togglePopover()">
|
||||
<template #icon>
|
||||
<SmileIcon class="h-4" />
|
||||
</template>
|
||||
</Button>
|
||||
<Button
|
||||
:tooltip="__('Insert Emoji')"
|
||||
:icon="SmileIcon"
|
||||
variant="ghost"
|
||||
@click="togglePopover()"
|
||||
/>
|
||||
</IconPicker>
|
||||
<FileUploader
|
||||
:upload-args="{
|
||||
@ -61,14 +62,11 @@
|
||||
>
|
||||
<template #default="{ openFileSelector }">
|
||||
<Button
|
||||
theme="gray"
|
||||
:tooltip="__('Attach a file')"
|
||||
variant="ghost"
|
||||
:icon="AttachmentIcon"
|
||||
@click="openFileSelector()"
|
||||
>
|
||||
<template #icon>
|
||||
<AttachmentIcon class="h-4" />
|
||||
</template>
|
||||
</Button>
|
||||
/>
|
||||
</template>
|
||||
</FileUploader>
|
||||
</div>
|
||||
|
||||
@ -8,24 +8,18 @@
|
||||
showEmailBox ? '!bg-surface-gray-4 hover:!bg-surface-gray-3' : '',
|
||||
]"
|
||||
:label="__('Reply')"
|
||||
:iconLeft="Email2Icon"
|
||||
@click="toggleEmailBox()"
|
||||
>
|
||||
<template #prefix>
|
||||
<Email2Icon class="h-4" />
|
||||
</template>
|
||||
</Button>
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
:label="__('Comment')"
|
||||
:class="[
|
||||
showCommentBox ? '!bg-surface-gray-4 hover:!bg-surface-gray-3' : '',
|
||||
]"
|
||||
:iconLeft="CommentIcon"
|
||||
@click="toggleCommentBox()"
|
||||
>
|
||||
<template #prefix>
|
||||
<CommentIcon class="h-4" />
|
||||
</template>
|
||||
</Button>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
||||
454
frontend/src/components/ConditionsFilter/CFCondition.vue
Normal file
454
frontend/src/components/ConditionsFilter/CFCondition.vue
Normal file
@ -0,0 +1,454 @@
|
||||
<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>
|
||||
142
frontend/src/components/ConditionsFilter/CFConditions.vue
Normal file
142
frontend/src/components/ConditionsFilter/CFConditions.vue
Normal file
@ -0,0 +1,142 @@
|
||||
<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>
|
||||
17
frontend/src/components/ConditionsFilter/filterableFields.ts
Normal file
17
frontend/src/components/ConditionsFilter/filterableFields.ts
Normal file
@ -0,0 +1,17 @@
|
||||
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
|
||||
},
|
||||
})
|
||||
@ -52,16 +52,14 @@
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-12">
|
||||
<div class="flex items-center justify-center w-12">
|
||||
<Button
|
||||
class="flex w-full items-center justify-center rounded !bg-surface-gray-2 border-0"
|
||||
:tooltip="__('Edit grid fields')"
|
||||
class="rounded !bg-surface-gray-2 border-0 !text-ink-gray-5"
|
||||
variant="outline"
|
||||
icon="settings"
|
||||
@click="showGridFieldsEditorModal = true"
|
||||
>
|
||||
<template #icon>
|
||||
<FeatherIcon name="settings" class="size-4 text-ink-gray-7" />
|
||||
</template>
|
||||
</Button>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Rows -->
|
||||
@ -72,6 +70,7 @@
|
||||
:delay="isTouchScreenDevice() ? 200 : 0"
|
||||
group="rows"
|
||||
item-key="name"
|
||||
@end="reorder"
|
||||
>
|
||||
<template #item="{ element: row, index }">
|
||||
<div
|
||||
@ -277,16 +276,14 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="edit-row w-12">
|
||||
<div class="edit-row flex items-center justify-center w-12">
|
||||
<Button
|
||||
class="flex w-full items-center justify-center rounded border-0"
|
||||
:tooltip="__('Edit row')"
|
||||
class="rounded border-0 !text-ink-gray-7"
|
||||
variant="outline"
|
||||
:icon="EditIcon"
|
||||
@click="showRowList[index] = true"
|
||||
>
|
||||
<template #icon>
|
||||
<EditIcon class="text-ink-gray-7" />
|
||||
</template>
|
||||
</Button>
|
||||
/>
|
||||
</div>
|
||||
<GridRowModal
|
||||
v-if="showRowList[index]"
|
||||
@ -350,7 +347,6 @@ import { usersStore } from '@/stores/users'
|
||||
import { getMeta } from '@/stores/meta'
|
||||
import { createDocument } from '@/composables/document'
|
||||
import {
|
||||
FeatherIcon,
|
||||
FormControl,
|
||||
Checkbox,
|
||||
DateTimePicker,
|
||||
@ -520,6 +516,13 @@ const deleteRows = () => {
|
||||
selectedRows.clear()
|
||||
}
|
||||
|
||||
const reorder = () => {
|
||||
rows.value.forEach((row, index) => {
|
||||
row.idx = index + 1
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function fieldChange(value, field, row) {
|
||||
triggerOnChange(field.fieldname, value, row)
|
||||
}
|
||||
|
||||
@ -54,13 +54,10 @@
|
||||
<template #target="{ togglePopover }">
|
||||
<Button
|
||||
class="w-full mt-2"
|
||||
@click="togglePopover()"
|
||||
:label="__('Add Field')"
|
||||
>
|
||||
<template #prefix>
|
||||
<FeatherIcon name="plus" class="h-4" />
|
||||
</template>
|
||||
</Button>
|
||||
iconLeft="plus"
|
||||
@click="togglePopover()"
|
||||
/>
|
||||
</template>
|
||||
<template #item-label="{ option }">
|
||||
<div class="flex flex-col gap-1 text-ink-gray-9">
|
||||
@ -75,7 +72,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<template #actions>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center gap-2 justify-end">
|
||||
<Button
|
||||
v-if="dirty"
|
||||
class="w-full"
|
||||
|
||||
@ -11,19 +11,18 @@
|
||||
<div class="flex items-center gap-1">
|
||||
<Button
|
||||
v-if="isManager()"
|
||||
:tooltip="__('Edit fields layout')"
|
||||
variant="ghost"
|
||||
class="w-7"
|
||||
:icon="EditIcon"
|
||||
@click="openGridRowFieldsModal"
|
||||
>
|
||||
<template #icon>
|
||||
<EditIcon />
|
||||
</template>
|
||||
</Button>
|
||||
<Button variant="ghost" class="w-7" @click="show = false">
|
||||
<template #icon>
|
||||
<FeatherIcon name="x" class="size-4" />
|
||||
</template>
|
||||
</Button>
|
||||
/>
|
||||
<Button
|
||||
icon="x"
|
||||
variant="ghost"
|
||||
class="w-7"
|
||||
@click="show = false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
<template>
|
||||
<FileUploader
|
||||
:file-types="image_type"
|
||||
class="text-base"
|
||||
@success="
|
||||
(file) => {
|
||||
$emit('upload', file.file_url)
|
||||
@ -10,21 +9,28 @@
|
||||
>
|
||||
<template v-slot="{ progress, uploading, openFileSelector }">
|
||||
<div class="flex items-end space-x-1">
|
||||
<Button @click="openFileSelector">
|
||||
{{
|
||||
<Button
|
||||
@click="openFileSelector"
|
||||
:iconLeft="uploading ? 'cloud-upload' : ImageUpIcon"
|
||||
:label="
|
||||
uploading
|
||||
? `Uploading ${progress}%`
|
||||
? __('Uploading {0}%', [progress])
|
||||
: image_url
|
||||
? 'Change'
|
||||
: 'Upload'
|
||||
}}
|
||||
</Button>
|
||||
<Button v-if="image_url" @click="$emit('remove')">Remove</Button>
|
||||
? __('Change')
|
||||
: __('Upload')
|
||||
"
|
||||
/>
|
||||
<Button
|
||||
v-if="image_url"
|
||||
:label="__('Remove')"
|
||||
@click="$emit('remove')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</FileUploader>
|
||||
</template>
|
||||
<script setup>
|
||||
import ImageUpIcon from '~icons/lucide/image-up'
|
||||
import { FileUploader, Button } from 'frappe-ui'
|
||||
|
||||
const prop = defineProps({
|
||||
@ -33,10 +39,6 @@ const prop = defineProps({
|
||||
type: String,
|
||||
default: 'image/*',
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
const emit = defineEmits(['upload', 'remove'])
|
||||
</script>
|
||||
|
||||
@ -48,24 +48,18 @@
|
||||
variant="ghost"
|
||||
class="w-full !justify-start"
|
||||
:label="__('Create New')"
|
||||
iconLeft="plus"
|
||||
@click="() => attrs.onCreate(value, close)"
|
||||
>
|
||||
<template #prefix>
|
||||
<FeatherIcon name="plus" class="h-4" />
|
||||
</template>
|
||||
</Button>
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="w-full !justify-start"
|
||||
:label="__('Clear')"
|
||||
iconLeft="x"
|
||||
@click="() => clearValue(close)"
|
||||
>
|
||||
<template #prefix>
|
||||
<FeatherIcon name="x" class="h-4" />
|
||||
</template>
|
||||
</Button>
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Autocomplete>
|
||||
|
||||
@ -18,14 +18,10 @@
|
||||
:key="g.label"
|
||||
>
|
||||
<Dropdown :options="g.action" v-slot="{ open }">
|
||||
<Button :label="g.label">
|
||||
<template #suffix>
|
||||
<FeatherIcon
|
||||
:name="open ? 'chevron-up' : 'chevron-down'"
|
||||
class="h-4"
|
||||
/>
|
||||
</template>
|
||||
</Button>
|
||||
<Button
|
||||
:label="g.label"
|
||||
:iconRight="open ? 'chevron-up' : 'chevron-down'"
|
||||
/>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
<Dialog v-model="show" :options="{ size: 'xl' }">
|
||||
<template #body v-if="!confirmDeleteInfo.show">
|
||||
<div class="bg-surface-modal px-4 pb-6 pt-5 sm:px-6">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="text-2xl leading-6 text-ink-gray-9 font-semibold">
|
||||
{{
|
||||
@ -32,11 +32,12 @@
|
||||
{
|
||||
label: 'Document',
|
||||
key: 'title',
|
||||
width: '19rem',
|
||||
},
|
||||
{
|
||||
label: 'Master',
|
||||
key: 'reference_doctype',
|
||||
width: '30%',
|
||||
width: '12rem',
|
||||
},
|
||||
]"
|
||||
@selectionsChanged="
|
||||
|
||||
@ -123,11 +123,12 @@
|
||||
v-slot="{ togglePopover }"
|
||||
@update:modelValue="() => appendEmoji()"
|
||||
>
|
||||
<Button variant="ghost" @click="togglePopover()">
|
||||
<template #icon>
|
||||
<SmileIcon class="h-4" />
|
||||
</template>
|
||||
</Button>
|
||||
<Button
|
||||
:tooltip="__('Insert Emoji')"
|
||||
:icon="SmileIcon"
|
||||
variant="ghost"
|
||||
@click="togglePopover()"
|
||||
/>
|
||||
</IconPicker>
|
||||
<FileUploader
|
||||
:upload-args="{
|
||||
@ -138,21 +139,20 @@
|
||||
@success="(f) => attachments.push(f)"
|
||||
>
|
||||
<template #default="{ openFileSelector }">
|
||||
<Button variant="ghost" @click="openFileSelector()">
|
||||
<template #icon>
|
||||
<AttachmentIcon class="h-4" />
|
||||
</template>
|
||||
</Button>
|
||||
<Button
|
||||
:tooltip="__('Attach a file')"
|
||||
:icon="AttachmentIcon"
|
||||
variant="ghost"
|
||||
@click="openFileSelector()"
|
||||
/>
|
||||
</template>
|
||||
</FileUploader>
|
||||
<Button
|
||||
:tooltip="__('Insert Email Template')"
|
||||
variant="ghost"
|
||||
:icon="EmailTemplateIcon"
|
||||
@click="showEmailTemplateSelectorModal = true"
|
||||
>
|
||||
<template #icon>
|
||||
<EmailTemplateIcon class="h-4" />
|
||||
</template>
|
||||
</Button>
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-2 flex items-center justify-end space-x-2 sm:mt-0">
|
||||
<Button v-bind="discardButtonProps || {}" :label="__('Discard')" />
|
||||
|
||||
@ -89,12 +89,9 @@
|
||||
v-if="data[field.fieldname] && field.edit"
|
||||
class="shrink-0"
|
||||
:label="__('Edit')"
|
||||
:iconLeft="EditIcon"
|
||||
@click="field.edit(data[field.fieldname])"
|
||||
>
|
||||
<template #prefix>
|
||||
<EditIcon class="h-4 w-4" />
|
||||
</template>
|
||||
</Button>
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TableMultiselectInput
|
||||
|
||||
@ -169,13 +169,10 @@
|
||||
<Button
|
||||
class="w-full !h-8 !bg-surface-modal"
|
||||
variant="outline"
|
||||
@click="togglePopover()"
|
||||
:label="__('Add Field')"
|
||||
>
|
||||
<template #prefix>
|
||||
<FeatherIcon name="plus" class="h-4" />
|
||||
</template>
|
||||
</Button>
|
||||
iconLeft="plus"
|
||||
@click="togglePopover()"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #item-label="{ option }">
|
||||
@ -198,6 +195,7 @@
|
||||
class="w-full h-8"
|
||||
variant="subtle"
|
||||
:label="__('Add Section')"
|
||||
iconLeft="plus"
|
||||
@click="
|
||||
tabs[tabIndex].sections.push({
|
||||
label: __('New Section'),
|
||||
@ -206,11 +204,7 @@
|
||||
columns: [{ name: 'column_' + getRandom(), fields: [] }],
|
||||
})
|
||||
"
|
||||
>
|
||||
<template #prefix>
|
||||
<FeatherIcon name="plus" class="h-4" />
|
||||
</template>
|
||||
</Button>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -29,6 +29,7 @@
|
||||
filesUploaderArea?.showWebLink || filesUploaderArea?.showCamera
|
||||
"
|
||||
:label="isMobileView ? __('Back') : __('Back to file upload')"
|
||||
iconLeft="arrow-left"
|
||||
@click="
|
||||
() => {
|
||||
filesUploaderArea.showWebLink = false
|
||||
@ -37,11 +38,7 @@
|
||||
filesUploaderArea.cameraImage = null
|
||||
}
|
||||
"
|
||||
>
|
||||
<template #prefix>
|
||||
<FeatherIcon name="arrow-left" class="size-4" />
|
||||
</template>
|
||||
</Button>
|
||||
/>
|
||||
<Button
|
||||
v-if="
|
||||
filesUploaderArea?.showCamera && !filesUploaderArea?.cameraImage
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
<template>
|
||||
<NestedPopover>
|
||||
<template #target>
|
||||
<Popover placement="bottom-end">
|
||||
<template #target="{ togglePopover, close }">
|
||||
<div class="flex items-center">
|
||||
<Button
|
||||
:label="__('Filter')"
|
||||
:class="filters?.size ? 'rounded-r-none' : ''"
|
||||
:iconLeft="FilterIcon"
|
||||
@click="togglePopover"
|
||||
>
|
||||
<template #prefix><FilterIcon class="h-4" /></template>
|
||||
<template v-if="filters?.size" #suffix>
|
||||
<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"
|
||||
@ -15,15 +16,13 @@
|
||||
</div>
|
||||
</template>
|
||||
</Button>
|
||||
<Tooltip v-if="filters?.size" :text="__('Clear all Filter')">
|
||||
<div>
|
||||
<Button
|
||||
class="rounded-l-none border-l"
|
||||
icon="x"
|
||||
@click.stop="clearfilter(false)"
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Button
|
||||
v-if="filters?.size"
|
||||
:tooltip="__('Clear all Filter')"
|
||||
class="rounded-l-none border-l"
|
||||
icon="x"
|
||||
@click.stop="clearfilter(close)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #body="{ close }">
|
||||
@ -134,13 +133,10 @@
|
||||
<Button
|
||||
class="!text-ink-gray-5"
|
||||
variant="ghost"
|
||||
@click="togglePopover()"
|
||||
:label="__('Add Filter')"
|
||||
>
|
||||
<template #prefix>
|
||||
<FeatherIcon name="plus" class="h-4" />
|
||||
</template>
|
||||
</Button>
|
||||
iconLeft="plus"
|
||||
@click="togglePopover()"
|
||||
/>
|
||||
</template>
|
||||
</Autocomplete>
|
||||
<Button
|
||||
@ -154,17 +150,16 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</NestedPopover>
|
||||
</Popover>
|
||||
</template>
|
||||
<script setup>
|
||||
import NestedPopover from '@/components/NestedPopover.vue'
|
||||
import FilterIcon from '@/components/Icons/FilterIcon.vue'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import Autocomplete from '@/components/frappe-ui/Autocomplete.vue'
|
||||
import {
|
||||
FormControl,
|
||||
createResource,
|
||||
Tooltip,
|
||||
Popover,
|
||||
DatePicker,
|
||||
DateTimePicker,
|
||||
DateRangePicker,
|
||||
@ -485,7 +480,7 @@ function removeFilter(index) {
|
||||
function clearfilter(close) {
|
||||
filters.value.clear()
|
||||
apply()
|
||||
close && close()
|
||||
close()
|
||||
}
|
||||
|
||||
function updateValue(value, filter) {
|
||||
|
||||
@ -7,18 +7,10 @@
|
||||
? groupByValue?.label
|
||||
: __('Group By: ') + groupByValue?.label
|
||||
"
|
||||
:iconLeft="DetailsIcon"
|
||||
:iconRight="isOpen ? 'chevron-up' : 'chevron-down'"
|
||||
@click="togglePopover()"
|
||||
>
|
||||
<template #prefix>
|
||||
<DetailsIcon />
|
||||
</template>
|
||||
<template #suffix>
|
||||
<FeatherIcon
|
||||
:name="isOpen ? 'chevron-up' : 'chevron-down'"
|
||||
class="h-4"
|
||||
/>
|
||||
</template>
|
||||
</Button>
|
||||
/>
|
||||
</template>
|
||||
</Autocomplete>
|
||||
</template>
|
||||
|
||||
@ -69,7 +69,7 @@
|
||||
</Popover>
|
||||
</template>
|
||||
<script setup>
|
||||
import Popover from '@/components/frappe-ui/Popover.vue'
|
||||
import { Popover } from 'frappe-ui'
|
||||
import { gemoji } from 'gemoji'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
|
||||
19
frontend/src/components/Icons/SettingsIcon2.vue
Normal file
19
frontend/src/components/Icons/SettingsIcon2.vue
Normal file
@ -0,0 +1,19 @@
|
||||
<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>
|
||||
16
frontend/src/components/Icons/SparkleIcon.vue
Normal file
16
frontend/src/components/Icons/SparkleIcon.vue
Normal file
@ -0,0 +1,16 @@
|
||||
<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>
|
||||
@ -3,11 +3,8 @@
|
||||
:label="__('Kanban Settings')"
|
||||
@click="showDialog = true"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<template #prefix>
|
||||
<KanbanIcon class="h-4" />
|
||||
</template>
|
||||
</Button>
|
||||
:iconLeft="KanbanIcon"
|
||||
/>
|
||||
<Dialog v-model="showDialog" :options="{ title: __('Kanban Settings') }">
|
||||
<template #body-content>
|
||||
<div>
|
||||
@ -23,8 +20,8 @@
|
||||
<template #target="{ togglePopover }">
|
||||
<Button
|
||||
class="w-full !justify-start"
|
||||
@click="togglePopover()"
|
||||
:label="columnField.label"
|
||||
@click="togglePopover()"
|
||||
/>
|
||||
</template>
|
||||
</Autocomplete>
|
||||
@ -80,13 +77,10 @@
|
||||
<template #target="{ togglePopover }">
|
||||
<Button
|
||||
class="w-full mt-2"
|
||||
@click="togglePopover()"
|
||||
:label="__('Add Field')"
|
||||
>
|
||||
<template #prefix>
|
||||
<FeatherIcon name="plus" class="h-4" />
|
||||
</template>
|
||||
</Button>
|
||||
iconLeft="plus"
|
||||
@click="togglePopover()"
|
||||
/>
|
||||
</template>
|
||||
<template #item-label="{ option }">
|
||||
<div class="flex flex-col gap-1 text-ink-gray-9">
|
||||
|
||||
@ -15,17 +15,18 @@
|
||||
>
|
||||
<div class="flex gap-2 items-center group justify-between">
|
||||
<div class="flex items-center text-base">
|
||||
<NestedPopover>
|
||||
<template #target>
|
||||
<Popover>
|
||||
<template #target="{ togglePopover }">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="hover:!bg-surface-gray-2"
|
||||
@click="togglePopover"
|
||||
>
|
||||
<IndicatorIcon :class="parseColor(column.column.color)" />
|
||||
</Button>
|
||||
</template>
|
||||
<template #body="{ close }">
|
||||
<template #body>
|
||||
<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"
|
||||
>
|
||||
@ -48,7 +49,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</NestedPopover>
|
||||
</Popover>
|
||||
<div class="text-ink-gray-9">{{ column.column.name }}</div>
|
||||
</div>
|
||||
<div class="flex">
|
||||
@ -153,13 +154,10 @@
|
||||
<template #target="{ togglePopover }">
|
||||
<Button
|
||||
class="w-full mt-2.5 mb-1 mr-5"
|
||||
@click="togglePopover()"
|
||||
:label="__('Add Column')"
|
||||
>
|
||||
<template #prefix>
|
||||
<FeatherIcon name="plus" class="h-4" />
|
||||
</template>
|
||||
</Button>
|
||||
iconLeft="plus"
|
||||
@click="togglePopover()"
|
||||
/>
|
||||
</template>
|
||||
</Autocomplete>
|
||||
</div>
|
||||
@ -167,11 +165,10 @@
|
||||
</template>
|
||||
<script setup>
|
||||
import Autocomplete from '@/components/frappe-ui/Autocomplete.vue'
|
||||
import NestedPopover from '@/components/NestedPopover.vue'
|
||||
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
|
||||
import { isTouchScreenDevice, colors, parseColor } from '@/utils'
|
||||
import Draggable from 'vuedraggable'
|
||||
import { Dropdown } from 'frappe-ui'
|
||||
import { Dropdown, Popover } from 'frappe-ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
|
||||
@ -3,8 +3,8 @@
|
||||
class="relative flex h-full flex-col justify-between transition-all duration-300 ease-in-out"
|
||||
:class="isSidebarCollapsed ? 'w-12' : 'w-[220px]'"
|
||||
>
|
||||
<div>
|
||||
<UserDropdown class="p-2" :isCollapsed="isSidebarCollapsed" />
|
||||
<div class="p-2">
|
||||
<UserDropdown :isCollapsed="isSidebarCollapsed" />
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<div class="mb-3 flex flex-col">
|
||||
@ -197,51 +197,50 @@ const isSidebarCollapsed = useStorage('isSidebarCollapsed', false)
|
||||
const isFCSite = ref(window.is_fc_site)
|
||||
const isDemoSite = ref(window.is_demo_site)
|
||||
|
||||
const allViews = computed(() => {
|
||||
const links = [
|
||||
{
|
||||
label: 'Dashboard',
|
||||
icon: LucideLayoutDashboard,
|
||||
to: 'Dashboard',
|
||||
condition: () => isManager(),
|
||||
},
|
||||
{
|
||||
label: 'Leads',
|
||||
icon: LeadsIcon,
|
||||
to: 'Leads',
|
||||
},
|
||||
{
|
||||
label: 'Deals',
|
||||
icon: DealsIcon,
|
||||
to: 'Deals',
|
||||
},
|
||||
{
|
||||
label: 'Contacts',
|
||||
icon: ContactsIcon,
|
||||
to: 'Contacts',
|
||||
},
|
||||
{
|
||||
label: 'Organizations',
|
||||
icon: OrganizationsIcon,
|
||||
to: 'Organizations',
|
||||
},
|
||||
{
|
||||
label: 'Notes',
|
||||
icon: NoteIcon,
|
||||
to: 'Notes',
|
||||
},
|
||||
{
|
||||
label: 'Tasks',
|
||||
icon: TaskIcon,
|
||||
to: 'Tasks',
|
||||
},
|
||||
{
|
||||
label: 'Call Logs',
|
||||
icon: PhoneIcon,
|
||||
to: 'Call Logs',
|
||||
},
|
||||
]
|
||||
const links = [
|
||||
{
|
||||
label: 'Dashboard',
|
||||
icon: LucideLayoutDashboard,
|
||||
to: 'Dashboard',
|
||||
},
|
||||
{
|
||||
label: 'Leads',
|
||||
icon: LeadsIcon,
|
||||
to: 'Leads',
|
||||
},
|
||||
{
|
||||
label: 'Deals',
|
||||
icon: DealsIcon,
|
||||
to: 'Deals',
|
||||
},
|
||||
{
|
||||
label: 'Contacts',
|
||||
icon: ContactsIcon,
|
||||
to: 'Contacts',
|
||||
},
|
||||
{
|
||||
label: 'Organizations',
|
||||
icon: OrganizationsIcon,
|
||||
to: 'Organizations',
|
||||
},
|
||||
{
|
||||
label: 'Notes',
|
||||
icon: NoteIcon,
|
||||
to: 'Notes',
|
||||
},
|
||||
{
|
||||
label: 'Tasks',
|
||||
icon: TaskIcon,
|
||||
to: 'Tasks',
|
||||
},
|
||||
{
|
||||
label: 'Call Logs',
|
||||
icon: PhoneIcon,
|
||||
to: 'Call Logs',
|
||||
},
|
||||
]
|
||||
|
||||
const allViews = computed(() => {
|
||||
let _views = [
|
||||
{
|
||||
name: 'All Views',
|
||||
|
||||
@ -106,6 +106,8 @@ function convertToDeal(selections, unselectAll) {
|
||||
}
|
||||
|
||||
function deleteValues(selections, unselectAll) {
|
||||
unselectAllAction.value = unselectAll
|
||||
|
||||
const selectedDocs = Array.from(selections)
|
||||
if (selectedDocs.length == 1) {
|
||||
showDeleteDocModal.value = {
|
||||
@ -217,6 +219,12 @@ function bulkActions(selections, unselectAll) {
|
||||
}
|
||||
|
||||
function reload(unselectAll) {
|
||||
showDeleteDocModal.value = {
|
||||
showLinkedDocsModal: false,
|
||||
showDeleteModal: false,
|
||||
docname: null,
|
||||
}
|
||||
|
||||
unselectAllAction.value?.()
|
||||
unselectAll?.()
|
||||
list.value?.reload()
|
||||
|
||||
@ -26,13 +26,14 @@
|
||||
<ListRowItem
|
||||
:item="item"
|
||||
@click="listViewRef.toggleRow(row['reference_docname'])"
|
||||
class="!w-full"
|
||||
>
|
||||
<template #default="{ label }">
|
||||
<div
|
||||
v-if="column.key === 'title'"
|
||||
class="truncate text-base flex gap-2"
|
||||
class="truncate text-base flex gap-2 w-full"
|
||||
>
|
||||
<span>
|
||||
<span class="max-w-[90%] truncate">
|
||||
{{ label }}
|
||||
</span>
|
||||
<FeatherIcon
|
||||
@ -102,6 +103,7 @@ const listViewRef = ref(null)
|
||||
const viewLinkedDoc = (doc) => {
|
||||
let page = ''
|
||||
let id = ''
|
||||
let openDesk = false
|
||||
switch (doc.reference_doctype) {
|
||||
case 'CRM Lead':
|
||||
page = 'leads'
|
||||
@ -123,6 +125,11 @@ const viewLinkedDoc = (doc) => {
|
||||
page = 'organizations'
|
||||
id = doc.reference_docname
|
||||
break
|
||||
case 'CRM Notification':
|
||||
page = 'crm-notification'
|
||||
id = doc.reference_docname
|
||||
openDesk = true
|
||||
break
|
||||
case 'FCRM Note':
|
||||
page = 'notes'
|
||||
id = `view?open=${doc.reference_docname}`
|
||||
@ -130,7 +137,11 @@ const viewLinkedDoc = (doc) => {
|
||||
default:
|
||||
break
|
||||
}
|
||||
window.open(`/crm/${page}/${id}`)
|
||||
let base = '/crm'
|
||||
if (openDesk) {
|
||||
base = '/app'
|
||||
}
|
||||
window.open(`${base}/${page}/${id}`)
|
||||
}
|
||||
|
||||
const getDoctypeName = (doctype) => {
|
||||
|
||||
@ -11,19 +11,18 @@
|
||||
<div class="flex items-center gap-1">
|
||||
<Button
|
||||
v-if="isManager() && !isMobileView"
|
||||
:tooltip="__('Edit fields layout')"
|
||||
variant="ghost"
|
||||
:icon="EditIcon"
|
||||
class="w-7"
|
||||
@click="openQuickEntryModal"
|
||||
>
|
||||
<template #icon>
|
||||
<EditIcon />
|
||||
</template>
|
||||
</Button>
|
||||
<Button variant="ghost" class="w-7" @click="show = false">
|
||||
<template #icon>
|
||||
<FeatherIcon name="x" class="size-4" />
|
||||
</template>
|
||||
</Button>
|
||||
/>
|
||||
<Button
|
||||
icon="x"
|
||||
variant="ghost"
|
||||
class="w-7"
|
||||
@click="show = false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="tabs.data && _address.doc">
|
||||
|
||||
@ -36,18 +36,17 @@
|
||||
<Button
|
||||
v-if="!isMobileView"
|
||||
variant="ghost"
|
||||
:tooltip="__('Edit call log')"
|
||||
:icon="EditIcon"
|
||||
class="w-7"
|
||||
@click="openCallLogModal"
|
||||
>
|
||||
<template #icon>
|
||||
<EditIcon />
|
||||
</template>
|
||||
</Button>
|
||||
<Button variant="ghost" class="w-7" @click="show = false">
|
||||
<template #icon>
|
||||
<FeatherIcon name="x" class="size-4" />
|
||||
</template>
|
||||
</Button>
|
||||
/>
|
||||
<Button
|
||||
icon="x"
|
||||
variant="ghost"
|
||||
class="w-7"
|
||||
@click="show = false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3.5">
|
||||
|
||||
@ -13,18 +13,17 @@
|
||||
<Button
|
||||
v-if="isManager() && !isMobileView"
|
||||
variant="ghost"
|
||||
:tooltip="__('Edit fields layout')"
|
||||
:icon="EditIcon"
|
||||
class="w-7"
|
||||
@click="openQuickEntryModal"
|
||||
>
|
||||
<template #icon>
|
||||
<EditIcon />
|
||||
</template>
|
||||
</Button>
|
||||
<Button variant="ghost" class="w-7" @click="show = false">
|
||||
<template #icon>
|
||||
<FeatherIcon name="x" class="size-4" />
|
||||
</template>
|
||||
</Button>
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="w-7"
|
||||
@click="show = false"
|
||||
icon="x"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="tabs.data">
|
||||
@ -37,7 +36,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-4 pt-4 pb-7 sm:px-6">
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button
|
||||
class="w-full"
|
||||
v-for="action in dialogOptions.actions"
|
||||
@ -61,7 +60,7 @@ import { showQuickEntryModal, quickEntryProps } from '@/composables/modals'
|
||||
import { getRandom } from '@/utils'
|
||||
import { capture } from '@/telemetry'
|
||||
import { useDocument } from '@/data/document'
|
||||
import { FeatherIcon, createResource, ErrorMessage, Badge } from 'frappe-ui'
|
||||
import { createResource, ErrorMessage, Badge } from 'frappe-ui'
|
||||
import { ref, nextTick, computed, onMounted } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
|
||||
@ -13,17 +13,16 @@
|
||||
v-if="isManager() && !isMobileView"
|
||||
variant="ghost"
|
||||
class="w-7"
|
||||
:tooltip="__('Edit fields layout')"
|
||||
:icon="EditIcon"
|
||||
@click="openQuickEntryModal"
|
||||
>
|
||||
<template #icon>
|
||||
<EditIcon />
|
||||
</template>
|
||||
</Button>
|
||||
<Button variant="ghost" class="w-7" @click="show = false">
|
||||
<template #icon>
|
||||
<FeatherIcon name="x" class="size-4" />
|
||||
</template>
|
||||
</Button>
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="w-7"
|
||||
@click="show = false"
|
||||
icon="x"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<FieldLayout
|
||||
@ -90,12 +89,16 @@ const { document: _contact, triggerOnBeforeCreate } = useDocument('Contact')
|
||||
|
||||
async function createContact() {
|
||||
if (_contact.doc.email_id) {
|
||||
_contact.doc.email_ids = [{ email_id: _contact.doc.email_id, is_primary: 1 }]
|
||||
_contact.doc.email_ids = [
|
||||
{ email_id: _contact.doc.email_id, is_primary: 1 },
|
||||
]
|
||||
delete _contact.doc.email_id
|
||||
}
|
||||
|
||||
if (_contact.doc.mobile_no) {
|
||||
_contact.doc.phone_nos = [{ phone: _contact.doc.mobile_no, is_primary_mobile_no: 1 }]
|
||||
_contact.doc.phone_nos = [
|
||||
{ phone: _contact.doc.mobile_no, is_primary_mobile_no: 1 },
|
||||
]
|
||||
delete _contact.doc.mobile_no
|
||||
}
|
||||
|
||||
|
||||
@ -1,17 +1,5 @@
|
||||
<template>
|
||||
<Dialog
|
||||
v-model="show"
|
||||
:options="{
|
||||
size: 'xl',
|
||||
actions: [
|
||||
{
|
||||
label: __('Convert'),
|
||||
variant: 'solid',
|
||||
onClick: convertToDeal,
|
||||
},
|
||||
],
|
||||
}"
|
||||
>
|
||||
<Dialog v-model="show" :options="{ size: 'xl' }">
|
||||
<template #body-header>
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
@ -23,12 +11,10 @@
|
||||
<Button
|
||||
v-if="isManager() && !isMobileView"
|
||||
variant="ghost"
|
||||
:tooltip="__('Edit deal\'s mandatory fields layout')"
|
||||
:icon="EditIcon"
|
||||
@click="openQuickEntryModal"
|
||||
>
|
||||
<template #icon>
|
||||
<EditIcon class="h-4 w-4" />
|
||||
</template>
|
||||
</Button>
|
||||
/>
|
||||
<Button icon="x" variant="ghost" @click="show = false" />
|
||||
</div>
|
||||
</div>
|
||||
@ -92,6 +78,11 @@
|
||||
/>
|
||||
<ErrorMessage class="mt-4" :message="error" />
|
||||
</template>
|
||||
<template #actions>
|
||||
<div class="flex justify-end">
|
||||
<Button :label="__('Convert')" variant="solid" @click="convertToDeal" />
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup>
|
||||
|
||||
@ -13,17 +13,16 @@
|
||||
v-if="isManager() && !isMobileView"
|
||||
variant="ghost"
|
||||
class="w-7"
|
||||
:tooltip="__('Edit fields layout')"
|
||||
:icon="EditIcon"
|
||||
@click="openQuickEntryModal"
|
||||
>
|
||||
<template #icon>
|
||||
<EditIcon />
|
||||
</template>
|
||||
</Button>
|
||||
<Button variant="ghost" class="w-7" @click="show = false">
|
||||
<template #icon>
|
||||
<FeatherIcon name="x" class="size-4" />
|
||||
</template>
|
||||
</Button>
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="w-7"
|
||||
icon="x"
|
||||
@click="show = false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="tabs.data">
|
||||
|
||||
@ -13,17 +13,16 @@
|
||||
v-if="isManager() && !isMobileView"
|
||||
variant="ghost"
|
||||
class="w-7"
|
||||
:tooltip="__('Edit fields layout')"
|
||||
:icon="EditIcon"
|
||||
@click="openQuickEntryModal"
|
||||
>
|
||||
<template #icon>
|
||||
<EditIcon />
|
||||
</template>
|
||||
</Button>
|
||||
<Button variant="ghost" class="w-7" @click="show = false">
|
||||
<template #icon>
|
||||
<FeatherIcon name="x" class="size-4" />
|
||||
</template>
|
||||
</Button>
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="w-7"
|
||||
icon="x"
|
||||
@click="show = false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@ -13,17 +13,16 @@
|
||||
v-if="isManager() && !isMobileView"
|
||||
variant="ghost"
|
||||
class="w-7"
|
||||
:tooltip="__('Edit fields layout')"
|
||||
:icon="EditIcon"
|
||||
@click="openQuickEntryModal"
|
||||
>
|
||||
<template #icon>
|
||||
<EditIcon />
|
||||
</template>
|
||||
</Button>
|
||||
<Button variant="ghost" class="w-7" @click="show = false">
|
||||
<template #icon>
|
||||
<FeatherIcon name="x" class="size-4" />
|
||||
</template>
|
||||
</Button>
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="w-7"
|
||||
@click="show = false"
|
||||
icon="x"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@ -1,45 +1,60 @@
|
||||
<template>
|
||||
<Dialog v-model="show" :options="{
|
||||
size: 'xl',
|
||||
actions: [
|
||||
{
|
||||
label: editMode ? __('Update') : __('Create'),
|
||||
variant: 'solid',
|
||||
onClick: () => updateNote(),
|
||||
},
|
||||
],
|
||||
}">
|
||||
<Dialog v-model="show" :options="{ size: 'xl' }">
|
||||
<template #body-title>
|
||||
<div class="flex items-center gap-3">
|
||||
<h3 class="text-2xl font-semibold leading-6 text-ink-gray-9">
|
||||
{{ editMode ? __('Edit Note') : __('Create Note') }}
|
||||
</h3>
|
||||
<Button v-if="_note?.reference_docname" size="sm" :label="_note.reference_doctype == 'CRM Deal'
|
||||
? __('Open Deal')
|
||||
: __('Open Lead')
|
||||
" @click="redirect()">
|
||||
<template #suffix>
|
||||
<ArrowUpRightIcon class="w-4 h-4" />
|
||||
</template>
|
||||
</Button>
|
||||
<Button
|
||||
v-if="_note?.reference_docname"
|
||||
size="sm"
|
||||
:label="
|
||||
_note.reference_doctype == 'CRM Deal'
|
||||
? __('Open Deal')
|
||||
: __('Open Lead')
|
||||
"
|
||||
:iconRight="ArrowUpRightIcon"
|
||||
@click="redirect()"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #body-content>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<FormControl ref="title" :label="__('Title')" v-model="_note.title" :placeholder="__('Call with John Doe')"
|
||||
required />
|
||||
<FormControl
|
||||
ref="title"
|
||||
:label="__('Title')"
|
||||
v-model="_note.title"
|
||||
:placeholder="__('Call with John Doe')"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-1.5 text-xs text-ink-gray-5">{{ __('Content') }}</div>
|
||||
<TextEditor variant="outline" ref="content"
|
||||
<TextEditor
|
||||
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"
|
||||
:bubbleMenu="true" :content="_note.content" @change="(val) => (_note.content = val)" :placeholder="__('Took a call with John Doe and discussed the new project.')
|
||||
" />
|
||||
:bubbleMenu="true"
|
||||
:content="_note.content"
|
||||
@change="(val) => (_note.content = val)"
|
||||
:placeholder="
|
||||
__('Took a call with John Doe and discussed the new project.')
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<ErrorMessage class="mt-4" v-if="error" :message="__(error)" />
|
||||
</div>
|
||||
</template>
|
||||
<template #actions>
|
||||
<div class="flex justify-end">
|
||||
<Button
|
||||
:label="editMode ? __('Update') : __('Create')"
|
||||
variant="solid"
|
||||
@click="updateNote"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
@ -92,21 +107,25 @@ async function updateNote() {
|
||||
emit('after', d)
|
||||
}
|
||||
} else {
|
||||
let d = await call('frappe.client.insert', {
|
||||
doc: {
|
||||
doctype: 'FCRM Note',
|
||||
title: _note.value.title,
|
||||
content: _note.value.content,
|
||||
reference_doctype: props.doctype,
|
||||
reference_docname: props.doc || '',
|
||||
let d = await call(
|
||||
'frappe.client.insert',
|
||||
{
|
||||
doc: {
|
||||
doctype: 'FCRM Note',
|
||||
title: _note.value.title,
|
||||
content: _note.value.content,
|
||||
reference_doctype: props.doctype,
|
||||
reference_docname: props.doc || '',
|
||||
},
|
||||
},
|
||||
}, {
|
||||
onError: (err) => {
|
||||
if (err.error.exc_type == 'MandatoryError') {
|
||||
error.value = "Title is mandatory"
|
||||
}
|
||||
}
|
||||
})
|
||||
{
|
||||
onError: (err) => {
|
||||
if (err.error.exc_type == 'MandatoryError') {
|
||||
error.value = 'Title is mandatory'
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
if (d.name) {
|
||||
updateOnboardingStep('create_first_note')
|
||||
capture('note_created')
|
||||
|
||||
@ -13,17 +13,16 @@
|
||||
v-if="isManager() && !isMobileView"
|
||||
variant="ghost"
|
||||
class="w-7"
|
||||
:tooltip="__('Edit fields layout')"
|
||||
:icon="EditIcon"
|
||||
@click="openQuickEntryModal"
|
||||
>
|
||||
<template #icon>
|
||||
<EditIcon />
|
||||
</template>
|
||||
</Button>
|
||||
<Button variant="ghost" class="w-7" @click="show = false">
|
||||
<template #icon>
|
||||
<FeatherIcon name="x" class="size-4" />
|
||||
</template>
|
||||
</Button>
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="w-7"
|
||||
@click="show = false"
|
||||
icon="x"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<FieldLayout
|
||||
@ -109,6 +108,7 @@ async function createOrganization() {
|
||||
onError: (err) => {
|
||||
if (err.error.exc_type == 'ValidationError') {
|
||||
error.value = err.error?.messages?.[0]
|
||||
loading.value = false
|
||||
}
|
||||
},
|
||||
},
|
||||
@ -175,6 +175,5 @@ function openAddressModal(_address) {
|
||||
doctype: 'Address',
|
||||
address: _address,
|
||||
}
|
||||
nextTick(() => (show.value = false))
|
||||
}
|
||||
</script>
|
||||
@ -1,17 +1,5 @@
|
||||
<template>
|
||||
<Dialog
|
||||
v-model="show"
|
||||
:options="{
|
||||
size: 'xl',
|
||||
actions: [
|
||||
{
|
||||
label: editMode ? __('Update') : __('Create'),
|
||||
variant: 'solid',
|
||||
onClick: () => updateTask(),
|
||||
},
|
||||
],
|
||||
}"
|
||||
>
|
||||
<Dialog v-model="show" :options="{ size: 'xl' }">
|
||||
<template #body-title>
|
||||
<div class="flex items-center gap-3">
|
||||
<h3 class="text-2xl font-semibold leading-6 text-ink-gray-9">
|
||||
@ -25,12 +13,9 @@
|
||||
? __('Open Deal')
|
||||
: __('Open Lead')
|
||||
"
|
||||
:iconRight="ArrowUpRightIcon"
|
||||
@click="redirect()"
|
||||
>
|
||||
<template #suffix>
|
||||
<ArrowUpRightIcon class="w-4 h-4" />
|
||||
</template>
|
||||
</Button>
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #body-content>
|
||||
@ -62,7 +47,7 @@
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Dropdown :options="taskStatusOptions(updateTaskStatus)">
|
||||
<Button :label="_task.status" class="justify-between w-full">
|
||||
<Button :label="_task.status">
|
||||
<template #prefix>
|
||||
<TaskStatusIcon :status="_task.status" />
|
||||
</template>
|
||||
@ -93,15 +78,17 @@
|
||||
</Tooltip>
|
||||
</template>
|
||||
</Link>
|
||||
<DateTimePicker
|
||||
class="datepicker w-36"
|
||||
v-model="_task.due_date"
|
||||
:placeholder="__('01/04/2024 11:30 PM')"
|
||||
:formatter="(date) => getFormat(date, '', true, true)"
|
||||
input-class="border-none"
|
||||
/>
|
||||
<div class="w-36">
|
||||
<DateTimePicker
|
||||
class="datepicker"
|
||||
v-model="_task.due_date"
|
||||
:placeholder="__('01/04/2024 11:30 PM')"
|
||||
:formatter="(date) => getFormat(date, '', true, true)"
|
||||
input-class="border-none"
|
||||
/>
|
||||
</div>
|
||||
<Dropdown :options="taskPriorityOptions(updateTaskPriority)">
|
||||
<Button :label="_task.priority" class="justify-between w-full">
|
||||
<Button :label="_task.priority">
|
||||
<template #prefix>
|
||||
<TaskPriorityIcon :priority="_task.priority" />
|
||||
</template>
|
||||
@ -111,6 +98,15 @@
|
||||
<ErrorMessage class="mt-4" v-if="error" :message="__(error)" />
|
||||
</div>
|
||||
</template>
|
||||
<template #actions>
|
||||
<div class="flex justify-end">
|
||||
<Button
|
||||
:label="editMode ? __('Update') : __('Create')"
|
||||
variant="solid"
|
||||
@click="updateTask"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
|
||||
@ -7,17 +7,6 @@
|
||||
: duplicateMode
|
||||
? __('Duplicate View')
|
||||
: __('Create View'),
|
||||
actions: [
|
||||
{
|
||||
label: editMode
|
||||
? __('Save Changes')
|
||||
: duplicateMode
|
||||
? __('Duplicate')
|
||||
: __('Create'),
|
||||
variant: 'solid',
|
||||
onClick: () => (editMode ? update() : create()),
|
||||
},
|
||||
],
|
||||
}"
|
||||
>
|
||||
<template #body-content>
|
||||
@ -42,6 +31,21 @@
|
||||
/>
|
||||
</div>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
|
||||
@ -9,21 +9,9 @@
|
||||
$attrs.class,
|
||||
showDropdown ? 'rounded-br-none rounded-tr-none' : '',
|
||||
]"
|
||||
:iconLeft="activeButton.icon"
|
||||
@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
|
||||
v-if="showDropdown"
|
||||
:options="parsedOptions"
|
||||
@ -54,7 +42,6 @@ const showDropdown = ref(props.options?.length > 1)
|
||||
const activeButton = ref(props.options?.[0] || {})
|
||||
|
||||
const parsedOptions = computed(() => {
|
||||
debugger
|
||||
return (
|
||||
props.options?.map((option) => {
|
||||
return {
|
||||
|
||||
@ -1,60 +0,0 @@
|
||||
<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>
|
||||
@ -16,24 +16,18 @@
|
||||
>
|
||||
<div class="text-base font-medium">{{ __('Notifications') }}</div>
|
||||
<div class="flex gap-1">
|
||||
<Tooltip :text="__('Mark all as read')">
|
||||
<div>
|
||||
<Button variant="ghost" @click="() => markAllAsRead()">
|
||||
<template #icon>
|
||||
<MarkAsDoneIcon class="h-4 w-4" />
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip :text="__('Close')">
|
||||
<div>
|
||||
<Button variant="ghost" @click="() => toggle()">
|
||||
<template #icon>
|
||||
<FeatherIcon name="x" class="h-4 w-4" />
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Button
|
||||
:tooltip="__('Mark all as read')"
|
||||
:icon="MarkAsDoneIcon"
|
||||
variant="ghost"
|
||||
@click="markAllAsRead"
|
||||
/>
|
||||
<Button
|
||||
:tooltip="__('Close')"
|
||||
icon="x"
|
||||
variant="ghost"
|
||||
@click="() => toggle()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@ -100,7 +94,6 @@ import { globalStore } from '@/stores/global'
|
||||
import { timeAgo } from '@/utils'
|
||||
import { onClickOutside } from '@vueuse/core'
|
||||
import { capture } from '@/telemetry'
|
||||
import { Tooltip } from 'frappe-ui'
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
|
||||
const { $socket } = globalStore()
|
||||
|
||||
69
frontend/src/components/PrimaryDropdown.vue
Normal file
69
frontend/src/components/PrimaryDropdown.vue
Normal file
@ -0,0 +1,69 @@
|
||||
<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>
|
||||
@ -19,53 +19,36 @@
|
||||
v-if="editMode"
|
||||
variant="ghost"
|
||||
:label="__('Save')"
|
||||
size="sm"
|
||||
class="opacity-0 hover:bg-surface-gray-4 group-hover:opacity-100"
|
||||
@click="saveOption"
|
||||
/>
|
||||
<Tooltip text="Set As Primary" v-if="!isNew && !option.selected">
|
||||
<div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="opacity-0 hover:bg-surface-gray-4 group-hover:opacity-100"
|
||||
@click="option.onClick"
|
||||
>
|
||||
<template #icon>
|
||||
<SuccessIcon />
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip v-if="!editMode" text="Edit">
|
||||
<div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="opacity-0 hover:bg-surface-gray-4 group-hover:opacity-100"
|
||||
@click="toggleEditMode"
|
||||
>
|
||||
<template #icon>
|
||||
<EditIcon />
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip text="Delete">
|
||||
<div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
icon="x"
|
||||
size="sm"
|
||||
class="opacity-0 hover:bg-surface-gray-4 group-hover:opacity-100"
|
||||
@click="() => option.onDelete(option, isNew)"
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Button
|
||||
v-if="!isNew && !option.selected"
|
||||
:tooltip="__('Set As Primary')"
|
||||
variant="ghost"
|
||||
:icon="SuccessIcon"
|
||||
class="opacity-0 hover:bg-surface-gray-4 group-hover:opacity-100"
|
||||
@click="option.onClick"
|
||||
/>
|
||||
<Button
|
||||
v-if="!editMode"
|
||||
:tooltip="__('Edit')"
|
||||
variant="ghost"
|
||||
:icon="EditIcon"
|
||||
class="opacity-0 hover:bg-surface-gray-4 group-hover:opacity-100"
|
||||
@click="toggleEditMode"
|
||||
/>
|
||||
<Button
|
||||
:tooltip="__('Delete')"
|
||||
variant="ghost"
|
||||
icon="x"
|
||||
class="opacity-0 hover:bg-surface-gray-4 group-hover:opacity-100"
|
||||
@click="() => option.onDelete(option, isNew)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="option.selected">
|
||||
<FeatherIcon name="check" class="text-ink-gray-5 h-4 w-6" size="sm" />
|
||||
<FeatherIcon name="check" class="text-ink-gray-5 h-4 w-6" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -73,7 +56,7 @@
|
||||
<script setup>
|
||||
import SuccessIcon from '@/components/Icons/SuccessIcon.vue'
|
||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||
import { TextInput, Tooltip } from 'frappe-ui'
|
||||
import { TextInput } from 'frappe-ui'
|
||||
import { nextTick, ref, onMounted } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user