Merge branch 'develop' of github.com:frappe/frappe into dashdash

This commit is contained in:
Shivam Mishra 2020-03-06 12:11:50 +05:30
commit b15df49225
181 changed files with 7614 additions and 3392 deletions

8
.snyk
View file

@ -1,5 +1,5 @@
# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
version: v1.14.1
version: v1.13.5
# ignores vulnerabilities until expiry date; change duration by modifying expiry date
ignore:
SNYK-JS-AWESOMPLETE-174474:
@ -10,6 +10,10 @@ ignore:
- showdown > yargs > os-locale > mem:
reason: No patch available
expires: '2019-06-11T14:12:04.995Z'
SNYK-PYTHON-PYYAML-550022:
- '*':
reason: Project is not directly dependant on the package
expires: 2021-04-01T18:02:21.256Z
# patches apply the minimum changes required to fix a vulnerability
patch:
'npm:extend:20180424':
@ -18,5 +22,3 @@ patch:
SNYK-JS-LODASH-450202:
- frappe-datatable > lodash:
patched: '2020-01-31T01:33:09.889Z'
- snyk > snyk-nuget-plugin > dotnet-deps-parser > lodash:
patched: '2020-02-21T02:41:07.568Z'

View file

@ -2,7 +2,7 @@ context('API Resources', () => {
before(() => {
cy.visit('/login');
cy.login();
cy.visit('/desk');
cy.visit('/desk#workspace/Website');
});
it('Creates two Comments', () => {

View file

@ -2,7 +2,7 @@ context('Awesome Bar', () => {
before(() => {
cy.visit('/login');
cy.login();
cy.visit('/desk');
cy.visit('/desk#workspace/Website');
});
beforeEach(() => {

View file

@ -1,7 +1,7 @@
context('Control Barcode', () => {
beforeEach(() => {
cy.login();
cy.visit('/desk');
cy.visit('/desk#workspace/Website');
});
function get_dialog_with_barcode() {

View file

@ -1,7 +1,7 @@
context('Control Link', () => {
beforeEach(() => {
cy.login();
cy.visit('/desk');
cy.visit('/desk#workspace/Website');
cy.create_records({
doctype: 'ToDo',
description: 'this is a test todo for link'

View file

@ -1,7 +1,7 @@
context('Control Rating', () => {
before(() => {
cy.login();
cy.visit('/desk');
cy.visit('/desk#workspace/Website');
});
function get_dialog_with_rating() {

View file

@ -4,7 +4,7 @@ const doctype_name = datetime_doctype.name;
context('Control Date, Time and DateTime', () => {
before(() => {
cy.login();
cy.visit('/desk');
cy.visit('/desk#workspace/Website');
cy.insert_doc('DocType', datetime_doctype, true);
});

View file

@ -1,11 +1,11 @@
context('Depends On', () => {
beforeEach(() => {
cy.login();
cy.visit('/desk');
cy.visit('/desk#workspace/Website');
});
before(() => {
cy.login();
cy.visit('/desk');
cy.visit('/desk#workspace/Website');
cy.window().its('frappe').then(frappe => {
frappe.call('frappe.tests.ui_test_helpers.create_doctype', {
name: 'Test Depends On',

View file

@ -1,7 +1,7 @@
context('FileUploader', () => {
before(() => {
cy.login();
cy.visit('/desk');
cy.visit('/desk#workspace/Website');
});
function open_upload_dialog() {

View file

@ -1,13 +1,13 @@
context('Form', () => {
before(() => {
cy.login();
cy.visit('/desk');
cy.visit('/desk#workspace/Website');
cy.window().its('frappe').then(frappe => {
frappe.call("frappe.tests.ui_test_helpers.create_contact_records");
});
});
beforeEach(() => {
cy.visit('/desk');
cy.visit('/desk#workspace/Website');
});
it('create a new form', () => {
cy.visit('/desk#Form/ToDo/New ToDo 1');

View file

@ -1,11 +1,11 @@
context('Grid Pagination', () => {
beforeEach(() => {
cy.login();
cy.visit('/desk');
cy.visit('/desk#workspace/Website');
});
before(() => {
cy.login();
cy.visit('/desk');
cy.visit('/desk#workspace/Website');
cy.window().its('frappe').then(frappe => {
frappe.call("frappe.tests.ui_test_helpers.create_contact_phone_nos_records");
});

View file

@ -1,7 +1,7 @@
context('List View', () => {
before(() => {
cy.login();
cy.visit('/desk');
cy.visit('/desk#workspace/Website');
return cy.window().its('frappe').then(frappe => {
return frappe.xcall("frappe.tests.ui_test_helpers.setup_workflow");
});

View file

@ -1,7 +1,7 @@
context('List View Settings', () => {
beforeEach(() => {
cy.login();
cy.visit('/desk');
cy.visit('/desk#workspace/Website');
});
it('Default settings', () => {
cy.visit('/desk#List/DocType/List');

View file

@ -1,7 +1,7 @@
context('Form', () => {
before(() => {
cy.login();
cy.visit('/desk');
cy.visit('/desk#workspace/Website');
});
it('add custom column in report', () => {

View file

@ -4,7 +4,7 @@ context('Recorder', () => {
});
it('Navigate to Recorder', () => {
cy.visit('/desk');
cy.visit('/desk#workspace/Website');
cy.awesomebar('recorder');
cy.get('h1').should('contain', 'Recorder');
cy.location('hash').should('eq', '#recorder');

View file

@ -1,11 +1,11 @@
context('Relative Timeframe', () => {
beforeEach(() => {
cy.login();
cy.visit('/desk');
cy.visit('/desk#workspace/Website');
});
before(() => {
cy.login();
cy.visit('/desk');
cy.visit('/desk#workspace/Website');
cy.window().its('frappe').then(frappe => {
frappe.call("frappe.tests.ui_test_helpers.create_todo_records");
});

View file

@ -4,7 +4,7 @@ const doctype_name = custom_submittable_doctype.name;
context('Report View', () => {
before(() => {
cy.login();
cy.visit('/desk');
cy.visit('/desk#workspace/Website');
cy.insert_doc('DocType', custom_submittable_doctype, true);
cy.clear_cache();
cy.insert_doc(doctype_name, {

View file

@ -605,7 +605,7 @@ def has_permission(doctype=None, ptype="read", doc=None, user=None, verbose=Fals
doctype = doc.doctype
import frappe.permissions
out = frappe.permissions.has_permission(doctype, ptype, doc=doc, verbose=verbose, user=user)
out = frappe.permissions.has_permission(doctype, ptype, doc=doc, verbose=verbose, user=user, raise_exception=throw)
if throw and not out:
if doc:
frappe.throw(_("No permission for {0}").format(doc.doctype + " " + doc.name))

View file

@ -25,6 +25,7 @@ from frappe.utils.error import make_error_snapshot
from frappe.core.doctype.comment.comment import update_comments_in_parent_after_request
from frappe import _
import frappe.recorder
import frappe.monitor
local_manager = LocalManager([frappe.local])
@ -52,6 +53,7 @@ def application(request):
init_request(request)
frappe.recorder.record()
frappe.monitor.start()
if frappe.local.form_dict.cmd:
response = frappe.handler.handle()
@ -91,6 +93,7 @@ def application(request):
if response and hasattr(frappe.local, 'cookie_manager'):
frappe.local.cookie_manager.flush_cookies(response=response)
frappe.monitor.stop(response)
frappe.recorder.dump()
frappe.destroy()

View file

@ -0,0 +1,65 @@
{
"cards": [
{
"icon": "octicon octicon-briefcase",
"links": "[\n {\n \"description\": \"Documents assigned to you and by you.\",\n \"label\": \"To Do\",\n \"name\": \"ToDo\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Event and other calendars.\",\n \"label\": \"Calendar\",\n \"link\": \"List/Event/Calendar\",\n \"name\": \"Event\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Private and public Notes.\",\n \"label\": \"Note\",\n \"name\": \"Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Files\",\n \"name\": \"File\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Activity log of all users.\",\n \"label\": \"Activity\",\n \"name\": \"activity\",\n \"type\": \"page\"\n }\n]",
"title": "Tools"
},
{
"links": "[\n {\n \"description\": \"Newsletters to contacts, leads.\",\n \"label\": \"Newsletter\",\n \"name\": \"Newsletter\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Email Group List\",\n \"label\": \"Email Group\",\n \"name\": \"Email Group\",\n \"type\": \"doctype\"\n }\n]",
"title": "Email"
},
{
"icon": "fa fa-cog",
"links": "[\n {\n \"type\": \"doctype\",\n \"name\": \"Assignment Rule\",\n \"description\": \"Set up rules for user assignments.\",\n \"label\": \"Assignment Rule\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Milestone\",\n \"description\": \"Tracks milestones on the lifecycle of a document if it undergoes multiple stages.\",\n \"label\": \"Milestone\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Auto Repeat\",\n \"description\": \"Automatically generates recurring documents.\",\n \"label\": \"Auto Repeat\"\n }\n]",
"title": "Automation"
},
{
"links": "[\n {\n \"type\": \"doctype\",\n \"name\": \"Event Producer\",\n \"description\": \"The site you want to subscribe to for consuming events.\",\n \"label\": \"Event Producer\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Event Consumer\",\n \"description\": \"The site which is consuming your events.\",\n \"label\": \"Event Consumer\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Event Update Log\",\n \"description\": \"Maintains a Log of all inserts, updates and deletions on Event Producer site for documents that have consumers.\",\n \"label\": \"Event Update Log\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Event Sync Log\",\n \"description\": \"Maintains a log of every event consumed along with the status of the sync and a Resync button in case sync fails.\",\n \"label\": \"Event Sync Log\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Document Type Mapping\",\n \"description\": \"The mapping configuration between two doctypes.\",\n \"label\": \"Document Type Mapping\"\n }\n]",
"title": "Event Streaming"
}
],
"category": "Administration",
"charts": [],
"creation": "2020-03-02 14:53:24.980279",
"developer_mode_only": 0,
"disable_user_customization": 0,
"docstatus": 0,
"doctype": "Desk Page",
"idx": 0,
"label": "Tools",
"modified": "2020-03-05 11:27:26.106013",
"modified_by": "Administrator",
"module": "Automation",
"name": "Tools",
"owner": "Administrator",
"pin_to_bottom": 0,
"pin_to_top": 0,
"shortcuts": [
{
"is_query_report": 0,
"link_to": "ToDo",
"type": "DocType"
},
{
"is_query_report": 0,
"link_to": "Note",
"type": "DocType"
},
{
"is_query_report": 0,
"link_to": "File",
"type": "DocType"
},
{
"is_query_report": 0,
"link_to": "Assignment Rule",
"type": "DocType"
},
{
"is_query_report": 0,
"link_to": "Auto Repeat",
"type": "DocType"
}
]
}

View file

@ -235,7 +235,7 @@ def add_home_page(bootinfo, docs):
except (frappe.DoesNotExistError, frappe.PermissionError):
if frappe.message_log:
frappe.message_log.pop()
page = frappe.desk.desk_page.get('desktop')
page = frappe.desk.desk_page.get('workspace')
bootinfo['home_page'] = page.name
docs.append(page)

View file

@ -0,0 +1,70 @@
{
"cards": [
{
"icon": "fa fa-th",
"links": "[\n {\n \"description\": \"Import Data from CSV / Excel files.\",\n \"icon\": \"octicon octicon-cloud-upload\",\n \"label\": \"Import Data\",\n \"name\": \"Data Import\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Export Data in CSV / Excel format.\",\n \"icon\": \"octicon octicon-cloud-upload\",\n \"label\": \"Export Data\",\n \"name\": \"Data Export\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Update many values at one time.\",\n \"hide_count\": true,\n \"label\": \"Bulk Update\",\n \"name\": \"Bulk Update\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"List of backups available for download\",\n \"icon\": \"fa fa-download\",\n \"label\": \"Download Backups\",\n \"name\": \"backups\",\n \"type\": \"page\"\n },\n {\n \"description\": \"Restore or permanently delete a document.\",\n \"label\": \"Deleted Documents\",\n \"name\": \"Deleted Document\",\n \"type\": \"doctype\"\n }\n]",
"title": "Data"
},
{
"icon": "fa fa-envelope",
"links": "[\n {\n \"description\": \"Add / Manage Email Accounts.\",\n \"label\": \"Email Account\",\n \"name\": \"Email Account\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Add / Manage Email Domains.\",\n \"label\": \"Email Domain\",\n \"name\": \"Email Domain\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Setup Notifications based on various criteria.\",\n \"label\": \"Notification\",\n \"name\": \"Notification\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Email Templates for common queries.\",\n \"label\": \"Email Template\",\n \"name\": \"Email Template\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Setup Reports to be emailed at regular intervals\",\n \"label\": \"Auto Email Report\",\n \"name\": \"Auto Email Report\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Create and manage newsletter\",\n \"label\": \"Newsletter\",\n \"name\": \"Newsletter\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Configure notifications for mentions, assignments, energy points and more.\",\n \"label\": \"Notification Settings\",\n \"name\": \"Notification Settings\",\n \"route\": \"Form/Notification Settings/Administrator\",\n \"type\": \"doctype\"\n }\n]",
"title": "Email / Notifications"
},
{
"icon": "fa fa-globe",
"links": "[\n {\n \"description\": \"Setup of top navigation bar, footer and logo.\",\n \"label\": \"Website Settings\",\n \"name\": \"Website Settings\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"List of themes for Website.\",\n \"label\": \"Website Theme\",\n \"name\": \"Website Theme\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Javascript to append to the head section of the page.\",\n \"label\": \"Website Script\",\n \"name\": \"Website Script\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Settings for About Us Page.\",\n \"label\": \"About Us Settings\",\n \"name\": \"About Us Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Settings for Contact Us Page.\",\n \"label\": \"Contact Us Settings\",\n \"name\": \"Contact Us Settings\",\n \"type\": \"doctype\"\n }\n]",
"title": "Website"
},
{
"icon": "fa fa-wrench",
"links": "[\n {\n \"description\": \"Language, Date and Time settings\",\n \"hide_count\": true,\n \"label\": \"System Settings\",\n \"name\": \"System Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Log of error on automated events (scheduler).\",\n \"label\": \"Error Log\",\n \"name\": \"Error Log\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Log of error during requests.\",\n \"label\": \"Error Snapshot\",\n \"name\": \"Error Snapshot\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Enable / Disable Domains\",\n \"hide_count\": true,\n \"label\": \"Domain Settings\",\n \"name\": \"Domain Settings\",\n \"type\": \"doctype\"\n }\n]",
"title": "Core"
},
{
"icon": "fa fa-print",
"links": "[\n {\n \"description\": \"Drag and Drop tool to build and customize Print Formats.\",\n \"label\": \"Print Format Builder\",\n \"name\": \"print-format-builder\",\n \"type\": \"page\"\n },\n {\n \"description\": \"Set default format, page size, print style etc.\",\n \"label\": \"Print Settings\",\n \"name\": \"Print Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Customized HTML Templates for printing transactions.\",\n \"label\": \"Print Format\",\n \"name\": \"Print Format\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Stylesheets for Print Formats\",\n \"label\": \"Print Style\",\n \"name\": \"Print Style\",\n \"type\": \"doctype\"\n }\n]",
"title": "Printing"
},
{
"icon": "fa fa-random",
"links": "[\n {\n \"description\": \"Define workflows for forms.\",\n \"label\": \"Workflow\",\n \"name\": \"Workflow\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"States for workflow (e.g. Draft, Approved, Cancelled).\",\n \"label\": \"Workflow State\",\n \"name\": \"Workflow State\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Actions for workflow (e.g. Approve, Cancel).\",\n \"label\": \"Workflow Action\",\n \"name\": \"Workflow Action\",\n \"type\": \"doctype\"\n }\n]",
"title": "Workflow"
}
],
"category": "Administration",
"charts": [],
"creation": "2020-03-02 15:09:40.527211",
"developer_mode_only": 0,
"disable_user_customization": 1,
"docstatus": 0,
"doctype": "Desk Page",
"idx": 0,
"label": "Settings",
"modified": "2020-03-05 11:27:25.766522",
"modified_by": "Administrator",
"module": "Core",
"name": "Settings",
"owner": "Administrator",
"pin_to_bottom": 0,
"pin_to_top": 1,
"shortcuts": [
{
"icon": "octicon octicon-settings",
"is_query_report": 0,
"link_to": "System Settings",
"type": "DocType"
},
{
"icon": "fa fa-print",
"is_query_report": 0,
"link_to": "Print Settings",
"type": "DocType"
},
{
"icon": "fa fa-globe",
"is_query_report": 0,
"link_to": "Website Settings",
"type": "DocType"
}
]
}

View file

@ -0,0 +1,57 @@
{
"cards": [
{
"icon": "fa fa-group",
"links": "[\n {\n \"description\": \"System and Website Users\",\n \"label\": \"User\",\n \"name\": \"User\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"User Roles\",\n \"label\": \"Role\",\n \"name\": \"Role\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Role Profile\",\n \"label\": \"Role Profile\",\n \"name\": \"Role Profile\",\n \"type\": \"doctype\"\n }\n]",
"title": "Users"
},
{
"icon": "fa fa-group",
"links": "[\n {\n \"description\": \"Activity Log by \",\n \"label\": \"Activity Log\",\n \"name\": \"Activity Log\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"View Log of all print, download and export events\",\n \"label\": \"Access Log\",\n \"name\": \"Access Log\",\n \"type\": \"doctype\"\n }\n]",
"title": "Logs"
},
{
"icon": "fa fa-lock",
"links": "[\n {\n \"description\": \"Set Permissions on Document Types and Roles\",\n \"icon\": \"fa fa-lock\",\n \"label\": \"Role Permissions Manager\",\n \"name\": \"permission-manager\",\n \"type\": \"page\"\n },\n {\n \"description\": \"Restrict user for specific document\",\n \"icon\": \"fa fa-lock\",\n \"label\": \"User Permissions\",\n \"name\": \"User Permission\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Set custom roles for page and report\",\n \"label\": \"Role Permission for Page and Report\",\n \"name\": \"Role Permission for Page and Report\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"User\"\n ],\n \"description\": \"Check which Documents are readable by a User\",\n \"doctype\": \"User\",\n \"icon\": \"fa fa-eye-open\",\n \"is_query_report\": true,\n \"label\": \"Permitted Documents For User\",\n \"name\": \"Permitted Documents For User\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"DocShare\"\n ],\n \"description\": \"Report of all document shares\",\n \"doctype\": \"DocShare\",\n \"icon\": \"fa fa-share\",\n \"label\": \"Document Share Report\",\n \"name\": \"Document Share Report\",\n \"type\": \"report\"\n }\n]",
"title": "Permissions"
}
],
"category": "Administration",
"charts": [],
"creation": "2020-03-02 15:12:16.754449",
"developer_mode_only": 0,
"disable_user_customization": 0,
"docstatus": 0,
"doctype": "Desk Page",
"idx": 0,
"label": "Users",
"modified": "2020-03-05 11:27:26.166080",
"modified_by": "Administrator",
"module": "Core",
"name": "Users",
"owner": "Administrator",
"pin_to_bottom": 0,
"pin_to_top": 0,
"shortcuts": [
{
"is_query_report": 0,
"link_to": "User",
"type": "DocType"
},
{
"is_query_report": 0,
"link_to": "Role",
"type": "DocType"
},
{
"is_query_report": 0,
"link_to": "permission-manager",
"type": "Page"
},
{
"is_query_report": 0,
"link_to": "user-profile",
"type": "Page"
}
]
}

View file

@ -39,3 +39,20 @@ class ModuleDef(Document):
frappe.clear_cache()
frappe.setup_module_map()
def on_trash(self):
"""Delete module name from modules.txt"""
modules = None
if frappe.local.module_app.get(frappe.scrub(self.name)):
with open(frappe.get_app_path(self.app_name, "modules.txt"), "r") as f:
content = f.read()
if self.name in content.splitlines():
modules = list(filter(None, content.splitlines()))
modules.remove(self.name)
if modules:
with open(frappe.get_app_path(self.app_name, "modules.txt"), "w") as f:
f.write("\n".join(modules))
frappe.clear_cache()
frappe.setup_module_map()

View file

@ -84,6 +84,10 @@ class ScheduledJobType(Document):
def update_scheduler_log(self, status):
if not self.create_log:
# self.get_next_execution will work properly iff self.last_execution is properly set
if self.frequency == "All" and status == 'Start':
self.db_set('last_execution', now_datetime(), update_modified=False)
frappe.db.commit()
return
if not self.scheduler_log:
self.scheduler_log = frappe.get_doc(dict(doctype = 'Scheduled Job Log', scheduled_job_type=self.name)).insert(ignore_permissions=True)

View file

@ -98,7 +98,11 @@ class User(Document):
clear_notifications(user=self.name)
frappe.clear_cache(user=self.name)
self.send_password_notification(self.__new_password)
create_contact(self, ignore_mandatory=True)
frappe.enqueue(
'frappe.core.doctype.user.user.create_contact',
user=self,
ignore_mandatory=True
)
if self.name not in ('Administrator', 'Guest') and not self.user_image:
frappe.enqueue('frappe.core.doctype.user.user.update_gravatar', name=self.name)
@ -1034,7 +1038,8 @@ def create_contact(user, ignore_links=False, ignore_mandatory=False):
from frappe.contacts.doctype.contact.contact import get_contact_name
if user.name in ["Administrator", "Guest"]: return
if not get_contact_name(user.email):
contact_exists = get_contact_name(user.email)
if not contact_exists:
contact = frappe.get_doc({
"doctype": "Contact",
"first_name": user.first_name,
@ -1052,6 +1057,34 @@ def create_contact(user, ignore_links=False, ignore_mandatory=False):
if user.mobile_no:
contact.add_phone(user.mobile_no, is_primary_mobile_no=True)
contact.insert(ignore_permissions=True, ignore_links=ignore_links, ignore_mandatory=ignore_mandatory)
else:
contact = frappe.get_doc("Contact", contact_exists)
contact.first_name = user.first_name
contact.last_name = user.last_name
contact.gender = user.gender
# Add mobile number if phone does not exists in contact
if user.phone and not any(new_contact.phone == user.phone for new_contact in contact.phone_nos):
# Set primary phone if there is no primary phone number
contact.add_phone(
user.phone,
is_primary_phone=not any(
new_contact.is_primary_phone == 1 for new_contact in contact.phone_nos
)
)
# Add mobile number if mobile does not exists in contact
if user.mobile_no and not any(new_contact.phone == user.mobile_no for new_contact in contact.phone_nos):
# Set primary mobile if there is no primary mobile number
contact.add_phone(
user.mobile_no,
is_primary_mobile_no=not any(
new_contact.is_primary_mobile_no == 1 for new_contact in contact.phone_nos
)
)
contact.save(ignore_permissions=True)
@frappe.whitelist()
def generate_keys(user):

View file

@ -85,7 +85,7 @@ class Dashboard {
chart_container.appendTo(this.container);
frappe.model.with_doc("Dashboard Chart", chart.chart).then( chart_doc => {
let dashboard_chart = new DashboardChart(chart_doc, chart_container);
let dashboard_chart = new frappe.ui.DashboardChart(chart_doc, chart_container);
dashboard_chart.show();
});
});
@ -215,7 +215,7 @@ class DashboardChart {
render_date_range_fields() {
if (!this.date_field_wrapper || !this.date_field_wrapper.is(':visible')) {
this.date_field_wrapper =
this.date_field_wrapper =
$(`<div class="dashboard-date-field pull-right"></div>`)
.insertBefore(this.chart_container.find('.chart-wrapper'));
@ -304,15 +304,15 @@ class DashboardChart {
}
setup_filter_button() {
this.is_document_type = this.chart_doc.chart_type!== 'Report' && this.chart_doc.chart_type!=='Custom';
this.filter_button =
this.filter_button =
$(`<div class="filter-chart btn btn-default btn-xs pull-right">${__("Filter")}</div>`);
this.filter_button.prependTo(this.chart_container);
this.filter_button.on('click', () => {
let fields;
frappe.dashboard_utils.get_filters_for_chart_type(this.chart_doc)
.then(filters => {
if (!this.is_document_type) {
@ -370,8 +370,8 @@ class DashboardChart {
}
dialog.show();
dialog.set_values(this.filters);
dialog.set_values(this.filters);
}
create_filter_group_and_add_filters(parent) {

View file

View file

@ -0,0 +1,3 @@
frappe.pages['workspace'].on_page_load = function(wrapper) {
frappe.utils.set_title(__("Home"));
}

View file

@ -0,0 +1,23 @@
{
"content": null,
"creation": "2020-02-27 15:07:57.124916",
"docstatus": 0,
"doctype": "Page",
"icon": "icon-th",
"idx": 0,
"modified": "2020-02-27 15:07:57.124916",
"modified_by": "Administrator",
"module": "Core",
"name": "workspace",
"owner": "Administrator",
"page_name": "workspace",
"roles": [
{
"role": "All"
}
],
"script": null,
"standard": "Yes",
"style": null,
"system_page": 0
}

View file

@ -0,0 +1,50 @@
{
"cards": [
{
"links": "[\n {\n \"label\": \"Dashboard\",\n \"name\": \"Dashboard\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Dashboard Chart\",\n \"name\": \"Dashboard Chart\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Dashboard Chart Source\",\n \"name\": \"Dashboard Chart Source\",\n \"type\": \"doctype\"\n }\n]",
"title": "Dashboards"
},
{
"icon": "fa fa-glass",
"links": "[\n {\n \"description\": \"Change field properties (hide, readonly, permission etc.)\",\n \"label\": \"Customize Form\",\n \"name\": \"Customize Form\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Add fields to forms.\",\n \"label\": \"Custom Field\",\n \"name\": \"Custom Field\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Add custom javascript to forms.\",\n \"label\": \"Custom Script\",\n \"name\": \"Custom Script\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Add custom forms.\",\n \"label\": \"DocType\",\n \"name\": \"DocType\",\n \"type\": \"doctype\"\n }\n]",
"title": "Form Customization"
},
{
"links": "[\n {\n \"description\": \"Add your own translations\",\n \"label\": \"Custom Translations\",\n \"name\": \"Translation\",\n \"type\": \"doctype\"\n }\n]",
"title": "Other"
}
],
"category": "Administration",
"charts": [],
"creation": "2020-03-02 15:15:03.839594",
"developer_mode_only": 0,
"disable_user_customization": 0,
"docstatus": 0,
"doctype": "Desk Page",
"idx": 0,
"label": "Customization",
"modified": "2020-03-05 11:27:26.137718",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customization",
"owner": "Administrator",
"pin_to_bottom": 0,
"pin_to_top": 0,
"shortcuts": [
{
"is_query_report": 0,
"link_to": "Customize Form",
"type": "DocType"
},
{
"is_query_report": 0,
"link_to": "Custom Role",
"type": "DocType"
},
{
"is_query_report": 0,
"link_to": "Custom Script",
"type": "DocType"
}
]
}

344
frappe/desk/desktop.py Normal file
View file

@ -0,0 +1,344 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
# Author - Shivam Mishra <shivam@frappe.io>
from __future__ import unicode_literals
import frappe
import json
from frappe import _, DoesNotExistError
from frappe.boot import get_allowed_pages, get_allowed_reports
from frappe.cache_manager import build_domain_restriced_doctype_cache, build_domain_restriced_page_cache, build_table_count_cache
class Workspace:
def __init__(self, page_name):
self.page_name = page_name
def build_cache(self):
self.doc = frappe.get_doc("Desk Page", self.page_name)
user = frappe.get_user()
user.build_permissions()
self.user = user
self.allowed_pages = get_allowed_pages()
self.allowed_reports = get_allowed_reports()
self.table_counts = build_table_count_cache()
self.restricted_doctypes = build_domain_restriced_doctype_cache()
self.restricted_pages = build_domain_restriced_page_cache()
def is_item_allowed(self, name, item_type):
item_type = item_type.lower()
if item_type == "doctype":
return (name in self.user.can_read and name in self.restricted_doctypes)
if item_type == "page":
return (name in self.allowed_pages and name in self.restricted_pages)
if item_type == "report":
return name in self.allowed_reports
if item_type == "help":
return True
return False
def build_workspace(self):
self.cards = {
'label': self.doc.charts_label,
'items': self.get_cards()
}
self.charts = {
'label': self.doc.shortcuts_label,
'items': self.get_charts()
}
self.shortcuts = {
'label': self.doc.shortcuts_label,
'items': self.get_shortcuts()
}
def get_cards(self):
cards = self.doc.cards + get_custom_reports_and_doctypes(self.doc.module)
default_country = frappe.db.get_default("country")
def _doctype_contains_a_record(name):
exists = self.table_counts.get(name)
if not exists:
if not frappe.db.get_value('DocType', name, 'issingle'):
exists = frappe.db.count(name)
else:
exists = True
self.table_counts[name] = exists
return exists
def _prepare_item(item):
if item.dependencies:
incomplete_dependencies = [d for d in item.dependencies if not _doctype_contains_a_record(d)]
if len(incomplete_dependencies):
item.incomplete_dependencies = incomplete_dependencies
if item.onboard:
# Mark Spotlights for initial
if item.get("type") == "doctype":
name = item.get("name")
count = _doctype_contains_a_record(name)
item["count"] = count
return item
new_data = []
for section in cards:
new_items = []
if isinstance(section.links, str):
links = json.loads(section.links)
else:
links = section.links
for item in links:
item = frappe._dict(item)
# Condition: based on country
if item.country and item.country != default_country:
continue
# Check if user is allowed to view
if self.is_item_allowed(item.name, item.type):
prepared_item = _prepare_item(item)
new_items.append(item)
if new_items:
if isinstance(section, frappe._dict):
new_section = section.copy()
else:
new_section = section.as_dict().copy()
new_section["links"] = new_items
new_section["label"] = section.title
new_data.append(new_section)
return new_data
def get_charts(self):
if frappe.has_permission("Dashboard Chart", throw=False):
return [chart for chart in self.doc.charts]
return []
def get_shortcuts(self):
items = []
for item in self.doc.shortcuts:
new_item = item.as_dict().copy()
new_item['name'] = _(item.link_to)
if self.is_item_allowed(item.link_to, item.type):
if item.type == "Page":
page = self.allowed_pages[item.link_to]
new_item['label'] = _(page.get("title", frappe.unscrub(item.link_to)))
if item.type == "Report":
report = self.allowed_reports.get(item.link_to, {})
if report.get("report_type") in ["Query Report", "Script Report"]:
new_item['is_query_report'] = 1
items.append(new_item)
return items
@frappe.whitelist()
def get_desktop_page(page):
"""Applies permissions, customizations and returns the configruration for a page
on desk.
Args:
page (string): page name
Returns:
dict: dictionary of cards, charts and shortcuts to be displayed on website
"""
wspace = Workspace(page)
try:
wspace.build_cache()
wspace.build_workspace()
return {
'charts': wspace.charts,
'shortcuts': wspace.shortcuts,
'cards': wspace.cards,
'allow_customization': not wspace.doc.disable_user_customization
}
except DoesNotExistError:
if frappe.message_log:
frappe.message_log.pop()
return None
@frappe.whitelist()
def get_desk_sidebar_items():
"""Get list of sidebar items for desk
"""
# don't get domain restricted pages
filters = {'restrict_to_domain': ['in', frappe.get_active_domains()]}
if not frappe.local.conf.developer_mode:
filters['developer_mode_only'] = '0'
# pages sorted based on pinned to top and then by name
order_by = "pin_to_top desc, pin_to_bottom asc, name asc"
pages = frappe.get_all("Desk Page", fields=["name", "category"], filters=filters, order_by=order_by, ignore_permissions=True)
from collections import defaultdict
sidebar_items = defaultdict(list)
for page in pages:
# The order will be maintained while categorizing
sidebar_items[page["category"]].append(page)
return sidebar_items
def get_table_with_counts():
counts = frappe.cache().get_value("information_schema:counts")
if not counts:
counts = build_table_count_cache()
return counts
def get_custom_reports_and_doctypes(module):
return [
frappe._dict({
"title": "Custom Reports",
"links": get_custom_report_list(module)
}),
frappe._dict({
"title": "Custom DocTypes",
"links": get_custom_doctype_list(module)
})
]
def get_custom_doctype_list(module):
doctypes = frappe.get_list("DocType", fields=["name"], filters={"custom": 1, "istable": 0, "module": module}, order_by="name", ignore_permissions=True)
out = []
for d in doctypes:
out.append({
"type": "doctype",
"name": d.name,
"label": _(d.name)
})
return out
def get_custom_report_list(module):
"""Returns list on new style reports for modules."""
reports = frappe.get_list("Report", fields=["name", "ref_doctype", "report_type"], filters=
{"is_standard": "No", "disabled": 0, "module": module},
order_by="name", ignore_permissions=True)
out = []
for r in reports:
out.append({
"type": "report",
"doctype": r.ref_doctype,
"is_query_report": 1 if r.report_type in ("Query Report", "Script Report", "Custom Report") else 0,
"label": _(r.name),
"name": r.name
})
return out
def make_them_pages():
"""Helper function to make pages
"""
pages = [
('Desk', 'frappe', 'octicon octicon-calendar'),
('Settings', 'frappe', 'octicon octicon-settings'),
('Users and Permissions', 'frappe', 'octicon octicon-settings'),
('Customization', 'frappe', 'octicon octicon-settings'),
('Integrations', 'frappe', 'octicon octicon-globe'),
('Core', 'frappe', 'octicon octicon-circuit-board'),
('Website', 'frappe', 'octicon octicon-globe'),
('Getting Started', 'erpnext', 'fa fa-check-square-o'),
('Accounts', 'erpnext', 'octicon octicon-repo'),
('Selling', 'erpnext', 'octicon octicon-tag'),
('Buying', 'erpnext', 'octicon octicon-briefcase'),
('Stock', 'erpnext', 'octicon octicon-package'),
('Assets', 'erpnext', 'octicon octicon-database'),
('Projects', 'erpnext', 'octicon octicon-rocket'),
('CRM', 'erpnext', 'octicon octicon-broadcast'),
('Support', 'erpnext', 'fa fa-check-square-o'),
('HR', 'erpnext', 'octicon octicon-organization'),
('Quality Management', 'erpnext', 'fa fa-check-square-o'),
('Manufacturing', 'erpnext', 'octicon octicon-tools'),
('Retail', 'erpnext', 'octicon octicon-credit-card'),
('Education', 'erpnext', 'octicon octicon-mortar-board'),
('Healthcare', 'erpnext', 'fa fa-heartbeat'),
('Agriculture', 'erpnext', 'octicon octicon-globe'),
('Non Profit', 'erpnext', 'octicon octicon-heart'),
('Help', 'erpnext', 'octicon octicon-device-camera-video')
]
for page in pages:
print("Processing Page: {0}".format(page[0]))
make_them_cards(page[0], page[2])
def make_them_cards(page_name, from_module=None, to_module=None, icon=None):
from frappe.desk.moduleview import get
if not from_module:
from_module = page_name
if not to_module:
to_module = page_name
try:
modules = get(from_module)['data']
except:
return
# Find or make page doc
if frappe.db.exists("Desk Page", page_name):
page = frappe.get_doc("Desk Page", page_name)
print("--- Got Page: {0}".format(page.name))
else:
page = frappe.new_doc("Desk Page")
page.label = page_name
page.cards = []
page.icon = icon
print("--- New Page: {0}".format(page.name))
# Guess Which Module
if not to_module and frappe.db.exists("Module Def", page_name):
page.module = page_name
if to_module:
page.module = to_module
elif frappe.db.exists("Module Def", page_name):
page.module = page_name
for data in modules:
# Create a New Card Child Doc
card = frappe.new_doc("Desk Card")
# Data clean up
for item in data['items']:
try:
del item['count']
del item['incomplete_dependencies']
except KeyError:
pass
# Set Child doc values
card.title = data['label']
card.icon = data.get('icon')
# Pretty dump JSON
card.links = json.dumps(data['items'], indent=4, sort_keys=True)
# Set Parent attributes
card.parent = page.name
card.parenttype = page.doctype
card.parentfield = "cards"
# Add cards to page doc
print("------- Adding Card: {0}".format(card.title))
page.cards.append(card)
# End it all
page.save()
frappe.db.commit()
return

View file

@ -0,0 +1,8 @@
// Copyright (c) 2020, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Desk Card', {
// refresh: function(frm) {
// }
});

View file

@ -0,0 +1,54 @@
{
"actions": [],
"creation": "2020-01-29 14:45:54.383089",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"title",
"column_break_2",
"icon",
"section_break_3",
"links"
],
"fields": [
{
"fieldname": "links",
"fieldtype": "Code",
"label": "Links",
"options": "JSON",
"reqd": 1
},
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title",
"reqd": 1
},
{
"fieldname": "section_break_3",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"fieldname": "icon",
"fieldtype": "Data",
"label": "Icon"
}
],
"istable": 1,
"links": [],
"modified": "2020-02-03 12:40:42.595122",
"modified_by": "Administrator",
"module": "Desk",
"name": "Desk Card",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, 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 DeskCard(Document):
pass

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestDeskCard(unittest.TestCase):
pass

View file

@ -0,0 +1,47 @@
{
"actions": [],
"creation": "2020-01-23 13:44:03.882158",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"chart_name",
"label",
"size"
],
"fields": [
{
"fieldname": "chart_name",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Chart Name",
"options": "Dashboard Chart",
"reqd": 1
},
{
"fieldname": "label",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Label"
},
{
"fieldname": "size",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Size",
"options": "Full\nHalf"
}
],
"istable": 1,
"links": [],
"modified": "2020-01-23 16:47:16.265651",
"modified_by": "Administrator",
"module": "Desk",
"name": "Desk Chart",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, 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 DeskChart(Document):
pass

View file

@ -0,0 +1,8 @@
// Copyright (c) 2020, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Desk Page', {
// refresh: function(frm) {
// }
});

View file

@ -0,0 +1,188 @@
{
"actions": [],
"autoname": "field:label",
"beta": 1,
"creation": "2020-01-23 13:45:59.470592",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"label",
"module",
"restrict_to_domain",
"category",
"icon",
"column_break_3",
"onboarding",
"developer_mode_only",
"disable_user_customization",
"pin_to_top",
"pin_to_bottom",
"section_break_2",
"charts_label",
"charts",
"section_break_15",
"shortcuts_label",
"shortcuts",
"section_break_18",
"cards_label",
"cards"
],
"fields": [
{
"fieldname": "label",
"fieldtype": "Data",
"label": "Name",
"length": 22,
"unique": 1
},
{
"collapsible": 1,
"fieldname": "section_break_2",
"fieldtype": "Section Break",
"label": "Dashboards"
},
{
"fieldname": "charts",
"fieldtype": "Table",
"label": "Charts",
"options": "Desk Chart"
},
{
"fieldname": "shortcuts",
"fieldtype": "Table",
"label": "Shortcuts",
"options": "Desk Shortcut"
},
{
"fieldname": "onboarding",
"fieldtype": "Data",
"label": "Onboarding"
},
{
"fieldname": "restrict_to_domain",
"fieldtype": "Link",
"label": "Restrict to Domain",
"options": "Domain",
"search_index": 1
},
{
"fieldname": "module",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Module",
"options": "Module Def"
},
{
"fieldname": "icon",
"fieldtype": "Data",
"label": "Icon"
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "cards",
"fieldtype": "Table",
"label": "Cards",
"options": "Desk Card"
},
{
"fieldname": "category",
"fieldtype": "Select",
"label": "Category",
"options": "Modules\nDomains\nPlaces\nAdministration",
"search_index": 1
},
{
"default": "0",
"fieldname": "developer_mode_only",
"fieldtype": "Check",
"label": "Developer Mode Only",
"search_index": 1
},
{
"default": "0",
"depends_on": "eval:doc.pin_to_bottom!=1",
"fieldname": "pin_to_top",
"fieldtype": "Check",
"label": "Pin To Top",
"search_index": 1
},
{
"default": "0",
"fieldname": "disable_user_customization",
"fieldtype": "Check",
"label": "Disable User Customization",
"search_index": 1
},
{
"default": "0",
"depends_on": "eval:doc.pin_to_top!=1",
"fieldname": "pin_to_bottom",
"fieldtype": "Check",
"label": "Pin To Bottom",
"search_index": 1
},
{
"fieldname": "charts_label",
"fieldtype": "Data",
"label": "Label"
},
{
"fieldname": "shortcuts_label",
"fieldtype": "Data",
"label": "Label"
},
{
"fieldname": "cards_label",
"fieldtype": "Data",
"label": "Label"
},
{
"collapsible": 1,
"fieldname": "section_break_15",
"fieldtype": "Section Break",
"label": "Shortcuts"
},
{
"collapsible": 1,
"fieldname": "section_break_18",
"fieldtype": "Section Break",
"label": "Link Cards"
}
],
"links": [],
"modified": "2020-03-02 20:08:44.856046",
"modified_by": "Administrator",
"module": "Desk",
"name": "Desk Page",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"share": 1
}
],
"sort_field": "modified",
"sort_order": "DESC"
}

View file

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.modules.export_file import export_to_files
from frappe.model.document import Document
class DeskPage(Document):
def validate(self):
if (not (frappe.flags.in_install or frappe.flags.in_patch or frappe.flags.in_test or frappe.flags.in_fixtures)
and not frappe.conf.developer_mode):
frappe.throw(_("You need to be in developer mode to edit this document"))
def on_update(self):
export_to_files(record_list=[['Desk Page', self.name]], record_module=self.module)

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestDeskPage(unittest.TestCase):
pass

View file

@ -0,0 +1,91 @@
{
"actions": [],
"creation": "2020-01-23 13:44:59.248426",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"type",
"icon",
"column_break_4",
"link_to",
"is_query_report",
"section_break_5",
"stats_filter",
"column_break_3",
"color",
"format"
],
"fields": [
{
"fieldname": "type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Type",
"options": "DocType\nReport\nPage",
"reqd": 1
},
{
"fieldname": "link_to",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Link To",
"options": "type",
"reqd": 1
},
{
"fieldname": "stats_filter",
"fieldtype": "Code",
"label": "Count Filter",
"options": "JSON"
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"description": "For example: {} Open",
"fieldname": "format",
"fieldtype": "Data",
"label": "Format"
},
{
"fieldname": "section_break_5",
"fieldtype": "Section Break",
"label": "Count Filter"
},
{
"fieldname": "color",
"fieldtype": "Color",
"label": "Color"
},
{
"default": "0",
"depends_on": "eval:doc.type === \"Report\"",
"fieldname": "is_query_report",
"fieldtype": "Check",
"label": "Is Query Report"
},
{
"fieldname": "icon",
"fieldtype": "Data",
"label": "Icon"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
}
],
"istable": 1,
"links": [],
"modified": "2020-02-28 12:59:46.870172",
"modified_by": "Administrator",
"module": "Desk",
"name": "Desk Shortcut",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, 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 DeskShortcut(Document):
pass

View file

@ -101,10 +101,15 @@ def is_continue_slide_required(first_slide):
def create_onboarding_docs(values, doctype=None, app=None, slide_type=None):
data = json.loads(values)
doc = frappe.new_doc(doctype)
if hasattr(doc, 'create_onboarding_docs'):
doc.create_onboarding_docs(data)
else:
create_generic_onboarding_doc(data, doctype, slide_type)
try:
if hasattr(doc, 'create_onboarding_docs'):
doc.flags.ignore_validate = True
doc.flags.ignore_mandatory = True
doc.create_onboarding_docs(data)
else:
create_generic_onboarding_doc(data, doctype, slide_type)
except Exception:
pass
def create_generic_onboarding_doc(data, doctype, slide_type):
if slide_type == 'Settings':
@ -117,8 +122,8 @@ def create_generic_onboarding_doc(data, doctype, slide_type):
doc = frappe.new_doc(doctype)
for entry in data:
doc.set(entry, data.get(entry))
doc.flags.ignore_validate = True
doc.flags.ignore_mandatory = True
doc.flags.ignore_links = True
doc.insert()
@frappe.whitelist()

View file

@ -5,6 +5,7 @@
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe.utils import unique
class Tag(Document):
pass
@ -81,7 +82,7 @@ class DocTags:
if not tl:
tags = ''
else:
tl = list(set(filter(lambda x: x, tl)))
tl = unique(filter(lambda x: x, tl))
tags = ',' + ','.join(tl)
try:
frappe.db.sql("update `tab%s` set _user_tags=%s where name=%s" % \

View file

@ -225,7 +225,7 @@ def add_all_roles_to(name):
user.save()
def disable_future_access():
frappe.db.set_default('desktop:home_page', 'desktop')
frappe.db.set_default('desktop:home_page', 'workspace')
frappe.db.set_value('System Settings', 'System Settings', 'setup_complete', 1)
frappe.db.set_value('System Settings', 'System Settings', 'is_first_startup', 1)

View file

@ -4,7 +4,7 @@
"docstatus": 0,
"doctype": "Page",
"idx": 0,
"modified": "2019-07-22 12:23:38.425877",
"modified": "2020-03-02 15:17:13.041650",
"modified_by": "Administrator",
"module": "Desk",
"name": "user-profile",
@ -18,5 +18,6 @@
"script": null,
"standard": "Yes",
"style": null,
"system_page": 0
"system_page": 0,
"title": "User Profile"
}

View file

@ -30,9 +30,12 @@ def get_form_params():
"""Stringify GET request parameters."""
data = frappe._dict(frappe.local.form_dict)
is_report = data.get('view') == 'Report'
data.pop('cmd', None)
data.pop('data', None)
data.pop('ignore_permissions', None)
data.pop('view', None)
if "csrf_token" in data:
del data["csrf_token"]
@ -65,10 +68,11 @@ def get_form_params():
df = frappe.get_meta(parenttype).get_field(fieldname)
fieldname = df.fieldname if df else None
report_hide = df.report_hide if df else None
# remove the field from the query if the report hide flag is set
if report_hide:
# remove the field from the query if the report hide flag is set and current view is Report
if report_hide and is_report:
fields.remove(field)

View file

@ -174,7 +174,8 @@ scheduler_events = {
"frappe.email.doctype.email_account.email_account.pull",
"frappe.email.doctype.email_account.email_account.notify_unreplied",
"frappe.integrations.doctype.razorpay_settings.razorpay_settings.capture_payment",
'frappe.utils.global_search.sync_global_search'
'frappe.utils.global_search.sync_global_search',
"frappe.monitor.flush",
],
"hourly": [
"frappe.model.utils.link_count.update_link_count",

View file

@ -0,0 +1,43 @@
{
"cards": [
{
"links": "[\n {\n \"description\": \"Dropbox backup settings\",\n \"label\": \"Dropbox Settings\",\n \"name\": \"Dropbox Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"S3 Backup Settings\",\n \"label\": \"S3 Backup Settings\",\n \"name\": \"S3 Backup Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Google Drive Backup.\",\n \"label\": \"Google Drive\",\n \"name\": \"Google Drive\",\n \"type\": \"doctype\"\n }\n]",
"title": "Backup"
},
{
"links": "[\n {\n \"description\": \"Google API Settings.\",\n \"label\": \"Google Settings\",\n \"name\": \"Google Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Google Contacts Integration.\",\n \"label\": \"Google Contacts\",\n \"name\": \"Google Contacts\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Google Calendar Integration.\",\n \"label\": \"Google Calendar\",\n \"name\": \"Google Calendar\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Google Drive Integration.\",\n \"label\": \"Google Drive\",\n \"name\": \"Google Drive\",\n \"type\": \"doctype\"\n }\n]",
"title": "Google Services"
},
{
"links": "[\n {\n \"description\": \"Webhooks calling API requests into web apps\",\n \"label\": \"Webhook\",\n \"name\": \"Webhook\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Slack Webhooks for internal integration\",\n \"label\": \"Slack Webhook URL\",\n \"name\": \"Slack Webhook URL\",\n \"type\": \"doctype\"\n }\n]",
"title": "Webhook"
},
{
"links": "[\n {\n \"description\": \"Enter keys to enable login via Facebook, Google, GitHub.\",\n \"label\": \"Social Login Key\",\n \"name\": \"Social Login Key\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Ldap settings\",\n \"label\": \"LDAP Settings\",\n \"name\": \"LDAP Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Register OAuth Client App\",\n \"label\": \"OAuth Client\",\n \"name\": \"OAuth Client\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Settings for OAuth Provider\",\n \"label\": \"OAuth Provider Settings\",\n \"name\": \"OAuth Provider Settings\",\n \"type\": \"doctype\"\n }\n]",
"title": "Authentication"
},
{
"icon": "fa fa-star",
"links": "[\n {\n \"description\": \"Braintree payment gateway settings\",\n \"label\": \"Braintree Settings\",\n \"name\": \"Braintree Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"PayPal payment gateway settings\",\n \"label\": \"PayPal Settings\",\n \"name\": \"PayPal Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Razorpay Payment gateway settings\",\n \"label\": \"Razorpay Settings\",\n \"name\": \"Razorpay Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Stripe payment gateway settings\",\n \"label\": \"Stripe Settings\",\n \"name\": \"Stripe Settings\",\n \"type\": \"doctype\"\n }\n]",
"title": "Payments"
}
],
"category": "Administration",
"charts": [],
"creation": "2020-03-02 15:16:18.714190",
"developer_mode_only": 0,
"disable_user_customization": 1,
"docstatus": 0,
"doctype": "Desk Page",
"icon": "frapicon-dashboard",
"idx": 0,
"label": "Integrations",
"modified": "2020-03-05 11:27:26.195829",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Integrations",
"owner": "Administrator",
"pin_to_bottom": 0,
"pin_to_top": 0,
"shortcuts": []
}

View file

@ -801,8 +801,8 @@ class BaseDocument(object):
else:
# get values from old doc
if self.get('parent_doc'):
self.parent_doc.get_latest()
ref_doc = [d for d in self.parent_doc.get(self.parentfield) if d.name == self.name][0]
parent_doc = self.parent_doc.get_latest()
ref_doc = [d for d in parent_doc.get(self.parentfield) if d.name == self.name][0]
else:
ref_doc = self.get_latest()

View file

@ -583,7 +583,7 @@ class Document(BaseDocument):
# check for child tables
for df in self.meta.get_table_fields():
high_permlevel_fields = frappe.get_meta(df.options).meta.get_high_permlevel_fields()
high_permlevel_fields = frappe.get_meta(df.options).get_high_permlevel_fields()
if high_permlevel_fields:
for d in self.get(df.fieldname):
d.reset_values_if_no_permlevel_access(has_access_to, high_permlevel_fields)

View file

@ -46,7 +46,11 @@ def sync_for(app_name, force=0, sync_everything = False, verbose=False, reset_pe
("data_migration", "data_migration_plan"),
("desk", "onboarding_slide_field"),
("desk", "onboarding_slide_help_link"),
("desk", "onboarding_slide")):
("desk", "onboarding_slide"),
("desk", "desk_card"),
("desk", "desk_chart"),
("desk", "desk_shortcut"),
("desk", "desk_page")):
files.append(os.path.join(frappe.get_app_path("frappe"), d[0],
"doctype", d[1], d[1] + ".json"))
@ -75,7 +79,7 @@ def get_doc_files(files, start_path, force=0, sync_everything = False, verbose=F
# load in sequence - warning for devs
document_types = ['doctype', 'page', 'report', 'dashboard_chart_source', 'print_format',
'website_theme', 'web_form', 'notification', 'print_style',
'data_migration_mapping', 'data_migration_plan', 'onboarding_slide']
'data_migration_mapping', 'data_migration_plan', 'onboarding_slide', 'desk_page']
for doctype in document_types:
doctype_path = os.path.join(start_path, doctype)
if os.path.exists(doctype_path):

107
frappe/monitor.py Normal file
View file

@ -0,0 +1,107 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
from datetime import datetime
import json
import traceback
import frappe
import os
import uuid
import rq
MONITOR_REDIS_KEY = "monitor-transactions"
MONITOR_MAX_ENTRIES = 1000000
def start(transaction_type="request", method=None, kwargs=None):
if frappe.conf.monitor:
frappe.local.monitor = Monitor(transaction_type, method, kwargs)
def stop(response=None):
if frappe.conf.monitor and hasattr(frappe.local, "monitor"):
frappe.local.monitor.dump(response)
def log_file():
return os.path.join(frappe.utils.get_bench_path(), "logs", "monitor.json.log")
class Monitor:
def __init__(self, transaction_type, method, kwargs):
try:
self.data = frappe._dict(
{
"site": frappe.local.site,
"timestamp": datetime.utcnow(),
"transaction_type": transaction_type,
"uuid": str(uuid.uuid4()),
}
)
if transaction_type == "request":
self.collect_request_meta()
else:
self.collect_job_meta(method, kwargs)
except Exception:
traceback.print_exc()
def collect_request_meta(self):
self.data.request = frappe._dict(
{
"ip": frappe.local.request_ip,
"method": frappe.request.method,
"path": frappe.request.path,
}
)
def collect_job_meta(self, method, kwargs):
self.data.job = frappe._dict({"method": method, "scheduled": False, "wait": 0})
if "run_scheduled_job" in method:
self.data.job.method = kwargs["job_type"]
self.data.job.scheduled = True
job = rq.get_current_job()
if job:
self.data.uuid = job.id
waitdiff = self.data.timestamp - job.enqueued_at
self.data.job.wait = int(waitdiff.total_seconds() * 1000000)
def dump(self, response=None):
try:
timediff = datetime.utcnow() - self.data.timestamp
# Obtain duration in microseconds
self.data.duration = int(timediff.total_seconds() * 1000000)
if self.data.transaction_type == "request":
self.data.request.status_code = response.status_code
self.data.request.response_length = int(response.headers["Content-Length"])
self.store()
except Exception:
traceback.print_exc()
def store(self):
if frappe.cache().llen(MONITOR_REDIS_KEY) > MONITOR_MAX_ENTRIES:
frappe.cache().ltrim(MONITOR_REDIS_KEY, 1, -1)
serialized = json.dumps(self.data, sort_keys=True, default=str)
frappe.cache().rpush(MONITOR_REDIS_KEY, serialized)
def flush():
try:
# Fetch all the logs without removing from cache
logs = frappe.cache().lrange(MONITOR_REDIS_KEY, 0, -1)
if logs:
logs = list(map(frappe.safe_decode, logs))
with open(log_file(), "a", os.O_NONBLOCK) as f:
f.write("\n".join(logs))
f.write("\n")
# Remove fetched entries from cache
frappe.cache().ltrim(MONITOR_REDIS_KEY, len(logs) - 1, -1)
except Exception:
traceback.print_exc()

View file

@ -263,4 +263,6 @@ frappe.patches.v11_0.make_all_prepared_report_attachments_private #2019-11-26
frappe.patches.v12_0.setup_email_linking
frappe.patches.v12_0.fix_home_settings_for_all_users
frappe.patches.v12_0.change_existing_dashboard_chart_filters
execute:frappe.delete_doc("Test Runner")execute:frappe.delete_doc_if_exists('DocType', 'Google Maps Settings')
execute:frappe.delete_doc("Test Runner")
execute:frappe.delete_doc_if_exists('DocType', 'Google Maps Settings')
execute:frappe.db.set_default('desktop:home_page', 'workspace')

View file

@ -441,8 +441,8 @@ frappe.PrintFormatBuilder = Class.extend({
});
},
setup_field_settings: function() {
var me = this;
this.page.main.on("click", ".field-settings", function() {
this.page.main.find(".field-settings").on("click", () => {
var field = $(this).parent();
// new dialog

View file

@ -237,6 +237,7 @@
"public/js/frappe/utils/energy_point_utils.js",
"public/js/frappe/utils/dashboard_utils.js",
"public/js/frappe/ui/chart.js",
"public/js/frappe/ui/dashboard_chart.js",
"public/js/frappe/barcode_scanner/index.js"
],
"css/module.min.css": [
@ -346,9 +347,6 @@
"js/social.min.js": [
"public/js/frappe/social/social_home.js"
],
"js/modules.min.js": [
"public/js/frappe/views/modules_home.js"
],
"js/barcode_scanner.min.js": [
"public/js/frappe/barcode_scanner/quagga.js"
],

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 61.2 (89653) - https://sketch.com -->
<title>icons/navigation/dashboard</title>
<desc>Created with Sketch.</desc>
<g id="icons/navigation/dashboard" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group" transform="translate(1.700000, 1.000000)">
<path d="M6,0 C6.368,0 6.66666667,0.298666667 6.66666667,0.666666667 L6.66666667,8.66666667 C6.66666667,9.03466667 6.368,9.33333333 6,9.33333333 L0.666666667,9.33333333 C0.298666667,9.33333333 1.95399252e-14,9.03466667 1.95399252e-14,8.66666667 L1.95399252e-14,0.666666667 C1.95399252e-14,0.298666667 0.298666667,0 0.666666667,0 L6,0 Z M14,-4.4408921e-16 C14.368,-4.4408921e-16 14.6666667,0.298666667 14.6666667,0.666666667 L14.6666667,4.66666667 C14.6666667,5.03466667 14.368,5.33333333 14,5.33333333 L8.66666667,5.33333333 C8.29866667,5.33333333 8,5.03466667 8,4.66666667 L8,0.666666667 C8,0.298666667 8.29866667,-4.4408921e-16 8.66666667,-4.4408921e-16 L14,-4.4408921e-16 Z" id="Combined-Shape" fill="#A1ABB4"></path>
<path d="M6,10.6666667 C6.368,10.6666667 6.66666667,10.9653333 6.66666667,11.3333333 L6.66666667,15.3333333 C6.66666667,15.7013333 6.368,16 6,16 L0.666666667,16 C0.298666667,16 1.95399252e-14,15.7013333 1.95399252e-14,15.3333333 L1.95399252e-14,11.3333333 C1.95399252e-14,10.9653333 0.298666667,10.6666667 0.666666667,10.6666667 L6,10.6666667 Z M14,6.66666667 C14.368,6.66666667 14.6666667,6.96533333 14.6666667,7.33333333 L14.6666667,15.3333333 C14.6666667,15.7013333 14.368,16 14,16 L8.66666667,16 C8.29866667,16 8,15.7013333 8,15.3333333 L8,7.33333333 C8,6.96533333 8.29866667,6.66666667 8.66666667,6.66666667 L14,6.66666667 Z" id="Combined-Shape" fill="#415668" fill-rule="nonzero"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 61.2 (89653) - https://sketch.com -->
<title>icons/income</title>
<desc>Created with Sketch.</desc>
<g id="icons/income" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M16,12.8586667 L16,14 C16,15.1046667 13.9106667,16 11.3333333,16 C8.756,16 6.66666667,15.1046667 6.66666667,14 L6.66666667,14 L6.66666667,12.8586667 C7.73133333,13.574 9.366,14 11.3333333,14 C13.3006667,14 14.9353333,13.574 16,12.8586667 L16,12.8586667 Z M16,9.52533333 L16,10.6666667 C16,11.7713333 13.9106667,12.6666667 11.3333333,12.6666667 C8.756,12.6666667 6.66666667,11.7713333 6.66666667,10.6666667 L6.66666667,10.6666667 L6.66666667,9.52533333 C7.73133333,10.2406667 9.366,10.6666667 11.3333333,10.6666667 C13.3006667,10.6666667 14.9353333,10.2406667 16,9.52533333 L16,9.52533333 Z M11.3333333,5.33333333 C13.9106622,5.33333333 16,6.22876383 16,7.33333333 C16,8.43790283 13.9106622,9.33333333 11.3333333,9.33333333 C8.7560045,9.33333333 6.66666667,8.43790283 6.66666667,7.33333333 C6.66666667,6.22876383 8.7560045,5.33333333 11.3333333,5.33333333 Z" id="Combined-Shape" fill="#415668" fill-rule="nonzero"></path>
<path d="M2.44249065e-14,10.8586667 C1.06466667,11.574 2.69933333,12 4.66666667,12 C4.894,12 5.11533333,11.9926667 5.33333333,11.982 L5.33333333,11.982 L5.33333333,13.9773333 C5.11533333,13.9906667 4.89333333,14 4.66666667,14 C2.08933333,14 2.44249065e-14,13.1046667 2.44249065e-14,12 L2.44249065e-14,12 Z M2.44249065e-14,7.52533333 C1.06466667,8.24066667 2.69933333,8.66666667 4.66666667,8.66666667 C4.894,8.66666667 5.11533333,8.65933333 5.33333333,8.64866667 L5.33333333,8.64866667 L5.33333333,10.644 C5.11533333,10.6573333 4.89333333,10.6666667 4.66666667,10.6666667 C2.08933333,10.6666667 2.44249065e-14,9.77133333 2.44249065e-14,8.66666667 L2.44249065e-14,8.66666667 Z M-5.57776048e-13,4.192 C1.06466667,4.90733333 2.69933333,5.33333333 4.66666667,5.33333333 C5.36133333,5.33333333 6.012,5.27733333 6.61333333,5.17733333 C5.80666667,5.73733333 5.34333333,6.46866667 5.33533333,7.31066667 C5.116,7.324 4.894,7.33333333 4.66666667,7.33333333 C2.08933333,7.33333333 -5.57776048e-13,6.438 -5.57776048e-13,5.33333333 L-5.57776048e-13,5.33333333 Z M4.66666667,0 C7.2439955,0 9.33333333,0.8954305 9.33333333,2 C9.33333333,3.1045695 7.2439955,4 4.66666667,4 C2.08933783,4 2.48689958e-14,3.1045695 2.48689958e-14,2 C2.48689958e-14,0.8954305 2.08933783,0 4.66666667,0 Z" id="Combined-Shape" fill="#A1ABB4"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 61.2 (89653) - https://sketch.com -->
<title>icons/invoice/mail</title>
<desc>Created with Sketch.</desc>
<g id="icons/invoice/mail" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="letter" transform="translate(0.000000, 1.000000)">
<path d="M15,0 L1,0 C0.4,0 0,0.4 0,1 L0,2.4 L8,6.9 L16,2.5 L16,1 C16,0.4 15.6,0 15,0 Z" id="Path" fill="#A1ABB4"></path>
<path d="M7.5,8.9 L0,4.7 L0,13 C0,13.6 0.4,14 1,14 L15,14 C15.6,14 16,13.6 16,13 L16,4.7 L8.5,8.9 C8.22,9.04 7.78,9.04 7.5,8.9 Z" id="Path" fill="#415668" fill-rule="nonzero"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 842 B

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 61.2 (89653) - https://sketch.com -->
<title>icons/folder/normal</title>
<desc>Created with Sketch.</desc>
<g id="icons/folder/normal" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="folder-15" transform="translate(0.000000, 0.700000)" fill="#415668" fill-rule="nonzero">
<path d="M8.33333333,2.66666667 L6.33333333,0 L0.666666667,0 C0.298476833,0 0,0.298476833 0,0.666666667 L0,12.6666667 C0,13.7712362 0.8954305,14.6666667 2,14.6666667 L14,14.6666667 C15.1045695,14.6666667 16,13.7712362 16,12.6666667 L16,3.33333333 C16,2.9651435 15.7015232,2.66666667 15.3333333,2.66666667 L8.33333333,2.66666667 Z" id="Path"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 915 B

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 61.2 (89653) - https://sketch.com -->
<title>icons/invoice/phone</title>
<desc>Created with Sketch.</desc>
<g id="icons/invoice/phone" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="phone-call">
<path d="M15.285,12.305 L12.707,9.711 C12.317,9.318 11.682,9.318 11.291,9.709 L9,12 L4,7 L6.294,4.706 C6.684,4.316 6.685,3.683 6.295,3.292 L3.715,0.708 C3.324,0.317 2.691,0.317 2.3,0.708 L0.004,3.003 L0,3 C0,10.18 5.82,16 13,16 L15.283,13.717 C15.673,13.327 15.674,12.696 15.285,12.305 Z" id="Path" fill="#415668" fill-rule="nonzero"></path>
<path d="M8,0 C12.411,0 16,3.589 16,8 L14,8 C14,4.691 11.309,2 8,2 L8,0 Z M8,4 C10.206,4 12,5.794 12,8 L10,8 C10,6.897 9.103,6 8,6 L8,4 Z" id="Combined-Shape" fill="#A1ABB4"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 61.2 (89653) - https://sketch.com -->
<title>icons/navigation/property</title>
<desc>Created with Sketch.</desc>
<g id="icons/navigation/property" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="new-construction" transform="translate(1.000000, 1.000000)">
<path d="M15.5246667,2.028 L8.858,0.028 C8.65616136,-0.0324717007 8.43761545,0.00603303671 8.2685973,0.131844525 C8.09957915,0.257656014 7.99998537,0.455963879 8,0.666666667 L8,4 L9.33333333,4 L9.33333333,1.56266667 L14.6666667,3.16266667 L14.6666667,14.6666667 L10.6666667,14.6666667 L10.6666667,6 C10.6666667,5.63181017 10.3681898,5.33333333 10,5.33333333 L3.33333333,5.33333333 C2.9651435,5.33333333 2.66666667,5.63181017 2.66666667,6 L2.66666667,14.6666667 L1.33333333,14.6666667 L1.33333333,8 L0.666666667,8 C0.298476833,8 0,8.29847683 0,8.66666667 L0,15.3333333 C0,15.7015232 0.298476833,16 0.666666667,16 L15.3333333,16 C15.7015232,16 16,15.7015232 16,15.3333333 L16,2.66666667 C16,2.3721545 15.8067889,2.11252499 15.5246667,2.028 Z M8,14 L5.33333333,14 L5.33333333,12.6666667 L8,12.6666667 L8,14 Z M8,11.3333333 L5.33333333,11.3333333 L5.33333333,10 L8,10 L8,11.3333333 Z M8,8.66666667 L5.33333333,8.66666667 L5.33333333,7.33333333 L8,7.33333333 L8,8.66666667 Z" id="Shape" fill="#415668" fill-rule="nonzero"></path>
<rect id="Rectangle" fill="#A1ABB4" x="12" y="5.33333333" width="1.33333333" height="8"></rect>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 61.2 (89653) - https://sketch.com -->
<title>icons/navigation/purchase</title>
<desc>Created with Sketch.</desc>
<g id="icons/navigation/purchase" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="bag-16" transform="translate(1.000000, 0.500000)">
<path d="M16,4.3972168 L16,15 C16,16.1045695 15.1045695,17 14,17 L2,17 C0.8954305,17 1.3527075e-16,16.1045695 0,15 L0,4.3972168 L16,4.3972168 Z M11.6363636,7.16666667 C11.2,7.16666667 10.9090909,7.45555556 10.9090909,7.88888889 C10.9090909,9.47777778 9.6,10.7777778 8,10.7777778 C6.4,10.7777778 5.09090909,9.47777778 5.09090909,7.88888889 C5.09090909,7.45555556 4.8,7.16666667 4.36363636,7.16666667 C3.92727273,7.16666667 3.63636364,7.45555556 3.63636364,7.88888889 C3.63636364,10.2722222 5.6,12.2222222 8,12.2222222 C10.4,12.2222222 12.3636364,10.2722222 12.3636364,7.88888889 C12.3636364,7.45555556 12.0727273,7.16666667 11.6363636,7.16666667 Z" id="Shape" fill="#415668" fill-rule="nonzero"></path>
<path d="M0,3 L1.49222874,1.12925513 C2.06146543,0.415626854 2.92465315,1.0558663e-15 3.83750348,0 L12.1683182,0 C13.0831751,-6.12145698e-16 13.9480333,0.417447486 14.5171563,1.13373106 L16,3 L16,3 L0,3 Z" id="Path-2" fill="#A1ABB4"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 61.2 (89653) - https://sketch.com -->
<title>icons/navigation/reports</title>
<desc>Created with Sketch.</desc>
<g id="icons/navigation/reports" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="icons/navigation/dashboard" transform="translate(0.800000, 1.000000)" fill="#112B42" fill-rule="nonzero">
<path d="M16.2857143,2.5 L16.2857143,7.26902668 L16.2857143,7.26902668 L11.9220779,7.26902668 C11.7032818,7.26906827 11.4982329,7.36737803 11.3613275,7.53318731 L11.2988052,7.62157447 L9.87116883,10 L7.50680519,4.09173512 C7.40433373,3.83523222 7.16527403,3.6589327 6.88983266,3.63673596 C6.64882146,3.61731381 6.41639286,3.71881295 6.26674592,3.90311574 L6.2078961,3.98706113 L4.23771429,7.26902668 L0.285714286,7.26902668 L0.285714286,2.5 C0.285714286,1.11928813 1.40500241,2.53632657e-16 2.78571429,0 L13.7857143,0 C15.1664262,-1.47995115e-15 16.2857143,1.11928813 16.2857143,2.5 Z" id="Path" opacity="0.396856399"></path>
<path d="M0.285714286,13.5 L0.285714286,8.73097332 L0.285714286,8.73097332 L4.64935065,8.73097332 C4.86814675,8.73093173 5.07319563,8.63262197 5.2101011,8.46681269 L5.27262338,8.37842553 L6.70025974,6 L9.06462338,11.9104456 C9.15434161,12.1350283 9.34876191,12.2981168 9.58061609,12.3501369 L9.68207792,12.3654867 L9.74025974,12.3654867 C9.95905584,12.3654451 10.1641047,12.2671353 10.3010102,12.101326 L10.3635325,12.0129389 L12.3337143,8.73097332 L16.2857143,8.73097332 L16.2857143,13.5 C16.2857143,14.8807119 15.1664262,16 13.7857143,16 L2.78571429,16 C1.40500241,16 0.285714286,14.8807119 0.285714286,13.5 Z" id="Path" opacity="0.800000012"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 61.2 (89653) - https://sketch.com -->
<title>icons/navigation/sales</title>
<desc>Created with Sketch.</desc>
<g id="icons/navigation/sales" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group-10" transform="translate(1.000000, 2.000000)">
<path d="M16,5.11230469 L16,12 C16,13.1045695 15.1045695,14 14,14 L2,14 C0.8954305,14 1.3527075e-16,13.1045695 0,12 L0,5.11230469 L16,5.11230469 Z M12.5,10 L8.5,10 C8.22385763,10 8,10.2238576 8,10.5 C8,10.7761424 8.22385763,11 8.5,11 L8.5,11 L12.5,11 C12.7761424,11 13,10.7761424 13,10.5 C13,10.2238576 12.7761424,10 12.5,10 L12.5,10 Z" id="Combined-Shape" fill="#415668"></path>
<path d="M2,0 L14,0 C15.1045695,-2.02906125e-16 16,0.8954305 16,2 L16,3.64526367 L16,3.64526367 L0,3.64526367 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z" id="Rectangle-Copy" fill="#A1ABB4"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 61.2 (89653) - https://sketch.com -->
<title>icons/navigation/settings</title>
<desc>Created with Sketch.</desc>
<g id="icons/navigation/settings" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="switches" transform="translate(1.000000, 0.500000)">
<path d="M4,8 L12,8 C14.1818182,8 16,6.18181818 16,4 C16,1.81818182 14.1818182,0 12,0 L4,0 C1.81818182,0 0,1.81818182 0,4 C0,6.18181818 1.81818182,8 4,8 Z M4,1.45454545 C5.38181818,1.45454545 6.54545455,2.61818182 6.54545455,4 C6.54545455,5.38181818 5.38181818,6.54545455 4,6.54545455 C2.61818182,6.54545455 1.45454545,5.38181818 1.45454545,4 C1.45454545,2.61818182 2.61818182,1.45454545 4,1.45454545 Z" id="Shape" fill="#A1ABB4"></path>
<path d="M12,9 L4,9 C1.81818182,9 0,10.8181818 0,13 C0,15.1818182 1.81818182,17 4,17 L12,17 C14.1818182,17 16,15.1818182 16,13 C16,10.8181818 14.1818182,9 12,9 Z M12,15.5454545 C10.6181818,15.5454545 9.45454545,14.3818182 9.45454545,13 C9.45454545,11.6181818 10.6181818,10.4545455 12,10.4545455 C13.3818182,10.4545455 14.5454545,11.6181818 14.5454545,13 C14.5454545,14.3818182 13.3818182,15.5454545 12,15.5454545 Z" id="Shape" fill="#415668"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -155,7 +155,8 @@ frappe.ui.form.Layout = Class.extend({
doctype: this.doctype,
parent: this.column.wrapper.get(0),
frm: this.frm,
render_input: render
render_input: render,
doc: this.doc
});
fieldobj.layout = this;

View file

@ -30,6 +30,7 @@ frappe.ui.form.QuickEntryForm = Class.extend({
let me = this;
return new Promise(resolve => {
frappe.model.with_doctype(this.doctype, function() {
me.check_quick_entry_doc();
me.set_meta_and_mandatory_fields();
if(me.is_quick_entry()) {
me.render_dialog();
@ -44,16 +45,16 @@ frappe.ui.form.QuickEntryForm = Class.extend({
},
set_meta_and_mandatory_fields: function(){
let fields = frappe.get_meta(this.doctype).fields;
if (fields.length < 7) {
// if less than 7 fields, then show everything
this.mandatory = fields;
} else {
// prepare a list of mandatory and bold fields
this.mandatory = $.map(fields,
function(d) { return ((d.reqd || d.bold || d.allow_in_quick_entry) && !d.read_only) ? $.extend({}, d) : null; });
}
this.meta = frappe.get_meta(this.doctype);
let fields = this.meta.fields;
// prepare a list of mandatory, bold and allow in quick entry fields
this.mandatory = $.map(fields, function(d) {
return ((d.reqd || d.bold || d.allow_in_quick_entry) && !d.read_only) ? $.extend({}, d) : null;
});
},
check_quick_entry_doc: function() {
if (!this.doc) {
this.doc = frappe.model.get_new_doc(this.doctype, null, null, true);
}
@ -66,8 +67,7 @@ frappe.ui.form.QuickEntryForm = Class.extend({
this.validate_for_prompt_autoname();
if (this.has_child_table()
|| !this.mandatory.length) {
if (this.has_child_table() || !this.mandatory.length) {
return false;
}
@ -103,8 +103,8 @@ frappe.ui.form.QuickEntryForm = Class.extend({
this.dialog = new frappe.ui.Dialog({
title: __("New {0}", [__(this.doctype)]),
fields: this.mandatory,
doc: this.doc
});
this.dialog.doc = this.doc;
this.register_primary_action();
this.render_edit_in_full_page_link();

View file

@ -344,7 +344,8 @@ frappe.views.BaseList = class BaseList {
filters: this.get_filters_for_args(),
order_by: this.sort_selector.get_sql_string(),
start: this.start,
page_length: this.page_length
page_length: this.page_length,
view: this.view
};
}

View file

@ -48,6 +48,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
setup_defaults() {
super.setup_defaults();
this.view = 'List';
// initialize with saved order by
this.sort_by = this.view_user_settings.sort_by || 'modified';
this.sort_order = this.view_user_settings.sort_order || 'desc';

View file

@ -135,23 +135,16 @@ $.extend(frappe.model, {
&& df.ignore_user_permissions != 1
&& allowed_records.length);
function is_document_allowed(doc) {
return doc && (!has_user_permissions || allowed_records.includes(doc));
}
// don't set defaults for "User" link field using User Permissions!
if (df.fieldtype==="Link" && df.options!=="User") {
// 1 - look in user permissions for document_type=="Setup".
// We don't want to include permissions of transactions to be used for defaults.
if (df.linked_document_type==="Setup" && has_user_permissions && default_doc) {
if (df.linked_document_type==="Setup"
&& has_user_permissions && default_doc) {
return default_doc;
}
if (is_document_allowed(df["default"])) {
user_default = df["default"];
}
if (!df.ignore_user_permissions && !user_default) {
if(!df.ignore_user_permissions) {
// 2 - look in user defaults
var user_defaults = frappe.defaults.get_user_defaults(df.options);
if (user_defaults && user_defaults.length===1) {
@ -168,8 +161,11 @@ $.extend(frappe.model, {
user_default = frappe.boot.user.last_selected_values[df.options];
}
var is_allowed_user_default = user_default &&
(!has_user_permissions || allowed_records.includes(user_default));
// is this user default also allowed as per user permissions?
if (is_document_allowed(user_default)) {
if (is_allowed_user_default) {
return user_default;
}
}
@ -191,8 +187,9 @@ $.extend(frappe.model, {
} else if (df["default"][0]===":") {
var boot_doc = frappe.model.get_default_from_boot_docs(df, doc, parent_doc);
var is_allowed_boot_doc = !has_user_permissions || allowed_records.includes(boot_doc);
if (is_document_allowed(boot_doc)) {
if (is_allowed_boot_doc) {
return boot_doc;
}
} else if (df.fieldname===meta.title_field) {
@ -201,7 +198,8 @@ $.extend(frappe.model, {
}
// is this default value is also allowed as per user permissions?
if (df.fieldtype!=="Link" || df.options==="User" || is_document_allowed(df.default)) {
var is_allowed_default = !has_user_permissions || allowed_records.includes(df.default);
if (df.fieldtype!=="Link" || df.options==="User" || is_allowed_default) {
return df["default"];
}

View file

@ -46,8 +46,7 @@ frappe.route = function() {
if(route[0]) {
const title_cased_route = frappe.utils.to_title_case(route[0]);
if (title_cased_route === 'Desktop') {
if (title_cased_route === 'Workspace') {
frappe.views.pageview.show('');
}

View file

@ -0,0 +1,183 @@
frappe.provide('ui')
frappe.ui.DashboardChart = class DashboardChart {
constructor(chart_doc, chart_container, options) {
this.chart_doc = chart_doc;
this.container = chart_container;
this.options = options || {};
this.chart_args = {};
}
show() {
this.get_settings().then(() => {
this.prepare_chart_object();
this.prepare_container();
if (!this.options.hide_actions || this.options.hide_actions == undefined) {
this.prepare_chart_actions();
}
this.fetch(this.filters).then((data) => {
if (!this.options.hide_last_sync || this.options.hide_last_sync == undefined) {
this.update_last_synced();
}
this.data = data;
this.render();
});
});
}
prepare_container() {
const column_width_map = {
"Half": "6",
"Full": "12",
};
let columns = column_width_map[this.chart_doc.width];
this.chart_container = $(`<div class="col-sm-${columns} chart-column-container">
<div class="chart-wrapper">
<div class="chart-loading-state text-muted">${__("Loading...")}</div>
<div class="chart-empty-state hide text-muted">${__("No Data")}</div>
</div>
</div>`);
this.chart_container.appendTo(this.container);
if (!this.options.hide_last_sync || this.options.hide_last_sync == undefined) {
let last_synced_text = $(`<span class="text-muted last-synced-text"></span>`);
last_synced_text.prependTo(this.chart_container);
}
}
prepare_chart_actions() {
let actions = [
{
label: __("Refresh"),
action: 'action-refresh',
handler: () => {
this.fetch(this.filters, true).then(data => {
this.update_chart_object();
this.data = data;
this.render();
});
}
},
{
label: __("Edit..."),
action: 'action-edit',
handler: () => {
frappe.set_route('Form', 'Dashboard Chart', this.chart_doc.name);
}
}
];
if (this.chart_doc.document_type) {
actions.push({
label: __("{0} List", [this.chart_doc.document_type]),
action: 'action-list',
handler: () => {
frappe.set_route('List', this.chart_doc.document_type);
}
})
}
this.set_chart_actions(actions);
}
set_chart_actions(actions) {
this.chart_actions = $(`<div class="chart-actions btn-group dropdown pull-right">
<a class="dropdown-toggle" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false"> <button class="btn btn-default btn-xs"><span class="caret"></span></button>
</a>
<ul class="dropdown-menu" style="max-height: 300px; overflow-y: auto;">
${actions.map(action => `<li><a data-action="${action.action}">${action.label}</a></li>`).join('')}
</ul>
</div>
`);
this.chart_actions.find("a[data-action]").each((i, o) => {
const action = o.dataset.action;
$(o).click(actions.find(a => a.action === action));
});
this.chart_actions.prependTo(this.chart_container);
}
fetch(filters, refresh=false) {
this.chart_container.find('.chart-loading-state').removeClass('hide');
let method = this.settings ? this.settings.method
: 'frappe.desk.doctype.dashboard_chart.dashboard_chart.get';
return frappe.xcall(
method,
{
chart_name: this.chart_doc.name,
filters: filters,
refresh: refresh ? 1 : 0,
}
);
}
render() {
this.chart_container.find('.chart-loading-state').addClass('hide');
if (!this.data) {
this.chart_container.find('.chart-empty-state').removeClass('hide');
} else {
this.prepare_chart_args();
if (!this.chart) {
this.chart = new frappe.Chart(this.chart_container.find(".chart-wrapper")[0], this.chart_args);
} else {
this.chart.update(this.data);
}
}
}
prepare_chart_args() {
const chart_type_map = {
"Line": "line",
"Bar": "bar",
};
this.chart_args.data = this.data;
this.chart_args.type = chart_type_map[this.chart_doc.type];
this.chart_args.colors = [this.chart_doc.color || "light-blue"];
this.chart_args.axisOptions = {
xIsSeries: this.chart_doc.timeseries,
shortenYAxisNumbers: 1
}
if (!this.options.hide_title || this.options.hide_title == undefined) {
this.chart_args.title = this.chart_doc.chart_name;
}
}
update_last_synced() {
let last_synced_text = __("Last synced {0}", [comment_when(this.chart_doc.last_synced_on)]);
this.container.find(".last-synced-text").html(last_synced_text);
}
update_chart_object() {
frappe.db.get_doc("Dashboard Chart", this.chart_doc.name).then(doc => {
this.chart_doc = doc;
this.prepare_chart_object();
this.update_last_synced();
});
}
prepare_chart_object() {
this.filters = JSON.parse(this.chart_doc.filters_json || '{}');
}
get_settings() {
if (this.chart_doc.chart_type == 'Custom') {
// custom source
if (frappe.dashboards && frappe.dashboards.chart_sources[this.chart_doc.source]) {
this.settings = frappe.dashboards.chart_sources[this.chart_doc.source];
return Promise.resolve();
} else {
return frappe.xcall('frappe.desk.doctype.dashboard_chart_source.dashboard_chart_source.get_config',
{name: this.chart_doc.source})
.then(config => {
frappe.dom.eval(config);
this.settings = frappe.dashboards.chart_sources[this.chart_doc.source];
});
}
} else {
return Promise.resolve();
}
}
}

View file

@ -94,8 +94,7 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
})
.on('scroll', function() {
var $input = $('input:focus');
if($input.length && ['Date', 'Datetime',
'Time'].includes($input.attr('data-fieldtype'))) {
if ($input.length && ['Date', 'Datetime', 'Time'].includes($input.attr('data-fieldtype'))) {
$input.blur();
}
});
@ -197,5 +196,3 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
this.header.find('.modal-title').toggleClass('cursor-pointer');
}
};

View file

@ -25,7 +25,7 @@ frappe.ui.GroupBy = class {
this.groupby_select = this.groupby_edit_area.find('select.groupby');
this.aggregate_function_select = this.groupby_edit_area.find('select.aggregate-function');
this.aggregate_on_select = this.groupby_edit_area.find('select.aggregate-on');
this.aggregate_on_html = ``;
// set default to count
this.aggregate_function_select.val("count");
this.page.wrapper.find(".frappe-list").append(
@ -51,21 +51,32 @@ frappe.ui.GroupBy = class {
}
show_hide_aggregate_on() {
this.report_view.meta.fields.forEach((field) => {
let fn = this.aggregate_function_select.val();
if(fn === 'sum' || fn === 'avg') {
// pick numeric fields for sum / avg
if(frappe.model.is_numeric_field(field.fieldtype)) {
this.aggregate_on_select.append(
$('<option>', { value : field.fieldname })
.text(field.label));
let fn = this.aggregate_function_select.val();
if (fn === 'sum' || fn === 'avg') {
if (!this.aggregate_on_html.length) {
this.aggregate_on_html = `<option value="" disabled selected>
${__("Select Field...")}</option>`;
for (let doctype in this.all_fields) {
const doctype_fields = this.all_fields[doctype];
doctype_fields.forEach(field => {
// pick numeric fields for sum / avg
if (frappe.model.is_numeric_field(field.fieldtype)) {
let option_text = doctype == this.doctype
? field.label
: `${field.label} (${doctype})`;
this.aggregate_on_html+= `<option data-doctype="${doctype}"
value="${field.fieldname}">${option_text}</option>`;
}
});
}
this.aggregate_on_select.show();
} else {
// count, so no aggregate function
this.aggregate_on_select.hide();
}
});
this.aggregate_on_select.html(this.aggregate_on_html);
this.aggregate_on_select.show();
} else {
// count, so no aggregate function
this.aggregate_on_select.hide();
}
}
get_settings() {
@ -114,8 +125,10 @@ frappe.ui.GroupBy = class {
if (this.aggregate_function === 'count') {
this.aggregate_on = 'name';
this.aggregate_on_doctype = null;
} else {
this.aggregate_on = this.aggregate_on_select.val();
this.aggregate_on_doctype = this.aggregate_on_select.find(':selected').attr('data-doctype');
}
@ -143,11 +156,13 @@ frappe.ui.GroupBy = class {
set_args(args) {
if (this.aggregate_function && this.group_by) {
let aggregate_column;
if(this.aggregate_function === 'count') {
let aggregate_column, aggregate_on_field;
if (this.aggregate_function === 'count') {
aggregate_column = 'count(`tab'+ this.doctype + '`.`name`)';
} else {
aggregate_column = `${this.aggregate_function}(\`tab${this.doctype}\`.\`${this.aggregate_on}\`)`;
aggregate_column =
`${this.aggregate_function}(\`tab${this.aggregate_on_doctype}\`.\`${this.aggregate_on}\`)`;
aggregate_on_field = '`tab' + this.aggregate_on_doctype + '`.`' + this.aggregate_on + '`';
}
this.report_view.group_by = this.group_by;
@ -166,10 +181,14 @@ frappe.ui.GroupBy = class {
// rebuild fields for group by
args.fields = this.report_view.get_fields();
// add aggregate column in both query args and report view
this.report_view.fields.push(['_aggregate_column', this.doctype]);
// add aggregate column in both query args and report views
this.report_view.fields.push(['_aggregate_column', this.aggregate_on_doctype || this.doctype]);
args.fields.push(aggregate_column + ' as _aggregate_column');
if (aggregate_on_field) {
args.fields.push(aggregate_on_field);
}
// setup columns in datatable
this.report_view.setup_columns();
@ -195,7 +214,7 @@ frappe.ui.GroupBy = class {
};
} else {
// get properties of "aggregate_on", for example Net Total
docfield = Object.assign({}, frappe.meta.docfield_map[this.doctype][this.aggregate_on]);
docfield = Object.assign({}, frappe.meta.docfield_map[this.aggregate_on_doctype][this.aggregate_on]);
if (this.aggregate_function === 'sum') {
docfield.label = __('Sum of {0}', [docfield.label]);
} else {
@ -230,9 +249,12 @@ frappe.ui.GroupBy = class {
}
get_group_by_fields() {
let group_by_fields = {};
this.group_by_fields = {};
this.all_fields = {};
let fields = this.report_view.meta.fields.filter(f => ["Select", "Link", "Data", "Int"].includes(f.fieldtype));
group_by_fields[this.doctype] = fields;
this.group_by_fields[this.doctype] = fields;
this.all_fields[this.doctype] = this.report_view.meta.fields;
const standard_fields_filter = df =>
!in_list(frappe.model.no_value_type, df.fieldtype) && !df.report_hide;
@ -243,10 +265,10 @@ frappe.ui.GroupBy = class {
table_fields.forEach(df => {
const cdt = df.options;
const child_table_fields = frappe.meta.get_docfields(cdt).filter(standard_fields_filter);
group_by_fields[cdt] = child_table_fields;
this.group_by_fields[cdt] = child_table_fields;
this.all_fields[cdt] = child_table_fields;
});
return group_by_fields;
return this.group_by_fields;
}
};

View file

@ -14,17 +14,30 @@ frappe.setup.OnboardingSlide = class OnboardingSlide extends frappe.ui.Slide {
this.$next_btn = this.slides_footer.find('.next-btn');
this.$complete_btn = this.slides_footer.find('.complete-btn');
this.$action_button = this.slides_footer.find('.next-btn');
if (this.help_links) {
this.$help_links = $(`<div class="text-center">
<div class="help-links"></div>
</div>`).appendTo(this.$body);
this.setup_help_links();
}
this.$skip_btn = this.slides_footer.find('.skip-btn').on('click', () => {
$('.onboarding-dialog').modal('toggle');
});
}
setup_form() {
super.setup_form();
const fields = this.get_atomic_fields();
// remove link indicator
fields.map((field) => {
if (field.fieldtype == 'Link') {
$('.link-btn').remove();
}
});
if (fields.length == 1) {
this.$form_wrapper.addClass("text-center");
} else {
@ -33,8 +46,13 @@ frappe.setup.OnboardingSlide = class OnboardingSlide extends frappe.ui.Slide {
}
before_show() {
(this.id === 0) ?
this.$next_btn.text(__('Let\'s Start')) : this.$next_btn.text(__('Next'));
if (this.id === 0) {
this.$next_btn.text(__('Let\'s Go'));
this.$skip_btn.removeClass('hide');
} else {
this.$next_btn.text(__('Next'));
this.$skip_btn.addClass('hide');
}
//last slide
if (this.is_last_slide()) {
this.$complete_btn.removeClass('hide').addClass('action primary');
@ -143,7 +161,10 @@ frappe.setup.OnboardingDialog = class OnboardingDialog {
before_load: ($footer) => {
$footer.find('.prev-btn').addClass('hide');
$footer.find('.next-btn').removeClass('btn-default').addClass('btn-primary action');
$footer.find('.text-right').prepend(
$footer.find('.prev-div').prepend(
$(`<a class="skip-btn text-muted btn btn-link btn-sm hide">
${__("Do It Later")}</a>`));
$footer.find('.next-div').prepend(
$(`<a class="complete-btn btn btn-primary btn-sm hide">
${__("Complete")}</a>`));
}

View file

@ -77,7 +77,7 @@ frappe.ui.Slide = class Slide {
// Form methods
get_atomic_fields() {
var fields = JSON.parse(JSON.stringify(this.fields));
if(this.add_more) {
if (this.add_more) {
this.count = 1;
fields = fields.map((field, i) => {
if (field.fieldname) {
@ -149,9 +149,14 @@ frappe.ui.Slide = class Slide {
bind_fields_to_action_btn() {
var me = this;
this.reqd_fields.map((field) => {
field.$wrapper.on('change input', () => {
field.$wrapper.on('change input click', () => {
me.reset_action_button_state();
});
field.$wrapper.on('keydown', 'input', e => {
if (e.key == 'Enter') {
me.reset_action_button_state();
}
});
});
}
@ -332,10 +337,10 @@ frappe.ui.Slides = class Slides {
make_prev_next_buttons() {
$(`<div class="row">
<div class="col-sm-4">
<div class="col-sm-4 text-left prev-div">
<a class="prev-btn btn btn-default btn-sm" tabindex="0">${__("Previous")}</a>
</div>
<div class="col-sm-8 text-right">
<div class="col-sm-8 text-right next-div">
<a class="next-btn btn btn-default btn-sm" tabindex="0">${__("Next")}</a>
</div>
</div>`).appendTo(this.$footer);

View file

@ -1,154 +0,0 @@
<template>
<div
v-if="!hidden"
class="border module-box"
:class="{ 'hovered-box': hovered }"
:data-module-name="module_name"
>
<div class="flush-top">
<div class="module-box-content">
<div class="level">
<a class="module-box-link" :href="type === 'module' ? '#modules/' + module_name : link">
<h4 class="h4">
<div>
<i :class="icon_class" style="color:#8d99a6;font-size:18px;margin-right:6px;"></i>
{{ label }}
</div>
</h4>
</a>
<dropdown v-if="dropdown_links && dropdown_links.length" :items="dropdown_links">
<span class="pull-right">
<i class="octicon octicon-chevron-down text-muted"></i>
</span>
</dropdown>
</div>
</div>
</div>
</div>
</template>
<script>
import Dropdown from "./Dropdown.vue";
export default {
props: [
"index",
"name",
"label",
"category",
"type",
"module_name",
"link",
"count",
"onboard_present",
"links",
"description",
"hidden",
"icon"
],
components: {
Dropdown
},
data() {
return {
hovered: 0
};
},
computed: {
icon_class() {
if (this.icon) {
return this.icon;
} else {
return "octicon octicon-file-text";
}
},
dropdown_links() {
return this.type === 'module' ? this.links
.filter(link => !link.hidden)
.concat([
{ label: __('Customize'), action: () => this.$emit('customize'), class: 'border-top' }
]) : [];
}
},
};
</script>
<style lang="less" scoped>
@import "frappe/public/less/variables";
.module-box {
border-radius: 4px;
padding: 5px 15px;
display: block;
background-color: #ffffff;
}
.module-box.sortable-chosen {
background-color: @disabled-background;
border-color: @disabled-background;
}
.modules-container:not(.dragging) .module-box:hover {
border-color: @text-muted;
}
.hovered-box {
background-color: @light-bg;
}
.octicon-chevron-down {
font-size: 14px;
padding: 4px 6px 2px 6px;
border-radius: 4px;
&:hover {
background: @btn-bg;
}
}
.octicon-chevron-down:hover {
cursor: pointer;
}
.module-box-content {
width: 100%;
p {
margin-top: 5px;
font-size: 80%;
display: flex;
overflow: hidden;
}
}
.module-box-link {
flex: 1;
padding-top: 5px;
padding-bottom: 5px;
text-decoration: none;
--moz-text-decoration-line: none;
}
.icon-box {
padding: 15px;
width: 54px;
display: flex;
justify-content: center;
}
.icon {
font-size: 24px;
}
.open-notification {
top: -2px;
}
.shortcut-tag {
margin-right: 5px;
}
.drag-handle {
font-size: 12px;
}
</style>

View file

@ -1,109 +0,0 @@
<template>
<div>
<div class="section-header level text-muted">
<div class="module-category h6 uppercase">{{ __(this.category) }}</div>
</div>
<div class="modules-container" :class="{'dragging': dragging}" ref="modules-container">
<desk-module-box
v-for="(module, index) in modules"
:key="module.module_name"
:index="index"
v-bind="module"
@customize="show_module_card_customize_dialog(module)"
></desk-module-box>
</div>
</div>
</template>
<script>
import DeskModuleBox from "./DeskModuleBox.vue";
export default {
props: ['category', 'modules'],
components: {
DeskModuleBox
},
data() {
return {
dragging: false,
fetched_module_links: {}
}
},
mounted() {
if (!frappe.utils.is_mobile()) {
this.setup_sortable();
}
},
methods: {
setup_sortable() {
let modules_container =this.$refs['modules-container'];
this.sortable = new Sortable(modules_container, {
animation: 150,
onStart: () => this.dragging = true,
onEnd: () => {
this.dragging = false;
let modules = Array.from(modules_container.querySelectorAll('.module-box'))
.map(node => node.dataset.moduleName);
this.$emit('module-order-change', {
module_category: this.category,
modules
});
}
})
},
show_module_card_customize_dialog(module) {
const me = this;
const d = new frappe.ui.Dialog({
title: __('Customize Shortcuts'),
fields: [
{
label: __('Shortcuts'),
fieldname: 'links',
fieldtype: 'MultiSelectPills',
get_data: () => {
const module_links = me.fetched_module_links[module.module_name];
if (!module_links) {
return frappe.xcall('frappe.desk.moduleview.get_links_for_module', {
app: module.app,
module: module.module_name,
}).then(links => {
me.fetched_module_links[module.module_name] = links;
return links;
});
} else {
return module_links;
}
},
default: module.links.filter(l => !l.hidden).map(l => l.name)
}
],
primary_action_label: __('Save'),
primary_action: ({ links }) => {
frappe.call('frappe.desk.moduleview.update_links_for_module', {
module_name: module.module_name,
links: links || []
}).then(r => {
this.$emit('update-desktop-settings', r.message);
});
d.hide();
}
});
d.show();
},
}
}
</script>
<style lang="less" scoped>
.modules-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
grid-auto-rows: minmax(62px, 1fr);
column-gap: 15px;
row-gap: 15px;
align-items: center;
}
</style>

View file

@ -1,239 +0,0 @@
<template>
<div class="modules-page-container" v-if="home_settings_fetched">
<a
class="btn-show-hide text-muted text-medium"
@click="show_hide_cards_dialog"
>
{{ __('Show / Hide Cards') }}
</a>
<div
class="modules-section"
v-for="(category, i) in module_categories" :key="category"
>
<desk-section
v-if="get_modules_for_category(category).length"
:category="category"
:modules="get_modules_for_category(category)"
@update-desktop-settings="update_desktop_settings"
@module-order-change="update_module_order"
>
</desk-section>
</div>
</div>
</template>
<script>
import DeskSection from './DeskSection.vue';
import { generate_route } from './utils';
export default {
components: {
DeskSection
},
data() {
return {
module_categories: ['Modules', 'Domains', 'Places', 'Administration'],
modules: [],
home_settings_fetched: false
};
},
created() {
this.fetch_desktop_settings();
},
methods: {
fetch_desktop_settings() {
frappe.call('frappe.desk.moduleview.get_desktop_settings')
.then(r => {
if (r.message) {
this.update_desktop_settings(r.message);
this.home_settings_fetched = true;
}
});
},
update_desktop_settings(desktop_settings) {
this.modules = this.add_routes_for_module_links(desktop_settings);
},
add_routes_for_module_links(user_settings) {
for (let category in user_settings) {
user_settings[category] = user_settings[category].map(m => {
m.links = (m.links || []).map(link => {
link.route = generate_route(link);
return link;
});
return m;
});
}
return user_settings;
},
update_module_order({ module_category, modules }) {
frappe.call('frappe.desk.moduleview.update_modules_order', { module_category, modules });
},
get_modules_for_category(category) {
return this.modules[category] || [];
},
show_hide_cards_dialog() {
frappe.call('frappe.desk.moduleview.get_options_for_show_hide_cards')
.then(r => {
let { user_options, global_options } = r.message;
let user_value = `User (${frappe.session.user})`
let fields = [
{
label: __('Setup For'),
fieldname: 'setup_for',
fieldtype: 'Select',
options: [
{
label: __('User ({0})', [frappe.session.user]),
value: user_value
},
{
label: __('Everyone'),
value: 'Everyone'
}
],
default: user_value,
depends_on: doc => frappe.user_roles.includes('System Manager'),
onchange() {
let value = d.get_value('setup_for');
let field = d.get_field('setup_for');
let description = value === 'Everyone' ? __('Hide cards for all users') : '';
field.set_description(description);
}
}
];
let user_section = this.module_categories.map(category => {
let options = user_options.filter(m => m.category === category);
return {
label: category,
fieldname: `user:${category}`,
fieldtype: 'MultiCheck',
options,
columns: 2
}
}).filter(f => f.options.length > 0);
user_section = [
{
fieldname: 'user_section',
fieldtype: 'Section Break',
depends_on: doc => doc.setup_for === user_value
}
].concat(user_section);
let global_section = this.module_categories.map(category => {
let options = global_options.filter(m => m.category === category);
return {
label: category,
fieldname: `global:${category}`,
fieldtype: 'MultiCheck',
options,
columns: 2
}
}).filter(f => f.options.length > 0);
global_section = [
{
fieldname: 'global_section',
fieldtype: 'Section Break',
depends_on: doc => doc.setup_for === 'Everyone'
}
].concat(global_section);
fields = fields.concat(user_section, global_section);
let old_values = null;
const d = new frappe.ui.Dialog({
title: __('Show / Hide Cards'),
fields: fields,
primary_action_label: __('Save'),
primary_action: (values) => {
if (values.setup_for === 'Everyone') {
this.update_global_modules(d);
} else {
this.update_user_modules(d, old_values);
}
}
});
d.show();
// deepcopy
old_values = JSON.parse(JSON.stringify(d.get_values()));
});
},
update_user_modules(d, old_values) {
let new_values = d.get_values();
let category_map = {};
for (let category of this.module_categories) {
let old_modules = old_values[`user:${category}`] || [];
let new_modules = new_values[`user:${category}`] || [];
let removed = old_modules.filter(module => !new_modules.includes(module));
let added = new_modules.filter(module => !old_modules.includes(module));
category_map[category] = { added, removed };
}
frappe.call({
method: 'frappe.desk.moduleview.update_hidden_modules',
args: { category_map },
btn: d.get_primary_btn()
}).then(r => {
this.update_desktop_settings(r.message);
d.hide();
});
},
update_global_modules(d) {
let blocked_modules = [];
for (let category of this.module_categories) {
let field = d.get_field(`global:${category}`);
if (field) {
let unchecked_options = field.get_unchecked_options();
blocked_modules = blocked_modules.concat(unchecked_options);
}
}
frappe.call({
method: 'frappe.desk.moduleview.update_global_hidden_modules',
args: {
modules: blocked_modules
},
btn: d.get_primary_btn()
}).then(r => {
this.update_desktop_settings(r.message);
d.hide();
});
}
}
}
</script>
<style lang="less" scoped>
.modules-page-container {
position: relative;
margin-top: 40px;
margin-bottom: 30px;
padding-top: 1px;
}
.modules-section {
position: relative;
margin-top: 30px;
}
.btn-show-hide {
position: absolute;
right: 0;
top: 39px;
z-index: 1;
}
.toolbar-underlay {
margin: 70px;
}
</style>

View file

@ -1,84 +0,0 @@
<template>
<Popover :align="align">
<slot></slot>
<ul slot="popover-content" class="list-reset border">
<li v-for="item of dropdownItems" :key="item.label" :class="item.class || null">
<a v-if="item.route" class="list-item" :href="item.route">{{ item.label }}</a>
<div v-else class="list-item" @click="item.action">{{ item.label }}</div>
</li>
</ul>
</Popover>
</template>
<script>
import Popover from "./Popover.vue";
export default {
name: "Dropdown",
components: {
Popover
},
props: {
items: {
type: Array,
default: () => []
},
label: {
type: String,
default: "Dropdown"
},
align: {
type: String,
default: "right"
}
},
data() {
return {
isOpen: false
};
},
computed: {
dropdownItems() {
return (this.items || []).map(item => {
if (typeof item === "string") {
return {
label: item,
action: console.log
};
}
if (!item.action && item.route) {
item.action = this.setRoute.bind(this, item.route);
}
return item;
});
}
},
methods: {
setRoute(route) {
this.$router.push(route);
}
}
};
</script>
<style scoped>
.list-reset {
list-style: none;
padding: 0;
cursor: pointer;
background-color: #fff;
width: 16rem;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.12), 0 2px 4px 0 rgba(0, 0, 0, 0.08);
border-bottom-right-radius: 0.25rem;
border-bottom-left-radius: 0.25rem;
}
.list-item:hover {
background-color: #f0f4f7;
}
.list-item {
padding: 14px;
transition: all 0.1s ease-in;
}
a {
font-size: 12px;
text-decoration: none;
}
</style>

View file

@ -1,57 +0,0 @@
<template>
<div>
<div v-if="sections.length" class="sections-container">
<div v-for="section in sections"
:key="section.label"
class="border section-box"
>
<h4 class="h4"> {{ section.label }} </h4>
<module-link-item v-for="item in section.items"
:key="section.label + item.label"
:data-youtube-id="item.type==='help' ? item.youtube_id : false"
v-bind="item"
>
</module-link-item>
</div>
</div>
<div v-else class="sections-container">
<div v-for="n in 3" :key="n" class="skeleton-section-box"></div>
</div>
</div>
</template>
<script>
import ModuleLinkItem from "./ModuleLinkItem.vue";
export default {
components: {
ModuleLinkItem
},
props: ['module_name', 'sections'],
}
</script>
<style lang="less" scoped>
.sections-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
column-gap: 15px;
row-gap: 15px;
}
.section-box {
padding: 5px 20px;
border-radius: 4px;
}
.skeleton-section-box {
background-color: #f5f7fa;
height: 250px;
border-radius: 4px;
}
.h4 {
margin-bottom: 15px;
}
</style>

View file

@ -1,116 +0,0 @@
<template>
<div class="link-item flush-top small"
:class="{'onboard-spotlight': onboard, 'disabled-link': disabled_dependent}"
@mouseover="mouseover" @mouseleave="mouseleave"
>
<span :class="['indicator', indicator_color]"></span>
<span v-if="disabled_dependent" class="link-content text-muted">{{ label || __(name) }}</span>
<a v-else class="link-content" :href="route" @click.prevent="handle_click">
{{ label || __(name) }}
</a>
<div v-if="disabled_dependent" v-show="popover_active"
@mouseover="popover_hover = true" @mouseleave="popover_hover = false"
class="module-link-popover popover fade top in" role="tooltip"
>
<div class="arrow"></div>
<h3 class="popover-title" style="display: none;"></h3>
<div class="popover-content" style="padding: 12px;">
<div class="small text-muted">{{ __("You need to create these first: ") }}</div>
<div class="small">{{ __(incomplete_dependencies.join(", ")) }}</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: ['label', 'name', 'dependencies', 'incomplete_dependencies',
'onboard', 'count', 'route', 'doctype', 'open_count', 'youtube_id'],
data() {
return {
hover: false,
popover_hover: false,
}
},
computed: {
disabled_dependent() {
return this.dependencies && this.incomplete_dependencies;
},
indicator_color() {
if(this.open_count) {
return 'red';
}
if(this.onboard) {
return this.count ? 'blue' : 'orange';
};
return 'grey';
},
popover_active() {
return this.popover_hover || this.hover;
}
},
methods: {
mouseover() {
$('.module-link-popover').hide();
this.hover = true;
},
mouseleave() {
setTimeout(() => {
this.hover = false;
}, 300);
},
handle_click(e) {
if (this.youtube_id) {
frappe.help.show_video(this.youtube_id);
} else {
frappe.set_route(this.route);
}
}
}
}
</script>
<style lang="less" scoped>
.link-item {
position: relative;
margin: 10px 0px;
cursor: default;
}
.onboard-spotlight {
.link-content {
font-weight: 600;
}
}
a:hover, a:focus {
text-decoration: underline;
}
// Overriding indicator styles
.indicator {
margin-right: 5px;
color: inherit;
font-weight: inherit;
}
.link-content {
flex: 1;
}
.popover {
display: block;
top: -60px;
max-width: 220px;
}
.popover.top > .arrow {
left: 20%;
}
</style>

View file

@ -1,103 +0,0 @@
<template>
<div class="modules-page-container">
<module-detail
v-if="
this.route && modules_list.map(m => m.module_name).includes(route[1])
"
:module_name="route[1]"
:sections="current_module_sections"
></module-detail>
</div>
</template>
<script>
import ModuleDetail from './ModuleDetail.vue'
import { generate_route } from './utils.js'
export default {
components: {
ModuleDetail,
},
data() {
return {
route: frappe.get_route(),
current_module_label: '',
current_module_sections: [],
modules_data_cache: {},
modules_list: frappe.boot.allowed_modules.filter(
d => (d.type === 'module' || d.category === 'Places') && !d.blocked
),
}
},
created() {
this.update_current_module()
},
mounted() {
frappe.module_links = {}
frappe.route.on('change', () => {
this.update_current_module()
})
},
methods: {
update_current_module() {
let route = frappe.get_route()
if (route[0] === 'modules') {
this.route = route
let module = this.modules_list.filter(m => m.module_name == route[1])[0]
let module_name = module && (module.label || module.module_name)
let title = this.current_module_label
? this.current_module_label
: module_name
frappe.modules.home && frappe.modules.home.page.set_title(title)
if (!frappe.modules.home) {
setTimeout(() => {
frappe.modules.home.page.set_title(title)
}, 200)
}
if (module_name) {
this.get_module_sections(module.module_name)
}
}
},
get_module_sections(module_name) {
let cache = this.modules_data_cache[module_name]
if (cache) {
this.current_module_sections = cache
} else {
this.current_module_sections = []
return frappe.call({
method: 'frappe.desk.moduleview.get',
args: {
module: module_name,
},
callback: r => {
var m = frappe.get_module(module_name)
this.current_module_sections = r.message.data
this.process_data(module_name, this.current_module_sections)
this.modules_data_cache[module_name] = this.current_module_sections
},
freeze: true,
})
}
},
process_data(module_name, data) {
frappe.module_links[module_name] = []
data.forEach(function(section) {
section.items.forEach(function(item) {
item.route = generate_route(item)
})
})
},
},
}
</script>
<style lang="less" scoped>
.modules-page-container {
margin: 15px 0px;
}
</style>

View file

@ -1,101 +0,0 @@
<template>
<div class="inline-block relative" :class="{ 'w-full': this.fullwidth }" v-outside="closePopover">
<div @click="togglePopover">
<slot :togglePopover="togglePopover" :closePopover="closePopover"></slot>
</div>
<div v-show="isOpen" class="absolute mt-default z-20" :class="popoverClasses">
<slot name="popover-content"></slot>
</div>
</div>
</template>
<script>
let instances = [];
function onDocumentClick(e, el, fn) {
let target = e.target;
if (el !== target && !el.contains(target)) {
fn(e);
}
}
export default {
name: "Popover",
props: {
align: {
default: "left"
},
fullwidth: {
default: false
}
},
data() {
return {
isOpen: false
};
},
directives: {
outside: {
bind(el, binding) {
el.dataset.outsideClickIndex = instances.length;
const fn = binding.value;
const click = function(e) {
onDocumentClick(e, el, fn);
};
document.addEventListener("click", click);
instances.push(click);
},
unbind(el) {
const index = el.dataset.outsideClickIndex;
const handler = instances[index];
document.addEventListener("click", handler);
instances.splice(index, 1);
}
}
},
computed: {
popoverClasses() {
return {
"pin-r": this.align === "right",
"pin-l": this.align === "left",
"w-full": this.fullwidth === true
};
}
},
methods: {
togglePopover() {
this.isOpen = !this.isOpen;
},
closePopover() {
this.isOpen = false;
}
}
};
</script>
<style scoped>
.relative {
position: relative;
}
.inline-block {
display: inline-block;
}
.w-full {
width: 100%;
}
.pin-r {
right: 0;
}
.pin-l {
left: 0;
}
.absolute {
position: absolute;
}
.mt-default {
margin-top: 25px;
}
.z-20 {
z-index: 20;
}
</style>

View file

@ -1,45 +0,0 @@
function generate_route(item) {
if(item.type==="doctype") {
item.doctype = item.name;
}
let route = '';
if(!item.route) {
if(item.link) {
route=strip(item.link, "#");
} else if(item.type==="doctype") {
if(frappe.model.is_single(item.doctype)) {
route = 'Form/' + item.doctype;
} else {
if (item.filters) {
frappe.route_options=item.filters;
}
route="List/" + item.doctype;
}
} else if(item.type==="report" && item.is_query_report) {
route="query-report/" + item.name;
} else if(item.type==="report") {
route="List/" + item.doctype + "/Report/" + item.name;
} else if(item.type==="page") {
route=item.name;
}
route = '#' + route;
} else {
route = item.route;
}
if(item.route_options) {
route += "?" + $.map(item.route_options, function(value, key) {
return encodeURIComponent(key) + "=" + encodeURIComponent(value); }).join('&');
}
// if(item.type==="page" || item.type==="help" || item.type==="report" ||
// (item.doctype && frappe.model.can_read(item.doctype))) {
// item.shown = true;
// }
return route;
}
export {
generate_route
};

View file

@ -0,0 +1,310 @@
import ChartWidget from "../widgets/chart_widget";
import WidgetGroup from "../widgets/widget_group";
export default class Desktop {
constructor({ wrapper }) {
this.wrapper = wrapper;
window.desk = this;
this.pages = {};
this.sidebar_items = {};
this.sidebar_categories = [
"Modules",
"Domains",
"Places",
"Administration"
];
this.make();
}
make() {
this.make_container();
// this.show_loading_state();
this.fetch_desktop_settings().then(() => {
this.route();
this.make_sidebar();
this.setup_events();
// this.hide_loading_state();
});
}
route() {
let page = this.get_page_to_show();
this.show_page(page);
}
make_container() {
this.container = $(`<div class="desk-container row">
<div class="desk-sidebar"></div>
<div class="desk-body"></div>
</div>`);
this.container.appendTo(this.wrapper);
this.sidebar = this.container.find(".desk-sidebar");
this.body = this.container.find(".desk-body");
}
show_loading_state() {
// Add skeleton
let loading_sidebar = $(
'<div class="skeleton skeleton-full" style="height: 90vh;"></div>'
);
let loading_body = $(
`<div class="skeleton skeleton-full" style="height: 90vh;"></div>`
);
// Append skeleton to body
loading_sidebar.appendTo(this.sidebar);
loading_body.appendTo(this.body);
}
hide_loading_state() {
// Remove all skeleton
this.container.find(".skeleton").remove();
}
fetch_desktop_settings() {
return frappe
.call("frappe.desk.desktop.get_desk_sidebar_items")
.then(response => {
if (response.message) {
this.desktop_settings = response.message;
} else {
frappe.throw({
title: "Couldn't Load Desk",
message:
"Something went wrong while loading Desk. <b>Please relaod the page</b>. If the problem persists, contact the Administrator",
indicator: "red",
primary_action: {
label: "Reload",
action: () => location.reload()
}
});
}
});
}
make_sidebar() {
const get_sidebar_item = function(item) {
return $(`<a href="${"desk#workspace/" +
item.name}" class="sidebar-item ${
item.selected ? "selected" : ""
}">
<span>${item.name}</span>
</div>`);
};
const make_sidebar_category_item = item => {
if (item.name == this.get_page_to_show()) {
item.selected = true;
this.current_page = item.name;
}
let $item = get_sidebar_item(item);
$item.appendTo(this.sidebar);
this.sidebar_items[item.name] = $item;
};
const make_category_title = name => {
let $title = $(
`<div class="sidebar-group-title h6 uppercase">${name}</div>`
);
$title.appendTo(this.sidebar);
};
this.sidebar_categories.forEach(category => {
if (this.desktop_settings.hasOwnProperty(category)) {
make_category_title(category);
this.desktop_settings[category].forEach(item => {
make_sidebar_category_item(item);
});
}
});
}
show_page(page) {
if (this.current_page && this.pages[this.current_page]) {
this.pages[this.current_page].hide();
}
if (this.sidebar_items && this.sidebar_items[this.current_page]) {
this.sidebar_items[this.current_page].removeClass("selected");
this.sidebar_items[page].addClass("selected");
}
this.current_page = page;
localStorage.current_desk_page = page;
frappe.set_route("workspace", page);
this.pages[page] ? this.pages[page].show() : this.make_page(page);
}
get_page_to_show() {
let page =
frappe.get_route()[1] ||
localStorage.current_desk_page ||
this.desktop_settings["Modules"][0].name;
return page;
}
make_page(page) {
const $page = new DesktopPage({
container: this.body,
page_name: page
});
this.pages[page] = $page;
return $page;
}
setup_events() {
$(document).keydown(e => {
if (e.keyCode == 9) {
console.log("navigate");
}
});
}
}
class DesktopPage {
constructor({ container, page_name }) {
this.container = container;
this.page_name = page_name;
this.sections = {};
this.allow_customization = false
this.make();
}
show() {
this.page.show();
}
hide() {
this.page.hide();
}
make() {
this.make_page();
this.get_data().then(res => {
this.data = res.message;
this.allow_customization = this.data.allow_customization
// this.make_onboarding()
if (!this.data) {
delete localStorage.current_desk_page
frappe.set_route('workspace')
}
!this.sections["onboarding"] &&
this.data.charts.items.length &&
this.make_charts();
this.data.shortcuts.items.length && this.make_shortcuts();
this.data.cards.items.length && this.make_cards();
});
}
make_page() {
this.page = $(
`<div class="desk-page" data-page-name=${this.page_name}></div>`
);
this.page.appendTo(this.container);
}
get_data() {
return frappe.call("frappe.desk.desktop.get_desktop_page", {
page: this.page_name
});
}
make_onboarding() {
this.sections["onboarding"] = new WidgetGroup({
title: `Getting Started`,
container: this.page,
type: "onboarding",
columns: 1,
widgets: [
{
label: "Unlock Great Customer Experience",
subtitle: "Just a few steps, and youre good to go.",
steps: [
{
label: "Configure Lead Sources",
completed: true
},
{
label: "Add Your Leads",
completed: false
},
{
label: "Create Your First Opportunity",
completed: false
},
{
label: "Onboard your Sales Team",
completed: false
},
{
label: "Assign Territories",
completed: false
}
]
}
]
});
}
make_charts() {
this.sections["charts"] = new WidgetGroup({
title: this.data.charts.label || `${this.page_name} Dashboard`,
container: this.page,
type: "chart",
columns: 1,
allow_sorting: false,
widgets: this.data.charts.items
});
}
make_shortcuts() {
this.sections["shortcuts"] = new WidgetGroup({
title: this.data.shortcuts.label || `Your Shortcuts`,
container: this.page,
type: "bookmark",
columns: 3,
allow_sorting: this.allow_customization && frappe.is_mobile(),
widgets: this.data.shortcuts.items
});
}
make_cards() {
let cards = new WidgetGroup({
title: this.data.cards.label || `Reports & Masters`,
container: this.page,
type: "links",
columns: 3,
allow_sorting: this.allow_customization && frappe.is_mobile(),
widgets: this.data.cards.items
});
this.sections["cards"] = cards;
const legend = [
{
color: "blue",
description: __("Important")
},
{
color: "orange",
description: __("No Records Created")
},
{
color: "red",
description: __("DocType has Open Entries")
}
].map(item => {
return `<div class="legend-item small text-muted justify-flex-start">
<span class="indicator ${item.color}"></span>
<span class="link-content ellipsis" draggable="false">${item.description}</span>
</div>`;
});
$(`<div class="legend">
${legend.join("\n")}
</div>`).insertAfter(cards.body);
}
}

View file

@ -1,7 +1,6 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// MIT License. See license.txt
import Desktop from './components/Desktop.vue';
import Desktop from './desktop/desktop.js';
frappe.provide('frappe.views.pageview');
frappe.provide("frappe.standard_pages");
@ -38,24 +37,25 @@ frappe.views.pageview = {
});
}
},
show: function(name) {
if(!name) {
name = (frappe.boot ? frappe.boot.home_page : window.page_name);
if(name === "desktop") {
if(!frappe.pages.desktop) {
let page = frappe.container.add_page('desktop');
if(name === "workspace") {
if(!frappe.workspace) {
let page = frappe.container.add_page('workspace');
let container = $('<div class="container"></div>').appendTo(page);
container = $('<div></div>').appendTo(container);
new Vue({
el: container[0],
render: h => h(Desktop)
});
frappe.workspace = new Desktop({
wrapper: container
})
}
frappe.container.change_to('desktop');
frappe.utils.set_title(__('Home'));
frappe.container.change_to('workspace');
frappe.workspace.route();
frappe.utils.set_title(__('Desk'));
return;
}
}
@ -186,6 +186,4 @@ frappe.views.ModulesFactory = class ModulesFactory extends frappe.views.Factory
});
});
}
};
};

View file

@ -14,6 +14,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
super.setup_defaults();
this.page_title = __('Report:') + ' ' + this.page_title;
this.menu_items = this.report_menu_items();
this.view = 'Report';
const route = frappe.get_route();
if (route.length === 4) {
@ -936,7 +937,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
}
}
}
if (!docfield) return;
if (!docfield || docfield.report_hide) return;
let title = __(docfield ? docfield.label : toTitle(fieldname));
if (doctype !== this.doctype) {
@ -1034,7 +1035,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
return {
name: name,
doctype: col.docfield.parent,
content: d[cdt_field(col.field)],
content: d[cdt_field(col.field)] || d[col.field],
editable: Boolean(name && this.is_editable(col.docfield, d)),
format: value => {
return frappe.format(value, col.docfield, { always_show_decimals: true }, d);

View file

@ -0,0 +1,55 @@
export default class Widget {
constructor(opts) {
Object.assign(this, opts);
this.make();
}
refresh() {
//
}
customize() {
}
make() {
this.make_widget();
this.widget.appendTo(this.container);
this.setup_events();
}
make_widget() {
this.widget = $(`<div class="widget">
<div class="widget-head">
<div class="widget-title"></div>
<div class="widget-control"></div>
</div>
<div class="widget-body">
</div>
</div>`);
this.title_field = this.widget.find(".widget-title");
this.body = this.widget.find(".widget-body");
this.action_area = this.widget.find(".widget-control");
this.head = this.widget.find(".widget-head");
this.set_title();
this.set_actions();
this.set_body();
}
set_title() {
this.title_field[0].innerHTML = this.label || this.name;
}
set_actions() {
//
}
set_body() {
//
}
setup_events() {
//
}
}

View file

@ -0,0 +1,60 @@
import Widget from "./base_widget.js";
export default class ChartWidget extends Widget {
constructor(opts) {
super(opts);
}
refresh() {
//
}
customize() {
this.setup_customize_actions();
}
make_chart() {
this.body.empty()
frappe.model.with_doc("Dashboard Chart", this.chart_name).then(chart_doc => {
chart_doc.width = 'Full'
this.dashboard = new frappe.ui.DashboardChart(chart_doc, this.body, { hide_title: true, hide_last_sync: true, hide_actions: true });
this.dashboard.show();
});
this.summary && this.set_summary();
}
set_body() {
this.widget.addClass('dashboard-widget-box')
this.make_chart();
}
set_summary() {
let summary = $(`<span class="dashboard-summary">$ 54,231</span>`);
this.title_field.addClass('text-muted')
summary.appendTo(this.body);
}
setup_events() {
//
}
setup_customize_actions() {
this.action_area.empty()
const buttons = $(`<button type="button" class="btn btn-xs btn-secondary btn-default selected">Resize</button>
<button class="btn btn-secondary btn-light btn-danger btn-xs"><i class="fa fa-trash" aria-hidden="true"></i></button>`);
buttons.appendTo(this.action_area);
}
set_actions() {
return
this.action_area.empty()
const buttons = $(`<div class="btn-group btn-group-xs" role="group" aria-label="Basic example">
<button type="button" class="btn btn-secondary btn-default selected">Monthly</button>
<button type="button" class="btn btn-secondary btn-default">Quaterly</button>
<button type="button" class="btn btn-secondary btn-default">Yearly</button>
</div>
<button class="btn btn-secondary btn-light btn-default btn-xs"><i class="fa fa-refresh" aria-hidden="true"></i></button>`);
buttons.appendTo(this.action_area);
}
}

View file

@ -0,0 +1,96 @@
import Widget from "./base_widget.js";
import { generate_route } from "./utils";
export default class LinksWidget extends Widget {
constructor(opts) {
super(opts);
}
refresh() {
//
}
set_body() {
this.options = {};
this.options.links = this.links;
this.widget.addClass("links-widget-box");
const is_link_disabled = item => {
return item.dependencies && item.incomplete_dependencies;
};
const disabled_dependent = item => {
return is_link_disabled(item) ? "disabled-link" : "";
};
const get_indicator_color = item => {
if (item.open_count) {
return "red";
}
if (item.onboard) {
return item.count ? "blue" : "orange";
}
return "grey";
};
const get_link_for_item = item => {
if (is_link_disabled(item)) {
return `<span class="link-content ellipsis disabled-link">${
item.label ? item.label : item.name
}</span>
<div class="module-link-popover popover fade top in" role="tooltip" style="display: none;">
<div class="arrow"></div>
<h3 class="popover-title" style="display: none;"></h3>
<div class="popover-content" style="padding: 12px;">
<div class="small text-muted">${__("You need to create these first: ")}</div>
<div class="small">${item.incomplete_dependencies.join(", ")}</div>
</div>
</div>`;
}
if (item.youtube_id)
return `<span class="link-content help-video-link ellipsis" data-youtubeid="${item.youtube_id}">
${item.label ? item.label : item.name}</span>`;
return `<a data-route="${generate_route(item)}" class="link-content ellipsis">
${item.label ? item.label : item.name}</a>`;
};
this.link_list = this.links.map(item => {
return $(`<div class="link-item flush-top small ${
item.onboard ? "onboard-spotlight" : ""
} ${disabled_dependent(item)}" type="${item.type}">
<span class="indicator ${get_indicator_color(item)}"></span>
${get_link_for_item(item)}
</div>`);
});
this.link_list.forEach(link => link.appendTo(this.body));
}
setup_events() {
this.link_list.forEach(link => {
// Bind Popver Event
const link_label = link.find(".link-content");
if (link.hasClass("disabled-link")) {
const popover = link.find(".module-link-popover");
link_label.mouseover(() => {
popover.show();
});
link_label.mouseout(() => popover.hide());
} else {
if (link_label.hasClass("help-video-link")) {
link_label.click(event => {
let yt_id = event.target.dataset.youtubeid;
frappe.help.show_video(yt_id);
});
} else {
link_label.click(event => {
let route = event.target.dataset.route;
frappe.set_route(route);
});
}
}
});
}
}

View file

@ -0,0 +1,44 @@
import Widget from "./base_widget.js";
export default class OnboardingWidget extends Widget {
constructor(opts) {
super(opts);
window.onb = this;
}
refresh() { }
customize() { }
make_body() {
this.steps.forEach(step => {
this.add_step(step);
})
}
add_step(step) {
let $step = $(`<div class="onboarding-step">
<i class="fa fa-check-circle ${step.completed ? 'complete' : 'incomplete'}" aria-hidden="true"></i>${step.label}
</div>`)
$step.appendTo(this.body)
return $step
}
set_body() {
this.widget.addClass('onboarding-widget-box')
this.make_body();
}
set_title() {
super.set_title();
let subtitle = $(`<div class="widget-subtitle">${this.subtitle}</div>`)
subtitle.appendTo(this.head);
}
setup_events() { }
setup_customize_actions() { }
set_actions() { }
}

View file

@ -0,0 +1,68 @@
import Widget from "./base_widget.js";
import { generate_route } from "./utils";
// import { get_luminosity, shadeColor } from "./utils";
String.prototype.format = function () {
var i = 0, args = arguments;
return this.replace(/{}/g, function () {
return typeof args[i] != 'undefined' ? args[i++] : '';
});
};
export default class ShortcutWidget extends Widget {
constructor(opts) {
super(opts);
}
refresh() {
//
}
setup_events() {
this.widget.click(() => {
let route = generate_route(this)
frappe.set_route(route)
})
}
set_actions() {
this.widget.addClass('shortcut-widget-box');
const get_filter = new Function(`return ${this.stats_filter}`)
if (this.type == "DocType" && this.stats_filter) {
frappe.db.count(this.link_to, {
filters: get_filter()
}).then(count => this.set_count(count))
}
}
set_title() {
if (this.icon) {
this.title_field[0].innerHTML = `<div>
<i class="${this.icon}" style="color: rgb(141, 153, 166); font-size: 18px; margin-right: 6px;"></i>
${this.label || this.name}
</div>`
}
else {
super.set_title();
}
}
set_count(count) {
const get_label = () => {
if (this.format) {
return this.format.format(count);
}
return count
}
this.action_area.empty();
const label = get_label();
const buttons = $(`<div class="small pill">${label}</div>`);
if(this.color) {
buttons.css('background-color', this.color);
buttons.css('color', frappe.ui.color.get_contrast_color(this.color))
}
buttons.appendTo(this.action_area);
}
}

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