mirror of
https://github.com/OCA/web.git
synced 2025-02-22 13:21:25 +02:00
120
web_view_searchpanel/README.rst
Normal file
120
web_view_searchpanel/README.rst
Normal file
@@ -0,0 +1,120 @@
|
||||
============
|
||||
Search Panel
|
||||
============
|
||||
|
||||
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
|
||||
:target: https://odoo-community.org/page/development-status
|
||||
:alt: Beta
|
||||
.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png
|
||||
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
|
||||
:alt: License: LGPL-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_view_searchpanel
|
||||
: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_view_searchpanel
|
||||
: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|
|
||||
|
||||
With Odoo version 13 a new feature is added which allows kanban views to be
|
||||
extended by a search panel. This can be defined via XML and is then automatically
|
||||
added to the view. With this module the function is ported back to version 12.
|
||||
|
||||
**Table of contents**
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
This tool allows to quickly filter data on the basis of given fields. The fields
|
||||
are specified as direct children of the ``searchpanel`` with tag name ``field``,
|
||||
and the following attributes:
|
||||
|
||||
* ``name`` (mandatory) the name of the field to filter on
|
||||
* ``select`` determines the behavior and display.
|
||||
* ``groups``: restricts to specific users
|
||||
* ``string``: determines the label to display
|
||||
* ``icon``: specifies which icon is used
|
||||
* ``color``: determines the icon color
|
||||
|
||||
Possible values for the ``select`` attribute are
|
||||
|
||||
* ``one`` (default) at most one value can be selected. Supported field types are many2one and selection.
|
||||
* ``multi`` several values can be selected (checkboxes). Supported field types are many2one, many2many and selection.
|
||||
|
||||
Additional optional attributes are available in the ``multi`` case:
|
||||
|
||||
* ``domain``: determines conditions that the comodel records have to satisfy.
|
||||
|
||||
A domain might be used to express a dependency on another field (with select="one")
|
||||
of the search panel. Consider
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<searchpanel>
|
||||
<field name="department_id"/>
|
||||
<field name="manager_id" select="multi" domain="[('department_id', '=', department_id)]"/>
|
||||
</searchpanel>
|
||||
|
||||
In the above example, the range of values for manager_id (manager names) available at screen
|
||||
will depend on the value currently selected for the field ``department_id``.
|
||||
|
||||
* ``groupby``: field name of the comodel (only available for many2one and many2many fields). Values will be grouped by that field.
|
||||
|
||||
* ``disable_counters``: default is false. If set to true the counters won't be computed.
|
||||
|
||||
This feature has been implemented in case performances would be too bad.
|
||||
|
||||
Another way to solve performance issues is to properly override the ``search_panel_select_multi_range`` method.
|
||||
|
||||
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_view_searchpanel%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
|
||||
~~~~~~~
|
||||
|
||||
* MuK IT
|
||||
|
||||
Contributors
|
||||
~~~~~~~~~~~~
|
||||
|
||||
* Mathias Markl <mathias.markl@mukit.at>
|
||||
* Enric Tobella <etobella@creublanca.es>
|
||||
|
||||
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_view_searchpanel>`_ project on GitHub.
|
||||
|
||||
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
|
||||
1
web_view_searchpanel/__init__.py
Normal file
1
web_view_searchpanel/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import models
|
||||
27
web_view_searchpanel/__manifest__.py
Normal file
27
web_view_searchpanel/__manifest__.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# Copyright 2017-2019 MuK IT GmbH.
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||
|
||||
{
|
||||
'name': 'Search Panel',
|
||||
'summary': 'Kanban Search Panel',
|
||||
'version': '12.0.1.0.0',
|
||||
'category': 'Extra Tools',
|
||||
'license': 'LGPL-3',
|
||||
'author': 'MuK IT, Odoo Community Association (OCA)',
|
||||
'website': 'https://github.com/OCA/web',
|
||||
'depends': [
|
||||
'web',
|
||||
],
|
||||
'data': [
|
||||
'template/assets.xml',
|
||||
],
|
||||
'qweb': [
|
||||
'static/src/xml/kanban.xml',
|
||||
],
|
||||
'images': [
|
||||
'static/description/banner.png'
|
||||
],
|
||||
'demo': [
|
||||
'demo/res_users_views.xml',
|
||||
]
|
||||
}
|
||||
27
web_view_searchpanel/demo/res_users_views.xml
Normal file
27
web_view_searchpanel/demo/res_users_views.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<!--
|
||||
|
||||
Copyright 2017-2019 MuK IT GmbH.
|
||||
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||
|
||||
-->
|
||||
|
||||
<odoo>
|
||||
|
||||
|
||||
<record id="view_res_users_kanban" model="ir.ui.view">
|
||||
<field name="name">res.users.kanban</field>
|
||||
<field name="model">res.users</field>
|
||||
<field name="inherit_id" ref="base.view_res_users_kanban"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//templates" position="after">
|
||||
<searchpanel>
|
||||
<field name="lang" select="multi"/>
|
||||
<field name="tz" select="multi"/>
|
||||
</searchpanel>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
1
web_view_searchpanel/models/__init__.py
Normal file
1
web_view_searchpanel/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import base
|
||||
198
web_view_searchpanel/models/base.py
Normal file
198
web_view_searchpanel/models/base.py
Normal file
@@ -0,0 +1,198 @@
|
||||
# Copyright 2017-2019 MuK IT GmbH.
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||
|
||||
import logging
|
||||
|
||||
from odoo import _, api, models
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.osv import expression
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Base(models.AbstractModel):
|
||||
|
||||
_inherit = 'base'
|
||||
|
||||
@api.model
|
||||
def search_panel_select_range(self, field_name, **kwargs):
|
||||
"""
|
||||
Return possible values of the field field_name (case select="one")
|
||||
and the parent field (if any) used to hierarchize them.
|
||||
|
||||
:param field_name: the name of a many2one category field
|
||||
:return: {
|
||||
'parent_field': parent field on the comodel of field, or False
|
||||
'values': array of dictionaries containing some info on the
|
||||
records available on the comodel of the field 'field_name'.
|
||||
The display name (and possibly parent_field) are fetched.
|
||||
}
|
||||
"""
|
||||
field = self._fields[field_name]
|
||||
supported_types = ['many2one']
|
||||
if field.type not in supported_types:
|
||||
raise UserError(_(
|
||||
'Only types %(supported_types)s are supported for category'
|
||||
'(found type %(field_type)s)'
|
||||
) % ({
|
||||
'supported_types': supported_types,
|
||||
'field_type': field.type
|
||||
}))
|
||||
|
||||
Comodel = self.env[field.comodel_name]
|
||||
fields = ['display_name']
|
||||
parent_name = (
|
||||
Comodel._parent_name if Comodel._parent_name
|
||||
in Comodel._fields else False
|
||||
)
|
||||
if parent_name:
|
||||
fields.append(parent_name)
|
||||
|
||||
model_domain = expression.AND([
|
||||
kwargs.get('search_domain', []),
|
||||
kwargs.get('category_domain', []),
|
||||
kwargs.get('filter_domain', []),
|
||||
])
|
||||
|
||||
return {
|
||||
'parent_field': parent_name,
|
||||
'values': Comodel.with_context(
|
||||
hierarchical_naming=False).search_read(model_domain, fields),
|
||||
}
|
||||
|
||||
@api.model
|
||||
def search_panel_select_multi_range(self, field_name, **kwargs):
|
||||
"""
|
||||
Return possible values of the field field_name (case select="multi"),
|
||||
possibly with counters and groups.
|
||||
|
||||
:param field_name: the name of a filter field;
|
||||
possible types are many2one, many2many, selection.
|
||||
:param search_domain: base domain of search
|
||||
:param category_domain: domain generated by categories
|
||||
:param filter_domain: domain generated by filters
|
||||
:param comodel_domain: domain of field values (if relational)
|
||||
:param group_by: extra field to read on comodel, to group comodel
|
||||
records
|
||||
:param disable_counters: whether to count records by value
|
||||
:return: a list of possible values, each being a dict with keys
|
||||
'id' (value),
|
||||
'name' (value label),
|
||||
'count' (how many records with that value),
|
||||
'group_id' (value of group),
|
||||
'group_name' (label of group).
|
||||
"""
|
||||
field = self._fields[field_name]
|
||||
supported_types = ['many2one', 'many2many', 'selection']
|
||||
if field.type not in supported_types:
|
||||
raise UserError(_(
|
||||
'Only types %(supported_types)s are supported for '
|
||||
'filter (found type %(field_type)s)'
|
||||
) % ({
|
||||
'supported_types': supported_types, 'field_type': field.type}))
|
||||
|
||||
Comodel = self.env.get(field.comodel_name)
|
||||
|
||||
model_domain = expression.AND([
|
||||
kwargs.get('search_domain', []),
|
||||
kwargs.get('category_domain', []),
|
||||
kwargs.get('filter_domain', []),
|
||||
[(field_name, '!=', False)],
|
||||
])
|
||||
comodel_domain = kwargs.get('comodel_domain', [])
|
||||
disable_counters = kwargs.get('disable_counters', False)
|
||||
|
||||
group_by = kwargs.get('group_by', False)
|
||||
if group_by:
|
||||
# determine the labeling of values returned by the group_by field
|
||||
group_by_field = Comodel._fields[group_by]
|
||||
|
||||
if group_by_field.type == 'many2one':
|
||||
def group_id_name(value):
|
||||
return value or (False, _("Not Set"))
|
||||
|
||||
elif group_by_field.type == 'selection':
|
||||
desc = Comodel.fields_get([group_by])[group_by]
|
||||
group_by_selection = dict(desc['selection'])
|
||||
group_by_selection[False] = _("Not Set")
|
||||
|
||||
def group_id_name(value):
|
||||
return value, group_by_selection[value]
|
||||
|
||||
else:
|
||||
def group_id_name(value):
|
||||
return (value, value) if value else (False, _("Not Set"))
|
||||
|
||||
# get filter_values
|
||||
filter_values = []
|
||||
|
||||
if field.type == 'many2one':
|
||||
counters = {}
|
||||
if not disable_counters:
|
||||
groups = self.read_group(
|
||||
model_domain, [field_name], [field_name])
|
||||
counters = {
|
||||
group[field_name][0]: group[field_name + '_count']
|
||||
for group in groups
|
||||
}
|
||||
# retrieve all possible values, and return them with their label
|
||||
# and counter
|
||||
field_names = ['display_name']
|
||||
if group_by:
|
||||
field_names.append(group_by)
|
||||
records = Comodel.search_read(comodel_domain, field_names)
|
||||
for record in records:
|
||||
record_id = record['id']
|
||||
values = {
|
||||
'id': record_id,
|
||||
'name': record['display_name'],
|
||||
'count': counters.get(record_id, 0),
|
||||
}
|
||||
if group_by:
|
||||
values['group_id'], values['group_name'] = group_id_name(
|
||||
record[group_by])
|
||||
filter_values.append(values)
|
||||
|
||||
elif field.type == 'many2many':
|
||||
# retrieve all possible values, and return them with their label
|
||||
# and counter
|
||||
field_names = ['display_name']
|
||||
if group_by:
|
||||
field_names.append(group_by)
|
||||
records = Comodel.search_read(comodel_domain, field_names)
|
||||
for record in records:
|
||||
record_id = record['id']
|
||||
values = {
|
||||
'id': record_id,
|
||||
'name': record['display_name'],
|
||||
'count': 0,
|
||||
}
|
||||
if not disable_counters:
|
||||
count_domain = expression.AND([
|
||||
model_domain, [(field_name, 'in', record_id)]])
|
||||
values['count'] = self.search_count(count_domain)
|
||||
if group_by:
|
||||
values['group_id'], values['group_name'] = group_id_name(
|
||||
record[group_by])
|
||||
filter_values.append(values)
|
||||
|
||||
elif field.type == 'selection':
|
||||
counters = {}
|
||||
if not disable_counters:
|
||||
groups = self.read_group(
|
||||
model_domain, [field_name], [field_name])
|
||||
counters = {
|
||||
group[field_name]: group[field_name + '_count']
|
||||
for group in groups
|
||||
}
|
||||
# retrieve all possible values, and return them with their label
|
||||
# and counter
|
||||
selection = self.fields_get([field_name])[field_name]['selection']
|
||||
for value, label in selection:
|
||||
filter_values.append({
|
||||
'id': value,
|
||||
'name': label,
|
||||
'count': counters.get(value, 0),
|
||||
})
|
||||
|
||||
return filter_values
|
||||
2
web_view_searchpanel/readme/CONTRIBUTORS.rst
Normal file
2
web_view_searchpanel/readme/CONTRIBUTORS.rst
Normal file
@@ -0,0 +1,2 @@
|
||||
* Mathias Markl <mathias.markl@mukit.at>
|
||||
* Enric Tobella <etobella@creublanca.es>
|
||||
3
web_view_searchpanel/readme/DESCRIPTION.rst
Normal file
3
web_view_searchpanel/readme/DESCRIPTION.rst
Normal file
@@ -0,0 +1,3 @@
|
||||
With Odoo version 13 a new feature is added which allows kanban views to be
|
||||
extended by a search panel. This can be defined via XML and is then automatically
|
||||
added to the view. With this module the function is ported back to version 12.
|
||||
40
web_view_searchpanel/readme/USAGE.rst
Normal file
40
web_view_searchpanel/readme/USAGE.rst
Normal file
@@ -0,0 +1,40 @@
|
||||
This tool allows to quickly filter data on the basis of given fields. The fields
|
||||
are specified as direct children of the ``searchpanel`` with tag name ``field``,
|
||||
and the following attributes:
|
||||
|
||||
* ``name`` (mandatory) the name of the field to filter on
|
||||
* ``select`` determines the behavior and display.
|
||||
* ``groups``: restricts to specific users
|
||||
* ``string``: determines the label to display
|
||||
* ``icon``: specifies which icon is used
|
||||
* ``color``: determines the icon color
|
||||
|
||||
Possible values for the ``select`` attribute are
|
||||
|
||||
* ``one`` (default) at most one value can be selected. Supported field types are many2one and selection.
|
||||
* ``multi`` several values can be selected (checkboxes). Supported field types are many2one, many2many and selection.
|
||||
|
||||
Additional optional attributes are available in the ``multi`` case:
|
||||
|
||||
* ``domain``: determines conditions that the comodel records have to satisfy.
|
||||
|
||||
A domain might be used to express a dependency on another field (with select="one")
|
||||
of the search panel. Consider
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<searchpanel>
|
||||
<field name="department_id"/>
|
||||
<field name="manager_id" select="multi" domain="[('department_id', '=', department_id)]"/>
|
||||
</searchpanel>
|
||||
|
||||
In the above example, the range of values for manager_id (manager names) available at screen
|
||||
will depend on the value currently selected for the field ``department_id``.
|
||||
|
||||
* ``groupby``: field name of the comodel (only available for many2one and many2many fields). Values will be grouped by that field.
|
||||
|
||||
* ``disable_counters``: default is false. If set to true the counters won't be computed.
|
||||
|
||||
This feature has been implemented in case performances would be too bad.
|
||||
|
||||
Another way to solve performance issues is to properly override the ``search_panel_select_multi_range`` method.
|
||||
BIN
web_view_searchpanel/static/description/icon.png
Normal file
BIN
web_view_searchpanel/static/description/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.2 KiB |
462
web_view_searchpanel/static/description/index.html
Normal file
462
web_view_searchpanel/static/description/index.html
Normal file
@@ -0,0 +1,462 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="generator" content="Docutils 0.15.1: http://docutils.sourceforge.net/" />
|
||||
<title>Search Panel</title>
|
||||
<style type="text/css">
|
||||
|
||||
/*
|
||||
:Author: David Goodger (goodger@python.org)
|
||||
:Id: $Id: html4css1.css 7952 2016-07-26 18:15:59Z milde $
|
||||
:Copyright: This stylesheet has been placed in the public domain.
|
||||
|
||||
Default cascading style sheet for the HTML output of Docutils.
|
||||
|
||||
See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to
|
||||
customize this style sheet.
|
||||
*/
|
||||
|
||||
/* used to remove borders from tables and images */
|
||||
.borderless, table.borderless td, table.borderless th {
|
||||
border: 0 }
|
||||
|
||||
table.borderless td, table.borderless th {
|
||||
/* Override padding for "table.docutils td" with "! important".
|
||||
The right padding separates the table cells. */
|
||||
padding: 0 0.5em 0 0 ! important }
|
||||
|
||||
.first {
|
||||
/* Override more specific margin styles with "! important". */
|
||||
margin-top: 0 ! important }
|
||||
|
||||
.last, .with-subtitle {
|
||||
margin-bottom: 0 ! important }
|
||||
|
||||
.hidden {
|
||||
display: none }
|
||||
|
||||
.subscript {
|
||||
vertical-align: sub;
|
||||
font-size: smaller }
|
||||
|
||||
.superscript {
|
||||
vertical-align: super;
|
||||
font-size: smaller }
|
||||
|
||||
a.toc-backref {
|
||||
text-decoration: none ;
|
||||
color: black }
|
||||
|
||||
blockquote.epigraph {
|
||||
margin: 2em 5em ; }
|
||||
|
||||
dl.docutils dd {
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Uncomment (and remove this text!) to get bold-faced definition list terms
|
||||
dl.docutils dt {
|
||||
font-weight: bold }
|
||||
*/
|
||||
|
||||
div.abstract {
|
||||
margin: 2em 5em }
|
||||
|
||||
div.abstract p.topic-title {
|
||||
font-weight: bold ;
|
||||
text-align: center }
|
||||
|
||||
div.admonition, div.attention, div.caution, div.danger, div.error,
|
||||
div.hint, div.important, div.note, div.tip, div.warning {
|
||||
margin: 2em ;
|
||||
border: medium outset ;
|
||||
padding: 1em }
|
||||
|
||||
div.admonition p.admonition-title, div.hint p.admonition-title,
|
||||
div.important p.admonition-title, div.note p.admonition-title,
|
||||
div.tip p.admonition-title {
|
||||
font-weight: bold ;
|
||||
font-family: sans-serif }
|
||||
|
||||
div.attention p.admonition-title, div.caution p.admonition-title,
|
||||
div.danger p.admonition-title, div.error p.admonition-title,
|
||||
div.warning p.admonition-title, .code .error {
|
||||
color: red ;
|
||||
font-weight: bold ;
|
||||
font-family: sans-serif }
|
||||
|
||||
/* Uncomment (and remove this text!) to get reduced vertical space in
|
||||
compound paragraphs.
|
||||
div.compound .compound-first, div.compound .compound-middle {
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
div.compound .compound-last, div.compound .compound-middle {
|
||||
margin-top: 0.5em }
|
||||
*/
|
||||
|
||||
div.dedication {
|
||||
margin: 2em 5em ;
|
||||
text-align: center ;
|
||||
font-style: italic }
|
||||
|
||||
div.dedication p.topic-title {
|
||||
font-weight: bold ;
|
||||
font-style: normal }
|
||||
|
||||
div.figure {
|
||||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
div.footer, div.header {
|
||||
clear: both;
|
||||
font-size: smaller }
|
||||
|
||||
div.line-block {
|
||||
display: block ;
|
||||
margin-top: 1em ;
|
||||
margin-bottom: 1em }
|
||||
|
||||
div.line-block div.line-block {
|
||||
margin-top: 0 ;
|
||||
margin-bottom: 0 ;
|
||||
margin-left: 1.5em }
|
||||
|
||||
div.sidebar {
|
||||
margin: 0 0 0.5em 1em ;
|
||||
border: medium outset ;
|
||||
padding: 1em ;
|
||||
background-color: #ffffee ;
|
||||
width: 40% ;
|
||||
float: right ;
|
||||
clear: right }
|
||||
|
||||
div.sidebar p.rubric {
|
||||
font-family: sans-serif ;
|
||||
font-size: medium }
|
||||
|
||||
div.system-messages {
|
||||
margin: 5em }
|
||||
|
||||
div.system-messages h1 {
|
||||
color: red }
|
||||
|
||||
div.system-message {
|
||||
border: medium outset ;
|
||||
padding: 1em }
|
||||
|
||||
div.system-message p.system-message-title {
|
||||
color: red ;
|
||||
font-weight: bold }
|
||||
|
||||
div.topic {
|
||||
margin: 2em }
|
||||
|
||||
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
|
||||
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
|
||||
margin-top: 0.4em }
|
||||
|
||||
h1.title {
|
||||
text-align: center }
|
||||
|
||||
h2.subtitle {
|
||||
text-align: center }
|
||||
|
||||
hr.docutils {
|
||||
width: 75% }
|
||||
|
||||
img.align-left, .figure.align-left, object.align-left, table.align-left {
|
||||
clear: left ;
|
||||
float: left ;
|
||||
margin-right: 1em }
|
||||
|
||||
img.align-right, .figure.align-right, object.align-right, table.align-right {
|
||||
clear: right ;
|
||||
float: right ;
|
||||
margin-left: 1em }
|
||||
|
||||
img.align-center, .figure.align-center, object.align-center {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
table.align-center {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left }
|
||||
|
||||
.align-center {
|
||||
clear: both ;
|
||||
text-align: center }
|
||||
|
||||
.align-right {
|
||||
text-align: right }
|
||||
|
||||
/* reset inner alignment in figures */
|
||||
div.align-right {
|
||||
text-align: inherit }
|
||||
|
||||
/* div.align-center * { */
|
||||
/* text-align: left } */
|
||||
|
||||
.align-top {
|
||||
vertical-align: top }
|
||||
|
||||
.align-middle {
|
||||
vertical-align: middle }
|
||||
|
||||
.align-bottom {
|
||||
vertical-align: bottom }
|
||||
|
||||
ol.simple, ul.simple {
|
||||
margin-bottom: 1em }
|
||||
|
||||
ol.arabic {
|
||||
list-style: decimal }
|
||||
|
||||
ol.loweralpha {
|
||||
list-style: lower-alpha }
|
||||
|
||||
ol.upperalpha {
|
||||
list-style: upper-alpha }
|
||||
|
||||
ol.lowerroman {
|
||||
list-style: lower-roman }
|
||||
|
||||
ol.upperroman {
|
||||
list-style: upper-roman }
|
||||
|
||||
p.attribution {
|
||||
text-align: right ;
|
||||
margin-left: 50% }
|
||||
|
||||
p.caption {
|
||||
font-style: italic }
|
||||
|
||||
p.credits {
|
||||
font-style: italic ;
|
||||
font-size: smaller }
|
||||
|
||||
p.label {
|
||||
white-space: nowrap }
|
||||
|
||||
p.rubric {
|
||||
font-weight: bold ;
|
||||
font-size: larger ;
|
||||
color: maroon ;
|
||||
text-align: center }
|
||||
|
||||
p.sidebar-title {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold ;
|
||||
font-size: larger }
|
||||
|
||||
p.sidebar-subtitle {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold }
|
||||
|
||||
p.topic-title {
|
||||
font-weight: bold }
|
||||
|
||||
pre.address {
|
||||
margin-bottom: 0 ;
|
||||
margin-top: 0 ;
|
||||
font: inherit }
|
||||
|
||||
pre.literal-block, pre.doctest-block, pre.math, pre.code {
|
||||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
pre.code .ln { color: grey; } /* line numbers */
|
||||
pre.code, code { background-color: #eeeeee }
|
||||
pre.code .comment, code .comment { color: #5C6576 }
|
||||
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
|
||||
pre.code .literal.string, code .literal.string { color: #0C5404 }
|
||||
pre.code .name.builtin, code .name.builtin { color: #352B84 }
|
||||
pre.code .deleted, code .deleted { background-color: #DEB0A1}
|
||||
pre.code .inserted, code .inserted { background-color: #A3D289}
|
||||
|
||||
span.classifier {
|
||||
font-family: sans-serif ;
|
||||
font-style: oblique }
|
||||
|
||||
span.classifier-delimiter {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold }
|
||||
|
||||
span.interpreted {
|
||||
font-family: sans-serif }
|
||||
|
||||
span.option {
|
||||
white-space: nowrap }
|
||||
|
||||
span.pre {
|
||||
white-space: pre }
|
||||
|
||||
span.problematic {
|
||||
color: red }
|
||||
|
||||
span.section-subtitle {
|
||||
/* font-size relative to parent (h1..h6 element) */
|
||||
font-size: 80% }
|
||||
|
||||
table.citation {
|
||||
border-left: solid 1px gray;
|
||||
margin-left: 1px }
|
||||
|
||||
table.docinfo {
|
||||
margin: 2em 4em }
|
||||
|
||||
table.docutils {
|
||||
margin-top: 0.5em ;
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
table.footnote {
|
||||
border-left: solid 1px black;
|
||||
margin-left: 1px }
|
||||
|
||||
table.docutils td, table.docutils th,
|
||||
table.docinfo td, table.docinfo th {
|
||||
padding-left: 0.5em ;
|
||||
padding-right: 0.5em ;
|
||||
vertical-align: top }
|
||||
|
||||
table.docutils th.field-name, table.docinfo th.docinfo-name {
|
||||
font-weight: bold ;
|
||||
text-align: left ;
|
||||
white-space: nowrap ;
|
||||
padding-left: 0 }
|
||||
|
||||
/* "booktabs" style (no vertical lines) */
|
||||
table.docutils.booktabs {
|
||||
border: 0px;
|
||||
border-top: 2px solid;
|
||||
border-bottom: 2px solid;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table.docutils.booktabs * {
|
||||
border: 0px;
|
||||
}
|
||||
table.docutils.booktabs th {
|
||||
border-bottom: thin solid;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
|
||||
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
|
||||
font-size: 100% }
|
||||
|
||||
ul.auto-toc {
|
||||
list-style-type: none }
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document" id="search-panel">
|
||||
<h1 class="title">Search Panel</h1>
|
||||
|
||||
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
|
||||
<p><a class="reference external" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external" href="http://www.gnu.org/licenses/lgpl-3.0-standalone.html"><img alt="License: LGPL-3" src="https://img.shields.io/badge/licence-LGPL--3-blue.png" /></a> <a class="reference external" href="https://github.com/OCA/web/tree/12.0/web_view_searchpanel"><img alt="OCA/web" src="https://img.shields.io/badge/github-OCA%2Fweb-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/web-12-0/web-12-0-web_view_searchpanel"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external" href="https://runbot.odoo-community.org/runbot/162/12.0"><img alt="Try me on Runbot" src="https://img.shields.io/badge/runbot-Try%20me-875A7B.png" /></a></p>
|
||||
<p>With Odoo version 13 a new feature is added which allows kanban views to be
|
||||
extended by a search panel. This can be defined via XML and is then automatically
|
||||
added to the view. With this module the function is ported back to version 12.</p>
|
||||
<p><strong>Table of contents</strong></p>
|
||||
<div class="contents local topic" id="contents">
|
||||
<ul class="simple">
|
||||
<li><a class="reference internal" href="#usage" id="id1">Usage</a></li>
|
||||
<li><a class="reference internal" href="#bug-tracker" id="id2">Bug Tracker</a></li>
|
||||
<li><a class="reference internal" href="#credits" id="id3">Credits</a><ul>
|
||||
<li><a class="reference internal" href="#authors" id="id4">Authors</a></li>
|
||||
<li><a class="reference internal" href="#contributors" id="id5">Contributors</a></li>
|
||||
<li><a class="reference internal" href="#maintainers" id="id6">Maintainers</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="usage">
|
||||
<h1><a class="toc-backref" href="#id1">Usage</a></h1>
|
||||
<p>This tool allows to quickly filter data on the basis of given fields. The fields
|
||||
are specified as direct children of the <tt class="docutils literal">searchpanel</tt> with tag name <tt class="docutils literal">field</tt>,
|
||||
and the following attributes:</p>
|
||||
<ul class="simple">
|
||||
<li><tt class="docutils literal">name</tt> (mandatory) the name of the field to filter on</li>
|
||||
<li><tt class="docutils literal">select</tt> determines the behavior and display.</li>
|
||||
<li><tt class="docutils literal">groups</tt>: restricts to specific users</li>
|
||||
<li><tt class="docutils literal">string</tt>: determines the label to display</li>
|
||||
<li><tt class="docutils literal">icon</tt>: specifies which icon is used</li>
|
||||
<li><tt class="docutils literal">color</tt>: determines the icon color</li>
|
||||
</ul>
|
||||
<p>Possible values for the <tt class="docutils literal">select</tt> attribute are</p>
|
||||
<ul class="simple">
|
||||
<li><tt class="docutils literal">one</tt> (default) at most one value can be selected. Supported field types are many2one and selection.</li>
|
||||
<li><tt class="docutils literal">multi</tt> several values can be selected (checkboxes). Supported field types are many2one, many2many and selection.</li>
|
||||
</ul>
|
||||
<p>Additional optional attributes are available in the <tt class="docutils literal">multi</tt> case:</p>
|
||||
<ul class="simple">
|
||||
<li><tt class="docutils literal">domain</tt>: determines conditions that the comodel records have to satisfy.</li>
|
||||
</ul>
|
||||
<p>A domain might be used to express a dependency on another field (with select=”one”)
|
||||
of the search panel. Consider</p>
|
||||
<pre class="code xml literal-block">
|
||||
<span class="nt"><searchpanel></span>
|
||||
<span class="nt"><field</span> <span class="na">name=</span><span class="s">"department_id"</span><span class="nt">/></span>
|
||||
<span class="nt"><field</span> <span class="na">name=</span><span class="s">"manager_id"</span> <span class="na">select=</span><span class="s">"multi"</span> <span class="na">domain=</span><span class="s">"[('department_id', '=', department_id)]"</span><span class="nt">/></span>
|
||||
<span class="nt"></searchpanel></span>
|
||||
</pre>
|
||||
<p>In the above example, the range of values for manager_id (manager names) available at screen
|
||||
will depend on the value currently selected for the field <tt class="docutils literal">department_id</tt>.</p>
|
||||
<ul class="simple">
|
||||
<li><tt class="docutils literal">groupby</tt>: field name of the comodel (only available for many2one and many2many fields). Values will be grouped by that field.</li>
|
||||
<li><tt class="docutils literal">disable_counters</tt>: default is false. If set to true the counters won’t be computed.</li>
|
||||
</ul>
|
||||
<p>This feature has been implemented in case performances would be too bad.</p>
|
||||
<p>Another way to solve performance issues is to properly override the <tt class="docutils literal">search_panel_select_multi_range</tt> method.</p>
|
||||
</div>
|
||||
<div class="section" id="bug-tracker">
|
||||
<h1><a class="toc-backref" href="#id2">Bug Tracker</a></h1>
|
||||
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/web/issues">GitHub Issues</a>.
|
||||
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
|
||||
<a class="reference external" href="https://github.com/OCA/web/issues/new?body=module:%20web_view_searchpanel%0Aversion:%2012.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
|
||||
<p>Do not contact contributors directly about support or help with technical issues.</p>
|
||||
</div>
|
||||
<div class="section" id="credits">
|
||||
<h1><a class="toc-backref" href="#id3">Credits</a></h1>
|
||||
<div class="section" id="authors">
|
||||
<h2><a class="toc-backref" href="#id4">Authors</a></h2>
|
||||
<ul class="simple">
|
||||
<li>MuK IT</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="contributors">
|
||||
<h2><a class="toc-backref" href="#id5">Contributors</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Mathias Markl <<a class="reference external" href="mailto:mathias.markl@mukit.at">mathias.markl@mukit.at</a>></li>
|
||||
<li>Enric Tobella <<a class="reference external" href="mailto:etobella@creublanca.es">etobella@creublanca.es</a>></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="maintainers">
|
||||
<h2><a class="toc-backref" href="#id6">Maintainers</a></h2>
|
||||
<p>This module is maintained by the OCA.</p>
|
||||
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
|
||||
<p>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.</p>
|
||||
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/web/tree/12.0/web_view_searchpanel">OCA/web</a> project on GitHub.</p>
|
||||
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
58
web_view_searchpanel/static/src/js/kanban_controller.js
Normal file
58
web_view_searchpanel/static/src/js/kanban_controller.js
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
*
|
||||
* Copyright 2017-2019 MuK IT GmbH.
|
||||
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||
*
|
||||
**/
|
||||
|
||||
odoo.define('web_view_searchpanel.KanbanController', function (require) {
|
||||
"use strict";
|
||||
var KanbanController = require('web.KanbanController');
|
||||
|
||||
KanbanController.include({
|
||||
custom_events: _.extend({}, KanbanController.prototype.custom_events, {
|
||||
search_panel_domain_updated: '_onSearchPanelDomainUpdated',
|
||||
}),
|
||||
init: function (parent, model, renderer, params) {
|
||||
this._super.apply(this, arguments);
|
||||
this._searchPanel = params.searchPanel;
|
||||
this.controlPanelDomain = params.controlPanelDomain || [];
|
||||
this.searchPanelDomain = this._searchPanel ?
|
||||
this._searchPanel.getDomain() : [];
|
||||
},
|
||||
start: function () {
|
||||
if (this._searchPanel) {
|
||||
this.$el.addClass('o_kanban_with_searchpanel');
|
||||
this.$el.prepend(this._searchPanel.$el);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
update: function (params) {
|
||||
if (!this._searchPanel) {
|
||||
return this._super.apply(this, arguments);
|
||||
}
|
||||
var self = this;
|
||||
if (params.domain) {
|
||||
this.controlPanelDomain = params.domain;
|
||||
}
|
||||
params.noRender = true;
|
||||
params.domain = this.controlPanelDomain.concat(
|
||||
this.searchPanelDomain);
|
||||
var superProm = this._super.apply(this, arguments);
|
||||
var searchPanelProm = this._updateSearchPanel();
|
||||
return $.when(superProm, searchPanelProm).then(function () {
|
||||
return self.renderer.render();
|
||||
});
|
||||
},
|
||||
_updateSearchPanel: function () {
|
||||
return this._searchPanel.update({
|
||||
searchDomain: this.controlPanelDomain,
|
||||
});
|
||||
},
|
||||
_onSearchPanelDomainUpdated: function (ev) {
|
||||
this.searchPanelDomain = ev.data.domain;
|
||||
this.reload({offset: 0});
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
19
web_view_searchpanel/static/src/js/kanban_renderer.js
Normal file
19
web_view_searchpanel/static/src/js/kanban_renderer.js
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
*
|
||||
* Copyright 2017-2019 MuK IT GmbH.
|
||||
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||
*
|
||||
**/
|
||||
|
||||
odoo.define('web_view_searchpanel.KanbanRenderer', function (require) {
|
||||
"use strict";
|
||||
|
||||
var KanbanRenderer = require('web.KanbanRenderer');
|
||||
|
||||
KanbanRenderer.include({
|
||||
render: function () {
|
||||
return this._render();
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
631
web_view_searchpanel/static/src/js/kanban_searchpanel.js
Normal file
631
web_view_searchpanel/static/src/js/kanban_searchpanel.js
Normal file
@@ -0,0 +1,631 @@
|
||||
/**********************************************************************************
|
||||
*
|
||||
* Copyright 2017-2019 MuK IT GmbH.
|
||||
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||
*
|
||||
**********************************************************************************/
|
||||
|
||||
odoo.define('web_view_searchpanel.SearchPanel', function (require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
var config = require('web.config');
|
||||
var Domain = require('web.Domain');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
var qweb = core.qweb;
|
||||
|
||||
var SearchPanel = Widget.extend({
|
||||
className: 'o_search_panel',
|
||||
events: {
|
||||
'click .o_search_panel_category_value header': '_onCategoryValueClicked',
|
||||
'click .o_search_panel_category_value .o_toggle_fold': '_onToggleFoldCategory',
|
||||
'click .o_search_panel_filter_group .o_toggle_fold': '_onToggleFoldFilterGroup',
|
||||
'change .o_search_panel_filter_value > div > input': '_onFilterValueChanged',
|
||||
'change .o_search_panel_filter_group > div > input': '_onFilterGroupChanged',
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @param {Object} params
|
||||
* @param {Object} [params.defaultCategoryValues={}] the category value to
|
||||
* activate by default, for each category
|
||||
* @param {Object} params.fields
|
||||
* @param {string} params.model
|
||||
* @param {Object} params.sections
|
||||
* @param {Array[]} params.searchDomain domain coming from controlPanel
|
||||
*/
|
||||
init: function (parent, params) {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
this.categories = _.pick(params.sections, function (section) {
|
||||
return section.type === 'category';
|
||||
});
|
||||
this.filters = _.pick(params.sections, function (section) {
|
||||
return section.type === 'filter';
|
||||
});
|
||||
|
||||
this.defaultCategoryValues = params.defaultCategoryValues || {};
|
||||
this.fields = params.fields;
|
||||
this.model = params.model;
|
||||
this.searchDomain = params.searchDomain;
|
||||
|
||||
this.loadProm = $.Deferred();
|
||||
this.loadPromLazy = true;
|
||||
},
|
||||
willStart: function () {
|
||||
var self = this;
|
||||
var loading = $.Deferred();
|
||||
var loadPromTimer = setTimeout(function () {
|
||||
if(loading.state() !== 'resolved') {
|
||||
loading.resolve();
|
||||
}
|
||||
}, this.loadPromMaxTime || 1000);
|
||||
this._fetchCategories().then(function () {
|
||||
self._fetchFilters().then(function () {
|
||||
if(loading.state() !== 'resolved') {
|
||||
clearTimeout(loadPromTimer);
|
||||
self.loadPromLazy = false;
|
||||
loading.resolve();
|
||||
}
|
||||
self.loadProm.resolve();
|
||||
});
|
||||
});
|
||||
return $.when(loading, this._super.apply(this, arguments));
|
||||
},
|
||||
start: function () {
|
||||
var self = this;
|
||||
if(this.loadProm.state() !== 'resolved') {
|
||||
this.$el.html($("<div/>", {
|
||||
'class': "o_search_panel_loading",
|
||||
'html': "<i class='fa fa-spinner fa-pulse' />"
|
||||
}));
|
||||
}
|
||||
this.loadProm.then(function() {
|
||||
self._render();
|
||||
});
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @returns {Array[]} the current searchPanel domain based on active
|
||||
* categories and checked filters
|
||||
*/
|
||||
getDomain: function () {
|
||||
return this._getCategoryDomain().concat(this._getFilterDomain());
|
||||
},
|
||||
/**
|
||||
* Reload the filters and re-render. Note that we only reload the filters if
|
||||
* the controlPanel domain or searchPanel domain has changed.
|
||||
*
|
||||
* @param {Object} params
|
||||
* @param {Array[]} params.searchDomain domain coming from controlPanel
|
||||
* @returns {$.Promise}
|
||||
*/
|
||||
update: function (params) {
|
||||
if(this.loadProm.state() === 'resolved') {
|
||||
var newSearchDomainStr = JSON.stringify(params.searchDomain);
|
||||
var currentSearchDomainStr = JSON.stringify(this.searchDomain);
|
||||
if (this.needReload || (currentSearchDomainStr !== newSearchDomainStr)) {
|
||||
this.needReload = false;
|
||||
this.searchDomain = params.searchDomain;
|
||||
this._fetchFilters().then(this._render.bind(this));
|
||||
} else {
|
||||
this._render();
|
||||
}
|
||||
}
|
||||
return $.when();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {string} categoryId
|
||||
* @param {Object[]} values
|
||||
*/
|
||||
_createCategoryTree: function (categoryId, values) {
|
||||
var category = this.categories[categoryId];
|
||||
var parentField = category.parentField;
|
||||
|
||||
category.values = {};
|
||||
values.forEach(function (value) {
|
||||
category.values[value.id] = _.extend({}, value, {
|
||||
childrenIds: [],
|
||||
folded: true,
|
||||
parentId: value[parentField] && value[parentField][0] || false,
|
||||
});
|
||||
});
|
||||
Object.keys(category.values).forEach(function (valueId) {
|
||||
var value = category.values[valueId];
|
||||
if (value.parentId && category.values[value.parentId]) {
|
||||
category.values[value.parentId].childrenIds.push(value.id);
|
||||
} else {
|
||||
value.parentId = false;
|
||||
value[parentField] = false;
|
||||
}
|
||||
});
|
||||
category.rootIds = Object.keys(category.values).filter(function (valueId) {
|
||||
var value = category.values[valueId];
|
||||
return value.parentId === false;
|
||||
});
|
||||
category.activeValueId = false;
|
||||
|
||||
if(!this.loadPromLazy) {
|
||||
// set active value
|
||||
var validValues = _.pluck(category.values, 'id').concat([false]);
|
||||
// set active value from context
|
||||
var value = this.defaultCategoryValues[category.fieldName];
|
||||
// if not set in context, or set to an unknown value, set active value
|
||||
// from localStorage
|
||||
if (!_.contains(validValues, value)) {
|
||||
var storageKey = this._getLocalStorageKey(category);
|
||||
value = this.call('local_storage', 'getItem', storageKey);
|
||||
}
|
||||
|
||||
// if not set in localStorage either, select 'All'
|
||||
category.activeValueId = _.contains(validValues, value) ? value : false;
|
||||
|
||||
// unfold ancestor values of active value to make it is visible
|
||||
if (category.activeValueId) {
|
||||
var parentValueIds = this._getAncestorValueIds(category, category.activeValueId);
|
||||
parentValueIds.forEach(function (parentValue) {
|
||||
category.values[parentValue].folded = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {string} filterId
|
||||
* @param {Object[]} values
|
||||
*/
|
||||
_createFilterTree: function (filterId, values) {
|
||||
var filter = this.filters[filterId];
|
||||
|
||||
// restore checked property
|
||||
values.forEach(function (value) {
|
||||
var oldValue = filter.values && filter.values[value.id];
|
||||
value.checked = oldValue && oldValue.checked || false;
|
||||
});
|
||||
|
||||
filter.values = {};
|
||||
var groupIds = [];
|
||||
if (filter.groupBy) {
|
||||
var groups = {};
|
||||
values.forEach(function (value) {
|
||||
var groupId = value.group_id;
|
||||
if (!groups[groupId]) {
|
||||
if (groupId) {
|
||||
groupIds.push(groupId);
|
||||
}
|
||||
groups[groupId] = {
|
||||
folded: false,
|
||||
id: groupId,
|
||||
name: value.group_name,
|
||||
values: {},
|
||||
tooltip: value.group_tooltip,
|
||||
sequence: value.group_sequence,
|
||||
sortedValueIds: [],
|
||||
};
|
||||
// restore former checked and folded state
|
||||
var oldGroup = filter.groups && filter.groups[groupId];
|
||||
groups[groupId].state = oldGroup && oldGroup.state || false;
|
||||
groups[groupId].folded = oldGroup && oldGroup.folded || false;
|
||||
}
|
||||
groups[groupId].values[value.id] = value;
|
||||
groups[groupId].sortedValueIds.push(value.id);
|
||||
});
|
||||
filter.groups = groups;
|
||||
filter.sortedGroupIds = _.sortBy(groupIds, function (groupId) {
|
||||
return groups[groupId].sequence || groups[groupId].name;
|
||||
});
|
||||
Object.keys(filter.groups).forEach(function (groupId) {
|
||||
filter.values = _.extend(filter.values, filter.groups[groupId].values);
|
||||
});
|
||||
} else {
|
||||
values.forEach(function (value) {
|
||||
filter.values[value.id] = value;
|
||||
});
|
||||
filter.sortedValueIds = values.map(function (value) {
|
||||
return value.id;
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Fetch values for each category. This is done only once, at startup.
|
||||
*
|
||||
* @private
|
||||
* @returns {$.Promise} resolved when all categories have been fetched
|
||||
*/
|
||||
_fetchCategories: function () {
|
||||
var self = this;
|
||||
var defs = Object.keys(this.categories).map(function (categoryId) {
|
||||
var category = self.categories[categoryId];
|
||||
var field = self.fields[category.fieldName];
|
||||
var def;
|
||||
if (field.type === 'selection') {
|
||||
var values = field.selection.map(function (value) {
|
||||
return {id: value[0], display_name: value[1]};
|
||||
});
|
||||
def = $.when(values);
|
||||
} else {
|
||||
var categoryDomain = self._getCategoryDomain();
|
||||
var filterDomain = self._getFilterDomain();
|
||||
def = self._rpc({
|
||||
method: 'search_panel_select_range',
|
||||
model: self.model,
|
||||
args: [category.fieldName],
|
||||
kwargs: {
|
||||
category_domain: categoryDomain,
|
||||
filter_domain: filterDomain,
|
||||
search_domain: self.searchDomain,
|
||||
},
|
||||
}, {
|
||||
shadow: true,
|
||||
}).then(function (result) {
|
||||
category.parentField = result.parent_field;
|
||||
return result.values;
|
||||
});
|
||||
}
|
||||
return def.then(function (values) {
|
||||
self._createCategoryTree(categoryId, values);
|
||||
});
|
||||
});
|
||||
return $.when.apply($, defs);
|
||||
},
|
||||
/**
|
||||
* Fetch values for each filter. This is done at startup, and at each reload
|
||||
* (when the controlPanel or searchPanel domain changes).
|
||||
*
|
||||
* @private
|
||||
* @returns {$.Promise} resolved when all filters have been fetched
|
||||
*/
|
||||
_fetchFilters: function () {
|
||||
var self = this;
|
||||
var evalContext = {};
|
||||
Object.keys(this.categories).forEach(function (categoryId) {
|
||||
var category = self.categories[categoryId];
|
||||
evalContext[category.fieldName] = category.activeValueId;
|
||||
});
|
||||
var categoryDomain = this._getCategoryDomain();
|
||||
var filterDomain = this._getFilterDomain();
|
||||
var defs = Object.keys(this.filters).map(function (filterId) {
|
||||
var filter = self.filters[filterId];
|
||||
return self._rpc({
|
||||
method: 'search_panel_select_multi_range',
|
||||
model: self.model,
|
||||
args: [filter.fieldName],
|
||||
kwargs: {
|
||||
category_domain: categoryDomain,
|
||||
comodel_domain: Domain.prototype.stringToArray(filter.domain, evalContext),
|
||||
disable_counters: filter.disableCounters,
|
||||
filter_domain: filterDomain,
|
||||
group_by: filter.groupBy || false,
|
||||
search_domain: self.searchDomain,
|
||||
},
|
||||
}, {
|
||||
shadow: true,
|
||||
}).then(function (values) {
|
||||
self._createFilterTree(filterId, values);
|
||||
});
|
||||
});
|
||||
return $.when.apply($, defs);
|
||||
},
|
||||
/**
|
||||
* Compute and return the domain based on the current active categories.
|
||||
*
|
||||
* @private
|
||||
* @returns {Array[]}
|
||||
*/
|
||||
_getCategoryDomain: function () {
|
||||
var self = this;
|
||||
function categoryToDomain(domain, categoryId) {
|
||||
var category = self.categories[categoryId];
|
||||
if (category.activeValueId) {
|
||||
domain.push([category.fieldName, '=', category.activeValueId]);
|
||||
} else if(self.loadPromLazy && self.loadProm.state() !== 'resolved') {
|
||||
var value = self.defaultCategoryValues[category.fieldName];
|
||||
if (value) {
|
||||
domain.push([category.fieldName, '=', value]);
|
||||
}
|
||||
}
|
||||
return domain;
|
||||
}
|
||||
return Object.keys(this.categories).reduce(categoryToDomain, []);
|
||||
},
|
||||
/**
|
||||
* Compute and return the domain based on the current checked filters.
|
||||
* The values of a single filter are combined using a simple rule: checked values within
|
||||
* a same group are combined with an 'OR' (this is expressed as single condition using a list)
|
||||
* and groups are combined with an 'AND' (expressed by concatenation of conditions).
|
||||
* If a filter has no groups, its checked values are implicitely considered as forming
|
||||
* a group (and grouped using an 'OR').
|
||||
*
|
||||
* @private
|
||||
* @returns {Array[]}
|
||||
*/
|
||||
_getFilterDomain: function () {
|
||||
var self = this;
|
||||
function getCheckedValueIds(values) {
|
||||
return Object.keys(values).reduce(function (checkedValues, valueId) {
|
||||
if (values[valueId].checked) {
|
||||
checkedValues.push(values[valueId].id);
|
||||
}
|
||||
return checkedValues;
|
||||
}, []);
|
||||
}
|
||||
function filterToDomain(domain, filterId) {
|
||||
var filter = self.filters[filterId];
|
||||
if (filter.groups) {
|
||||
Object.keys(filter.groups).forEach(function (groupId) {
|
||||
var group = filter.groups[groupId];
|
||||
var checkedValues = getCheckedValueIds(group.values);
|
||||
if (checkedValues.length) {
|
||||
domain.push([filter.fieldName, 'in', checkedValues]);
|
||||
}
|
||||
});
|
||||
} else if (filter.values) {
|
||||
var checkedValues = getCheckedValueIds(filter.values);
|
||||
if (checkedValues.length) {
|
||||
domain.push([filter.fieldName, 'in', checkedValues]);
|
||||
}
|
||||
}
|
||||
return domain;
|
||||
}
|
||||
return Object.keys(this.filters).reduce(filterToDomain, []);
|
||||
},
|
||||
/**
|
||||
* The active id of each category is stored in the localStorage, s.t. it
|
||||
* can be restored afterwards (when the action is reloaded, for instance).
|
||||
* This function returns the key in the sessionStorage for a given category.
|
||||
*
|
||||
* @param {Object} category
|
||||
* @returns {string}
|
||||
*/
|
||||
_getLocalStorageKey: function (category) {
|
||||
return 'searchpanel_' + this.model + '_' + category.fieldName;
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Object} category
|
||||
* @param {integer} categoryValueId
|
||||
* @returns {integer[]} list of ids of the ancestors of the given value in
|
||||
* the given category
|
||||
*/
|
||||
_getAncestorValueIds: function (category, categoryValueId) {
|
||||
var categoryValue = category.values[categoryValueId];
|
||||
var parentId = categoryValue.parentId;
|
||||
if (parentId) {
|
||||
return [parentId].concat(this._getAncestorValueIds(category, parentId));
|
||||
}
|
||||
return [];
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_render: function () {
|
||||
var self = this;
|
||||
this.$el.empty();
|
||||
|
||||
// sort categories and filters according to their index
|
||||
var categories = Object.keys(this.categories).map(function (categoryId) {
|
||||
return self.categories[categoryId];
|
||||
});
|
||||
var filters = Object.keys(this.filters).map(function (filterId) {
|
||||
return self.filters[filterId];
|
||||
});
|
||||
var sections = categories.concat(filters).sort(function (s1, s2) {
|
||||
return s1.index - s2.index;
|
||||
});
|
||||
|
||||
sections.forEach(function (section) {
|
||||
if (Object.keys(section.values).length) {
|
||||
if (section.type === 'category') {
|
||||
self.$el.append(self._renderCategory(section));
|
||||
} else {
|
||||
self.$el.append(self._renderFilter(section));
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Object} category
|
||||
* @returns {string}
|
||||
*/
|
||||
_renderCategory: function (category) {
|
||||
return qweb.render('SearchPanel.Category', {category: category});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Object} filter
|
||||
* @returns {jQuery}
|
||||
*/
|
||||
_renderFilter: function (filter) {
|
||||
var $filter = $(qweb.render('SearchPanel.Filter', {filter: filter}));
|
||||
|
||||
// set group inputs in indeterminate state when necessary
|
||||
Object.keys(filter.groups || {}).forEach(function (groupId) {
|
||||
var state = filter.groups[groupId].state;
|
||||
// group 'false' is not displayed
|
||||
if (groupId !== 'false' && state === 'indeterminate') {
|
||||
$filter
|
||||
.find('.o_search_panel_filter_group[data-group-id=' + groupId + '] input')
|
||||
.get(0)
|
||||
.indeterminate = true;
|
||||
}
|
||||
});
|
||||
|
||||
return $filter;
|
||||
},
|
||||
/**
|
||||
* Compute the current searchPanel domain based on categories and filters,
|
||||
* and notify environment of the domain change.
|
||||
*
|
||||
* Note that this assumes that the environment will update the searchPanel.
|
||||
* This is done as such to ensure the coordination between the reloading of
|
||||
* the searchPanel and the reloading of the data.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_notifyDomainUpdated: function () {
|
||||
this.needReload = true;
|
||||
this.trigger_up('search_panel_domain_updated', {
|
||||
domain: this.getDomain(),
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onCategoryValueClicked: function (ev) {
|
||||
ev.stopPropagation();
|
||||
var $item = $(ev.currentTarget).closest('.o_search_panel_category_value');
|
||||
var category = this.categories[$item.data('categoryId')];
|
||||
var valueId = $item.data('id') || false;
|
||||
category.activeValueId = valueId;
|
||||
var storageKey = this._getLocalStorageKey(category);
|
||||
this.call('local_storage', 'setItem', storageKey, valueId);
|
||||
this._notifyDomainUpdated();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onFilterGroupChanged: function (ev) {
|
||||
ev.stopPropagation();
|
||||
var $item = $(ev.target).closest('.o_search_panel_filter_group');
|
||||
var filter = this.filters[$item.data('filterId')];
|
||||
var groupId = $item.data('groupId');
|
||||
var group = filter.groups[groupId];
|
||||
group.state = group.state === 'checked' ? 'unchecked' : 'checked';
|
||||
Object.keys(group.values).forEach(function (valueId) {
|
||||
group.values[valueId].checked = group.state === 'checked';
|
||||
});
|
||||
this._notifyDomainUpdated();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onFilterValueChanged: function (ev) {
|
||||
ev.stopPropagation();
|
||||
var $item = $(ev.target).closest('.o_search_panel_filter_value');
|
||||
var valueId = $item.data('valueId');
|
||||
var filter = this.filters[$item.data('filterId')];
|
||||
var value = filter.values[valueId];
|
||||
value.checked = !value.checked;
|
||||
var group = filter.groups && filter.groups[value.group_id];
|
||||
if (group) {
|
||||
var valuePartition = _.partition(Object.keys(group.values), function (valueId) {
|
||||
return group.values[valueId].checked;
|
||||
});
|
||||
if (valuePartition[0].length && valuePartition[1].length) {
|
||||
group.state = 'indeterminate';
|
||||
} else if (valuePartition[0].length) {
|
||||
group.state = 'checked';
|
||||
} else {
|
||||
group.state = 'unchecked';
|
||||
}
|
||||
}
|
||||
this._notifyDomainUpdated();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onToggleFoldCategory: function (ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
var $item = $(ev.currentTarget).closest('.o_search_panel_category_value');
|
||||
var category = this.categories[$item.data('categoryId')];
|
||||
var valueId = $item.data('id');
|
||||
category.values[valueId].folded = !category.values[valueId].folded;
|
||||
this._render();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onToggleFoldFilterGroup: function (ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
var $item = $(ev.currentTarget).closest('.o_search_panel_filter_group');
|
||||
var filter = this.filters[$item.data('filterId')];
|
||||
var groupId = $item.data('groupId');
|
||||
filter.groups[groupId].folded = !filter.groups[groupId].folded;
|
||||
this._render();
|
||||
},
|
||||
});
|
||||
|
||||
if (config.device.isMobile) {
|
||||
SearchPanel.include({
|
||||
tagName: 'details',
|
||||
|
||||
_getCategorySelection: function () {
|
||||
var self = this;
|
||||
return Object.keys(this.categories).reduce(function (selection, categoryId) {
|
||||
var category = self.categories[categoryId];
|
||||
if (category.activeValueId) {
|
||||
var ancestorIds = [category.activeValueId].concat(self._getAncestorValueIds(category, category.activeValueId));
|
||||
var breadcrumb = ancestorIds.map(function (valueId) {
|
||||
return category.values[valueId].display_name;
|
||||
});
|
||||
selection.push({ breadcrumb: breadcrumb, icon: category.icon, color: category.color});
|
||||
}
|
||||
return selection;
|
||||
}, []);
|
||||
},
|
||||
|
||||
_getFilterSelection: function () {
|
||||
var self = this;
|
||||
return Object.keys(this.filters).reduce(function (selection, filterId) {
|
||||
var filter = self.filters[filterId];
|
||||
if (filter.groups) {
|
||||
Object.keys(filter.groups).forEach(function (groupId) {
|
||||
var group = filter.groups[groupId];
|
||||
Object.keys(group.values).forEach(function (valueId) {
|
||||
var value = group.values[valueId];
|
||||
if (value.checked) {
|
||||
selection.push({name: value.name, icon: filter.icon, color: filter.color});
|
||||
}
|
||||
});
|
||||
});
|
||||
} else if (filter.values) {
|
||||
Object.keys(filter.values).forEach(function (valueId) {
|
||||
var value = filter.values[valueId];
|
||||
if (value.checked) {
|
||||
selection.push({name: value.name, icon: filter.icon, color: filter.color});
|
||||
}
|
||||
});
|
||||
}
|
||||
return selection;
|
||||
}, []);
|
||||
},
|
||||
|
||||
_render: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.$el.prepend(qweb.render('SearchPanel.MobileSummary', {
|
||||
categories: this._getCategorySelection(),
|
||||
filterValues: this._getFilterSelection(),
|
||||
separator: ' / ',
|
||||
}));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return SearchPanel;
|
||||
|
||||
});
|
||||
118
web_view_searchpanel/static/src/js/kanban_view.js
Normal file
118
web_view_searchpanel/static/src/js/kanban_view.js
Normal file
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
*
|
||||
* Copyright 2017-2019 MuK IT GmbH.
|
||||
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||
*
|
||||
**/
|
||||
|
||||
odoo.define('web_view_searchpanel.KanbanView', function (require) {
|
||||
"use strict";
|
||||
|
||||
var pyUtils = require('web.py_utils');
|
||||
|
||||
var KanbanView = require('web.KanbanView');
|
||||
var SearchPanel = require('web.SearchPanel');
|
||||
|
||||
|
||||
KanbanView.include({
|
||||
config: _.extend({}, KanbanView.prototype.config, {
|
||||
SearchPanel: SearchPanel,
|
||||
}),
|
||||
init: function (viewInfo, params) {
|
||||
this.searchPanelSections = Object.create(null);
|
||||
this._super.apply(this, arguments);
|
||||
this.hasSearchPanel = !_.isEmpty(this.searchPanelSections);
|
||||
|
||||
},
|
||||
getController: function (parent) {
|
||||
var self = this;
|
||||
var def = undefined;
|
||||
if (this.hasSearchPanel) {
|
||||
def = this._createSearchPanel(parent);
|
||||
}
|
||||
var _super = this._super.bind(this);
|
||||
return $.when(def).then(function (searchPanel) {
|
||||
return _super(parent).done(function (controller) {
|
||||
if (self.hasSearchPanel) {
|
||||
self.controllerParams.searchPanel.setParent(
|
||||
controller);
|
||||
}
|
||||
return controller
|
||||
});
|
||||
});
|
||||
},
|
||||
_createSearchPanel: function (parent) {
|
||||
var self = this;
|
||||
var defaultCategoryValues = {};
|
||||
Object.keys(this.loadParams.context).forEach(function (key) {
|
||||
var match = /^searchpanel_default_(.*)$/.exec(key);
|
||||
if (match) {
|
||||
defaultCategoryValues[
|
||||
match[1]
|
||||
] = self.loadParams.context[key];
|
||||
}
|
||||
});
|
||||
var controlPanelDomain = this.loadParams.domain;
|
||||
var searchPanel = new this.config.SearchPanel(parent, {
|
||||
defaultCategoryValues: defaultCategoryValues,
|
||||
fields: this.fields,
|
||||
model: this.loadParams.modelName,
|
||||
searchDomain: controlPanelDomain,
|
||||
sections: this.searchPanelSections,
|
||||
});
|
||||
this.controllerParams.searchPanel = searchPanel;
|
||||
this.controllerParams.controlPanelDomain = controlPanelDomain;
|
||||
return searchPanel.appendTo(
|
||||
document.createDocumentFragment()
|
||||
).then(function () {
|
||||
var searchPanelDomain = searchPanel.getDomain();
|
||||
self.loadParams.domain = controlPanelDomain.concat(searchPanelDomain);
|
||||
});
|
||||
},
|
||||
_processNode: function (node, fv) {
|
||||
if (node.tag === 'searchpanel') {
|
||||
this._processSearchPanelNode(node, fv);
|
||||
return false;
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
_processSearchPanelNode: function (node, fv) {
|
||||
var self = this;
|
||||
node.children.forEach(function (childNode, index) {
|
||||
if (childNode.tag !== 'field') {
|
||||
return;
|
||||
}
|
||||
if (childNode.attrs.invisible === "1") {
|
||||
return;
|
||||
}
|
||||
var fieldName = childNode.attrs.name;
|
||||
var type = childNode.attrs.select === 'multi' ?
|
||||
'filter' : 'category';
|
||||
|
||||
var sectionId = _.uniqueId('section_');
|
||||
var section = {
|
||||
color: childNode.attrs.color,
|
||||
description: childNode.attrs.string || fv.fields[
|
||||
fieldName].string,
|
||||
fieldName: fieldName,
|
||||
icon: childNode.attrs.icon,
|
||||
id: sectionId,
|
||||
index: index,
|
||||
type: type,
|
||||
};
|
||||
if (section.type === 'category') {
|
||||
section.icon = section.icon || 'fa-folder';
|
||||
} else if (section.type === 'filter') {
|
||||
section.disableCounters = !!pyUtils.py_eval(
|
||||
childNode.attrs.disable_counters || '0');
|
||||
section.domain = childNode.attrs.domain || '[]';
|
||||
section.groupBy = childNode.attrs.groupby;
|
||||
section.icon = section.icon || 'fa-filter';
|
||||
}
|
||||
self.searchPanelSections[sectionId] = section;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
158
web_view_searchpanel/static/src/scss/kanban_view.scss
Normal file
158
web_view_searchpanel/static/src/scss/kanban_view.scss
Normal file
@@ -0,0 +1,158 @@
|
||||
/**********************************************************************************
|
||||
*
|
||||
* Copyright 2017-2019 MuK IT GmbH.
|
||||
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||
*
|
||||
**********************************************************************************/
|
||||
|
||||
.o_kanban_with_searchpanel {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
align-items: flex-start;
|
||||
|
||||
.o_onboarding_container {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
.o_kanban_view {
|
||||
flex: 1 1 calc(100% - #{$o-searchpanel-w});
|
||||
overflow: auto;
|
||||
max-height: 100%;
|
||||
}
|
||||
.o_search_panel {
|
||||
flex: 0 0 $o-searchpanel-w;
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
padding: $o-searchpanel-p-small $o-searchpanel-p-small $o-searchpanel-p*2 $o-searchpanel-p;
|
||||
border-right: 1px solid $gray-300;
|
||||
background-color: white;
|
||||
position: relative;
|
||||
|
||||
.o_search_panel_loading {
|
||||
position: absolute;
|
||||
margin-top: -30px;
|
||||
margin-left: -30px;
|
||||
margin-bottom: 0;
|
||||
margin-right: 0;
|
||||
font-size: 60px;
|
||||
height: 60px;
|
||||
width: 60px;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
.o_search_panel_category .o_search_panel_section_icon {
|
||||
color: $o-searchpanel-category-default-color;
|
||||
}
|
||||
.o_search_panel_filter .o_search_panel_section_icon {
|
||||
color: $o-searchpanel-filter-default-color;
|
||||
}
|
||||
|
||||
.o_search_panel_label {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
.o_toggle_fold {
|
||||
padding: 3px;
|
||||
}
|
||||
}
|
||||
.o_search_panel_section_header {
|
||||
padding: $o-searchpanel-p-small 0;
|
||||
}
|
||||
.list-group-item {
|
||||
padding: 0 0 $o-searchpanel-p-small 0;
|
||||
|
||||
.list-group-item {
|
||||
padding: 0 0 0 $custom-control-gutter;
|
||||
margin-bottom: $o-searchpanel-p-tiny*0.5;
|
||||
&:first-child {
|
||||
margin-top: $o-searchpanel-p-tiny*0.5;
|
||||
}
|
||||
}
|
||||
span.o_search_panel_label_title {
|
||||
color: $headings-color;
|
||||
@include o-text-overflow(inline-block, calc(100% - 22px));
|
||||
}
|
||||
header.active {
|
||||
background-color: $list-group-action-active-bg;
|
||||
}
|
||||
}
|
||||
.o_search_panel_category_value {
|
||||
header {
|
||||
margin-left: -$o-searchpanel-p-tiny;
|
||||
padding-left: $o-searchpanel-p-tiny;
|
||||
}
|
||||
.o_search_panel_category_value {
|
||||
position: relative;
|
||||
padding-left: $o-searchpanel-p;
|
||||
padding-bottom: $o-searchpanel-p-tiny;
|
||||
margin-bottom: 0;
|
||||
|
||||
&:before, &:after {
|
||||
@include o-position-absolute(0, $left: $o-searchpanel-p-tiny)
|
||||
@include size(1px, 100%);
|
||||
background: $gray-500;
|
||||
content: '';
|
||||
}
|
||||
&:after {
|
||||
top: 10px;
|
||||
@include size(8px, 1px);
|
||||
}
|
||||
&:last-child {
|
||||
&:before {
|
||||
height: 11px;
|
||||
}
|
||||
&:after {
|
||||
top: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
.o_kanban_with_searchpanel {
|
||||
flex-direction: column;
|
||||
|
||||
.o_onboarding_container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
details.o_search_panel {
|
||||
flex-basis: auto;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
|
||||
> summary {
|
||||
padding: $o-searchpanel-p-small;
|
||||
// Hide the caret. For details see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/summary
|
||||
list-style-type: none;
|
||||
&::-webkit-details-marker {
|
||||
display: none
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 100%;
|
||||
background-color: white;
|
||||
}
|
||||
}
|
||||
|
||||
> .o_search_panel_section {
|
||||
margin: 0 $o-searchpanel-p-small 0 $o-searchpanel-p;
|
||||
}
|
||||
|
||||
&[open] {
|
||||
z-index: $zindex-dropdown;
|
||||
|
||||
> summary {
|
||||
background-color: $list-group-action-active-bg;
|
||||
}
|
||||
|
||||
.fa-chevron-left:before {
|
||||
content: "\f078";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
web_view_searchpanel/static/src/scss/variables.scss
Normal file
15
web_view_searchpanel/static/src/scss/variables.scss
Normal file
@@ -0,0 +1,15 @@
|
||||
/**********************************************************************************
|
||||
*
|
||||
* Copyright 2017-2019 MuK IT GmbH.
|
||||
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||
*
|
||||
**********************************************************************************/
|
||||
|
||||
$o-searchpanel-w: 220px;
|
||||
|
||||
$o-searchpanel-p: $o-horizontal-padding;
|
||||
$o-searchpanel-p-small: $o-horizontal-padding*0.5;
|
||||
$o-searchpanel-p-tiny: $o-searchpanel-p-small*0.5;
|
||||
|
||||
$o-searchpanel-category-default-color: $o-brand-primary;
|
||||
$o-searchpanel-filter-default-color: $o-brand-odoo;
|
||||
132
web_view_searchpanel/static/src/xml/kanban.xml
Normal file
132
web_view_searchpanel/static/src/xml/kanban.xml
Normal file
@@ -0,0 +1,132 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<!--
|
||||
Copyright 2017-2019 MuK IT GmbH.
|
||||
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||
-->
|
||||
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="SearchPanel.SectionHeader">
|
||||
<header class="o_search_panel_section_header text-uppercase">
|
||||
<i t-attf-class="mr-1 fa #{section.icon} o_search_panel_section_icon" t-attf-style="#{section.color ? ('color: ' + section.color) : ''}"/>
|
||||
<b><t t-esc="section.description"/></b>
|
||||
</header>
|
||||
</t>
|
||||
|
||||
<t t-name="SearchPanel.Category">
|
||||
<section class="o_search_panel_section o_search_panel_category">
|
||||
<t t-call="SearchPanel.SectionHeader">
|
||||
<t t-set="section" t-value="category"/>
|
||||
</t>
|
||||
<ul class="list-group d-block">
|
||||
<li class="o_search_panel_category_value border-0 list-group-item" t-att-data-category-id="category.id">
|
||||
<header t-att-class="'list-group-item-action' + (!category.activeValueId ? ' active' : '')">
|
||||
<label class="o_search_panel_label mb0 d-block">
|
||||
<span class="o_search_panel_label_title"><b>All</b></span>
|
||||
</label>
|
||||
</header>
|
||||
</li>
|
||||
<t t-call="SearchPanel.CategoryValues">
|
||||
<t t-set="values" t-value="category.rootIds"/>
|
||||
</t>
|
||||
</ul>
|
||||
</section>
|
||||
</t>
|
||||
<t t-name="SearchPanel.CategoryValues">
|
||||
<t t-foreach="values" t-as="valueId">
|
||||
<t t-set="value" t-value="category.values[valueId]"/>
|
||||
<li class="o_search_panel_category_value border-0 list-group-item" t-att-data-id="value.id" t-att-data-category-id="category.id">
|
||||
<header t-att-class="'list-group-item-action' + (value.id === category.activeValueId ? ' active' : '')">
|
||||
<label t-att-for="value.display_name" class="o_search_panel_label mb0 d-block">
|
||||
<i t-if="value.childrenIds.length" t-att-class="'fa fa-fw pull-right o_toggle_fold ' + (value.folded ? 'fa-caret-left' : 'fa-caret-down')"/>
|
||||
<span class="o_search_panel_label_title"><t t-esc="value.display_name"/></span>
|
||||
</label>
|
||||
</header>
|
||||
<ul t-if="!value.folded" class="list-group d-block">
|
||||
<t t-call="SearchPanel.CategoryValues">
|
||||
<t t-set="values" t-value="value.childrenIds"/>
|
||||
</t>
|
||||
</ul>
|
||||
</li>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-name="SearchPanel.Filter">
|
||||
<section class="o_search_panel_section o_search_panel_filter">
|
||||
<t t-call="SearchPanel.SectionHeader">
|
||||
<t t-set="section" t-value="filter"/>
|
||||
</t>
|
||||
<ul class="list-group d-block">
|
||||
<t t-if="filter.groups" t-call="SearchPanel.FilterGroups">
|
||||
<t t-set="groups" t-value="filter.groups"/>
|
||||
</t>
|
||||
<t t-else="" t-call="SearchPanel.FilterValues">
|
||||
<t t-set="sortedValueIds" t-value="filter.sortedValueIds"/>
|
||||
<t t-set="values" t-value="filter.values"/>
|
||||
</t>
|
||||
</ul>
|
||||
</section>
|
||||
</t>
|
||||
<t t-name="SearchPanel.FilterGroups">
|
||||
<li t-foreach="filter.sortedGroupIds" t-as="groupId" t-att-data-group-id="groupId" t-att-data-filter-id="filter.id"
|
||||
class="o_search_panel_filter_group list-group-item border-0">
|
||||
<t t-set="group" t-value="groups[groupId]"/>
|
||||
<div class="custom-control custom-checkbox">
|
||||
<t t-set="inputId" t-value="_.uniqueId('input_')"/>
|
||||
<input type="checkbox" t-att-id="inputId" t-att-checked="group.state === 'checked' ? 'checked' : undefined" class="custom-control-input"/>
|
||||
<label t-att-for="inputId" class="o_search_panel_label custom-control-label d-block" t-att-title="group.tooltip">
|
||||
<i t-att-class="'fa fa-fw pull-right o_toggle_fold ' + (group.folded ? 'fa-caret-left' : 'fa-caret-down')"/>
|
||||
<span class="o_search_panel_label_title"><t t-esc="group.name"/></span>
|
||||
</label>
|
||||
</div>
|
||||
<ul t-if="!group.folded" class="list-group d-block">
|
||||
<t t-call="SearchPanel.FilterValues">
|
||||
<t t-set="sortedValueIds" t-value="group.sortedValueIds"/>
|
||||
<t t-set="values" t-value="group.values"/>
|
||||
</t>
|
||||
</ul>
|
||||
</li>
|
||||
<ul t-if="groups['false']" class="list-group d-block">
|
||||
<t t-call="SearchPanel.FilterValues">
|
||||
<t t-set="group" t-value="groups['false']"/>
|
||||
<t t-set="sortedValueIds" t-value="group.sortedValueIds"/>
|
||||
<t t-set="values" t-value="group.values"/>
|
||||
</t>
|
||||
</ul>
|
||||
</t>
|
||||
<t t-name="SearchPanel.FilterValues">
|
||||
<li t-foreach="sortedValueIds" t-as="valueId" t-att-data-value-id="valueId" t-att-data-filter-id="filter.id"
|
||||
class="o_search_panel_filter_value list-group-item border-0">
|
||||
<t t-set="value" t-value="values[valueId]"/>
|
||||
<div class="custom-control custom-checkbox">
|
||||
<t t-set="inputId" t-value="_.uniqueId('input_')"/>
|
||||
<input type="checkbox" t-att-id="inputId" t-att-checked="value.checked ? 'checked' : undefined" class="custom-control-input"/>
|
||||
<label t-att-for="inputId" class="o_search_panel_label custom-control-label d-block" t-att-title="group && group.tooltip">
|
||||
<span class="o_search_panel_label_title"><t t-esc="value.name"/></span>
|
||||
<span t-if="value.count > 0" class="pull-right text-muted mr-2 mt-1 small"><t t-esc="value.count"/></span>
|
||||
<span t-if="filter.disableCounters" class="pull-right text-muted mr-2 mt-1 small">?</span>
|
||||
</label>
|
||||
</div>
|
||||
</li>
|
||||
</t>
|
||||
|
||||
<t t-name="SearchPanel.MobileSummary">
|
||||
<t t-set="emptySelection" t-value="!categories.length & !filterValues.length"/>
|
||||
<summary class="d-flex align-items-center">
|
||||
<div class="text-truncate font-italic ml-2 mr-auto">
|
||||
<t t-if="emptySelection">Filters...</t>
|
||||
<span t-foreach="categories" t-as="category" class="o_search_panel_category mr-1">
|
||||
<i t-if="category.icon" t-attf-class="o_search_panel_section_icon fa fa-w #{category.icon}" t-attf-style="#{category.color ? ('color: ' + category.color) : undefined}"/>
|
||||
<t t-esc="category.breadcrumb.join(separator)"/>
|
||||
</span>
|
||||
<span t-foreach="filterValues" t-as="filterValue" class="o_search_panel_filter mr-1">
|
||||
<i t-if="filterValue.icon" t-attf-class="o_search_panel_section_icon fa fa-w #{filterValue.icon}" t-attf-style="#{filterValue.color ? ('color: ' + filterValue.color) : undefined}"/>
|
||||
<t t-esc="filterValue.name"/>
|
||||
</span>
|
||||
</div>
|
||||
<i class="fa fa-fw fa-chevron-left"/>
|
||||
</summary>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
25
web_view_searchpanel/template/assets.xml
Normal file
25
web_view_searchpanel/template/assets.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<!--
|
||||
|
||||
Copyright 2017-2019 MuK IT GmbH.
|
||||
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||
|
||||
-->
|
||||
|
||||
<odoo>
|
||||
|
||||
<template id="assets_backend" name="Backend Assets" inherit_id="web.assets_backend">
|
||||
<xpath expr="//link[last()]" position="after">
|
||||
<link rel="stylesheet" type="text/css" href="/web_view_searchpanel/static/src/scss/variables.scss" />
|
||||
<link rel="stylesheet" type="text/css" href="/web_view_searchpanel/static/src/scss/kanban_view.scss" />
|
||||
</xpath>
|
||||
<xpath expr="//script[last()]" position="after">
|
||||
<script type="text/javascript" src="/web_view_searchpanel/static/src/js/kanban_searchpanel.js" />
|
||||
<script type="text/javascript" src="/web_view_searchpanel/static/src/js/kanban_controller.js" />
|
||||
<script type="text/javascript" src="/web_view_searchpanel/static/src/js/kanban_renderer.js" />
|
||||
<script type="text/javascript" src="/web_view_searchpanel/static/src/js/kanban_view.js" />
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user