Browse Source
[ADD] web_edit_user_filter
[ADD] web_edit_user_filter
[FIX] Readme [FIX] Lint [ADD] Tests [IMP] UI/UX [IMP] Hide popover when other is opened [IMP] Add item to roadmappull/1193/head
tarteo
6 years ago
committed by
Dennis Sluijk
16 changed files with 517 additions and 0 deletions
-
100web_edit_user_filter/README.rst
-
1web_edit_user_filter/__init__.py
-
22web_edit_user_filter/__manifest__.py
-
1web_edit_user_filter/models/__init__.py
-
23web_edit_user_filter/models/ir_filters.py
-
1web_edit_user_filter/readme/CONTRIBUTORS.rst
-
4web_edit_user_filter/readme/DESCRIPTION.rst
-
2web_edit_user_filter/readme/ROADMAP.rst
-
15web_edit_user_filter/readme/USAGE.rst
-
BINweb_edit_user_filter/static/description/edit_facet.png
-
242web_edit_user_filter/static/src/js/backend.js
-
27web_edit_user_filter/static/src/scss/backend.scss
-
30web_edit_user_filter/static/src/xml/backend.xml
-
16web_edit_user_filter/templates/assets.xml
-
1web_edit_user_filter/tests/__init__.py
-
32web_edit_user_filter/tests/test_edit_user_filter.py
@ -0,0 +1,100 @@ |
|||
================= |
|||
Edit User Filters |
|||
================= |
|||
|
|||
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! |
|||
!! This file is generated by oca-gen-addon-readme !! |
|||
!! changes will be overwritten. !! |
|||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! |
|||
|
|||
.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png |
|||
:target: https://odoo-community.org/page/development-status |
|||
:alt: Production/Stable |
|||
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png |
|||
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html |
|||
:alt: License: AGPL-3 |
|||
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fweb-lightgray.png?logo=github |
|||
:target: https://github.com/OCA/web/tree/12.0/web_edit_user_filter |
|||
:alt: OCA/web |
|||
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png |
|||
:target: https://translation.odoo-community.org/projects/web-12-0/web-12-0-web_edit_user_filter |
|||
:alt: Translate me on Weblate |
|||
.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png |
|||
:target: https://runbot.odoo-community.org/runbot/162/12.0 |
|||
:alt: Try me on Runbot |
|||
|
|||
|badge1| |badge2| |badge3| |badge4| |badge5| |
|||
|
|||
In standard Odoo you can edit user filters via the debug module. |
|||
The problem is that normal users often don't have access to this menu therefore can't adjust filters once they're saved. |
|||
This module makes this feature available for normal users with a user friendly interface. |
|||
It also adds the ability to adjust facets (a single part of the filter). |
|||
|
|||
**Table of contents** |
|||
|
|||
.. contents:: |
|||
:local: |
|||
|
|||
Usage |
|||
===== |
|||
|
|||
Edit a favourite filter: |
|||
|
|||
#. Go to a list or kanban view; |
|||
#. open the advanced search options; |
|||
#. open the 'Favorites' menu; |
|||
#. click on the pencil icon to start editing the filter. |
|||
|
|||
Edit a facet: |
|||
|
|||
#. Click on the facet; |
|||
#. a menu is now shown which allows you to remove values from the facet; |
|||
#. to cancel removal you can click outside the popover. |
|||
|
|||
.. image:: /web_edit_user_filter/static/description/edit_facet.png |
|||
:alt: Edit Facet |
|||
|
|||
Known issues / Roadmap |
|||
====================== |
|||
|
|||
* Make individual values in facets editable. |
|||
|
|||
Bug Tracker |
|||
=========== |
|||
|
|||
Bugs are tracked on `GitHub Issues <https://github.com/OCA/web/issues>`_. |
|||
In case of trouble, please check there if your issue has already been reported. |
|||
If you spotted it first, help us smashing it by providing a detailed and welcomed |
|||
`feedback <https://github.com/OCA/web/issues/new?body=module:%20web_edit_user_filter%0Aversion:%2012.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_. |
|||
|
|||
Do not contact contributors directly about support or help with technical issues. |
|||
|
|||
Credits |
|||
======= |
|||
|
|||
Authors |
|||
~~~~~~~ |
|||
|
|||
* Onestein |
|||
|
|||
Contributors |
|||
~~~~~~~~~~~~ |
|||
|
|||
* Dennis Sluijk <d.sluijk@onestein.nl> |
|||
|
|||
Maintainers |
|||
~~~~~~~~~~~ |
|||
|
|||
This module is maintained by the OCA. |
|||
|
|||
.. image:: https://odoo-community.org/logo.png |
|||
:alt: Odoo Community Association |
|||
:target: https://odoo-community.org |
|||
|
|||
OCA, or the Odoo Community Association, is a nonprofit organization whose |
|||
mission is to support the collaborative development of Odoo features and |
|||
promote its widespread use. |
|||
|
|||
This module is part of the `OCA/web <https://github.com/OCA/web/tree/12.0/web_edit_user_filter>`_ project on GitHub. |
|||
|
|||
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. |
@ -0,0 +1 @@ |
|||
from . import models |
@ -0,0 +1,22 @@ |
|||
# Copyright 2019 Onestein |
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). |
|||
|
|||
{ |
|||
'name': 'Edit User Filters', |
|||
'category': 'Extra Tools', |
|||
'version': '12.0.1.0.0', |
|||
'development_status': 'Production/Stable', |
|||
'author': 'Onestein,Odoo Community Association (OCA)', |
|||
'license': 'AGPL-3', |
|||
'website': 'https://github.com/OCA/web', |
|||
'depends': [ |
|||
'web' |
|||
], |
|||
'data': [ |
|||
'templates/assets.xml' |
|||
], |
|||
'qweb': [ |
|||
'static/src/xml/backend.xml' |
|||
], |
|||
'installable': True, |
|||
} |
@ -0,0 +1 @@ |
|||
from . import ir_filters |
@ -0,0 +1,23 @@ |
|||
# Copyright 2019 Onestein |
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). |
|||
|
|||
from odoo import api, fields, models |
|||
|
|||
|
|||
class IrFilters(models.Model): |
|||
_inherit = 'ir.filters' |
|||
|
|||
facet = fields.Text() |
|||
|
|||
@api.model |
|||
def get_filters(self, model, action_id=None): |
|||
res = super().get_filters(model, action_id) |
|||
ids = map(lambda f: f['id'], res) |
|||
# Browse filters that are in res |
|||
filters = self.browse(ids) |
|||
for i, res_filter in enumerate(res): |
|||
# Add the field 'facet' to the result |
|||
res[i]['facet'] = filters.filtered( |
|||
lambda f: f.id == res_filter['id'] |
|||
).facet |
|||
return res |
@ -0,0 +1 @@ |
|||
* Dennis Sluijk <d.sluijk@onestein.nl> |
@ -0,0 +1,4 @@ |
|||
In standard Odoo you can edit user filters via the debug module. |
|||
The problem is that normal users often don't have access to this menu therefore can't adjust filters once they're saved. |
|||
This module makes this feature available for normal users with a user friendly interface. |
|||
It also adds the ability to adjust facets (a single part of the filter). |
@ -0,0 +1,2 @@ |
|||
* Make individual values in facets editable. |
|||
* Make saved filters easy overwritable. |
@ -0,0 +1,15 @@ |
|||
Edit a favourite filter: |
|||
|
|||
#. Go to a list or kanban view; |
|||
#. open the advanced search options; |
|||
#. open the 'Favorites' menu; |
|||
#. click on the pencil icon to start editing the filter. |
|||
|
|||
Edit a facet: |
|||
|
|||
#. Click on the facet; |
|||
#. a menu is now shown which allows you to remove values from the facet; |
|||
#. to cancel removal you can click outside the popover. |
|||
|
|||
.. image:: /web_edit_user_filter/static/description/edit_facet.png |
|||
:alt: Edit Facet |
After Width: 414 | Height: 150 | Size: 12 KiB |
@ -0,0 +1,242 @@ |
|||
/* Copyright 2019 Onestein |
|||
* License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */
|
|||
|
|||
odoo.define('web_edit_user_filter', function (require) { |
|||
"use strict"; |
|||
|
|||
var FavoriteMenu = require('web.FavoriteMenu'), |
|||
core = require('web.core'), |
|||
SearchView = require('web.SearchView'); |
|||
var qweb = core.qweb; |
|||
var _t = core._t; |
|||
|
|||
|
|||
FavoriteMenu.include({ |
|||
|
|||
/** |
|||
* Adds the facets data to the filter. |
|||
* |
|||
* @override |
|||
* @private |
|||
*/ |
|||
_createFilter: function (filter) { |
|||
var facets = []; |
|||
|
|||
this.query.each(function (facet) { |
|||
var json_facet = facet.attributes; |
|||
json_facet.values = facet.get('values'); |
|||
|
|||
_.each(json_facet.values, function (value, i) { |
|||
if (typeof value.value === 'object' && |
|||
'attrs' in value.value) { |
|||
json_facet.values[i] = { |
|||
attrs: value.value.attrs, |
|||
}; |
|||
} |
|||
}); |
|||
|
|||
if ('field' in json_facet) { |
|||
json_facet.field = { |
|||
attrs: json_facet.field.attrs, |
|||
}; |
|||
} |
|||
|
|||
facets.push(json_facet); |
|||
}); |
|||
|
|||
filter.facet = JSON.stringify(facets); |
|||
return this._super(filter); |
|||
}, |
|||
|
|||
/** |
|||
* Adds the edit button to the favourite filter menu item. |
|||
* |
|||
* @override |
|||
* @private |
|||
*/ |
|||
append_filter: function (filter) { |
|||
var self = this; |
|||
var res = this._super(filter); |
|||
var key = this.key_for(filter); |
|||
this.$filters[key].append($('<span>', { |
|||
class: 'fa fa-pencil o-edit-user-filter', |
|||
on: { |
|||
click: function (event) { |
|||
event.stopImmediatePropagation(); |
|||
self._unpackFilter(filter); |
|||
}, |
|||
}, |
|||
})); |
|||
return res; |
|||
}, |
|||
|
|||
/** |
|||
* Unpacks a saved filter and updates the search view's facets. |
|||
* |
|||
* @private |
|||
*/ |
|||
_unpackFilter: function (filter) { |
|||
var self = this; |
|||
var facets = JSON.parse(filter.facet); |
|||
|
|||
var new_facets = []; |
|||
this.query.reset([]); |
|||
|
|||
_.each(facets, function (segment) { |
|||
if (segment.cat === 'groupByCategory') { |
|||
_.each(segment.values, function (value) { |
|||
var groupBy = _.find( |
|||
self.searchview.groupbysMapping, |
|||
function (mapping) { |
|||
return mapping.groupby.attrs.context === value.attrs.context; |
|||
} |
|||
); |
|||
var eventData = { |
|||
category: 'groupByCategory', |
|||
itemId: groupBy.groupbyId, |
|||
isActive: true, |
|||
groupId: groupBy.groupId, |
|||
}; |
|||
self.trigger_up('menu_item_toggled', eventData); |
|||
}); |
|||
} else if (segment.cat === 'filterCategory') { |
|||
_.each(segment.values, function (value) { |
|||
var filterDomain = _.find( |
|||
self.searchview.filtersMapping, |
|||
function (mapping) { |
|||
return mapping.filter.attrs.domain === value.attrs.domain; |
|||
} |
|||
); |
|||
var eventData = { |
|||
category: 'filterCategory', |
|||
itemId: filterDomain.filterId, |
|||
isActive: true, |
|||
groupId: filterDomain.groupId, |
|||
}; |
|||
self.trigger_up('menu_item_toggled', eventData); |
|||
}); |
|||
} else { |
|||
var search_widget = _.find( |
|||
self.searchview.search_fields, function (f) { |
|||
return f.attrs.name === segment.field.attrs.name; |
|||
} |
|||
); |
|||
new_facets.push({ |
|||
category: segment.category, |
|||
field: search_widget, |
|||
values: segment.values, |
|||
}); |
|||
} |
|||
}); |
|||
|
|||
this.query.add(new_facets); |
|||
}, |
|||
}); |
|||
|
|||
SearchView.include({ |
|||
|
|||
/** |
|||
* Removes a value from a facet. |
|||
* |
|||
* @private |
|||
* @param {Backbone.Model} model |
|||
* @param {Integer|Object} value The value to remove |
|||
*/ |
|||
_removeValue: function (model, value) { |
|||
var toRemove = model.values.filter(function (v) { |
|||
if (typeof v.attributes.value === 'object') { |
|||
return v.attributes.value.attrs.name === value; |
|||
} |
|||
|
|||
return v.attributes.value.toString() === value; |
|||
}); |
|||
model.values.remove(toRemove); |
|||
}, |
|||
|
|||
/** |
|||
* Renders a popover for a facet. |
|||
* |
|||
* @private |
|||
* @param {jQuery} $facet Element of the facet |
|||
* @param {Backbone.Model} model |
|||
*/ |
|||
_renderPopover: function ($facet, model) { |
|||
var self = this; |
|||
|
|||
var $content = $(qweb.render('web_edit_user_filter.Popover', { |
|||
values: model.get('values'), |
|||
})); |
|||
// Cannot use Widget.events here because renderFacets is
|
|||
// triggered apart from renderElement
|
|||
$content.find('.list-group-item').click(function () { |
|||
self._removeValue(model, $(this).attr('data-value')); |
|||
}); |
|||
|
|||
$facet.popover({ |
|||
title: _t('Edit Facet'), |
|||
template: qweb.render('web_edit_user_filter.PopoverTemplate'), |
|||
content: $content, |
|||
container: this.$el, |
|||
html: true, |
|||
trigger: 'manual', |
|||
placement: 'bottom', |
|||
animation: false, |
|||
}); |
|||
}, |
|||
|
|||
/** |
|||
* Hides all popovers. |
|||
* |
|||
* @private |
|||
*/ |
|||
_hidePopovers: function () { |
|||
this.$el.find('.popover').popover('hide'); |
|||
}, |
|||
|
|||
/** |
|||
* @override |
|||
*/ |
|||
renderFacets: function () { |
|||
var self = this; |
|||
var res = this._super.apply(this, arguments); |
|||
|
|||
this.$el.find('.o-edit-user-filter-popover').remove(); |
|||
|
|||
_.each(this.input_subviews, function (input_subview) { |
|||
if (!input_subview.model || |
|||
input_subview.model.attributes.is_custom_filter) { |
|||
return; |
|||
} |
|||
|
|||
input_subview.$el.addClass('o-edit-user-filter-editable'); |
|||
self._renderPopover(input_subview.$el, input_subview.model); |
|||
|
|||
input_subview.$el.click(function () { |
|||
self._hidePopovers(); |
|||
input_subview.$el.popover('show'); |
|||
}); |
|||
}); |
|||
return res; |
|||
}, |
|||
|
|||
/** |
|||
* @override |
|||
*/ |
|||
start: function () { |
|||
var self = this; |
|||
var res = this._super.apply(this, arguments); |
|||
this._proxyHidePopovers = this.proxy('_hidePopovers'); |
|||
$(document).click(this._proxyHidePopovers); |
|||
return res; |
|||
}, |
|||
|
|||
/** |
|||
* @override |
|||
*/ |
|||
destroy: function () { |
|||
var res = this._super.apply(this, arguments); |
|||
$(document).unbind('click', this._proxyHidePopovers); |
|||
return res; |
|||
} |
|||
}); |
|||
}); |
@ -0,0 +1,27 @@ |
|||
/* Copyright 2019 Onestein |
|||
* License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */ |
|||
|
|||
.o_favorites_menu { |
|||
.o-edit-user-filter { |
|||
@include o-position-absolute(50%, 30px); |
|||
margin-top: -6px; |
|||
cursor: pointer; |
|||
} |
|||
} |
|||
|
|||
.o-edit-user-filter-popover { |
|||
a { |
|||
cursor: pointer; |
|||
padding: 0.3rem 0.75rem; |
|||
font-size: 1.2rem; |
|||
|
|||
.fa-close { |
|||
padding-left: 13px; |
|||
font-size: 1.2rem; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.o-edit-user-filter-editable { |
|||
cursor: pointer; |
|||
} |
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<!-- Copyright 2019 Onestein |
|||
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). --> |
|||
|
|||
<template> |
|||
<div t-name="web_edit_user_filter.Popover"> |
|||
|
|||
<div class="list-group"> |
|||
<t t-foreach="values" t-as="value"> |
|||
<a class="list-group-item list-group-item-action" |
|||
t-attf-data-value="#{typeof value.value === 'object' ? value.value.attrs.name : value.value}"> |
|||
<span> |
|||
<t t-esc="value.label"/> |
|||
</span> |
|||
<button class="btn btn-sm btn-link pull-right"> |
|||
<span class="fa fa-close" /> |
|||
</button> |
|||
</a> |
|||
</t> |
|||
</div> |
|||
</div> |
|||
|
|||
<t t-name="web_edit_user_filter.PopoverTemplate"> |
|||
<div class="popover o-edit-user-filter-popover" role="tooltip"> |
|||
<h3 class="popover-header"></h3> |
|||
<div class="arrow"></div> |
|||
<div class="popover-body"></div> |
|||
</div> |
|||
</t> |
|||
</template> |
@ -0,0 +1,16 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<!-- Copyright 2019 Onestein |
|||
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). --> |
|||
|
|||
<odoo> |
|||
<template id="assets_backend" inherit_id="web.assets_backend"> |
|||
<xpath expr="."> |
|||
<script src="/web_edit_user_filter/static/src/js/backend.js"></script> |
|||
</xpath> |
|||
<xpath expr="//link[last()]"> |
|||
<link href="/web_edit_user_filter/static/src/scss/backend.scss" |
|||
rel="stylesheet" |
|||
type="text/scss"/> |
|||
</xpath> |
|||
</template> |
|||
</odoo> |
@ -0,0 +1 @@ |
|||
from . import test_edit_user_filter |
@ -0,0 +1,32 @@ |
|||
# Copyright 2019 Onestein |
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). |
|||
|
|||
from odoo.tests.common import SingleTransactionCase, post_install |
|||
|
|||
|
|||
@post_install(True) |
|||
class TestEditUserFilter(SingleTransactionCase): |
|||
def test_filter_facet_inclusion(self): |
|||
self.env['ir.filters'].create({ |
|||
'name': 'any2', |
|||
'model_id': 'ir.filters', |
|||
'domain': '[]', |
|||
'facet': 'test2' |
|||
}) |
|||
new_filter = self.env['ir.filters'].create({ |
|||
'name': 'any', |
|||
'model_id': 'ir.filters', |
|||
'domain': '[]', |
|||
'facet': 'test' |
|||
}) |
|||
res = self.env['ir.filters'].get_filters('ir.filters') |
|||
self.assertTrue('facet' in res[0]) |
|||
self.assertEqual( |
|||
list( |
|||
filter( |
|||
lambda f: f['id'] == new_filter.id, |
|||
res |
|||
) |
|||
)[0]['facet'], |
|||
'test' |
|||
) |
Write
Preview
Loading…
Cancel
Save
Reference in new issue