diff --git a/web_view_searchpanel/LICENSE b/web_view_searchpanel/LICENSE deleted file mode 100644 index 153d416dc..000000000 --- a/web_view_searchpanel/LICENSE +++ /dev/null @@ -1,165 +0,0 @@ - GNU LESSER GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - - This version of the GNU Lesser General Public License incorporates -the terms and conditions of version 3 of the GNU General Public -License, supplemented by the additional permissions listed below. - - 0. Additional Definitions. - - As used herein, "this License" refers to version 3 of the GNU Lesser -General Public License, and the "GNU GPL" refers to version 3 of the GNU -General Public License. - - "The Library" refers to a covered work governed by this License, -other than an Application or a Combined Work as defined below. - - An "Application" is any work that makes use of an interface provided -by the Library, but which is not otherwise based on the Library. -Defining a subclass of a class defined by the Library is deemed a mode -of using an interface provided by the Library. - - A "Combined Work" is a work produced by combining or linking an -Application with the Library. The particular version of the Library -with which the Combined Work was made is also called the "Linked -Version". - - The "Minimal Corresponding Source" for a Combined Work means the -Corresponding Source for the Combined Work, excluding any source code -for portions of the Combined Work that, considered in isolation, are -based on the Application, and not on the Linked Version. - - The "Corresponding Application Code" for a Combined Work means the -object code and/or source code for the Application, including any data -and utility programs needed for reproducing the Combined Work from the -Application, but excluding the System Libraries of the Combined Work. - - 1. Exception to Section 3 of the GNU GPL. - - You may convey a covered work under sections 3 and 4 of this License -without being bound by section 3 of the GNU GPL. - - 2. Conveying Modified Versions. - - If you modify a copy of the Library, and, in your modifications, a -facility refers to a function or data to be supplied by an Application -that uses the facility (other than as an argument passed when the -facility is invoked), then you may convey a copy of the modified -version: - - a) under this License, provided that you make a good faith effort to - ensure that, in the event an Application does not supply the - function or data, the facility still operates, and performs - whatever part of its purpose remains meaningful, or - - b) under the GNU GPL, with none of the additional permissions of - this License applicable to that copy. - - 3. Object Code Incorporating Material from Library Header Files. - - The object code form of an Application may incorporate material from -a header file that is part of the Library. You may convey such object -code under terms of your choice, provided that, if the incorporated -material is not limited to numerical parameters, data structure -layouts and accessors, or small macros, inline functions and templates -(ten or fewer lines in length), you do both of the following: - - a) Give prominent notice with each copy of the object code that the - Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the object code with a copy of the GNU GPL and this license - document. - - 4. Combined Works. - - You may convey a Combined Work under terms of your choice that, -taken together, effectively do not restrict modification of the -portions of the Library contained in the Combined Work and reverse -engineering for debugging such modifications, if you also do each of -the following: - - a) Give prominent notice with each copy of the Combined Work that - the Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the Combined Work with a copy of the GNU GPL and this license - document. - - c) For a Combined Work that displays copyright notices during - execution, include the copyright notice for the Library among - these notices, as well as a reference directing the user to the - copies of the GNU GPL and this license document. - - d) Do one of the following: - - 0) Convey the Minimal Corresponding Source under the terms of this - License, and the Corresponding Application Code in a form - suitable for, and under terms that permit, the user to - recombine or relink the Application with a modified version of - the Linked Version to produce a modified Combined Work, in the - manner specified by section 6 of the GNU GPL for conveying - Corresponding Source. - - 1) Use a suitable shared library mechanism for linking with the - Library. A suitable mechanism is one that (a) uses at run time - a copy of the Library already present on the user's computer - system, and (b) will operate properly with a modified version - of the Library that is interface-compatible with the Linked - Version. - - e) Provide Installation Information, but only if you would otherwise - be required to provide such information under section 6 of the - GNU GPL, and only to the extent that such information is - necessary to install and execute a modified version of the - Combined Work produced by recombining or relinking the - Application with a modified version of the Linked Version. (If - you use option 4d0, the Installation Information must accompany - the Minimal Corresponding Source and Corresponding Application - Code. If you use option 4d1, you must provide the Installation - Information in the manner specified by section 6 of the GNU GPL - for conveying Corresponding Source.) - - 5. Combined Libraries. - - You may place library facilities that are a work based on the -Library side by side in a single library together with other library -facilities that are not Applications and are not covered by this -License, and convey such a combined library under terms of your -choice, if you do both of the following: - - a) Accompany the combined library with a copy of the same work based - on the Library, uncombined with any other library facilities, - conveyed under the terms of this License. - - b) Give prominent notice with the combined library that part of it - is a work based on the Library, and explaining where to find the - accompanying uncombined form of the same work. - - 6. Revised Versions of the GNU Lesser General Public License. - - The Free Software Foundation may publish revised and/or new versions -of the GNU Lesser General Public License from time to time. Such new -versions will be similar in spirit to the present version, but may -differ in detail to address new problems or concerns. - - Each version is given a distinguishing version number. If the -Library as you received it specifies that a certain numbered version -of the GNU Lesser General Public License "or any later version" -applies to it, you have the option of following the terms and -conditions either of that published version or of any later version -published by the Free Software Foundation. If the Library as you -received it does not specify a version number of the GNU Lesser -General Public License, you may choose any version of the GNU Lesser -General Public License ever published by the Free Software Foundation. - - If the Library as you received it specifies that a proxy can decide -whether future versions of the GNU Lesser General Public License shall -apply, that proxy's public statement of acceptance of any version is -permanent authorization for you to choose that version for the -Library. \ No newline at end of file diff --git a/web_view_searchpanel/README.rst b/web_view_searchpanel/README.rst index a54d3c4ef..bce549d8b 100644 --- a/web_view_searchpanel/README.rst +++ b/web_view_searchpanel/README.rst @@ -1,95 +1,48 @@ -================ -MuK Search Panel -================ +============ +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. -Installation -============ +**Table of contents** -To install this module, you need to: - -Download the module and add it to your Odoo addons folder. Afterward, log on to -your Odoo server and go to the Apps menu. Trigger the debug mode and update the -list by clicking on the "Update Apps List" link. Now install the module by -clicking on the install button. - -Another way to install this module is via the package management for Python -(`PyPI `_). - -To install our modules using the package manager make sure -`odoo-autodiscover `_ is installed -correctly. Then open a console and install the module by entering the following -command: - -``pip install --extra-index-url https://nexus.mukit.at/repository/odoo/simple `` - -The module name consists of the Odoo version and the module name, where -underscores are replaced by a dash. - -**Module:** - -``odoo-addon-`` - -**Example:** - -``sudo -H pip3 install --extra-index-url https://nexus.mukit.at/repository/odoo/simple odoo11-addon-muk-utils`` - -Once the installation has been successfully completed, the app is already in the -correct folder. Log on to your Odoo server and go to the Apps menu. Trigger the -debug mode and update the list by clicking on the "Update Apps List" link. Now -install the module by clicking on the install button. - -The biggest advantage of this variant is that you can now also update the app -using the "pip" command. To do this, enter the following command in your console: - -``pip install --upgrade --extra-index-url https://nexus.mukit.at/repository/odoo/simple `` - -When the process is finished, restart your server and update the application in -Odoo. The steps are the same as for the installation only the button has changed -from "Install" to "Upgrade". - -You can also view available Apps directly in our `repository `_ -and find a more detailed installation guide on our `website `_. - -For modules licensed under OPL-1, you will receive access data when you purchase -the module. If the modules were not purchased directly from -`MuK IT `_ please contact our support (support@mukit.at) -with a confirmation of purchase to receive the corresponding access data. - -Upgrade -============ - -To upgrade this module, you need to: - -Download the module and add it to your Odoo addons folder. Restart the server -and log on to your Odoo server. Select the Apps menu and upgrade the module by -clicking on the upgrade button. - -If you installed the module using the "pip" command, you can also update the -module in the same way. Just type the following command into the console: - -``pip install --upgrade --extra-index-url https://nexus.mukit.at/repository/odoo/simple `` - -When the process is finished, restart your server and update the application in -Odoo, just like you would normally. - -Configuration -============= - -No additional configuration is needed to use this module. +.. 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. +* ``select`` determines the behavior and display. * ``groups``: restricts to specific users * ``string``: determines the label to display * ``icon``: specifies which icon is used @@ -112,8 +65,8 @@ of the search panel. Consider - - + + 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``. @@ -124,37 +77,44 @@ will depend on the value currently selected for the field ``department_id``. 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 `_. +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 `_. + +Do not contact contributors directly about support or help with technical issues. + Credits ======= +Authors +~~~~~~~ + +* MuK IT + Contributors ------------- +~~~~~~~~~~~~ * Mathias Markl +* Enric Tobella -Images ------------- +Maintainers +~~~~~~~~~~~ -Some pictures are based on or inspired by the icon set of Font Awesome: +This module is maintained by the OCA. -* `Font Awesome `_ +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org -Projects ------------- +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. -Parts of the module are based on or inspired by: +This module is part of the `OCA/web `_ project on GitHub. -* `Odoo `_ - -Author & Maintainer -------------------- - -This module is maintained by the `MuK IT GmbH `_. - -MuK IT is an Austrian company specialized in customizing and extending Odoo. -We develop custom solutions for your individual needs to help you focus on -your strength and expertise to grow your business. - -If you want to get in touch please contact us via mail -(sale@mukit.at) or visit our website (https://mukit.at). +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/web_view_searchpanel/__init__.py b/web_view_searchpanel/__init__.py index c4c92ba30..0650744f6 100644 --- a/web_view_searchpanel/__init__.py +++ b/web_view_searchpanel/__init__.py @@ -1,23 +1 @@ -################################################################################### -# -# Copyright (c) 2017-2019 MuK IT GmbH. -# -# This file is part of MuK Search Panel -# (see https://mukit.at). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see . -# -################################################################################### - -from . import models \ No newline at end of file +from . import models diff --git a/web_view_searchpanel/__manifest__.py b/web_view_searchpanel/__manifest__.py index 21fbf5a72..b94cca3a2 100644 --- a/web_view_searchpanel/__manifest__.py +++ b/web_view_searchpanel/__manifest__.py @@ -1,54 +1,27 @@ -################################################################################### -# -# Copyright (c) 2017-2019 MuK IT GmbH. -# -# This file is part of MuK Search Panel -# (see https://mukit.at). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see . -# -################################################################################### +# Copyright 2017-2019 MuK IT GmbH. +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). { - 'name': 'MuK Search Panel', + 'name': 'Search Panel', 'summary': 'Kanban Search Panel', - 'version': '12.0.2.0.1', + 'version': '12.0.1.0.0', 'category': 'Extra Tools', 'license': 'LGPL-3', - 'author': 'MuK IT', - 'website': 'https://www.mukit.at', - 'live_test_url': 'https://mukit.at/r/SgN', - 'contributors': [ - 'Mathias Markl ', - ], + 'author': 'MuK IT, Odoo Community Association (OCA)', + 'website': 'https://github.com/OCA/web', 'depends': [ - 'muk_web_utils', + 'web', ], 'data': [ - "template/assets.xml", + 'template/assets.xml', ], 'qweb': [ - 'static/src/xml/*.xml', + 'static/src/xml/kanban.xml', ], 'images': [ 'static/description/banner.png' ], - 'external_dependencies': { - 'python': [], - 'bin': [], - }, - 'application': False, - 'installable': True, - 'auto_install': False, -} \ No newline at end of file + 'demo': [ + 'demo/res_users_views.xml', + ] +} diff --git a/web_view_searchpanel/demo/res_users_views.xml b/web_view_searchpanel/demo/res_users_views.xml new file mode 100644 index 000000000..12404680c --- /dev/null +++ b/web_view_searchpanel/demo/res_users_views.xml @@ -0,0 +1,27 @@ + + + + + + + + + res.users.kanban + res.users + + + + + + + + + + + + diff --git a/web_view_searchpanel/doc/changelog.rst b/web_view_searchpanel/doc/changelog.rst deleted file mode 100644 index 4c6a0d20e..000000000 --- a/web_view_searchpanel/doc/changelog.rst +++ /dev/null @@ -1,15 +0,0 @@ -`1.2.0` -------- - -- Mobile Support - -`1.1.0` -------- - -- Lazy Loading on long requests - -`1.0.0` -------- - -- Init version - diff --git a/web_view_searchpanel/doc/index.rst b/web_view_searchpanel/doc/index.rst deleted file mode 100644 index a54d3c4ef..000000000 --- a/web_view_searchpanel/doc/index.rst +++ /dev/null @@ -1,160 +0,0 @@ -================ -MuK Search Panel -================ - -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. - -Installation -============ - -To install this module, you need to: - -Download the module and add it to your Odoo addons folder. Afterward, log on to -your Odoo server and go to the Apps menu. Trigger the debug mode and update the -list by clicking on the "Update Apps List" link. Now install the module by -clicking on the install button. - -Another way to install this module is via the package management for Python -(`PyPI `_). - -To install our modules using the package manager make sure -`odoo-autodiscover `_ is installed -correctly. Then open a console and install the module by entering the following -command: - -``pip install --extra-index-url https://nexus.mukit.at/repository/odoo/simple `` - -The module name consists of the Odoo version and the module name, where -underscores are replaced by a dash. - -**Module:** - -``odoo-addon-`` - -**Example:** - -``sudo -H pip3 install --extra-index-url https://nexus.mukit.at/repository/odoo/simple odoo11-addon-muk-utils`` - -Once the installation has been successfully completed, the app is already in the -correct folder. Log on to your Odoo server and go to the Apps menu. Trigger the -debug mode and update the list by clicking on the "Update Apps List" link. Now -install the module by clicking on the install button. - -The biggest advantage of this variant is that you can now also update the app -using the "pip" command. To do this, enter the following command in your console: - -``pip install --upgrade --extra-index-url https://nexus.mukit.at/repository/odoo/simple `` - -When the process is finished, restart your server and update the application in -Odoo. The steps are the same as for the installation only the button has changed -from "Install" to "Upgrade". - -You can also view available Apps directly in our `repository `_ -and find a more detailed installation guide on our `website `_. - -For modules licensed under OPL-1, you will receive access data when you purchase -the module. If the modules were not purchased directly from -`MuK IT `_ please contact our support (support@mukit.at) -with a confirmation of purchase to receive the corresponding access data. - -Upgrade -============ - -To upgrade this module, you need to: - -Download the module and add it to your Odoo addons folder. Restart the server -and log on to your Odoo server. Select the Apps menu and upgrade the module by -clicking on the upgrade button. - -If you installed the module using the "pip" command, you can also update the -module in the same way. Just type the following command into the console: - -``pip install --upgrade --extra-index-url https://nexus.mukit.at/repository/odoo/simple `` - -When the process is finished, restart your server and update the application in -Odoo, just like you would normally. - -Configuration -============= - -No additional configuration is needed to use this module. - -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 - - - - - - -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. - -Credits -======= - -Contributors ------------- - -* Mathias Markl - -Images ------------- - -Some pictures are based on or inspired by the icon set of Font Awesome: - -* `Font Awesome `_ - -Projects ------------- - -Parts of the module are based on or inspired by: - -* `Odoo `_ - -Author & Maintainer -------------------- - -This module is maintained by the `MuK IT GmbH `_. - -MuK IT is an Austrian company specialized in customizing and extending Odoo. -We develop custom solutions for your individual needs to help you focus on -your strength and expertise to grow your business. - -If you want to get in touch please contact us via mail -(sale@mukit.at) or visit our website (https://mukit.at). diff --git a/web_view_searchpanel/models/__init__.py b/web_view_searchpanel/models/__init__.py index 2f434a23a..0e4444933 100644 --- a/web_view_searchpanel/models/__init__.py +++ b/web_view_searchpanel/models/__init__.py @@ -1,24 +1 @@ -################################################################################### -# -# Copyright (c) 2017-2019 MuK IT GmbH. -# -# This file is part of MuK Search Panel -# (see https://mukit.at). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see . -# -################################################################################### - from . import base - diff --git a/web_view_searchpanel/models/base.py b/web_view_searchpanel/models/base.py index f87653170..5052f08c7 100644 --- a/web_view_searchpanel/models/base.py +++ b/web_view_searchpanel/models/base.py @@ -1,42 +1,19 @@ -################################################################################### -# -# Copyright (c) 2017-2019 MuK IT GmbH. -# -# This file is part of MuK Search Panel -# (see https://mukit.at). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see . -# -################################################################################### +# 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, fields, models +from odoo import _, api, models from odoo.exceptions import UserError from odoo.osv import expression -from odoo.tools import lazy _logger = logging.getLogger(__name__) + class Base(models.AbstractModel): - + _inherit = 'base' - - #---------------------------------------------------------- - # Functions - #---------------------------------------------------------- - + @api.model def search_panel_select_range(self, field_name, **kwargs): """ @@ -46,32 +23,41 @@ class Base(models.AbstractModel): :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. + '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})) + 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 + 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), + 'values': Comodel.with_context( + hierarchical_naming=False).search_read(model_domain, fields), } @api.model @@ -86,7 +72,8 @@ class Base(models.AbstractModel): :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 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), @@ -98,8 +85,11 @@ class Base(models.AbstractModel): 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})) + 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) @@ -139,13 +129,17 @@ class Base(models.AbstractModel): if field.type == 'many2one': counters = {} if not disable_counters: - groups = self.read_group(model_domain, [field_name], [field_name]) + 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', group_by] if group_by else ['display_name'] + # 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'] @@ -155,12 +149,16 @@ class Base(models.AbstractModel): 'count': counters.get(record_id, 0), } if group_by: - values['group_id'], values['group_name'] = group_id_name(record[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', group_by] if group_by else ['display_name'] + # 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'] @@ -170,22 +168,26 @@ class Base(models.AbstractModel): 'count': 0, } if not disable_counters: - count_domain = expression.AND([model_domain, [(field_name, 'in', record_id)]]) + 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]) + 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]) + 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] + # 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, diff --git a/web_view_searchpanel/readme/CONTRIBUTORS.rst b/web_view_searchpanel/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..1c5c42ceb --- /dev/null +++ b/web_view_searchpanel/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Mathias Markl +* Enric Tobella \ No newline at end of file diff --git a/web_view_searchpanel/readme/DESCRIPTION.rst b/web_view_searchpanel/readme/DESCRIPTION.rst new file mode 100644 index 000000000..e80c27775 --- /dev/null +++ b/web_view_searchpanel/readme/DESCRIPTION.rst @@ -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. diff --git a/web_view_searchpanel/readme/USAGE.rst b/web_view_searchpanel/readme/USAGE.rst new file mode 100644 index 000000000..931e56968 --- /dev/null +++ b/web_view_searchpanel/readme/USAGE.rst @@ -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 + + + + + + +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. diff --git a/web_view_searchpanel/static/description/banner.png b/web_view_searchpanel/static/description/banner.png deleted file mode 100644 index 4718e7585..000000000 Binary files a/web_view_searchpanel/static/description/banner.png and /dev/null differ diff --git a/web_view_searchpanel/static/description/icon.png b/web_view_searchpanel/static/description/icon.png index 80205d335..3a0328b51 100644 Binary files a/web_view_searchpanel/static/description/icon.png and b/web_view_searchpanel/static/description/icon.png differ diff --git a/web_view_searchpanel/static/description/icon.svg b/web_view_searchpanel/static/description/icon.svg deleted file mode 100644 index e9106763e..000000000 --- a/web_view_searchpanel/static/description/icon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web_view_searchpanel/static/description/index.html b/web_view_searchpanel/static/description/index.html index 45b4ee928..cc7f5b43e 100644 --- a/web_view_searchpanel/static/description/index.html +++ b/web_view_searchpanel/static/description/index.html @@ -1,124 +1,462 @@ -
-
-

