Merge branch 'develop' into cust_export

This commit is contained in:
Suraj Shetty 2020-04-30 16:32:52 +05:30 committed by GitHub
commit 131cbec99a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
65 changed files with 2040 additions and 243 deletions

View file

@ -44,4 +44,21 @@ context('Form', () => {
list_view.filter_area.filter_list.clear_filters();
});
});
it('validates behaviour of Data options validations in child table', () => {
// test email validations for set_invalid controller
let website_input = 'website.in';
let expectBackgroundColor = 'rgb(255, 220, 220)';
cy.visit('/desk#Form/Contact/New Contact 1');
cy.get('.frappe-control[data-fieldname="email_ids"]').as('table');
cy.get('@table').find('button.grid-add-row').click();
cy.get('.grid-body .rows [data-fieldname="email_id"]').click();
cy.get('@table').find('input.input-with-feedback.form-control').as('email_input');
cy.get('@email_input').type(website_input, { waitForAnimations: false });
cy.fill_field('company_name', 'Test Company');
cy.get('@email_input').should($div => {
const style = window.getComputedStyle($div[0]);
expect(style.backgroundColor).to.equal(expectBackgroundColor);
});
});
});

View file

@ -16,7 +16,7 @@ global_cache_keys = ("app_hooks", "installed_apps",
'scheduler_events', 'time_zone', 'webhooks', 'active_domains',
'active_modules', 'assignment_rule', 'server_script_map', 'wkhtmltopdf_version',
'domain_restricted_doctypes', 'domain_restricted_pages', 'information_schema:counts',
'sitemap_routes')
'sitemap_routes', 'db_tables')
user_cache_keys = ("bootinfo", "user_recent", "roles", "user_doc", "lang",
"defaults", "user_permissions", "home_page", "linked_with",

View file

@ -20,7 +20,7 @@ class TestExporter(unittest.TestCase):
e = Exporter('Web Page', export_fields='All')
csv_array = e.get_csv_array()
header = csv_array[0]
self.assertEqual(len(header), 35)
self.assertEqual(len(header), 36)
def test_exports_selected_fields(self):

View file

@ -712,9 +712,10 @@ def validate_fields(meta):
if d.fieldtype == "Currency" and cint(d.width) < 100:
frappe.throw(_("Max width for type Currency is 100px in row {0}").format(d.idx))
def check_in_list_view(d):
def check_in_list_view(is_table, d):
if d.in_list_view and (d.fieldtype in not_allowed_in_list_view):
frappe.throw(_("'In List View' not allowed for type {0} in row {1}").format(d.fieldtype, d.idx))
property_label = 'In Grid View' if is_table else 'In List View'
frappe.throw(_("'{0}' not allowed for type {1} in row {2}").format(property_label, d.fieldtype, d.idx))
def check_in_global_search(d):
if d.in_global_search and d.fieldtype in no_value_fields:
@ -906,6 +907,16 @@ def validate_fields(meta):
frappe.msgprint(text_str + df_options_str, title="Invalid Data Field", raise_exception=True)
def check_child_table_option(docfield):
if docfield.fieldtype not in ['Table MultiSelect', 'Table']: return
doctype = docfield.options
meta = frappe.get_meta(doctype)
if not meta.istable:
frappe.throw(_('Option {0} for field {1} is not a child table')
.format(frappe.bold(doctype), frappe.bold(docfield.fieldname)), title=_("Invalid Option"))
fields = meta.get("fields")
fieldname_list = [d.fieldname for d in fields]
@ -929,11 +940,12 @@ def validate_fields(meta):
check_link_table_options(meta.get("name"), d)
check_dynamic_link_options(d)
check_hidden_and_mandatory(meta.get("name"), d)
check_in_list_view(d)
check_in_list_view(meta.get('istable'), d)
check_in_global_search(d)
check_illegal_default(d)
check_unique_and_text(meta.get("name"), d)
check_illegal_depends_on_conditions(d)
check_child_table_option(d)
check_table_multiselect_option(d)
scrub_options_in_select(d)
scrub_fetch_from(d)

View file

@ -76,7 +76,16 @@ class Dashboard {
}
refresh() {
this.get_permitted_dashboard_charts().then(charts => {
frappe.run_serially([
() => this.render_cards(),
() => this.render_charts()
]);
}
render_charts() {
return this.get_permitted_items(
'frappe.desk.doctype.dashboard.dashboard.get_permitted_charts'
).then(charts => {
if (!charts.length) {
frappe.msgprint(__('No Permitted Charts on this Dashboard'), __('No Permitted Charts'))
}
@ -92,6 +101,7 @@ class Dashboard {
...chart
}
});
this.chart_group = new frappe.widget.WidgetGroup({
title: null,
container: this.container,
@ -110,14 +120,46 @@ class Dashboard {
});
}
get_permitted_dashboard_charts() {
render_cards() {
return this.get_permitted_items(
'frappe.desk.doctype.dashboard.dashboard.get_permitted_cards'
).then(cards => {
if (!cards.length) {
return;
}
this.number_cards =
cards.map(card => {
return {
name: card.card,
};
});
this.number_card_group = new frappe.widget.WidgetGroup({
container: this.container,
type: "number_card",
columns: 3,
options: {
allow_sorting: false,
allow_create: false,
allow_delete: false,
allow_hiding: false,
allow_edit: false,
},
widgets: this.number_cards,
});
});
}
get_permitted_items(method) {
return frappe.xcall(
'frappe.desk.doctype.dashboard.dashboard.get_permitted_charts',
method,
{
dashboard_name: this.dashboard_name
}).then(charts => {
return charts;
});
}
).then(items => {
return items;
});
}
set_dropdown() {

View file

@ -124,6 +124,8 @@ class Database(object):
# in transaction validations
self.check_transaction_status(query)
self.clear_db_table_cache(query)
# autocommit
if auto_commit: self.commit()
@ -277,6 +279,11 @@ class Database(object):
ret.append(frappe._dict(zip(keys, values)))
return ret
@staticmethod
def clear_db_table_cache(query):
if query and query.strip().split()[0].lower() in {'drop', 'create'}:
frappe.cache().delete_key('db_tables')
@staticmethod
def needs_formatting(result, formatted):
"""Returns true if the first row in the result has a Date, Datetime, Long Int."""
@ -769,7 +776,16 @@ class Database(object):
return ("tab" + doctype) in self.get_tables()
def get_tables(self):
return [d[0] for d in self.sql("select table_name from information_schema.tables where table_schema not in ('pg_catalog', 'information_schema')")]
tables = frappe.cache().get_value('db_tables')
if not tables:
table_rows = self.sql("""
SELECT table_name
FROM information_schema.tables
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
""")
tables = {d[0] for d in table_rows}
frappe.cache().set_value('db_tables', tables)
return tables
def a_row_exists(self, doctype):
"""Returns True if atleast one row exists."""

View file

@ -137,16 +137,14 @@ class DBTable:
if frappe.db.is_missing_column(e):
# Unknown column 'column_name' in 'field list'
continue
else:
raise
raise
if max_length and max_length[0][0] and max_length[0][0] > new_length:
if col.fieldname in self.columns:
self.columns[col.fieldname].length = current_length
frappe.msgprint(_("""Reverting length to {0} for '{1}' in '{2}';
Setting the length as {3} will cause truncation of data.""")
.format(current_length, col.fieldname, self.doctype, new_length))
info_message = _("Reverting length to {0} for '{1}' in '{2}'. Setting the length as {3} will cause truncation of data.") \
.format(current_length, col.fieldname, self.doctype, new_length)
frappe.msgprint(info_message)
def is_new(self):
return self.table_name not in frappe.db.get_tables()

View file

@ -4,5 +4,21 @@
frappe.ui.form.on('Dashboard', {
refresh: function(frm) {
frm.add_custom_button(__("Show Dashboard"), () => frappe.set_route('dashboard', frm.doc.name));
frm.set_query("chart", "charts", function() {
return {
filters: {
is_public: 1
}
};
});
frm.set_query("card", "cards", function() {
return {
filters: {
is_public: 1
}
};
});
}
});

View file

@ -8,7 +8,8 @@
"field_order": [
"dashboard_name",
"is_default",
"charts"
"charts",
"cards"
],
"fields": [
{
@ -31,10 +32,16 @@
"label": "Charts",
"options": "Dashboard Chart Link",
"reqd": 1
},
{
"fieldname": "cards",
"fieldtype": "Table",
"label": "Cards",
"options": "Number Card Link"
}
],
"links": [],
"modified": "2020-03-25 21:09:37.080132",
"modified": "2020-04-19 17:44:36.237163",
"modified_by": "Administrator",
"module": "Desk",
"name": "Dashboard",

View file

@ -21,3 +21,12 @@ def get_permitted_charts(dashboard_name):
if frappe.has_permission('Dashboard Chart', doc=chart.chart):
permitted_charts.append(chart)
return permitted_charts
@frappe.whitelist()
def get_permitted_cards(dashboard_name):
permitted_cards = []
dashboard = frappe.get_doc('Dashboard', dashboard_name)
for card in dashboard.cards:
if frappe.has_permission('Number Card', doc=card.card):
permitted_cards.append(card)
return permitted_cards

View file

@ -9,6 +9,7 @@ frappe.ui.form.on('Dashboard Chart', {
frm.add_fetch('source', 'timeseries', 'timeseries');
},
refresh: function(frm) {
frm.chart_filters = null;
frm.add_custom_button('Add Chart to Dashboard', () => {

View file

@ -22,6 +22,7 @@
"aggregate_function_based_on",
"number_of_groups",
"column_break_6",
"is_public",
"timespan",
"from_date",
"to_date",
@ -99,7 +100,7 @@
},
{
"default": "0",
"depends_on": "eval:doc.chart_type !== 'Group By'",
"depends_on": "eval: ['Count', 'Sum', 'Average'].includes(doc.chart_type)",
"fieldname": "timeseries",
"fieldtype": "Check",
"label": "Time Series"
@ -220,10 +221,18 @@
"fieldname": "custom_options",
"fieldtype": "Code",
"label": "Custom Options"
},
{
"default": "0",
"description": "This chart will be public to all Users if this is set",
"fieldname": "is_public",
"fieldtype": "Check",
"label": "Is Public",
"permlevel": 1
}
],
"links": [],
"modified": "2020-04-20 23:49:11.389909",
"modified": "2020-04-23 13:01:07.178866",
"modified_by": "Administrator",
"module": "Desk",
"name": "Dashboard Chart",
@ -254,6 +263,7 @@
"write": 1
},
{
"create": 1,
"email": 1,
"export": 1,
"print": 1,

View file

@ -92,20 +92,25 @@ def get(chart_name = None, chart = None, no_cache = None, filters = None, from_d
return chart_config
@frappe.whitelist()
def create_report_chart(args):
def create_dashboard_chart(args):
args = frappe.parse_json(args)
_doc = frappe.new_doc('Dashboard Chart')
doc = frappe.new_doc('Dashboard Chart')
_doc.update(args)
doc.update(args)
if (args.get("custom_options")):
_doc.custom_options = json.dumps(args.get("custom_options"))
if args.get('custom_options'):
doc.custom_options = json.dumps(args.get('custom_options'))
if frappe.db.exists('Dashboard Chart', args.chart_name):
args.chart_name = append_number_if_name_exists('Dashboard Chart', args.chart_name)
_doc.chart_name = args.chart_name
_doc.insert(ignore_permissions=True)
doc.chart_name = args.chart_name
doc.insert(ignore_permissions=True)
return doc
@frappe.whitelist()
def create_report_chart(args):
create_dashboard_chart()
if args.dashboard:
add_chart_to_dashboard(json.dumps(args))
@ -356,6 +361,13 @@ def get_year_ending(date):
# last day of this month
return add_to_date(date, days=-1)
def get_charts_for_user(doctype, txt, searchfield, start, page_len, filters):
or_filters = {'owner': frappe.session.user, 'is_public': 1}
return frappe.db.get_list('Dashboard Chart',
fields=['name'],
filters=filters,
or_filters=or_filters,
as_list = 1)
class DashboardChart(Document):

View file

@ -0,0 +1,114 @@
// Copyright (c) 2020, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Number Card', {
refresh: function(frm) {
frm.set_df_property("filters_section", "hidden", 1);
frm.trigger('set_options');
frm.trigger('render_filters_table');
},
document_type: function(frm) {
frm.set_query('document_type', function() {
return {
filters: {
'issingle': false
}
};
});
frm.set_value('filters_json', '[]');
frm.set_value('aggregate_function_based_on', '');
if (frm.doc.document_type) {
frm.trigger('set_options');
}
},
set_options: function(frm) {
let aggregate_based_on_fields = [];
const doctype = frm.doc.document_type;
frappe.model.with_doctype(doctype, () => {
frappe.get_meta(doctype).fields.map(df => {
if (frappe.model.numeric_fieldtypes.includes(df.fieldtype)) {
aggregate_based_on_fields.push({label: df.label, value: df.fieldname});
}
});
frm.set_df_property('aggregate_function_based_on', 'options', aggregate_based_on_fields);
});
},
render_filters_table: function(frm) {
frm.set_df_property("filters_section", "hidden", 0);
let wrapper = $(frm.get_field('filters_json').wrapper).empty();
frm.filter_table = $(`<table class="table table-bordered" style="cursor:pointer; margin:0px;">
<thead>
<tr>
<th style="width: 33%">${__('Filter')}</th>
<th style="width: 33%">${__('Condition')}</th>
<th>${__('Value')}</th>
</tr>
</thead>
<tbody></tbody>
</table>`).appendTo(wrapper);
frm.filters = JSON.parse(frm.doc.filters_json || '[]');
frm.trigger('set_filters_in_table');
frm.filter_table.on('click', () => {
let dialog = new frappe.ui.Dialog({
title: __('Set Filters'),
fields: [{
fieldtype: 'HTML',
fieldname: 'filter_area',
}],
primary_action: function() {
let values = this.get_values();
if (values) {
this.hide();
frm.filters = frm.filter_group.get_filters();
frm.set_value('filters_json', JSON.stringify(frm.filters));
frm.trigger('set_filters_in_table');
}
},
primary_action_label: "Set"
});
frappe.dashboards.filters_dialog = dialog;
frm.filter_group = new frappe.ui.FilterGroup({
parent: dialog.get_field('filter_area').$wrapper,
doctype: frm.doc.document_type,
on_change: () => {},
});
frm.filter_group.add_filters_to_filter_group(frm.filters);
dialog.show();
dialog.set_values(frm.filters);
});
},
set_filters_in_table: function(frm) {
if (!frm.filters.length) {
const filter_row = $(`<tr><td colspan="3" class="text-muted text-center">
${__("Click to Set Filters")}</td></tr>`);
frm.filter_table.find('tbody').html(filter_row);
} else {
let filter_rows = '';
frm.filters.forEach(filter => {
filter_rows +=
`<tr>
<td>${filter[1]}</td>
<td>${filter[2] || ""}</td>
<td>${filter[3]}</td>
</tr>`;
});
frm.filter_table.find('tbody').html(filter_rows);
}
}
});

View file

@ -0,0 +1,148 @@
{
"actions": [],
"autoname": "CARD.#####",
"creation": "2020-04-15 18:06:39.444683",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"label",
"function",
"aggregate_function_based_on",
"column_break_2",
"document_type",
"is_public",
"stats_section",
"show_percentage_stats",
"stats_time_interval",
"filters_section",
"filters_json",
"color"
],
"fields": [
{
"fieldname": "document_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Document Type",
"options": "DocType",
"reqd": 1
},
{
"depends_on": "eval: doc.document_type",
"fieldname": "function",
"fieldtype": "Select",
"label": "Function",
"options": "Count\nSum\nAverage\nMinimum\nMaximum",
"reqd": 1
},
{
"depends_on": "eval: doc.function !== 'Count'",
"fieldname": "aggregate_function_based_on",
"fieldtype": "Select",
"label": "Aggregate Function Based On",
"mandatory_depends_on": "eval: doc.function !== 'Count'"
},
{
"fieldname": "filters_json",
"fieldtype": "Code",
"label": "Filters JSON",
"options": "JSON"
},
{
"fieldname": "label",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Label",
"reqd": 1
},
{
"fieldname": "color",
"fieldtype": "Color",
"label": "Color"
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"fieldname": "filters_section",
"fieldtype": "Section Break",
"label": "Filters Section"
},
{
"default": "0",
"description": "This card will be public to all Users if this is set",
"fieldname": "is_public",
"fieldtype": "Check",
"label": "Is Public",
"permlevel": 1
},
{
"default": "1",
"fieldname": "show_percentage_stats",
"fieldtype": "Check",
"label": "Show Percentage Stats"
},
{
"default": "Daily",
"depends_on": "eval: doc.show_percentage_stats",
"description": "Show percentage difference according to this time interval",
"fieldname": "stats_time_interval",
"fieldtype": "Select",
"label": "Stats Time Interval",
"options": "Daily\nWeekly\nMonthly\nYearly"
},
{
"fieldname": "stats_section",
"fieldtype": "Section Break",
"label": "Stats"
}
],
"links": [],
"modified": "2020-04-25 17:31:34.204607",
"modified_by": "Administrator",
"module": "Desk",
"name": "Number Card",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Dashboard Manager",
"share": 1,
"write": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"share": 1
}
],
"search_fields": "label, document_type",
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "label",
"track_changes": 1
}

View file

@ -0,0 +1,144 @@
# -*- 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
from frappe.utils import cint
class NumberCard(Document):
pass
def get_permission_query_conditions(user=None):
if not user:
user = frappe.session.user
if user == 'Administrator':
return
roles = frappe.get_roles(user)
if "System Manager" in roles:
return None
allowed_doctypes = tuple(frappe.permissions.get_doctypes_with_read())
return '''
`tabNumber Card`.`document_type` in {allowed_doctypes}
'''.format(
allowed_doctypes=allowed_doctypes,
)
def has_permission(doc, ptype, user):
roles = frappe.get_roles(user)
if "System Manager" in roles:
return True
allowed_doctypes = tuple(frappe.permissions.get_doctypes_with_read())
if doc.document_type in allowed_doctypes:
return True
return False
@frappe.whitelist()
def get_result(doc, to_date=None):
doc = frappe.parse_json(doc)
fields = []
sql_function_map = {
'Count': 'count',
'Sum': 'sum',
'Average': 'avg',
'Minimum': 'min',
'Maximum': 'max'
}
function = sql_function_map[doc.function]
if function == 'count':
fields = ['{function}(*) as result'.format(function=function)]
else:
fields = ['{function}({based_on}) as result'.format(function=function, based_on=doc.aggregate_function_based_on)]
filters = frappe.parse_json(doc.filters_json)
if to_date:
filters.append([doc.document_type, 'creation', '<', to_date, False])
res = frappe.db.get_all(doc.document_type, fields=fields, filters=filters)
number = res[0]['result'] if res else 0
return cint(number)
@frappe.whitelist()
def get_percentage_difference(doc, result):
doc = frappe.parse_json(doc)
result = frappe.parse_json(result)
doc = frappe.get_doc('Number Card', doc.name)
if not doc.get('show_percentage_stats'):
return
previous_result = calculate_previous_result(doc)
difference = (result - previous_result)/100.0
return difference
def calculate_previous_result(doc):
from frappe.utils import add_to_date
current_date = frappe.utils.now()
if doc.stats_time_interval == 'Daily':
previous_date = add_to_date(current_date, days=-1)
elif doc.stats_time_interval == 'Weekly':
previous_date = add_to_date(current_date, weeks=-1)
elif doc.stats_time_interval == 'Monthly':
previous_date = add_to_date(current_date, months=-1)
else:
previous_date = add_to_date(current_date, years=-1)
number = get_result(doc, previous_date)
return number
@frappe.whitelist()
def create_number_card(args):
args = frappe.parse_json(args)
doc = frappe.new_doc('Number Card')
doc.update(args)
doc.insert(ignore_permissions=True)
return doc
def get_cards_for_user(doctype, txt, searchfield, start, page_len, filters):
meta = frappe.get_meta(doctype)
searchfields = meta.get_search_fields()
search_conditions = []
if txt:
for field in searchfields:
search_conditions.append('`tab{doctype}`.`{field}` like %(txt)s'.format(field=field, doctype=doctype, txt=txt))
search_conditions = ' or '.join(search_conditions)
search_conditions = 'and (' + search_conditions +')' if search_conditions else ''
conditions, values = frappe.db.build_conditions(filters)
values['txt'] = '%' + txt + '%'
return frappe.db.sql(
'''select
`tabNumber Card`.name, `tabNumber Card`.label, `tabNumber Card`.document_type
from
`tabNumber Card`
where
{conditions} and
(`tabNumber Card`.owner = '{user}' or
`tabNumber Card`.is_public = 1)
{search_conditions}
'''.format(
filters=filters,
user=frappe.session.user,
search_conditions=search_conditions,
conditions=conditions
), values)

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 TestNumberCard(unittest.TestCase):
pass

View file

@ -0,0 +1,31 @@
{
"actions": [],
"creation": "2020-04-19 17:43:50.858343",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"card"
],
"fields": [
{
"fieldname": "card",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Card",
"options": "Number Card"
}
],
"istable": 1,
"links": [],
"modified": "2020-04-19 17:45:11.878472",
"modified_by": "Administrator",
"module": "Desk",
"name": "Number Card Link",
"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 NumberCardLink(Document):
pass

View file

@ -10,7 +10,7 @@ import socket
import time
from frappe import _
from frappe.model.document import Document
from frappe.utils import validate_email_address, cint, get_datetime, DATE_FORMAT, strip, comma_or, sanitize_html
from frappe.utils import validate_email_address, cint, get_datetime, DATE_FORMAT, strip, comma_or, sanitize_html, add_days
from frappe.utils.user import is_system_user
from frappe.utils.jinja import render_template
from frappe.email.smtp import SMTPServer
@ -533,28 +533,37 @@ class EmailAccount(Document):
parent = None
in_reply_to = (email.mail.get("In-Reply-To") or "").strip(" <>")
if in_reply_to and "@{0}".format(frappe.local.site) in in_reply_to:
# reply to a communication sent from the system
email_queue = frappe.db.get_value('Email Queue', dict(message_id=in_reply_to), ['communication','reference_doctype', 'reference_name'])
if email_queue:
parent_communication, parent_doctype, parent_name = email_queue
if parent_communication:
communication.in_reply_to = parent_communication
if in_reply_to:
if "@{0}".format(frappe.local.site) in in_reply_to:
# reply to a communication sent from the system
email_queue = frappe.db.get_value('Email Queue', dict(message_id=in_reply_to), ['communication','reference_doctype', 'reference_name'])
if email_queue:
parent_communication, parent_doctype, parent_name = email_queue
if parent_communication:
communication.in_reply_to = parent_communication
else:
reference, domain = in_reply_to.split("@", 1)
parent_doctype, parent_name = 'Communication', reference
if frappe.db.exists(parent_doctype, parent_name):
parent = frappe._dict(doctype=parent_doctype, name=parent_name)
# set in_reply_to of current communication
if parent_doctype=='Communication':
# communication.in_reply_to = email_queue.communication
if parent.reference_name:
# the true parent is the communication parent
parent = frappe.get_doc(parent.reference_doctype,
parent.reference_name)
else:
reference, domain = in_reply_to.split("@", 1)
parent_doctype, parent_name = 'Communication', reference
if frappe.db.exists(parent_doctype, parent_name):
parent = frappe._dict(doctype=parent_doctype, name=parent_name)
# set in_reply_to of current communication
if parent_doctype=='Communication':
# communication.in_reply_to = email_queue.communication
if parent.reference_name:
# the true parent is the communication parent
parent = frappe.get_doc(parent.reference_doctype,
parent.reference_name)
comm = frappe.db.get_value('Communication',
dict(
message_id=in_reply_to,
creation=['>=', add_days(get_datetime(), -30)]),
['reference_doctype', 'reference_name'], as_dict=1)
if comm:
parent = frappe._dict(doctype=comm.reference_doctype, name=comm.reference_name)
return parent

View file

@ -14,6 +14,12 @@ from frappe.core.doctype.server_script.server_script_utils import run_server_scr
from werkzeug.wrappers import Response
from six import string_types
ALLOWED_MIMETYPES = ('image/png', 'image/jpeg', 'application/pdf', 'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.oasis.opendocument.text', 'application/vnd.oasis.opendocument.spreadsheet')
def handle():
"""handle request"""
validate_auth()
@ -148,12 +154,14 @@ def uploadfile():
@frappe.whitelist(allow_guest=True)
def upload_file():
user = None
if frappe.session.user == 'Guest':
if frappe.get_system_settings('allow_guests_to_upload_files'):
ignore_permissions = True
else:
return
else:
user = frappe.get_doc("User", frappe.session.user)
ignore_permissions = False
files = frappe.request.files
@ -175,11 +183,11 @@ def upload_file():
frappe.local.uploaded_file = content
frappe.local.uploaded_filename = filename
if frappe.session.user == 'Guest':
if frappe.session.user == 'Guest' or (user and not user.has_desk_access()):
import mimetypes
filetype = mimetypes.guess_type(filename)[0]
if filetype not in ['image/png', 'image/jpeg', 'application/pdf']:
frappe.throw("You can only upload JPG, PNG or PDF files.")
if filetype not in ALLOWED_MIMETYPES:
frappe.throw(_("You can only upload JPG, PNG, PDF, or Microsoft documents."))
if method:
method = frappe.get_attr(method)

View file

@ -89,6 +89,7 @@ permission_query_conditions = {
"Dashboard Settings": "frappe.desk.doctype.dashboard_settings.dashboard_settings.get_permission_query_conditions",
"Notification Log": "frappe.desk.doctype.notification_log.notification_log.get_permission_query_conditions",
"Dashboard Chart": "frappe.desk.doctype.dashboard_chart.dashboard_chart.get_permission_query_conditions",
"Number Card": "frappe.desk.doctype.number_card.number_card.get_permission_query_conditions",
"Notification Settings": "frappe.desk.doctype.notification_settings.notification_settings.get_permission_query_conditions",
"Note": "frappe.desk.doctype.note.note.get_permission_query_conditions",
"Kanban Board": "frappe.desk.doctype.kanban_board.kanban_board.get_permission_query_conditions",
@ -105,6 +106,7 @@ has_permission = {
"User": "frappe.core.doctype.user.user.has_permission",
"Note": "frappe.desk.doctype.note.note.has_permission",
"Dashboard Chart": "frappe.desk.doctype.dashboard_chart.dashboard_chart.has_permission",
"Number Card": "frappe.desk.doctype.number_card.number_card.has_permission",
"Kanban Board": "frappe.desk.doctype.kanban_board.kanban_board.has_permission",
"Contact": "frappe.contacts.address_and_contact.has_permission",
"Address": "frappe.contacts.address_and_contact.has_permission",

View file

@ -23,7 +23,7 @@ def start(transaction_type="request", method=None, kwargs=None):
def stop(response=None):
if frappe.conf.monitor and hasattr(frappe.local, "monitor"):
if hasattr(frappe.local, "monitor"):
frappe.local.monitor.dump(response)

View file

@ -273,3 +273,4 @@ execute:frappe.delete_doc_if_exists('DocType', 'GCalendar Settings')
frappe.patches.v12_0.remove_parent_and_parenttype_from_print_formats
execute:from frappe.desk.page.setup_wizard.install_fixtures import update_genders;update_genders()
frappe.patches.v13_0.website_theme_custom_scss
frappe.patches.v13_0.set_existing_dashboard_charts_as_public

View file

@ -0,0 +1,29 @@
import frappe
def execute():
frappe.reload_doc('desk', 'doctype', 'dashboard_chart')
if not frappe.db.table_exists('Dashboard Chart'):
return
users_with_permission = frappe.get_all(
"Has Role",
fields=["parent"],
filters={"role": ['in', ['System Manager', 'Dashboard Manager']], "parenttype": "User"},
distinct=True,
as_list=True
)
users = tuple(
[item if type(item) == str else item.encode('utf8') for sublist in users_with_permission for item in sublist]
)
frappe.db.sql("""
UPDATE
`tabDashboard Chart`
SET
`tabDashboard Chart`.`is_public`=1
WHERE
`tabDashboard Chart`.owner in {users}
""".format(users=users)
)

View file

@ -107,6 +107,7 @@
"public/less/form.less",
"public/less/mobile.less",
"public/less/kanban.less",
"public/less/dashboard_view.less",
"public/less/controls.less",
"public/less/chat.less",
"public/less/filters.less",
@ -299,6 +300,7 @@
"public/js/frappe/views/gantt/gantt_view.js",
"public/js/frappe/views/calendar/calendar.js",
"public/js/frappe/views/dashboard/dashboard_view.js",
"public/js/frappe/views/image/image_view.js",
"public/js/frappe/views/kanban/kanban_view.js",
"public/js/frappe/views/inbox/inbox_view.js",

View file

@ -152,12 +152,14 @@ frappe.ui.form.Control = Class.extend({
() => me.set_model_value(value),
() => {
me.set_mandatory && me.set_mandatory(value);
me.set_invalid && me.set_invalid();
if(me.df.change || me.df.onchange) {
// onchange event specified in df
return (me.df.change || me.df.onchange).apply(me, [e]);
let set = (me.df.change || me.df.onchange).apply(me, [e]);
me.set_invalid && me.set_invalid();
return set;
}
me.set_invalid && me.set_invalid();
}
]);
};

View file

@ -180,7 +180,14 @@ frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({
this.$wrapper.toggleClass("has-error", (this.df.reqd && is_null(value)) ? true : false);
},
set_invalid: function () {
this.$wrapper.toggleClass("has-error", (this.df.invalid ? true : false));
let invalid = !!this.df.invalid;
if (this.grid) {
this.$wrapper.parents('.grid-static-col').toggleClass('invalid', invalid);
this.$input.toggleClass('invalid', invalid);
this.grid_row.columns[this.df.fieldname].is_invalid = invalid;
} else {
this.$wrapper.toggleClass('has-error', invalid);
}
},
set_bold: function() {
if(this.$input) {

View file

@ -265,7 +265,9 @@ export default class GridRow {
if(df.reqd && !txt) {
column.addClass('error');
}
if (df.reqd || df.bold) {
if (column.is_invalid) {
column.addClass('invalid');
} else if (df.reqd || df.bold) {
column.addClass('bold');
}
}

View file

@ -498,11 +498,18 @@ frappe.ui.form.Layout = Class.extend({
},
set_dependant_property: function(condition, fieldname, property) {
let set_property = this.evaluate_depends_on_value(condition);
let form_obj;
if (this.frm) {
form_obj = this.frm;
} else if (this.is_dialog) {
form_obj = this;
}
if (form_obj) {
if (set_property) {
this.frm.set_df_property(fieldname, property, 1);
form_obj.set_df_property(fieldname, property, 1);
} else {
this.frm.set_df_property(fieldname, property, 0);
form_obj.set_df_property(fieldname, property, 0);
}
}
},

View file

@ -36,7 +36,7 @@ frappe.ui.form.save = function (frm, action, callback, btn) {
freeze_message: freeze_message
});
} else {
frappe.show_alert({message: __("No changes in document"), indicator: "blue"});
!frm.is_dirty() && frappe.show_alert({message: __("No changes in document"), indicator: "blue"});
$(btn).prop("disabled", false);
}
};

View file

@ -686,5 +686,5 @@ class FilterArea {
}
// utility function to validate view modes
frappe.views.view_modes = ['List', 'Gantt', 'Kanban', 'Calendar', 'Image', 'Inbox', 'Report'];
frappe.views.view_modes = ['List', 'Gantt', 'Kanban', 'Calendar', 'Image', 'Inbox', 'Report', 'Dashboard'];
frappe.views.is_valid = view_mode => frappe.views.view_modes.includes(view_mode);

View file

@ -26,6 +26,8 @@
<li class="divider"></li>
<li class="list-link" data-view="List">
<a href="#List/{%= doctype %}/List">{%= __("List") %}</a></li>
<li class="list-link" data-view="Dashboard">
<a href="#List/{%= doctype %}/Dashboard">{%= __("Dashboard") %}</a></li>
<li class="hide list-link" data-view="Image">
<a href="#List/{%= doctype %}/Image">{%= __("Images") %}</a></li>
<li class="hide list-link" data-view="Gantt">

View file

@ -57,13 +57,18 @@ frappe.dashboard_utils = {
},
get_dashboard_settings() {
return frappe.model.with_doc('Dashboard Settings', frappe.session.user).then(settings => {
if (!settings) {
return frappe.db.get_list('Dashboard Settings', {
filters: {
name: frappe.session.user
},
fields: ['*']
}).then(settings => {
if (!settings.length) {
return this.create_dashboard_settings().then(settings => {
return settings;
});
} else {
return settings;
return settings[0];
}
});
},
@ -71,7 +76,9 @@ frappe.dashboard_utils = {
create_dashboard_settings() {
return frappe.xcall(
'frappe.desk.doctype.dashboard_settings.dashboard_settings.create_dashboard_settings',
{user: frappe.session.user}
{
user: frappe.session.user
}
).then(settings => {
return settings;
});

View file

@ -0,0 +1,450 @@
frappe.provide('frappe.views');
frappe.views.DashboardView = class DashboardView extends frappe.views.ListView {
get view_name() {
return 'Dashboard';
}
setup_defaults() {
return super.setup_defaults()
.then(() => {
this.dashboard_settings = frappe.get_user_settings(this.doctype)['dashboard_settings'] || null;
});
}
render() {
}
setup_view() {
if (this.chart_group || this.number_card_group) {
return;
}
this.setup_dashboard_page();
this.setup_dashboard_customization();
this.make_dashboard();
}
setup_dashboard_customization() {
this.$customize = this.$chart_header.find('.customize-dashboard');
this.$save_or_discard = this.$chart_header.find('.customize-options');
}
setup_dashboard_page() {
const dashboard_name = __('{0} Dashboard', [this.doctype]);
const chart_wrapper_html = `<div class="dashboard-view"></div>`;
this.$frappe_list.html(chart_wrapper_html);
this.page.clear_secondary_action();
this.$dashboard_page = this.$page.find('.layout-main-section-wrapper').addClass('dashboard-page');
this.$page.find('.page-form').empty().html(
`<div class="dashboard-header">
<div class="text-muted uppercase">${dashboard_name}</div>
<div class="text-muted customize-dashboard" data-action="customize">${__('Customize')}</div>
<div class="small text-muted customize-options small-bounce">
<span class="reset-customization" data-action="reset_dashboard_customization">
${__('Reset')}
</span> / <span class="save-customization" data-action="save_dashboard_customization">
${__('Save')}
</span> / <span class="discard-customization" data-action="discard_dashboard_customization">
${__('Discard')}
</span>
</div>
</div>`);
this.$dashboard_wrapper = this.$page.find('.dashboard-view');
this.$chart_header = this.$page.find('.dashboard-header');
frappe.utils.bind_actions_with_object(this.$dashboard_page, this);
}
make_dashboard() {
if (this.dashboard_settings) {
this.charts = this.dashboard_settings.charts;
this.number_cards = this.dashboard_settings.number_cards;
this.render_dashboard();
} else {
frappe.run_serially([
() => this.fetch_dashboard_items(
'Dashboard Chart',
{
chart_type: ['in', ['Count', 'Sum', 'Group By']],
document_type: this.doctype,
},
'charts'
),
() => this.fetch_dashboard_items('Number Card',
{
document_type: this.doctype,
},
'number_cards'
),
() => this.render_dashboard()
]);
}
}
render_dashboard() {
this.$dashboard_wrapper.empty();
frappe.dashboard_utils.get_dashboard_settings().then(settings => {
this.dashboard_chart_settings = settings.chart_config ? JSON.parse(settings.chart_config) : {};
this.charts.map(chart => {
chart.label = chart.chart_name;
chart.chart_settings = this.dashboard_chart_settings[chart.chart_name] || {};
});
this.render_dashboard_charts();
});
this.render_number_cards();
if (!this.charts.length && !this.number_cards.length) {
this.render_empty_state();
}
}
fetch_dashboard_items(doctype, filters, obj_name) {
return frappe.db.get_list(doctype, {
filters: filters,
fields: ['*']
}).then(items => {
this[obj_name] = items;
});
}
render_number_cards() {
this.number_card_group = new frappe.widget.WidgetGroup({
container: this.$dashboard_wrapper,
type: "number_card",
columns: 3,
options: {
allow_sorting: true,
allow_create: true,
allow_delete: true,
allow_hiding: true,
},
default_values: {doctype: this.doctype},
widgets: this.number_cards || [],
in_customize_mode: this.in_customize_mode || false,
});
this.in_customize_mode && this.number_card_group.customize();
}
render_dashboard_charts() {
this.chart_group = new frappe.widget.WidgetGroup({
container: this.$dashboard_wrapper,
type: "chart",
columns: 2,
options: {
allow_sorting: true,
allow_create: true,
allow_delete: true,
allow_hiding: true,
allow_resize: true,
},
custom_dialog: () => this.show_add_chart_dialog(),
widgets: this.charts,
in_customize_mode: this.in_customize_mode || false,
});
this.in_customize_mode && this.chart_group.customize();
this.chart_group.container.find('.widget-group-head').hide();
}
render_empty_state() {
const no_result_message_html =
`<p>${__("You haven't added any Dashboard Charts or Number Cards yet.")}
<br>${__("Click On Customize to add your first widget")}</p>`;
const customize_button =
`<p><button class="btn btn-primary btn-sm" data-action="customize">
${__('Customize')}
</button></p>`;
const empty_state_image = '/assets/frappe/images/ui-states/empty.png';
const empty_state_html = `<div class="msg-box no-border empty-dashboard">
<div>
<img src="${empty_state_image}" alt="Generic Empty State" class="null-state">
</div>
${no_result_message_html}
${customize_button}
</div>`;
this.$dashboard_wrapper.append(empty_state_html);
this.$empty_state = this.$dashboard_wrapper.find('.empty-dashboard');
}
customize() {
if (this.in_customize_mode) {
return;
}
if (this.$empty_state) {
this.$empty_state.remove();
}
this.toggle_customize(true);
this.chart_group.in_customize_mode = true;
this.chart_group.customize();
this.number_cards.in_customize_mode = true;
this.number_card_group.customize();
}
get_widgets_to_save(widget_group) {
const config = widget_group.get_widget_config();
let widgets = [];
config.order.map(widget_name => {
widgets.push(config.widgets[widget_name]);
});
return this.remove_duplicates(widgets);
}
save_dashboard_customization() {
this.toggle_customize(false);
const charts = this.get_widgets_to_save(this.chart_group);
const number_cards = this.get_widgets_to_save(this.number_card_group);
this.dashboard_settings = {
charts: charts,
number_cards: number_cards,
};
frappe.model.user_settings.save(this.doctype, 'dashboard_settings', this.dashboard_settings);
this.make_dashboard();
}
discard_dashboard_customization() {
this.dashboard_settings = frappe.get_user_settings(this.doctype)['dashboard_settings'] || null;
this.toggle_customize(false);
this.render_dashboard();
}
reset_dashboard_customization() {
this.dashboard_settings = null;
frappe.model.user_settings.save(
this.doctype, 'dashboard_settings', this.dashboard_settings
).then(() => this.make_dashboard());
this.toggle_customize(false);
}
toggle_customize(show) {
this.$customize.toggle(!show);
this.$save_or_discard.toggle(show);
this.in_customize_mode = show;
}
show_add_chart_dialog() {
let fields = this.get_field_options();
const dialog = new frappe.ui.Dialog({
title: __(`Add a ${this.doctype} Chart`),
fields: [
{
fieldname: 'new_or_existing',
fieldtype: 'Select',
label: 'Choose an existing chart or create a new chart',
options: ['New Chart', 'Existing Chart'],
reqd: 1,
},
{
label: 'Chart',
fieldname: 'chart',
fieldtype: 'Link',
get_query: () => {
return {
'query': 'frappe.desk.doctype.dashboard_chart.dashboard_chart.get_charts_for_user',
filters: {
document_type: this.doctype,
}
};
},
options: 'Dashboard Chart',
depends_on: 'eval: doc.new_or_existing == "Existing Chart"'
},
{
fieldname: 'sb_2',
fieldtype: 'Section Break',
depends_on: 'eval: doc.new_or_existing == "New Chart"'
},
{
label: 'Chart Label',
fieldname: 'label',
fieldtype: 'Data',
mandatory_depends_on: 'eval: doc.new_or_existing == "New Chart"'
},
{
fieldname: 'cb_1',
fieldtype: 'Column Break'
},
{
label: 'Chart Type',
fieldname: 'chart_type',
fieldtype: 'Select',
options: ['Time Series', 'Group By'],
mandatory_depends_on: 'eval: doc.new_or_existing == "New Chart"',
},
{
fieldname: 'sb_2',
fieldtype: 'Section Break',
label: 'Chart Config',
depends_on: 'eval: doc.chart_type == "Time Series" && doc.new_or_existing == "New Chart"',
},
{
label: 'Function',
fieldname: 'chart_function',
fieldtype: 'Select',
options: ['Count', 'Sum', 'Average'],
default: 'Count',
},
{
label: 'Timespan',
fieldtype: 'Select',
fieldname: 'timespan',
depends_on: 'eval: doc.chart_type == "Time Series"',
options: ['Last Year', 'Last Quarter', 'Last Month', 'Last Week'],
default: 'Last Year',
},
{
fieldname: 'cb_2',
fieldtype: 'Column Break'
},
{
label: 'Value Based On',
fieldtype: 'Select',
fieldname: 'based_on',
options: fields.value_fields,
depends_on: 'eval: doc.chart_function=="Sum"'
},
{
label: 'Time Series Based On',
fieldtype: 'Select',
fieldname: 'based_on',
options: fields.date_fields,
mandatory_depends_on: 'eval: doc.chart_type == "Time Series"'
},
{
label: 'Time Interval',
fieldname: 'time_interval',
fieldtype: 'Select',
depends_on: 'eval: doc.chart_type == "Time Series"',
options: ['Yearly', 'Quarterly', 'Monthly', 'Weekly', 'Daily'],
default: 'Monthly'
},
{
fieldname: 'sb_2',
fieldtype: 'Section Break',
label: 'Chart Config',
depends_on: 'eval: doc.chart_type == "Group By" && doc.new_or_existing == "New Chart"',
},
{
label: 'Group By Type',
fieldname: 'group_by_type',
fieldtype: 'Select',
options: ['Count', 'Sum', 'Average'],
default: 'Count',
},
{
label: 'Aggregate Function Based On',
fieldtype: 'Select',
fieldname: 'aggregate_function_based_on',
options: fields.aggregate_function_fields,
depends_on: 'eval: ["Sum", "Avergage"].includes(doc.group_by_type)',
},
{
fieldname: 'cb_2',
fieldtype: 'Column Break'
},
{
label: 'Group By Based On',
fieldtype: 'Select',
fieldname: 'group_by_based_on',
options: fields.group_by_fields,
default: 'Last Year',
},
{
label: 'Number of Groups',
fieldtype: 'Int',
fieldname: 'number_of_groups',
default: 0,
},
{
fieldname: 'sb_3',
fieldtype: 'Section Break',
depends_on: 'eval: doc.new_or_existing == "New Chart"'
},
{
label: 'Chart Type',
fieldname: 'type',
fieldtype: 'Select',
options: ['Line', 'Bar', 'Percentage', 'Pie'],
depends_on: 'eval: doc.new_or_existing == "New Chart"'
},
{
fieldname: 'cb_1',
fieldtype: 'Column Break'
},
{
label: 'Chart Color',
fieldname: 'color',
fieldtype: 'Color',
depends_on: 'eval: doc.new_or_existing == "New Chart"',
},
],
primary_action_label: __('Add'),
primary_action: (values) => {
let chart = values;
if (chart.new_or_existing == 'New Chart') {
chart.chart_name = chart.label;
chart.chart_type = chart.chart_type == 'Time Series' ? chart.chart_function : chart.chart_type;
chart.document_type = this.doctype;
chart.filters_json = '[]';
frappe.xcall('frappe.desk.doctype.dashboard_chart.dashboard_chart.create_dashboard_chart', {'args': chart}).then((doc)=> {
this.chart_group.new_widget.on_create({'chart_name': doc.chart_name, 'name': doc.chart_name, 'label': chart.label});
});
} else {
this.chart_group.new_widget.on_create({'chart_name': chart.chart, 'label': chart.chart, 'name': chart.chart});
}
dialog.hide();
}
});
dialog.show();
}
get_field_options() {
let date_fields = [
{label: __('Created On'), value: 'creation'},
{label: __('Last Modified On'), value: 'modified'}
];
let value_fields = [];
let group_by_fields = [];
let aggregate_function_fields = [];
frappe.get_meta(this.doctype).fields.map(df => {
if (['Date', 'Datetime'].includes(df.fieldtype)) {
date_fields.push({label: df.label, value: df.fieldname});
}
if (frappe.model.numeric_fieldtypes.includes(df.fieldtype)) {
value_fields.push({label: df.label, value: df.fieldname});
aggregate_function_fields.push({label: df.label, value: df.fieldname});
}
if (['Link', 'Select'].includes(df.fieldtype)) {
group_by_fields.push({label: df.label, value: df.fieldname});
}
});
return {
date_fields: date_fields,
value_fields: value_fields,
group_by_fields: group_by_fields,
aggregate_function_fields: aggregate_function_fields
};
}
remove_duplicates(items) {
return items.filter((item, index) => items.indexOf(item) === index);
}
};

View file

@ -29,7 +29,7 @@ export default class WebForm extends frappe.ui.FieldGroup {
// webform client script
frappe.init_client_script && frappe.init_client_script();
frappe.web_form.events.trigger('after_load');
this.after_load && this.after_load();
}
on(fieldname, handler) {
@ -102,7 +102,9 @@ export default class WebForm extends frappe.ui.FieldGroup {
}
save() {
this.validate && this.validate();
if (this.validate && !this.validate()) {
frappe.throw(__("Couldn't save, please check the data you have entered"), __("Validation Error"));
}
// validation hack: get_values will check for missing data
let doc_values = super.get_values(this.allow_incomplete);
@ -134,7 +136,7 @@ export default class WebForm extends frappe.ui.FieldGroup {
if (!response.exc) {
// Success
this.handle_success(response.message);
frappe.web_form.events.trigger('after_save');
this.after_save && this.after_save();
}
},
always: function() {

View file

@ -27,14 +27,16 @@ export default class Widget {
options.allow_delete &&
this.add_custom_button(
'<i class="fa fa-trash" aria-hidden="true"></i>',
() => this.delete()
() => this.delete(),
"",
`${__('Delete')}`
);
options.allow_sorting &&
this.add_custom_button(
'<i class="fa fa-arrows" aria-hidden="true"></i>',
null,
"drag-handle"
"drag-handle",
);
if (options.allow_hiding) {
@ -45,10 +47,12 @@ export default class Widget {
this.footer.css("opacity", 0.5);
}
const classname = this.hidden ? 'fa fa-eye' : 'fa fa-eye-slash';
const title = this.hidden ? `${__('Show')}` : `${__('Hide')}`;
this.add_custom_button(
`<i class="${classname}" aria-hidden="true"></i>`,
() => this.hide_or_show(),
"show-or-hide-button"
"show-or-hide-button",
title
);
this.show_or_hide_button = this.action_area.find(
@ -62,6 +66,19 @@ export default class Widget {
() => this.edit()
);
if (options.allow_resize) {
const title = this.width == 'Full'? `${__('Collapse')}` : `${__('Expand')}`;
this.add_custom_button(
'<i class="fa fa-expand" aria-hidden="true"></i>',
() => this.toggle_width(),
"resize-button",
title
);
this.resize_button = this.action_area.find(
".resize-button"
);
}
}
make() {
@ -72,12 +89,12 @@ export default class Widget {
make_widget() {
this.widget = $(`<div class="widget ${
this.hidden ? "hidden" : ""
}" data-widget-name=${this.name ? this.name : ''}>
}" data-widget-name="${this.name ? this.name : ''}">
<div class="widget-head">
<div class="widget-title ellipsis"></div>
<div class="widget-control"></div>
</div>
<div class="widget-body">
<div class="widget-body">
</div>
<div class="widget-footer">
</div>
@ -91,13 +108,16 @@ export default class Widget {
this.refresh();
}
set_title() {
this.title_field[0].innerHTML = this.label;
set_title(max_chars) {
this.title_field[0].innerHTML = max_chars ? frappe.ellipsis(this.label, max_chars) : this.label;
if (max_chars) {
this.title_field[0].setAttribute('title', this.label);
}
}
add_custom_button(html, action, class_name = "") {
add_custom_button(html, action, class_name = "", title="") {
let button = $(
`<button class="btn btn-default btn-xs ${class_name}">${html}</button>`
`<button class="btn btn-default btn-xs ${class_name}" title="${title}">${html}</button>`
);
button.click(event => {
event.stopPropagation();
@ -106,13 +126,21 @@ export default class Widget {
button.appendTo(this.action_area);
}
delete() {
this.widget.addClass("zoomOutDelete");
// wait for animation
setTimeout(() => {
delete(animate=true) {
let remove_widget = () => {
this.widget.remove();
this.options.on_delete && this.options.on_delete(this.name);
}, 300);
};
if (animate) {
this.widget.addClass("zoomOutDelete");
// wait for animation
setTimeout(() => {
remove_widget();
}, 300);
} else {
remove_widget();
}
}
edit() {
@ -134,6 +162,21 @@ export default class Widget {
this.edit_dialog.make();
}
toggle_width() {
if (this.width == 'Full') {
this.widget.removeClass("full-width");
this.width = null;
this.refresh();
} else {
this.widget.addClass("full-width");
this.width = 'Full';
this.refresh();
}
const title = this.width == 'Full' ? `${__('Collapse')}` : `${__('Expand')}`;
this.resize_button.attr('title', title);
}
hide_or_show() {
if (!this.hidden) {
this.body.css("opacity", 0.5);
@ -149,7 +192,9 @@ export default class Widget {
this.show_or_hide_button.empty();
const classname = this.hidden ? 'fa fa-eye' : 'fa fa-eye-slash';
$(`<i class="${classname}" aria-hidden="true"></i>`).appendTo(
const title = this.hidden ? `${__('Show')}` : `${__('Hide')}`;
$(`<i class="${classname}" aria-hidden="true" title="${title}"></i>`).appendTo(
this.show_or_hide_button
);
}

View file

@ -6,7 +6,7 @@ frappe.provide("frappe.dashboards.chart_sources");
export default class ChartWidget extends Widget {
constructor(opts) {
super(opts);
this.height = 240;
this.height = this.height || 240;
}
get_config() {
@ -14,22 +14,27 @@ export default class ChartWidget extends Widget {
name: this.name,
chart_name: this.chart_name,
label: this.label,
hidden: this.hidden,
width: this.width,
};
}
refresh() {
delete this.dashboard_chart;
this.set_title();
this.set_body();
this.make_chart();
}
set_chart_title() {
const max_chars = this.widget.width() < 500 ? 20 : 60;
this.set_title(max_chars);
}
set_body() {
this.widget.addClass("dashboard-widget-box");
if (this.width == "Full") {
this.widget.addClass("full-width");
}
this.make_chart();
}
setup_container() {
@ -40,7 +45,7 @@ export default class ChartWidget extends Widget {
"Loading..."
)}</div>`
);
this.loading.hide().appendTo(this.body);
this.loading.appendTo(this.body);
this.empty = $(
`<div class="chart-loading-state text-muted" style="height: ${this.height}px;">${__(
@ -51,6 +56,8 @@ export default class ChartWidget extends Widget {
this.chart_wrapper = $(`<div></div>`);
this.chart_wrapper.appendTo(this.body);
this.set_chart_title();
}
set_summary() {
@ -69,6 +76,12 @@ export default class ChartWidget extends Widget {
make_chart() {
this.get_settings().then(() => {
if (!this.settings) {
this.deleted = true;
this.widget.remove();
return;
}
if (!this.chart_settings) {
this.chart_settings = {};
}
@ -442,11 +455,10 @@ export default class ChartWidget extends Widget {
chart_name: this.chart_doc.name,
filters: filters,
refresh: refresh ? 1 : 0,
time_interval:
args && args.time_interval? args.time_interval: null,
timespan: args && args.timespan? args.timespan: null,
from_date: args && args.from_date? args.from_date: null,
to_date: args && args.to_date? args.to_date: null
time_interval: args && args.time_interval ? args.time_interval : null,
timespan: args && args.timespan ? args.timespan : null,
from_date: args && args.from_date ? args.from_date : null,
to_date: args && args.to_date ? args.to_date : null
};
}
return frappe.xcall(method, args);
@ -536,40 +548,42 @@ export default class ChartWidget extends Widget {
return frappe.model
.with_doc("Dashboard Chart", this.chart_name)
.then(chart_doc => {
this.chart_doc = chart_doc;
if (this.chart_doc.chart_type == "Custom") {
// custom source
if (
frappe.dashboards.chart_sources[this.chart_doc.source]
) {
this.settings =
frappe.dashboards.chart_sources[
this.chart_doc.source
];
if (chart_doc) {
this.chart_doc = chart_doc;
if (this.chart_doc.chart_type == "Custom") {
// custom source
if (
frappe.dashboards.chart_sources[this.chart_doc.source]
) {
this.settings =
frappe.dashboards.chart_sources[
this.chart_doc.source
];
return Promise.resolve();
} else {
const method =
"frappe.desk.doctype.dashboard_chart_source.dashboard_chart_source.get_config";
return frappe
.xcall(method, { name: this.chart_doc.source })
.then(config => {
frappe.dom.eval(config);
this.settings =
frappe.dashboards.chart_sources[
this.chart_doc.source
];
});
}
} else if (this.chart_doc.chart_type == "Report") {
this.settings = {
method: "frappe.desk.query_report.run"
};
return Promise.resolve();
} else {
const method =
"frappe.desk.doctype.dashboard_chart_source.dashboard_chart_source.get_config";
return frappe
.xcall(method, { name: this.chart_doc.source })
.then(config => {
frappe.dom.eval(config);
this.settings =
frappe.dashboards.chart_sources[
this.chart_doc.source
];
});
this.settings = {
method: "frappe.desk.doctype.dashboard_chart.dashboard_chart.get"
};
return Promise.resolve();
}
} else if (this.chart_doc.chart_type == "Report") {
this.settings = {
method: "frappe.desk.query_report.run"
};
return Promise.resolve();
} else {
this.settings = {
method: "frappe.desk.doctype.dashboard_chart.dashboard_chart.get"
};
return Promise.resolve();
}
});
}

View file

@ -18,19 +18,26 @@ export default class NewWidget {
get_title() {
// DO NOT REMOVE: Comment to load translation
// __("New Chart") __("New Shortcut")
return __(`New ${frappe.utils.to_title_case(this.type)}`);
// __("New Chart") __("New Shortcut") __("New Number Card")
return __(`New ${frappe.model.unscrub(this.type)}`);
}
make_widget() {
this.widget = $(`<div class="widget new-widget">
const new_widget_class = `new-${frappe.scrub(frappe.model.unscrub(this.type), '-')}-widget`;
this.widget = $(`<div class="widget new-widget ${new_widget_class}">
+ ${this.get_title()}
</div>`);
this.body = this.widget;
}
setup_events() {
this.widget.on("click", () => this.open_dialog());
this.widget.on("click", () => {
if (!this.custom_dialog) {
this.open_dialog();
} else {
this.custom_dialog();
}
});
}
open_dialog() {
@ -40,6 +47,7 @@ export default class NewWidget {
label: this.label,
type: this.type,
values: false,
default_values: this.default_values,
primary_action: this.on_create,
});

View file

@ -0,0 +1,221 @@
import Widget from "./base_widget.js";
import { go_to_list_with_filters, shorten_number } from "./utils";
export default class NumberCardWidget extends Widget {
constructor(opts) {
super(opts);
}
get_config() {
return {
name: this.name,
label: this.label,
color: this.color,
hidden: this.hidden,
};
}
refresh() {
this.set_body();
}
set_body() {
this.widget.addClass("number-widget-box");
this.make_card();
}
set_title() {
$(this.title_field).html(`<div class="number-label">${this.card_doc.label}</div>`);
}
make_card() {
frappe.model.with_doc('Number Card', this.name).then(card => {
if (!card) {
if (this.document_type) {
this.create_number_card();
this.render_card();
} else {
// widget doesn't exist so delete
this.delete(false);
return;
}
} else {
this.card_doc = card;
this.render_card();
}
this.set_events();
});
}
create_number_card() {
this.set_doc_args();
frappe.xcall(
'frappe.desk.doctype.number_card.number_card.create_number_card',
{
'args': this.card_doc
}
).then(doc => {
this.name = doc.name;
this.widget.attr('data-widget-name', this.name);
});
}
set_events() {
$(this.body).click(() => {
if (this.in_customize_mode) return;
let filters = JSON.parse(this.card_doc.filters_json);
go_to_list_with_filters(this.card_doc.document_type, filters);
});
}
set_doc_args() {
this.card_doc = Object.assign({}, {
document_type: this.document_type,
label: this.label,
function: this.function,
aggregate_function_based_on: this.aggregate_function_based_on,
color: this.color,
filters_json: this.stats_filter
});
}
render_card() {
this.prepare_actions();
this.set_title();
this.set_loading_state();
frappe.run_serially([
() => this.render_number(),
() => this.render_stats(),
]);
}
set_loading_state() {
$(this.body).html(`<div class="number-card-loading text-muted">
${__('Loading...')}
</div>`);
}
get_number() {
return frappe.xcall('frappe.desk.doctype.number_card.number_card.get_result', {
doc: this.card_doc
}).then(res => {
this.number = res;
if (this.card_doc.function !== 'Count') {
return frappe.model.with_doctype(this.card_doc.document_type, () => {
this.get_formatted_number();
});
} else {
this.number_html = res;
}
});
}
get_formatted_number() {
const based_on_df =
frappe.meta.get_docfield(this.card_doc.document_type, this.card_doc.aggregate_function_based_on);
const shortened_number = shorten_number(this.number);
let number_parts = shortened_number.split(' ');
const symbol = number_parts[1] || '';
const formatted_number = $(frappe.format(number_parts[0], based_on_df)).text();
this.number_html = formatted_number + ' ' + symbol;
}
render_number() {
return this.get_number().then(() => {
$(this.body).html(`<div class="widget-content">
<div class="number" style="color:${this.card_doc.color}">${this.number_html}</div>
</div>`);
});
}
render_stats() {
let caret_html ='';
let color_class = '';
return this.get_percentage_stats().then(() => {
if (this.percentage_stat == undefined) return;
if (this.percentage_stat == 0) {
color_class = 'grey-stat';
} else if (this.percentage_stat > 0) {
caret_html = '<i class="fa fa-caret-up"></i>';
color_class = 'green-stat';
} else {
caret_html = '<i class="fa fa-caret-down"></i>';
color_class = 'red-stat';
}
$(this.body).find('.widget-content').append(`<div class="card-stats ${color_class}">
${caret_html}
<span class="percentage-stat">${Math.abs(this.percentage_stat)} %</span>
</div>`);
});
}
get_percentage_stats() {
return frappe.xcall('frappe.desk.doctype.number_card.number_card.get_percentage_difference', {
doc: this.card_doc,
result: this.number
}).then(res => {
if (res !== undefined) {
this.percentage_stat = +res.toFixed(2);
}
});
}
prepare_actions() {
let actions = [
{
label: __('Refresh'),
action: 'action-refresh',
handler: () => {
this.render_card();
}
},
{
label: __('Edit'),
action: 'action-edit',
handler: () => {
frappe.set_route(
'Form',
'Number Card',
this.name
);
}
},
];
this.set_card_actions(actions);
}
set_card_actions(actions) {
/* eslint-disable indent */
this.card_actions =
$(`<div class="card-actions 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>`);
/* eslint-disable indent */
this.card_actions.find("a[data-action]").each((i, o) => {
const action = o.dataset.action;
$(o).click(actions.find(a => a.action === action));
});
this.action_area.html(this.card_actions);
}
}

View file

@ -113,4 +113,29 @@ const build_summary_item = (summary) => {
</div>`);
};
export { generate_route, generate_grid, build_summary_item };
function go_to_list_with_filters(doctype, filters) {
const route = `List/${doctype}/List`;
frappe.set_route(route).then(()=> {
let list_view = frappe.views.list_view[route];
let filter_area = list_view.filter_area;
filter_area.clear();
filter_area.filter_list.add_filters_to_filter_group(filters);
});
}
function shorten_number(number) {
let x = Math.abs(Math.round(number));
switch (true) {
case x >= 1.0e+12:
return Math.round(number/1.0e+12) + " T";
case x >= 1.0e+9:
return Math.round(number/1.0e+9) + " B";
case x >= 1.0e+6:
return Math.round(number/1.0e+6) + " M";
default:
return number.toFixed();
}
}
export { generate_route, generate_grid, build_summary_item, go_to_list_with_filters, shorten_number };

View file

@ -19,7 +19,7 @@ class WidgetDialog {
primary_action: (data) => {
data = this.process_data(data);
if (!this.editing) {
if (!this.editing && !data.name) {
data.name = `${this.type}-${this.label}-${frappe.utils.get_random(20)}`;
}
@ -35,7 +35,7 @@ class WidgetDialog {
// __("New Chart") __("New Shortcut") __("Edit Chart") __("Edit Shortcut")
let action = this.editing ? "Edit" : "Add";
return __(`${action} ${frappe.utils.to_title_case(this.type)}`);
return __(`${action} ${frappe.model.unscrub(this.type)}`);
}
get_fields() {
@ -61,6 +61,38 @@ class WidgetDialog {
show_field(fieldname) {
this.dialog.set_df_property(fieldname, "hidden", false);
}
setup_filter(doctype) {
if (this.filter_group) {
this.filter_group.wrapper.empty();
delete this.filter_group;
}
let $loading = this.dialog.get_field("filter_area_loading").$wrapper;
$(`<span class="text-muted">Loading Filters...</span>`).appendTo($loading);
this.filters = [];
if (this.values && this.values.stats_filter) {
const filters_json = JSON.parse(this.values.stats_filter);
this.filters = Object.keys(filters_json).map((filter) => {
let val = filters_json[filter];
return [this.values.link_to, filter, val[0], val[1], false];
});
}
this.filter_group = new frappe.ui.FilterGroup({
parent: this.dialog.get_field("filter_area").$wrapper,
doctype: doctype,
on_change: () => {},
});
frappe.model.with_doctype(doctype, () => {
this.filter_group.add_filters_to_filter_group(this.filters);
this.hide_field("filter_area_loading");
this.show_field("filter_area");
});
}
}
class ChartDialog extends WidgetDialog {
@ -222,37 +254,139 @@ class ShortcutDialog extends WidgetDialog {
return data;
}
}
setup_filter(doctype) {
if (this.filter_group) {
this.filter_group.wrapper.empty();
delete this.filter_group;
class NumberCardDialog extends WidgetDialog {
constructor(opts) {
super(opts);
}
get_fields() {
let fields;
fields = [
{
fieldtype: 'Select',
label: 'Choose Existing Card or create New Card',
fieldname: 'new_or_existing',
options: ['New Card', 'Existing Card']
},
{
fieldtype: 'Link',
fieldname: 'card',
label: 'Number Cards',
options: 'Number Card',
get_query: () => {
return {
'query': 'frappe.desk.doctype.number_card.number_card.get_cards_for_user',
filters: {
document_type: this.document_type,
}
};
},
depends_on: 'eval: doc.new_or_existing == "Existing Card"'
},
{
fieldtype: 'Section Break',
fieldname: 'sb_1',
depends_on: 'eval: doc.new_or_existing == "New Card"'
},
{
label: 'Label',
fieldname: 'label',
fieldtype: 'Data',
mandatory_depends_on: 'eval: doc.new_or_existing == "New Card"'
},
{
label: 'Doctype',
fieldname: 'document_type',
fieldtype: 'Link',
options: 'DocType',
onchange: () => {
this.document_type = this.dialog.get_value("document_type");
this.set_aggregate_function_fields(this.dialog.get_values());
this.setup_filter(this.document_type);
},
hidden: 1
},
{
label: 'Color',
fieldname: 'color',
fieldtype: 'Color'
},
{
fieldtype: "Column Break",
fieldname: "cb_1",
},
{
label: 'Function',
fieldname: 'function',
fieldtype: 'Select',
options: ['Count', 'Sum', 'Average', 'Minimum', 'Maximum'],
mandatory_depends_on: 'eval: doc.new_or_existing == "New Card"'
},
{
label: 'Function Based On',
fieldname: 'aggregate_function_based_on',
fieldtype: 'Select',
depends_on: "eval: doc.function !== 'Count'",
mandatory_depends_on: 'eval: doc.function !== "Count" && doc.new_or_existing == "New Card"'
},
{
fieldtype: "Section Break",
fieldname: "sb_1",
label: 'Add Filters',
depends_on: 'eval: doc.new_or_existing == "New Card"'
},
{
fieldtype: "HTML",
fieldname: "filter_area_loading",
},
{
fieldtype: "HTML",
fieldname: "filter_area",
hidden: 1,
},
{
fieldtype: "Section Break",
fieldname: "sb_1",
},
];
return fields;
}
setup_dialog_events() {
if (!this.document_type) {
if (this.default_values['doctype']) {
this.document_type = this.default_values['doctype'];
this.setup_filter(this.default_values['doctype']);
this.set_aggregate_function_fields();
} else {
this.show_field('document_type');
}
}
}
let $loading = this.dialog.get_field("filter_area_loading").$wrapper;
$(`<span class="text-muted">Loading Filters...</span>`).appendTo($loading);
this.filters = [];
if (this.values && this.values.stats_filter) {
const filters_json = JSON.parse(this.values.stats_filter);
this.filters = Object.keys(filters_json).map((filter) => {
let val = filters_json[filter];
return [this.values.link_to, filter, val[0], val[1], false];
set_aggregate_function_fields() {
let aggregate_function_fields = [];
if (this.document_type) {
frappe.get_meta(this.document_type).fields.map(df => {
if (frappe.model.numeric_fieldtypes.includes(df.fieldtype)) {
aggregate_function_fields.push({label: df.label, value: df.fieldname});
}
});
}
this.dialog.set_df_property('aggregate_function_based_on', 'options', aggregate_function_fields);
}
this.filter_group = new frappe.ui.FilterGroup({
parent: this.dialog.get_field("filter_area").$wrapper,
doctype: doctype,
on_change: () => {},
});
process_data(data) {
if (data.new_or_existing == 'Existing Card') {
data.name = data.card;
}
data.stats_filter = JSON.stringify(this.filter_group.get_filters());
data.document_type = this.document_type;
frappe.model.with_doctype(doctype, () => {
this.filter_group.add_filters_to_filter_group(this.filters);
this.hide_field("filter_area_loading");
this.show_field("filter_area");
});
return data;
}
}
@ -260,6 +394,7 @@ export default function get_dialog_constructor(type) {
const widget_map = {
chart: ChartDialog,
shortcut: ShortcutDialog,
number_card: NumberCardDialog,
};
return widget_map[type] || WidgetDialog;

View file

@ -4,6 +4,7 @@ import ShortcutWidget from "../widgets/shortcut_widget";
import LinksWidget from "../widgets/links_widget";
import OnboardingWidget from "../widgets/onboarding_widget";
import NewWidget from "../widgets/new_widget";
import NumberCardWidget from "../widgets/number_card_widget";
frappe.provide("frappe.widget");
@ -13,6 +14,7 @@ const widget_factory = {
shortcut: ShortcutWidget,
links: LinksWidget,
onboarding: OnboardingWidget,
number_card: NumberCardWidget,
};
export default class WidgetGroup {
@ -77,6 +79,14 @@ export default class WidgetGroup {
return widget_object;
}
remove_widget(widget_obj) {
widget_obj.widget.remove();
this.widgets_list.filter((widget) => {
if (widget.name == widget_obj.name) return false;
});
delete this.widgets_dict[widget_obj.name];
}
customize() {
this.widget_area.show();
this.widgets_list.forEach((wid) => {
@ -96,6 +106,8 @@ export default class WidgetGroup {
this.new_widget = new NewWidget({
container: this.body,
type: this.type,
custom_dialog: this.custom_dialog,
default_values: this.default_values,
on_create: (config) => {
// Remove new widget
this.new_widget.delete();

View file

@ -162,6 +162,6 @@
top: 8px;
height: 15px;
right: 12px;
top: 8px;
pointer-events: none;
}
}

View file

@ -0,0 +1,44 @@
.dashboard-page {
.dashboard-view {
min-height: calc(100vh - 284px);
padding: 20px 20px 0 20px;
.new-widget {
text-align: center;
}
.new-chart-widget {
min-height: 200px;
}
.new-number-card-widget {
min-height: 100px;
}
}
.empty-dashboard {
margin-top: 45px;
}
.page-form {
height: 50px;
.dashboard-header {
padding: 10px;
display: flex;
justify-content: space-between;
width: 100%;
}
.customize-dashboard {
font-size: 13px;
cursor: pointer;
}
.customize-options {
display: none;
cursor: pointer;
}
}
}

View file

@ -392,6 +392,76 @@
}
}
}
&.number-widget-box {
cursor: pointer;
height: 100px;
padding: 10px;
&:hover {
border-color: @text-muted;
}
.widget-head {
.widget-title {
font-weight: normal;
color: @text-muted;
font-size: 14px;
margin-top: -5px;
}
.widget-control {
right: -10px;
top: -10px;
}
}
.widget-body {
text-align: left;
.number-card-loading {
display: flex;
height: 50px;
align-items: center;
justify-content: center;
}
.widget-content {
padding-top: 20px;
display: flex;
justify-content: space-between;
.number {
font-size: 26px;
font-weight: bold;
}
.number-text {
color: @text-muted;
padding: 5px;
font-size: 14px;
}
.card-stats {
padding-top: 15px;
}
.green-stat {
color: green;
}
.red-stat {
color: @red;
}
.grey-stat {
color: @text-muted;
}
}
}
}
}
.pill {

View file

@ -80,6 +80,10 @@
background-color: @extra-light-yellow;
}
.editable-form .grid-static-col.invalid {
background-color: @label-danger-bg;
}
.validated-form .grid-static-col.error {
background-color: @label-danger-bg;
}
@ -150,6 +154,10 @@
}
}
input.form-control.invalid {
background-color: @label-danger-bg;
}
input[data-fieldtype="Int"], input[data-fieldtype="Float"], input[data-fieldtype="Currency"] {
text-align: right;
}

View file

@ -13,11 +13,10 @@ details.hide-summary-arrow summary::-webkit-details-marker {
}
.from-markdown {
@apply text-gray-900;
@apply leading-relaxed;
> * + * {
@apply mt-6;
@apply mt-4;
}
> :first-child {
@ -50,28 +49,45 @@ details.hide-summary-arrow summary::-webkit-details-marker {
@apply px-4 py-3 text-sm font-medium text-gray-900 border border-gray-400 rounded-md bg-gray-50;
}
> h1 {
@apply text-4xl;
@apply mt-16;
@apply mb-4;
@apply leading-none;
@apply font-bold;
h1 {
@apply mt-16 mb-4 text-3xl font-extrabold leading-tight tracking-tight;
@screen sm {
@apply text-4xl leading-10;
}
@screen xl {
@apply text-5xl leading-none;
}
}
> h2 {
@apply mt-16;
@apply mb-4;
@apply leading-none;
@apply font-bold;
@apply text-3xl;
h1 + p {
@apply max-w-2xl mt-3 text-base text-gray-900;
@screen sm {
@apply mt-5 text-lg;
}
@screen md {
@apply mt-5 text-xl;
}
}
> h3 {
@apply mt-16;
@apply mb-4;
@apply leading-none;
@apply font-bold;
@apply text-2xl;
h2 {
@apply mb-4 text-2xl font-bold leading-tight mt-14;
}
h3 {
@apply mt-12 mb-4 text-xl font-semibold leading-tight;
}
h4 {
@apply mt-10 mb-4 text-lg font-semibold leading-tight;
}
h5 {
@apply mt-8 mb-4 text-base font-semibold leading-tight;
}
h6 {
@apply mt-6 mb-4 text-sm font-semibold leading-tight;
}
> a,
@ -84,9 +100,17 @@ details.hide-summary-arrow summary::-webkit-details-marker {
}
}
table {
@apply w-full my-8 border-t;
}
tbody {
@apply border-t;
}
tr > td,
tr > th {
@apply px-4 py-2 border border-gray-400;
@apply py-4 pr-6 text-sm leading-6 text-left border-b;
}
th:empty {

View file

@ -7,6 +7,7 @@
web_block.css_class
]) -%}
{%- if not web_block.hide_block -%}
<{{htmltag}} class="{{ classes }}" data-section-idx="{{ web_block.idx | e }}"
data-section-template="{{ web_block.web_template | e }}">
{%- if web_block.add_container -%}
@ -17,3 +18,4 @@
</div>
{%- endif -%}
</{{htmltag}}>
{%- endif -%}

View file

@ -30,7 +30,7 @@ def two_factor_is_enabled(user=None):
if bypass_two_factor_auth and user:
user_doc = frappe.get_doc("User", user)
restrict_ip_list = user_doc.get_restricted_ip_list() #can be None or one or more than one ip address
if restrict_ip_list:
if restrict_ip_list and frappe.local.request_ip:
for ip in restrict_ip_list:
if frappe.local.request_ip.startswith(ip):
enabled = False

View file

@ -12,7 +12,8 @@
"column_break_5",
"add_container",
"add_padding",
"add_shade"
"add_shade",
"hide_block"
],
"fields": [
{
@ -54,18 +55,24 @@
"default": "0",
"fieldname": "add_shade",
"fieldtype": "Check",
"label": "Shaded Section"
"label": "Add Gray Background"
},
{
"default": "1",
"fieldname": "add_container",
"fieldtype": "Check",
"label": "Add Container"
},
{
"default": "0",
"fieldname": "hide_block",
"fieldtype": "Check",
"label": "Hide Block"
}
],
"istable": 1,
"links": [],
"modified": "2020-04-19 16:16:44.524042",
"modified": "2020-04-29 15:08:25.976179",
"modified_by": "Administrator",
"module": "Website",
"name": "Web Page Block",

View file

@ -31,7 +31,7 @@
"fieldtype": "Select",
"in_list_view": 1,
"label": "Fieldtype",
"options": "Attach Image\nCheck\nData\nInt\nSelect\nSmall Text\nText\nMarkdown Editor",
"options": "Attach Image\nCheck\nData\nInt\nSelect\nSmall Text\nText\nMarkdown Editor\nSection Break\nColumn Break",
"reqd": 1
},
{
@ -48,7 +48,7 @@
],
"istable": 1,
"links": [],
"modified": "2020-04-24 17:05:25.322767",
"modified": "2020-04-29 14:53:23.192395",
"modified_by": "Administrator",
"module": "Website",
"name": "Web Template Field",

View file

@ -9,22 +9,12 @@
</p>
{%- endif -%}
{%- if primary_action or secondary_action -%}
<div class="mt-5 sm:mt-8 sm:flex sm:justify-center lg:justify-start">
<div class="mt-5 sm:mt-8">
{%- if primary_action -%}
<div class="rounded-md shadow">
<a href="{{ primary_action }}"
class="flex items-center justify-center w-full px-8 py-3 text-base font-medium leading-6 text-white transition duration-150 ease-in-out border border-transparent rounded-md bg-primary-500 hover:bg-primary-400 focus:outline-none focus:shadow-outline md:py-4 md:text-lg md:px-10">
{{ primary_action_label }}
</a>
</div>
{{ c('button', label=primary_action_label, url=primary_action, variant="primary", size="large") }}
{%- endif -%}
{%- if secondary_action -%}
<div class="mt-3 sm:mt-0 sm:ml-3">
<a href="{{ secondary_action }}"
class="flex items-center justify-center w-full px-8 py-3 text-base font-medium leading-6 transition duration-150 ease-in-out border border-transparent rounded-md text-primary-700 bg-primary-100 hover:text-primary-600 hover:bg-primary-50 focus:outline-none focus:shadow-outline focus:border-primary-300 md:py-4 md:text-lg md:px-10">
{{ secondary_action_label }}
</a>
</div>
{{ c('button', label=secondary_action_label, url=secondary_action, variant="secondary", size="large", class="ml-4") }}
{%- endif -%}
</div>
{%- endif -%}

View file

@ -1,5 +1,5 @@
<div class="relative flex">
<div class="relative flex items-center flex-shrink-0 w-full sm:w-7/12">
<div class="relative flex items-center flex-shrink-0 w-full md:w-6/12">
<div>
<h1
class="text-3xl font-extrabold leading-tight tracking-tight sm:leading-10 sm:text-4xl xl:leading-none xl:text-5xl">
@ -22,8 +22,16 @@
</div>
</div>
{%- if image -%}
<div class="hidden sm:block lg:w-5/12">
<img class="object-cover w-full h-56 lg:h-full lg:w-full md:h-96 sm:h-72" src="{{ image }}" alt="">
</div>
{{ c('image_with_blur',
class=["hidden md:block max-h-144", "w-full md:w-6/12" if contain_image else "md:max-w-md lg:max-w-lg xl:max-w-xl xxl:max-w-2xl"],
src=image,
alt="")
}}
{%- endif -%}
</div>
{%- if not contain_image -%}
<script>
document.addEventListener('DOMContentLoaded', () => document.body.classList.add('overflow-x-hidden'));
</script>
{%- endif -%}

View file

@ -21,6 +21,12 @@
"label": "Image",
"reqd": 0
},
{
"fieldname": "contain_image",
"fieldtype": "Check",
"label": "Restrict Image inside Container",
"reqd": 0
},
{
"fieldname": "primary_action_label",
"fieldtype": "Data",
@ -47,7 +53,7 @@
}
],
"idx": 0,
"modified": "2020-04-26 15:08:26.937576",
"modified": "2020-04-29 14:12:31.613545",
"modified_by": "Administrator",
"name": "Hero with Right Image",
"owner": "Administrator",

View file

@ -6,8 +6,8 @@
</a>
</div>
<details class="z-10 flex items-center sm:hidden hide-summary-arrow">
<summary>
<button>
<summary class="block h-6 list-none cursor-pointer focus:outline-none focus:shadow-outline">
<button class="pointer-events-none">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="feather feather-menu">

View file

@ -5,9 +5,9 @@
'p-8': card_size == 'Large'
}) -%}
{%- set title_classes = resolve_class({
'text-base': card_size == 'Small',
'text-lg md:text-xl': card_size == 'Medium',
'text-xl md:text-2xl': card_size == 'Large'
'text-base font-semibold': card_size == 'Small',
'text-lg md:text-xl font-semibold': card_size == 'Medium',
'text-xl md:text-2xl font-bold': card_size == 'Large'
}) -%}
{%- set content_classes = resolve_class({
'text-sm': card_size == 'Small',
@ -16,8 +16,8 @@
}) -%}
<a href="{{ url or '#' }}"
class="block bg-white border rounded-xl hover:shadow-lg {{ card_classes }}">
<h3 class="font-semibold {{ title_classes }}">{{ title }}</h3>
class="block bg-white border rounded-xl hover:border-gray-600 {{ card_classes }}">
<h3 class="leading-none {{ title_classes }}">{{ title }}</h3>
<p class="mt-4 text-gray-900 {{ content_classes }}">{{ content or '' }}</p>
</a>
{%- endmacro -%}

View file

@ -22,153 +22,201 @@
"options": "Small\nMedium\nLarge",
"reqd": 0
},
{
"fieldname": "card_1",
"fieldtype": "Section Break",
"label": "Card 1",
"reqd": 0
},
{
"fieldname": "card_1_title",
"fieldtype": "Data",
"label": "Card 1 Title",
"label": "Title",
"reqd": 0
},
{
"fieldname": "card_1_content",
"fieldtype": "Small Text",
"label": "Card 1 Content",
"label": "Content",
"reqd": 0
},
{
"fieldname": "card_1_url",
"fieldtype": "Data",
"label": "Card 1 URL",
"label": "URL",
"reqd": 0
},
{
"fieldname": "card_2",
"fieldtype": "Section Break",
"label": "Card 2",
"reqd": 0
},
{
"fieldname": "card_2_title",
"fieldtype": "Data",
"label": "Card 2 Title",
"label": "Title",
"reqd": 0
},
{
"fieldname": "card_2_content",
"fieldtype": "Small Text",
"label": "Card 2 Content",
"label": "Content",
"reqd": 0
},
{
"fieldname": "card_2_url",
"fieldtype": "Data",
"label": "Card 2 URL",
"label": "URL",
"reqd": 0
},
{
"fieldname": "card_3",
"fieldtype": "Section Break",
"label": "Card 3",
"reqd": 0
},
{
"fieldname": "card_3_title",
"fieldtype": "Data",
"label": "Card 3 Title",
"label": "Title",
"reqd": 0
},
{
"fieldname": "card_3_content",
"fieldtype": "Small Text",
"label": "Card 3 Content",
"label": "Content",
"reqd": 0
},
{
"fieldname": "card_3_url",
"fieldtype": "Data",
"label": "Card 3 URL",
"label": "URL",
"reqd": 0
},
{
"fieldname": "card_4",
"fieldtype": "Section Break",
"label": "Card 4",
"reqd": 0
},
{
"fieldname": "card_4_title",
"fieldtype": "Data",
"label": "Card 4 Title",
"label": "Title",
"reqd": 0
},
{
"fieldname": "card_4_content",
"fieldtype": "Small Text",
"label": "Card 4 Content",
"label": "Content",
"reqd": 0
},
{
"fieldname": "card_4_url",
"fieldtype": "Data",
"label": "Card 4 URL",
"label": "URL",
"reqd": 0
},
{
"fieldname": "card_5",
"fieldtype": "Section Break",
"label": "Card 5",
"reqd": 0
},
{
"fieldname": "card_5_title",
"fieldtype": "Data",
"label": "Card 5 Title",
"label": "Title",
"reqd": 0
},
{
"fieldname": "card_5_content",
"fieldtype": "Small Text",
"label": "Card 5 Content",
"label": "Content",
"reqd": 0
},
{
"fieldname": "card_5_url",
"fieldtype": "Data",
"label": "Card 5 URL",
"label": "URL",
"reqd": 0
},
{
"fieldname": "card_6",
"fieldtype": "Section Break",
"label": "Card 6",
"reqd": 0
},
{
"fieldname": "card_6_title",
"fieldtype": "Data",
"label": "Card 6 Title",
"label": "Title",
"reqd": 0
},
{
"fieldname": "card_6_content",
"fieldtype": "Small Text",
"label": "Card 6 Content",
"label": "Content",
"reqd": 0
},
{
"fieldname": "card_6_url",
"fieldtype": "Data",
"label": "Card 6 URL",
"label": "URL",
"reqd": 0
},
{
"fieldname": "card_7",
"fieldtype": "Section Break",
"label": "Card 7",
"reqd": 0
},
{
"fieldname": "card_7_title",
"fieldtype": "Data",
"label": "Card 7 Title",
"label": "Title",
"reqd": 0
},
{
"fieldname": "card_7_content",
"fieldtype": "Small Text",
"label": "Card 7 Content",
"label": "Content",
"reqd": 0
},
{
"fieldname": "card_7_url",
"fieldtype": "Data",
"label": "Card 7 URL",
"label": "URL",
"reqd": 0
},
{
"fieldname": "card_8",
"fieldtype": "Section Break",
"label": "Card 8",
"reqd": 0
},
{
"fieldname": "card_8_title",
"fieldtype": "Data",
"label": "Card 8 Title",
"label": "Title",
"reqd": 0
},
{
"fieldname": "card_8_content",
"fieldtype": "Small Text",
"label": "Card 8 Content",
"label": "Content",
"reqd": 0
},
{
"fieldname": "card_8_url",
"fieldtype": "Data",
"label": "Card 8 URL",
"label": "URL",
"reqd": 0
}
],
"idx": 0,
"modified": "2020-04-21 21:24:04.192839",
"modified": "2020-04-29 22:40:03.362229",
"modified_by": "Administrator",
"name": "Section with Cards",
"owner": "Administrator",

View file

@ -1,12 +1,12 @@
<div class="relative">
<div class="px-8 py-12 text-center sm:px-12 md:py-20 bg-primary-100 rounded-xl">
<h2 class="max-w-xl mx-auto text-2xl font-bold leading-tight md:text-3xl xl:text-4xl">{{ title }}</h2>
<p class="max-w-xl mx-auto mt-2 text-base text-gray-900 sm:text-lg">{{ subtitle }}</p>
<h2 class="max-w-xl mx-auto text-2xl font-extrabold leading-tight md:text-4xl">{{ title }}</h2>
<p class="max-w-xl mx-auto mt-2 text-base text-gray-900 md:text-lg">{{ subtitle }}</p>
<p class="mt-8">
{{ c('button', label=cta_label, url=cta_url, variant="primary", size="large") }}
</p>
{%- if cta_description -%}
<div class="max-w-xl mx-auto mt-2 text-sm text-gray-900">
<div class="max-w-xl mx-auto mt-2 text-xs text-gray-900">
{{ cta_description }}
</div>
{%- endif -%}

View file

@ -10,7 +10,7 @@
"font_properties": "300,600",
"footer": [],
"idx": 26,
"modified": "2020-04-24 23:52:27.211811",
"modified": "2020-04-29 12:26:48.399125",
"modified_by": "Administrator",
"module": "Website",
"name": "Standard",

View file

@ -27,7 +27,7 @@
"cssnano": "^4.1.10",
"express": "^4.17.1",
"fast-deep-equal": "^2.0.1",
"frappe-charts": "^1.3.0",
"frappe-charts": "^1.3.2",
"frappe-datatable": "^1.15.1",
"frappe-gantt": "^0.1.0",
"fuse.js": "^3.4.6",

View file

@ -46,6 +46,9 @@ module.exports = {
borderRadius: {
xl: '0.75rem'
},
maxHeight: {
'144': '36rem'
},
boxShadow: theme => ({
'outline-primary': `0 0 0 3px ${rgba(theme('colors.blue.300'), 0.45)}`
}),

View file

@ -2304,10 +2304,10 @@ fragment-cache@^0.2.1:
dependencies:
map-cache "^0.2.2"
frappe-charts@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/frappe-charts/-/frappe-charts-1.3.0.tgz#9ed033fa64833906bba16554187fa2f8a3a54ef6"
integrity sha512-hdLv4fOIVgIL5eV9KYlsQaEpxkcJvuEVVDJewJL8PG0ySPy5EEiG5KZGL2uj7YegVWbtsqJ4Oq/74mjgQoMdag==
frappe-charts@^1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/frappe-charts/-/frappe-charts-1.3.2.tgz#28b26637f03efeb86a07a2a9180c0ef2d5f52c56"
integrity sha512-9VscidL7TUxgnI8dM+s0WIphnthUsDwnknHFnUH1zsISWzuw1FEUd2v29cn+E1+eNlD1a0bNBd+rJPtMdMnnvQ==
frappe-datatable@^1.15.1:
version "1.15.1"