From d8d6311b50d0d3446cab3f3386ad672ac486bab2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Tue, 7 Oct 2014 16:27:43 +0200 Subject: [PATCH] [IMP] rewrite of search view/view manager Second part of a large refactoring. Lots of cleanup, simpler code organization, and it should be faster. --- addons/web/static/src/css/base.css | 554 ++++------- addons/web/static/src/css/base.sass | 499 ++++------ addons/web/static/src/js/search.js | 1404 +++++++++++---------------- addons/web/static/src/js/views.js | 346 +++++-- addons/web/static/src/xml/base.xml | 448 ++++----- 5 files changed, 1428 insertions(+), 1823 deletions(-) diff --git a/addons/web/static/src/css/base.css b/addons/web/static/src/css/base.css index 23f5782300c..61938b8fc59 100644 --- a/addons/web/static/src/css/base.css +++ b/addons/web/static/src/css/base.css @@ -752,59 +752,6 @@ filter: alpha(opacity=50); opacity: 0.5; } -.openerp .oe_sidebar { - white-space: nowrap; -} -.openerp .oe_sidebar .oe_dropdown_menu .oe_sidebar_add_attachment { - height: 20px; - cursor: pointer; - padding-left: 6px; - margin-top: 6px; -} -.openerp .oe_sidebar .oe_dropdown_menu .oe_sidebar_add_attachment span { - font-weight: bold; -} -.openerp .oe_sidebar .oe_dropdown_menu .oe_sidebar_add_attachment .oe_hidden_input_file { - width: 200px; -} -.openerp .oe_sidebar .oe_dropdown_menu .oe_sidebar_add_attachment:hover { - background-color: #efeff8; - background-image: -webkit-gradient(linear, left top, left bottom, from(#f0f0fa), to(#eeeef6)); - background-image: -webkit-linear-gradient(top, #f0f0fa, #eeeef6); - background-image: -moz-linear-gradient(top, #f0f0fa, #eeeef6); - background-image: -ms-linear-gradient(top, #f0f0fa, #eeeef6); - background-image: -o-linear-gradient(top, #f0f0fa, #eeeef6); - background-image: linear-gradient(to bottom, #f0f0fa, #eeeef6); - -moz-box-shadow: none; - -webkit-box-shadow: none; - box-shadow: none; -} -.openerp .oe_sidebar .oe_dropdown_menu li .oe_sidebar_delete_item { - position: absolute; - top: 4px; - right: 4px; - display: none; - width: 12px; - height: 12px; - padding: 1px; - color: #8786b7; - line-height: 8px; - text-align: center; - font-weight: bold; - text-shadow: 0 1px 1px white; -} -.openerp .oe_sidebar .oe_dropdown_menu li .oe_sidebar_delete_item:hover { - text-decoration: none; - color: white; - background: #8786b7; - text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4); - -moz-border-radius: 2px; - -webkit-border-radius: 2px; - border-radius: 2px; -} -.openerp .oe_sidebar .oe_dropdown_menu li:hover .oe_sidebar_delete_item { - display: inline-block; -} .openerp .oe_loading { display: none; z-index: 1001; @@ -1044,6 +991,22 @@ .openerp .oe_application { width: 100%; height: 100%; + overflow: hidden; +} +.openerp .oe_application > div { + position: relative; + height: 100%; +} +.openerp .oe_application .oe-view-manager .oe-view-manager-debug { + margin-right: 5px; +} +.openerp .oe_application .oe-view-manager .oe-view-manager-content { + display: table-row; + height: 100%; +} +.openerp .oe_application .oe-view-manager-body { + position: relative; + height: 100%; } .openerp .oe_application .oe-view-manager-body a { color: #7C7BAD; @@ -1051,42 +1014,26 @@ .openerp .oe_application .oe-view-manager-body .oe-view-manager-view-kanban:not(:empty) { height: 100%; } -.openerp .oe_application > div { - position: relative; - height: 100%; -} -.openerp .oe_application > div > .oe_view_manager > .oe_view_manager_wrapper { - display: table-row; - height: 100%; -} -.openerp .oe_application > div > .oe_view_manager > .oe_view_manager_wrapper > div { - position: relative; - height: 100%; -} -.openerp .oe_application > div > .oe_view_manager > .oe_view_manager_wrapper > div > .oe_view_manager_body { +.openerp .oe_application .oe-view-manager-body > div { position: absolute; - position: static\9; top: 0; bottom: 0; left: 0; right: 0; overflow: auto; } -.openerp .oe_application .oe_breadcrumb_item:not(:last-child) { - max-width: 7em; - white-space: nowrap; - text-overflow: ellipsis; +.openerp .oe_application .oe_form_field_one2many .oe-view-manager-views { + position: initial; } -.openerp .oe_application .oe_breadcrumb_title > * { - display: inline-block; - overflow: hidden; - font-weight: bold; +.openerp .oe-view-manager { + display: table; + width: 100%; + height: 100%; } .openerp .oe-view-manager-header { - height: 90px; - overflow: visible; - border-bottom: 1px solid black; - display: none; + display: table-row; + background-color: #f0eeee; + border-bottom: 1px solid #afafb6; } .openerp .oe-view-manager-header .oe-right-toolbar { float: right; @@ -1095,26 +1042,34 @@ display: inline-block; } .openerp .oe-view-manager-header .row { - height: 45px; -} -.openerp .oe-view-manager-header .oe-view-title { - float: left; - font-size: 18px; - font-weight: bold; margin: 0; } -.openerp .oe-view-manager-header .oe-header-title { +.openerp .oe-view-manager-header .row:first-child { + padding-top: 3px; + padding-bottom: 3px; +} +.openerp .oe-view-manager-header .row:last-child { + padding-bottom: 10px; +} +.openerp .oe-view-manager-header .oe_tag { + -moz-border-radius: 0px; + -webkit-border-radius: 0px; + border-radius: 0px; +} +.openerp .oe-view-manager-header .oe-view-title { + font-size: 18px; padding-left: 0; + margin: 0; + background-color: #f0eeee; } .openerp .oe-view-manager-header .oe-view-manager-search-view { - float: right; padding-top: 5px; } .openerp .oe-view-manager-header .oe-view-manager-switch .oe-vm-switch-kanban:before { content: ""; } .openerp .oe-view-manager-header .oe-view-manager-switch .oe-vm-switch-list:before { - content: ""; + content: ""; } .openerp .oe-view-manager-header .oe-view-manager-switch .oe-vm-switch-form:before { content: ""; @@ -1128,14 +1083,27 @@ .openerp .oe-view-manager-header .oe-view-manager-switch .oe-vm-switch-gantt:before { content: ""; } +.openerp .oe-view-manager-header .oe-view-manager-switch .oe-vm-switch-pivot:before { + content: ""; +} .openerp .oe-view-manager-header .oe-view-manager-buttons { - float: left; + display: inline-block; } .openerp .oe-view-manager-header .oe-view-manager-buttons > div { display: none; } -.openerp .oe-view-manager-header .oe_pager_value { +.openerp .oe-view-manager-header .oe-view-manager-sidebar { display: inline-block; + float: right; +} +.openerp .oe-view-manager-header .oe_form_buttons { + padding: 0; +} +.openerp .oe-view-manager-header .oe-pager-buttons { + min-height: 30px; +} +.openerp .oe_view_manager_inline > .oe-view-manager-header, .openerp .oe_view_manager_inlineview > .oe-view-manager-header { + display: none; } .openerp .oe_application > div > div > .oe-view-manager-body { position: absolute; @@ -1145,12 +1113,6 @@ right: 0; overflow: auto; } -.openerp .oe_view_manager_inline, .openerp .oe_view_manager_inlineview { - height: 100%; -} -.openerp .oe_view_manager_inline > .oe_view_manager_header, .openerp .oe_view_manager_inlineview > .oe_view_manager_header { - display: none; -} .openerp .oe_popup_form { display: table; } @@ -1163,71 +1125,33 @@ .openerp .oe_popup_list_pager { float: right; } +.openerp .oe_popup_search { + width: 50%; + float: right; +} .openerp .oe_searchview { cursor: text; position: relative; - padding: 1px 0; - min-width: 400px; - width: 400px; - -moz-border-radius: 13px; - -webkit-border-radius: 13px; - border-radius: 13px; -} -.openerp .oe_searchview input, .openerp .oe_searchview textarea { - padding: 3px; - height: 14px; - font-size: 12px; - line-height: 18px; -} -.openerp .oe_searchview input:not([type]), .openerp .oe_searchview input[type="text"], .openerp .oe_searchview input[type="number"] { - width: 156px; - height: 22px; -} -.openerp .oe_searchview input[type="checkbox"] { - margin: 3px 3px 3px 4px; -} -.openerp .oe_searchview select { - margin: 2px 4px 2px 0; -} -.openerp .oe_searchview.oe_focused { - border-color: #a6a6fe; - -moz-box-shadow: 0 1px 2px #a6a6fe inset; - -webkit-box-shadow: 0 1px 2px #a6a6fe inset; - box-shadow: 0 1px 2px #a6a6fe inset; -} -.openerp .oe_searchview .oe_searchview_clear { - cursor: pointer; - position: absolute; - top: 3px; - right: 18px; - width: 15px; - height: 24px; + display: none; + height: auto; + padding-top: 1px; + padding-bottom: 1px; } .openerp .oe_searchview .oe_searchview_unfold_drawer { position: absolute; - top: 0; - right: 0; - color: #ccc; + top: 8px; + left: 5px; cursor: pointer; } -.openerp .oe_searchview .oe_searchview_unfold_drawer:hover { - color: #999; -} -.openerp .oe_searchview .oe_searchview_unfold_drawer:before { - position: absolute; - top: 7px; - right: 18px; - width: 0; -} .openerp .oe_searchview .oe_searchview_search { cursor: pointer; position: absolute; - left: 6px; + right: 6px; top: 8px; } .openerp .oe_searchview .oe_searchview_facets { min-height: 22px; - margin: 2px 35px 0 20px; + margin: 2px 20px 0 10px; } .openerp .oe_searchview .oe_searchview_facets * { vertical-align: top; @@ -1235,18 +1159,21 @@ font-size: 100%; } .openerp .oe_searchview .oe_searchview_facets .oe_searchview_facet { - height: 20px; margin: 1px 0; font-size: 11px; } .openerp .oe_searchview .oe_searchview_facets .oe_searchview_facet:focus { outline: none; } +.openerp .oe_searchview .oe_searchview_facets .oe_searchview_facet .label { + -moz-border-radius: 0px; + -webkit-border-radius: 0px; + border-radius: 0px; +} .openerp .oe_searchview .oe_searchview_facets .oe_searchview_input { padding: 0 0 0 6px; font-size: 12px; height: 16px; - margin-top: 3px; } .openerp .oe_searchview .oe_searchview_facets .oe_searchview_input:focus { outline: none; @@ -1257,9 +1184,6 @@ padding: 0; -webkit-font-smoothing: auto; } -.openerp .oe_searchview .oe_searchview_facets .oe_searchview_facet span { - height: 18px; -} .openerp .oe_searchview .oe_searchview_facets .oe_searchview_facet:focus { border-color: #a6a6fe; -moz-box-shadow: 0 0 3px 1px #a6a6fe; @@ -1280,11 +1204,15 @@ font-size: 16px; } .openerp .oe_searchview .oe_searchview_facets .oe_searchview_facet .oe_facet_value { + border-left: 1px solid #afafb6; color: #4C4C4C; } .openerp .oe_searchview .oe_searchview_facets .oe_searchview_facet .oe_facet_value:last-child { padding-right: 16px; } +.openerp .oe_searchview .oe_searchview_facets .oe_searchview_facet .oe_facet_value:first-child { + border-left: none; +} .openerp .oe_searchview .oe_searchview_facets .oe_searchview_facet .oe_facet_remove { position: absolute; top: 3px; @@ -1310,8 +1238,8 @@ .openerp .oe_searchview .oe-autocomplete { display: none; position: absolute; - width: 300px; background-color: white; + width: 400px; border: 1px solid #afafb6; z-index: 666; margin-top: 2px; @@ -1319,9 +1247,6 @@ -moz-border-radius: 3px; -webkit-border-radius: 3px; border-radius: 3px; - -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); - -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); } .openerp .oe_searchview .oe-autocomplete ul { list-style-type: none; @@ -1353,216 +1278,133 @@ .openerp .oe_searchview .oe-autocomplete ul li.oe-separator:last-child { display: none; } -.openerp .oe_searchview_drawer_container { - overflow: auto; -} -.openerp .oe_searchview_drawer { - display: none; +.openerp .oe-search-options a { + padding-left: 25px; + padding-right: 25px; width: 100%; - cursor: default; - display: none; - overflow: hidden; - border-bottom: 1px solid #afafb6; - text-align: left; - padding: 8px 0; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; } -.openerp .oe_searchview_drawer .badge { - font-size: 12px; - line-height: 12px; +.openerp .oe-search-options .oe-apply-filter { + margin-left: 25px; } -.openerp .oe_searchview_drawer > div:first-child { - border: none; - padding-left: 0; -} -.openerp .oe_searchview_drawer > div:first-child li:hover:not(.badge) { - background-color: #f0f0fa; -} -.openerp .oe_searchview_drawer .col-md-5 { - padding-left: 0; -} -.openerp .oe_searchview_drawer dl { - margin-bottom: 0; -} -.openerp .oe_searchview_drawer dt { - color: #7C7BAD; - font-size: 13px; - line-height: 24px; -} -.openerp .oe_searchview_drawer dd { - line-height: 24px; - font-size: 13px; - padding-top: 3px; -} -.openerp .oe_searchview_drawer h4, .openerp .oe_searchview_drawer h4 * { - margin: 0 0 0 2px; - padding-left: 20px; - cursor: pointer; - font-weight: normal; - display: inline-block; -} -.openerp .oe_searchview_drawer h4:hover, .openerp .oe_searchview_drawer h4 *:hover { - background-color: #f0f0fa; -} -.openerp .oe_searchview_drawer h4:before { - content: "▸ "; - color: #a3a3a3; -} -.openerp .oe_searchview_drawer button { - margin: 4px 0; -} -.openerp .oe_searchview_drawer .button { - border: none; - background: transparent; - padding: 0 2px; - -moz-box-shadow: none; - -webkit-box-shadow: none; - box-shadow: none; - -moz-border-radius: 0; - -webkit-border-radius: 0; - border-radius: 0; -} -.openerp .oe_searchview_drawer .oe_searchview_section ul { - margin: 0 8px; - padding: 0; - list-style: none; - display: inline; -} -.openerp .oe_searchview_drawer .oe_searchview_section li { - display: inline-block; - cursor: pointer; - position: relative; - margin-right: 8px; -} -.openerp .oe_searchview_drawer .oe_searchview_section li > span { - display: inline-block; - max-width: 250px; - text-overflow: ellipsis; - vertical-align: bottom; - overflow: hidden; -} -.openerp .oe_searchview_drawer form { - margin-left: 12px; -} -.openerp .oe_searchview_drawer form p { - margin: 4px 0; - line-height: 18px; -} -.openerp .oe_searchview_drawer form button { - margin: 0 0 8px -3px; -} -.openerp .oe_searchview_drawer .oe_searchview_savefilter form { +.openerp .oe-search-options .oe-add-filter-menu { display: none; } -.openerp .oe_searchview_drawer .oe_searchview_custom { - display: none; -} -.openerp .oe_searchview_drawer .oe_searchview_custom li { - cursor: pointer; - position: relative; - line-height: 14px; - margin-right: 0; -} -.openerp .oe_searchview_drawer .oe_searchview_custom li button { - position: absolute; - top: 0; - right: 5px; -} -.openerp .oe_searchview_drawer .oe_searchview_custom li a { +.openerp .oe-search-options .oe-add-condition { + width: auto; margin-left: 10px; - position: inherit; - visibility: hidden; - display: inline-block; + padding-left: 0; + padding-right: 10px; } -.openerp .oe_searchview_drawer .oe_searchview_custom li span:hover:not(.badge) { - background-color: #f0f0fa; -} -.openerp .oe_searchview_drawer .oe_searchview_custom li:hover a { - visibility: visible; -} -.openerp .oe_searchview_drawer .oe_searchview_custom label { - font-weight: normal; -} -.openerp .oe_searchview_drawer .oe_searchview_dashboard form { - display: none; - margin-top: 2px; -} -.openerp .oe_searchview_drawer .oe_searchview_advanced { - overflow: auto; -} -.openerp .oe_searchview_drawer .oe_searchview_advanced form { - display: none; - margin-top: 8px; -} -.openerp .oe_searchview_drawer .oe_searchview_advanced button.oe_add_condition:before { - content: "Z"; - font-family: "entypoRegular" !important; - font-size: 24px; - font-weight: 300 !important; - margin-right: 4px; -} -.openerp .oe_searchview_drawer .oe_searchview_advanced ul { - list-style: none; - padding: 0; -} -.openerp .oe_searchview_drawer .oe_searchview_advanced li { - position: relative; - list-style: none; - margin: 0; - white-space: nowrap; -} -.openerp .oe_searchview_drawer .oe_searchview_advanced li:first-child .searchview_extended_prop_or { - visibility: hidden; - margin-left: -14px; -} -.openerp .oe_searchview_drawer .oe_searchview_advanced .searchview_extended_prop_or { - opacity: 0.5; - margin-left: -14px; -} -.openerp .oe_searchview_drawer .oe_opened h4:before { - content: "▾ "; - position: relative; - top: -1px; -} -.openerp .oe_searchview_drawer .oe_opened form { +.openerp .oe-search-options .selected { display: block; } -.openerp .oe_searchview_drawer .oe_searchview_custom_delete, .openerp .oe_searchview_drawer .searchview_extended_delete_prop { - display: inline-block; - width: 12px; - height: 12px; - line-height: 12px; - padding: 1px; - color: #8786b7; - line-height: 8px; - text-align: center; +.openerp .oe-search-options .selected a { font-weight: bold; - text-shadow: 0 1px 1px white; } -.openerp .oe_searchview_drawer .oe_searchview_custom_delete:hover, .openerp .oe_searchview_drawer .searchview_extended_delete_prop:hover { - text-decoration: none; - color: white; - background: #8786b7; - text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4); - -moz-border-radius: 2px; - -webkit-border-radius: 2px; - border-radius: 2px; -} -.openerp .oe_searchview_drawer .oe_searchview_custom_delete { - display: none; +.openerp .oe-search-options .selected a:before { + font-family: FontAwesome; position: absolute; - bottom: 1px; - right: 4px; + left: 6px; + top: 3px; + content: ""; } -.openerp .oe_searchview_drawer .oe_searchview_custom_private:hover .oe_searchview_custom_delete, .openerp .oe_searchview_drawer .oe_searchview_custom_public:hover .oe_searchview_custom_delete { +.openerp .oe-search-options .oe-group-selector { + margin: 3px 25px; + min-width: calc(100% - 40px); + max-width: 250px; + width: auto; +} +.openerp .oe-search-options .divider { + display: none; +} +.openerp .oe-search-options .filters-menu .divider { + display: list-item; +} +.openerp .oe-search-options .closed-menu a:before { + font-family: FontAwesome; + position: absolute; + left: 12px; + top: 3px; + content: ""; +} +.openerp .oe-search-options .open-menu a:before { + font-family: FontAwesome; + position: absolute; + left: 9px; + top: 3px; + content: ""; +} +.openerp .oe-search-options .oe-select-group { + margin: 3px 25px; +} +.openerp .oe-search-options .oe-add-group { + display: none; +} +.openerp .oe-search-options .oe-save-name { + display: none; + margin: 3px 25px; +} +.openerp .oe-search-options .oe-save-name span { + white-space: nowrap; +} +.openerp .oe-search-options .oe-save-name span input { + margin-left: 0; +} +.openerp .oe-search-options .searchview_extended_prop_field { + display: inline; + width: calc(100% - 20px); +} +.openerp .oe-search-options .searchview_extended_prop_op { + margin: 3px 0; + width: calc(100% - 20px); +} +.openerp .oe-search-options .searchview_extended_delete_prop { + float: right; + display: inline; +} +.openerp .oe-search-options .searchview_extended_prop_value { + width: calc(100% - 20px); +} +.openerp .oe-search-options .searchview_extended_prop_value > select, .openerp .oe-search-options .searchview_extended_prop_value > input { + width: calc(100% - 20px); +} +.openerp .oe-search-options .searchview_extended_prop_value .oe_datepicker_main { + width: calc(100% - 20px); +} +.openerp .oe-search-options .searchview_extended_prop_value .oe_datepicker_master { + width: 100%; +} +.openerp .oe-search-options .searchview_extended_prop_value .fa-calendar { + margin-left: -21px; + cursor: pointer; +} +.openerp .oe-search-options span.fa-trash-o { + position: absolute; + top: 6px; + right: 10px; + cursor: pointer; +} +.openerp .oe-search-options .dropdown-menu > li { + position: relative; + white-space: nowrap; +} +.openerp .oe-search-options .dropdown-menu li.oe-filter-condition { + white-space: normal; + padding-left: 25px; + padding-right: 10px; + margin-bottom: 5px; +} +.openerp .oe-search-options a { display: inline-block; } -.openerp .oe_searchview_drawer .oe_searchview_custom_public:after { - content: ","; - font-family: "entypoRegular" !important; - font-size: 22px; - font-weight: 300 !important; - margin: 0 0 0 4px; - padding: 0; +.openerp input.oe-save-name { + width: auto; +} +.openerp .oe_pager_value { + display: inline-block; } .openerp .oe_view_nocontent { padding: 15px; @@ -1996,6 +1838,10 @@ .openerp .oe_form .oe_datepicker_root { display: inline-block; } +.openerp .oe_form .oe_datepicker_root .datepickerbutton { + margin-left: 5px; + cursor: pointer; +} .openerp .oe_form .oe_form_required input:not([disabled]):not([readonly]), .openerp .oe_form .oe_form_required select:not([disabled]):not([readonly]), .openerp .oe_form .oe_form_required textarea:not([disabled]):not([readonly]) { background-color: #D2D2FF !important; } @@ -2737,6 +2583,8 @@ } .openerp .oe_debug_view { float: left; + margin-top: 5px; + width: auto; } .openerp .oe_debug_view_log { font-size: 95%; @@ -2796,9 +2644,6 @@ font-size: 11px; background-color: #7c7bad; } -.openerp button, .openerp body { - line-height: normal; -} .openerp h1, .openerp h2 { font-weight: bold; } @@ -2836,9 +2681,6 @@ } @-moz-document url-prefix() { - .openerp .oe_searchview .oe_searchview_search { - top: -1px; - } .openerp .oe_form_field_many2one .oe_m2o_cm_button { line-height: 18px; } diff --git a/addons/web/static/src/css/base.sass b/addons/web/static/src/css/base.sass index e39f04f7559..845c00588b5 100644 --- a/addons/web/static/src/css/base.sass +++ b/addons/web/static/src/css/base.sass @@ -653,44 +653,6 @@ $sheet-padding: 16px border-right: 4px solid transparent border-top: 4px solid#404040 @include opacity(0.5) - .oe_sidebar - white-space: nowrap - .oe_dropdown_menu - .oe_sidebar_add_attachment - height: 20px - cursor: pointer - padding-left: 6px - margin-top: 6px - span - font-weight: bold - .oe_hidden_input_file - width: 200px - &:hover - @include vertical-gradient(#f0f0fa, #eeeef6) - @include box-shadow(none) - li - .oe_sidebar_delete_item - position: absolute - top: 4px - right: 4px - display: none - width: 12px - height: 12px - padding: 1px - color: #8786b7 - line-height: 8px - text-align: center - font-weight: bold - text-shadow: 0 1px 1px white - &:hover - text-decoration: none - color: white - background: #8786b7 - text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4) - @include radius(2px) - &:hover - .oe_sidebar_delete_item - display: inline-block // }}} // Loading {{{ .oe_loading @@ -899,67 +861,70 @@ $sheet-padding: 16px .oe_application width: 100% height: 100% + overflow: hidden + > div + position: relative + height: 100% + .oe-view-manager + .oe-view-manager-debug + margin-right: 5px + .oe-view-manager-content + display: table-row + height: 100% + .oe-view-manager-body + position: relative + height: 100% a color: $link-color .oe-view-manager-view-kanban:not(:empty) height: 100% - > div - position: relative - height: 100% - > .oe_view_manager - > .oe_view_manager_wrapper - display: table-row - height: 100% - > div - position: relative - height: 100% - > .oe_view_manager_body - position: absolute - position: static\9 - top: 0 - bottom: 0 - left: 0 - right: 0 - overflow: auto + > div + position: absolute + top: 0 + bottom: 0 + left: 0 + right: 0 + overflow: auto + .oe_form_field_one2many + .oe-view-manager-views + position: initial - - .oe_breadcrumb_item:not(:last-child) - max-width: 7em - white-space: nowrap - text-overflow: ellipsis - .oe_breadcrumb_title > * - display: inline-block - overflow: hidden - font-weight: bold // }}} // ViewManager common {{{ + .oe-view-manager + display: table + width: 100% + height: 100% .oe-view-manager-header - height: 90px - overflow: visible - border-bottom: 1px solid black - display: none + display: table-row + background-color: rgb(240, 238, 238) + border-bottom: 1px solid #afafb6 .oe-right-toolbar float: right > div display: inline-block .row - height: 45px - .oe-view-title - float: left - font-size: 18px - font-weight: bold margin: 0 - .oe-header-title + .row:first-child + padding-top: 3px + padding-bottom: 3px + .row:last-child + padding-bottom: 10px + .oe_tag + @include radius(0px) + .oe-view-title + font-size: 18px padding-left: 0 + margin: 0 + background-color: rgb(240, 238, 238) .oe-view-manager-search-view - float: right padding-top: 5px .oe-view-manager-switch .oe-vm-switch-kanban:before content: "\f009" .oe-vm-switch-list:before - content: "\f0c9" + content: "" .oe-vm-switch-form:before content: "\f044" .oe-vm-switch-graph:before @@ -968,12 +933,22 @@ $sheet-padding: 16px content: "\f073" .oe-vm-switch-gantt:before content: "\f0ae" + .oe-vm-switch-pivot:before + content: "\f0ce" .oe-view-manager-buttons - float: left + display: inline-block > div display: none - .oe_pager_value - display: inline-block + .oe-view-manager-sidebar + display: inline-block + float: right + .oe_form_buttons + padding: 0 + .oe-pager-buttons + min-height: 30px + .oe_view_manager_inline, .oe_view_manager_inlineview + > .oe-view-manager-header + display: none .oe_application > div > div @@ -985,26 +960,6 @@ $sheet-padding: 16px right: 0 overflow: auto // }}} - // ViewManager application {{{ - // .oe_view_manager_current - // height: 100% - // > .oe_view_manager_header - // border-bottom: 1px solid #cacaca - // @include vertical-gradient(#fcfcfc, #dedede) - // @include box-shadow((0 1px 0 rgba(255,255,255,0.4), 0 0 9px rgba(0,0,0,0.1))) - // .oe_header_row - // td - // padding: 8px - // .oe_header_row:first-child - // td - // padding-top: 8px - // }}} - // ViewManager inline {{{ - .oe_view_manager_inline, .oe_view_manager_inlineview - height: 100% - > .oe_view_manager_header - display: none - // }}} // FormPopup {{{ .oe_popup_form display: table @@ -1016,77 +971,48 @@ $sheet-padding: 16px // End of customize .oe_popup_list_pager float: right + .oe_popup_search + width: 50% + float: right // }}} // SearchView {{{ .oe_searchview cursor: text position: relative - padding: 1px 0 - min-width: 400px - width: 400px - @include radius(13px) - input, textarea - padding: 3px - height: 14px - font-size: 12px - line-height: 18px - //Customize searchview input, select - input:not([type]), input[type="text"], input[type="number"] - width: 156px - height: 22px - input[type="checkbox"] - margin: 3px 3px 3px 4px - select - margin: 2px 4px 2px 0 - //End of customize - &.oe_focused - border-color: $tag-border-selected - @include box-shadow(0 1px 2px $tag-border-selected inset) - .oe_searchview_clear - cursor: pointer - position: absolute - top: 3px - right: 18px - width: 15px - height: 24px + display: none + height: auto + padding-top: 1px + padding-bottom: 1px .oe_searchview_unfold_drawer position: absolute - top: 0 - right: 0 - color: #ccc + top: 8px + left: 5px cursor: pointer - &:hover - color: #999 - &:before - position: absolute - top: 7px - right: 18px - width: 0 .oe_searchview_search cursor: pointer position: absolute - left: 6px + right: 6px top: 8px .oe_searchview_facets min-height: 22px - margin: 2px 35px 0 20px + margin: 2px 20px 0 10px * vertical-align: top display: inline-block font-size: 100% .oe_searchview_facet - height: 20px margin: 1px 0 font-size: 11px &:focus outline: none + .label + @include radius(0px) .oe_searchview_input padding: 0 0 0 6px font-size: 12px height: 16px - margin-top: 3px &:focus outline: none .oe_searchview_facet @@ -1095,8 +1021,6 @@ $sheet-padding: 16px padding: 0 -webkit-font-smoothing: auto // spacing for opera, FF - span - height: 18px &:focus border-color: $tag-border-selected @include box-shadow(0 0 3px 1px $tag-border-selected) @@ -1110,9 +1034,12 @@ $sheet-padding: 16px .oe_facet_category.oe_i font-size: 16px .oe_facet_value + border-left: 1px solid #afafb6 color: #4C4C4C &:last-child padding-right: 16px + &:first-child + border-left: none .oe_facet_remove position: absolute top: 3px @@ -1135,14 +1062,13 @@ $sheet-padding: 16px .oe-autocomplete display: none position: absolute - width: 300px background-color: white + width: 400px border: 1px solid #afafb6 z-index: 666 margin-top: 2px cursor: default @include radius(3px) - @include box-shadow(0 1px 4px rgba(0, 0, 0, 0.3)) ul list-style-type: none padding-left: 0 @@ -1166,180 +1092,112 @@ $sheet-padding: 16px li.oe-separator:last-child display: none - - .oe_searchview_drawer_container - overflow: auto - .oe_searchview_drawer - display: none - width: 100% - cursor: default - display: none - overflow: hidden - border-bottom: 1px solid $tag-border - text-align: left - padding: 8px 0 - .badge - font-size: 12px - line-height: 12px - > div:first-child - border: none - padding-left: 0 - li:hover:not(.badge) - background-color: $hover-background - .col-md-5 - padding-left: 0 - dl - margin-bottom: 0 - dt - color: $section-title-color - font-size: 13px - line-height: 24px - dd - line-height: 24px - font-size: 13px - padding-top: 3px - h4, h4 * - margin: 0 0 0 2px - padding-left: 20px - cursor: pointer - font-weight: normal - display: inline-block - &:hover - background-color: $hover-background - h4:before - content: "▸ " - color: #a3a3a3 - button - margin: 4px 0 - .button - border: none - background: transparent - padding: 0 2px - @include box-shadow(none) - @include radius(0) - .oe_searchview_section - ul - margin: 0 8px - padding: 0 - list-style: none - display: inline - li - display: inline-block - cursor: pointer - position: relative - margin-right: 8px - > span - display: inline-block - max-width: 250px - text-overflow: ellipsis - vertical-align: bottom - overflow: hidden - form - margin-left: 12px - p - margin: 4px 0 - line-height: 18px - button - margin: 0 0 8px -3px // Managed margin-left according bootstrap3 - .oe_searchview_savefilter - form - display: none - .oe_searchview_custom + .oe-search-options + a + padding-left: 25px + padding-right: 25px + width: 100% + -webkit-user-select: none + -moz-user-select: none + user-select: none + .oe-apply-filter + margin-left: 25px + .oe-add-filter-menu display: none - li - cursor: pointer - position: relative - line-height: 14px - margin-right: 0 - button - position: absolute - top: 0 - right: 5px - a - margin-left: 10px - position: inherit - visibility: hidden - display: inline-block - span:hover:not(.badge) - background-color: $hover-background - li:hover a - visibility: visible - //Customize for searchview label - label - font-weight: normal - //End of Customize - .oe_searchview_dashboard - form - display: none - margin-top: 2px + .oe-add-condition + width: auto + margin-left: 10px + padding-left: 0 + padding-right: 10px + .selected + display: block + a + font-weight: bold + a:before + font-family: FontAwesome + position: absolute + left: 6px + top: 3px + content: "\f00c" + .oe-group-selector + margin: 3px 25px + min-width: calc(100% - 40px) + max-width: 250px + width: auto + .divider + display: none + .filters-menu + .divider + display: list-item + .closed-menu + a:before + font-family: FontAwesome + position: absolute + left: 12px + top: 3px + content: "\f0da" + .open-menu + a:before + font-family: FontAwesome + position: absolute + left: 9px + top: 3px + content: "\f0d7" - .oe_searchview_advanced - overflow: auto - form - display: none - margin-top: 8px - button.oe_add_condition:before - content: "Z" - font-family: "entypoRegular" !important - font-size: 24px - font-weight: 300 !important - margin-right: 4px - ul - list-style: none - padding: 0 - li - position: relative - list-style: none - margin: 0 + .oe-select-group + margin: 3px 25px + .oe-add-group + display: none + .oe-save-name + display: none + margin: 3px 25px + span white-space: nowrap - &:first-child .searchview_extended_prop_or - visibility: hidden - margin-left: -14px - .searchview_extended_prop_or - opacity: 0.5 - margin-left: -14px //Customize 'or' in searchview - .oe_opened - h4:before - content: "▾ " - position: relative - top: -1px - form - display: block - - // delete buttons - .oe_searchview_custom_delete, .searchview_extended_delete_prop - display: inline-block - width: 12px - height: 12px - line-height: 12px - padding: 1px - color: #8786b7 - line-height: 8px - text-align: center - font-weight: bold - text-shadow: 0 1px 1px white - &:hover - text-decoration: none - color: white - background: #8786b7 - text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4) - @include radius(2px) - .oe_searchview_custom_delete - display: none + input + margin-left: 0 + .searchview_extended_prop_field + display: inline + width: calc(100% - 20px) + + .searchview_extended_prop_op + margin: 3px 0 + width: calc(100% - 20px) + .searchview_extended_delete_prop + float: right + display: inline + .searchview_extended_prop_value + width: calc(100% - 20px) + > select, > input + width: calc(100% - 20px) + .oe_datepicker_main + width: calc(100% - 20px) + .oe_datepicker_master + width: 100% + .fa-calendar + margin-left: -21px + cursor: pointer + span.fa-trash-o position: absolute - bottom: 1px - right: 4px - .oe_searchview_custom_private, .oe_searchview_custom_public - &:hover - .oe_searchview_custom_delete - display: inline-block - .oe_searchview_custom_public:after - content: "," - font-family: "entypoRegular" !important - font-size: 22px - font-weight: 300 !important - margin: 0 0 0 4px - padding: 0 + top: 6px + right: 10px + cursor: pointer + .dropdown-menu + > li + position: relative + white-space: nowrap + li.oe-filter-condition + white-space: normal + padding-left: 25px + padding-right: 10px + margin-bottom: 5px + a + display: inline-block + + input.oe-save-name + width: auto + .oe_pager_value + display: inline-block + // }}} // Views Common {{{ .oe_view_nocontent @@ -1674,6 +1532,9 @@ $sheet-padding: 16px width: auto .oe_datepicker_root display: inline-block + .datepickerbutton + margin-left: 5px + cursor: pointer .oe_form_required input:not([disabled]):not([readonly]), select:not([disabled]):not([readonly]), textarea:not([disabled]):not([readonly]) background-color: #D2D2FF !important @@ -2250,6 +2111,8 @@ $sheet-padding: 16px content: "[newline]" .oe_debug_view float: left + margin-top: 5px + width: auto .oe_debug_view_log font-size: 95% line-height: 1.2em @@ -2300,8 +2163,6 @@ $sheet-padding: 16px background-color: #7c7bad // Customize for global tags - button, body - line-height: normal h1,h2 font-weight: bold h3 @@ -2337,8 +2198,6 @@ $sheet-padding: 16px @-moz-document url-prefix() .openerp - .oe_searchview .oe_searchview_search - top: -1px .oe_form_field_many2one .oe_m2o_cm_button line-height: 18px .oe_webclient diff --git a/addons/web/static/src/js/search.js b/addons/web/static/src/js/search.js index c07a82620a9..29d345b70f2 100644 --- a/addons/web/static/src/js/search.js +++ b/addons/web/static/src/js/search.js @@ -330,18 +330,16 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea } }, // search button - 'click i.oe_searchview_search': function (e) { + 'click div.oe_searchview_search': function (e) { e.stopImmediatePropagation(); this.do_search(); }, - 'click .oe_searchview_clear': function (e) { - e.stopImmediatePropagation(); - this.query.reset(); - }, 'click .oe_searchview_unfold_drawer': function (e) { e.stopImmediatePropagation(); - if (this.drawer) - this.drawer.toggle(); + $(e.target).toggleClass('fa-chevron-right') + .toggleClass('fa-chevron-down'); + localStorage.visible_search_menu = !(localStorage.visible_search_menu === 'true'); + this.toggle_buttons(); }, 'keydown .oe_searchview_input, .oe_searchview_facet': function (e) { switch(e.which) { @@ -379,88 +377,195 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea disable_custom_filters: false, }); this._super(parent); + this.query = undefined; this.dataset = dataset; - this.model = dataset.model; this.view_id = view_id; - + this.search_fields = []; + this.filters = []; + this.groupbys = []; + this.visible_filters = !!(localStorage.visible_search_menu === 'true'); + this.input_subviews = []; // for user input in searchbar this.defaults = defaults || {}; - this.has_defaults = !_.isEmpty(this.defaults); + this.headless = this.options.hidden && _.isEmpty(this.defaults); + this.$buttons = this.options.$buttons; - this.headless = this.options.hidden && !this.has_defaults; - - this.input_subviews = []; - this.view_manager = null; - this.$view_manager_header = null; - - this.ready = $.Deferred(); - this.drawer_ready = $.Deferred(); - this.fields_view_get = $.Deferred(); - this.drawer = new instance.web.SearchViewDrawer(parent, this); - - }, + this.filter_menu = undefined; + this.groupby_menu = undefined; + this.favorite_menu = undefined + }, start: function() { - var self = this; - var p = this._super(); - - this.$view_manager_header = this.$el.parents(".oe_view_manager_header").first(); - + if (this.headless) { + this.$el.hide(); + } + this.toggle_visibility(false); + this.$facets_container = this.$('div.oe_searchview_facets'); this.setup_global_completion(); this.query = new my.SearchQuery() .on('add change reset remove', this.proxy('do_search')) .on('change', this.proxy('renderChangedFacets')) .on('add reset remove', this.proxy('renderFacets')); - - if (this.options.hidden) { - this.$el.hide(); - } - if (this.headless) { - this.ready.resolve(); - } else { - var load_view = instance.web.fields_view_get({ - model: this.dataset._model, - view_id: this.view_id, - view_type: 'search', - context: this.dataset.get_context(), - }); - - this.alive($.when(load_view)).then(function (r) { - self.fields_view_get.resolve(r); - return self.search_view_loaded(r); - }).fail(function () { - self.ready.reject.apply(null, arguments); - }); - } - - var view_manager = this.getParent(); - while (!(view_manager instanceof instance.web.ViewManager) && - view_manager && view_manager.getParent) { - view_manager = view_manager.getParent(); - } - - if (view_manager) { - this.view_manager = view_manager; - view_manager.on('switch_mode', this, function (e) { - self.drawer.toggle(false); - }); - } - return $.when(p, this.ready); + var load_view = instance.web.fields_view_get({ + model: this.dataset._model, + view_id: this.view_id, + view_type: 'search', + context: this.dataset.get_context(), + }); + this.$('.oe_searchview_unfold_drawer') + .toggleClass('fa-chevron-right', !this.visible_filters) + .toggleClass('fa-chevron-down', this.visible_filters); + return $.when(this._super(), this.alive($.when(load_view)) + .then(this.view_loaded.bind(this))); }, + view_loaded: function (r) { + var self = this; + this.fields_view_get = r; + this.prepare_search_inputs(); + if (this.$buttons) { - set_drawer: function (drawer) { - this.drawer = drawer; + var fields_def = new instance.web.Model(this.dataset.model).call('fields_get', { + context: this.dataset.context + }); + + this.groupby_menu = new my.GroupByMenu(this, this.groupbys, fields_def); + this.filter_menu = new my.FilterMenu(this, this.filters, fields_def); + this.favorite_menu = new my.FavoriteMenu(this, this.query, this.dataset.model); + + this.filter_menu.appendTo(this.$buttons); + this.groupby_menu.appendTo(this.$buttons); + var custom_filters_ready = this.favorite_menu.appendTo(this.$buttons); + } + return $.when(custom_filters_ready).then(this.proxy('set_default_filters')); }, + set_default_filters: function (a, b) { + var self = this, + default_custom_filter = this.$buttons && this.favorite_menu.get_default_filter(); + if (default_custom_filter) { + return this.favorite_menu.toggle_filter(default_custom_filter, true); + } + if (!_.isEmpty(this.defaults)) { + var inputs = this.search_fields.concat(this.filters, this.groupbys), + defaults = _.invoke(inputs, 'facet_for_defaults', this.defaults); + return $.when.apply(null, defaults).then(function () { + self.query.reset(_(arguments).compact(), {preventSearch: true}); + }); + } + this.query.reset([], {preventSearch: true}); + return $.when(); + }, + /** + * Performs the search view collection of widget data. + * + * If the collection went well (all fields are valid), then triggers + * :js:func:`instance.web.SearchView.on_search`. + * + * If at least one field failed its validation, triggers + * :js:func:`instance.web.SearchView.on_invalid` instead. + * + * @param [_query] + * @param {Object} [options] + */ + do_search: function (_query, options) { + if (options && options.preventSearch) { + return; + } + var search = this.build_search_data(); + this.trigger('search_data', search.domains, search.contexts, search.groupbys); + }, + /** + * Extract search data from the view's facets. + * + * Result is an object with 3 (own) properties: + * + * domains + * Array of domains + * contexts + * Array of contexts + * groupbys + * Array of domains, in groupby order rather than view order + * + * @return {Object} + */ + build_search_data: function () { + var domains = [], contexts = [], groupbys = []; + this.query.each(function (facet) { + var field = facet.get('field'); + var domain = field.get_domain(facet); + if (domain) { + domains.push(domain); + } + var context = field.get_context(facet); + if (context) { + contexts.push(context); + } + var group_by = field.get_groupby(facet); + if (group_by) { + groupbys.push.apply(groupbys, group_by); + } + }); + return { + domains: domains, + contexts: contexts, + groupbys: groupbys, + }; + }, toggle_visibility: function (is_visible) { - this.$el.toggle(is_visible); + this.$el.toggle(!this.headless && is_visible); + this.$buttons && this.$buttons.toggle(!this.headless && is_visible && this.visible_filters); }, - - show: function () { - this.$el.show(); + toggle_buttons: function (is_visible) { + this.visible_filters = is_visible || !this.visible_filters; + this.$buttons && this.$buttons.toggle(this.visible_filters); }, - hide: function () { - this.$el.hide(); + /** + * Sets up search view's view-wide auto-completion widget + */ + setup_global_completion: function () { + var self = this; + this.autocomplete = new my.AutoComplete(this, { + source: this.proxy('complete_global_search'), + select: this.proxy('select_completion'), + delay: 0, + get_search_string: function () { + return self.$('div.oe_searchview_input').text(); + }, + }); + this.autocomplete.appendTo(this.$el); + }, + /** + * Provide auto-completion result for req.term (an array to `resp`) + * + * @param {Object} req request to complete + * @param {String} req.term searched term to complete + * @param {Function} resp response callback + */ + complete_global_search: function (req, resp) { + var inputs = this.search_fields.concat(this.filters, this.groupbys); + $.when.apply(null, _(inputs).chain() + .filter(function (input) { return input.visible(); }) + .invoke('complete', req.term) + .value()).then(function () { + resp(_(arguments).chain() + .compact() + .flatten(true) + .value()); + }); + }, + /** + * Action to perform in case of selection: create a facet (model) + * and add it to the search collection + * + * @param {Object} e selection event, preventDefault to avoid setting value on object + * @param {Object} ui selection information + * @param {Object} ui.item selected completion item + */ + select_completion: function (e, ui) { + e.preventDefault(); + var input_index = _(this.input_subviews).indexOf( + this.subviewForRoot( + this.$('div.oe_searchview_input:focus')[0])); + this.query.add(ui.item.facet, {at: input_index / 2}); }, - subviewForRoot: function (subview_root) { return _(this.input_subviews).detect(function (subview) { return subview.$el[0] === subview_root; @@ -485,77 +590,6 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea this.subviewForRoot(subview_root), +1, true) .$el.focus(); }, - - /** - * Sets up search view's view-wide auto-completion widget - */ - setup_global_completion: function () { - var self = this; - this.autocomplete = new instance.web.search.AutoComplete(this, { - source: this.proxy('complete_global_search'), - select: this.proxy('select_completion'), - delay: 0, - get_search_string: function () { - return self.$('div.oe_searchview_input').text(); - }, - width: this.$el.width(), - }); - this.autocomplete.appendTo(this.$el); - }, - /** - * Provide auto-completion result for req.term (an array to `resp`) - * - * @param {Object} req request to complete - * @param {String} req.term searched term to complete - * @param {Function} resp response callback - */ - complete_global_search: function (req, resp) { - $.when.apply(null, _(this.drawer.inputs).chain() - .filter(function (input) { return input.visible(); }) - .invoke('complete', req.term) - .value()).then(function () { - resp(_(arguments).chain() - .compact() - .flatten(true) - .value()); - }); - }, - - /** - * Action to perform in case of selection: create a facet (model) - * and add it to the search collection - * - * @param {Object} e selection event, preventDefault to avoid setting value on object - * @param {Object} ui selection information - * @param {Object} ui.item selected completion item - */ - select_completion: function (e, ui) { - e.preventDefault(); - - var input_index = _(this.input_subviews).indexOf( - this.subviewForRoot( - this.$('div.oe_searchview_input:focus')[0])); - this.query.add(ui.item.facet, {at: input_index / 2}); - }, - childFocused: function () { - this.$el.addClass('oe_focused'); - }, - childBlurred: function () { - var val = this.$el.val(); - this.$el.val(''); - this.$el.removeClass('oe_focused') - .trigger('blur'); - this.autocomplete.close(); - }, - /** - * Call the renderFacets method with the correct arguments. - * This is due to the fact that change events are called with two arguments - * (model, options) while add, reset and remove events are called with - * (collection, model, options) as arguments - */ - renderChangedFacets: function (model, options) { - this.renderFacets(undefined, model, options); - }, /** * @param {openerp.web.search.SearchQuery | undefined} Undefined if event is change * @param {openerp.web.search.Facet} @@ -564,20 +598,19 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea renderFacets: function (collection, model, options) { var self = this; var started = []; - var $e = this.$('div.oe_searchview_facets'); _.invoke(this.input_subviews, 'destroy'); this.input_subviews = []; var i = new my.InputView(this); - started.push(i.appendTo($e)); + started.push(i.appendTo(this.$facets_container)); this.input_subviews.push(i); this.query.each(function (facet) { var f = new my.FacetView(this, facet); - started.push(f.appendTo($e)); + started.push(f.appendTo(self.$facets_container)); self.input_subviews.push(f); var i = new my.InputView(this); - started.push(i.appendTo($e)); + started.push(i.appendTo(self.$facets_container)); self.input_subviews.push(i); }, this); _.each(this.input_subviews, function (childView) { @@ -598,320 +631,77 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea input_to_focus.$el.focus(); }); }, - - search_view_loaded: function(data) { - var self = this; - this.fields_view = data; - if (data.type !== 'search' || - data.arch.tag !== 'search') { - throw new Error(_.str.sprintf( - "Got non-search view after asking for a search view: type %s, arch root %s", - data.type, data.arch.tag)); - } - - return this.drawer_ready - .then(this.proxy('setup_default_query')) - .then(function () { - self.trigger("search_view_loaded", data); - self.ready.resolve(); - }); + childFocused: function () { + this.$el.addClass('active'); }, - setup_default_query: function () { - // Hacky implementation of CustomFilters#facet_for_defaults ensure - // CustomFilters will be ready (and CustomFilters#filters will be - // correctly filled) by the time this method executes. - var custom_filters = this.drawer.custom_filters.filters; - if (!this.options.disable_custom_filters && !_(custom_filters).isEmpty()) { - // Check for any is_default custom filter - var personal_filter = _(custom_filters).find(function (filter) { - return filter.user_id && filter.is_default; - }); - if (personal_filter) { - this.drawer.custom_filters.toggle_filter(personal_filter, true); - return; - } - - var global_filter = _(custom_filters).find(function (filter) { - return !filter.user_id && filter.is_default; - }); - if (global_filter) { - this.drawer.custom_filters.toggle_filter(global_filter, true); - return; - } - } - // No custom filter, or no is_default custom filter, apply view defaults - this.query.reset(_(arguments).compact(), {preventSearch: true}); + childBlurred: function () { + var val = this.$el.val(); + this.$el.val(''); + this.$el.removeClass('active') + .trigger('blur'); + this.autocomplete.close(); }, /** - * Extract search data from the view's facets. - * - * Result is an object with 4 (own) properties: - * - * errors - * An array of any error generated during data validation and - * extraction, contains the validation error objects - * domains - * Array of domains - * contexts - * Array of contexts - * groupbys - * Array of domains, in groupby order rather than view order - * - * @return {Object} + * Call the renderFacets method with the correct arguments. + * This is due to the fact that change events are called with two arguments + * (model, options) while add, reset and remove events are called with + * (collection, model, options) as arguments */ - build_search_data: function () { - var domains = [], contexts = [], groupbys = [], errors = []; + renderChangedFacets: function (model, options) { + this.renderFacets(undefined, model, options); + }, + // it should parse the arch field of the view, instantiate the corresponding + // filters/fields, and put them in the correct variables: + // * this.search_fields is a list of all the fields, + // * this.filters: groups of filters + // * this.group_by: group_bys + prepare_search_inputs: function () { + var self = this, + arch = this.fields_view_get.arch; - this.query.each(function (facet) { - var field = facet.get('field'); - try { - var domain = field.get_domain(facet); - if (domain) { - domains.push(domain); - } - var context = field.get_context(facet); - if (context) { - contexts.push(context); - } - var group_by = field.get_groupby(facet); - if (group_by) { - groupbys.push.apply(groupbys, group_by); - } - } catch (e) { - if (e instanceof instance.web.search.Invalid) { - errors.push(e); - } else { - throw e; + var filters = [].concat.apply([], _.map(arch.children, function (item) { + return item.tag !== 'group' ? eval_item(item) : item.children.map(eval_item); + })); + function eval_item (item) { + var category = 'filters'; + if (item.attrs.context) { + var context = instance.web.pyeval.eval('context', item.attrs.context); + if (context.group_by) { + category = 'group_by'; } } - }); - return { - domains: domains, - contexts: contexts, - groupbys: groupbys, - errors: errors - }; - }, - /** - * Performs the search view collection of widget data. - * - * If the collection went well (all fields are valid), then triggers - * :js:func:`instance.web.SearchView.on_search`. - * - * If at least one field failed its validation, triggers - * :js:func:`instance.web.SearchView.on_invalid` instead. - * - * @param [_query] - * @param {Object} [options] - */ - do_search: function (_query, options) { - if (options && options.preventSearch) { - return; + return { + item: item, + category: category, + } } - var search = this.build_search_data(); - if (!_.isEmpty(search.errors)) { - this.on_invalid(search.errors); - return; - } - this.trigger('search_data', search.domains, search.contexts, search.groupbys); - }, - /** - * Triggered after the SearchView has collected all relevant domains and - * contexts. - * - * It is provided with an Array of domains and an Array of contexts, which - * may or may not be evaluated (each item can be either a valid domain or - * context, or a string to evaluate in order in the sequence) - * - * It is also passed an array of contexts used for group_by (they are in - * the correct order for group_by evaluation, which contexts may not be) - * - * @event - * @param {Array} domains an array of literal domains or domain references - * @param {Array} contexts an array of literal contexts or context refs - * @param {Array} groupbys ordered contexts which may or may not have group_by keys - */ - /** - * Triggered after a validation error in the SearchView fields. - * - * Error objects have three keys: - * * ``field`` is the name of the invalid field - * * ``value`` is the invalid value - * * ``message`` is the (in)validation message provided by the field - * - * @event - * @param {Array} errors a never-empty array of error objects - */ - on_invalid: function (errors) { - this.do_notify(_t("Invalid Search"), _t("triggered from search view")); - this.trigger('invalid_search', errors); - }, + var current_group = [], + current_category = 'filters', + categories = {filters: this.filters, group_by: this.groupbys}; - // The method appendTo is overwrited to be able to insert the drawer anywhere - appendTo: function ($searchview_parent, $searchview_drawer_node) { - var $searchview_drawer_node = $searchview_drawer_node || $searchview_parent; - - return $.when( - this._super($searchview_parent), - this.drawer.appendTo($searchview_drawer_node) - ); - }, - - destroy: function () { - this.drawer.destroy(); - this.getParent().destroy.call(this); - } -}); - -instance.web.SearchViewDrawer = instance.web.Widget.extend({ - template: "SearchViewDrawer", - - init: function(parent, searchview) { - this._super(parent); - this.searchview = searchview; - this.searchview.set_drawer(this); - this.ready = searchview.drawer_ready; - this.controls = []; - this.inputs = []; - }, - - toggle: function (visibility) { - this.$el.toggle(visibility); - var $view_manager_body = this.$el.closest('.oe_view_manager_body'); - if ($view_manager_body.length) { - $view_manager_body.scrollTop(0); - } - }, - - start: function() { - var self = this; - if (this.searchview.headless) return $.when(this._super(), this.searchview.ready); - var filters_ready = this.searchview.fields_view_get - .then(this.proxy('prepare_filters')); - return $.when(this._super(), filters_ready).then(function () { - var defaults = arguments[1][0]; - self.ready.resolve.apply(null, defaults); + _.each(filters.concat({category:'filters', item: 'separator'}), function (filter) { + if (filter.item.tag === 'filter' && filter.category === current_category) { + return current_group.push(new my.Filter(filter.item, self)); + } + if (current_group.length) { + var group = new my.FilterGroup(current_group, self); + categories[current_category].push(group); + current_group = []; + } + if (filter.item.tag === 'field') { + var attrs = filter.item.attrs, + field = self.fields_view_get.fields[attrs.name], + Obj = my.fields.get_any([attrs.widget, field.type]); + if (Obj) { + self.search_fields.push(new (Obj) (filter.item, field, self)); + } + } + if (filter.item.tag === 'filter') { + current_group.push(new my.Filter(filter.item, self)); + } + current_category = filter.category; }); }, - prepare_filters: function (data) { - this.make_widgets( - data['arch'].children, - data.fields); - - this.add_common_inputs(); - - // build drawer - var in_drawer = this.select_for_drawer(); - - var $first_col = this.$(".col-md-7"), - $snd_col = this.$(".col-md-5"); - - var add_custom_filters = in_drawer[0].appendTo($first_col), - add_filters = in_drawer[1].appendTo($first_col), - add_rest = $.when.apply(null, _(in_drawer.slice(2)).invoke('appendTo', $snd_col)), - defaults_fetched = $.when.apply(null, _(this.inputs).invoke( - 'facet_for_defaults', this.searchview.defaults)); - - return $.when(defaults_fetched, add_custom_filters, add_filters, add_rest); - }, - /** - * Sets up thingie where all the mess is put? - */ - select_for_drawer: function () { - return _(this.inputs).filter(function (input) { - return input.in_drawer(); - }); - }, - - /** - * Builds a list of widget rows (each row is an array of widgets) - * - * @param {Array} items a list of nodes to convert to widgets - * @param {Object} fields a mapping of field names to (ORM) field attributes - * @param {Object} [group] group to put the new controls in - */ - make_widgets: function (items, fields, group) { - if (!group) { - group = new instance.web.search.Group( - this, 'q', {attrs: {string: _t("Filters")}}); - } - var self = this; - var filters = []; - _.each(items, function (item) { - if (filters.length && item.tag !== 'filter') { - group.push(new instance.web.search.FilterGroup(filters, group)); - filters = []; - } - - switch (item.tag) { - case 'separator': case 'newline': - break; - case 'filter': - filters.push(new instance.web.search.Filter(item, group)); - break; - case 'group': - self.add_separator(); - self.make_widgets(item.children, fields, - new instance.web.search.Group(group, 'w', item)); - self.add_separator(); - break; - case 'field': - var field = this.make_field( - item, fields[item['attrs'].name], group); - group.push(field); - // filters - self.make_widgets(item.children, fields, group); - break; - } - }, this); - - if (filters.length) { - group.push(new instance.web.search.FilterGroup(filters, this)); - } - }, - - add_separator: function () { - if (!(_.last(this.inputs) instanceof instance.web.search.Separator)) - new instance.web.search.Separator(this); - }, - /** - * Creates a field for the provided field descriptor item (which comes - * from fields_view_get) - * - * @param {Object} item fields_view_get node for the field - * @param {Object} field fields_get result for the field - * @param {Object} [parent] - * @returns instance.web.search.Field - */ - make_field: function (item, field, parent) { - // M2O combined with selection widget is pointless and broken in search views, - // but has been used in the past for unsupported hacks -> ignore it - if (field.type === "many2one" && item.attrs.widget === "selection"){ - item.attrs.widget = undefined; - } - var obj = instance.web.search.fields.get_any( [item.attrs.widget, field.type]); - if(obj) { - return new (obj) (item, field, parent || this); - } else { - console.group('Unknown field type ' + field.type); - console.error('View node', item); - console.info('View field', field); - console.info('In view', this); - console.groupEnd(); - return null; - } - }, - - add_common_inputs: function() { - // add custom filters to this.inputs - this.custom_filters = new instance.web.search.CustomFilters(this); - // add Filters to this.inputs, need view.controls filled - (new instance.web.search.Filters(this)); - (new instance.web.search.SaveFilter(this, this.custom_filters)); - // add Advanced to this.inputs - (new instance.web.search.Advanced(this)); - }, - }); /** @@ -933,91 +723,17 @@ instance.web.search.fields = new instance.web.Registry({ 'many2many': 'instance.web.search.CharField', 'one2many': 'instance.web.search.CharField' }); -instance.web.search.Invalid = instance.web.Class.extend( /** @lends instance.web.search.Invalid# */{ - /** - * Exception thrown by search widgets when they hold invalid values, - * which they can not return when asked. - * - * @constructs instance.web.search.Invalid - * @extends instance.web.Class - * - * @param field the name of the field holding an invalid value - * @param value the invalid value - * @param message validation failure message - */ - init: function (field, value, message) { - this.field = field; - this.value = value; - this.message = message; - }, - toString: function () { - return _.str.sprintf( - _t("Incorrect value for field %(fieldname)s: [%(value)s] is %(message)s"), - {fieldname: this.field, value: this.value, message: this.message} - ); - } -}); -instance.web.search.Widget = instance.web.Widget.extend( /** @lends instance.web.search.Widget# */{ - template: null, - /** - * Root class of all search widgets - * - * @constructs instance.web.search.Widget - * @extends instance.web.Widget - * - * @param parent parent of this widget - */ - init: function (parent) { - this._super(parent); - var ancestor = parent; - do { - this.drawer = ancestor; - } while (!(ancestor instanceof instance.web.SearchViewDrawer) - && (ancestor = (ancestor.getParent && ancestor.getParent()))); - this.view = this.drawer.searchview || this.drawer; - } -}); -instance.web.search.add_expand_listener = function($root) { - $root.find('a.searchview_group_string').click(function (e) { - $root.toggleClass('folded expanded'); - e.stopPropagation(); - e.preventDefault(); - }); -}; -instance.web.search.Group = instance.web.search.Widget.extend({ - init: function (parent, icon, node) { - this._super(parent); - var attrs = node.attrs; - this.modifiers = attrs.modifiers = - attrs.modifiers ? JSON.parse(attrs.modifiers) : {}; - this.attrs = attrs; - this.icon = icon; - this.name = attrs.string; - this.children = []; - - this.drawer.controls.push(this); - }, - push: function (input) { - this.children.push(input); - }, - visible: function () { - return !this.modifiers.invisible; - }, -}); - -instance.web.search.Input = instance.web.search.Widget.extend( /** @lends instance.web.search.Input# */{ - _in_drawer: false, +instance.web.search.Input = instance.web.Widget.extend( /** @lends instance.web.search.Input# */{ /** * @constructs instance.web.search.Input - * @extends instance.web.search.Widget + * @extends instance.web.Widget * * @param parent */ init: function (parent) { this._super(parent); this.load_attrs({}); - this.drawer.inputs.push(this); }, /** * Fetch auto-completion values for the widget. @@ -1049,9 +765,6 @@ instance.web.search.Input = instance.web.search.Widget.extend( /** @lends instan } return this.facet_for(defaults[this.attrs.name]); }, - in_drawer: function () { - return !!this._in_drawer; - }, get_context: function () { throw new Error( "get_context not implemented for widget " + this.attrs.type); @@ -1076,23 +789,22 @@ instance.web.search.Input = instance.web.search.Widget.extend( /** @lends instan * @returns {Boolean} */ visible: function () { - if (this.attrs.modifiers.invisible) { - return false; - } - var parent = this; - while ((parent = parent.getParent()) && - ( (parent instanceof instance.web.search.Group) - || (parent instanceof instance.web.search.Input))) { - if (!parent.visible()) { - return false; - } - } - return true; + return !this.attrs.modifiers.invisible; + + // var parent = this; + // while ((parent = parent.getParent()) && + // ( (parent instanceof instance.web.search.Group) + // || (parent instanceof instance.web.search.Input))) { + // if (!parent.visible()) { + // return false; + // } + // } + // return true; }, }); instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends instance.web.search.FilterGroup# */{ template: 'SearchView.filters', - icon: 'q', + icon: "fa-filter", completion_label: _lt("Filter on: %s"), /** * Inclusive group of filters, creates a continuous "button" with clickable @@ -1116,10 +828,11 @@ instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends in } this._super(parent); this.filters = filters; - this.view.query.on('add remove change reset', this.proxy('search_change')); + this.searchview = parent; + this.searchview.query.on('add remove change reset', this.proxy('search_change')); }, start: function () { - this.$el.on('click', 'li', this.proxy('toggle_filter')); + this.$el.on('click', 'a', this.proxy('toggle_filter')); return $.when(null); }, /** @@ -1128,15 +841,15 @@ instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends in */ search_change: function () { var self = this; - var $filters = this.$('> li').removeClass('badge'); - var facet = this.view.query.find(_.bind(this.match_facet, this)); + var $filters = this.$el.removeClass('selected'); + var facet = this.searchview.query.find(_.bind(this.match_facet, this)); if (!facet) { return; } facet.values.each(function (v) { var i = _(self.filters).indexOf(v.get('value')); if (i === -1) { return; } $filters.filter(function () { return Number($(this).data('index')) === i; - }).addClass('badge'); + }).addClass('selected'); }); }, /** @@ -1226,10 +939,11 @@ instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends in }); }, toggle_filter: function (e) { - this.toggle(this.filters[Number($(e.target).data('index'))]); + e.stopPropagation(); + this.toggle(this.filters[Number($(e.target).parent().data('index'))]); }, - toggle: function (filter) { - this.view.query.toggle(this.make_facet([this.make_value(filter)])); + toggle: function (filter, options) { + this.searchview.query.toggle(this.make_facet([this.make_value(filter)]), options); }, complete: function (item) { var self = this; @@ -1260,10 +974,11 @@ instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends in } }); instance.web.search.GroupbyGroup = instance.web.search.FilterGroup.extend({ - icon: 'w', + icon: 'fa-bars', completion_label: _lt("Group by: %s"), init: function (filters, parent) { this._super(filters, parent); + this.searchview = parent; // Not flanders: facet unicity is handled through the // (category, field) pair of facet attributes. This is all well and // good for regular filter groups where a group matches a facet, but for @@ -1271,8 +986,8 @@ instance.web.search.GroupbyGroup = instance.web.search.FilterGroup.extend({ // view which proxies to the first GroupbyGroup, so it can be used // for every GroupbyGroup and still provides the various methods needed // by the search view. Use weirdo name to avoid risks of conflicts - if (!this.view._s_groupby) { - this.view._s_groupby = { + if (!this.searchview._s_groupby) { + this.searchview._s_groupby = { help: "See GroupbyGroup#init", get_context: this.proxy('get_context'), get_domain: this.proxy('get_domain'), @@ -1281,14 +996,14 @@ instance.web.search.GroupbyGroup = instance.web.search.FilterGroup.extend({ } }, match_facet: function (facet) { - return facet.get('field') === this.view._s_groupby; + return facet.get('field') === this.searchview._s_groupby; }, make_facet: function (values) { return { category: _t("GroupBy"), icon: this.icon, values: values, - field: this.view._s_groupby + field: this.searchview._s_groupby }; } }); @@ -1317,14 +1032,6 @@ instance.web.search.Filter = instance.web.search.Input.extend(/** @lends instanc get_domain: function () { }, }); -instance.web.search.Separator = instance.web.search.Input.extend({ - _in_drawer: false, - - complete: function () { - return {is_separator: true}; - } -}); - instance.web.search.Field = instance.web.search.Input.extend( /** @lends instance.web.search.Field# */ { template: 'SearchView.field', default_operator: '=', @@ -1499,7 +1206,7 @@ instance.web.search.FloatField = instance.web.search.NumberField.extend(/** @len function facet_from(field, pair) { return { field: field, - category: field['attrs'].string, + category: field.attrs.string, values: [{label: pair[1], value: pair[0]}] }; } @@ -1614,6 +1321,7 @@ instance.web.search.ManyToOneField = instance.web.search.CharField.extend({ init: function (view_section, field, parent) { this._super(view_section, field, parent); this.model = new instance.web.Model(this.attrs.relation); + this.searchview = parent; }, complete: function (value) { @@ -1637,7 +1345,7 @@ instance.web.search.ManyToOneField = instance.web.search.CharField.extend({ var self = this; // FIXME: "concurrent" searches (multiple requests, mis-ordered responses) var context = instance.web.pyeval.eval( - 'contexts', [this.view.dataset.get_context()]); + 'contexts', [this.searchview.dataset.get_context()]); return this.model.call('name_search', [], { name: needle, args: (typeof this.attrs.domain === 'string') ? [] : this.attrs.domain, @@ -1697,40 +1405,286 @@ instance.web.search.ManyToOneField = instance.web.search.CharField.extend({ } }); -instance.web.search.CustomFilters = instance.web.search.Input.extend({ - template: 'SearchView.Custom', - _in_drawer: true, - init: function () { - this.is_ready = $.Deferred(); - this._super.apply(this,arguments); +instance.web.search.FilterMenu = instance.web.Widget.extend({ + template: 'SearchView.FilterMenu', + events: { + 'click .oe-add-filter': function () { + this.toggle_custom_filter_menu(); + }, + 'click li': function (event) {event.stopImmediatePropagation();}, + 'hidden.bs.dropdown': function () { + this.toggle_custom_filter_menu(false); + }, + 'click .oe-add-condition': 'append_proposition', + 'click .oe-apply-filter': 'commit_search', + }, + init: function (parent, filters, fields_def) { + var self = this; + this._super(parent); + this.filters = filters || []; + this.searchview = parent; + this.propositions = []; + this.fields_def = fields_def.then(function (data) { + var fields = { + id: { string: 'ID', type: 'id', searchable: true } + }; + _.each(data, function(field_def, field_name) { + if (field_def.selectable !== false && field_name !== 'id') { + fields[field_name] = field_def; + } + }); + return fields; + }); }, start: function () { var self = this; + this.$menu = this.$('.filters-menu'); + this.$add_filter = this.$('.oe-add-filter'); + this.$apply_filter = this.$('.oe-apply-filter'); + this.$add_filter_menu = this.$('.oe-add-filter-menu'); + _.each(this.filters, function (group) { + group.insertBefore(self.$add_filter); + $('
  • ').addClass('divider').insertBefore(self.$add_filter); + }); + this.append_proposition().then(function (prop) { + prop.$el.hide(); + }); + }, + toggle_custom_filter_menu: function (is_open) { + this.$add_filter + .toggleClass('closed-menu', !is_open) + .toggleClass('open-menu', is_open); + this.$add_filter_menu.toggle(is_open); + if (this.$add_filter.hasClass('closed-menu') && (!this.propositions.length)) { + this.append_proposition(); + } + this.$('.oe-filter-condition').toggle(is_open); + }, + append_proposition: function () { + var self = this; + return this.fields_def.then(function (fields) { + var prop = new instance.web.search.ExtendedSearchProposition(self, fields); + self.propositions.push(prop); + prop.insertBefore(self.$add_filter_menu); + self.$apply_filter.prop('disabled', false); + return prop; + }); + }, + remove_proposition: function (prop) { + this.propositions = _.reject(this.propositions, function (p) { return p === prop}); + if (!this.propositions.length) { + this.$apply_filter.prop('disabled', true); + } + prop.destroy(); + }, + commit_search: function () { + var self = this, + filters = _.invoke(this.propositions, 'get_filter'), + filters_widgets = _.map(filters, function (filter) { + return new my.Filter(filter, self); + }); + var filter_group = new my.FilterGroup(filters_widgets, this.searchview); + filter_group.insertBefore(this.$add_filter); + $('
  • ').addClass('divider').insertBefore(self.$add_filter); + + filters_widgets.forEach(function (filter) { + self.searchview.query.add(filter_group.make_facet([filter_group.make_value(filter)]), {silent: true}); + }); + this.searchview.query.trigger('reset'); + + _.invoke(this.propositions, 'destroy'); + this.propositions = []; + this.append_proposition(); + this.toggle_custom_filter_menu(false); + }, +}); + +instance.web.search.GroupByMenu = instance.web.Widget.extend({ + template: 'SearchView.GroupByMenu', + events: { + 'click li': function (event) { + event.stopImmediatePropagation(); + }, + 'hidden.bs.dropdown': function () { + this.toggle_add_menu(false); + }, + 'click .add-custom-group a': function () { + this.toggle_add_menu(); + }, + }, + init: function (parent, groups, fields_def) { + this._super(parent); + this.groups = groups || []; + this.groupable_fields = {}; + this.searchview = parent; + this.fields_def = fields_def.then(this.proxy('get_groupable_fields')); + }, + start: function () { + var self = this; + this.$menu = this.$('.group-by-menu'); + var divider = this.$menu.find('.divider'); + _.invoke(this.groups, 'insertBefore', divider); + if (this.groups.length) { + divider.show(); + } + this.$add_group = this.$menu.find('.add-custom-group'); + this.fields_def.then(function () { + self.$menu.append(QWeb.render('GroupByMenuSelector', self)); + self.$add_group_menu = self.$('.oe-add-group'); + self.$group_selector = self.$('.oe-group-selector'); + self.$('.oe-select-group').click(function (event) { + self.toggle_add_menu(false); + var field = self.$group_selector.find(':selected').data('name'); + self.add_groupby_to_menu(field); + }); + }); + }, + get_groupable_fields: function (fields) { + var self = this, + groupable_types = ['many2one', 'char', 'boolean', 'selection', 'date', 'datetime']; + + _.each(fields, function (field, name) { + if (field.store && _.contains(groupable_types, field.type)) { + self.groupable_fields[name] = field; + } + }); + }, + toggle_add_menu: function (is_open) { + this.$add_group + .toggleClass('closed-menu', !is_open) + .toggleClass('open-menu', is_open); + this.$add_group_menu.toggle(is_open); + if (this.$add_group.hasClass('open-menu')) { + this.$group_selector.focus(); + } + }, + add_groupby_to_menu: function (field_name) { + var filter = new my.Filter({attrs:{ + context:"{'group_by':'" + field_name + "''}", + name: this.groupable_fields[field_name].string, + }}, this.searchview); + var group = new my.FilterGroup([filter], this.searchview), + divider = this.$('.divider').show(); + group.insertBefore(divider); + group.toggle(filter); + }, +}); + +instance.web.search.FavoriteMenu = instance.web.Widget.extend({ + template: 'SearchView.FavoriteMenu', + events: { + 'click li': function (event) { + event.stopImmediatePropagation(); + }, + 'click .oe-save-search a': function () { + this.toggle_save_menu(); + }, + 'click .oe-save-name button': 'save_favorite', + 'hidden.bs.dropdown': function () { + this.close_menus(); + }, + }, + init: function (parent, query, target_model) { + this._super.apply(this,arguments); + this.searchview = parent; + this.query = query; + this.target_model = target_model; this.model = new instance.web.Model('ir.filters'); this.filters = {}; this.$filters = {}; - this.view.query + var action = instance.client.action_manager.inner_action; + this.action_id = action && action.id; + }, + start: function () { + var self = this; + this.$save_search = this.$('.oe-save-search'); + this.$save_name = this.$('.oe-save-name'); + this.$inputs = this.$save_name.find('input'); + this.$divider = this.$('.divider'); + + var $shared_filter = $(this.$inputs[1]), + $default_filter = $(this.$inputs[2]); + $shared_filter.click(function () {$default_filter.prop('checked', false)}); + $default_filter.click(function () {$shared_filter.prop('checked', false)}); + + this.query .on('remove', function (facet) { - if (!facet.get('is_custom_filter')) { - return; + if (facet.get('is_custom_filter')) { + self.clear_selection(); } - self.clear_selection(); }) .on('reset', this.proxy('clear_selection')); - return this.model.call('get_filters', [this.view.model, this.get_action_id()]) - .then(this.proxy('set_filters')) - .done(function () { self.is_ready.resolve(); }) - .fail(function () { self.is_ready.reject.apply(self.is_ready, arguments); }); + return this.model.call('get_filters', [this.target_model, this.action_id]) + .done(this.proxy('prepare_dropdown_menu')); }, - get_action_id: function(){ - var action = instance.client.action_manager.inner_action; - if (action) return action.id; + prepare_dropdown_menu: function (filters) { + filters.map(this.append_filter.bind(this)); }, - /** - * Special implementation delaying defaults until CustomFilters is loaded - */ - facet_for_defaults: function () { - return this.is_ready; + toggle_save_menu: function (is_open) { + this.$save_search + .toggleClass('closed-menu', !is_open) + .toggleClass('open-menu', is_open); + this.$save_name.toggle(is_open); + if (this.$save_search.hasClass('open-menu')) { + this.$save_name.find('input').first().focus(); + } + }, + close_menus: function () { + this.toggle_save_menu(false); + }, + save_favorite: function () { + var self = this, + filter_name = this.$inputs[0].value, + shared_filter = this.$inputs[1].checked, + default_filter = this.$inputs[2].checked; + if (!filter_name.length){ + this.do_warn(_t("Error"), _t("Filter name is required.")); + this.$inputs.first().focus(); + return; + } + var search = this.searchview.build_search_data(), + results = instance.web.pyeval.sync_eval_domains_and_contexts({ + domains: search.domains, + contexts: search.contexts, + group_by_seq: search.groupbys || [], + }); + if (!_.isEmpty(results.group_by)) { + results.context.group_by = results.group_by; + } + // Don't save user_context keys in the custom filter, otherwise end + // up with e.g. wrong uid or lang stored *and used in subsequent + // reqs* + var ctx = results.context; + _(_.keys(instance.session.user_context)).each(function (key) { + delete ctx[key]; + }); + var filter = { + name: filter_name, + user_id: shared_filter ? false : instance.session.uid, + model_id: this.searchview.dataset.model, + context: results.context, + domain: results.domain, + is_default: default_filter, + action_id: this.action_id, + }; + return this.model.call('create_or_replace', [filter]).done(function (id) { + filter.id = id; + self.toggle_save_menu(false); + self.$save_name.find('input').val('').prop('checked', false); + self.append_filter(filter); + self.toggle_filter(filter, true); + }); + }, + get_default_filter: function () { + var personal_filter = _.find(this.filters, function (filter) { + return filter.user_id && filter.is_default; + }); + if (personal_filter) { + return personal_filter; + } + return _.find(this.filters, function (filter) { + return !filter.user_id && filter.is_default; + }); }, /** * Generates a mapping key (in the filters and $filter mappings) for the @@ -1746,9 +1700,9 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({ */ key_for: function (filter) { var user_id = filter.user_id, - action_id = filter.action_id; - var uid = (user_id instanceof Array) ? user_id[0] : user_id; - var act_id = (action_id instanceof Array) ? action_id[0] : action_id; + action_id = filter.action_id, + uid = (user_id instanceof Array) ? user_id[0] : user_id, + act_id = (action_id instanceof Array) ? action_id[0] : action_id; return _.str.sprintf('(%s)(%s)%s', uid, act_id, filter.name); }, /** @@ -1764,264 +1718,82 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({ facet_for: function (filter) { return { category: _t("Custom Filter"), - icon: 'M', + icon: 'fa-star', field: { get_context: function () { return filter.context; }, get_groupby: function () { return [filter.context]; }, get_domain: function () { return filter.domain; } }, - _id: filter['id'], + _id: filter.id, is_custom_filter: true, values: [{label: filter.name, value: null}] }; }, clear_selection: function () { - this.$('span.badge').removeClass('badge'); + this.$('li.selected').removeClass('selected'); }, append_filter: function (filter) { - var self = this; - var key = this.key_for(filter); - var warning = _t("This filter is global and will be removed for everybody if you continue."); + var self = this, + key = this.key_for(filter), + $filter, + warning = _t("This filter is global and will be removed for everybody if you continue."); - var $filter; + this.$divider.show(); if (key in this.$filters) { $filter = this.$filters[key]; } else { - var id = filter.id; this.filters[key] = filter; $filter = $('
  • ') - .appendTo(this.$('.oe_searchview_custom_list')) + .insertBefore(this.$divider) .toggleClass('oe_searchview_custom_default', filter.is_default) - .append(this.$filters[key] = $('').text(filter.name)); + .append($('').text(filter.name)); + this.$filters[key] = $filter; this.$filters[key].addClass(filter.user_id ? 'oe_searchview_custom_private' : 'oe_searchview_custom_public') - - $('x') - .click(function (e) { - e.stopPropagation(); - if (!(filter.user_id || confirm(warning))) { - return; - } - self.model.call('unlink', [id]).done(function () { - $filter.remove(); - delete self.$filters[key]; - delete self.filters[key]; - if (_.isEmpty(self.filters)) { - self.hide(); - } - }); + $('') + .addClass('fa fa-trash-o') + .click(function (event) { + event.stopImmediatePropagation(); + self.remove_filter(filter, $filter, key); }) .appendTo($filter); } - - this.$filters[key].unbind('click').click(function () { + this.$filters[key].unbind('click').click(function (event) { + event.stopPropagation(); self.toggle_filter(filter); }); - this.show(); }, toggle_filter: function (filter, preventSearch) { - var current = this.view.query.find(function (facet) { + var current = this.query.find(function (facet) { return facet.get('_id') === filter.id; }); if (current) { - this.view.query.remove(current); - this.$filters[this.key_for(filter)].removeClass('badge'); + this.query.remove(current); + this.$filters[this.key_for(filter)].removeClass('selected'); return; } - this.view.query.reset([this.facet_for(filter)], { + this.query.reset([this.facet_for(filter)], { preventSearch: preventSearch || false}); - this.$filters[this.key_for(filter)].addClass('badge'); + this.$filters[this.key_for(filter)].addClass('selected'); }, - set_filters: function (filters) { - _(filters).map(_.bind(this.append_filter, this)); - if (!filters.length) { - this.hide(); - } - }, - hide: function () { - this.$el.hide(); - }, - show: function () { - this.$el.show(); - }, -}); - -instance.web.search.SaveFilter = instance.web.search.Input.extend({ - template: 'SearchView.SaveFilter', - _in_drawer: true, - init: function (parent, custom_filters) { - this._super(parent); - this.custom_filters = custom_filters; - }, - start: function () { + remove_filter: function (filter, $filter, key) { var self = this; - this.model = new instance.web.Model('ir.filters'); - this.$el.on('submit', 'form', this.proxy('save_current')); - this.$el.on('click', 'input[type=checkbox]', function() { - $(this).siblings('input[type=checkbox]').prop('checked', false); - }); - this.$el.on('click', 'h4', function () { - self.$el.toggleClass('oe_opened'); - }); - }, - save_current: function () { - var self = this; - var $name = this.$('input:first'); - var private_filter = !this.$('#oe_searchview_custom_public').prop('checked'); - var set_as_default = this.$('#oe_searchview_custom_default').prop('checked'); - if (_.isEmpty($name.val())){ - this.do_warn(_t("Error"), _t("Filter name is required.")); - return false; + var warning = _t("This filter is global and will be removed for everybody if you continue."); + if (!(filter.user_id || confirm(warning))) { + return; } - var search = this.view.build_search_data(); - instance.web.pyeval.eval_domains_and_contexts({ - domains: search.domains, - contexts: search.contexts, - group_by_seq: search.groupbys || [] - }).done(function (results) { - if (!_.isEmpty(results.group_by)) { - results.context.group_by = results.group_by; + this.model.call('unlink', [filter.id]).done(function () { + $filter.remove(); + delete self.$filters[key]; + delete self.filters[key]; + if (_.isEmpty(self.filters)) { + self.$('li.divider').remove(); } - // Don't save user_context keys in the custom filter, otherwise end - // up with e.g. wrong uid or lang stored *and used in subsequent - // reqs* - var ctx = results.context; - _(_.keys(instance.session.user_context)).each(function (key) { - delete ctx[key]; - }); - var filter = { - name: $name.val(), - user_id: private_filter ? instance.session.uid : false, - model_id: self.view.model, - context: results.context, - domain: results.domain, - is_default: set_as_default, - action_id: self.custom_filters.get_action_id() - }; - // FIXME: current context? - return self.model.call('create_or_replace', [filter]).done(function (id) { - filter.id = id; - if (self.custom_filters) { - self.custom_filters.append_filter(filter); - } - self.$el - .removeClass('oe_opened') - .find('form')[0].reset(); - }); - }); - return false; + }); }, }); -instance.web.search.Filters = instance.web.search.Input.extend({ - template: 'SearchView.Filters', - _in_drawer: true, - start: function () { - var self = this; - var is_group = function (i) { return i instanceof instance.web.search.FilterGroup; }; - var visible_filters = _(this.drawer.controls).chain().reject(function (group) { - return _(_(group.children).filter(is_group)).isEmpty() - || group.modifiers.invisible; - }); - - var groups = visible_filters.map(function (group) { - var filters = _(group.children).filter(is_group); - return { - name: _.str.sprintf("%s %s", - group.icon, group.name), - filters: filters, - length: _(filters).chain().map(function (i) { - return i.filters.length; }).sum().value() - }; - }).value(); - - var $dl = $('
    ').appendTo(this.$el); - - var rendered_lines = _.map(groups, function (group) { - $('
    ').html(group.name).appendTo($dl); - var $dd = $('
    ').appendTo($dl); - return $.when.apply(null, _(group.filters).invoke('appendTo', $dd)); - }); - - return $.when.apply(this, rendered_lines); - }, -}); - -instance.web.search.Advanced = instance.web.search.Input.extend({ - template: 'SearchView.advanced', - _in_drawer: true, - start: function () { - var self = this; - this.$el - .on('keypress keydown keyup', function (e) { e.stopPropagation(); }) - .on('click', 'h4', function () { - self.$el.toggleClass('oe_opened'); - }).on('click', 'button.oe_add_condition', function () { - self.append_proposition(); - }).on('submit', 'form', function (e) { - e.preventDefault(); - self.commit_search(); - }); - return $.when( - this._super(), - new instance.web.Model(this.view.model).call('fields_get', { - context: this.view.dataset.context - }).done(function(data) { - self.fields = { - id: { string: 'ID', type: 'id', searchable: true } - }; - _.each(data, function(field_def, field_name) { - if (field_def.selectable !== false && field_name != 'id') { - self.fields[field_name] = field_def; - } - }); - })).done(function () { - self.append_proposition(); - }); - }, - append_proposition: function () { - var self = this; - return (new instance.web.search.ExtendedSearchProposition(this, this.fields)) - .appendTo(this.$('ul')).done(function () { - self.$('button.oe_apply').prop('disabled', false); - }); - }, - remove_proposition: function (prop) { - // removing last proposition, disable apply button - if (this.getChildren().length <= 1) { - this.$('button.oe_apply').prop('disabled', true); - } - prop.destroy(); - }, - commit_search: function () { - // Get domain sections from all propositions - var children = this.getChildren(); - var propositions = _.invoke(children, 'get_proposition'); - var domain = _(propositions).pluck('value'); - for (var i = domain.length; --i;) { - domain.unshift('|'); - } - - this.view.query.add({ - category: _t("Advanced"), - values: propositions, - field: { - get_context: function () { }, - get_domain: function () { return domain;}, - get_groupby: function () { } - } - }); - - // remove all propositions - _.invoke(children, 'destroy'); - // add new empty proposition - this.append_proposition(); - // TODO: API on searchview - this.view.$el.removeClass('oe_searchview_open_drawer'); - } -}); - instance.web.search.ExtendedSearchProposition = instance.web.Widget.extend(/** @lends instance.web.search.ExtendedSearchProposition# */{ template: 'SearchView.extended_search.proposition', events: { @@ -2030,7 +1802,7 @@ instance.web.search.ExtendedSearchProposition = instance.web.Widget.extend(/** @ 'click .searchview_extended_delete_prop': function (e) { e.stopPropagation(); this.getParent().remove_proposition(this); - } + }, }, /** * @constructs instance.web.search.ExtendedSearchProposition @@ -2101,18 +1873,22 @@ instance.web.search.ExtendedSearchProposition = instance.web.Widget.extend(/** @ this.value.appendTo($value_loc); }, - get_proposition: function() { + get_filter: function () { if (this.attrs.selected === null || this.attrs.selected === undefined) return null; - var field = this.attrs.selected; - var op_select = this.$('.searchview_extended_prop_op')[0]; - var operator = op_select.options[op_select.selectedIndex]; + var field = this.attrs.selected, + op_select = this.$('.searchview_extended_prop_op')[0], + operator = op_select.options[op_select.selectedIndex]; return { - label: this.value.get_label(field, operator), - value: this.value.get_domain(field, operator), + attrs: { + domain: [this.value.get_domain(field, operator)], + string: this.value.get_label(field, operator), + }, + children: [], + tag: 'filter', }; - } + }, }); instance.web.search.ExtendedSearchProposition.Field = instance.web.Widget.extend({ @@ -2337,7 +2113,6 @@ instance.web.search.AutoComplete = instance.web.Widget.extend({ this.delay = options.delay; this.select = options.select, this.get_search_string = options.get_search_string; - this.width = options.width || 400; this.current_result = null; @@ -2347,7 +2122,6 @@ instance.web.search.AutoComplete = instance.web.Widget.extend({ }, start: function () { var self = this; - this.$el.width(this.width); this.$input.on('keyup', function (ev) { if (ev.which === $.ui.keyCode.RIGHT) { self.searching = true; diff --git a/addons/web/static/src/js/views.js b/addons/web/static/src/js/views.js index 88e62433adb..2659d46772f 100644 --- a/addons/web/static/src/js/views.js +++ b/addons/web/static/src/js/views.js @@ -19,9 +19,7 @@ instance.web.ActionManager = instance.web.Widget.extend({ this.dialog = null; this.dialog_widget = null; this.view_managers = []; - this.on('history_back', this, function() { - return this.history_back(); - }); + this.on('history_back', this, this.proxy('history_back')); }, dialog_stop: function (reason) { if (this.dialog) { @@ -53,11 +51,20 @@ instance.web.ActionManager = instance.web.Widget.extend({ var to_destroy = this.view_managers; this.view_managers = []; } - this.view_managers.push(widget); + if (widget instanceof instance.web.ViewManager) { + this.view_managers.push(widget); + } else { + this.view_managers.push({ + view_stack: [{ + controller: {get: function () {return action.display_name || action.name; }}, + }], + destroy: function () {}, + }); + } this.inner_action = action; this.inner_widget = widget; return $.when(this.inner_widget.appendTo(this.$el)).done(function () { - widget.$header && widget.$header.show(); + (action.target !== 'inline') && (!action.flags.headless) && widget.$header && widget.$header.show(); old_widget && old_widget.$el.hide(); if (clear_breadcrumbs) { self.clear_view_managers(to_destroy) @@ -76,33 +83,32 @@ instance.web.ActionManager = instance.web.Widget.extend({ }), true); }, history_back: function() { - // var last = this.breadcrumbs.slice(-1)[0]; - // if (!last) { - // return false; - // } - // var title = last.get_title(); - // if (_.isArray(title) && title.length > 1) { - // return this.select_breadcrumb(this.breadcrumbs.length - 1, title.length - 2); - // } else if (this.breadcrumbs.length === 1) { - // // Only one single titled item in breadcrumb, most of the time you want to trigger back to home - // return false; - // } else { - // var prev = this.breadcrumbs[this.breadcrumbs.length - 2]; - // title = prev.get_title(); - // return this.select_breadcrumb(this.breadcrumbs.length - 2, _.isArray(title) ? title.length - 1 : undefined); - // } + var view_manager = _.last(this.view_managers), + nbr_views = view_manager.view_stack.length; + if (nbr_views > 1) { + this.select_view_manager(view_manager, nbr_views - 2); + } else if (this.view_managers.length > 1) { + view_manager = this.view_managers[this.view_managers.length -2]; + nbr_views = view_manager.view_stack.length; + this.select_view(view_managers, nbr_views - 2) + } }, select_view_manager: function(view_manager, index) { var self = this; + if (this.webclient.has_uncommitted_changes()) { + return false; + } var vm_index = this.view_managers.indexOf(view_manager); - view_manager.select_view(index).done(function () { - _.each(self.view_managers.splice(vm_index + 1), function (vm) { - vm.destroy(); + if (view_manager.select_view) { + view_manager.select_view(index).done(function () { + _.each(self.view_managers.splice(vm_index + 1), function (vm) { + vm.destroy(); + }); + self.inner_widget = _.last(self.view_managers); + self.inner_widget.display_breadcrumbs(); + self.inner_widget.$el.show(); }); - self.inner_widget = _.last(self.view_managers); - self.inner_widget.display_breadcrumbs(); - self.inner_widget.$el.show(); - }); + } }, clear_view_managers: function(vms) { _.each(vms || this.view_managers, function (vm) { @@ -127,7 +133,7 @@ instance.web.ActionManager = instance.web.Widget.extend({ // }); }, do_push_state: function(state) { - if (!this.webclient || !this.webclient.do_push_state) { + if (!this.webclient || !this.webclient.do_push_state || this.dialog) { return; } state = state || {}; @@ -168,9 +174,7 @@ instance.web.ActionManager = instance.web.Widget.extend({ } } } - if(!this.dialog) { - this.webclient.do_push_state(state); - } + this.webclient.do_push_state(state); }, do_load_state: function(state, warm) { var self = this, @@ -386,6 +390,9 @@ instance.web.ActionManager = instance.web.Widget.extend({ $buttons: this.dialog.$buttons, footer_to_buttons: true, }); + if (widget.action.view_mode === 'form') { + widget.flags.headless = true; + } } this.dialog_widget = widget; this.dialog_widget.setParent(this.dialog); @@ -398,9 +405,6 @@ instance.web.ActionManager = instance.web.Widget.extend({ } widget = executor.widget(); this.dialog_stop(executor.action); - if (options.clear_breadcrumbs) { - // this.clear_view_managers(); - } return this.push_view_manager(widget, executor.action, options.clear_breadcrumbs); }, ir_actions_act_window: function (action, options) { @@ -412,13 +416,6 @@ instance.web.ActionManager = instance.web.Widget.extend({ }, action: action, klass: 'oe_act_window', - // post_process: function (widget) { - // self.push_breadcrumb({ - // widget: widget, - // on_reverse_breadcrumb: options.on_reverse_breadcrumb, - // hide_breadcrumb: options.hide_breadcrumb, - // }); - // }, }, options); }, ir_actions_client: function (action, options) { @@ -440,18 +437,9 @@ instance.web.ActionManager = instance.web.Widget.extend({ widget: function () { return new ClientWidget(self, action); }, action: action, klass: 'oe_act_client', - // post_process: function(widget) { - // self.push_breadcrumb({ - // widget: widget, - // title: action.name, - // on_reverse_breadcrumb: options.on_reverse_breadcrumb, - // hide_breadcrumb: options.hide_breadcrumb, - // }); - // if (action.tag !== 'reload') { - // self.do_push_state({}); - // } - // } - }, options); + }, options).then(function () { + if (action.tag !== 'reload') {self.do_push_state({});} + }); }, ir_actions_act_window_close: function (action, options) { if (!this.dialog) { @@ -544,38 +532,46 @@ instance.web.ViewManager = instance.web.Widget.extend({ _.each(views, function (view) { var view_type = view[1] || view.view_type, View = instance.web.views.get_object(view_type, true), - view_label = View ? View.prototype.display_name : (void 'nope'); - self.view_order.push(view_type); - self.views[view_type] = { - controller: null, - options: view.options || {}, - view_id: view[0] || view.view_id, - type: view_type, - label: view_label, - embedded_view: view.embedded_view, - title: self.action && self.action.name, - button_label: View ? _.str.sprintf(_t('%(view_type)s view'), {'view_type': (view_label || view_type)}) : (void 'nope'), - }; + view_label = View ? View.prototype.display_name: (void 'nope'), + view = { + controller: null, + options: view.options || {}, + view_id: view[0] || view.view_id, + type: view_type, + label: view_label, + embedded_view: view.embedded_view, + title: self.action && self.action.name, + button_label: View ? _.str.sprintf(_t('%(view_type)s view'), {'view_type': (view_label || view_type)}) : (void 'nope'), + }; + self.view_order.push(view); + self.views[view_type] = view; }); + this.multiple_views = (self.view_order.length - ('form' in this.views ? 1 : 0)) > 1; }, /** * @returns {jQuery.Deferred} initial view loading promise */ start: function() { var self = this; - var default_view = this.flags.default_view || this.view_order[0], + var default_view = this.flags.default_view || this.view_order[0].type, default_options = this.flags[default_view] && this.flags[default_view].options; + if (this.flags.headless) { + this.$('.oe-view-manager-header').hide(); + } this._super(); - var $sidebar = this.flags.sidebar ? this.$('.oe_view_manager_sidebar') : undefined, + var $sidebar = this.flags.sidebar ? this.$('.oe-view-manager-sidebar') : undefined, $pager = this.$('.oe-view-manager-pager'); this.$breadcrumbs = this.$('.oe-view-title'); - this.$switch_buttons = this.$('.oe-view-manager-switch a'); + this.$switch_buttons = this.$('.oe-view-manager-switch button'); this.$header = this.$('.oe-view-manager-header'); - - self.$switch_buttons.click(function () { - self.switch_mode($(this).data('view-type')); + this.$header_col = this.$header.find('.oe-header-title'); + this.$search_col = this.$header.find('.oe-view-manager-search-view'); + this.$switch_buttons.click(function (event) { + if (!$(event.target).hasClass('active')) { + self.switch_mode($(this).data('view-type')); + } }); var views_ids = {}; _.each(this.views, function (view) { @@ -587,21 +583,29 @@ instance.web.ViewManager = instance.web.Widget.extend({ action : self.action, action_views_ids : views_ids, }, self.flags, self.flags[view.type], view.options); + if (view.type !== 'form') { + self.$('.oe-vm-switch-' + view.type).tooltip(); + } }); + this.$('.oe_debug_view').click(this.on_debug_changed); + this.$el.addClass("oe_view_manager_" + ((this.action && this.action.target) || 'current')); - var search_view_loaded = this.flags.search_view ? this.setup_search_view() : $.when(), - main_view_loaded = this.switch_mode(default_view, null, default_options); + this.search_view_loaded = this.setup_search_view(); + var main_view_loaded = this.switch_mode(default_view, null, default_options); - return $.when(main_view_loaded, search_view_loaded); + return $.when(main_view_loaded, this.search_view_loaded); }, switch_mode: function(view_type, no_store, view_options) { var self = this, view = this.views[view_type]; + if (!view) { + return $.Deferred().reject(); + } if (view_type !== 'form') { this.view_stack = []; - } + } this.view_stack.push(view); this.active_view = view; @@ -613,7 +617,7 @@ instance.web.ViewManager = instance.web.Widget.extend({ if (this.searchview && this.flags.auto_search && view.controller.searchable !== false) { - $.when(this.searchview.ready, view.created).done(this.searchview.do_search); + $.when(this.search_view_loaded, view.created).done(this.searchview.do_search); } else { this.active_search.resolve(); } @@ -623,6 +627,12 @@ instance.web.ViewManager = instance.web.Widget.extend({ self.active_view = view; self._display_view(view.type, view_options); self.trigger('switch_mode', view_type, no_store, view_options); + if (self.session.debug) { + self.$('.oe_debug_view').html(QWeb.render('ViewManagerDebug', { + view: self.active_view.controller, + view_manager: self, + })); + } }); }, update_header: function () { @@ -642,8 +652,10 @@ instance.web.ViewManager = instance.web.Widget.extend({ }); self.active_view.options.$buttons && self.active_view.options.$buttons.show(); if (self.searchview) { - var is_hidden = self.active_view.controller.searchable === false || self.searchview.options.hidden; + var is_hidden = self.active_view.controller.searchable === false; self.searchview.toggle_visibility(!is_hidden); + self.$header_col.toggleClass('col-md-6', !is_hidden).toggleClass('col-md-12', is_hidden); + self.$search_col.toggle(!is_hidden); } self.display_breadcrumbs(); }); @@ -667,7 +679,8 @@ instance.web.ViewManager = instance.web.Widget.extend({ create_view: function(view) { var self = this, View = this.registry.get_object(view.type), - options = _.clone(view.options); + options = _.clone(view.options), + view_loaded = $.Deferred(); if (view.type === "form" && this.action && (this.action.target === 'new' || this.action.target === 'inline')) { options.initial_mode = 'edit'; @@ -677,25 +690,26 @@ instance.web.ViewManager = instance.web.Widget.extend({ $container.hide(); view.controller = controller; + view.$container = $container; if (view.embedded_view) { controller.set_embedded_view(view.embedded_view); } controller.on('switch_mode', this, this.switch_mode.bind(this)); - - view.$container = $container; - - var view_loaded = $.Deferred(); - view.controller.on('view_loaded', this, function () { + controller.on('history_back', this, function () { + self.action_manager && self.action_manager.trigger('history_back'); + }); + controller.on("change:title", this, function() { + self.display_breadcrumbs(); + }); + controller.on('view_loaded', this, function () { view_loaded.resolve(); }); - return $.when(controller.appendTo($container), view_loaded) .done(function () { self.trigger("controller_inited", view.type, controller); }); }, - select_view: function (index) { var view_type = this.view_stack[index].type; this.view_stack.splice(index); @@ -713,6 +727,7 @@ instance.web.ViewManager = instance.web.Widget.extend({ } var view_id = (this.action && this.action.search_view_id && this.action.search_view_id[0]) || false; + var search_defaults = {}; var context = this.action ? this.action.context : []; @@ -727,13 +742,13 @@ instance.web.ViewManager = instance.web.Widget.extend({ var options = { hidden: this.flags.search_view === false, disable_custom_filters: this.flags.search_disable_custom_filters, + $buttons: this.$('.oe-search-options'), }; var SearchView = instance.web.SearchView; this.searchview = new SearchView(this, this.dataset, view_id, search_defaults, options); this.searchview.on('search_data', this, this.search.bind(this)); - return this.searchview.appendTo(this.$(".oe-view-manager-search-view:first"), - this.$('.oe_searchview_drawer_container')); + return this.searchview.appendTo(this.$(".oe-view-manager-search-view:first")); }, search: function(domains, contexts, groupbys) { var self = this, @@ -783,6 +798,145 @@ instance.web.ViewManager = instance.web.Widget.extend({ self.active_view.controller.do_load_state(state, warm); }); }, + on_debug_changed: function (evt) { + var self = this, + params = $(evt.target).data(), + val = params.action, + current_view = this.active_view.controller; + switch (val) { + case 'fvg': + var dialog = new instance.web.Dialog(this, { title: _t("Fields View Get") }).open(); + $('
    ').text(instance.web.json_node_to_xml(current_view.fields_view.arch, true)).appendTo(dialog.$el);
    +                break;
    +            case 'tests':
    +                this.do_action({
    +                    name: _t("JS Tests"),
    +                    target: 'new',
    +                    type : 'ir.actions.act_url',
    +                    url: '/web/tests?mod=*'
    +                });
    +                break;
    +            case 'get_metadata':
    +                var ids = current_view.get_selected_ids();
    +                if (ids.length === 1) {
    +                    this.dataset.call('get_metadata', [ids]).done(function(result) {
    +                        var dialog = new instance.web.Dialog(this, {
    +                            title: _.str.sprintf(_t("Metadata (%s)"), self.dataset.model),
    +                            size: 'medium',
    +                        }, QWeb.render('ViewManagerDebugViewLog', {
    +                            perm : result[0],
    +                            format : instance.web.format_value
    +                        })).open();
    +                    });
    +                }
    +                break;
    +            case 'toggle_layout_outline':
    +                current_view.rendering_engine.toggle_layout_debugging();
    +                break;
    +            case 'set_defaults':
    +                current_view.open_defaults_dialog();
    +                break;
    +            case 'translate':
    +                this.do_action({
    +                    name: _t("Technical Translation"),
    +                    res_model : 'ir.translation',
    +                    domain : [['type', '!=', 'object'], '|', ['name', '=', this.dataset.model], ['name', 'ilike', this.dataset.model + ',']],
    +                    views: [[false, 'list'], [false, 'form']],
    +                    type : 'ir.actions.act_window',
    +                    view_type : "list",
    +                    view_mode : "list"
    +                });
    +                break;
    +            case 'fields':
    +                this.dataset.call('fields_get', [false, {}]).done(function (fields) {
    +                    var $root = $('
    '); + _(fields).each(function (attributes, name) { + $root.append($('
    ').append($('

    ').text(name))); + var $attrs = $('
    ').appendTo($('
    ').appendTo($root)); + _(attributes).each(function (def, name) { + if (def instanceof Object) { + def = JSON.stringify(def); + } + $attrs + .append($('
    ').text(name)) + .append($('
    ').text(def)); + }); + }); + new instance.web.Dialog(self, { + title: _.str.sprintf(_t("Model %s fields"), + self.dataset.model), + }, $root).open(); + }); + break; + case 'edit_workflow': + return this.do_action({ + res_model : 'workflow', + name: _t('Edit Workflow'), + domain : [['osv', '=', this.dataset.model]], + views: [[false, 'list'], [false, 'form'], [false, 'diagram']], + type : 'ir.actions.act_window', + view_type : 'list', + view_mode : 'list' + }); + case 'edit': + this.do_edit_resource(params.model, params.id, evt.target.text); + break; + case 'manage_filters': + this.do_action({ + res_model: 'ir.filters', + name: _t('Manage Filters'), + views: [[false, 'list'], [false, 'form']], + type: 'ir.actions.act_window', + context: { + search_default_my_filters: true, + search_default_model_id: this.dataset.model + } + }); + break; + case 'print_workflow': + if (current_view.get_selected_ids && current_view.get_selected_ids().length == 1) { + instance.web.blockUI(); + var action = { + context: { active_ids: current_view.get_selected_ids() }, + report_name: "workflow.instance.graph", + datas: { + model: this.dataset.model, + id: current_view.get_selected_ids()[0], + nested: true, + } + }; + this.session.get_file({ + url: '/web/report', + data: {action: JSON.stringify(action)}, + complete: instance.web.unblockUI + }); + } + break; + case 'leave_debug': + window.location.search="?"; + break; + default: + if (val) { + console.warn("No debug handler for ", val); + } + } + }, + do_edit_resource: function(model, id, name) { + this.do_action({ + res_model : model, + res_id : id, + name: name, + type : 'ir.actions.act_window', + view_type : 'form', + view_mode : 'form', + views : [[false, 'form']], + target : 'new', + flags : { + action_buttons : true, + headless: true, + } + }); + }, }); instance.web.ViewManagerAction = instance.web.ViewManager.extend({ @@ -793,9 +947,25 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({ if (!('auto_search' in flags)) { flags.auto_search = action.auto_search !== false; } + if (action.res_model === 'board.board' && action.view_mode === 'form') { + action.target = 'inline'; + // Special case for Dashboards + _.extend(flags, { + views_switcher : false, + display_title : false, + search_view : false, + pager : false, + sidebar : false, + action_buttons : false + }); + } this.action = action; this.action_manager = parent; var dataset = new instance.web.DataSetSearch(this, action.res_model, action.context, action.domain); + if (action.res_id) { + dataset.ids.push(action.res_id); + dataset.index = 0; + } this._super(parent, dataset, action.views, flags); } }); @@ -824,7 +994,7 @@ instance.web.Sidebar = instance.web.Widget.extend({ var self = this; this._super(this); this.redraw(); - this.$el.on('click','.oe_dropdown_menu li a', function(event) { + this.$el.on('click','.dropdown-menu li a', function(event) { var section = $(this).data('section'); var index = $(this).data('index'); var item = self.items[section][index]; diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index e27524cc2c8..c45278196eb 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -25,13 +25,6 @@ - - [ - -
    - -
    -