diff --git a/addons/mail/static/src/js/discuss_mobile.js b/addons/mail/static/src/js/discuss_mobile.js index 556651ec67e..531553e6804 100644 --- a/addons/mail/static/src/js/discuss_mobile.js +++ b/addons/mail/static/src/js/discuss_mobile.js @@ -14,7 +14,6 @@ if (!config.device.isMobile) { Discuss.include({ template: 'mail.discuss_mobile', - need_control_panel: false, // in mobile, we use a custom control panel events: _.extend(Discuss.prototype.events, { 'click .o_mail_mobile_tab': '_onMobileTabClicked', 'click .o_channel_inbox_item': '_onMobileInboxButtonClicked', @@ -122,14 +121,6 @@ Discuss.include({ this._super.apply(this, arguments); } }, - /** - * @private - */ - _toggleSearchView: function () { - this.searchviewDisplayed = !this.searchviewDisplayed; - this.searchview.$el.toggleClass('o_hidden', !this.searchviewDisplayed); - this.$buttons.toggleClass('o_hidden', this.searchviewDisplayed); - }, /** * Overrides to toggle the visibility of the tabs when a message is unselected * @@ -198,10 +189,6 @@ Discuss.include({ self.$buttons.find('.o_mail_chat_button_' + type).removeClass('o_hidden'); self.$buttons.find('.o_mail_chat_button_mark_read').toggleClass('o_hidden', type !== 'channel_inbox'); self.$buttons.find('.o_mail_chat_button_unstar_all').toggleClass('o_hidden', type !== 'channel_starred'); - self.$('.o_enable_searchview').toggleClass('o_hidden', !inInbox); - if (!inInbox && self.searchviewDisplayed) { - self._toggleSearchView(); // close the searchview when leaving Inbox - } // update Inbox page buttons if (inInbox) { @@ -219,17 +206,6 @@ Discuss.include({ self.$('.o_mail_mobile_tab[data-type=' + type + ']').addClass('active'); }); }, - /** - * @override - */ - _updateControlPanel: function () { - this.$buttons.appendTo(this.$('.o_mail_chat_mobile_control_panel')); - this.searchview.$el.appendTo(this.$('.o_mail_chat_mobile_control_panel')); - var $enable_searchview = $('', {type: 'button'}) - .addClass('o_enable_searchview btn fa fa-search') - .on('click', this._toggleSearchView.bind(this)); - $enable_searchview.insertAfter(this.searchview.$el); - }, //-------------------------------------------------------------------------- // Handlers diff --git a/addons/mail/static/src/scss/discuss.scss b/addons/mail/static/src/scss/discuss.scss index de129eaee4c..8bada8804cb 100644 --- a/addons/mail/static/src/scss/discuss.scss +++ b/addons/mail/static/src/scss/discuss.scss @@ -271,35 +271,6 @@ @include o-flex-flow(column, nowrap); background: white; - .o_mail_chat_mobile_control_panel { - @include o-flex(0, 0, auto); - padding: 5px; - min-height: 44px; - border-bottom: 1px solid #ebebeb; - box-shadow: 0px 0px 10px #afafaf; - z-index: 1; // so that box shaddow is above thread's images - .o_searchview { - border: none; - padding: 5px 5px 0px 5px; - .o_searchview_input { - font-size: 15px; - } - .o_searchview_more { - display: none; - } - } - .o_enable_searchview, .o_enable_searchview:focus, - .o_enable_searchview:hover, .o_enable_searchview:active { - @include o-position-absolute($right: 5px, $top: 5px); - color: gray; - background: none; - font-size: 15px; - &:active { - box-shadow: none; - } - } - } - .o_mail_chat_mobile_inbox_buttons { @include o-flex-display; @include o-flex(0, 0, auto); diff --git a/addons/mail/static/src/xml/discuss.xml b/addons/mail/static/src/xml/discuss.xml index 744d624d564..b0a3bfe1ba9 100644 --- a/addons/mail/static/src/xml/discuss.xml +++ b/addons/mail/static/src/xml/discuss.xml @@ -149,7 +149,6 @@ - Inbox diff --git a/addons/mail/static/tests/discuss_mobile_tests.js b/addons/mail/static/tests/discuss_mobile_tests.js index 921436d4657..e7a7162a03d 100644 --- a/addons/mail/static/tests/discuss_mobile_tests.js +++ b/addons/mail/static/tests/discuss_mobile_tests.js @@ -26,7 +26,7 @@ QUnit.test('mobile basic rendering', function (assert) { // /mail/client_action route is always called when the test suite is // launched), and we must wait for this RPC to be done before starting to // test the interface. This should be refactored to facilitate the testing. - assert.expect(9); + assert.expect(8); var done = assert.async(); createDiscuss({ @@ -37,8 +37,6 @@ QUnit.test('mobile basic rendering', function (assert) { services: this.services, }).then(function (discuss) { // test basic rendering in mobile - assert.strictEqual(discuss.$('.o_mail_chat_mobile_control_panel').length, 1, - "should have rendered a control panel"); assert.strictEqual(discuss.$('.o_mail_chat_content .o_mail_no_content').length, 1, "should display the no content message"); assert.strictEqual(discuss.$('.o_mail_mobile_tabs').length, 1, @@ -56,7 +54,7 @@ QUnit.test('mobile basic rendering', function (assert) { "should be in DMs tab"); assert.strictEqual(discuss.$('.o_mail_chat_content .o_mail_no_content').length, 0, "should display the no content message"); - discuss.$('.o_mail_chat_button_dm').click(); // click to add a channel + $('.o_mail_chat_button_dm').click(); // click to add a channel assert.strictEqual(discuss.$('.o_mail_add_channel input:visible').length, 1, "should display the input to add a channel"); diff --git a/addons/web/static/src/js/chrome/search_menus.js b/addons/web/static/src/js/chrome/search_menus.js index d33cdce6500..a6548b051f4 100644 --- a/addons/web/static/src/js/chrome/search_menus.js +++ b/addons/web/static/src/js/chrome/search_menus.js @@ -1,6 +1,7 @@ odoo.define('web.FavoriteMenu', function (require) { "use strict"; +var config = require('web.config'); var core = require('web.core'); var data_manager = require('web.data_manager'); var pyeval = require('web.pyeval'); @@ -38,6 +39,7 @@ return Widget.extend({ this.action = action; this.action_id = action.id; this.filters = {}; + this.isMobile = config.device.isMobile; _.each(filters, this.add_filter.bind(this)); }, start: function () { @@ -305,6 +307,7 @@ return Widget.extend({ odoo.define('web.FilterMenu', function (require) { "use strict"; +var config = require('web.config'); var search_filters = require('web.search_filters'); var search_inputs = require('web.search_inputs'); var Widget = require('web.Widget'); @@ -333,6 +336,7 @@ return Widget.extend({ }, init: function (parent, filters, fields) { this._super(parent); + this.isMobile = config.device.isMobile; this.filters = filters || []; this.searchview = parent; this.propositions = []; @@ -409,6 +413,7 @@ return Widget.extend({ odoo.define('web.GroupByMenu', function (require) { "use strict"; +var config = require('web.config'); var core = require('web.core'); var search_inputs = require('web.search_inputs'); var Widget = require('web.Widget'); @@ -433,6 +438,7 @@ return Widget.extend({ var self = this; this._super(parent); this.searchview = parent; + this.isMobile = config.device.isMobile; this.groups = groups || []; this.groupableFields = []; var groupable_types = ['many2one', 'char', 'boolean', 'selection', 'date', 'datetime']; diff --git a/addons/web/static/src/js/chrome/search_view.js b/addons/web/static/src/js/chrome/search_view.js index 94087c999b6..8fc3cd73a2b 100644 --- a/addons/web/static/src/js/chrome/search_view.js +++ b/addons/web/static/src/js/chrome/search_view.js @@ -326,18 +326,19 @@ var SearchView = Widget.extend({ .toggleClass('fa-search-plus', !this.visible_filters); var menu_defs = []; this.prepare_search_inputs(); - if (this.$buttons) { + var $buttons = this._getButtonsElement(); + if ($buttons) { if (!this.options.disable_filters) { this.filter_menu = new FilterMenu(this, this.filters, this.fields); - menu_defs.push(this.filter_menu.appendTo(this.$buttons)); + menu_defs.push(this.filter_menu.appendTo($buttons)); } if (!this.options.disable_groupby) { this.groupby_menu = new GroupByMenu(this, this.groupbys, this.fields); - menu_defs.push(this.groupby_menu.appendTo(this.$buttons)); + menu_defs.push(this.groupby_menu.appendTo($buttons)); } if (!this.options.disable_favorites) { this.favorite_menu = new FavoriteMenu(this, this.query, this.dataset.model, this.action, this.favorite_filters); - menu_defs.push(this.favorite_menu.appendTo(this.$buttons)); + menu_defs.push(this.favorite_menu.appendTo($buttons)); } } return $.when.apply($, menu_defs).then(this.set_default_filters.bind(this)); @@ -454,7 +455,7 @@ var SearchView = Widget.extend({ return self.$('.o_searchview_input').val().trim(); }, }); - this.autocomplete.appendTo(this.$el); + this.autocomplete.appendTo(this.$('.o_searchview_input_container')); }, /** * Provide auto-completion result for req.term (an array to `resp`) @@ -528,11 +529,11 @@ var SearchView = Widget.extend({ this.query.each(function (facet) { var f = new FacetView(this, facet); - started.push(f.appendTo(self.$el)); + started.push(f.appendTo(self.$('.o_searchview_input_container'))); self.input_subviews.push(f); }, this); var i = new InputView(this); - started.push(i.appendTo(self.$el)); + started.push(i.appendTo(self.$('.o_searchview_input_container'))); self.input_subviews.push(i); _.each(this.input_subviews, function (childView) { childView.on('focused', self, self.proxy('childFocused')); @@ -540,7 +541,12 @@ var SearchView = Widget.extend({ }); $.when.apply(null, started).then(function () { - _.last(self.input_subviews).$el.focus(); + if (!config.device.isMobile) { + // in mobile mode, we would rathor not focusing manually the + // input, because it opens up the integrated keyboard, which is + // not what you expect when you just selected a filter. + _.last(self.input_subviews).$el.focus(); + } }); }, childFocused: function () { @@ -666,6 +672,18 @@ var SearchView = Widget.extend({ // Private //-------------------------------------------------------------------------- + + /** + * Will return $element where Filters, Group By and Favorite buttons are + * going to be pushed. This method is overriden by the mobile search view + * to add these buttons somewhere else in the dom. + * + * @private + * @returns {jQueryElement} + */ + _getButtonsElement: function () { + return this.$buttons; + }, /** * Processes a fieldsView in place. In particular, parses its arch. * diff --git a/addons/web/static/src/js/chrome/search_view_mobile.js b/addons/web/static/src/js/chrome/search_view_mobile.js new file mode 100644 index 00000000000..852a65c0a37 --- /dev/null +++ b/addons/web/static/src/js/chrome/search_view_mobile.js @@ -0,0 +1,96 @@ + odoo.define('web.SearchViewMobile', function (require) { +"use strict"; + +var config = require('web.config'); +var SearchView = require('web.SearchView'); + +if (!config.device.isMobile) { + return; +} + +SearchView.include({ + template:'SearchViewMobile', + events:_.extend({}, SearchView.prototype.events, { + 'click .o_mobile_search_close, .o_mobile_search_show_result, .o_enable_searchview': '_toggleMobileSearchView', + 'click': '_onOpenMobileSearchView', + 'click .o_mobile_search_clear_facets': '_onEmptyAll', + 'show.bs.dropdown .o_mobile_search_filter .o_dropdown': '_onDropdownToggle', + 'hide.bs.dropdown .o_mobile_search_filter .o_dropdown': '_onDropdownToggle', + }), + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + */ + renderFacets: function () { + this._super.apply(this, arguments); + this.$('.o_mobile_search_clear_facets') + .toggleClass('o_hidden', !this.query.length); + }, + /** + * @override + */ + toggle_visibility: function (is_visible) { + // Do not do anything, toggling visibility of searchview is handled + // explicitly for mobile + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _getButtonsElement: function () { + return this.$('.o_mobile_search_filter'); + }, + /** + * Toggle mobile search view screen + * + * @private + */ + _toggleMobileSearchView: function () { + this.$('.o_enable_searchview').toggleClass('btn-default', !!this.query.length); + this.$('.o_mobile_search').toggleClass('o_hidden'); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Toggle the little arrow in main buttons + * + * @private + * @param {BootstrapEvent} ev + */ + _onDropdownToggle: function (ev) { + $(ev.currentTarget).find('.fa-chevron-right').toggleClass('fa-chevron-down'); + }, + /** + * Clears all filters from the search view + * + * @private + * @param {MouseEvent} event + */ + _onEmptyAll: function () { + this.query.reset(); + }, + /** + * Open the mobile search view screen + * + * @private + * @param {MouseEvent} ev + */ + _onOpenMobileSearchView: function (ev) { + if (ev.target === this.el) { + this._toggleMobileSearchView(); + } + }, +}); + +}); \ No newline at end of file diff --git a/addons/web/static/src/scss/control_panel.scss b/addons/web/static/src/scss/control_panel.scss index f883631c713..ef94b4c0f17 100644 --- a/addons/web/static/src/scss/control_panel.scss +++ b/addons/web/static/src/scss/control_panel.scss @@ -30,9 +30,9 @@ } .btn.o_enable_searchview { - width: 10%; margin-left: 2%; padding: 0px; + background: white; } > .o_cp_left { diff --git a/addons/web/static/src/scss/search_view.scss b/addons/web/static/src/scss/search_view.scss index f2b0f7fd14f..be6cb0b3f5b 100644 --- a/addons/web/static/src/scss/search_view.scss +++ b/addons/web/static/src/scss/search_view.scss @@ -5,84 +5,90 @@ } .o_searchview { - @include o-flex-display; - @include o-flex-flow(row, wrap); @include o-align-items(flex-end); position: relative; padding: 0 20px 1px 0; - .o_searchview_facet { - $o-searchview-facet-remove-width: 18px; - - @include o-flex(0, 0, auto); - max-width: 100%; - @include o-flex-display; - position: relative; - margin: 1px 3px 0 0; - - .o_searchview_facet_label { - @include o-flex(0, 0, auto); - @include o-text-overflow; - padding: 0 3px; - color: white; - - @include o-flex-display; - @include o-align-items(center); - } - - .o_facet_values { - padding: 0 $o-searchview-facet-remove-width 0 5px; - .o_facet_values_sep { - font-style: italic; - } - } - .o_facet_remove { - @include o-flex(0, 0, auto); - @include o-flex-display; - @include o-justify-content(center); - @include o-align-items(center); - @include o-position-absolute(0, 0, 0); - width: $o-searchview-facet-remove-width; - cursor: pointer; - } - } - - .o_searchview_input { - width: 100px; - @include o-flex(1, 0, auto); - } - .o_searchview_more { font-size: 16px; cursor: pointer; } - .o_searchview_autocomplete { - @include o-position-absolute(100%, $left: auto); - width: 100%; + .o_searchview_input_container { + @include o-flex-display; + @include o-flex-flow(row, wrap); - li { - padding-left: 25px; + .o_searchview_facet { + $o-searchview-facet-remove-width: 18px; + @include o-flex(0, 0, auto); + max-width: 100%; + @include o-flex-display; position: relative; - &.o-indent { - padding-left: 50px; + margin: 1px 3px 0 0; + + .o_searchview_facet_label { + @include o-flex(0, 0, auto); + @include o-text-overflow; + padding: 0 3px; + color: white; + + @include o-flex-display; + @include o-align-items(center); } - a { - display: inline-block; - padding-left: 0px; - padding-right: 0px; - &:hover { - background-color: inherit; + + .o_facet_values { + padding: 0 $o-searchview-facet-remove-width 0 5px; + .o_facet_values_sep { + font-style: italic; } - &.o-expand, &.o-expanded { // caret - @include o-position-absolute($left: 6px); - padding: 3px; + } + .o_facet_remove { + @include o-flex(0, 0, auto); + @include o-flex-display; + @include o-justify-content(center); + @include o-align-items(center); + @include o-position-absolute(0, 0, 0); + width: $o-searchview-facet-remove-width; + cursor: pointer; + } + } + .o_searchview_input { + width: 100px; + @include o-flex(1, 0, auto); + } + + .o_searchview_more { + font-size: 16px; + cursor: pointer; + } + + .o_searchview_autocomplete { + @include o-position-absolute(100%, $left: auto); + width: 100%; + + li { + padding-left: 25px; + position: relative; + &.o-indent { + padding-left: 50px; } - &.o-expand:before { - @include o-caret-right; - } - &.o-expanded:before { - @include o-caret-down; + a { + display: inline-block; + padding-left: 0px; + padding-right: 0px; + &:hover { + background-color: inherit; + } + &.o-expand, &.o-expanded { // caret + @include o-position-absolute($left: 6px); + padding: 3px; + } + &.o-expand:before { + @include o-caret-right; + } + &.o-expanded:before { + @include o-caret-down; + } } } } diff --git a/addons/web/static/src/scss/search_view_mobile.scss b/addons/web/static/src/scss/search_view_mobile.scss new file mode 100644 index 00000000000..2f0a3c3fc90 --- /dev/null +++ b/addons/web/static/src/scss/search_view_mobile.scss @@ -0,0 +1,88 @@ +.o_web_client.o_touch_device { + .o_mobile_search { + position: fixed; + top: 0; + left: 0; + bottom: 0; + padding: 0; + width: 100%; + background-color: white; + z-index: 1000; + overflow: auto; + .o_mobile_search_header { + height: 46px; + margin-bottom: 10px; + width: 100%; + background-color: $o-brand-primary; + color: white; + span:active { + background-color: darken($o-brand-primary, 10%); + } + } + .o_searchview_input_container { + display: flex; + padding: 15px 20px 0 20px; + position: relative; + .o_searchview_input { + width: 100%; + margin-bottom: 15px; + border-bottom: 1px solid $o-brand-secondary; + } + .o_searchview_facet { + border-radius: 10px; + display: inline-flex; + order:1; + .o_searchview_facet_label { + border-radius: 2em 0em 0em 2em; + } + } + .o_searchview_autocomplete { + top:100%; + > li { + margin: 5px 0px; + } + } + } + .o_mobile_search_filter { + padding-bottom: 15%; + .o_dropdown { + width: 100%; + margin: 15px 5px 0px 5px; + border: solid 1px darken($gray-lighter, 20%); + + } + .o_dropdown_toggler_btn { + width: 100%; + text-align: left; + } + + // We disable the backdrop in this case because it prevents any + // interaction outside of a dropdown while it is open. + .dropdown-backdrop { + z-index: -1; + } + ul { + position: relative; + width: 100%; + box-shadow: none; + border: none; + color: $gray-light; + .divider { + margin: 0px; + } + > li > a { + padding: 10px 26px; + } + } + } + .o_mobile_search_show_result { + position: fixed; + bottom: 0; + left: 0; + right: 0; + padding: 15px; + font-size: 17px; + z-index: 1000; + } + } +} \ No newline at end of file diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index 20fee925a0f..0debc65dc54 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -1013,7 +1013,26 @@ + + + + + + + + + FILTER + + CLEAR + + + + SEE RESULT + + + + @@ -1031,7 +1050,7 @@ - Filters + Filters @@ -1141,9 +1160,9 @@ - + - Group By + Group By @@ -1164,7 +1183,7 @@ - Favorites + Favorites diff --git a/addons/web/static/tests/chrome/search_view_mobile_tests.js b/addons/web/static/tests/chrome/search_view_mobile_tests.js new file mode 100644 index 00000000000..1508f45952c --- /dev/null +++ b/addons/web/static/tests/chrome/search_view_mobile_tests.js @@ -0,0 +1,83 @@ +odoo.define('web.search_tests', function (require) { +"use strict"; + +var testUtils = require('web.test_utils'); + +var createActionManager = testUtils.createActionManager; + +QUnit.module('Mobile Search view Screen', { + beforeEach: function () { + this.data = { + partner: { + fields: { + foo: {string: "Foo", type: "char"}, + boolean_field: {string: "I am a boolean", type: "boolean"}, + }, + records: [ + {id: 1, display_name: "First record", foo: "yop"}, + ], + }, + }; + + this.actions = [{ + id: 1, + name: 'Partners Action 1', + res_model: 'partner', + type: 'ir.actions.act_window', + views: [[false, 'list']], + }]; + + this.archs = { + // list views + 'partner,false,list': '', + + // search views + 'partner,false,search': '' + + '' + + '' + + '', + }; + }, +}, function () { + QUnit.test('can activate a filter with mobile search view', function (assert) { + assert.expect(3); + + var filterActiveFlag = false; + + var actionManager = createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + mockRPC: function (route, args) { + if (filterActiveFlag) { + assert.deepEqual(args.domain, [['boolean_field', '=', true]], + "domain should have been properly transferred to list view"); + } + return this._super.apply(this, arguments); + }, + }); + + actionManager.doAction(1); + + assert.ok(!$('.o_mobile_search').is(':visible'), + 'mobile search view is not visible'); + // open the search view + $('button.o_enable_searchview').click(); + + assert.ok($('.o_mobile_search').is(':visible'), + 'mobile search view is visible'); + + // open filter sub menu + $('button.o_dropdown_toggler_btn').first().click(); + + filterActiveFlag = true; + + // click on Active filter + $('.o_filters_menu a:contains(Active)').click(); + + actionManager.destroy(); + }); + +}); + +}); diff --git a/addons/web/static/tests/helpers/test_utils.js b/addons/web/static/tests/helpers/test_utils.js index f993a2f9250..2da3eb5ddd9 100644 --- a/addons/web/static/tests/helpers/test_utils.js +++ b/addons/web/static/tests/helpers/test_utils.js @@ -100,6 +100,9 @@ var createActionManager = function (params) { addMockEnvironment(widget, _.defaults(params, {debounce: false})); widget.appendTo($target); widget.$el.addClass('o_web_client'); + if (config.device.isMobile) { + widget.$el.addClass('o_touch_device'); + } var userContext = params.context && params.context.user_context || {}; var actionManager = new ActionManager(widget, userContext); diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml index 6bd1bcfa12b..8e13b294952 100644 --- a/addons/web/views/webclient_templates.xml +++ b/addons/web/views/webclient_templates.xml @@ -155,6 +155,7 @@ + @@ -211,6 +212,7 @@ + @@ -579,6 +581,7 @@ + diff --git a/addons/website/static/src/xml/website.backend.xml b/addons/website/static/src/xml/website.backend.xml index d9095b806a0..347402caf57 100644 --- a/addons/website/static/src/xml/website.backend.xml +++ b/addons/website/static/src/xml/website.backend.xml @@ -61,7 +61,8 @@ - + + Last Week Last Month Last Year