feat(Sidebar): Add "Group By" dropdown fields to list sidebarversion-14
@@ -6,7 +6,7 @@ context('List View Settings', () => { | |||
it('Default settings', () => { | |||
cy.visit('/desk#List/DocType/List'); | |||
cy.get('.list-count').should('contain', "20 of"); | |||
cy.get('.sidebar-stat').should('contain', "No Tags"); | |||
cy.get('.sidebar-stat').should('contain', "Tags"); | |||
}); | |||
it('disable count and sidebar stats then verify', () => { | |||
cy.visit('/desk#List/DocType/List'); | |||
@@ -3,8 +3,6 @@ | |||
from __future__ import unicode_literals | |||
import frappe | |||
import json | |||
@frappe.whitelist() | |||
def get_list_settings(doctype): | |||
@@ -22,31 +20,37 @@ def set_list_settings(doctype, values): | |||
doc = frappe.new_doc("List View Setting") | |||
doc.name = doctype | |||
frappe.clear_messages() | |||
doc.update(json.loads(values)) | |||
doc.update(frappe.parse_json(values)) | |||
doc.save() | |||
@frappe.whitelist() | |||
def get_user_assignments_and_count(doctype, current_filters): | |||
@frappe.whitelist() | |||
def get_group_by_count(doctype, current_filters, field): | |||
current_filters = frappe.parse_json(current_filters) | |||
subquery_condition = '' | |||
if current_filters: | |||
# get the subquery | |||
subquery = frappe.get_all(doctype, | |||
filters=current_filters, return_query = True) | |||
subquery = frappe.get_all(doctype, filters=current_filters, return_query = True) | |||
if field == 'assigned_to': | |||
subquery_condition = ' and `tabToDo`.reference_name in ({subquery})'.format(subquery = subquery) | |||
return frappe.db.sql("""select `tabToDo`.owner as name, count(*) as count | |||
from | |||
`tabToDo`, `tabUser` | |||
where | |||
`tabToDo`.status='Open' and | |||
`tabToDo`.owner = `tabUser`.name and | |||
`tabUser`.user_type = 'System User' | |||
{subquery_condition} | |||
group by | |||
`tabToDo`.owner | |||
order by | |||
count desc | |||
limit 50""".format(subquery_condition = subquery_condition), as_dict=True) | |||
else : | |||
return frappe.db.get_list(doctype, | |||
filters=current_filters, | |||
group_by=field, | |||
fields=['count(*) as count', field + ' as name'], | |||
order_by='count desc', | |||
limit=50, | |||
) | |||
todo_list = frappe.db.sql("""select `tabToDo`.owner as name, count(*) as count | |||
from | |||
`tabToDo`, `tabUser` | |||
where | |||
`tabToDo`.status='Open' and | |||
`tabToDo`.owner = `tabUser`.name and | |||
`tabUser`.user_type = 'System User' | |||
{subquery_condition} | |||
group by | |||
`tabToDo`.owner | |||
order by | |||
count desc | |||
limit 50""".format(subquery_condition = subquery_condition), as_dict=True) | |||
return todo_list |
@@ -276,6 +276,7 @@ | |||
"public/js/frappe/list/list_sidebar.js", | |||
"public/js/frappe/list/list_sidebar.html", | |||
"public/js/frappe/list/list_sidebar_stat.html", | |||
"public/js/frappe/list/list_sidebar_group_by.js", | |||
"public/js/frappe/list/list_view_permission_restrictions.html", | |||
"public/js/frappe/views/gantt/gantt_view.js", | |||
@@ -52,21 +52,31 @@ | |||
</ul> | |||
</div> | |||
</li> | |||
<li class="assigned-to" style="display: none"> | |||
<a class = "dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" href="#" onclick="return false;"> | |||
{%= __("Assigned To") %} <span class="caret"></span> | |||
</a> | |||
<ul class="dropdown-menu assigned-dropdown" style="max-height: 300px; overflow-y: auto;" role="menu"> | |||
<li><div class="list-loading text-center assigned-loading text-muted"> | |||
{%= (__("Loading") + "..." ) %} | |||
</div> | |||
</li> | |||
</ul> | |||
</li> | |||
{% if(frappe.help.has_help(doctype)) { %} | |||
<li><a class="help-link list-link" data-doctype="{{ doctype }}">{{ __("Help") }}</a></li> | |||
{% } %} | |||
</ul> | |||
<ul class="list-unstyled sidebar-menu list-group-by"> | |||
</ul> | |||
<ul class="list-unstyled sidebar-menu sidebar-stat"> | |||
<li class="list-sidebar-label stat-label">{{ __("Tags") }}</li> | |||
<li class="list-stats list-link"> | |||
<div class="btn-group"> | |||
<a class = "dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" href="#" onclick="return false;"> | |||
{{ __("Tags") }}<span class="caret"></span> | |||
</a> | |||
<ul class="dropdown-menu list-stats-dropdown" role="menu"> | |||
<div class="dropdown-search"> | |||
<input type="text" placeholder="Search" class="form-control dropdown-search-input input-xs"> | |||
</div> | |||
</ul> | |||
</div> | |||
</li> | |||
<li class="list-link" style="margin-top: 10px;"> | |||
<a class="list-tag-preview hidden-xs text-muted">{{ __("Show tags") }}</a> | |||
</li> | |||
</ul> | |||
<ul class="list-unstyled sidebar-menu charts-menu hide"> | |||
<li class="h6">{%= __("Charts") %}</li> | |||
<li class="list-link"> | |||
@@ -13,7 +13,6 @@ frappe.views.ListSidebar = class ListSidebar { | |||
constructor(opts) { | |||
$.extend(this, opts); | |||
this.make(); | |||
this.get_stats(); | |||
this.cat_tags = []; | |||
} | |||
@@ -23,7 +22,7 @@ frappe.views.ListSidebar = class ListSidebar { | |||
this.sidebar = $('<div class="list-sidebar overlay-sidebar hidden-xs hidden-sm"></div>') | |||
.html(sidebar_content) | |||
.appendTo(this.page.sidebar.empty()); | |||
this.setup_reports(); | |||
this.setup_list_filter(); | |||
this.setup_views(); | |||
@@ -31,6 +30,7 @@ frappe.views.ListSidebar = class ListSidebar { | |||
this.setup_calendar_view(); | |||
this.setup_email_inbox(); | |||
this.setup_keyboard_shortcuts(); | |||
this.setup_list_group_by(); | |||
let limits = frappe.boot.limits; | |||
@@ -38,13 +38,14 @@ frappe.views.ListSidebar = class ListSidebar { | |||
this.setup_upgrade_box(); | |||
} | |||
if(this.doctype !== 'ToDo') { | |||
$('.assigned-to').show(); | |||
if (this.list_view.list_view_settings && this.list_view.list_view_settings.disable_sidebar_stats) { | |||
this.sidebar.find('.sidebar-stat').remove(); | |||
} else { | |||
this.sidebar.find('.list-stats').on('click', (e) => { | |||
$(e.currentTarget).find('.stat-link').remove(); | |||
this.get_stats(); | |||
}); | |||
} | |||
$('.assigned-to').on('click', () => { | |||
$('.assigned').remove(); | |||
this.setup_assigned_to(); | |||
}); | |||
} | |||
@@ -225,31 +226,6 @@ frappe.views.ListSidebar = class ListSidebar { | |||
}); | |||
} | |||
setup_assigned_to() { | |||
$('.assigned-loading').show(); | |||
let dropdown = this.page.sidebar.find('.assigned-dropdown'); | |||
let current_filters = this.list_view.get_filters_for_args(); | |||
frappe.call('frappe.desk.listview.get_user_assignments_and_count', {doctype: this.doctype, current_filters: current_filters}).then((data) => { | |||
$('.assigned-loading').hide(); | |||
let current_user = data.message.find(user => user.name === frappe.session.user); | |||
if(current_user) { | |||
let current_user_count = current_user.count; | |||
this.get_html_for_assigned(frappe.session.user, current_user_count).appendTo(dropdown); | |||
} | |||
let user_list = data.message.filter(user => !['Guest', frappe.session.user, 'Administrator'].includes(user.name) && user.count!==0 ); | |||
user_list.forEach((user) => { | |||
this.get_html_for_assigned(user.name, user.count).appendTo(dropdown); | |||
}); | |||
$(".assigned-dropdown li a").on("click", (e) => { | |||
let assigned_user = $(e.currentTarget).find($('.assigned-user')).text(); | |||
if(assigned_user === 'Me') assigned_user = frappe.session.user; | |||
this.list_view.filter_area.remove('_assign'); | |||
this.list_view.filter_area.add(this.list_view.doctype, "_assign", "like", `%${assigned_user}%`); | |||
}); | |||
}); | |||
} | |||
setup_keyboard_shortcuts() { | |||
this.sidebar.find('.list-link > a, .list-link > .btn-group > a').each((i, el) => { | |||
frappe.ui.keys | |||
@@ -258,12 +234,38 @@ frappe.views.ListSidebar = class ListSidebar { | |||
}); | |||
} | |||
get_html_for_assigned(name, count) { | |||
if (name === frappe.session.user) name='Me'; | |||
if (count > 99) count='99+'; | |||
let html = $('<li class="assigned"><a class="badge-hover" href="#" onclick="return false;" role="assigned-item"><span class="assigned-user">' | |||
+ name + '</span><span class="badge pull-right" style="position:relative">' + count + '</span></a></li>'); | |||
return html; | |||
setup_list_group_by() { | |||
this.list_group_by = new frappe.views.ListGroupBy({ | |||
doctype: this.doctype, | |||
sidebar: this, | |||
list_view: this.list_view, | |||
page: this.page | |||
}); | |||
} | |||
setup_dropdown_search(dropdown, text_class) { | |||
let $dropdown_search = dropdown.find('.dropdown-search').show(); | |||
let $search_input = $dropdown_search.find('.dropdown-search-input'); | |||
$search_input.focus(); | |||
$dropdown_search.on('click',(e)=>{ | |||
e.stopPropagation(); | |||
}); | |||
let $elements = dropdown.find('li'); | |||
$dropdown_search.on('keyup',()=> { | |||
let text_filter = $search_input.val().toLowerCase(); | |||
let text; | |||
for (var i = 0; i < $elements.length; i++) { | |||
text = $elements.eq(i).find(text_class).text(); | |||
if (text.toLowerCase().indexOf(text_filter) > -1) { | |||
$elements.eq(i).css('display',''); | |||
} else { | |||
$elements.eq(i).css('display','none'); | |||
} | |||
} | |||
}); | |||
dropdown.parent().on('hide.bs.dropdown',()=> { | |||
$dropdown_search.val(''); | |||
}); | |||
} | |||
setup_upgrade_box() { | |||
@@ -302,9 +304,6 @@ frappe.views.ListSidebar = class ListSidebar { | |||
get_stats() { | |||
var me = this; | |||
if (this.list_view.list_view_settings && this.list_view.list_view_settings.disable_sidebar_stats) { | |||
return; | |||
} | |||
frappe.call({ | |||
method: 'frappe.desk.reportview.get_sidebar_stats', | |||
type: 'GET', | |||
@@ -337,6 +336,8 @@ frappe.views.ListSidebar = class ListSidebar { | |||
//render normal stats | |||
me.render_stat("_user_tags", (r.message.stats || {})["_user_tags"]); | |||
} | |||
let stats_dropdown = me.sidebar.find('.list-stats-dropdown'); | |||
me.setup_dropdown_search(stats_dropdown,'.stat-label'); | |||
} | |||
}); | |||
} | |||
@@ -399,7 +400,7 @@ frappe.views.ListSidebar = class ListSidebar { | |||
me.list_view.refresh(); | |||
}); | |||
}) | |||
.insertBefore(this.sidebar.find(".close-sidebar-button")); | |||
.appendTo(this.sidebar.find(".list-stats-dropdown")); | |||
} | |||
set_fieldtype(df) { | |||
@@ -0,0 +1,175 @@ | |||
frappe.provide('frappe.views'); | |||
frappe.views.ListGroupBy = class ListGroupBy { | |||
constructor(opts) { | |||
$.extend(this, opts); | |||
this.make_wrapper(); | |||
this.user_settings = frappe.get_user_settings(this.doctype); | |||
this.group_by_fields = ['assigned_to']; | |||
if(this.user_settings.group_by_fields) { | |||
this.group_by_fields = this.group_by_fields.concat(this.user_settings.group_by_fields); | |||
} | |||
this.render_group_by_items(); | |||
this.make_group_by_fields_modal(); | |||
this.setup_dropdown(); | |||
this.setup_filter_by(); | |||
} | |||
make_group_by_fields_modal() { | |||
let d = new frappe.ui.Dialog ({ | |||
title: __("Add Filter By"), | |||
fields: this.get_group_by_dropdown_fields() | |||
}); | |||
d.set_primary_action("Add", ({ group_by_fields }) => { | |||
frappe.model.user_settings.save(this.doctype, 'group_by_fields', group_by_fields || null); | |||
this.group_by_fields = group_by_fields ? ['assigned_to', ...group_by_fields] : ['assigned_to']; | |||
this.render_group_by_items(); | |||
d.hide(); | |||
}); | |||
this.page.sidebar.find(".add-list-group-by a ").on("click", () => { | |||
d.show(); | |||
}); | |||
} | |||
make_wrapper() { | |||
this.$wrapper = this.sidebar.sidebar.find('.list-group-by'); | |||
let html = ` | |||
<li class="list-sidebar-label"> | |||
${__('Filter By')} | |||
</li> | |||
<div class="list-group-by-fields"> | |||
</div> | |||
<li class="add-list-group-by list-link"> | |||
<a class="add-group-by hidden-xs text-muted"> | |||
${__("Add Fields")} <i class="octicon octicon-plus" style="margin-left: 2px;"></i> | |||
</a> | |||
</li> | |||
`; | |||
this.$wrapper.html(html); | |||
} | |||
render_group_by_items() { | |||
let get_item_html = (fieldname) => { | |||
let label = fieldname === 'assigned_to' | |||
? __('Assigned To') | |||
: frappe.meta.get_label(this.doctype, fieldname); | |||
return `<li class="group-by-field list-link"> | |||
<div class="btn-group"> | |||
<a class = "dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" | |||
data-label="${label}" data-fieldname="${fieldname}" href="#" onclick="return false;"> | |||
${__(label)}<span class="caret"></span> | |||
</a> | |||
<ul class="dropdown-menu group-by-dropdown" role="menu"> | |||
<li><div class="list-loading text-center group-by-loading text-muted"> | |||
${__("Loading...")} | |||
</div> | |||
</li> | |||
</ul> | |||
</div> | |||
</li>`; | |||
}; | |||
let html = this.group_by_fields.map(get_item_html).join(''); | |||
this.$wrapper.find('.list-group-by-fields').html(html); | |||
} | |||
setup_dropdown() { | |||
this.$wrapper.on('click', '.group-by-field', (e)=> { | |||
let dropdown = $(e.currentTarget).find('.group-by-dropdown'); | |||
let fieldname = $(e.currentTarget).find('a').attr('data-fieldname'); | |||
this.get_group_by_count(fieldname).then(field_count_list => { | |||
if (field_count_list.length) { | |||
this.render_dropdown_items(field_count_list, dropdown); | |||
this.sidebar.setup_dropdown_search(dropdown, '.group-by-value'); | |||
} else { | |||
dropdown.find('.group-by-loading').hide(); | |||
} | |||
}); | |||
}); | |||
} | |||
get_group_by_dropdown_fields() { | |||
let group_by_fields = []; | |||
let fields = this.list_view.meta.fields.filter((f)=> ["Select", "Link"].includes(f.fieldtype)); | |||
group_by_fields.push({ | |||
label: __(this.doctype), | |||
fieldname: 'group_by_fields', | |||
fieldtype: 'MultiCheck', | |||
columns: 2, | |||
options: fields | |||
.map(df => ({ | |||
label: __(df.label), | |||
value: df.fieldname, | |||
checked: this.group_by_fields.includes(df.fieldname) | |||
})) | |||
}); | |||
return group_by_fields; | |||
} | |||
get_group_by_count(field) { | |||
let args = { | |||
doctype: this.doctype, | |||
current_filters: this.list_view.get_filters_for_args(), | |||
field: field, | |||
}; | |||
return frappe.call('frappe.desk.listview.get_group_by_count', args).then((r) => { | |||
let field_counts = r.message || []; | |||
field_counts = field_counts.filter(f => f.count !== 0); | |||
if (field === 'assigned_to') { | |||
field_counts = field_counts.filter(f => !['Guest', 'Administrator'].includes(f.name)); | |||
} | |||
return field_counts; | |||
}); | |||
} | |||
render_dropdown_items(fields, dropdown) { | |||
let get_dropdown_html = (field) => { | |||
let label = field.name == null ? __('Not Specified') : field.name; | |||
if (label === frappe.session.user) { | |||
label = __('Me'); | |||
} | |||
let value = field.name == null ? '' : encodeURIComponent(field.name); | |||
return `<li class="group-by-item" data-value="${value}"> | |||
<a class="badge-hover" href="#" onclick="return false;"> | |||
<span class="group-by-value">${label}</span> | |||
<span class="badge pull-right group-by-count">${field.count}</span> | |||
</a> | |||
</li>`; | |||
}; | |||
let standard_html = ` | |||
<div class="dropdown-search"> | |||
<input type="text" placeholder="${__('Search')}" class="form-control dropdown-search-input input-xs"> | |||
</div> | |||
`; | |||
let dropdown_html = standard_html + fields.map(get_dropdown_html).join(''); | |||
dropdown.html(dropdown_html); | |||
} | |||
setup_filter_by() { | |||
this.$wrapper.on('click', '.group-by-item', (e) => { | |||
let $target = $(e.currentTarget); | |||
let fieldname = $target.parents('.group-by-field').find('a').data('fieldname'); | |||
let value = decodeURIComponent($target.data('value').trim()); | |||
fieldname = fieldname === 'assigned_to' ? '_assign': fieldname; | |||
this.list_view.filter_area.remove(fieldname); | |||
let operator = '='; | |||
if (value === '') { | |||
operator = 'is'; | |||
value = 'not set'; | |||
} | |||
if (fieldname === '_assign') { | |||
operator = 'like'; | |||
value = `%${value}%`; | |||
} | |||
this.list_view.filter_area.add(this.doctype, fieldname, operator, value); | |||
}); | |||
} | |||
}; |
@@ -1,22 +1,16 @@ | |||
<ul class="list-unstyled sidebar-menu sidebar-stat"> | |||
<li class="divider"></li> | |||
<li class="h6 stat-label">{{ label }}</li> | |||
{% if(!stat.length) { %} | |||
<li class="stat-no-records text-muted">{{ __("No records tagged.") }}</li> | |||
{% } else { | |||
for (var i=0, l=stat.length; i < l; i++) { | |||
var stat_label = stat[i][0]; | |||
var stat_count = stat[i][1]; | |||
%} | |||
<li> | |||
<a class="stat-link badge-hover" data-label="{{ stat_label %}" data-field="{{ field %}"> | |||
<span class="badge">{{ stat_count }}</span> | |||
<span>{{ __(stat_label) }}</span> | |||
</a> | |||
</li> | |||
{% } | |||
} %} | |||
</ul> | |||
<div style="margin-top: 10px;"> | |||
<a class="list-tag-preview hidden-xs text-muted">{{ __("Show tags") }}</a> | |||
</div> | |||
{% if(!stat.length) { %} | |||
<li class="stat-no-records text-muted">{{ __("No records tagged.") }}</li> | |||
{% } else { | |||
for (var i=0, l=stat.length; i < l; i++) { | |||
var stat_label = stat[i][0]; | |||
var stat_count = stat[i][1]; | |||
%} | |||
<li> | |||
<a class="stat-link badge-hover" data-label="{{ stat_label %}" data-field="{{ field %}" href="#" onclick="return false;"> | |||
<span class="badge pull-right" style="position: relative">{{ stat_count }}</span> | |||
<span class="stat-label">{{ __(stat_label) }}</span> | |||
</a> | |||
</li> | |||
{% } | |||
} %} |
@@ -126,9 +126,6 @@ body[data-route^="Module"] .main-menu { | |||
} | |||
} | |||
.stat-link { | |||
margin-bottom: 0.5em; | |||
} | |||
a.close { | |||
position: absolute; | |||
@@ -387,14 +384,25 @@ body[data-route^="Module"] .main-menu { | |||
} | |||
.sidebar-left .list-sidebar { | |||
.stat-label, | |||
.stat-no-records { | |||
.sidebar-padding; | |||
.list-sidebar { | |||
.list-sidebar-label { | |||
color: @text-muted; | |||
text-transform: uppercase; | |||
margin-bottom: 0; | |||
font-size: @text-small; | |||
} | |||
.group-by-count { | |||
position:relative | |||
} | |||
.stat-label { | |||
margin-bottom: -10px; | |||
.group-by-dropdown, .list-stats-dropdown { | |||
max-height: 300px; | |||
overflow-y: auto; | |||
max-width: 200px; | |||
} | |||
.dropdown-search { | |||
padding: 8px; | |||
} | |||
} | |||
@@ -4,7 +4,7 @@ from __future__ import unicode_literals | |||
import frappe, unittest | |||
import frappe.desk.form.assign_to | |||
from frappe.desk.listview import get_user_assignments_and_count | |||
from frappe.desk.listview import get_group_by_count | |||
from frappe.automation.doctype.assignment_rule.test_assignment_rule import make_note | |||
class TestAssign(unittest.TestCase): | |||
@@ -44,13 +44,13 @@ class TestAssign(unittest.TestCase): | |||
note = make_note() | |||
assign(note, "test_assign2@example.com") | |||
data = {d.name: d.count for d in get_user_assignments_and_count('Note', [])} | |||
data = {d.name: d.count for d in get_group_by_count('Note', '[]', 'assigned_to')} | |||
self.assertTrue('test_assign1@example.com' in data) | |||
self.assertEqual(data['test_assign1@example.com'], 1) | |||
self.assertEqual(data['test_assign2@example.com'], 3) | |||
data = {d.name: d.count for d in get_user_assignments_and_count('Note', [{'public': 1}])} | |||
data = {d.name: d.count for d in get_group_by_count('Note', '[{"public": 1}]', 'assigned_to')} | |||
self.assertFalse('test_assign1@example.com' in data) | |||
self.assertEqual(data['test_assign2@example.com'], 2) | |||