Merge branch 'develop' into cust_export
This commit is contained in:
commit
131cbec99a
65 changed files with 2040 additions and 243 deletions
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
||||
|
|
|
|||
0
frappe/desk/doctype/number_card/__init__.py
Normal file
0
frappe/desk/doctype/number_card/__init__.py
Normal file
114
frappe/desk/doctype/number_card/number_card.js
Normal file
114
frappe/desk/doctype/number_card/number_card.js
Normal 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
148
frappe/desk/doctype/number_card/number_card.json
Normal file
148
frappe/desk/doctype/number_card/number_card.json
Normal 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
|
||||
}
|
||||
144
frappe/desk/doctype/number_card/number_card.py
Normal file
144
frappe/desk/doctype/number_card/number_card.py
Normal 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)
|
||||
10
frappe/desk/doctype/number_card/test_number_card.py
Normal file
10
frappe/desk/doctype/number_card/test_number_card.py
Normal 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
|
||||
0
frappe/desk/doctype/number_card_link/__init__.py
Normal file
0
frappe/desk/doctype/number_card_link/__init__.py
Normal file
31
frappe/desk/doctype/number_card_link/number_card_link.json
Normal file
31
frappe/desk/doctype/number_card_link/number_card_link.json
Normal 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
|
||||
}
|
||||
10
frappe/desk/doctype/number_card_link/number_card_link.py
Normal file
10
frappe/desk/doctype/number_card_link/number_card_link.py
Normal 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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
]);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
|
|
|
|||
450
frappe/public/js/frappe/views/dashboard/dashboard_view.js
Normal file
450
frappe/public/js/frappe/views/dashboard/dashboard_view.js
Normal 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);
|
||||
}
|
||||
|
||||
};
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
|||
221
frappe/public/js/frappe/widgets/number_card_widget.js
Normal file
221
frappe/public/js/frappe/widgets/number_card_widget.js
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -162,6 +162,6 @@
|
|||
top: 8px;
|
||||
height: 15px;
|
||||
right: 12px;
|
||||
top: 8px;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
44
frappe/public/less/dashboard_view.less
Normal file
44
frappe/public/less/dashboard_view.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 -%}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 -%}
|
||||
|
|
|
|||
|
|
@ -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 -%}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 -%}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 -%}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)}`
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue