diff --git a/frappe/core/doctype/tag/tag.json b/frappe/core/doctype/tag/tag.json deleted file mode 100644 index 5b206eb506..0000000000 --- a/frappe/core/doctype/tag/tag.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "allow_copy": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "", - "creation": "2016-05-25 09:43:44.767581", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "fields": [ - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "tag_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Tags", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2016-05-31 08:29:01.773065", - "modified_by": "Administrator", - "module": "Core", - "name": "Tag", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "read_only": 0, - "read_only_onload": 0, - "sort_field": "modified", - "sort_order": "DESC" -} \ No newline at end of file diff --git a/frappe/core/doctype/tag/tag.py b/frappe/core/doctype/tag/tag.py deleted file mode 100644 index cc8e17e8d2..0000000000 --- a/frappe/core/doctype/tag/tag.py +++ /dev/null @@ -1,11 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2015, Frappe Technologies and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -import frappe -from frappe.model.document import Document - -class Tag(Document): - def validate(self): - self.tag_name = self.tag_name.title() diff --git a/frappe/core/doctype/tag_category/tag_category.js b/frappe/core/doctype/tag_category/tag_category.js deleted file mode 100644 index e01dad063d..0000000000 --- a/frappe/core/doctype/tag_category/tag_category.js +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) 2016, Frappe Technologies and contributors -// For license information, please see license.txt -frappe.ui.form.on('Tag', { - tag_name:function(frm){ - for (var i = 0 ;i 0: diff --git a/frappe/core/doctype/tag/__init__.py b/frappe/desk/doctype/tag/__init__.py similarity index 100% rename from frappe/core/doctype/tag/__init__.py rename to frappe/desk/doctype/tag/__init__.py diff --git a/frappe/desk/doctype/tag/tag.js b/frappe/desk/doctype/tag/tag.js new file mode 100644 index 0000000000..f55f98c3d0 --- /dev/null +++ b/frappe/desk/doctype/tag/tag.js @@ -0,0 +1,8 @@ +// Copyright (c) 2019, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Tag', { + // refresh: function(frm) { + + // } +}); diff --git a/frappe/desk/doctype/tag/tag.json b/frappe/desk/doctype/tag/tag.json new file mode 100644 index 0000000000..895516594e --- /dev/null +++ b/frappe/desk/doctype/tag/tag.json @@ -0,0 +1,49 @@ +{ + "autoname": "Prompt", + "creation": "2016-05-25 09:43:44.767581", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "description" + ], + "fields": [ + { + "fieldname": "description", + "fieldtype": "Small Text", + "in_list_view": 1, + "label": "Description" + } + ], + "modified": "2019-09-25 17:47:41.712237", + "modified_by": "Administrator", + "module": "Desk", + "name": "Tag", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/frappe/desk/doctype/tag/tag.py b/frappe/desk/doctype/tag/tag.py new file mode 100644 index 0000000000..0e2afbb35c --- /dev/null +++ b/frappe/desk/doctype/tag/tag.py @@ -0,0 +1,179 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document + +class Tag(Document): + pass + +def check_user_tags(dt): + "if the user does not have a tags column, then it creates one" + try: + frappe.db.sql("select `_user_tags` from `tab%s` limit 1" % dt) + except Exception as e: + if frappe.db.is_column_missing(e): + DocTags(dt).setup() + +@frappe.whitelist() +def add_tag(tag, dt, dn, color=None): + "adds a new tag to a record, and creates the Tag master" + DocTags(dt).add(dn, tag) + + return tag + +@frappe.whitelist() +def remove_tag(tag, dt, dn): + "removes tag from the record" + DocTags(dt).remove(dn, tag) + +@frappe.whitelist() +def get_tagged_docs(doctype, tag): + frappe.has_permission(doctype, throw=True) + + return frappe.db.sql("""SELECT name + FROM `tab{0}` + WHERE _user_tags LIKE '%{1}%'""".format(doctype, tag)) + +@frappe.whitelist() +def get_tags(doctype, txt): + tag = frappe.get_list("Tag", filters=[["name", "like", "%{}%".format(txt)]]) + tags = [t.name for t in tag] + + return sorted(filter(lambda t: t and txt.lower() in t.lower(), list(set(tags)))) + +class DocTags: + """Tags for a particular doctype""" + def __init__(self, dt): + self.dt = dt + + def get_tag_fields(self): + """returns tag_fields property""" + return frappe.db.get_value('DocType', self.dt, 'tag_fields') + + def get_tags(self, dn): + """returns tag for a particular item""" + return (frappe.db.get_value(self.dt, dn, '_user_tags', ignore=1) or '').strip() + + def add(self, dn, tag): + """add a new user tag""" + tl = self.get_tags(dn).split(',') + if not tag in tl: + tl.append(tag) + if not frappe.db.exists("Tag", tag): + frappe.get_doc({"doctype": "Tag", "name": tag}).insert(ignore_permissions=True) + self.update(dn, tl) + + def remove(self, dn, tag): + """remove a user tag""" + tl = self.get_tags(dn).split(',') + self.update(dn, filter(lambda x:x.lower()!=tag.lower(), tl)) + + def remove_all(self, dn): + """remove all user tags (call before delete)""" + self.update(dn, []) + + def update(self, dn, tl): + """updates the _user_tag column in the table""" + + if not tl: + tags = '' + else: + tl = list(set(filter(lambda x: x, tl))) + tags = ',' + ','.join(tl) + try: + frappe.db.sql("update `tab%s` set _user_tags=%s where name=%s" % \ + (self.dt,'%s','%s'), (tags , dn)) + doc= frappe.get_doc(self.dt, dn) + update_tags(doc, tags) + except Exception as e: + if frappe.db.is_column_missing(e): + if not tags: + # no tags, nothing to do + return + + self.setup() + self.update(dn, tl) + else: raise + + def setup(self): + """adds the _user_tags column if not exists""" + from frappe.database.schema import add_column + add_column(self.dt, "_user_tags", "Data") + +def delete_tags_for_document(doc): + """ + Delete the Tag Link entry of a document that has + been deleted + :param doc: Deleted document + """ + if not frappe.db.table_exists("Tag Link"): + return + + frappe.db.sql("""DELETE FROM `tabTag Link` WHERE `document_type`=%s AND `document_name`=%s""", (doc.doctype, doc.name)) + +def update_tags(doc, tags): + """ + Adds tags for documents + :param doc: Document to be added to global tags + """ + + new_tags = list(set([tag.strip() for tag in tags.split(",") if tag])) + + for tag in new_tags: + if not frappe.db.exists("Tag Link", {"parenttype": doc.doctype, "parent": doc.name, "tag": tag}): + frappe.get_doc({ + "doctype": "Tag Link", + "document_type": doc.doctype, + "document_name": doc.name, + "parenttype": doc.doctype, + "parent": doc.name, + "title": doc.get_title() or '', + "tag": tag + }).insert(ignore_permissions=True) + + existing_tags = [tag.tag for tag in frappe.get_list("Tag Link", filters={ + "document_type": doc.doctype, + "document_name": doc.name + }, fields=["tag"])] + + deleted_tags = get_deleted_tags(new_tags, existing_tags) + + if deleted_tags: + for tag in deleted_tags: + delete_tag_for_document(doc.doctype, doc.name, tag) + +def get_deleted_tags(new_tags, existing_tags): + + return list(set(existing_tags) - set(new_tags)) + +def delete_tag_for_document(dt, dn, tag): + frappe.db.sql("""DELETE FROM `tabTag Link` WHERE `document_type`=%s AND `document_name`=%s AND tag=%s""", (dt, dn, tag)) + +@frappe.whitelist() +def get_documents_for_tag(tag): + """ + Search for given text in Tag Link + :param tag: tag to be searched + """ + # remove hastag `#` from tag + tag = tag[1:] + results = [] + + result = frappe.get_list("Tag Link", filters={"tag": tag}, fields=["document_type", "document_name", "title", "tag"]) + + for res in result: + results.append({ + "doctype": res.document_type, + "name": res.document_name, + "content": res.title + }) + + print(results) + return results + +@frappe.whitelist() +def get_tags_list_for_awesomebar(): + return [t.name for t in frappe.get_list("Tag")] \ No newline at end of file diff --git a/frappe/desk/doctype/tag/test_tag.py b/frappe/desk/doctype/tag/test_tag.py new file mode 100644 index 0000000000..8efd692f43 --- /dev/null +++ b/frappe/desk/doctype/tag/test_tag.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestTag(unittest.TestCase): + pass diff --git a/frappe/core/doctype/tag_category/__init__.py b/frappe/desk/doctype/tag_link/__init__.py similarity index 100% rename from frappe/core/doctype/tag_category/__init__.py rename to frappe/desk/doctype/tag_link/__init__.py diff --git a/frappe/desk/doctype/tag_link/tag_link.js b/frappe/desk/doctype/tag_link/tag_link.js new file mode 100644 index 0000000000..d85655bb90 --- /dev/null +++ b/frappe/desk/doctype/tag_link/tag_link.js @@ -0,0 +1,8 @@ +// Copyright (c) 2019, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Tag Link', { + // refresh: function(frm) { + + // } +}); diff --git a/frappe/desk/doctype/tag_link/tag_link.json b/frappe/desk/doctype/tag_link/tag_link.json new file mode 100644 index 0000000000..00a7349c5c --- /dev/null +++ b/frappe/desk/doctype/tag_link/tag_link.json @@ -0,0 +1,70 @@ +{ + "creation": "2019-09-24 13:25:36.435685", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "document_type", + "document_name", + "tag", + "title" + ], + "fields": [ + { + "fieldname": "title", + "fieldtype": "Data", + "label": "Document Title", + "read_only": 1 + }, + { + "fieldname": "tag", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Document Tag", + "options": "Tag", + "read_only": 1 + }, + { + "fieldname": "document_type", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Document Type", + "options": "DocType", + "read_only": 1 + }, + { + "fieldname": "document_name", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Document Name", + "options": "document_type", + "read_only": 1 + } + ], + "modified": "2019-10-03 16:42:35.932409", + "modified_by": "Administrator", + "module": "Desk", + "name": "Tag Link", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "read_only": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/core/doctype/tag_category/tag_category.py b/frappe/desk/doctype/tag_link/tag_link.py similarity index 61% rename from frappe/core/doctype/tag_category/tag_category.py rename to frappe/desk/doctype/tag_link/tag_link.py index 8d4d70a063..87c8af7212 100644 --- a/frappe/core/doctype/tag_category/tag_category.py +++ b/frappe/desk/doctype/tag_link/tag_link.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2015, Frappe Technologies and contributors +# Copyright (c) 2019, Frappe Technologies and contributors # For license information, please see license.txt from __future__ import unicode_literals -import frappe +# import frappe from frappe.model.document import Document -class TagCategory(Document): +class TagLink(Document): pass diff --git a/frappe/desk/doctype/tag_link/test_tag_link.py b/frappe/desk/doctype/tag_link/test_tag_link.py new file mode 100644 index 0000000000..1c22ac18bc --- /dev/null +++ b/frappe/desk/doctype/tag_link/test_tag_link.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestTagLink(unittest.TestCase): + pass diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py index 8c7082401a..4044a3dcfc 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -100,7 +100,8 @@ def get_docinfo(doc=None, doctype=None, name=None): "views": get_view_logs(doc.doctype, doc.name), "energy_point_logs": get_point_logs(doc.doctype, doc.name), "milestones": get_milestones(doc.doctype, doc.name), - "is_document_followed": is_document_followed(doc.doctype, doc.name, frappe.session.user) + "is_document_followed": is_document_followed(doc.doctype, doc.name, frappe.session.user), + "tags": get_tags(doc.doctype, doc.name) } def get_milestones(doctype, name): @@ -255,3 +256,11 @@ def get_view_logs(doctype, docname): if view_logs: logs = view_logs return logs + +def get_tags(doctype, name): + tags = [tag.tag for tag in frappe.get_all("Tag Link", filters={ + "document_type": doctype, + "document_name": name + }, fields=["tag"])] + + return ",".join([tag for tag in tags]) \ No newline at end of file diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index dd984625fd..d5b43807a8 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -261,13 +261,17 @@ def delete_bulk(doctype, items): @frappe.whitelist() @frappe.read_only() def get_sidebar_stats(stats, doctype, filters=[]): - cat_tags = frappe.db.sql("""select `tag`.parent as `category`, `tag`.tag_name as `tag` - from `tabTag Doc Category` as `docCat` - INNER JOIN `tabTag` as `tag` on `tag`.parent = `docCat`.parent - where `docCat`.tagdoc=%s - ORDER BY `tag`.parent asc, `tag`.idx""", doctype, as_dict=1) - return {"defined_cat":cat_tags, "stats":get_stats(stats, doctype, filters)} + if not frappe.cache().hget("tags_count", doctype): + tags = [tag.name for tag in frappe.get_list("Tag")] + _user_tags = [] + for tag in tags: + count = frappe.db.count("Tag Link", filters={"document_type": doctype, "tag": tag}) + if count > 0: + _user_tags.append([tag, count]) + frappe.cache().hset("tags_count", doctype, _user_tags) + + return {"stats": {"_user_tags": frappe.cache().hget("tags_count", doctype)}} @frappe.whitelist() @frappe.read_only() diff --git a/frappe/desk/tags.py b/frappe/desk/tags.py deleted file mode 100644 index 0d130f48cf..0000000000 --- a/frappe/desk/tags.py +++ /dev/null @@ -1,127 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - -from __future__ import unicode_literals, print_function -import json -""" -Server side functions for tagging. - -- Tags can be added to any record (doctype, name) in the system. -- Items are filtered by tags -- Top tags are shown in the sidebar (?) -- Tags are also identified by the tag_fields property of the DocType - -Discussion: - -Tags are shown in the docbrowser and ideally where-ever items are searched. -There should also be statistics available for tags (like top tags etc) - - -Design: - -- free tags (user_tags) are stored in __user_tags -- doctype tags are set in tag_fields property of the doctype -- top tags merges the tags from both the lists (only refreshes once an hour (max)) - -""" - -import frappe -from frappe.utils.global_search import update_global_search - -def check_user_tags(dt): - "if the user does not have a tags column, then it creates one" - try: - frappe.db.sql("select `_user_tags` from `tab%s` limit 1" % dt) - except Exception as e: - if frappe.db.is_column_missing(e): - DocTags(dt).setup() - -@frappe.whitelist() -def add_tag(tag, dt, dn, color=None): - "adds a new tag to a record, and creates the Tag master" - DocTags(dt).add(dn, tag) - - return tag - -@frappe.whitelist() -def remove_tag(tag, dt, dn): - "removes tag from the record" - DocTags(dt).remove(dn, tag) - -@frappe.whitelist() -def get_tagged_docs(doctype, tag): - frappe.has_permission(doctype, throw=True) - - return frappe.db.sql("""SELECT name - FROM `tab{0}` - WHERE _user_tags LIKE '%{1}%'""".format(doctype, tag)) - -@frappe.whitelist() -def get_tags(doctype, txt, cat_tags): - tags = json.loads(cat_tags) - try: - for _user_tags in frappe.db.sql_list("""select DISTINCT `_user_tags` - from `tab{0}` - where _user_tags like {1} - limit 50""".format(doctype, frappe.db.escape('%' + txt + '%'))): - tags.extend(_user_tags[1:].split(",")) - except Exception as e: - if not frappe.db.is_column_missing(e): raise - return sorted(filter(lambda t: t and txt.lower() in t.lower(), list(set(tags)))) - -class DocTags: - """Tags for a particular doctype""" - def __init__(self, dt): - self.dt = dt - - def get_tag_fields(self): - """returns tag_fields property""" - return frappe.db.get_value('DocType', self.dt, 'tag_fields') - - def get_tags(self, dn): - """returns tag for a particular item""" - return (frappe.db.get_value(self.dt, dn, '_user_tags', ignore=1) or '').strip() - - def add(self, dn, tag): - """add a new user tag""" - tl = self.get_tags(dn).split(',') - if not tag in tl: - tl.append(tag) - self.update(dn, tl) - - def remove(self, dn, tag): - """remove a user tag""" - tl = self.get_tags(dn).split(',') - self.update(dn, filter(lambda x:x.lower()!=tag.lower(), tl)) - - def remove_all(self, dn): - """remove all user tags (call before delete)""" - self.update(dn, []) - - def update(self, dn, tl): - """updates the _user_tag column in the table""" - - if not tl: - tags = '' - else: - tl = list(set(filter(lambda x: x, tl))) - tags = ',' + ','.join(tl) - try: - frappe.db.sql("update `tab%s` set _user_tags=%s where name=%s" % \ - (self.dt,'%s','%s'), (tags , dn)) - doc= frappe.get_doc(self.dt, dn) - update_global_search(doc) - except Exception as e: - if frappe.db.is_column_missing(e): - if not tags: - # no tags, nothing to do - return - - self.setup() - self.update(dn, tl) - else: raise - - def setup(self): - """adds the _user_tags column if not exists""" - from frappe.database.schema import add_column - add_column(self.dt, "_user_tags", "Data") diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index 04a77f65ec..33d7f8e0af 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -16,10 +16,11 @@ from frappe.core.doctype.file.file import remove_all from frappe.utils.password import delete_all_passwords_for from frappe.model.naming import revert_series_if_last from frappe.utils.global_search import delete_for_document +from frappe.desk.doctype.tag.tag import delete_tags_for_document from frappe.exceptions import FileNotFoundError -doctypes_to_skip = ("Communication", "ToDo", "DocShare", "Email Unsubscribe", "Activity Log", "File", "Version", "Document Follow", "Comment" , "View Log") +doctypes_to_skip = ("Communication", "ToDo", "DocShare", "Email Unsubscribe", "Activity Log", "File", "Version", "Document Follow", "Comment" , "View Log", "Tag Link") def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reload=False, ignore_permissions=False, flags=None, ignore_on_trash=False, ignore_missing=True): @@ -116,6 +117,8 @@ def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reloa # delete global search entry delete_for_document(doc) + # delete tag link entry + delete_tags_for_document(doc) if doc and not for_reload: add_to_deleted_document(doc) diff --git a/frappe/patches.txt b/frappe/patches.txt index 36aa390d65..3a1706cda1 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -252,3 +252,4 @@ frappe.patches.v12_0.move_email_and_phone_to_child_table frappe.patches.v12_0.delete_duplicate_indexes frappe.patches.v12_0.set_default_incoming_email_port frappe.patches.v12_0.update_global_search +frappe.patches.v12_0.setup_tags diff --git a/frappe/patches/v12_0/setup_tags.py b/frappe/patches/v12_0/setup_tags.py new file mode 100644 index 0000000000..33ea39c898 --- /dev/null +++ b/frappe/patches/v12_0/setup_tags.py @@ -0,0 +1,31 @@ +import frappe + +def execute(): + frappe.delete_doc_if_exists("DocType", "Tag Category") + frappe.delete_doc_if_exists("DocType", "Tag Doc Category") + + frappe.reload_doc("desk", "doctype", "tag") + frappe.reload_doc("desk", "doctype", "tag_link") + + tag_list = [] + tag_links = [] + time = frappe.utils.get_datetime() + + for doctype in frappe.get_list("DocType", filters={"istable": 0, "issingle": 0}): + for dt_tags in frappe.db.sql("select `name`, `_user_tags` from `tab{0}`".format(doctype.name), as_dict=True): + tags = dt_tags.get("_user_tags").split(",") if dt_tags.get("_user_tags") else None + if not tags: + continue + + for tag in tags: + if not tag: + continue + + escaped_tag = frappe.db.escape(tag.strip()) + tag_list.append((escaped_tag, time, time, 'Administrator')) + + tag_link_name = frappe.generate_hash(dt_tags.name + escaped_tag, 10), + tag_links.append((tag_link_name, doctype.name, dt_tags.name, escaped_tag, time, time, 'Administrator')) + + frappe.db.bulk_insert("Tag", fields=["name", "creation", "modified", "modified_by"], values=tag_list) + frappe.db.bulk_insert("Tag Link", fields=["name", "document_type", "document_name", "tag", "creation", "modified", "modified_by"], values=tag_links) \ No newline at end of file diff --git a/frappe/public/build.json b/frappe/public/build.json index a88e0f5d54..c59df8034c 100755 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -186,6 +186,7 @@ "public/js/frappe/ui/toolbar/awesome_bar.js", "public/js/frappe/ui/toolbar/energy_points_notifications.js", "public/js/frappe/ui/toolbar/search.js", + "public/js/frappe/ui/toolbar/tag_utils.js", "public/js/frappe/ui/toolbar/search.html", "public/js/frappe/ui/toolbar/search_header.html", "public/js/frappe/ui/toolbar/search_utils.js", diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index db7ca76852..88a3ba9803 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -147,6 +147,8 @@ frappe.Application = Class.extend({ }); }, 300000); // check every 5 minutes } + + this.fetch_tags(); }, setup_frappe_vue() { @@ -599,6 +601,10 @@ frappe.Application = Class.extend({ frappe.show_alert(message); }); }, + + fetch_tags() { + frappe.tags.utils.fetch_tags(); + } }); frappe.get_module = function(m, default_module) { diff --git a/frappe/public/js/frappe/form/sidebar/form_sidebar.js b/frappe/public/js/frappe/form/sidebar/form_sidebar.js index d2cb0f698d..5034b8969c 100644 --- a/frappe/public/js/frappe/form/sidebar/form_sidebar.js +++ b/frappe/public/js/frappe/form/sidebar/form_sidebar.js @@ -76,7 +76,7 @@ frappe.ui.form.Sidebar = Class.extend({ this.frm.shared.refresh(); this.frm.follow.refresh(); this.frm.viewers.refresh(); - this.frm.tags && this.frm.tags.refresh(this.frm.doc._user_tags); + this.frm.tags && this.frm.tags.refresh(this.frm.get_docinfo().tags); this.sidebar.find(".modified-by").html(__("{0} edited this {1}", ["" + frappe.user.full_name(this.frm.doc.modified_by) + "", "
" + comment_when(this.frm.doc.modified)])); @@ -119,7 +119,6 @@ frappe.ui.form.Sidebar = Class.extend({ }, make_tags: function() { - var me = this; if (this.frm.meta.issingle) { this.sidebar.find(".form-tags").toggle(false); return; @@ -129,7 +128,7 @@ frappe.ui.form.Sidebar = Class.extend({ parent: this.sidebar.find(".tag-area"), frm: this.frm, on_change: function(user_tags) { - me.frm.doc._user_tags = user_tags; + this.frm.tags && this.frm.tags.refresh(user_tags); } }); }, diff --git a/frappe/public/js/frappe/list/list_sidebar.js b/frappe/public/js/frappe/list/list_sidebar.js index a43d92591d..0000a3dc73 100644 --- a/frappe/public/js/frappe/list/list_sidebar.js +++ b/frappe/public/js/frappe/list/list_sidebar.js @@ -288,29 +288,7 @@ frappe.views.ListSidebar = class ListSidebar { filters: me.default_filters || [] }, callback: function(r) { - me.defined_category = r.message; - if (r.message.defined_cat) { - me.defined_category = r.message.defined_cat; - me.cats = {}; - //structure the tag categories - for (var i in me.defined_category) { - if (me.cats[me.defined_category[i].category] === undefined) { - me.cats[me.defined_category[i].category] = [me.defined_category[i].tag]; - } else { - me.cats[me.defined_category[i].category].push(me.defined_category[i].tag); - } - me.cat_tags[i] = me.defined_category[i].tag; - } - me.tempstats = r.message.stats; - - $.each(me.cats, function(i, v) { - me.render_stat(i, (me.tempstats || {})["_user_tags"], v); - }); - me.render_stat("_user_tags", (me.tempstats || {})["_user_tags"]); - } else { - //render normal stats - me.render_stat("_user_tags", (r.message.stats || {})["_user_tags"]); - } + me.render_stat("_user_tags", (r.message.stats || {})["_user_tags"]); let stats_dropdown = me.sidebar.find('.list-stats-dropdown'); me.setup_dropdown_search(stats_dropdown,'.stat-label'); } @@ -355,13 +333,14 @@ frappe.views.ListSidebar = class ListSidebar { field: field, stat: stats, sum: sum, - label: field === '_user_tags' ? (tags ? __(label) : __("Tags")) : __(label), + label: field === '_user_tags' ? (tags ? __(label) : __("Tag")) : __(label), }; $(frappe.render_template("list_sidebar_stat", context)) .on("click", ".stat-link", function() { + var doctype = "Tag Link"; var fieldname = $(this).attr('data-field'); var label = $(this).attr('data-label'); - var condition = "like"; + var condition = "="; var existing = me.list_view.filter_area.filter_list.get_filter(fieldname); if(existing) { existing.remove(); @@ -370,7 +349,7 @@ frappe.views.ListSidebar = class ListSidebar { label = "%,%"; condition = "not like"; } - me.list_view.filter_area.filter_list.add_filter(me.list_view.doctype, fieldname, condition, label) + me.list_view.filter_area.filter_list.add_filter(doctype, fieldname, condition, label) .then(function() { me.list_view.refresh(); }); diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 19079e1c24..ac6511fcd6 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -432,7 +432,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { tag_editor.wrapper.on('click', '.tagit-label', (e) => { const $this = $(e.currentTarget); - this.filter_area.add(this.doctype, '_user_tags', '=', $this.text()); + this.filter_area.add('Tag Link', 'tag', '=', $this.text()); }); }); } diff --git a/frappe/public/js/frappe/ui/filters/filter.js b/frappe/public/js/frappe/ui/filters/filter.js index fdf5266216..2a99253b11 100644 --- a/frappe/public/js/frappe/ui/filters/filter.js +++ b/frappe/public/js/frappe/ui/filters/filter.js @@ -173,6 +173,13 @@ frappe.ui.Filter = class { if(this.field) for(let k in this.field.df) cur[k] = this.field.df[k]; let original_docfield = (this.fieldselect.fields_by_name[doctype] || {})[fieldname]; + + if (doctype === "Tag Link" || fieldname === "_user_tags") { + original_docfield = {fieldname: "tag", fieldtype: "Data", label: "Tags", parent: "Tag Link"}; + doctype = "Tag Link"; + condition = "="; + } + if(!original_docfield) { console.warn(`Field ${fieldname} is not selectable.`); this.remove(); diff --git a/frappe/public/js/frappe/ui/tag_editor.js b/frappe/public/js/frappe/ui/tag_editor.js index 453d5014f7..bf5844e790 100644 --- a/frappe/public/js/frappe/ui/tag_editor.js +++ b/frappe/public/js/frappe/ui/tag_editor.js @@ -35,13 +35,14 @@ frappe.ui.TagEditor = Class.extend({ onTagAdd: (tag) => { if(me.initialized && !me.refreshing) { return frappe.call({ - method: 'frappe.desk.tags.add_tag', + method: "frappe.desk.doctype.tag.tag.add_tag", args: me.get_args(tag), callback: function(r) { var user_tags = me.user_tags ? me.user_tags.split(",") : []; user_tags.push(tag) me.user_tags = user_tags.join(","); me.on_change && me.on_change(me.user_tags); + frappe.tags.utils.fetch_tags(); } }); } @@ -49,13 +50,14 @@ frappe.ui.TagEditor = Class.extend({ onTagRemove: (tag) => { if(!me.refreshing) { return frappe.call({ - method: 'frappe.desk.tags.remove_tag', + method: "frappe.desk.doctype.tag.tag.remove_tag", args: me.get_args(tag), callback: function(r) { var user_tags = me.user_tags.split(","); user_tags.splice(user_tags.indexOf(tag), 1); me.user_tags = user_tags.join(","); me.on_change && me.on_change(me.user_tags); + frappe.tags.utils.fetch_tags(); } }); } @@ -82,12 +84,10 @@ frappe.ui.TagEditor = Class.extend({ $input.on("input", function(e) { var value = e.target.value; frappe.call({ - method:"frappe.desk.tags.get_tags", + method: "frappe.desk.doctype.tag.tag.get_tags", args:{ doctype: me.frm.doctype, txt: value.toLowerCase(), - cat_tags: me.list_sidebar ? - JSON.stringify(me.list_sidebar.get_cat_tags()) : '[]' }, callback: function(r) { me.awesomplete.list = r.message; diff --git a/frappe/public/js/frappe/ui/tags.js b/frappe/public/js/frappe/ui/tags.js index 1508e3651e..90c53beae7 100644 --- a/frappe/public/js/frappe/ui/tags.js +++ b/frappe/public/js/frappe/ui/tags.js @@ -67,7 +67,6 @@ frappe.ui.Tags = class { } addTag(label) { - label = toTitle(label); if(label && label!== '' && !this.tagsList.includes(label)) { let $tag = this.getTag(label); this.getListElement($tag).insertBefore(this.$inputWrapper); diff --git a/frappe/public/js/frappe/ui/toolbar/awesome_bar.js b/frappe/public/js/frappe/ui/toolbar/awesome_bar.js index 412117f49e..bfcc5a2d77 100644 --- a/frappe/public/js/frappe/ui/toolbar/awesome_bar.js +++ b/frappe/public/js/frappe/ui/toolbar/awesome_bar.js @@ -1,6 +1,7 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // MIT License. See license.txt frappe.provide('frappe.search'); +frappe.provide('frappe.tags'); frappe.search.AwesomeBar = Class.extend({ setup: function(element) { @@ -140,6 +141,8 @@ frappe.search.AwesomeBar = Class.extend({ __("document type..., e.g. customer")+'\ '+__("Search in a document type")+''+ __("text in document type")+'\ + '+__("Tags")+''+ + __("tag name..., e.g. #tag")+'\ '+__("Open a module or tool")+''+ __("module name...")+'\ '+__("Calculate")+''+ @@ -177,6 +180,9 @@ frappe.search.AwesomeBar = Class.extend({ frappe.search.utils.get_recent_pages(txt || ""), frappe.search.utils.get_executables(txt) ); + if (txt.charAt(0) === "#") { + options = frappe.tags.utils.get_tags(txt); + } var out = this.deduplicate(options); return out.sort(function(a, b) { return b.index - a.index; @@ -215,6 +221,11 @@ frappe.search.AwesomeBar = Class.extend({ make_global_search: function(txt) { var me = this; + + if (txt.charAt(0) === "#") { + return; + } + this.options.push({ label: __("Search for '{0}'", [txt.bold()]), value: __("Search for '{0}'", [txt]), diff --git a/frappe/public/js/frappe/ui/toolbar/search.js b/frappe/public/js/frappe/ui/toolbar/search.js index e27c383ec4..acc623fa20 100644 --- a/frappe/public/js/frappe/ui/toolbar/search.js +++ b/frappe/public/js/frappe/ui/toolbar/search.js @@ -124,6 +124,11 @@ frappe.search.SearchDialog = Class.extend({ // Help results // this.$modal_body.on('click', 'a[data-path]', frappe.help.show_results); this.bind_keyboard_events(); + + // Setup Minimizable functionality + this.search_dialog.minimizable = true; + this.search_dialog.is_minimized = false; + this.search_dialog.$wrapper.find('.btn-modal-minimize').click(() => this.toggle_minimize()); }, bind_keyboard_events: function() { @@ -178,7 +183,17 @@ frappe.search.SearchDialog = Class.extend({ } else { this.$search_modal.find('.loading-state').removeClass('hide'); } + + if (this.current_keyword.charAt(0) === "#") { + this.search = this.searches["tags"]; + } else { + this.search = this.searches["global_search"]; + } + this.search.get_results(keywords, this.parse_results.bind(this)); + if (this.search_dialog.is_minimized) { + this.toggle_minimize(); + } }, parse_results: function(result_sets, keyword) { @@ -308,7 +323,7 @@ frappe.search.SearchDialog = Class.extend({ frappe.route_options = result.route_options; } $result.on('click', (e) => { - this.search_dialog.hide(); + this.toggle_minimize(); if(result.onclick) { result.onclick(result.match); } else { @@ -353,12 +368,25 @@ frappe.search.SearchDialog = Class.extend({ this.$modal_body.find('.more-results.last').slideDown(200, function() {}); }, + get_minimize_btn: function() { + return this.search_dialog.$wrapper.find(".modal-header .btn-modal-minimize"); + }, + + toggle_minimize: function() { + let modal = this.search_dialog.$wrapper.closest('.modal').toggleClass('modal-minimize'); + modal.attr('tabindex') ? modal.removeAttr('tabindex') : modal.attr('tabindex', -1); + this.get_minimize_btn().find('i').toggleClass('octicon-chevron-down').toggleClass('octicon-chevron-up'); + this.search_dialog.is_minimized = !this.search_dialog.is_minimized; + this.on_minimize_toggle && this.on_minimize_toggle(this.search_dialog.is_minimized); + this.search_dialog.header.find('.modal-title').toggleClass('cursor-pointer'); + }, + // Search objects searches: { global_search: { - input_placeholder: __("Global Search"), + input_placeholder: __("Search"), empty_state_text: __("Search for anything"), - no_results_status: (keyword) => __("

No results found for '" + keyword + "' in Global Search

"), + no_results_status: (keyword) => "

" + __("No results found for {0} in Global Search", [keyword]) + "

", get_results: function(keywords, callback) { var start = 0, limit = 1000; @@ -372,6 +400,22 @@ frappe.search.SearchDialog = Class.extend({ }); } }, + tags: { + input_placeholder: __("Search"), + empty_state_text: __("Search for anything"), + no_results_status: (keyword) => "

" + __("No documents found tagged with {0}", [keyword]) + "

", + + get_results: function(keywords, callback) { + var results = frappe.search.utils.get_nav_results(keywords); + frappe.tags.utils.get_tag_results(keywords) + .then(function(global_results) { + results = results.concat(global_results); + callback(results, keywords); + }, function (err) { + console.error(err); + }); + } + }, }, }); \ No newline at end of file diff --git a/frappe/public/js/frappe/ui/toolbar/search_header.html b/frappe/public/js/frappe/ui/toolbar/search_header.html index f36e87c7ea..86c3fa1783 100644 --- a/frappe/public/js/frappe/ui/toolbar/search_header.html +++ b/frappe/public/js/frappe/ui/toolbar/search_header.html @@ -1,6 +1,12 @@
- - -

{%= __("Searching")%} ...

- + + +

{%= __("Searching")%} ...

+ + + +
\ No newline at end of file diff --git a/frappe/public/js/frappe/ui/toolbar/tag_utils.js b/frappe/public/js/frappe/ui/toolbar/tag_utils.js new file mode 100644 index 0000000000..0982260624 --- /dev/null +++ b/frappe/public/js/frappe/ui/toolbar/tag_utils.js @@ -0,0 +1,108 @@ +// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors +// MIT License. See license.txt + +frappe.provide("frappe.tags"); + +frappe.tags.utils = { + get_tags: function(txt) { + txt = txt.slice(1); + let out = []; + + for (let i in frappe.tags.tags) { + let tag = frappe.tags.tags[i]; + let level = frappe.search.utils.fuzzy_search(txt, tag); + if (level) { + out.push({ + type: "Tag", + label: __("#{0}", [frappe.search.utils.bolden_match_part(__(tag), txt)]), + value: __("#{0}", [__(tag)]), + index: 1 + level, + match: tag, + onclick() { + // Use Global Search Dialog for tag search too. + frappe.searchdialog.search.init_search("#".concat(tag), "tags"); + } + }); + } + } + + return out; + }, + + fetch_tags() { + frappe.call({ + method: "frappe.desk.doctype.tag.tag.get_tags_list_for_awesomebar", + callback: function(r) { + if (r && r.message) { + frappe.tags.tags = $.extend([], r.message); + } + } + }); + }, + + get_tag_results: function(tag) { + function get_results_sets(data) { + var results_sets = [], result, set; + function get_existing_set(doctype) { + return results_sets.find(function(set) { + return set.title === doctype; + }); + } + + function make_description(content) { + var field_length = 110; + var field_value = null; + if (content.length > field_length) { + field_value = content.slice(0, field_length) + "..."; + } else { + var length = content.length; + field_value = content.slice(0, length) + "..."; + } + return field_value; + } + + data.forEach(function(d) { + // more properties + var description = ""; + if (d.content) { + description = make_description(d.content); + } + result = { + label: d.name, + value: d.name, + description: description, + route: ['Form', d.doctype, d.name], + + }; + set = get_existing_set(d.doctype); + if (set) { + set.results.push(result); + } else { + set = { + title: d.doctype, + results: [result], + fetch_type: "Global" + }; + results_sets.push(set); + } + + }); + return results_sets; + } + return new Promise(function(resolve) { + frappe.call({ + method: "frappe.desk.doctype.tag.tag.get_documents_for_tag", + args: { + tag: tag + }, + callback: function(r) { + if (r.message) { + resolve(get_results_sets(r.message)); + } else { + resolve([]); + } + } + }); + }); + }, +}; diff --git a/frappe/utils/global_search.py b/frappe/utils/global_search.py index 76435305ee..447466770d 100644 --- a/frappe/utils/global_search.py +++ b/frappe/utils/global_search.py @@ -242,10 +242,6 @@ def update_global_search(doc): if doc.get(field.fieldname) and field.fieldtype not in frappe.model.table_fields: content.append(get_formatted_value(doc.get(field.fieldname), field)) - tags = (doc.get('_user_tags') or '').strip() - if tags: - content.extend(list(filter(lambda x: x, tags.split(',')))) - # Get children for child in doc.meta.get_table_fields(): for d in doc.get(child.fieldname):