MuK Search Panel

-

Kanban Search Panel

-

MuK IT GmbH - - www.mukit.at

-
-
+ + + + + + +Search Panel + + + +
+

Search Panel

+ + +

Beta License: LGPL-3 OCA/web Translate me on Weblate Try me on Runbot

+

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

+ +
+

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

+
+<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. +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.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • MuK IT
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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 project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/web_view_searchpanel/static/description/logo.png b/web_view_searchpanel/static/description/logo.png deleted file mode 100644 index 9427ce33e..000000000 Binary files a/web_view_searchpanel/static/description/logo.png and /dev/null differ diff --git a/web_view_searchpanel/static/description/preview.png b/web_view_searchpanel/static/description/preview.png deleted file mode 100644 index 1deb1ccef..000000000 Binary files a/web_view_searchpanel/static/description/preview.png and /dev/null differ diff --git a/web_view_searchpanel/static/description/service_customization.png b/web_view_searchpanel/static/description/service_customization.png deleted file mode 100644 index 3eac66488..000000000 Binary files a/web_view_searchpanel/static/description/service_customization.png and /dev/null differ diff --git a/web_view_searchpanel/static/description/service_development.png b/web_view_searchpanel/static/description/service_development.png deleted file mode 100644 index 580d46046..000000000 Binary files a/web_view_searchpanel/static/description/service_development.png and /dev/null differ diff --git a/web_view_searchpanel/static/description/service_implementation.png b/web_view_searchpanel/static/description/service_implementation.png deleted file mode 100644 index d64b66bda..000000000 Binary files a/web_view_searchpanel/static/description/service_implementation.png and /dev/null differ diff --git a/web_view_searchpanel/static/description/service_integration.png b/web_view_searchpanel/static/description/service_integration.png deleted file mode 100644 index 76c5e80f4..000000000 Binary files a/web_view_searchpanel/static/description/service_integration.png and /dev/null differ diff --git a/web_view_searchpanel/static/description/service_support.png b/web_view_searchpanel/static/description/service_support.png deleted file mode 100644 index 4c530fafd..000000000 Binary files a/web_view_searchpanel/static/description/service_support.png and /dev/null differ diff --git a/web_view_searchpanel/static/src/js/kanban_controller.js b/web_view_searchpanel/static/src/js/kanban_controller.js index c4e9a36a7..d0f2f297d 100644 --- a/web_view_searchpanel/static/src/js/kanban_controller.js +++ b/web_view_searchpanel/static/src/js/kanban_controller.js @@ -1,80 +1,58 @@ -/********************************************************************************** +/** * -* Copyright (c) 2017-2019 MuK IT GmbH. +* Copyright 2017-2019 MuK IT GmbH. +* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). * -* This file is part of MuK Search Panel -* (see https://mukit.at). -* -* This program is free software: you can redistribute it and/or modify -* it under the terms of the GNU Lesser General Public License as published by -* the Free Software Foundation, either version 3 of the License, or -* (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU Lesser General Public License for more details. -* -* You should have received a copy of the GNU Lesser General Public License -* along with this program. If not, see . -* -**********************************************************************************/ +**/ -odoo.define('muk_web_searchpanel.KanbanController', function (require) { -"use strict"; +odoo.define('web_view_searchpanel.KanbanController', function (require) { + "use strict"; + var KanbanController = require('web.KanbanController'); -var core = require('web.core'); -var session = require('web.session'); -var view_dialogs = require('web.view_dialogs'); -var viewUtils = require('web.viewUtils'); - -var Domain = require('web.Domain'); -var Context = require('web.Context'); -var KanbanController = require('web.KanbanController'); - -var _t = core._t; -var QWeb = core.qweb; - -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) { + 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); - } - 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}); - }, -}); + }, + 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}); + }, + }); }); diff --git a/web_view_searchpanel/static/src/js/kanban_renderer.js b/web_view_searchpanel/static/src/js/kanban_renderer.js index 6e13cc6f1..6664e3eb0 100644 --- a/web_view_searchpanel/static/src/js/kanban_renderer.js +++ b/web_view_searchpanel/static/src/js/kanban_renderer.js @@ -1,41 +1,19 @@ -/********************************************************************************** +/** * -* Copyright (c) 2017-2019 MuK IT GmbH. +* Copyright 2017-2019 MuK IT GmbH. +* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). * -* This file is part of MuK Search Panel -* (see https://mukit.at). -* -* This program is free software: you can redistribute it and/or modify -* it under the terms of the GNU Lesser General Public License as published by -* the Free Software Foundation, either version 3 of the License, or -* (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU Lesser General Public License for more details. -* -* You should have received a copy of the GNU Lesser General Public License -* along with this program. If not, see . -* -**********************************************************************************/ +**/ -odoo.define('muk_web_searchpanel.KanbanRenderer', function (require) { -"use strict"; +odoo.define('web_view_searchpanel.KanbanRenderer', function (require) { + "use strict"; -var core = require('web.core'); -var config = require('web.config'); -var session = require('web.session'); + var KanbanRenderer = require('web.KanbanRenderer'); -var KanbanRenderer = require('web.KanbanRenderer'); - -var _t = core._t; -var QWeb = core.qweb; - -KanbanRenderer.include({ - render: function () { - return this._render(); - }, -}); + KanbanRenderer.include({ + render: function () { + return this._render(); + }, + }); }); diff --git a/web_view_searchpanel/static/src/js/kanban_searchpanel.js b/web_view_searchpanel/static/src/js/kanban_searchpanel.js index 95b1f78a7..78512e644 100644 --- a/web_view_searchpanel/static/src/js/kanban_searchpanel.js +++ b/web_view_searchpanel/static/src/js/kanban_searchpanel.js @@ -1,650 +1,631 @@ /********************************************************************************** * -* Copyright (c) 2017-2019 MuK IT GmbH. -* -* This file is part of MuK Search Panel -* (see https://mukit.at). -* -* This program is free software: you can redistribute it and/or modify -* it under the terms of the GNU Lesser General Public License as published by -* the Free Software Foundation, either version 3 of the License, or -* (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU Lesser General Public License for more details. -* -* You should have received a copy of the GNU Lesser General Public License -* along with this program. If not, see . +* Copyright 2017-2019 MuK IT GmbH. +* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). * **********************************************************************************/ -odoo.define('web.SearchPanel', function (require) { -"use strict"; +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 core = require('web.core'); + var config = require('web.config'); + var Domain = require('web.Domain'); + var Widget = require('web.Widget'); -var qweb = core.qweb; + 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', - }, + 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); + /** + * @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(); + this.categories = _.pick(params.sections, function (section) { + return section.type === 'category'; }); - }); - return $.when(loading, this._super.apply(this, arguments)); - }, - start: function () { - var self = this; - if(this.loadProm.state() !== 'resolved') { - this.$el.html($("
", { - 'class': "o_search_panel_loading", - 'html': "" - })); - } - 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, + this.filters = _.pick(params.sections, function (section) { + return section.type === 'filter'; }); - }); - 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; - }); + this.defaultCategoryValues = params.defaultCategoryValues || {}; + this.fields = params.fields; + this.model = params.model; + this.searchDomain = params.searchDomain; - 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; + this.loadProm = $.Deferred(); + this.loadPromLazy = true; + }, + willStart: function () { + var self = this; + var loading = $.Deferred(); + var loadPromTimer = setTimeout(function () { + if(loading.state() !== 'resolved') { + loading.resolve(); } - 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]}; + }, 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($("
", { + 'class': "o_search_panel_loading", + 'html': "" + })); + } + 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); }); - def = $.when(values); } else { - var categoryDomain = self._getCategoryDomain(); - var filterDomain = self._getFilterDomain(); - def = self._rpc({ - method: 'search_panel_select_range', + 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: [category.fieldName], + 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 (result) { - category.parentField = result.parent_field; - return result.values; + shadow: true, + }).then(function (values) { + self._createFilterTree(filterId, 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]; - console.log('category', category); - 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}); - } - console.log('selection', selection); - return selection; - }, []); + return $.when.apply($, defs); }, - - _getFilterSelection: function () { + /** + * Compute and return the domain based on the current active categories. + * + * @private + * @returns {Array[]} + */ + _getCategoryDomain: function () { var self = this; - return Object.keys(this.filters).reduce(function (selection, filterId) { + 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]; - console.log('filter', filter); 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]; + 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}); } }); - }); - } 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}); - } - }); - } - console.log('selection', selection); - return selection; - }, []); - }, + } + return selection; + }, []); + }, - _render: function () { - this._super.apply(this, arguments); - this.$el.prepend(qweb.render('SearchPanel.MobileSummary', { - categories: this._getCategorySelection(), - filterValues: this._getFilterSelection(), - separator: ' / ', - })); - }, - }); -} + _render: function () { + this._super.apply(this, arguments); + this.$el.prepend(qweb.render('SearchPanel.MobileSummary', { + categories: this._getCategorySelection(), + filterValues: this._getFilterSelection(), + separator: ' / ', + })); + }, + }); + } -return SearchPanel; + return SearchPanel; -}); \ No newline at end of file +}); diff --git a/web_view_searchpanel/static/src/js/kanban_view.js b/web_view_searchpanel/static/src/js/kanban_view.js index 17705c612..22cefaf2b 100644 --- a/web_view_searchpanel/static/src/js/kanban_view.js +++ b/web_view_searchpanel/static/src/js/kanban_view.js @@ -1,133 +1,118 @@ -/********************************************************************************** +/** * -* Copyright (c) 2017-2019 MuK IT GmbH. +* Copyright 2017-2019 MuK IT GmbH. +* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). * -* This file is part of MuK Search Panel -* (see https://mukit.at). -* -* This program is free software: you can redistribute it and/or modify -* it under the terms of the GNU Lesser General Public License as published by -* the Free Software Foundation, either version 3 of the License, or -* (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU Lesser General Public License for more details. -* -* You should have received a copy of the GNU Lesser General Public License -* along with this program. If not, see . -* -**********************************************************************************/ +**/ -odoo.define('muk_web_searchpanel.KanbanView', function (require) { -"use strict"; +odoo.define('web_view_searchpanel.KanbanView', function (require) { + "use strict"; -var core = require('web.core'); -var config = require('web.config'); -var pyUtils = require('web.py_utils'); -var utils = require('web.utils'); + var pyUtils = require('web.py_utils'); -var KanbanView = require('web.KanbanView'); -var SearchPanel = require('web.SearchPanel'); + var KanbanView = require('web.KanbanView'); + var SearchPanel = require('web.SearchPanel'); -var _t = core._t; -var QWeb = core.qweb; -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); + 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 + }, + 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]; + }, + _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; } - }); - 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') { -// if (!config.device.isMobile) { -// this._processSearchPanelNode(node, fv); -// } - 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'; + 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; - }); - }, -}); + 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; + }); + }, + }); }); diff --git a/web_view_searchpanel/static/src/scss/kanban_view.scss b/web_view_searchpanel/static/src/scss/kanban_view.scss index 6c2495a90..fb0a6c8d6 100644 --- a/web_view_searchpanel/static/src/scss/kanban_view.scss +++ b/web_view_searchpanel/static/src/scss/kanban_view.scss @@ -1,22 +1,7 @@ /********************************************************************************** * -* Copyright (c) 2017-2019 MuK IT GmbH. -* -* This file is part of MuK Search Panel -* (see https://mukit.at). -* -* This program is free software: you can redistribute it and/or modify -* it under the terms of the GNU Lesser General Public License as published by -* the Free Software Foundation, either version 3 of the License, or -* (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU Lesser General Public License for more details. -* -* You should have received a copy of the GNU Lesser General Public License -* along with this program. If not, see . +* Copyright 2017-2019 MuK IT GmbH. +* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). * **********************************************************************************/ @@ -128,11 +113,11 @@ @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; diff --git a/web_view_searchpanel/static/src/scss/variables.scss b/web_view_searchpanel/static/src/scss/variables.scss index c4d2259a6..31304c8a5 100644 --- a/web_view_searchpanel/static/src/scss/variables.scss +++ b/web_view_searchpanel/static/src/scss/variables.scss @@ -1,22 +1,7 @@ /********************************************************************************** * -* Copyright (c) 2017-2019 MuK IT GmbH. -* -* This file is part of MuK Search Panel -* (see https://mukit.at). -* -* This program is free software: you can redistribute it and/or modify -* it under the terms of the GNU Lesser General Public License as published by -* the Free Software Foundation, either version 3 of the License, or -* (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU Lesser General Public License for more details. -* -* You should have received a copy of the GNU Lesser General Public License -* along with this program. If not, see . +* Copyright 2017-2019 MuK IT GmbH. +* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). * **********************************************************************************/ diff --git a/web_view_searchpanel/static/src/xml/kanban.xml b/web_view_searchpanel/static/src/xml/kanban.xml index 6dd8110ce..a58efb319 100644 --- a/web_view_searchpanel/static/src/xml/kanban.xml +++ b/web_view_searchpanel/static/src/xml/kanban.xml @@ -1,25 +1,8 @@ @@ -146,4 +129,4 @@ - \ No newline at end of file + diff --git a/web_view_searchpanel/template/assets.xml b/web_view_searchpanel/template/assets.xml index 255e47971..457dd0e35 100644 --- a/web_view_searchpanel/template/assets.xml +++ b/web_view_searchpanel/template/assets.xml @@ -2,23 +2,8 @@ @@ -26,15 +11,15 @@