Parcourir la source

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

feat(Sidebar): Add "Group By" dropdown fields to list sidebar
version-14
mergify[bot] il y a 6 ans
committed by GitHub
Parent
révision
e40ef5e8d6
Aucune clé connue n'a été trouvée dans la base pour cette signature ID de la clé GPG: 4AEE18F83AFDEB23
9 fichiers modifiés avec 306 ajouts et 113 suppressions
  1. +1
    -1
      cypress/integration/list_view_settings.js
  2. +28
    -24
      frappe/desk/listview.py
  3. +1
    -0
      frappe/public/build.json
  4. +21
    -11
      frappe/public/js/frappe/list/list_sidebar.html
  5. +44
    -43
      frappe/public/js/frappe/list/list_sidebar.js
  6. +175
    -0
      frappe/public/js/frappe/list/list_sidebar_group_by.js
  7. +16
    -22
      frappe/public/js/frappe/list/list_sidebar_stat.html
  8. +17
    -9
      frappe/public/less/sidebar.less
  9. +3
    -3
      frappe/tests/test_assign.py

+ 1
- 1
cypress/integration/list_view_settings.js Voir le fichier

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


+ 28
- 24
frappe/desk/listview.py Voir le fichier

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

+ 1
- 0
frappe/public/build.json Voir le fichier

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


+ 21
- 11
frappe/public/js/frappe/list/list_sidebar.html Voir le fichier

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


+ 44
- 43
frappe/public/js/frappe/list/list_sidebar.js Voir le fichier

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


+ 175
- 0
frappe/public/js/frappe/list/list_sidebar_group_by.js Voir le fichier

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

};

+ 16
- 22
frappe/public/js/frappe/list/list_sidebar_stat.html Voir le fichier

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

+ 17
- 9
frappe/public/less/sidebar.less Voir le fichier

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



+ 3
- 3
frappe/tests/test_assign.py Voir le fichier

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


Chargement…
Annuler
Enregistrer