summaryrefslogtreecommitdiff
path: root/addons/web/static/src
diff options
context:
space:
mode:
authorXavier Morel <xmo@openerp.com>2012-05-22 15:24:42 +0200
committerXavier Morel <xmo@openerp.com>2012-05-22 15:24:42 +0200
commitb23e1ed416c7c211eff62cab341c1edb22d1c5d8 (patch)
treef36558de76f8de99e46c4d9fe476b018d50d4cbc /addons/web/static/src
parentca6283de8efc7d790fe1d6d843ec264777b3ae47 (diff)
parent983b63f8edaaebbb1cc8c701e23912730fb2bf80 (diff)
[IMP] search view
bzr revid: xmo@openerp.com-20120522132442-am276368d5nkwn2k
Diffstat (limited to 'addons/web/static/src')
-rw-r--r--addons/web/static/src/css/base.css256
-rw-r--r--addons/web/static/src/css/base.sass285
-rw-r--r--addons/web/static/src/js/formats.js4
-rw-r--r--addons/web/static/src/js/search.js1412
-rw-r--r--addons/web/static/src/js/views.js11
-rw-r--r--addons/web/static/src/xml/base.xml95
6 files changed, 1301 insertions, 762 deletions
diff --git a/addons/web/static/src/css/base.css b/addons/web/static/src/css/base.css
index 4a426e13e39..f2fcc9abf33 100644
--- a/addons/web/static/src/css/base.css
+++ b/addons/web/static/src/css/base.css
@@ -1160,140 +1160,164 @@
white-space: nowrap;
}
.openerp .oe_searchview {
+ cursor: text;
position: relative;
float: right;
-}
-.openerp .oe_searchview .VS-search .VS-search-box {
- min-height: 0;
- padding: 0;
- width: 480px;
+ padding-right: 20px;
+ width: 410px;
border: 1px solid #ababab;
- -moz-border-radius: 13px;
- -webkit-border-radius: 13px;
- border-radius: 13px;
+ background: white;
+ -moz-border-radius: 1em;
+ -webkit-border-radius: 1em;
+ border-radius: 1em;
-moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2) inset;
-webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2) inset;
-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2) inset;
}
-.openerp .oe_searchview .VS-search .VS-icon {
- top: 6px;
+.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 .VS-search .VS-icon-search {
- left: 2px;
- top: 3px;
- height: 18px;
- width: 18px;
+.openerp .oe_searchview .oe_searchview_clear {
+ cursor: pointer;
+ position: absolute;
+ top: 0;
+ right: 22px;
+ width: 15px;
+ height: 100%;
+ background: url(../img/attachments-close.png) center center no-repeat;
}
-.openerp .oe_searchview .VS-search .VS-search-inner {
- margin: 0 40px 0 17px;
- padding: 1px 0;
- font-size: 13px;
+.openerp .oe_searchview .oe_searchview_unfold_drawer {
+ position: absolute;
+ top: 0;
+ right: 0;
+ height: 100%;
+ line-height: 2.5em;
+ padding: 0 7px 0 4px;
+ color: #cccccc;
+ cursor: pointer;
}
-.openerp .oe_searchview .VS-search .VS-search-inner input {
- font-size: inherit;
- line-height: inherit;
- height: auto;
- padding: 0;
+.openerp .oe_searchview .oe_searchview_unfold_drawer:hover {
+ color: #999999;
}
-.openerp .oe_searchview .VS-search .VS-search-inner .VS-input-width-tester {
- font-size: inherit;
- padding: 0;
+.openerp .oe_searchview .oe_searchview_unfold_drawer:before {
+ content: "◀";
}
-.openerp .oe_searchview .VS-search .VS-search-inner .search_input {
- margin-left: 0;
- height: 22px;
+.openerp .oe_searchview .oe_searchview_facets:before {
+ color: #cccccc;
+ font-family: "mnmliconsRegular";
+ content: "r";
+ font-size: 150%;
+ padding: 0 1px 0 3px;
+ display: inline;
}
-.openerp .oe_searchview .VS-search .VS-search-inner .search_input input, .openerp .oe_searchview .VS-search .VS-search-inner .search_input .VS-input-width-tester {
- height: inherit;
- margin: 0;
+.openerp .oe_searchview .oe_searchview_facets * {
+ vertical-align: top;
+ display: inline-block;
+ line-height: 26px;
}
-.openerp .oe_searchview .VS-search .VS-search-inner .search_facet {
- padding: 0;
- margin: 1px 0;
+.openerp .oe_searchview .oe_searchview_facets .oe_searchview_input, .openerp .oe_searchview .oe_searchview_facets .oe_searchview_facet {
+ height: 26px;
+}
+.openerp .oe_searchview .oe_searchview_facets .oe_searchview_input:focus, .openerp .oe_searchview .oe_searchview_facets .oe_searchview_facet:focus {
+ outline: none;
+}
+.openerp .oe_searchview .oe_searchview_facets .oe_searchview_input {
+ padding: 0 3px;
+}
+.openerp .oe_searchview .oe_searchview_facets .oe_searchview_facet {
+ position: relative;
+ cursor: pointer;
border: 1px solid #afafb6;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
border-radius: 3px;
- background: #f0f0fa;
- height: 18px;
+ background: #8786b7;
+ -webkit-font-smoothing: auto;
+ padding-left: 1.1em;
+ margin: 1px 0;
}
-.openerp .oe_searchview .VS-search .VS-search-inner .search_facet.is_selected {
+.openerp .oe_searchview .oe_searchview_facets .oe_searchview_facet:focus {
border-color: #a6a6fe;
-moz-box-shadow: 0 0 3px 1px #a6a6fe;
-webkit-box-shadow: 0 0 3px 1px #a6a6fe;
-box-shadow: 0 0 3px 1px #a6a6fe;
}
-.openerp .oe_searchview .VS-search .VS-search-inner .search_facet.is_selected .category {
- margin-left: 0;
+.openerp .oe_searchview .oe_searchview_facets .oe_searchview_facet .oe_facet_values {
+ background: #f0f0fa;
+ -moz-border-radius: 0 3px 3px 0;
+ -webkit-border-radius: 0 3px 3px 0;
+ border-radius: 0 3px 3px 0;
}
-.openerp .oe_searchview .VS-search .VS-search-inner .search_facet.is_selected .VS-icon-cancel {
- filter: alpha(opacity=100);
- opacity: 1;
- background-position: center 0;
+.openerp .oe_searchview .oe_searchview_facets .oe_searchview_facet .oe_facet_category, .openerp .oe_searchview .oe_searchview_facets .oe_searchview_facet .oe_facet_value {
+ height: 24px;
+ padding: 1px 0.1em;
}
-.openerp .oe_searchview .VS-search .VS-search-inner .search_facet .category {
- padding: 0 4px 0 14px;
+.openerp .oe_searchview .oe_searchview_facets .oe_searchview_facet .oe_facet_category {
color: white;
- background: #8786b7;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4);
- font-weight: normal;
- text-transform: none;
- height: 18px;
- line-height: 18px;
- font-size: inherit;
}
-.openerp .oe_searchview .VS-search .VS-search-inner .search_facet .search_facet_input_container {
+.openerp .oe_searchview .oe_searchview_facets .oe_searchview_facet .oe_facet_value {
border-left: 1px solid #afafb6;
- height: 18px;
- line-height: 18px;
- padding: 0 4px;
- cursor: pointer;
-}
-.openerp .oe_searchview .VS-search .VS-search-inner .search_facet .search_facet_remove {
- left: 1px;
- top: 3px;
}
-.openerp .oe_searchview .VS-search .VS-icon-cancel {
- right: 24px;
-}
-.openerp .oe_searchview .VS-search .oe_vs_unfold_drawer {
+.openerp .oe_searchview .oe_searchview_facets .oe_searchview_facet .oe_facet_remove {
position: absolute;
top: 0;
- right: 0;
- height: 100%;
- line-height: 23px;
- padding: 0 7px 0 4px;
- color: #4c4c4c;
- cursor: pointer;
+ left: 2px;
+ color: white;
}
-.openerp .oe_searchview .VS-search .oe_vs_unfold_drawer:before {
- position: absolute;
- top: 9px;
- right: 8px;
- width: 0;
- height: 0;
- display: inline-block;
- content: "";
- vertical-align: top;
- border-top: 5px solid #4c4c4c;
- border-left: 5px solid transparent;
- border-right: 5px solid transparent;
- filter: alpha(opacity=50);
- opacity: 0.5;
+.openerp .oe_searchview .oe_searchview_facets .oe_searchview_facet .oe_facet_remove:hover {
+ color: #cccccc;
}
.openerp .oe_searchview.oe_searchview_open_drawer .oe_searchview_drawer {
display: block;
}
+.openerp .oe_searchview.oe_searchview_open_drawer .oe_searchview_unfold_drawer:before {
+ content: "▼";
+}
.openerp .oe_searchview .oe_searchview_drawer {
position: absolute;
z-index: 1;
+ margin-top: 3px;
top: 100%;
right: 0;
background-color: white;
- width: 480px;
+ min-width: 100%;
display: none;
border: 1px solid #cccccc;
text-align: left;
+ padding-bottom: 0.5em;
+ -moz-border-radius: 1em;
+ -webkit-border-radius: 1em;
+ border-radius: 1em;
+}
+.openerp .oe_searchview .oe_searchview_drawer > div {
+ border-top: 1px solid #cccccc;
+ margin: 3px 0;
+}
+.openerp .oe_searchview .oe_searchview_drawer > div:first-child {
+ border-top: none;
+ margin: 0;
+}
+.openerp .oe_searchview .oe_searchview_drawer h4, .openerp .oe_searchview .oe_searchview_drawer h4 * {
+ margin: 0;
+ cursor: pointer;
+}
+.openerp .oe_searchview .oe_searchview_drawer h4:before {
+ content: "▸ ";
+}
+.openerp .oe_searchview .oe_searchview_drawer button, .openerp .oe_searchview .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 .oe_searchview_drawer .oe_searchview_filters {
display: table;
@@ -1317,23 +1341,49 @@
}
.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_filters li {
list-style: none;
- padding: 3px 6px;
- height: 14px;
+ padding: 3px 6px 3px 18px;
+ line-height: 14px;
color: inherit;
cursor: pointer;
}
+.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_filters li.oe_selected {
+ background: url(/web/static/src/img/icons/gtk-apply.png) left 2px no-repeat;
+}
.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_filters li:hover {
background-color: #f0f0fa;
}
-.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_advanced h4 {
- border-top: 1px solid #cccccc;
- margin: 5px 0 3px;
+.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_custom form {
+ display: none;
+}
+.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_custom form button {
+ font-size: 1px;
+ letter-spacing: -1px;
+ color: transparent;
+}
+.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_custom form button:before {
+ font-family: "mnmliconsRegular";
+ content: "S";
+ font-size: 20px;
+ color: #404040;
+}
+.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_custom li {
cursor: pointer;
+ position: relative;
+ line-height: 1.2em;
+ padding: 2px 20px 2px 25px;
}
-.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_advanced h4:before {
- content: "▸ ";
+.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_custom li.oe_searchview_custom_private {
+ background: url(/web/static/src/img/icons/terp-locked.png) 5px center no-repeat;
+}
+.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_custom li:hover {
+ background-color: #f0f0fa;
+}
+.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_custom li button {
+ position: absolute;
+ top: 0;
+ right: 5px;
}
-.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_advanced div {
+.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_advanced form {
display: none;
}
.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_advanced button.oe_add_condition:before {
@@ -1349,13 +1399,25 @@
.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_advanced li {
list-style: none;
margin: 0;
+ white-space: nowrap;
}
-.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_advanced.oe_opened h4:before {
+.openerp .oe_searchview .oe_searchview_drawer .oe_opened h4:before {
content: "▾ ";
}
-.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_advanced.oe_opened div {
+.openerp .oe_searchview .oe_searchview_drawer .oe_opened form {
display: block;
}
+.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_custom_delete, .openerp .oe_searchview .oe_searchview_drawer .searchview_extended_delete_prop {
+ font-size: 1px;
+ letter-spacing: -1px;
+ color: transparent;
+}
+.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_custom_delete:before, .openerp .oe_searchview .oe_searchview_drawer .searchview_extended_delete_prop:before {
+ font-family: "mnmliconsRegular";
+ content: "d";
+ font-size: 20px;
+ color: #404040;
+}
.openerp .oe_view_nocontent > img {
float: left;
margin: 1.5em;
diff --git a/addons/web/static/src/css/base.sass b/addons/web/static/src/css/base.sass
index f128d93c4c1..3dcde9c9f70 100644
--- a/addons/web/static/src/css/base.sass
+++ b/addons/web/static/src/css/base.sass
@@ -4,6 +4,7 @@
$section-title-color: #8786b7
$facets-border: #afafb6
$facets-border-selected: #a6a6fe
+$hover-background: #f0f0fa
$colour4: #8a89ba
// }}}
// Mixins {{{
@@ -74,6 +75,18 @@ $colour4: #8a89ba
-webkit-box-sizing: border-box
box-sizing: border-box
+// Transforms the (readable) text of an inline element into an mmlicons icon,
+// allows for actual readable text in-code (and in readers?) with iconic looks
+@mixin text-to-icon($icon-name, $color: #404040)
+ font-size: 1px
+ letter-spacing: -1px
+ color: transparent
+ &:before
+ font-family: "mnmliconsRegular"
+ content: $icon-name
+ font-size: 20px
+ color: $color
+
// }}}
.openerp.openerp-web-client-container
@@ -918,132 +931,145 @@ $colour4: #8a89ba
// }}}
// SearchView xmo {{{
.oe_searchview
+ cursor: text
position: relative
float: right
- .VS-search
- .VS-search-box
- min-height: 0
- padding: 0
- width: 480px
- border: 1px solid #ababab
- @include radius(13px)
- @include box-shadow(0 1px 2px rgba(0,0,0,0.2) inset)
- //padding: 4px 19px
- //font-size: 13px
- //background: url('../img/search.png') no-repeat 5px
- //background-color: white
- .VS-icon
- top: 6px
-
- .VS-icon-search
- left: 2px
- top: 3px
- height: 18px
- width: 18px
-
- .VS-search-inner
- margin: 0 40px 0 17px
- padding: 1px 0
- font-size: 13px
-
- input
- font-size: inherit
- line-height: inherit
- height: auto
- padding: 0
+ padding-right: 20px
+ width: 410px
+ border: 1px solid #ababab
+ background: white
+ @include radius(1em)
+ @include box-shadow(0 1px 2px rgba(0,0,0,0.2) inset)
- .VS-input-width-tester
- font-size: inherit
- padding: 0
+ &.oe_focused
+ border-color: $facets-border-selected
+ @include box-shadow(0 1px 2px $facets-border-selected inset)
- .search_input
- margin-left: 0
- height: 22px
- input, .VS-input-width-tester
- height: inherit
- margin: 0
+ .oe_searchview_clear
+ cursor: pointer
+ position: absolute
+ top: 0
+ right: 22px
+ width: 15px
+ height: 100%
+ background: url(../img/attachments-close.png) center center no-repeat
- .search_facet
- padding: 0
- margin: 1px 0
- border: 1px solid $facets-border
- @include radius(3px)
- background: #f0f0fa
- height: 18px
+ .oe_searchview_unfold_drawer
+ position: absolute
+ top: 0
+ right: 0
+ height: 100%
+ line-height: 2.5em
+ padding: 0 7px 0 4px
+ color: #ccc
+ cursor: pointer
+ &:hover
+ color: #999
+ &:before
+ content: "◀"
+
+ .oe_searchview_facets
+ &:before
+ color: #ccc
+ font-family: "mnmliconsRegular"
+ content: "r"
+ font-size: 150%
+ padding: 0 1px 0 3px
+ display: inline
+
+ *
+ vertical-align: top
+ display: inline-block
+ line-height: 26px
- &.is_selected
- border-color: $facets-border-selected
- @include box-shadow(0 0 3px 1px $facets-border-selected)
+ .oe_searchview_input, .oe_searchview_facet
+ height: 26px
+ &:focus
+ outline: none
- .category
- margin-left: 0
- .VS-icon-cancel
- // don't change the icon on selection
- @include opacity(1.0)
- background-position: center 0
+ .oe_searchview_input
+ padding: 0 3px
- .category
- padding: 0 4px 0 14px
- color: white
- background: #8786b7
- text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4)
- font-weight: normal
- text-transform: none
- height: 18px
- line-height: 18px
- font-size: inherit
-
- .search_facet_input_container
- border-left: 1px solid $facets-border
- height: 18px
- line-height: 18px
- padding: 0 4px
- cursor: pointer
-
- .search_facet_remove
- left: 1px
- top: 3px
-
- .VS-icon-cancel
- right: 24px
-
- .oe_vs_unfold_drawer
- position: absolute
- top: 0
- right: 0
- height: 100%
- line-height: 23px
- padding: 0 7px 0 4px
- color: #4c4c4c
+ .oe_searchview_facet
+ position: relative
cursor: pointer
- &:before
+ border: 1px solid $facets-border
+ @include radius(3px)
+ background: #8786b7
+ -webkit-font-smoothing: auto
+ padding-left: 1.1em
+ // spacing for opera, FF
+ margin: 1px 0
+
+ &:focus
+ border-color: $facets-border-selected
+ @include box-shadow(0 0 3px 1px $facets-border-selected)
+
+ .oe_facet_values
+ background: #f0f0fa
+ @include radius(0 3px 3px 0)
+
+ .oe_facet_category, .oe_facet_value
+ height: 24px
+ padding: 1px 0.1em
+
+ .oe_facet_category
+ color: white
+ text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4)
+
+ .oe_facet_value
+ border-left: 1px solid $facets-border
+
+ .oe_facet_remove
position: absolute
- top: 9px
- right: 8px
- width: 0
- height: 0
- display: inline-block
- content: ""
- vertical-align: top
- border-top: 5px solid #4c4c4c
- border-left: 5px solid transparent
- border-right: 5px solid transparent
- @include opacity(0.5)
+ top: 0
+ left: 2px
+ color: white
+
+ &:hover
+ color: #ccc
&.oe_searchview_open_drawer
.oe_searchview_drawer
display: block
+ .oe_searchview_unfold_drawer:before
+ content: "▼"
.oe_searchview_drawer
position: absolute
z-index: 1
+ // detach drawer from field slightly
+ margin-top: 3px
top: 100%
right: 0
background-color: white
- width: 480px
+ min-width: 100%
display: none
border: 1px solid #ccc
text-align: left
+ padding-bottom: 0.5em
+ @include radius(1em)
+
+ > div
+ border-top: 1px solid #ccc
+ margin: 3px 0
+ > div:first-child
+ border-top: none
+ margin: 0
+
+ h4, h4 *
+ margin: 0
+ cursor: pointer
+
+ h4:before
+ content: "▸ "
+
+ button, .button
+ border: none
+ background: transparent
+ padding: 0 2px
+ @include box-shadow(none)
+ @include radius(0)
.oe_searchview_filters
display: table
@@ -1065,24 +1091,44 @@ $colour4: #8a89ba
li
list-style: none
- padding: 3px 6px
- height: 14px
+ padding: 3px 6px 3px 18px
+ line-height: 14px
color: inherit
cursor: pointer
+ &.oe_selected
+ background: url(/web/static/src/img/icons/gtk-apply.png) left 2px no-repeat
+ // after oe_selected so background color is not overridden
&:hover
- background-color: #f0f0fa
+ background-color: $hover-background
- .oe_searchview_advanced
- h4
- border-top: 1px solid #ccc
- margin: 5px 0 3px
+
+ .oe_searchview_custom
+ form
+ display: none
+
+ button
+ @include text-to-icon("S")
+ li
cursor: pointer
+ position: relative
+ line-height: 1.2em
+ padding: 2px 20px 2px 25px
- h4:before
- content: "▸ "
+ &.oe_searchview_custom_private
+ background: url(/web/static/src/img/icons/terp-locked.png) 5px center no-repeat
+
+ &:hover
+ background-color: $hover-background
- div
+ button
+ position: absolute
+ top: 0
+ right: 5px
+
+
+ .oe_searchview_advanced
+ form
display: none
button.oe_add_condition:before
@@ -1096,12 +1142,17 @@ $colour4: #8a89ba
li
list-style: none
margin: 0
+ white-space: nowrap
+
+ .oe_opened
+ h4:before
+ content: "▾ "
+ form
+ display: block
- &.oe_opened
- h4:before
- content: "▾ "
- div
- display: block
+ // delete buttons
+ .oe_searchview_custom_delete, .searchview_extended_delete_prop
+ @include text-to-icon("d")
// }}}
// Views Common {{{
.oe_view_nocontent
diff --git a/addons/web/static/src/js/formats.js b/addons/web/static/src/js/formats.js
index 640ba4d5a93..2b527686ddd 100644
--- a/addons/web/static/src/js/formats.js
+++ b/addons/web/static/src/js/formats.js
@@ -111,7 +111,7 @@ instance.web.format_value = function (value, descriptor, value_if_empty) {
return value_if_empty === undefined ? '' : value_if_empty;
}
var l10n = _t.database.parameters;
- switch (descriptor.type || (descriptor.field && descriptor.field.type)) {
+ switch (descriptor.widget || descriptor.type || (descriptor.field && descriptor.field.type)) {
case 'id':
return value.toString();
case 'integer':
@@ -171,7 +171,7 @@ instance.web.parse_value = function (value, descriptor, value_if_empty) {
case "":
return value_if_empty === undefined ? false : value_if_empty;
}
- switch (descriptor.type || (descriptor.field && descriptor.field.type)) {
+ switch (descriptor.widget || descriptor.type || (descriptor.field && descriptor.field.type)) {
case 'integer':
var tmp;
do {
diff --git a/addons/web/static/src/js/search.js b/addons/web/static/src/js/search.js
index c2df866b5c6..c70cf2ab470 100644
--- a/addons/web/static/src/js/search.js
+++ b/addons/web/static/src/js/search.js
@@ -6,67 +6,269 @@ _.mixin({
sum: function (obj) { return _.reduce(obj, function (a, b) { return a + b; }, 0); }
});
-// Have SearchBox optionally use callback function to produce inputs and facets
-// (views) set on callbacks.make_facet and callbacks.make_input keys when
-// initializing VisualSearch
-var SearchBox_renderFacet = function (facet, position) {
- var view = new (this.app.options.callbacks['make_facet'] || VS.ui.SearchFacet)({
- app : this.app,
- model : facet,
- order : position
- });
+/** @namespace */
+var my = instance.web.search = {};
- // Input first, facet second.
- this.renderSearchInput();
- this.facetViews.push(view);
- this.$('.VS-search-inner').children().eq(position*2).after(view.render().el);
-
- view.calculateSize();
- _.defer(_.bind(view.calculateSize, view));
-
- return view;
- }; // warning: will not match
-// Ensure we're replacing the function we think
-if (SearchBox_renderFacet.toString() !== VS.ui.SearchBox.prototype.renderFacet.toString().replace(/(VS\.ui\.SearchFacet)/, "(this.app.options.callbacks['make_facet'] || $1)")) {
- throw new Error(
- "Trying to replace wrong version of VS.ui.SearchBox#renderFacet. "
- + "Please fix replacement.");
-}
-var SearchBox_renderSearchInput = function () {
- var input = new (this.app.options.callbacks['make_input'] || VS.ui.SearchInput)({position: this.inputViews.length, app: this.app});
- this.$('.VS-search-inner').append(input.render().el);
- this.inputViews.push(input);
- };
-// Ensure we're replacing the function we think
-if (SearchBox_renderSearchInput.toString() !== VS.ui.SearchBox.prototype.renderSearchInput.toString().replace(/(VS\.ui\.SearchInput)/, "(this.app.options.callbacks['make_input'] || $1)")) {
- throw new Error(
- "Trying to replace wrong version of VS.ui.SearchBox#renderSearchInput. "
- + "Please fix replacement.");
-}
-var SearchBox_searchEvent = function (e) {
- var query = null;
- this.renderFacets();
- this.focusSearch(e);
- this.app.options.callbacks.search(query, this.app.searchQuery);
- };
-if (SearchBox_searchEvent.toString() !== VS.ui.SearchBox.prototype.searchEvent.toString().replace(
- /this\.value\(\);\n[ ]{4}this\.focusSearch\(e\);\n[ ]{4}this\.value\(query\)/,
- 'null;\n this.renderFacets();\n this.focusSearch(e)')) {
- throw new Error(
- "Trying to replace wrong version of VS.ui.SearchBox#searchEvent. "
- + "Please fix replacement.");
-}
-_.extend(VS.ui.SearchBox.prototype, {
- renderFacet: SearchBox_renderFacet,
- renderSearchInput: SearchBox_renderSearchInput,
- searchEvent: SearchBox_searchEvent
+var B = Backbone;
+my.FacetValue = B.Model.extend({
+
+});
+my.FacetValues = B.Collection.extend({
+ model: my.FacetValue
+});
+my.Facet = B.Model.extend({
+ initialize: function (attrs) {
+ var values = attrs.values;
+ delete attrs.values;
+
+ B.Model.prototype.initialize.apply(this, arguments);
+
+ this.values = new my.FacetValues(values || []);
+ this.values.on('add remove change reset', function () {
+ this.trigger('change', this);
+ }, this);
+ },
+ get: function (key) {
+ if (key !== 'values') {
+ return B.Model.prototype.get.call(this, key);
+ }
+ return this.values.toJSON();
+ },
+ set: function (key, value) {
+ if (key !== 'values') {
+ return B.Model.prototype.set.call(this, key, value);
+ }
+ this.values.reset(value);
+ },
+ toJSON: function () {
+ var out = {};
+ var attrs = this.attributes;
+ for(var att in attrs) {
+ if (!attrs.hasOwnProperty(att) || att === 'field') {
+ continue;
+ }
+ out[att] = attrs[att];
+ }
+ out.values = this.values.toJSON();
+ return out;
+ }
+});
+my.SearchQuery = B.Collection.extend({
+ model: my.Facet,
+ initialize: function () {
+ B.Collection.prototype.initialize.apply(
+ this, arguments);
+ this.on('change', function (facet) {
+ if(!facet.values.isEmpty()) { return; }
+
+ this.remove(facet);
+ }, this);
+ },
+ add: function (values, options) {
+ options || (options = {});
+ if (!(values instanceof Array)) {
+ values = [values];
+ }
+
+ _(values).each(function (value) {
+ var model = this._prepareModel(value, options);
+ var previous = this.detect(function (facet) {
+ return facet.get('category') === model.get('category')
+ && facet.get('field') === model.get('field');
+ });
+ if (previous) {
+ previous.values.add(model.get('values'));
+ return;
+ }
+ B.Collection.prototype.add.call(this, model, options);
+ }, this);
+ return this;
+ },
+ toggle: function (value, options) {
+ options || (options = {});
+
+ var facet = this.detect(function (facet) {
+ return facet.get('category') === value.category
+ && facet.get('field') === value.field;
+ });
+ if (!facet) {
+ return this.add(value, options);
+ }
+
+ var changed = false;
+ _(value.values).each(function (val) {
+ var already_value = facet.values.detect(function (v) {
+ return v.get('value') === val.value
+ && v.get('label') === val.label;
+ });
+ // toggle value
+ if (already_value) {
+ facet.values.remove(already_value, {silent: true});
+ } else {
+ facet.values.add(val, {silent: true});
+ }
+ changed = true;
+ });
+ // "Commit" changes to values array as a single call, so observers of
+ // change event don't get misled by intermediate incomplete toggling
+ // states
+ facet.trigger('change', facet);
+ return this;
+ }
});
-_.extend(VS.model.SearchFacet.prototype, {
- value: function () {
- if (this.has('json')) {
- return this.get('json');
+
+function assert(condition, message) {
+ if(!condition) {
+ throw new Error(message);
+ }
+}
+my.InputView = instance.web.Widget.extend({
+ template: 'SearchView.InputView',
+ start: function () {
+ var p = this._super.apply(this, arguments);
+ this.$element.on('focus', this.proxy('onFocus'));
+ this.$element.on('blur', this.proxy('onBlur'));
+ this.$element.on('keydown', this.proxy('onKeydown'));
+ return p;
+ },
+ onFocus: function () {
+ this.trigger('focused', this);
+ },
+ onBlur: function () {
+ this.$element.text('');
+ this.trigger('blurred', this);
+ },
+ getSelection: function () {
+ // get Text node
+ var root = this.$element[0].childNodes[0];
+ if (!root || !root.textContent) {
+ // if input does not have a child node, or the child node is an
+ // empty string, then the selection can only be (0, 0)
+ return {start: 0, end: 0};
+ }
+ if (window.getSelection) {
+ var domRange = window.getSelection().getRangeAt(0);
+ assert(domRange.startContainer === root,
+ "selection should be in the input view");
+ assert(domRange.endContainer === root,
+ "selection should be in the input view");
+ return {
+ start: domRange.startOffset,
+ end: domRange.endOffset
+ }
+ } else if (document.selection) {
+ var ieRange = document.selection.createRange();
+ var rangeParent = ieRange.parentElement();
+ assert(rangeParent === root,
+ "selection should be in the input view");
+ var offsetRange = document.body.createTextRange();
+ offsetRange = offsetRange.moveToElementText(rangeParent);
+ offsetRange.setEndPoint("EndToStart", ieRange);
+ var start = offsetRange.text.length;
+ return {
+ start: start,
+ end: start + ieRange.text.length
+ }
+ }
+ throw new Error("Could not get caret position");
+ },
+ onKeydown: function (e) {
+ var sel;
+ switch (e.which) {
+ // Do not insert newline, but let it bubble so searchview can use it
+ case $.ui.keyCode.ENTER:
+ e.preventDefault();
+ break;
+
+ // FIXME: may forget content if non-empty but caret at index 0, ok?
+ case $.ui.keyCode.BACKSPACE:
+ sel = this.getSelection();
+ if (sel.start === 0 && sel.start === sel.end) {
+ e.preventDefault();
+ var preceding = this.getParent().siblingSubview(this, -1);
+ if (preceding && (preceding instanceof my.FacetView)) {
+ preceding.model.destroy();
+ }
+ }
+ break;
+
+ // let left/right events propagate to view if caret is at input border
+ // and not a selection
+ case $.ui.keyCode.LEFT:
+ sel = this.getSelection();
+ if (sel.start !== 0 || sel.start !== sel.end) {
+ e.stopPropagation();
+ }
+ break;
+ case $.ui.keyCode.RIGHT:
+ sel = this.getSelection();
+ var len = this.$element.text().length;
+ if (sel.start !== len || sel.start !== sel.end) {
+ e.stopPropagation();
+ }
+ break;
}
- return this.get('value');
+ }
+});
+my.FacetView = instance.web.Widget.extend({
+ template: 'SearchView.FacetView',
+ init: function (parent, model) {
+ this._super(parent);
+ this.model = model;
+ this.model.on('change', this.model_changed, this);
+ },
+ destroy: function () {
+ this.model.off('change', this.model_changed, this);
+ this._super();
+ },
+ start: function () {
+ var self = this;
+ this.$element.on('focus', function () { self.trigger('focused', self); });
+ this.$element.on('blur', function () { self.trigger('blurred', self); });
+ this.$element.on('click', function (e) {
+ if ($(e.target).is('.oe_facet_remove')) {
+ self.model.destroy();
+ return false;
+ }
+ self.$element.focus();
+ e.stopPropagation();
+ });
+ this.$element.on('keydown', function (e) {
+ var keys = $.ui.keyCode;
+ switch (e.which) {
+ case keys.BACKSPACE:
+ case keys.DELETE:
+ self.model.destroy();
+ return false;
+ }
+ });
+ var $e = self.$element.find('> span:last-child');
+ var q = $.when(this._super());
+ return q.pipe(function () {
+ var values = self.model.values.map(function (value) {
+ return new my.FacetValueView(self, value).appendTo($e);
+ });
+
+ return $.when.apply(null, values);
+ });
+ },
+ model_changed: function () {
+ this.$element.text(this.$element.text() + '*');
+ }
+});
+my.FacetValueView = instance.web.Widget.extend({
+ template: 'SearchView.FacetView.Value',
+ init: function (parent, model) {
+ this._super(parent);
+ this.model = model;
+ this.model.on('change', this.model_changed, this);
+ },
+ destroy: function () {
+ this.model.off('change', this.model_changed, this);
+ this._super();
+ },
+ model_changed: function () {
+ this.$element.text(this.$element.text() + '*');
}
});
@@ -74,13 +276,13 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
template: "SearchView",
/**
* @constructs instance.web.SearchView
- * @extends instance.web.OldWidget
+ * @extends instance.web.Widget
*
* @param parent
- * @param element_id
* @param dataset
* @param view_id
* @param defaults
+ * @param hidden
*/
init: function(parent, dataset, view_id, defaults, hidden) {
this._super(parent);
@@ -97,7 +299,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
this.hidden = !!hidden;
this.headless = this.hidden && !this.has_defaults;
- this.filter_data = {};
+ this.input_subviews = [];
this.ready = $.Deferred();
},
@@ -106,33 +308,9 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
var p = this._super();
this.setup_global_completion();
- this.vs = VS.init({
- container: this.$element,
- query: '',
- callbacks: {
- make_facet: this.proxy('make_visualsearch_facet'),
- make_input: this.proxy('make_visualsearch_input'),
- search: function (query, searchCollection) {
- self.do_search();
- },
- facetMatches: function (callback) {
- },
- valueMatches : function(facet, searchTerm, callback) {
- }
- }
- });
-
- var search = function () { self.vs.searchBox.searchEvent({}); };
- // searchQuery operations
- this.vs.searchQuery
- .off('add').on('add', search)
- .off('change').on('change', search)
- .off('reset').on('reset', search)
- .off('remove').on('remove', function (record, collection, options) {
- if (options['trigger_search']) {
- search();
- }
- });
+ this.query = new my.SearchQuery()
+ .on('add change reset remove', this.proxy('do_search'))
+ .on('add change reset remove', this.proxy('renderFacets'));
if (this.hidden) {
this.$element.hide();
@@ -144,19 +322,52 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
model: this.model,
view_id: this.view_id,
context: this.dataset.get_context() });
- // FIXME: local eval of domain and context to get rid of special endpoint
- var filters = this.rpc('/web/searchview/get_filters', {
- model: this.model
- }).then(function (filters) { self.custom_filters = filters; });
-
- $.when(load_view, filters)
- .pipe(function (load) { return load[0]; })
- .then(this.on_loaded);
+
+ $.when(load_view)
+ .pipe(this.on_loaded)
+ .fail(function () {
+ self.ready.reject.apply(null, arguments);
+ });
}
- this.$element.on('click', '.oe_vs_unfold_drawer', function () {
+ this.$element.on('keydown',
+ '.oe_searchview_input, .oe_searchview_facet', function (e) {
+ switch(e.which) {
+ case $.ui.keyCode.LEFT:
+ self.focusPreceding(this);
+ e.preventDefault();
+ break;
+ case $.ui.keyCode.RIGHT:
+ self.focusFollowing(this);
+ e.preventDefault();
+ break;
+ }
+ });
+
+ this.$element.on('click', '.oe_searchview_clear', function (e) {
+ e.stopImmediatePropagation();
+ self.query.reset();
+ });
+ this.$element.on('click', '.oe_searchview_unfold_drawer', function (e) {
+ e.stopImmediatePropagation();
self.$element.toggleClass('oe_searchview_open_drawer');
});
+ // Focus last input if the view itself is clicked (empty section of
+ // facets element)
+ this.$element.on('click', function (e) {
+ if (e.target === self.$element.find('.oe_searchview_facets')[0]) {
+ self.$element.find('.oe_searchview_input:last').focus();
+ }
+ });
+ // when the completion list opens/refreshes, automatically select the
+ // first completion item so if the user just hits [RETURN] or [TAB] it
+ // automatically selects it
+ this.$element.on('autocompleteopen', function () {
+ var menu = self.$element.data('autocomplete').menu;
+ menu.activate(
+ $.Event({ type: "mouseenter" }),
+ menu.element.children().first());
+ });
return $.when(p, this.ready);
},
@@ -167,81 +378,57 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
this.$element.hide();
},
- /**
- * Sets up thingie where all the mess is put?
- */
- setup_stuff_drawer: function () {
- var self = this;
- $('<div class="oe_vs_unfold_drawer">').appendTo(this.$element.find('.VS-search-box'));
- var $drawer = $('<div class="oe_searchview_drawer">').appendTo(this.$element);
- var $filters = $('<div class="oe_searchview_filters">').appendTo($drawer);
-
- var running_count = 0;
- // get total filters count
- var is_group = function (i) { return i instanceof instance.web.search.FilterGroup; };
- var filters_count = _(this.controls).chain()
- .flatten()
- .filter(is_group)
- .map(function (i) { return i.filters.length; })
- .sum()
- .value();
-
- var col1 = [], col2 = _(this.controls).map(function (inputs, group) {
- var filters = _(inputs).filter(is_group);
- return {
- name: group === 'null' ? _t("Filters") : group,
- filters: filters,
- length: _(filters).chain().map(function (i) {
- return i.filters.length; }).sum().value()
- };
+ subviewForRoot: function (subview_root) {
+ return _(this.input_subviews).detect(function (subview) {
+ return subview.$element[0] === subview_root;
});
-
- while (col2.length) {
- // col1 + group should be smaller than col2 + group
- if ((running_count + col2[0].length) <= (filters_count - running_count)) {
- running_count += col2[0].length;
- col1.push(col2.shift());
- } else {
- break;
- }
+ },
+ siblingSubview: function (subview, direction, wrap_around) {
+ var index = _(this.input_subviews).indexOf(subview) + direction;
+ if (wrap_around && index < 0) {
+ index = this.input_subviews.length - 1;
+ } else if (wrap_around && index >= this.input_subviews.length) {
+ index = 0;
}
+ return this.input_subviews[index];
+ },
+ focusPreceding: function (subview_root) {
+ return this.siblingSubview(
+ this.subviewForRoot(subview_root), -1, true)
+ .$element.focus();
+ },
+ focusFollowing: function (subview_root) {
+ return this.siblingSubview(
+ this.subviewForRoot(subview_root), +1, true)
+ .$element.focus();
+ },
- // Create a Custom Filter FilterGroup for each custom filter read from
- // the db, add all of this as a group in the smallest column
- [].push.call(col1.length <= col2.length ? col1 : col2, {
- name: _t("Custom Filters"),
- filters: _.map(this.custom_filters, function (filter) {
- // FIXME: handling of ``disabled`` being set
- var f = new instance.web.search.Filter({attrs: {
- string: filter.name,
- context: filter.context,
- domain: filter.domain
- }}, self);
- return new instance.web.search.FilterGroup([f], self);
- }),
- length: 3
+ /**
+ * Sets up thingie where all the mess is put?
+ */
+ select_for_drawer: function () {
+ return _(this.inputs).filter(function (input) {
+ return input.in_drawer();
});
-
- return $.when(
- this.render_column(col1, $('<div>').appendTo($filters)),
- this.render_column(col2, $('<div>').appendTo($filters)),
- (new instance.web.search.Advanced(this).appendTo($drawer)));
- },
- render_column: function (column, $el) {
- return $.when.apply(null, _(column).map(function (group) {
- $('<h3>').text(group.name).appendTo($el);
- return $.when.apply(null,
- _(group.filters).invoke('appendTo', $el));
- }));
},
/**
* Sets up search view's view-wide auto-completion widget
*/
setup_global_completion: function () {
- // Prevent keydown from within a facet's input from reaching the
- // auto-completion widget and opening the completion list
- this.$element.on('keydown', '.search_facet input', function (e) {
- e.stopImmediatePropagation();
+ var self = this;
+
+ // autocomplete only correctly handles being initialized on the actual
+ // editable element (and only an element with a @value in 1.8 e.g.
+ // input or textarea), cheat by setting val() on $element
+ this.$element.on('keydown', function () {
+ // keydown is triggered *before* the element's value is set, so
+ // delay this. Pray that setTimeout are executed in FIFO (if they
+ // have the same delay) as autocomplete uses the exact same trick.
+ // FIXME: brittle as fuck
+ setTimeout(function () {
+ self.$element.val(self.currentInputValue());
+ }, 0);
+
});
this.$element.autocomplete({
@@ -249,7 +436,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
select: this.proxy('select_completion'),
focus: function (e) { e.preventDefault(); },
html: true,
- minLength: 0,
+ minLength: 1,
delay: 0
}).data('autocomplete')._renderItem = function (ul, item) {
// item of completion list
@@ -257,14 +444,14 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
.data( "item.autocomplete", item )
.appendTo( ul );
- if (item.value !== undefined) {
+ if (item.facet !== undefined) {
// regular completion item
return $item.append(
(item.label)
? $('<a>').html(item.label)
: $('<a>').text(item.value));
}
- return $item.text(item.category)
+ return $item.text(item.label)
.css({
borderTop: '1px solid #cccccc',
margin: 0,
@@ -274,7 +461,14 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
clear: 'left',
width: '100%'
});
- }
+ };
+ },
+ /**
+ * Gets value out of the currently focused "input" (a
+ * div[contenteditable].oe_searchview_input)
+ */
+ currentInputValue: function () {
+ return this.$element.find('div.oe_searchview_input:focus').text();
},
/**
* Provide auto-completion result for req.term (an array to `resp`)
@@ -301,50 +495,62 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
*/
select_completion: function (e, ui) {
e.preventDefault();
- this.vs.searchQuery.add(new VS.model.SearchFacet(_.extend(
- {app: this.vs}, ui.item)));
- this.vs.searchBox.searchEvent({});
- },
-
- /**
- * Builds the right SearchFacet view based on the facet object to render
- * (e.g. readonly facets for filters)
- *
- * @param {Object} options
- * @param {VS.model.SearchFacet} options.model facet object to render
- */
- make_visualsearch_facet: function (options) {
- return new instance.web.search.FilterGroupFacet(options);
-// if (options.model.get('field') instanceof instance.web.search.FilterGroup) {
-// return new instance.web.search.FilterGroupFacet(options);
-// }
-// return new VS.ui.SearchFacet(options);
+ var input_index = _(this.input_subviews).indexOf(
+ this.subviewForRoot(
+ this.$element.find('div.oe_searchview_input:focus')[0]));
+ this.query.add(ui.item.facet, {at: input_index / 2});
+ },
+ childFocused: function () {
+ this.$element.addClass('oe_focused');
+ },
+ childBlurred: function () {
+ this.$element.removeClass('oe_focused');
},
/**
- * Proxies searches on a SearchInput to the search view's global completion
*
- * Also disables SearchInput.autocomplete#_move so search view's
- * autocomplete can get the corresponding events, or something.
- *
- * @param options
+ * @param {openerp.web.search.SearchQuery | openerp.web.search.Facet} _1
+ * @param {openerp.web.search.Facet} [_2]
+ * @param {Object} [options]
*/
- make_visualsearch_input: function (options) {
- var self = this, input = new VS.ui.SearchInput(options);
- input.setupAutocomplete = function () {
- _.extend(this.box.autocomplete({
- minLength: 1,
- delay: 0,
- search: function () {
- self.$element.autocomplete('search', input.box.val());
- return false;
- }
- }).data('autocomplete'), {
- _move: function () {},
- close: function () { self.$element.autocomplete('close'); }
- });
- };
- return input;
+ renderFacets: function (_1, _2, options) {
+ // _1: model if event=change, otherwise collection
+ // _2: undefined if event=change, otherwise model
+ var self = this;
+ var started = [];
+ var $e = this.$element.find('div.oe_searchview_facets');
+ _.invoke(this.input_subviews, 'destroy');
+ this.input_subviews = [];
+
+ var i = new my.InputView(this);
+ started.push(i.appendTo($e));
+ this.input_subviews.push(i);
+ this.query.each(function (facet) {
+ var f = new my.FacetView(this, facet);
+ started.push(f.appendTo($e));
+ self.input_subviews.push(f);
+
+ var i = new my.InputView(this);
+ started.push(i.appendTo($e));
+ self.input_subviews.push(i);
+ }, this);
+ _.each(this.input_subviews, function (childView) {
+ childView.on('focused', self, self.proxy('childFocused'));
+ childView.on('blurred', self, self.proxy('childBlurred'));
+ });
+
+ $.when.apply(null, started).then(function () {
+ var input_to_focus;
+ // options.at: facet inserted at given index, focus next input
+ // otherwise just focus last input
+ if (!options || typeof options.at !== 'number') {
+ input_to_focus = _.last(self.input_subviews);
+ } else {
+ input_to_focus = self.input_subviews[(options.at + 1) * 2];
+ }
+
+ input_to_focus.$element.focus();
+ });
},
/**
@@ -423,14 +629,25 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
data.fields_view['arch'].children,
data.fields_view.fields);
+ // add Filters to this.inputs, need view.controls filled
+ (new instance.web.search.Filters(this));
+ // add custom filters to this.inputs
+ (new instance.web.search.CustomFilters(this));
+ // add Advanced to this.inputs
+ (new instance.web.search.Advanced(this));
+
+ // build drawer
+ var drawer_started = $.when.apply(
+ null, _(this.select_for_drawer()).invoke(
+ 'appendTo', this.$element.find('.oe_searchview_drawer')));
+
// load defaults
- return $.when(
- this.setup_stuff_drawer(),
- $.when.apply(null, _(this.inputs).invoke('facet_for_defaults', this.defaults))
- .then(function () {
- self.vs.searchQuery.reset(_(arguments).compact(), {silent: true});
- self.vs.searchBox.renderFacets();
- }))
+ var defaults_fetched = $.when.apply(null, _(this.inputs).invoke(
+ 'facet_for_defaults', this.defaults)).then(function () {
+ self.query.reset(_(arguments).compact(), {preventSearch: true});
+ });
+
+ return $.when(drawer_started, defaults_fetched)
.then(function () { self.ready.resolve(); })
},
/**
@@ -447,53 +664,6 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
case 'add_to_dashboard':
this.on_add_to_dashboard();
break;
- case 'manage_filters':
- this.do_action({
- res_model: 'ir.filters',
- views: [[false, 'list'], [false, 'form']],
- type: 'ir.actions.act_window',
- context: {"search_default_user_id": this.session.uid,
- "search_default_model_id": this.dataset.model},
- target: "current",
- limit : 80
- });
- break;
- case 'save_filter':
- var data = this.build_search_data();
- var context = new instance.web.CompoundContext();
- _.each(data.contexts, function(x) {
- context.add(x);
- });
- var domain = new instance.web.CompoundDomain();
- _.each(data.domains, function(x) {
- domain.add(x);
- });
- var groupbys = _.pluck(data.groupbys, "group_by").join();
- context.add({"group_by": groupbys});
- var dial_html = QWeb.render("SearchView.managed-filters.add");
- var $dial = $(dial_html);
- instance.web.dialog($dial, {
- modal: true,
- title: _t("Filter Entry"),
- buttons: [
- {text: _t("Cancel"), click: function() {
- $(this).dialog("close");
- }},
- {text: _t("OK"), click: function() {
- $(this).dialog("close");
- var name = $(this).find("input").val();
- self.rpc('/web/searchview/save_filter', {
- model: self.dataset.model,
- context_to_save: context,
- domain: domain,
- name: name
- }).then(function() {
- self.reload_managed_filters();
- });
- }}
- ]
- });
- break;
case '':
this.do_clear();
}
@@ -570,20 +740,26 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
});
},
/**
- * Performs the search view collection of widget data.
+ * Extract search data from the view's facets.
*
- * If the collection went well (all fields are valid), then triggers
- * :js:func:`instance.web.SearchView.on_search`.
+ * Result is an object with 4 (own) properties:
*
- * If at least one field failed its validation, triggers
- * :js:func:`instance.web.SearchView.on_invalid` instead.
+ * 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
*
- * @param e jQuery event object coming from the "Search" button
+ * @return {Object}
*/
- do_search: function () {
+ build_search_data: function () {
var domains = [], contexts = [], groupbys = [], errors = [];
- this.vs.searchQuery.each(function (facet) {
+ this.query.each(function (facet) {
var field = facet.get('field');
try {
var domain = field.get_domain(facet);
@@ -606,12 +782,33 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
}
}
});
-
- if (!_.isEmpty(errors)) {
- this.on_invalid(errors);
+ 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 e jQuery event object coming from the "Search" button
+ */
+ do_search: function (_query, options) {
+ if (options && options.preventSearch) {
return;
}
- return this.on_search(domains, contexts, groupbys);
+ var search = this.build_search_data();
+ if (!_.isEmpty(search.errors)) {
+ this.on_invalid(search.errors);
+ return;
+ }
+ return this.on_search(search.domains, search.contexts, search.groupbys);
},
/**
* Triggered after the SearchView has collected all relevant domains and
@@ -647,40 +844,6 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
}
});
-/** @namespace */
-instance.web.search = {};
-
-instance.web.search.FilterGroupFacet = VS.ui.SearchFacet.extend({
- events: _.extend({
- 'click': 'selectFacet'
- }, VS.ui.SearchFacet.prototype.events),
-
- render: function () {
- this.setMode('not', 'editing');
- this.setMode('not', 'selected');
-
- var value = this.model.get('value');
- this.$el.html(QWeb.render('SearchView.filters.facet', {
- facet: this.model
- }));
- // virtual input so SearchFacet code has something to play with
- this.box = $('<input>').val(value);
-
- return this;
- },
- enableEdit: function () {
- this.selectFacet()
- },
- keydown: function (e) {
- var key = VS.app.hotkeys.key(e);
- if (key !== 'right') {
- return VS.ui.SearchFacet.prototype.keydown.call(this, e);
- }
- e.preventDefault();
- this.deselectFacet();
- this.options.app.searchBox.focusNextFacet(this, 1);
- }
-});
/**
* Registry of search fields, called by :js:class:`instance.web.SearchView` to
* find and instantiate its field widgets.
@@ -756,6 +919,7 @@ instance.web.search.Group = instance.web.search.Widget.extend({
});
instance.web.search.Input = instance.web.search.Widget.extend( /** @lends instance.web.search.Input# */{
+ _in_drawer: false,
/**
* @constructs instance.web.search.Input
* @extends instance.web.search.Widget
@@ -780,8 +944,8 @@ instance.web.search.Input = instance.web.search.Widget.extend( /** @lends instan
return $.when(null)
},
/**
- * Returns a VS.model.SearchFacet instance for the provided defaults if
- * they apply to this widget, or null if they don't.
+ * Returns a Facet instance for the provided defaults if they apply to
+ * this widget, or null if they don't.
*
* This default implementation will try calling
* :js:func:`instance.web.search.Input#facet_for` if the widget's name
@@ -797,6 +961,9 @@ 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);
@@ -822,6 +989,8 @@ instance.web.search.Input = instance.web.search.Widget.extend( /** @lends instan
});
instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends instance.web.search.FilterGroup# */{
template: 'SearchView.filters',
+ icon: 'q',
+ completion_label: _lt("Filter on: %s"),
/**
* Inclusive group of filters, creates a continuous "button" with clickable
* sections (the normal display for filters is to be a self-contained button)
@@ -833,36 +1002,77 @@ instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends in
* @param {instance.web.SearchView} view view in which the filters are contained
*/
init: function (filters, view) {
+ // If all filters are group_by and we're not initializing a GroupbyGroup,
+ // create a GroupbyGroup instead of the current FilterGroup
+ if (!(this instanceof instance.web.search.GroupbyGroup) &&
+ _(filters).all(function (f) {
+ return f.attrs.context && f.attrs.context.group_by; })) {
+ return new instance.web.search.GroupbyGroup(filters, view);
+ }
this._super(view);
this.filters = filters;
+ this.view.query.on('add remove change reset', this.proxy('search_change'));
},
start: function () {
this.$element.on('click', 'li', this.proxy('toggle_filter'));
return $.when(null);
},
- facet_for_defaults: function (defaults) {
- var fs = _(this.filters).filter(function (f) {
- return f.attrs && f.attrs.name && !!defaults[f.attrs.name];
+ /**
+ * Handles change of the search query: any of the group's filter which is
+ * in the search query should be visually checked in the drawer
+ */
+ search_change: function () {
+ var self = this;
+ var $filters = this.$element.find('> li').removeClass('oe_selected');
+ var facet = this.view.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.eq(i).addClass('oe_selected');
});
- if (_.isEmpty(fs)) { return $.when(null); }
- return $.when(new VS.model.SearchFacet({
+ },
+ /**
+ * Matches the group to a facet, in order to find if the group is
+ * represented in the current search query
+ */
+ match_facet: function (facet) {
+ return facet.get('field') === this;
+ },
+ make_facet: function (values) {
+ return {
category: _t("Filter"),
- value: _(fs).map(function (f) {
- return f.attrs.string || f.attrs.name }).join(' | '),
- json: fs,
- field: this,
- app: this.view.vs
- }));
+ icon: this.icon,
+ values: values,
+ field: this
+ }
+ },
+ make_value: function (filter) {
+ return {
+ label: filter.attrs.string || filter.attrs.help || filter.attrs.name,
+ value: filter
+ };
+ },
+ facet_for_defaults: function (defaults) {
+ var self = this;
+ var fs = _(this.filters).chain()
+ .filter(function (f) {
+ return f.attrs && f.attrs.name && !!defaults[f.attrs.name];
+ }).map(function (f) {
+ return self.make_value(f);
+ }).value();
+ if (_.isEmpty(fs)) { return $.when(null); }
+ return $.when(this.make_facet(fs));
},
/**
* Fetches contexts for all enabled filters in the group
*
- * @param {VS.model.SearchFacet} facet
+ * @param {openerp.web.search.Facet} facet
* @return {*} combined contexts of the enabled filters in this group
*/
get_context: function (facet) {
- var contexts = _(facet.get('json')).chain()
- .map(function (filter) { return filter.attrs.context; })
+ var contexts = facet.values.chain()
+ .map(function (f) { return f.get('value').attrs.context; })
.reject(_.isEmpty)
.value();
@@ -879,8 +1089,8 @@ instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends in
* @return {Array} enabled filters in this group
*/
get_groupby: function (facet) {
- return _(facet.get('json')).chain()
- .map(function (filter) { return filter.attrs.context; })
+ return facet.values.chain()
+ .map(function (f) { return f.get('value').attrs.context; })
.reject(_.isEmpty)
.value();
},
@@ -891,8 +1101,8 @@ instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends in
* @return {*} combined domains of the enabled filters in this group
*/
get_domain: function (facet) {
- var domains = _(facet.get('json')).chain()
- .map(function (filter) { return filter.attrs.domain; })
+ var domains = facet.values.chain()
+ .map(function (f) { return f.get('value').attrs.domain; })
.reject(_.isEmpty)
.value();
@@ -909,40 +1119,66 @@ instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends in
this.toggle(this.filters[$(e.target).index()]);
},
toggle: function (filter) {
- // FIXME: oh god, my eyes, they hurt
- var self = this, fs;
- var facet = this.view.vs.searchQuery.detect(function (f) {
- return f.get('field') === self; });
- if (facet) {
- fs = facet.get('json');
-
- if (_.include(fs, filter)) {
- fs = _.without(fs, filter);
- } else {
- fs.push(filter);
+ this.view.query.toggle(this.make_facet([this.make_value(filter)]));
+ },
+ complete: function (item) {
+ var self = this;
+ item = item.toLowerCase();
+ var facet_values = _(this.filters).chain()
+ .filter(function (filter) {
+ var at = {
+ string: filter.attrs.string || '',
+ help: filter.attrs.help || '',
+ name: filter.attrs.name || ''
+ };
+ var include = _.str.include;
+ return include(at.string.toLowerCase(), item)
+ || include(at.help.toLowerCase(), item)
+ || include(at.name.toLowerCase(), item);
+ })
+ .map(this.make_value)
+ .value();
+ if (_(facet_values).isEmpty()) { return $.when(null); }
+ return $.when(_.map(facet_values, function (facet_value) {
+ return {
+ label: _.str.sprintf(self.completion_label.toString(),
+ facet_value.label),
+ facet: self.make_facet([facet_value])
}
- if (_(fs).isEmpty()) {
- this.view.vs.searchQuery.remove(facet, {trigger_search: true});
- } else {
- facet.set({
- json: fs,
- value: _(fs).map(function (f) {
- return f.attrs.string || f.attrs.name }).join(' | ')
- });
+ }));
+ }
+});
+instance.web.search.GroupbyGroup = instance.web.search.FilterGroup.extend({
+ icon: 'w',
+ completion_label: _lt("Group by: %s"),
+ init: function (filters, view) {
+ this._super(filters, view);
+ // 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
+ // groupby we want a single facet. So cheat: add an attribute on the
+ // 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.getParent()._s_groupby) {
+ this.getParent()._s_groupby = {
+ help: "See GroupbyGroup#init",
+ get_context: this.proxy('get_context'),
+ get_domain: this.proxy('get_domain'),
+ get_groupby: this.proxy('get_groupby')
}
- return;
- } else {
- fs = [filter];
}
-
- this.view.vs.searchQuery.add({
- category: _t("Filter"),
- value: _(fs).map(function (f) {
- return f.attrs.string || f.attrs.name }).join(' | '),
- json: fs,
- field: this,
- app: this.view.vs
- });
+ },
+ match_facet: function (facet) {
+ return facet.get('field') === this.getParent()._s_groupby;
+ },
+ make_facet: function (values) {
+ return {
+ category: _t("GroupBy"),
+ icon: this.icon,
+ values: values,
+ field: this.getParent()._s_groupby
+ };
}
});
instance.web.search.Filter = instance.web.search.Input.extend(/** @lends instance.web.search.Filter# */{
@@ -985,28 +1221,32 @@ instance.web.search.Field = instance.web.search.Input.extend( /** @lends instanc
this.load_attrs(_.extend({}, field, view_section.attrs));
},
facet_for: function (value) {
- return $.when(new VS.model.SearchFacet({
- category: this.attrs.string || this.attrs.name,
- value: String(value),
- json: value,
+ return $.when({
field: this,
- app: this.view.vs
- }));
+ category: this.attrs.string || this.attrs.name,
+ values: [{label: String(value), value: value}]
+ });
},
- get_value: function (facet) {
- return facet.value();
+ value_from: function (facetValue) {
+ return facetValue.get('value');
},
get_context: function (facet) {
- var val = this.get_value(facet);
- // A field needs a value to be "active", and a context to send when
- // active
- var has_value = (val !== null && val !== '');
+ var self = this;
+ // A field needs a context to send when active
var context = this.attrs.context;
- if (!(has_value && context)) {
+ if (!context || !facet.values.length) {
return;
}
- return new instance.web.CompoundContext(context)
- .set_eval_context({self: val});
+ var contexts = facet.values.map(function (facetValue) {
+ return new instance.web.CompoundContext(context)
+ .set_eval_context({self: self.value_from(facetValue)});
+ });
+
+ if (contexts.length === 1) { return contexts[0]; }
+
+ return _.extend(new instance.web.CompoundContext, {
+ __contexts: contexts
+ });
},
get_groupby: function () { },
/**
@@ -1021,23 +1261,37 @@ instance.web.search.Field = instance.web.search.Input.extend( /** @lends instanc
* @returns {Array<Array>} domain to include in the resulting search
*/
make_domain: function (name, operator, facet) {
- return [[name, operator, this.get_value(facet)]];
+ return [[name, operator, this.value_from(facet)]];
},
get_domain: function (facet) {
- var val = this.get_value(facet);
- if (val === null || val === '') {
- return;
- }
+ if (!facet.values.length) { return; }
+ var value_to_domain;
+ var self = this;
var domain = this.attrs['filter_domain'];
- if (!domain) {
- return this.make_domain(
- this.attrs.name,
- this.attrs.operator || this.default_operator,
- facet);
+ if (domain) {
+ value_to_domain = function (facetValue) {
+ return new instance.web.CompoundDomain(domain)
+ .set_eval_context({self: self.value_from(facetValue)});
+ };
+ } else {
+ value_to_domain = function (facetValue) {
+ return self.make_domain(
+ self.attrs.name,
+ self.attrs.operator || self.default_operator,
+ facetValue);
+ };
+ }
+ var domains = facet.values.map(value_to_domain);
+
+ if (domains.length === 1) { return domains[0]; }
+ for (var i = domains.length; --i;) {
+ domains.unshift(['|']);
}
- return new instance.web.CompoundDomain(domain)
- .set_eval_context({self: val});
+
+ return _.extend(new instance.web.CompoundDomain, {
+ __domains: domains
+ });
}
});
/**
@@ -1059,15 +1313,17 @@ instance.web.search.CharField = instance.web.search.Field.extend( /** @lends ins
field: '<em>' + this.attrs.string + '</em>',
value: '<strong>' + _.str.escapeHTML(value) + '</strong>'});
return $.when([{
- category: this.attrs.string,
label: label,
- value: value,
- field: this
+ facet: {
+ category: this.attrs.string,
+ field: this,
+ values: [{label: value, value: value}]
+ }
}]);
}
});
instance.web.search.NumberField = instance.web.search.Field.extend(/** @lends instance.web.search.NumberField# */{
- get_value: function () {
+ value_from: function () {
if (!this.$element.val()) {
return null;
}
@@ -1110,6 +1366,22 @@ instance.web.search.FloatField = instance.web.search.NumberField.extend(/** @len
}
}
});
+
+/**
+ * Utility function for m2o & selection fields taking a selection/name_get pair
+ * (value, name) and converting it to a Facet descriptor
+ *
+ * @param {instance.web.search.Field} field holder field
+ * @param {Array} pair pair value to convert
+ */
+function facet_from(field, pair) {
+ return {
+ field: field,
+ category: field['attrs'].string,
+ values: [{label: pair[1], value: pair[0]}]
+ };
+}
+
/**
* @class
* @extends instance.web.search.Field
@@ -1143,15 +1415,13 @@ instance.web.search.SelectionField = instance.web.search.Field.extend(/** @lends
})
.map(function (sel) {
return {
- category: self.attrs.string,
- field: self,
- value: sel[1],
- json: sel[0]
+ label: sel[1],
+ facet: facet_from(self, sel)
};
}).value();
if (_.isEmpty(results)) { return $.when(null); }
- return $.when.apply(null, [{
- category: this.attrs.string
+ return $.when.call(null, [{
+ label: this.attrs.string
}].concat(results));
},
facet_for: function (value) {
@@ -1159,16 +1429,7 @@ instance.web.search.SelectionField = instance.web.search.Field.extend(/** @lends
return sel[0] === value;
});
if (!match) { return $.when(null); }
- return $.when(new VS.model.SearchFacet({
- category: this.attrs.string,
- value: match[1],
- json: match[0],
- field: this,
- app: this.view.app
- }));
- },
- get_value: function (facet) {
- return facet.get('json');
+ return $.when(facet_from(this, match));
}
});
instance.web.search.BooleanField = instance.web.search.SelectionField.extend(/** @lends instance.web.search.BooleanField# */{
@@ -1179,16 +1440,9 @@ instance.web.search.BooleanField = instance.web.search.SelectionField.extend(/**
init: function () {
this._super.apply(this, arguments);
this.attrs.selection = [
- ['true', _t("Yes")],
- ['false', _t("No")]
+ [true, _t("Yes")],
+ [false, _t("No")]
];
- },
- get_value: function (facet) {
- switch (this._super(facet)) {
- case 'false': return false;
- case 'true': return true;
- default: return null;
- }
}
});
/**
@@ -1196,23 +1450,24 @@ instance.web.search.BooleanField = instance.web.search.SelectionField.extend(/**
* @extends instance.web.search.DateField
*/
instance.web.search.DateField = instance.web.search.Field.extend(/** @lends instance.web.search.DateField# */{
- get_value: function (facet) {
- return instance.web.date_to_str(facet.get('json'));
+ value_from: function (facetValue) {
+ return instance.web.date_to_str(facetValue.get('value'));
},
complete: function (needle) {
var d = Date.parse(needle);
if (!d) { return $.when(null); }
- var value = instance.web.format_value(d, this.attrs);
+ var date_string = instance.web.format_value(d, this.attrs);
var label = _.str.sprintf(_.str.escapeHTML(
_t("Search %(field)s at: %(value)s")), {
field: '<em>' + this.attrs.string + '</em>',
- value: '<strong>' + value + '</strong>'});
+ value: '<strong>' + date_string + '</strong>'});
return $.when([{
- category: this.attrs.string,
label: label,
- value: value,
- json: d,
- field: this
+ facet: {
+ category: this.attrs.string,
+ field: this,
+ values: [{label: date_string, value: d}]
+ }
}]);
}
});
@@ -1228,11 +1483,12 @@ instance.web.search.DateField = instance.web.search.Field.extend(/** @lends inst
* @extends instance.web.DateField
*/
instance.web.search.DateTimeField = instance.web.search.DateField.extend(/** @lends instance.web.search.DateTimeField# */{
- get_value: function (facet) {
- return instance.web.datetime_to_str(facet.get('json'));
+ value_from: function (facetValue) {
+ return instance.web.datetime_to_str(facetValue.get('value'));
}
});
instance.web.search.ManyToOneField = instance.web.search.CharField.extend({
+ default_operator: {},
init: function (view_section, field, view) {
this._super(view_section, field, view);
this.model = new instance.web.Model(this.attrs.relation);
@@ -1247,13 +1503,11 @@ instance.web.search.ManyToOneField = instance.web.search.CharField.extend({
context: {}
}).pipe(function (results) {
if (_.isEmpty(results)) { return null; }
- return [{category: self.attrs.string}].concat(
+ return [{label: self.attrs.string}].concat(
_(results).map(function (result) {
return {
- category: self.attrs.string,
- value: result[1],
- json: result[0],
- field: self
+ label: result[1],
+ facet: facet_from(self, result)
};
}));
});
@@ -1261,36 +1515,166 @@ instance.web.search.ManyToOneField = instance.web.search.CharField.extend({
facet_for: function (value) {
var self = this;
if (value instanceof Array) {
- return $.when(new VS.model.SearchFacet({
- category: this.attrs.string,
- value: value[1],
- json: value[0],
- field: this,
- app: this.view.vs
- }));
+ return $.when(facet_from(this, value));
}
return this.model.call('name_get', [value], {}).pipe(function (names) {
- return new VS.model.SearchFacet({
- category: self.attrs.string,
- value: names[0][1],
- json: names[0][0],
- field: self,
- app: self.view.vs
- });
+ if (_(names).isEmpty()) { return null; }
+ return facet_from(self, names[0]);
})
},
- make_domain: function (name, operator, facet) {
- // ``json`` -> actual auto-completed id
- if (facet.get('json')) {
- return [[name, '=', facet.get('json')]];
+ value_from: function (facetValue) {
+ return facetValue.get('label');
+ },
+ make_domain: function (name, operator, facetValue) {
+ if (operator === this.default_operator) {
+ return [[name, '=', facetValue.get('value')]];
}
+ return this._super(name, operator, facetValue);
+ }
+});
+
+instance.web.search.CustomFilters = instance.web.search.Input.extend({
+ template: 'SearchView.CustomFilters',
+ _in_drawer: true,
+ start: function () {
+ var self = this;
+ this.model = new instance.web.Model('ir.filters');
+ this.filters = {};
+ this.$element.on('submit', 'form', this.proxy('save_current'));
+ this.$element.on('click', 'h4', function () {
+ self.$element.toggleClass('oe_opened');
+ });
+ // FIXME: local eval of domain and context to get rid of special endpoint
+ return this.rpc('/web/searchview/get_filters', {
+ model: this.view.model
+ }).pipe(this.proxy('set_filters'));
+ },
+ append_filter: function (filter) {
+ var self = this;
+ var key = _.str.sprintf('(%s)%s', filter.user_id, filter.name);
- return this._super(name, operator, facet);
+ var $filter;
+ if (key in this.filters) {
+ $filter = this.filters[key];
+ } else {
+ var id = filter.id;
+ $filter = this.filters[key] = $('<li></li>')
+ .appendTo(this.$element.find('.oe_searchview_custom_list'))
+ .addClass(filter.user_id ? 'oe_searchview_custom_private'
+ : 'oe_searchview_custom_public')
+ .text(filter.name);
+
+ $('<button type="button" class="oe_searchview_custom_delete">')
+ .text(_t("Delete"))
+ .click(function (e) {
+ e.stopPropagation();
+ self.model.call('unlink', [id]).then(function () {
+ $filter.remove();
+ });
+ })
+ .appendTo($filter);
+ }
+
+ $filter.unbind('click').click(function () {
+ self.view.query.reset([{
+ category: _("Custom Filter"),
+ icon: 'M',
+ field: {
+ get_context: function () { return filter.context; },
+ get_groupby: function () { return [filter.context]; },
+ get_domain: function () { return filter.domain; }
+ },
+ values: [{label: filter.name, value: null}]
+ }]);
+ });
+ },
+ set_filters: function (filters) {
+ _(filters).map(_.bind(this.append_filter, this));
+ },
+ save_current: function () {
+ var self = this;
+ var $name = this.$element.find('input:first');
+ var private_filter = !this.$element.find('input:last').prop('checked');
+
+ var search = this.view.build_search_data();
+ this.rpc('/web/session/eval_domain_and_context', {
+ domains: search.domains,
+ contexts: search.contexts,
+ group_by_seq: search.groupbys || []
+ }).then(function (results) {
+ if (!_.isEmpty(results.group_by)) {
+ results.context.group_by = results.group_by;
+ }
+ var filter = {
+ name: $name.val(),
+ user_id: private_filter ? instance.connection.uid : false,
+ model_id: self.view.model,
+ context: results.context,
+ domain: results.domain
+ };
+ // FIXME: current context?
+ return self.model.call('create_or_replace', [filter]).then(function (id) {
+ filter.id = id;
+ self.append_filter(filter);
+ self.$element
+ .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 running_count = 0;
+ // get total filters count
+ var is_group = function (i) { return i instanceof instance.web.search.FilterGroup; };
+ var filters_count = _(this.view.controls).chain()
+ .flatten()
+ .filter(is_group)
+ .map(function (i) { return i.filters.length; })
+ .sum()
+ .value();
+
+ var col1 = [], col2 = _(this.view.controls).map(function (inputs, group) {
+ var filters = _(inputs).filter(is_group);
+ return {
+ name: group === 'null' ? _t("Filters") : group,
+ filters: filters,
+ length: _(filters).chain().map(function (i) {
+ return i.filters.length; }).sum().value()
+ };
+ });
+
+ while (col2.length) {
+ // col1 + group should be smaller than col2 + group
+ if ((running_count + col2[0].length) <= (filters_count - running_count)) {
+ running_count += col2[0].length;
+ col1.push(col2.shift());
+ } else {
+ break;
+ }
+ }
+
+ return $.when(
+ this.render_column(col1, $('<div>').appendTo(this.$element)),
+ this.render_column(col2, $('<div>').appendTo(this.$element)));
+ },
+ render_column: function (column, $el) {
+ return $.when.apply(null, _(column).map(function (group) {
+ $('<h3>').text(group.name).appendTo($el);
+ return $.when.apply(null,
+ _(group.filters).invoke('appendTo', $el));
+ }));
+ }
+});
instance.web.search.Advanced = instance.web.search.Input.extend({
template: 'SearchView.advanced',
+ _in_drawer: true,
start: function () {
var self = this;
this.$element
@@ -1320,26 +1704,23 @@ instance.web.search.Advanced = instance.web.search.Input.extend({
commit_search: function () {
var self = this;
// Get domain sections from all propositions
- var children = this.getChildren(),
- domain = _.invoke(children, 'get_proposition');
- var filters = _(domain).map(function (section) {
- return new instance.web.search.Filter({attrs: {
- string: _.str.sprintf('%s(%s)%s',
- section[0], section[1], section[2]),
- domain: [section]
- }}, self.view);
- });
- // Create Filter (& FilterGroup around it) with that domain
- var f = new instance.web.search.FilterGroup(filters, this.view);
- // add group to query
- this.view.vs.searchQuery.add({
+ 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"),
- value: _(filters).map(function (f) {
- return f.attrs.string || f.attrs.name }).join(' | '),
- json: filters,
- field: f,
- app: this.view.vs
+ values: propositions,
+ field: {
+ get_context: function () { },
+ get_domain: function () { return domain;},
+ get_groupby: function () { }
+ }
});
+
// remove all propositions
_.invoke(children, 'destroy');
// add new empty proposition
@@ -1401,37 +1782,57 @@ instance.web.search.ExtendedSearchProposition = instance.web.OldWidget.extend(/*
}
var type = field.type;
- var obj = instance.web.search.custom_filters.get_object(type);
- if(obj === null) {
- obj = instance.web.search.custom_filters.get_object("char");
- }
- this.value = new (obj) (this);
- if(this.value.set_field) {
- this.value.set_field(field);
+ var Field = instance.web.search.custom_filters.get_object(type);
+ if(!Field) {
+ Field = instance.web.search.custom_filters.get_object("char");
}
+ this.value = new Field(this, field);
_.each(this.value.operators, function(operator) {
$('<option>', {value: operator.value})
.text(String(operator.text))
.appendTo(self.$element.find('.searchview_extended_prop_op'));
});
- this.$element.find('.searchview_extended_prop_value').html(
- this.value.render({}));
- this.value.start();
+ var $value_loc = this.$element.find('.searchview_extended_prop_value').empty();
+ this.value.appendTo($value_loc);
},
get_proposition: function() {
if ( this.attrs.selected == null)
return null;
- var field = this.attrs.selected.name;
- var op = this.$element.find('.searchview_extended_prop_op').val();
- var value = this.value.get_value();
- return [field, op, value];
+ var field = this.attrs.selected;
+ var op = this.$element.find('.searchview_extended_prop_op')[0];
+ var operator = op.options[op.selectedIndex];
+ return {
+ label: _.str.sprintf(_t('%(field)s %(operator)s "%(value)s"'), {
+ field: field.string,
+ // According to spec, HTMLOptionElement#label should return
+ // HTMLOptionElement#text when not defined/empty, but it does
+ // not in older Webkit (between Safari 5.1.5 and Chrome 17) and
+ // Gecko (pre Firefox 7) browsers, so we need a manual fallback
+ // for those
+ operator: operator.label || operator.text,
+ value: this.value}),
+ value: [field.name, operator.value, this.value.get_value()]
+ };
}
});
-instance.web.search.ExtendedSearchProposition.Field = instance.web.OldWidget.extend({
- start: function () {
- this.$element = $("#" + this.element_id);
+instance.web.search.ExtendedSearchProposition.Field = instance.web.Widget.extend({
+ init: function (parent, field) {
+ this._super(parent);
+ this.field = field;
+ },
+ /**
+ * Returns a human-readable version of the value, in case the "logical"
+ * and the "semantic" values of a field differ (as for selection fields,
+ * for instance).
+ *
+ * The default implementation simply returns the value itself.
+ *
+ * @return {String} human-readable version of the value
+ */
+ toString: function () {
+ return this.get_value();
}
});
instance.web.search.ExtendedSearchProposition.Char = instance.web.search.ExtendedSearchProposition.Field.extend({
@@ -1456,33 +1857,23 @@ instance.web.search.ExtendedSearchProposition.DateTime = instance.web.search.Ext
{value: ">=", text: _lt("greater or equal than")},
{value: "<=", text: _lt("less or equal than")}
],
+ /**
+ * Date widgets live in view_form which is not yet loaded when this is
+ * initialized -_-
+ */
+ widget: function () { return instance.web.DateTimeWidget; },
get_value: function() {
return this.datewidget.get_value();
},
start: function() {
- this._super();
- this.datewidget = new instance.web.DateTimeWidget(this);
- this.datewidget.prependTo(this.$element);
+ var ready = this._super();
+ this.datewidget = new (this.widget())(this);
+ this.datewidget.appendTo(this.$element);
+ return ready;
}
});
-instance.web.search.ExtendedSearchProposition.Date = instance.web.search.ExtendedSearchProposition.Field.extend({
- template: 'SearchView.extended_search.proposition.empty',
- operators: [
- {value: "=", text: _lt("is equal to")},
- {value: "!=", text: _lt("is not equal to")},
- {value: ">", text: _lt("greater than")},
- {value: "<", text: _lt("less than")},
- {value: ">=", text: _lt("greater or equal than")},
- {value: "<=", text: _lt("less or equal than")}
- ],
- get_value: function() {
- return this.datewidget.get_value();
- },
- start: function() {
- this._super();
- this.datewidget = new instance.web.DateWidget(this);
- this.datewidget.prependTo(this.$element);
- }
+instance.web.search.ExtendedSearchProposition.Date = instance.web.search.ExtendedSearchProposition.DateTime.extend({
+ widget: function () { return instance.web.DateWidget; }
});
instance.web.search.ExtendedSearchProposition.Integer = instance.web.search.ExtendedSearchProposition.Field.extend({
template: 'SearchView.extended_search.proposition.integer',
@@ -1494,6 +1885,9 @@ instance.web.search.ExtendedSearchProposition.Integer = instance.web.search.Exte
{value: ">=", text: _lt("greater or equal than")},
{value: "<=", text: _lt("less or equal than")}
],
+ toString: function () {
+ return this.$element.val();
+ },
get_value: function() {
try {
return instance.web.parse_value(this.$element.val(), {'widget': 'integer'});
@@ -1515,6 +1909,9 @@ instance.web.search.ExtendedSearchProposition.Float = instance.web.search.Extend
{value: ">=", text: _lt("greater or equal than")},
{value: "<=", text: _lt("less or equal than")}
],
+ toString: function () {
+ return this.$element.val();
+ },
get_value: function() {
try {
return instance.web.parse_value(this.$element.val(), {'widget': 'float'});
@@ -1529,19 +1926,22 @@ instance.web.search.ExtendedSearchProposition.Selection = instance.web.search.Ex
{value: "=", text: _lt("is")},
{value: "!=", text: _lt("is not")}
],
- set_field: function(field) {
- this.field = field;
+ toString: function () {
+ var select = this.$element[0];
+ var option = select.options[select.selectedIndex];
+ return option.label || option.text;
},
get_value: function() {
return this.$element.val();
}
});
instance.web.search.ExtendedSearchProposition.Boolean = instance.web.search.ExtendedSearchProposition.Field.extend({
- template: 'SearchView.extended_search.proposition.boolean',
+ template: 'SearchView.extended_search.proposition.empty',
operators: [
{value: "=", text: _lt("is true")},
{value: "!=", text: _lt("is false")}
],
+ toString: function () { return ''; },
get_value: function() {
return true;
}
diff --git a/addons/web/static/src/js/views.js b/addons/web/static/src/js/views.js
index d0af4811142..6bf7e273eab 100644
--- a/addons/web/static/src/js/views.js
+++ b/addons/web/static/src/js/views.js
@@ -615,6 +615,17 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({
case 'edit':
this.do_edit_resource($option.data('model'), $option.data('id'), { name : $option.text() });
break;
+ case 'manage_filters':
+ this.do_action({
+ res_model: 'ir.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;
default:
if (val) {
console.log("No debug handler for ", val);
diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml
index 97eeab36657..47939a76b4b 100644
--- a/addons/web/static/src/xml/base.xml
+++ b/addons/web/static/src/xml/base.xml
@@ -264,32 +264,6 @@
</div>
</t>
-<t t-name="WebClient">
- <table class="oe_webclient">
- <tr>
- <td colspan="2" class="oe_topbar">
- <div class="oe_menu_placeholder"/>
- <div class="oe_user_menu_placeholder"/>
- <div class="oe_systray"/>
- </td>
- </tr>
- <tr>
- <td class="oe_leftbar" valign="top">
- <t t-js="d">
- d.url = '/' + (window.location.search || '');
- </t>
- <a t-att-href="url" class="oe_logo"><img t-att-src='_s + "/web/static/src/img/logo.png"'/></a>
- <div class="oe_secondary_menus_container"/>
- <div class="oe_footer">
- Powered by <a href="http://www.openerp.com"><span>Open</span>ERP</a>
- </div>
- </td>
- <td class="oe_application">
- </td>
- </tr>
- </table>
-</t>
-
<t t-name="Menu">
<ul class="oe_menu" t-if="widget.data">
<li t-foreach="widget.data.data.children" t-as="menu">
@@ -409,7 +383,10 @@
</tr>
<tr>
<td class="oe_leftbar" valign="top">
- <a href="#" class="oe_logo"><img t-att-src='_s + "/web/static/src/img/logo.png"'/></a>
+ <t t-js="d">
+ d.url = '/' + (window.location.search || '');
+ </t>
+ <a t-att-href="url" class="oe_logo"><img t-att-src='_s + "/web/static/src/img/logo.png"'/></a>
<div class="oe_secondary_menus_container"/>
<div class="oe_footer">
Powered by <a href="http://www.openerp.com"><span>Open</span>ERP</a>
@@ -476,6 +453,7 @@
<option t-if="view_manager.active_view === 'form'" value="toggle_layout_outline">Toggle Form Layout Outline</option>
<option value="fields">View Fields</option>
<option value="fvg">Fields View Get</option>
+ <option value="manage_filters">Manage Filters</option>
<t t-if="view_manager.session.uid === 1">
<option value="manage_views">Manage Views</option>
<option value="edit" data-model="ir.ui.view" t-att-data-id="view.fields_view.view_id">Edit <t t-esc="_.str.capitalize(view.fields_view.type)"/>View</option>
@@ -998,11 +976,12 @@
</div>
</t>
<t t-name="web.datetimepicker">
+ <t t-set="placeholder" t-value="widget.getParent().node and widget.getParent().node.attrs.placeholder"/>
<div class="oe_datepicker_root oe_form_field_datetime">
<input type="text" class="oe_datepicker_container" disabled="disabled" style="display: none;"/>
<input type="text"
t-att-name="widget.name"
- t-att-placeholder="widget.getParent().node.attrs.placeholder"
+ t-att-placeholder="placeholder"
t-attf-class="oe_datepicker_master field_#{widget.type_of_date} oe_form_field_#{widget.type_of_date}"
/><img class="oe_input_icon oe_datepicker_trigger" t-att-src='_s + "/web/static/src/img/ui/field_calendar.png"'
title="Select date" width="16" height="16" border="0"/>
@@ -1318,7 +1297,31 @@
</t>
<div t-name="SearchView" class="oe_searchview">
+ <div class="oe_searchview_facets"/>
+ <div class="oe_searchview_clear"/>
+ <div class="oe_searchview_unfold_drawer"/>
+ <div class="oe_searchview_drawer"/>
</div>
+
+<div t-name="SearchView.InputView"
+ class="oe_searchview_input"
+ contenteditable="true"/>
+<!-- tabindex: makes div focusable -->
+<div t-name="SearchView.FacetView"
+ class="oe_searchview_facet"
+ tabindex="0"
+ ><span class="oe_facet_remove">✖</span
+ ><span class="oe_facet_category oe_i" t-if="widget.model.has('icon')">
+ <t t-esc="widget.model.get('icon')"/>
+ </span
+ ><span class="oe_facet_category" t-if="!widget.model.has('icon')">
+ <t t-esc="widget.model.get('category')"/>
+ </span ><span class="oe_facet_values"
+/></div>
+<span t-name="SearchView.FacetView.Value" class="oe_facet_value">
+ <t t-esc="widget.model.get('label')"/>
+</span>
+
<t t-name="SearchView.managed-filters">
<option class="oe-filters-title" value="">Filters</option>
<optgroup label="-- Filters --">
@@ -1375,8 +1378,9 @@
<t t-esc="attrs.string"/>
</button>
<ul t-name="SearchView.filters">
- <li t-foreach="widget.filters" t-as="filter">
- <t t-esc="filter.attrs.string or filter.attrs.name or 'Ω'"/>
+ <li t-foreach="widget.filters" t-as="filter"
+ t-att-title="filter.attrs.string ? filter.attrs.help : undefined">
+ <t t-esc="filter.attrs.string or filter.attrs.help or filter.attrs.name or 'Ω'"/>
</li>
</ul>
<t t-name="SearchView.filters.facet">
@@ -1471,8 +1475,22 @@
</t>
</t>
</t>
+<div t-name="SearchView.Filters" class="oe_searchview_filters">
+
+</div>
+<div t-name="SearchView.CustomFilters" class="oe_searchview_custom">
+ <ul class="oe_searchview_custom_list"/>
+ <h4 class="oe_searchview_custom_title">
+ <label for="oe_searchview_custom_input">Save search</label></h4>
+ <form>
+ <input id="oe_searchview_custom_input"/>
+ <button>Save</button><br/>
+ <label for="oe_searchview_custom_public">Share with all users:</label>
+ <input id="oe_searchview_custom_public" type="checkbox"/>
+ </form>
+</div>
<div t-name="SearchView.advanced" class="oe_searchview_advanced">
- <h4>Advanced Search...</h4>
+ <h4>Advanced Search</h4>
<form>
<ul>
@@ -1494,26 +1512,23 @@
</select>
<select class="searchview_extended_prop_op"/>
<span class="searchview_extended_prop_value"/>
- <a class="searchview_extended_delete_prop"
- href="javascript:void(0)"><span> </span></a>
+ <button type="button" class="searchview_extended_delete_prop">Delete</button>
</li>
</t>
<t t-name="SearchView.extended_search.proposition.char">
- <input t-att-id="widget.element_id" class="field_char"/>
+ <input class="field_char"/>
</t>
<t t-name="SearchView.extended_search.proposition.empty">
- <span t-att-id="widget.element_id"></span>
+ <span/>
</t>
<t t-name="SearchView.extended_search.proposition.integer">
- <input type="number" t-att-id="widget.element_id" class="field_integer" step="1"/>
+ <input type="number" class="field_integer" step="1"/>
</t>
<t t-name="SearchView.extended_search.proposition.float">
- <input type="number" t-att-id="widget.element_id" class="field_float" step="0.01"/>
-</t>
-<t t-name="SearchView.extended_search.proposition.boolean">
+ <input type="number" class="field_float" step="0.01"/>
</t>
<t t-name="SearchView.extended_search.proposition.selection">
- <select t-att-id="widget.element_id">
+ <select>
<t t-foreach="widget.field.selection" t-as="element">
<option t-att-value="element[0]"><t t-esc="element[1]"/></option>
</t>