diff --git a/frappe/boot.py b/frappe/boot.py index 1bcdca6a65..c46709d3d7 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -105,8 +105,8 @@ def load_conf_settings(bootinfo): if key in conf: bootinfo[key] = conf.get(key) def load_desktop_data(bootinfo): - from frappe.desk.doctype.workspace.workspace import get_pages - bootinfo.allowed_workspaces = get_pages().get('pages') + from frappe.desk.desktop import get_wspace_sidebar_items + bootinfo.allowed_workspaces = get_wspace_sidebar_items().get('pages') bootinfo.module_page_map = get_controller("Workspace").get_module_page_map() bootinfo.dashboards = frappe.get_all("Dashboard") diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py index ac0db1b2d3..14b735db10 100644 --- a/frappe/desk/desktop.py +++ b/frappe/desk/desktop.py @@ -3,10 +3,10 @@ # Author - Shivam Mishra import frappe -import json from json import loads, dumps from frappe import _, DoesNotExistError, ValidationError, _dict from frappe.boot import get_allowed_pages, get_allowed_reports +from frappe.core.doctype.custom_role.custom_role import get_custom_allowed_roles from functools import wraps from frappe.cache_manager import ( build_domain_restriced_doctype_cache, @@ -35,13 +35,14 @@ class Workspace: self.extended_links = [] self.extended_charts = [] self.extended_shortcuts = [] + self.workspace_manager = "Workspace Manager" in frappe.get_roles() self.user = frappe.get_user() self.allowed_modules = self.get_cached('user_allowed_modules', self.get_allowed_modules) - self.doc = self.get_page_for_user() + self.doc = frappe.get_cached_doc("Workspace", self.page_name) - if self.doc.module and self.doc.module not in self.allowed_modules: + if self.doc and self.doc.module and self.doc.module not in self.allowed_modules and not self.workspace_manager: raise frappe.PermissionError self.can_read = self.get_cached('user_perm_can_read', self.get_can_read_items) @@ -58,8 +59,8 @@ class Workspace: self.restricted_pages = frappe.cache().get_value("domain_restricted_pages") or build_domain_restriced_page_cache() def is_page_allowed(self): - cards = self.doc.get_link_groups() + get_custom_reports_and_doctypes(self.doc.module) + self.extended_links - shortcuts = self.doc.shortcuts + self.extended_shortcuts + cards = self.doc.get_link_groups() + get_custom_reports_and_doctypes(self.doc.module) + shortcuts = self.doc.shortcuts for section in cards: links = loads(section.get('links')) if isinstance(section.get('links'), str) else section.get('links') @@ -77,8 +78,28 @@ class Workspace: if self.is_item_allowed(item.link_to, item.type) and _in_active_domains(item): return True + if not shortcuts and not self.doc.links: + return True + return False + def is_permitted(self): + """Returns true if Has Role is not set or the user is allowed.""" + from frappe.utils import has_common + + allowed = [d.role for d in frappe.get_all("Has Role", fields=["role"], filters={"parent": self.doc.name})] + + custom_roles = get_custom_allowed_roles('page', self.doc.name) + allowed.extend(custom_roles) + + if not allowed: + return True + + roles = frappe.get_roles() + + if has_common(roles, allowed): + return True + def get_cached(self, cache_key, fallback_fn): _cache = frappe.cache() @@ -104,24 +125,6 @@ class Workspace: return self.user.allow_modules - def get_page_for_user(self): - if self.public_page: - filters = { - 'label': self.page_name, - 'public': 1 - } - public_pages = frappe.get_all("Workspace", filters=filters, limit=1) - if public_pages: - return frappe.get_cached_doc("Workspace", public_pages[0]) - - filters = { - 'label': self.page_title + "-" + frappe.session.user, - 'for_user': frappe.session.user - } - user_pages = frappe.get_all("Workspace", filters=filters, limit=1) - if user_pages: - return frappe.get_cached_doc("Workspace", user_pages[0]) - def get_onboarding_doc(self): # Check if onboarding is enabled if not frappe.get_system_settings("enable_onboarding"): @@ -372,39 +375,45 @@ def get_desktop_page(page): return {} @frappe.whitelist() -def get_desk_sidebar_items(): +def get_wspace_sidebar_items(): """Get list of sidebar items for desk""" + has_access = "Workspace Manager" in frappe.get_roles() # don't get domain restricted pages blocked_modules = frappe.get_doc('User', frappe.session.user).get_blocked_modules() + blocked_modules.append('Dummy Module') filters = { 'restrict_to_domain': ['in', frappe.get_active_domains()], - 'extends_another_page': 0, - 'for_user': '', 'module': ['not in', blocked_modules] } - if not frappe.local.conf.developer_mode: - filters['developer_mode_only'] = '0' + if has_access: + filters = [] - # pages sorted based on pinned to top and then by name - order_by = "pin_to_top desc, pin_to_bottom asc, name asc" - all_pages = frappe.get_all("Workspace", fields=["name", "title", "public", "module", "icon"], - filters=filters, order_by=order_by, ignore_permissions=True) + # pages sorted based on sequence id + order_by = "sequence_id asc" + fields = ["name", "title", "for_user", "parent_page", "content", "public", "module", "icon"] + all_pages = frappe.get_all("Workspace", fields=fields, filters=filters, order_by=order_by, ignore_permissions=True) pages = [] + private_pages = [] # Filter Page based on Permission for page in all_pages: try: wspace = Workspace(page) - if wspace.is_page_allowed(): - pages.append(page) + if wspace.is_permitted() and wspace.is_page_allowed() or has_access: + if page.public: + pages.append(page) + elif page.for_user == frappe.session.user: + private_pages.append(page) page['label'] = _(page.get('name')) except frappe.PermissionError: pass + if private_pages: + pages.extend(private_pages) - return pages + return {'pages': pages, 'has_access': has_access} def get_table_with_counts(): counts = frappe.cache().get_value("information_schema:counts") @@ -575,7 +584,7 @@ def clean_up(original_page, blocks): for wid in ['shortcut', 'card', 'chart']: # get list of widget's name from blocks - page_widgets[wid] = [x['data'][wid + '_name'] for x in json.loads(blocks) if x['type'] == wid] + page_widgets[wid] = [x['data'][wid + '_name'] for x in loads(blocks) if x['type'] == wid] # shortcut & chart cleanup for wid in ['shortcut', 'chart']: diff --git a/frappe/desk/doctype/workspace/workspace.json b/frappe/desk/doctype/workspace/workspace.json index b531274884..8d95c9987f 100644 --- a/frappe/desk/doctype/workspace/workspace.json +++ b/frappe/desk/doctype/workspace/workspace.json @@ -38,7 +38,8 @@ "shortcuts", "section_break_18", "cards_label", - "links" + "links", + "roles" ], "fields": [ { @@ -252,10 +253,16 @@ "fieldname": "sequence_id", "fieldtype": "Int", "label": "Sequence Id" + }, + { + "fieldname": "roles", + "fieldtype": "Table", + "label": "Roles", + "options": "Has Role" } ], "links": [], - "modified": "2021-07-05 17:41:36.272294", + "modified": "2021-07-15 14:20:37.623926", "modified_by": "Administrator", "module": "Desk", "name": "Workspace", @@ -269,7 +276,7 @@ "print": 1, "read": 1, "report": 1, - "role": "System Manager", + "role": "Workspace Manager", "share": 1, "write": 1 }, diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py index 93a7b13d17..2305155592 100644 --- a/frappe/desk/doctype/workspace/workspace.py +++ b/frappe/desk/doctype/workspace/workspace.py @@ -3,7 +3,6 @@ # For license information, please see license.txt import frappe -import json from frappe import _ from frappe.modules.export_file import export_to_files from frappe.model.document import Document @@ -26,8 +25,8 @@ class Workspace(Document): frappe.throw(_("You can only have one default page that extends a particular standard page.")) def on_update(self): - # if disable_saving_as_standard(): - # return + if disable_saving_as_standard(): + return if frappe.conf.developer_mode and self.module and self.public: export_to_files(record_list=[['Workspace', self.name]], record_module=self.module) @@ -159,20 +158,7 @@ def get_report_type(report): @frappe.whitelist() -def get_pages(): - has_access = "System Manager" in frappe.get_roles() - fields = ['name', 'title', 'icon', 'public', 'parent_page', 'content'] - - pages = get_page_list(fields, {'public': 1}) - private_pages = get_page_list(fields, {'for_user': frappe.session.user}) - - if private_pages: - pages.extend(private_pages) - - return {'pages': pages, 'has_access': has_access} - -@frappe.whitelist() -def save_page(title, parent, public, sb_items, deleted_pages, new_widgets, blocks, save): +def save_page(title, parent, public, sb_public_items, sb_private_items, deleted_pages, new_widgets, blocks, save): save = frappe.parse_json(save) public = frappe.parse_json(public) if save: @@ -206,20 +192,20 @@ def save_page(title, parent, public, sb_items, deleted_pages, new_widgets, block doc.content = blocks doc.save(ignore_permissions=True) - if json.loads(new_widgets): + if loads(new_widgets): save_new_widget(doc, title, blocks, new_widgets) - if json.loads(sb_items): - sort_pages(json.loads(sb_items)) + if loads(sb_public_items) or loads(sb_private_items): + sort_pages(loads(sb_public_items), loads(sb_private_items)) - if json.loads(deleted_pages): - return delete_pages(json.loads(deleted_pages)) + if loads(deleted_pages): + return delete_pages(loads(deleted_pages)) return {"name": title, "public": public} def delete_pages(deleted_pages): for page in deleted_pages: - if page.get("public") and "System Manager" not in frappe.get_roles(): + if page.get("public") and "Workspace Manager" not in frappe.get_roles(): return {"name": page.get("title"), "public": 1} if frappe.db.exists("Workspace", page.get("name")): @@ -227,17 +213,15 @@ def delete_pages(deleted_pages): return {"name": "Home", "public": 1} -def sort_pages(sb_items): - public_pages = [page for page in sb_items if page.get('public')=='1'] - private_pages = [page for page in sb_items if page.get('public')=='0'] - +def sort_pages(sb_public_items, sb_private_items): wspace_public_pages = get_page_list(['name', 'title'], {'public': 1}) wspace_private_pages = get_page_list(['name', 'title'], {'for_user': frappe.session.user}) - sort_page(wspace_private_pages, private_pages) + if sb_private_items: + sort_page(wspace_private_pages, sb_private_items) - if "System Manager" in frappe.get_roles(): - sort_page(wspace_public_pages, public_pages) + if sb_public_items and "Workspace Manager" in frappe.get_roles(): + sort_page(wspace_public_pages, sb_public_items) def sort_page(wspace_pages, pages): for seq, d in enumerate(pages): diff --git a/frappe/patches/v13_0/update_workspace2.py b/frappe/patches/v13_0/update_workspace2.py index 3561a8ff3e..53abe2994b 100644 --- a/frappe/patches/v13_0/update_workspace2.py +++ b/frappe/patches/v13_0/update_workspace2.py @@ -24,7 +24,8 @@ def create_content(doc): del doc.charts[doc.charts.index(l)] if doc.shortcuts: invalid_links = [] - content.append({"type":"spacer","data":{"col":12,"pt":0,"pr":0,"pb":0,"pl":0}}) + if doc.charts: + content.append({"type":"spacer","data":{"col":12,"pt":0,"pr":0,"pb":0,"pl":0}}) content.append({"type":"header","data":{"text":doc.shortcuts_label or _("Your Shortcuts"),"level":4,"col":12,"pt":0,"pr":0,"pb":0,"pl":0}}) for s in doc.shortcuts: if s.get_invalid_links()[0]: diff --git a/frappe/public/js/frappe/views/workspace/workspace.js b/frappe/public/js/frappe/views/workspace/workspace.js index c65c8aa232..512a56c1d1 100644 --- a/frappe/public/js/frappe/views/workspace/workspace.js +++ b/frappe/public/js/frappe/views/workspace/workspace.js @@ -22,7 +22,8 @@ frappe.views.Workspace = class Workspace { this.page = wrapper.page; this.isReadOnly = true; this.new_page = null; - this.sorted_sidebar_items = []; + this.sorted_public_items = []; + this.sorted_private_items = []; this.deleted_sidebar_items = []; this.current_page = {}; this.sidebar_items = { @@ -30,8 +31,8 @@ frappe.views.Workspace = class Workspace { 'private': {} }; this.sidebar_categories = [ - "Public", - frappe.user.first_name() + 'Public', + frappe.user.first_name() || 'Private' ]; this.tools = { header: { @@ -113,7 +114,7 @@ frappe.views.Workspace = class Workspace { } get_pages() { - return frappe.xcall("frappe.desk.doctype.workspace.workspace.get_pages"); + return frappe.xcall("frappe.desk.desktop.get_wspace_sidebar_items"); } sidebar_item_container(item) { @@ -124,7 +125,7 @@ frappe.views.Workspace = class Workspace { href="/app/${item.public ? frappe.router.slug(item.title) : 'private/'+frappe.router.slug(item.title) }" class="item-anchor ${item.is_editable ? "" : "block-click" }" title="${item.title}" > - ${frappe.utils.icon(item.icon || "folder-normal", "md")} + ${frappe.utils.icon(item.icon || "folder-normal", "md")} ${item.title} @@ -146,7 +147,9 @@ frappe.views.Workspace = class Workspace { this.build_sidebar_section(category, root_pages); }); - this.sidebar.find('.selected')[0].scrollIntoView(); + // Scroll sidebar to selected page if it is not in viewport. + !frappe.dom.is_element_in_viewport(this.sidebar.find('.selected')) + && this.sidebar.find('.selected')[0].scrollIntoView(); } build_sidebar_section(title, root_pages) { @@ -448,26 +451,36 @@ frappe.views.Workspace = class Workspace { animation: 150, fallbackOnBody: true, swapThreshold: 0.65, - onEnd: function () { - me.prepare_sorted_sidebar(); + onEnd: function (evt) { + let is_public = $(evt.item).attr('item-public') == '1'; + me.prepare_sorted_sidebar(is_public); } }); }); } - prepare_sorted_sidebar() { - this.sorted_sidebar_items = []; - for (let page of $('.standard-sidebar-section').find('.sidebar-item-container')) { + prepare_sorted_sidebar(is_public) { + if (is_public) { + this.sorted_public_items = this.sort_sidebar(this.sidebar.find('.standard-sidebar-section').first()); + } else { + this.sorted_private_items = this.sort_sidebar(this.sidebar.find('.standard-sidebar-section').last()); + } + } + + sort_sidebar($sidebar_section) { + let sorted_items = []; + for (let page of $sidebar_section.find('.sidebar-item-container')) { let parent_page = ""; if (page.closest('.nested-container').classList.contains('sidebar-child-item')) { parent_page = page.parentElement.parentElement.attributes["item-name"].value; } - this.sorted_sidebar_items.push({ + sorted_items.push({ title: page.attributes['item-name'].value, parent_page: parent_page, public: page.attributes['item-public'].value }); } + return sorted_items; } make_blocks_sortable() { @@ -542,11 +555,16 @@ frappe.views.Workspace = class Workspace { this.show_sidebar_actions(); this.make_sidebar_sortable(); this.make_blocks_sortable(); - this.prepare_sorted_sidebar(); + this.prepare_sorted_sidebar(values.is_public); }); } }); d.show(); + + // to enable focusing on input field when modal is open. + d.$wrapper.on('shown.bs.modal', function() { + $(document).off('focusin.modal'); + }); } validate_page(values) { @@ -652,7 +670,8 @@ frappe.views.Workspace = class Workspace { title: me.title, parent: me.parent || '', public: me.public || 0, - sb_items: me.sorted_sidebar_items, + sb_public_items: me.sorted_public_items, + sb_private_items: me.sorted_private_items, deleted_pages: me.deleted_sidebar_items, new_widgets: new_widgets, blocks: JSON.stringify(outputData.blocks), @@ -666,7 +685,8 @@ frappe.views.Workspace = class Workspace { me.title = ''; me.parent = ''; me.public = false; - me.sorted_sidebar_items = []; + me.sorted_public_items = []; + me.sorted_private_items = []; me.deleted_sidebar_items = []; me.reload(); }