Merge pull request #7742 from prssanna/list-group-by

feat(Sidebar): Add "Group By" dropdown fields to list sidebar
This commit is contained in:
mergify[bot] 2019-07-10 08:35:12 +00:00 committed by GitHub
commit e40ef5e8d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 306 additions and 113 deletions

View file

@ -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');

View file

@ -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):
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

View file

@ -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",

View file

@ -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">

View file

@ -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) {

View file

@ -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);
});
}
};

View file

@ -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>
{% }
} %}

View file

@ -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;
}
.stat-label {
margin-bottom: -10px;
.group-by-count {
position:relative
}
.group-by-dropdown, .list-stats-dropdown {
max-height: 300px;
overflow-y: auto;
max-width: 200px;
}
.dropdown-search {
padding: 8px;
}
}

View file

@ -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)