diff options
| author | Xavier Morel <xmo@openerp.com> | 2012-11-07 12:25:16 +0100 |
|---|---|---|
| committer | Xavier Morel <xmo@openerp.com> | 2012-11-07 12:25:16 +0100 |
| commit | 36dad4cef994449755d7d01560cd84a4f92b629e (patch) | |
| tree | 4b8aa0e2c8d0ca788856d8f542516b7619a18452 /addons/web/static/src | |
| parent | 64dc8880532ab198147a03fe6c78b28bd1af794e (diff) | |
| parent | 1c5e8da2765e51b2c8bb49c6d1bf3c6b55c95dbf (diff) | |
[MERGE] from trunk
bzr revid: xmo@openerp.com-20121107112516-64hqps4jjgmrs3a4
Diffstat (limited to 'addons/web/static/src')
28 files changed, 9934 insertions, 9056 deletions
diff --git a/addons/web/static/src/css/Makefile b/addons/web/static/src/css/Makefile index eca044497ef..d6b4f4b2fc8 100644 --- a/addons/web/static/src/css/Makefile +++ b/addons/web/static/src/css/Makefile @@ -1,3 +1,3 @@ base.css: base.sass - sass -t expanded base.sass base.css + sass --trace -t expanded base.sass base.css diff --git a/addons/web/static/src/css/base.css b/addons/web/static/src/css/base.css index f2fcc9abf33..30fe4d415cf 100644 --- a/addons/web/static/src/css/base.css +++ b/addons/web/static/src/css/base.css @@ -20,9 +20,19 @@ font-style: normal; } -.openerp.openerp-web-client-container { +@media print { + .oe_topbar, .oe_leftbar, .oe_loading { + display: none !important; + } +} + +.openerp.openerp_webclient_container { height: 100%; - position: relative; +} + +.text-tag .text-button { + height: auto !important; + min-height: 16px; } .openerp { @@ -32,16 +42,25 @@ color: #4c4c4c; font-size: 13px; background: white; + text-shadow: 0 1px 1px rgba(255, 255, 255, 0.5); /* http://www.quirksmode.org/dom/inputfile.html * http://stackoverflow.com/questions/2855589/replace-input-type-file-by-an-image */ } +.openerp :-moz-placeholder { + color: #afafb6 !important; + font-style: italic !important; +} +.openerp ::-webkit-input-placeholder { + color: #afafb6 !important; + font-style: italic !important; +} .openerp a { text-decoration: none; + cursor: pointer !important; } .openerp table { padding: 0; - font-size: 13px; border-collapse: collapse; } .openerp thead { @@ -49,10 +68,13 @@ background-color: #f0f0f0; } .openerp thead th { - border-right: 1px dotted #afafb6; + border-left: 1px solid #dfdfdf; } -.openerp thead th:last-child { - border-right: none; +.openerp thead th:first-child { + border-left: none; +} +.openerp thead th.null { + border-left: none; } .openerp th, .openerp td { padding: 0; @@ -84,59 +106,26 @@ background-image: -o-linear-gradient(top, #eeeeee, #dedede); background-image: linear-gradient(to bottom, #eeeeee, #dedede); } -.openerp .numeric { - text-align: right; - width: 82px; -} -.openerp .numeric input { - text-align: right; -} -.openerp ul, .openerp li, .openerp ol { - margin: 0; - padding: 0; -} -.openerp li { - list-style-type: none; -} -.openerp .oe_i { - font-family: "mnmliconsRegular" !important; - font-size: 21px; - font-weight: 300 !important; -} -.openerp .oe_e { - font-family: "entypoRegular" !important; - font-size: 34px; - font-weight: 300 !important; -} -.openerp .oe_left { - float: left; - margin-right: 8px; -} -.openerp .oe_right { - float: right; - margin-left: 8px; -} -.openerp .oe_text_right { - text-align: right; -} -.openerp .oe_clear { - clear: both; -} -.openerp .oe_wait { - cursor: wait; +.openerp input, .openerp textarea, .openerp select { + padding: 2px 4px; + border: 1px solid #cccccc; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; + background: white; } -.openerp .oe_fade { - color: #888888; +.openerp img { + vertical-align: middle; } -.openerp .oe_bold { - font-weight: bold; +.openerp h4 { + margin: 4px 0; } -.openerp a.button:link, .openerp a.button:visited, .openerp button, .openerp input[type='submit'] { +.openerp a.button:link, .openerp a.button:visited, .openerp button, .openerp input[type='submit'], .openerp .ui-dialog-buttonpane .ui-dialog-buttonset .ui-button { display: inline-block; - border: 1px solid #ababab; - color: #404040; + border: 1px solid rgba(0, 0, 0, 0.4); + color: #4c4c4c; margin: 0; - padding: 4px 12px; + padding: 3px 12px; font-size: 13px; text-align: center; background-color: #efefef; @@ -151,12 +140,12 @@ border-radius: 3px; -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 1px rgba(255, 255, 255, 0.8) inset; -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 1px rgba(255, 255, 255, 0.8) inset; - -box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 1px rgba(255, 255, 255, 0.8) inset; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 1px rgba(255, 255, 255, 0.8) inset; text-shadow: 0 1px 1px rgba(255, 255, 255, 0.5); -webkit-font-smoothing: antialiased; outline: none; } -.openerp a.button:hover, .openerp button:hover, .openerp input[type='submit']:hover { +.openerp a.button:hover, .openerp button:hover, .openerp input[type='submit']:hover, .openerp .ui-dialog-buttonpane .ui-dialog-buttonset .ui-button.ui-state-hover { background-color: #f6f6f6; background-image: -webkit-gradient(linear, left top, left bottom, from(#f6f6f6), to(#e3e3e3)); background-image: -webkit-linear-gradient(top, #f6f6f6, #e3e3e3); @@ -165,9 +154,11 @@ background-image: -o-linear-gradient(top, #f6f6f6, #e3e3e3); background-image: linear-gradient(to bottom, #f6f6f6, #e3e3e3); cursor: pointer; + background-position: 0; } -.openerp a.button:focus, .openerp button:focus, .openerp input[type='submit']:focus { +.openerp a.button:focus, .openerp button:focus, .openerp input[type='submit']:focus, .openerp .ui-dialog-buttonpane .ui-dialog-buttonset .ui-button.ui-state-focus { border: 1px solid #80bfff; + background-position: 0; background-color: #f6f6f6; background-image: -webkit-gradient(linear, left top, left bottom, from(#f6f6f6), to(#e3e3e3)); background-image: -webkit-linear-gradient(top, #f6f6f6, #e3e3e3); @@ -177,29 +168,259 @@ background-image: linear-gradient(to bottom, #f6f6f6, #e3e3e3); -moz-box-shadow: 0 0 3px #80bfff, 0 1px 1px rgba(255, 255, 255, 0.8) inset; -webkit-box-shadow: 0 0 3px #80bfff, 0 1px 1px rgba(255, 255, 255, 0.8) inset; - -box-shadow: 0 0 3px #80bfff, 0 1px 1px rgba(255, 255, 255, 0.8) inset; -} -.openerp a.button:active, .openerp a.button.active, .openerp button:active, .openerp button.active, .openerp input[type='submit']:active, .openerp input[type='submit'].active { - background: #e3e3e3; - background: -moz-linear-gradient(top, #e3e3e3, #f6f6f6) #1b468f; - background: -webkit-gradient(linear, left top, left bottom, from(#e3e3e3), to(#f6f6f6)) #1b468f; - background: linear-gradient(top, #e3e3e3, #f6f6f6) #1b468f; + box-shadow: 0 0 3px #80bfff, 0 1px 1px rgba(255, 255, 255, 0.8) inset; +} +.openerp a.button:active, .openerp a.button.active, .openerp button:active, .openerp button.active, .openerp input[type='submit']:active, .openerp input[type='submit'].active, .openerp .ui-dialog-buttonpane .ui-dialog-buttonset .ui-button.ui-state-active { + background-color: #e3e3e3; + background-image: -webkit-gradient(linear, left top, left bottom, from(#e3e3e3), to(#f6f6f6)); + background-image: -webkit-linear-gradient(top, #e3e3e3, #f6f6f6); + background-image: -moz-linear-gradient(top, #e3e3e3, #f6f6f6); + background-image: -ms-linear-gradient(top, #e3e3e3, #f6f6f6); + background-image: -o-linear-gradient(top, #e3e3e3, #f6f6f6); + background-image: linear-gradient(to bottom, #e3e3e3, #f6f6f6); -moz-box-shadow: none; -webkit-box-shadow: none; - -box-shadow: none; + box-shadow: none; } .openerp a.button.disabled, .openerp button:disabled, .openerp input[type='submit']:disabled { background: #efefef !important; border: 1px solid #d1d1d1 !important; -moz-box-shadow: none !important; -webkit-box-shadow: none !important; - -box-shadow: none !important; + box-shadow: none !important; color: #aaaaaa !important; cursor: default; text-shadow: 0 1px 1px white !important; } -.openerp button.primary { +.openerp .ui-widget-content a { + color: #7c7bad; +} +.openerp .oe_bounce_container { + display: inline-block; +} +.openerp .ui-tabs { + position: static; +} +.openerp.ui-dialog { + display: none; + padding: 6px; + background-color: rgba(60, 60, 60, 0.7); + border: 1px solid; + border-color: #888888 #555555 #444444; + -moz-border-radius: 8px; + -webkit-border-radius: 8px; + border-radius: 8px; + -moz-box-shadow: 0 1px 12px rgba(0, 0, 0, 0.6); + -webkit-box-shadow: 0 1px 12px rgba(0, 0, 0, 0.6); + box-shadow: 0 1px 12px rgba(0, 0, 0, 0.6); + -webkit-background-clip: padding-box; + -moz-background-clip: padding-box; + background-clip: padding-box; +} +.openerp.ui-dialog .ui-dialog-content { + padding: 0; +} +.openerp.ui-dialog .ui-dialog-titlebar, .openerp.ui-dialog .ui-dialog-content, .openerp.ui-dialog .ui-dialog-buttonpane { + padding: 16px; +} +.openerp.ui-dialog .ui-dialog-titlebar { + border-bottom: 1px solid #cacaca; + -moz-border-radius: 2px 2px 0 0; + -webkit-border-radius: 2px 2px 0 0; + border-radius: 2px 2px 0 0; + background-color: #fcfcfc; + background-image: -webkit-gradient(linear, left top, left bottom, from(#fcfcfc), to(#dedede)); + background-image: -webkit-linear-gradient(top, #fcfcfc, #dedede); + background-image: -moz-linear-gradient(top, #fcfcfc, #dedede); + background-image: -ms-linear-gradient(top, #fcfcfc, #dedede); + background-image: -o-linear-gradient(top, #fcfcfc, #dedede); + background-image: linear-gradient(to bottom, #fcfcfc, #dedede); +} +.openerp.ui-dialog .ui-dialog-titlebar .ui-dialog-title { + margin: 0; + padding: 0; +} +.openerp.ui-dialog .ui-widget-header { + border: none; +} +.openerp.ui-dialog .ui-dialog-content { + background: white; +} +.openerp.ui-dialog .ui-dialog-buttonpane { + border-top: 1px solid #e0e0e0; + background: #f5f7f9; + margin: 0; + -moz-border-radius: 0 0 2px 2px; + -webkit-border-radius: 0 0 2px 2px; + border-radius: 0 0 2px 2px; +} +.openerp.ui-dialog .ui-dialog-buttonpane button { + margin: 0 4px 0 0; +} +.openerp.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset { + float: left; +} +.openerp.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset .ui-button { + margin-right: 4px; +} +.openerp.ui-dialog .ui-dialog-titlebar-close { + padding: 0; +} +.openerp.ui-dialog .ui-dialog-titlebar-close .ui-icon-closethick { + display: none; +} +.openerp.ui-dialog .ui-dialog-titlebar-close:before { + content: "×"; + font-size: 18px; + font-weight: bold; + line-height: 16px; + color: black; + text-shadow: 0 1px 0 white; + padding: 0; + cursor: pointer; + background: transparent; + border: 0; +} +.openerp.ui-dialog .ui-dialog-titlebar-close:before:hover { + color: black; + text-decoration: none; +} +.openerp.ui-dialog .oe_about { + background-color: white; + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAAAKUlEQVQIHWO8e/fufwYsgAUkJigoiCIF5DMyoYggcUiXgNnBiGQKmAkARpcEQeriln4AAAAASUVORK5CYII=); + -moz-border-radius: 0 0 2px 2px; + -webkit-border-radius: 0 0 2px 2px; + border-radius: 0 0 2px 2px; +} +.openerp.ui-dialog .oe_about a { + color: #7c7bad; +} +.openerp.ui-dialog .oe_about a:hover { + text-decoration: underline; +} +.openerp.ui-dialog .oe_about a:focus { + outline: none; +} +.openerp.ui-dialog .oe_about .oe_logo { + margin-left: -6px; +} +.openerp.ui-dialog .oe_about .oe_bottom { + position: absolute; + top: 50%; + left: 0; + right: 0; + bottom: 0; + text-shadow: 0 1px 1px #999999; + background-color: #b41616; + background-image: -webkit-gradient(linear, left top, left bottom, from(#b41616), to(#600606)); + background-image: -webkit-linear-gradient(top, #b41616, #600606); + background-image: -moz-linear-gradient(top, #b41616, #600606); + background-image: -ms-linear-gradient(top, #b41616, #600606); + background-image: -o-linear-gradient(top, #b41616, #600606); + background-image: linear-gradient(to bottom, #b41616, #600606); + color: #eeeeee; + padding: 0 16px; + -moz-border-radius: 0 0 2px 2px; + -webkit-border-radius: 0 0 2px 2px; + border-radius: 0 0 2px 2px; +} +.openerp.ui-dialog .oe_about .oe_bottom a { + color: #eeeeee; +} +.openerp.ui-dialog.oe_act_window .ui-dialog-content { + padding: 0px; +} +.openerp .modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + background-color: black; + filter: alpha(opacity=30); + opacity: 0.3; +} +.openerp .oe_i { + font-family: "mnmliconsRegular" !important; + font-size: 21px; + font-weight: 300 !important; +} +.openerp .oe_e { + font-family: "entypoRegular" !important; + font-size: 34px; + font-weight: 300 !important; +} +.openerp .oe_left { + float: left; + margin-right: 8px; +} +.openerp .oe_right { + float: right; + margin-left: 8px; +} +.openerp .oe_text_right { + text-align: right; +} +.openerp .oe_clear { + clear: both; +} +.openerp .oe_wait { + cursor: wait; +} +.openerp .oe_fade { + color: #888888; + font-weight: normal; +} +.openerp .oe_bold { + font-weight: bold; +} +.openerp .oe_inline { + width: auto !important; +} +.openerp .oe_highlight { color: white; + background: #dc5f59; +} +.openerp button.oe_highlight { + background-color: #df3f3f; + background-image: -webkit-gradient(linear, left top, left bottom, from(#df3f3f), to(#a21a1a)); + background-image: -webkit-linear-gradient(top, #df3f3f, #a21a1a); + background-image: -moz-linear-gradient(top, #df3f3f, #a21a1a); + background-image: -ms-linear-gradient(top, #df3f3f, #a21a1a); + background-image: -o-linear-gradient(top, #df3f3f, #a21a1a); + background-image: linear-gradient(to bottom, #df3f3f, #a21a1a); + -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 1px rgba(155, 155, 155, 0.4) inset; + -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 1px rgba(155, 155, 155, 0.4) inset; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 1px rgba(155, 155, 155, 0.4) inset; +} +.openerp button.oe_highlight:hover { + background-color: #e25858; + background-image: -webkit-gradient(linear, left top, left bottom, from(#e25858), to(#ab1b1b)); + background-image: -webkit-linear-gradient(top, #e25858, #ab1b1b); + background-image: -moz-linear-gradient(top, #e25858, #ab1b1b); + background-image: -ms-linear-gradient(top, #e25858, #ab1b1b); + background-image: -o-linear-gradient(top, #e25858, #ab1b1b); + background-image: linear-gradient(to bottom, #e25858, #ab1b1b); +} +.openerp button.oe_highlight:active { + background-color: #c52020; + background-image: -webkit-gradient(linear, left top, left bottom, from(#c52020), to(#d22323)); + background-image: -webkit-linear-gradient(top, #c52020, #d22323); + background-image: -moz-linear-gradient(top, #c52020, #d22323); + background-image: -ms-linear-gradient(top, #c52020, #d22323); + background-image: -o-linear-gradient(top, #c52020, #d22323); + background-image: linear-gradient(to bottom, #c52020, #d22323); +} +.openerp .oe_background_grey { + background: #eeeeee !important; +} +.openerp .oe_form_dirty .oe_highlight_on_dirty { + color: white; + background: #dc5f59; + font-weight: bold; +} +.openerp .oe_form_dirty button.oe_highlight_on_dirty { background-color: #dc5f59; background-image: -webkit-gradient(linear, left top, left bottom, from(#dc5f59), to(#b33630)); background-image: -webkit-linear-gradient(top, #dc5f59, #b33630); @@ -209,54 +430,358 @@ background-image: linear-gradient(to bottom, #dc5f59, #b33630); -moz-box-shadow: none; -webkit-box-shadow: none; - -box-shadow: none; -} -.openerp button.primary:hover { - background-color: #df6b66; - background-image: -webkit-gradient(linear, left top, left bottom, from(#df6b66), to(#bf3a33)); - background-image: -webkit-linear-gradient(top, #df6b66, #bf3a33); - background-image: -moz-linear-gradient(top, #df6b66, #bf3a33); - background-image: -ms-linear-gradient(top, #df6b66, #bf3a33); - background-image: -o-linear-gradient(top, #df6b66, #bf3a33); - background-image: linear-gradient(to bottom, #df6b66, #bf3a33); - -moz-box-shadow: 0 0 1px rgba(0, 0, 0, 0.2); - -webkit-box-shadow: 0 0 1px rgba(0, 0, 0, 0.2); - -box-shadow: 0 0 1px rgba(0, 0, 0, 0.2); -} -.openerp button.primary:active { - background-color: #b33630; - background-image: -webkit-gradient(linear, left top, left bottom, from(#b33630), to(#dc5f59)); - background-image: -webkit-linear-gradient(top, #b33630, #dc5f59); - background-image: -moz-linear-gradient(top, #b33630, #dc5f59); - background-image: -ms-linear-gradient(top, #b33630, #dc5f59); - background-image: -o-linear-gradient(top, #b33630, #dc5f59); - background-image: linear-gradient(to bottom, #b33630, #dc5f59); + box-shadow: none; +} +.openerp .oe_form_dirty button.oe_highlight_on_dirty:hover { + background: #ed6f6a; +} +.openerp .oe_title { + width: 50%; + float: left; +} +.openerp .oe_title:after { + content: "."; + display: block; + height: 0; + clear: both; + visibility: hidden; +} +.openerp .oe_button_box { + width: 270px; + text-align: right; +} +.openerp .oe_button_box button { + margin: 4px; +} +.openerp .oe_avatar > img { + max-height: 90px; + max-width: 90px; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; + -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4); + -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4); + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4); + border: none; + margin-bottom: 10px; +} +.openerp .oe_avatar + div { + margin-left: 5px; +} +.openerp .oe_image_small > img { + max-width: 50px; + max-height: 50px; +} +.openerp .oe_image_medium > img { + max-width: 180px; + max-height: 180px; +} +.openerp .oe_button.oe_link { + border: none; + padding: 0; + margin: 0; + background: none; + -moz-border-radius: 0; + -webkit-border-radius: 0; + border-radius: 0; + -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; +} +.openerp .oe_button.oe_link img { + display: none; +} +.openerp .oe_button.oe_link span { + border: none; + padding: 0; + margin: 0; + background: none; + -moz-border-radius: 0; + -webkit-border-radius: 0; + border-radius: 0; -moz-box-shadow: none; -webkit-box-shadow: none; - -box-shadow: none; + box-shadow: none; + color: #7c7bad; + font-weight: bold; +} +.openerp .oe_button.oe_link span:hover { + text-decoration: underline; +} +.openerp .oe_webclient .oe_star_on, .openerp .oe_webclient .oe_star_off { + color: #cccccc; + text-shadow: 0 0 2px black; + vertical-align: top; + position: relative; + top: -5px; +} +.openerp .oe_webclient .oe_star_on:hover, .openerp .oe_webclient .oe_star_off:hover { + text-decoration: none; +} +.openerp .oe_webclient .oe_star_on { + color: gold; +} +.openerp p.oe_grey { + max-width: 650px; +} +.openerp .oe_grey { + color: #aaaaaa; +} +.openerp .oe_tag { + border: 1px solid #afafb6; + font-size: 11px; + padding: 2px 4px; + margin: 0 2px 2px 0; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; + background: #f0f0fa; + color: #4c4c4c; } -.openerp button.following { +.openerp .oe_tag_dark { + background: #7c7bad; + color: #eeeeee; +} +.openerp .oe_tags { + margin-bottom: 1px; +} +.openerp .oe_tags.oe_inline { + min-width: 250px; +} +.openerp .oe_tags .text-wrap { + width: 100% !important; +} +.openerp .oe_tags .text-wrap textarea { + width: 100% !important; +} +.openerp .oe_tags .text-core .text-wrap .text-dropdown .text-list .text-suggestion em { + font-style: italic; + text-decoration: none; +} +.openerp.oe_tooltip { + font-size: 12px; +} +.openerp.oe_tooltip .oe_tooltip_string { + color: #ffdd55; + font-weight: bold; + font-size: 13px; +} +.openerp.oe_tooltip .oe_tooltip_help { + white-space: pre-wrap; +} +.openerp.oe_tooltip .oe_tooltip_technical { + padding: 0 0 4px 0; + margin: 5px 0 0 15px; +} +.openerp.oe_tooltip .oe_tooltip_technical li { + list-style: circle; +} +.openerp.oe_tooltip .oe_tooltip_technical_title { + font-weight: bold; +} +.openerp.oe_tooltip .oe_tooltip_close { + margin: -5px 0 0 2px; + cursor: default; + float: right; color: white; - width: 120px; - background-color: #8a89ba; - background-image: -webkit-gradient(linear, left top, left bottom, from(#8a89ba), to(#807fb4)); - background-image: -webkit-linear-gradient(top, #8a89ba, #807fb4); - background-image: -moz-linear-gradient(top, #8a89ba, #807fb4); - background-image: -ms-linear-gradient(top, #8a89ba, #807fb4); - background-image: -o-linear-gradient(top, #8a89ba, #807fb4); - background-image: linear-gradient(to bottom, #8a89ba, #807fb4); -} -.openerp button.unfollow { +} +.openerp.oe_tooltip .oe_tooltip_close:hover { + color: #999999; + cursor: pointer; +} +.openerp.oe_tooltip .oe_tooltip_message { + max-width: 310px; +} +.openerp .oe_notebook { + margin: 8px 0; + padding: 0 16px; + list-style: none; + zoom: 1; +} +.openerp .oe_notebook.ui-corner-all { + -moz-border-radius: 0; + -webkit-border-radius: 0; + border-radius: 0; +} +.openerp .oe_notebook:before, .openerp .oe_notebook:after { + display: table; + content: ""; + zoom: 1; +} +.openerp .oe_notebook:after { + clear: both; +} +.openerp .oe_notebook > li { + float: left; +} +.openerp .oe_notebook > li > a { + display: block; + color: #4c4c4c; +} +.openerp .oe_notebook { + border-color: #dddddd; + border-style: solid; + border-width: 0 0 1px; +} +.openerp .oe_notebook > li { + position: relative; +} +.openerp .oe_notebook > li > a { + padding: 0 12px; + margin-right: 2px; + line-height: 30px; + border: 1px solid transparent; + -moz-border-radius: 4px 4px 0 0; + -webkit-border-radius: 4px 4px 0 0; + border-radius: 4px 4px 0 0; +} +.openerp .oe_notebook > li > a:hover { + text-decoration: none; + background-color: #eeeeee; + border-color: #eeeeee #eeeeee #dddddd; +} +.openerp .oe_notebook > li.ui-state-active > a, .openerp .oe_notebook > li.ui-state-active > a:hover { + background-color: white; + border: 1px solid #dddddd; + border-bottom-color: transparent; + cursor: default; +} +.openerp .oe_notebook_page { + padding: 0; +} +.openerp div.ui-tabs { + padding: 3px 0px 3px 0px; +} +.openerp .ui-tabs-hide { display: none; +} +.openerp .oe_dropdown, .openerp .oe_dropdown_hover, .openerp .oe_dropdown_toggle { + position: relative; + cursor: pointer; +} +.openerp .oe_dropdown_toggle { + color: rgba(0, 0, 0, 0.3); + font-weight: normal; +} +.openerp .oe_dropdown_hover:hover .oe_dropdown_menu, .openerp .oe_dropdown_menu.oe_opened { + display: block; +} +.openerp .oe_dropdown_menu { + display: none; + position: absolute; + top: 26px; + left: 0; + z-index: 3; + margin: 0; + padding: 0; + border: 1px solid #afafb6; + background: white; + padding: 4px 0; + min-width: 140px; + text-align: left; + -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_dropdown_menu > li { + list-style-type: none; + float: none; + display: block; + position: relative; + margin: 0; + padding: 2px 8px; +} +.openerp .oe_dropdown_menu > li:hover { + background-color: #f0f0fa; + 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_dropdown_menu > li > a { + white-space: nowrap; + display: block; + color: #4c4c4c; + text-decoration: none; +} +.openerp .oe_dropdown_menu > li > a:hover { + text-decoration: none; +} +.openerp .oe_dropdown_arrow:after { + width: 0; + height: 0; + display: inline-block; + content: "&darr"; + text-indent: -99999px; + vertical-align: top; + margin-top: 8px; + margin-left: 3px; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 4px solid #404040; + 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: #f0f0fa; + 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; - width: 120px; - background-color: #dc5f59; - background-image: -webkit-gradient(linear, left top, left bottom, from(#dc5f59), to(#b33630)); - background-image: -webkit-linear-gradient(top, #dc5f59, #b33630); - background-image: -moz-linear-gradient(top, #dc5f59, #b33630); - background-image: -ms-linear-gradient(top, #dc5f59, #b33630); - background-image: -o-linear-gradient(top, #dc5f59, #b33630); - background-image: linear-gradient(to bottom, #dc5f59, #b33630); + 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; @@ -279,14 +804,15 @@ z-index: 1050; } .openerp .oe_login { - background: url("/web/static/src/img/pattern.png") repeat; + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAAAKUlEQVQIHWO8e/fufwYsgAUkJigoiCIF5DMyoYggcUiXgNnBiGQKmAkARpcEQeriln4AAAAASUVORK5CYII=); text-align: center; font-size: 14px; height: 100%; } -.openerp .oe_login li { +.openerp .oe_login ul, .openerp .oe_login li { + margin: 0; list-style-type: none; - padding-bottom: 4px; + padding: 0 0 4px 0; } .openerp .oe_login button { float: right; @@ -308,16 +834,16 @@ border-radius: 4px; -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 1px rgba(155, 155, 155, 0.4) inset; -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 1px rgba(155, 155, 155, 0.4) inset; - -box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 1px rgba(155, 155, 155, 0.4) inset; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 1px rgba(155, 155, 155, 0.4) inset; } .openerp .oe_login input, .openerp .oe_login select { width: 252px; font-family: "Lucida Grande", Helvetica, Verdana, Arial; border: 1px solid #999999; background: whitesmoke; - -moz-box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.3); - -webkit-box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.3); - -box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.3); + -moz-box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.4); + -webkit-box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.4); + box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.4); -moz-border-radius: 3px; -webkit-border-radius: 3px; border-radius: 3px; @@ -377,7 +903,7 @@ border-radius: 8px; -moz-box-shadow: 0 0 18px rgba(0, 0, 0, 0.9); -webkit-box-shadow: 0 0 18px rgba(0, 0, 0, 0.9); - -box-shadow: 0 0 18px rgba(0, 0, 0, 0.9); + box-shadow: 0 0 18px rgba(0, 0, 0, 0.9); } .openerp .oe_login .oe_login_pane h2 { margin-top: 0; @@ -421,7 +947,7 @@ border-radius: 4px; -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.8); -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.8); - -box-shadow: 0 1px 4px rgba(0, 0, 0, 0.8); + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.8); } .openerp .oe_login_invalid .oe_login_error_message { display: inline-block; @@ -448,8 +974,8 @@ } .openerp .oe_topbar { width: 100%; - height: 31px; - border-top: solid 1px #d3d3d3; + height: 32px; + background-color: #414141; background-color: #646060; background-image: -webkit-gradient(linear, left top, left bottom, from(#646060), to(#262626)); background-image: -webkit-linear-gradient(top, #646060, #262626); @@ -458,189 +984,47 @@ background-image: -o-linear-gradient(top, #646060, #262626); background-image: linear-gradient(to bottom, #646060, #262626); } -.openerp .oe_topbar .oe_topbar_item li { - float: left; -} -.openerp .oe_topbar .oe_topbar_item li a { +.openerp .oe_topbar .oe_topbar_item { display: block; padding: 5px 10px 7px; line-height: 20px; height: 20px; + text-decoration: none; color: #eeeeee; vertical-align: top; text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); -} -.openerp .oe_topbar .oe_topbar_item li a:hover { - background: #303030; - color: white; - -moz-box-shadow: 0 1px 2px rgba(255, 255, 255, 0.3) inset; - -webkit-box-shadow: 0 1px 2px rgba(255, 255, 255, 0.3) inset; - -box-shadow: 0 1px 2px rgba(255, 255, 255, 0.3) inset; -} -.openerp .oe_topbar .oe_topbar_item .oe_active { - background: #303030; - font-weight: bold; + -webkit-transition: all 0.2s ease-out; + -moz-transition: all 0.2s ease-out; + -ms-transition: all 0.2s ease-out; + -o-transition: all 0.2s ease-out; + transition: all 0.2s ease-out; +} +.openerp .oe_topbar .oe_topbar_item:hover { + background: rgba(0, 0, 0, 0.2); + text-shadow: black 0px 0px 3px; color: white; - -moz-box-shadow: 0 1px 2px rgba(255, 255, 255, 0.3) inset; - -webkit-box-shadow: 0 1px 2px rgba(255, 255, 255, 0.3) inset; - -box-shadow: 0 1px 2px rgba(255, 255, 255, 0.3) inset; + -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4) inset; + -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4) inset; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4) inset; } .openerp .oe_topbar .oe_topbar_avatar { width: 24px; height: 24px; margin: -2px 2px 0 0; - -moz-border-radius: 4px; - -webkit-border-radius: 4px; - border-radius: 4px; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; } .openerp .oe_topbar .oe_topbar_avatar { vertical-align: top; } -.openerp .oe_topbar .oe_dropdown_toggle:after { - width: 0; - height: 0; - display: inline-block; - content: "&darr"; - text-indent: -99999px; - vertical-align: top; - margin-top: 8px; - margin-left: 4px; - border-left: 4px solid transparent; - border-right: 4px solid transparent; +.openerp .oe_topbar .oe_dropdown_arrow:after { border-top: 4px solid white; - filter: alpha(opacity=50); - opacity: 0.5; -} -.openerp .oe_leftbar { - width: 220px; - background: #f0eeee; - border-right: 1px solid #afafb6; - text-shadow: 0 1px 1px white; - padding-bottom: 16px; - line-height: 18px; -} -.openerp a.oe_logo { - width: 220px; - display: block; - text-align: center; - height: 70px; - line-height: 70px; } -.openerp a.oe_logo img { - height: 40px; - width: 157px; - margin: 14px 0; -} -.openerp .oe_footer { - position: fixed; - bottom: 0; - padding: 4px 0; - background: #f0eeee; - width: 220px; - text-align: center; -} -.openerp .oe_footer a { - font-weight: 800; - font-family: serif; - font-size: 16px; - color: black; -} -.openerp .oe_footer a span { - color: #c81010; - font-style: italic; -} -.openerp .oe_user_menu { - float: right; - padding: 0; - margin: 0; -} -.openerp .oe_user_menu li { - list-style-type: none; - float: left; -} -.openerp .oe_user_menu .oe_dropdown { - position: relative; -} -.openerp .oe_user_menu .oe_dropdown_options { - float: left; - background: #333333; - background: rgba(37, 37, 37, 0.9); - display: none; - position: absolute; +.openerp .oe_topbar .oe_dropdown_menu { top: 32px; - right: -1px; - border: 0; - z-index: 900; - margin-left: 0; - margin-right: 0; - padding: 6px 0; - zoom: 1; - border-color: #999999; - border-color: rgba(0, 0, 0, 0.2); - border-style: solid; - border-width: 0 1px 1px; - -moz-border-radius: 0 0 6px 6px; - -webkit-border-radius: 0 0 6px 6px; - border-radius: 0 0 6px 6px; - -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); - -webkit-background-clip: padding-box; - -moz-background-clip: padding-box; - background-clip: padding-box; -} -.openerp .oe_user_menu .oe_dropdown_options li { - float: none; - display: block; - background-color: none; -} -.openerp .oe_user_menu .oe_dropdown_options li a { - display: block; - padding: 4px 15px; - clear: both; - font-weight: normal; - line-height: 18px; - color: #eeeeee; -} -.openerp .oe_user_menu .oe_dropdown_options li a:hover { - background-color: #292929; - background-image: -webkit-gradient(linear, left top, left bottom, from(#292929), to(#191919)); - background-image: -webkit-linear-gradient(top, #292929, #191919); - background-image: -moz-linear-gradient(top, #292929, #191919); - background-image: -ms-linear-gradient(top, #292929, #191919); - background-image: -o-linear-gradient(top, #292929, #191919); - background-image: linear-gradient(to bottom, #292929, #191919); - -moz-box-shadow: none; - -webkit-box-shadow: none; - -box-shadow: none; -} -.openerp .oe_user_menu .oe_dropdown_options li hr { - border-top: 1px solid #999999; - border-bottom: 0; -} -.openerp .oe_systray > div { - float: left; - padding: 0 4px 0 4px; -} -.openerp .oe_systray { - float: right; -} -.openerp .oe_systray .oe_systray_dropdown { - position: relative; -} -.openerp .oe_systray .oe_systray_dropdown:hover .oe_systray_dropdown_options { - display: block; -} -.openerp .oe_systray .oe_systray_dropdown_options { background: #333333; background: rgba(37, 37, 37, 0.9); - display: none; - position: absolute; - z-index: 900; - border: 0; - margin-left: 0; - margin-right: 0; - padding: 6px 0; border-color: #999999; border-color: rgba(0, 0, 0, 0.2); border-style: solid; @@ -648,27 +1032,18 @@ -moz-border-radius: 0 0 6px 6px; -webkit-border-radius: 0 0 6px 6px; border-radius: 0 0 6px 6px; - -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); -webkit-background-clip: padding-box; -moz-background-clip: padding-box; background-clip: padding-box; } -.openerp .oe_systray .oe_systray_dropdown_options li { +.openerp .oe_topbar .oe_dropdown_menu li { float: none; - display: block; - background-color: none; + padding: 3px 12px; } -.openerp .oe_systray .oe_systray_dropdown_options li a { - display: block; - padding: 4px 15px; - clear: both; - font-weight: normal; - line-height: 18px; +.openerp .oe_topbar .oe_dropdown_menu li a { color: #eeeeee; } -.openerp .oe_systray .oe_systray_dropdown_options li a:hover { +.openerp .oe_topbar .oe_dropdown_menu li:hover { background-color: #292929; background-image: -webkit-gradient(linear, left top, left bottom, from(#292929), to(#191919)); background-image: -webkit-linear-gradient(top, #292929, #191919); @@ -678,62 +1053,113 @@ background-image: linear-gradient(to bottom, #292929, #191919); -moz-box-shadow: none; -webkit-box-shadow: none; - -box-shadow: none; + box-shadow: none; } .openerp .oe_menu { float: left; padding: 0; margin: 0; + font-size: 13px; } -.openerp .oe_menu li { +.openerp .oe_menu > li { + list-style-type: none; + padding: 0; + margin: 0; float: left; + display: block; + color: #eeeeee; } -.openerp .oe_menu a { +.openerp .oe_menu > li > a { display: block; padding: 5px 10px 7px; line-height: 20px; height: 20px; + text-decoration: none; color: #eeeeee; vertical-align: top; text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); -} -.openerp .oe_menu a:hover { - background: #303030; + -webkit-transition: all 0.2s ease-out; + -moz-transition: all 0.2s ease-out; + -ms-transition: all 0.2s ease-out; + -o-transition: all 0.2s ease-out; + transition: all 0.2s ease-out; +} +.openerp .oe_menu > li > a:hover { + background: rgba(0, 0, 0, 0.2); + text-shadow: black 0px 0px 3px; color: white; - -moz-box-shadow: 0 1px 2px rgba(255, 255, 255, 0.3) inset; - -webkit-box-shadow: 0 1px 2px rgba(255, 255, 255, 0.3) inset; - -box-shadow: 0 1px 2px rgba(255, 255, 255, 0.3) inset; + -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4) inset; + -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4) inset; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4) inset; } -.openerp .oe_menu .oe_active { - background: #303030; - font-weight: bold; - color: white; - -moz-box-shadow: 0 1px 2px rgba(255, 255, 255, 0.3) inset; - -webkit-box-shadow: 0 1px 2px rgba(255, 255, 255, 0.3) inset; - -box-shadow: 0 1px 2px rgba(255, 255, 255, 0.3) inset; -} -.openerp .oe_menu_more_container { - position: relative; +.openerp .oe_menu > li > .oe_active { + background: rgba(0, 0, 0, 0.3); + text-shadow: black 0px 0px 3px; + -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4) inset; + -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4) inset; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4) inset; } -.openerp .oe_menu_more_container .oe_menu_more { - position: absolute; +.openerp .oe_user_menu { + float: right; padding: 0; - background-color: #646060; - z-index: 1; - border: 1px solid black; - border-bottom-left-radius: 5px; - border-bottom-right-radius: 5px; + margin: 0; } -.openerp .oe_menu_more_container .oe_menu_more li { - float: none; +.openerp .oe_user_menu li { + list-style-type: none; + float: left; } -.openerp .oe_menu_more_container .oe_menu_more li a { - white-space: nowrap; +.openerp .oe_user_menu .oe_dropdown_menu { + right: -1px; +} +.openerp .oe_systray > div { + float: left; + padding: 0 4px 0 4px; +} +.openerp .oe_systray { + float: right; +} +.openerp .oe_leftbar { + display: none; + width: 220px; + background: #f0eeee; + border-right: 1px solid #afafb6; + text-shadow: 0 1px 1px white; + padding-bottom: 16px; +} +.openerp a.oe_logo { + width: 220px; + display: block; + text-align: center; + height: 70px; + line-height: 70px; +} +.openerp a.oe_logo img { + height: 40px; + width: 157px; + margin: 14px 0; +} +.openerp .oe_footer { + position: fixed; + bottom: 0; + padding: 4px 0; + background: #f0eeee; + width: 220px; + text-align: center; +} +.openerp .oe_footer a { + font-weight: 800; + font-family: serif; + font-size: 16px; + color: black; +} +.openerp .oe_footer a span { + color: #c81010; + font-style: italic; } .openerp .oe_secondary_menu_section { font-weight: bold; margin-left: 8px; - color: #8a89ba; + color: #7c7bad; } .openerp .oe_secondary_submenu { padding: 2px 0 8px 0; @@ -741,21 +1167,23 @@ width: 100%; display: inline-block; } -.openerp .oe_secondary_submenu > li { +.openerp .oe_secondary_submenu li { position: relative; - padding: 1px 0 1px 20px; + margin: 0; + padding: 1px 0 1px 20px !important; + list-style-type: none; } -.openerp .oe_secondary_submenu > li a { +.openerp .oe_secondary_submenu li a { display: block; color: #4c4c4c; padding: 2px 4px 2px 0; } -.openerp .oe_secondary_submenu > li .oe_menu_label { +.openerp .oe_secondary_submenu li .oe_menu_label { position: absolute; top: 1px; right: 1px; font-size: 10px; - background: #8a89ba; + background: #7c7bad; color: white; padding: 2px 4px; margin: 1px 6px 0 0; @@ -766,44 +1194,49 @@ border-radius: 4px; -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.2); -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.2); - -box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.2); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.2); } .openerp .oe_secondary_submenu .oe_menu_counter { float: right; - background: #8a89ba; - color: #eeeeee; - font-size: 12px; - border: 1px solid lightgray; - padding: 0px 4px 0px 4px; text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); - margin: 0px; - -moz-border-radius: 4px; - -webkit-border-radius: 4px; - border-radius: 4px; + margin: 1px; + padding: 1px 4px; + border: none; +} +.openerp .oe_secondary_submenu .oe_menu_counter:hover { + cursor: pointer; + -webkit-transform: scale(1.1); + -moz-transform: scale(1.1); + -ms-transform: scale(1.1); + -o-transform: scale(1.1); + transform: scale(1.1); } .openerp .oe_secondary_submenu .oe_active { - background: #8a89ba; border-top: 1px solid lightGray; - border-bottom: 1px solid lightGray; + border-bottom: 1px solid #dedede; text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.2); - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.2); - -box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.2); + -moz-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2), inset 0 -1px 3px rgba(40, 40, 40, 0.2); + -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2), inset 0 -1px 3px rgba(40, 40, 40, 0.2); + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2), inset 0 -1px 3px rgba(40, 40, 40, 0.2); + background: #7c7bad; } .openerp .oe_secondary_submenu .oe_active a { color: white; } .openerp .oe_secondary_submenu .oe_active .oe_menu_label { background: #eeeeee; - color: #8a89ba; + color: #7c7bad; text-shadow: 0 1px 1px white; -moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); - -box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); } .openerp .oe_secondary_submenu .oe_active .oe_menu_counter { background: #eeeeee; - color: #8a89ba; + color: #7c7bad; + -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.2); + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.2); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.2); } .openerp .oe_secondary_submenu .oe_menu_toggler:before { width: 0; @@ -829,190 +1262,153 @@ border-right: 4px solid transparent; border-top: 4px solid #4c4c4c; } -.openerp .oe_application { - width: 100%; +.openerp .oe_about { + background-color: white; + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAAAKUlEQVQIHWO8e/fufwYsgAUkJigoiCIF5DMyoYggcUiXgNnBiGQKmAkARpcEQeriln4AAAAASUVORK5CYII=); + -moz-border-radius: 0 0 2px 2px; + -webkit-border-radius: 0 0 2px 2px; + border-radius: 0 0 2px 2px; } -.openerp .oe_application a { - color: #8a89ba; +.openerp .oe_about a { + color: #7c7bad; } -.openerp .oe_application a:hover { +.openerp .oe_about a:hover { text-decoration: underline; } -.openerp .oe-view-manager-header { - border-top: 1px solid #cacaca; - border-bottom: 1px solid #cacaca; - background-color: #fcfcfc; - background-image: -webkit-gradient(linear, left top, left bottom, from(#fcfcfc), to(#dedede)); - background-image: -webkit-linear-gradient(top, #fcfcfc, #dedede); - background-image: -moz-linear-gradient(top, #fcfcfc, #dedede); - background-image: -ms-linear-gradient(top, #fcfcfc, #dedede); - background-image: -o-linear-gradient(top, #fcfcfc, #dedede); - background-image: linear-gradient(to bottom, #fcfcfc, #dedede); - -moz-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.4), 0 0 9px rgba(0, 0, 0, 0.1); - -webkit-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.4), 0 0 9px rgba(0, 0, 0, 0.1); - -box-shadow: 0 1px 0 rgba(255, 255, 255, 0.4), 0 0 9px rgba(0, 0, 0, 0.1); -} -.openerp .oe-view-manager-header h2 { - float: left; - font-size: 18px; - margin: 1px 0; -} -.openerp .oe-view-manager-header h2 a { - color: #8a89ba; -} -.openerp .oe-view-manager-header .buttons { - padding-right: 8px; +.openerp .oe_about a:focus { + outline: none; } -.openerp .oe-view-manager-header .buttons li { - float: left; - margin-right: 12px; +.openerp .oe_about .oe_logo { + margin-left: -6px; } -.openerp .oe-view-manager-header .filter { - vertical-align: top; +.openerp .oe_about .oe_bottom { + position: absolute; + top: 50%; + left: 0; + right: 0; + bottom: 0; + text-shadow: 0 1px 1px #999999; + background-color: #b41616; + background-image: -webkit-gradient(linear, left top, left bottom, from(#b41616), to(#600606)); + background-image: -webkit-linear-gradient(top, #b41616, #600606); + background-image: -moz-linear-gradient(top, #b41616, #600606); + background-image: -ms-linear-gradient(top, #b41616, #600606); + background-image: -o-linear-gradient(top, #b41616, #600606); + background-image: linear-gradient(to bottom, #b41616, #600606); + color: #eeeeee; + padding: 0 16px; + -moz-border-radius: 0 0 2px 2px; + -webkit-border-radius: 0 0 2px 2px; + border-radius: 0 0 2px 2px; } -.openerp .oe-view-manager-header .filter li { - height: 24px; - line-height: 24px; - padding: 0 8px; +.openerp .oe_about .oe_bottom a { + color: #eeeeee; } -.openerp .oe-view-manager-header .header-row { - height: 26px; - line-height: 26px; - margin: 8px; - clear: both; - text-shadow: 0 1px 1px white; +.openerp .oe_application { + width: 100%; } -.openerp .oe-view-manager-header .header-row .dropdown-toggle:after { - margin-top: 7px; - margin-left: 6px; - border-top-color: #404040; +.openerp .oe_application a { + color: #7c7bad; } -.openerp .oe-view-manager-header .oe_vm_switch li { - padding-left: 1px; - text-align: center; - width: 24px; - height: 24px; - line-height: 24px; +.openerp .oe_application a:hover { + text-decoration: underline; } -.openerp .oe-view-manager-header .pagination li { - height: 24px; - line-height: 24px; - padding: 0 8px; +.openerp .oe_application > div { + height: 100%; } -.openerp .oe-view-manager-header .button-group { +.openerp .oe_application .oe_breadcrumb_item:not(:last-child) { display: inline-block; - border: 1px solid #ababab; - -moz-border-radius: 5px; - -webkit-border-radius: 5px; - border-radius: 5px; -} -.openerp .oe-view-manager-header .button-group li { - float: left; - border-right: 1px solid #ababab; -} -.openerp .oe-view-manager-header .button-group li:last-child { - border: none; -} -.openerp .oe-view-manager-header .button-group a { - color: #4c4c4c; + max-width: 7em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } -.openerp .oe-view-manager-header .button-group a:hover { - text-decoration: none; +.openerp .oe_view_manager .oe_view_manager_body { + height: inherit; } -.openerp .oe-view-manager-header .button-group .active { - width: 100%; - background: #999999; - -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3) inset; - -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3) inset; - -box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3) inset; - color: white; - text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4); +.openerp .oe_view_manager .oe_view_manager_view_kanban { + height: inherit; } -.openerp .oe_view_manager_header { +.openerp .oe_view_manager table.oe_view_manager_header { width: 100%; - border-top: 1px solid #cacaca; - border-bottom: 1px solid #cacaca; - background-color: #fcfcfc; - background-image: -webkit-gradient(linear, left top, left bottom, from(#fcfcfc), to(#dedede)); - background-image: -webkit-linear-gradient(top, #fcfcfc, #dedede); - background-image: -moz-linear-gradient(top, #fcfcfc, #dedede); - background-image: -ms-linear-gradient(top, #fcfcfc, #dedede); - background-image: -o-linear-gradient(top, #fcfcfc, #dedede); - background-image: linear-gradient(to bottom, #fcfcfc, #dedede); - -moz-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.4), 0 0 9px rgba(0, 0, 0, 0.1); - -webkit-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.4), 0 0 9px rgba(0, 0, 0, 0.1); - -box-shadow: 0 1px 0 rgba(255, 255, 255, 0.4), 0 0 9px rgba(0, 0, 0, 0.1); + table-layout: fixed; } -.openerp .oe_view_manager_header .oe_header_row { +.openerp .oe_view_manager table.oe_view_manager_header .oe_header_row { clear: both; text-shadow: 0 1px 1px white; } -.openerp .oe_view_manager_header .oe_header_row td { - padding: 8px; -} -.openerp .oe_view_manager_header .oe_header_row:last-child td { +.openerp .oe_view_manager table.oe_view_manager_header .oe_header_row:last-child td { padding-top: 0; } -.openerp .oe_view_manager_header .oe_header_row:first-child td { +.openerp .oe_view_manager table.oe_view_manager_header .oe_header_row:first-child td { padding-top: 8px; } -.openerp .oe_view_manager_header .oe_view_manager_sidebar { +.openerp .oe_view_manager table.oe_view_manager_header .oe_view_manager_sidebar { margin: 0px auto; - width: 400px; text-align: center; } -.openerp .oe_view_manager_header td { +.openerp .oe_view_manager table.oe_view_manager_header td { line-height: 26px; } -.openerp .oe_view_manager_header h2 { +.openerp .oe_view_manager table.oe_view_manager_header h2 { font-size: 18px; margin: 0; float: left; } -.openerp .oe_view_manager_header h2 a { - color: #8a89ba; +.openerp .oe_view_manager table.oe_view_manager_header h2 a { + color: #7c7bad; +} +.openerp .oe_view_manager table.oe_view_manager_header .oe_dropdown_menu { + line-height: normal; } -.openerp .oe_view_manager_header .oe_button_group { +.openerp .oe_view_manager table.oe_view_manager_header .oe_button_group { display: inline-block; border: 1px solid #ababab; -moz-border-radius: 5px; -webkit-border-radius: 5px; border-radius: 5px; } -.openerp .oe_view_manager_header .oe_button_group li { +.openerp .oe_view_manager table.oe_view_manager_header .oe_button_group li { float: left; border-right: 1px solid #ababab; } -.openerp .oe_view_manager_header .oe_button_group li:last-child { +.openerp .oe_view_manager table.oe_view_manager_header .oe_button_group li:last-child { border: none; } -.openerp .oe_view_manager_header .oe_button_group a { +.openerp .oe_view_manager table.oe_view_manager_header .oe_button_group a { color: #4c4c4c; } -.openerp .oe_view_manager_header .oe_button_group a:hover { +.openerp .oe_view_manager table.oe_view_manager_header .oe_button_group a:hover { text-decoration: none; } -.openerp .oe_view_manager_header .oe_button_group .active { +.openerp .oe_view_manager table.oe_view_manager_header .oe_button_group .active { background: #999999; -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3) inset; -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3) inset; - -box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3) inset; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3) inset; } -.openerp .oe_view_manager_header .oe_button_group .active a { +.openerp .oe_view_manager table.oe_view_manager_header .oe_button_group .active a { color: white; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4); } -.openerp .oe_view_manager_header .oe_view_manager_buttons { +.openerp .oe_view_manager table.oe_view_manager_header .oe_view_manager_buttons { white-space: nowrap; } -.openerp .oe_view_manager_pager { +.openerp .oe_view_manager .oe_view_manager_pager { line-height: 26px; } -.openerp .oe_pager_value { +.openerp .oe_view_manager .oe_view_manager_pager .oe_list_pager_single_page .oe_pager_group { + display: none; +} +.openerp .oe_view_manager .oe_pager_value { float: left; margin-right: 8px; } -.openerp .oe_pager_group { +.openerp .oe_view_manager ul.oe_pager_group { + padding: 0; + margin: 0; +} +.openerp .oe_view_manager .oe_pager_group { float: left; height: 24px; line-height: 24px; @@ -1023,47 +1419,59 @@ -webkit-border-radius: 5px; border-radius: 5px; } -.openerp .oe_pager_group li { +.openerp .oe_view_manager .oe_pager_group li { height: 24px; line-height: 24px; padding: 0; + margin: 0; + list-style-type: none; float: left; border-right: 1px solid #ababab; } -.openerp .oe_pager_group li:last-child { +.openerp .oe_view_manager .oe_pager_group li:last-child { border: none; } -.openerp .oe_pager_group a { +.openerp .oe_view_manager .oe_pager_group a { color: #4c4c4c; padding: 0 8px; } -.openerp .oe_pager_group a:hover { +.openerp .oe_view_manager .oe_pager_group a:hover { text-decoration: none; } -.openerp .oe_pager_group .active { +.openerp .oe_view_manager .oe_pager_group .active { background: #999999; -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3) inset; -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3) inset; - -box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3) inset; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3) inset; } -.openerp .oe_pager_group .active a { +.openerp .oe_view_manager .oe_pager_group .active a { color: white; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4); } -.openerp .oe_view_manager_switch li { - text-align: center; +.openerp .oe_view_manager .oe_view_manager_switch { + padding: 0; + margin: 0 0 0 8px; +} +.openerp .oe_view_manager .oe_view_manager_switch li { + margin: 0; width: 24px; height: 24px; line-height: 16px; + padding: 0; + text-align: center; + list-style-type: none; } -.openerp .oe_view_manager_switch li a { +.openerp .oe_view_manager .oe_view_manager_switch li a { position: relative; } -.openerp .oe_view_manager_switch .oe_vm_switch_list:after, .openerp .oe_view_manager_switch .oe_vm_switch_tree:after { +.openerp .oe_view_manager .oe_view_manager_switch .oe_vm_switch_list:after, .openerp .oe_view_manager .oe_view_manager_switch .oe_vm_switch_tree:after { padding: 2px; content: "i"; } -.openerp .oe_view_manager_switch .oe_vm_switch_graph:after { +.openerp .oe_view_manager .oe_view_manager_switch .oe_vm_switch_form:after { + content: "m"; +} +.openerp .oe_view_manager .oe_view_manager_switch .oe_vm_switch_graph:after { font-family: "mnmliconsRegular" !important; font-size: 21px; font-weight: 300 !important; @@ -1071,7 +1479,7 @@ top: -2px; position: relative; } -.openerp .oe_view_manager_switch .oe_vm_switch_gantt:after { +.openerp .oe_view_manager .oe_view_manager_switch .oe_vm_switch_gantt:after { font-family: "mnmliconsRegular" !important; font-size: 21px; font-weight: 300 !important; @@ -1079,122 +1487,88 @@ top: -2px; position: relative; } -.openerp .oe_view_manager_switch .oe_vm_switch_calendar:after { +.openerp .oe_view_manager .oe_view_manager_switch .oe_vm_switch_calendar:after { content: "P"; } -.openerp .oe_view_manager_switch .oe_vm_switch_kanban:after { +.openerp .oe_view_manager .oe_view_manager_switch .oe_vm_switch_kanban:after { content: "k"; } -.openerp .oe_view_manager_switch .oe_vm_switch_diagram:after { +.openerp .oe_view_manager .oe_view_manager_switch .oe_vm_switch_diagram:after { content: "f"; } -.openerp .oe_form_dropdown_section { - position: relative; - display: inline-block; +.openerp .oe_view_manager_current { + height: 100%; } -.openerp .oe_dropdown_toggle:after { - width: 0; - height: 0; - display: inline-block; - content: "&darr"; - text-indent: -99999px; - vertical-align: top; - border-left: 4px solid transparent; - border-right: 4px solid transparent; - border-top: 4px solid white; - filter: alpha(opacity=50); - opacity: 0.5; - margin-top: 7px; - margin-left: 6px; - border-top-color: #404040; +.openerp .oe_view_manager_current > .oe_view_manager_header { + border-bottom: 1px solid #cacaca; + background-color: #fcfcfc; + background-image: -webkit-gradient(linear, left top, left bottom, from(#fcfcfc), to(#dedede)); + background-image: -webkit-linear-gradient(top, #fcfcfc, #dedede); + background-image: -moz-linear-gradient(top, #fcfcfc, #dedede); + background-image: -ms-linear-gradient(top, #fcfcfc, #dedede); + background-image: -o-linear-gradient(top, #fcfcfc, #dedede); + background-image: linear-gradient(to bottom, #fcfcfc, #dedede); + -moz-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.4), 0 0 9px rgba(0, 0, 0, 0.1); + -webkit-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.4), 0 0 9px rgba(0, 0, 0, 0.1); + box-shadow: 0 1px 0 rgba(255, 255, 255, 0.4), 0 0 9px rgba(0, 0, 0, 0.1); } -.openerp .oe_dropdown_menu { - display: none; - position: absolute; - top: 28px; - left: 0px; +.openerp .oe_view_manager_current > .oe_view_manager_header .oe_header_row td { padding: 8px; - border: 1px solid #afafb6; - width: 120px; - overflow-x: hidden; - z-index: 900; - text-align: left; - background: white; - -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_dropdown_menu li { - list-style-type: none; - float: none; - display: block; - background-color: none; +.openerp .oe_view_manager_current > .oe_view_manager_header .oe_header_row:first-child td { + padding-top: 8px; } -.openerp .oe_dropdown_menu li a { - display: block; - padding: 3px 6px; - clear: both; - font-weight: normal; - line-height: 14px; - color: #4c4c4c; - text-decoration: none; - cursor: pointer; +.openerp .oe_view_manager_inline, .openerp .oe_view_manager_inlineview { + height: 100%; } -.openerp .oe_dropdown_menu li a:hover { - text-decoration: none; - background-color: #f0f0fa; - 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_view_manager_inline > .oe_view_manager_header, .openerp .oe_view_manager_inlineview > .oe_view_manager_header { + display: none; } -.openerp .oe_sidebar { - white-space: nowrap; +.openerp .oe_popup_form > .oe_formview > .oe_form_pager { + display: none !important; } .openerp .oe_searchview { cursor: text; position: relative; float: right; - padding-right: 20px; - width: 410px; + padding: 1px 0; + line-height: 18px; + width: 400px; border: 1px solid #ababab; background: white; - -moz-border-radius: 1em; - -webkit-border-radius: 1em; - border-radius: 1em; + -moz-border-radius: 13px; + -webkit-border-radius: 13px; + border-radius: 13px; -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; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2) inset; +} +.openerp .oe_searchview input, .openerp .oe_searchview textarea { + padding: 3px; + height: 14px; + font-size: 12px; + line-height: 18px; } .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; + box-shadow: 0 1px 2px #a6a6fe inset; } .openerp .oe_searchview .oe_searchview_clear { cursor: pointer; position: absolute; top: 0; - right: 22px; + right: 18px; width: 15px; height: 100%; - background: url(../img/attachments-close.png) center center no-repeat; + background: url(../img/search_reset.gif) center center no-repeat; } .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; @@ -1203,47 +1577,80 @@ color: #999999; } .openerp .oe_searchview .oe_searchview_unfold_drawer:before { - content: "◀"; + position: absolute; + top: 10px; + right: 7px; + 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:before { - color: #cccccc; - font-family: "mnmliconsRegular"; +.openerp .oe_searchview .oe_searchview_search { + font-size: 1px; + letter-spacing: -1px; + color: transparent; + font-weight: normal; + -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; + -moz-border-radius: 0; + -webkit-border-radius: 0; + border-radius: 0; + position: absolute; + left: 3px; + top: 1px; + padding: 0; + border: none; + background: transparent; +} +.openerp .oe_searchview .oe_searchview_search:before { + font: 21px "mnmliconsRegular"; content: "r"; - font-size: 150%; - padding: 0 1px 0 3px; - display: inline; + color: #a3a3a3; +} +.openerp .oe_searchview .oe_searchview_facets { + min-height: 22px; + margin-left: 15px; } .openerp .oe_searchview .oe_searchview_facets * { vertical-align: top; display: inline-block; - line-height: 26px; + line-height: 17px; } -.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_facet { + height: 18px; + margin: 1px 0; + font-size: 11px; } -.openerp .oe_searchview .oe_searchview_facets .oe_searchview_input:focus, .openerp .oe_searchview .oe_searchview_facets .oe_searchview_facet: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; + 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; } .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: #8786b7; + padding: 0; -webkit-font-smoothing: auto; - padding-left: 1.1em; - margin: 1px 0; } .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; + box-shadow: 0 0 3px 1px #a6a6fe; } .openerp .oe_searchview .oe_searchview_facets .oe_searchview_facet .oe_facet_values { background: #f0f0fa; @@ -1252,128 +1659,167 @@ border-radius: 0 3px 3px 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; + height: 18px; + padding: 0 4px; } .openerp .oe_searchview .oe_searchview_facets .oe_searchview_facet .oe_facet_category { color: white; text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4); } +.openerp .oe_searchview .oe_searchview_facets .oe_searchview_facet .oe_facet_category.oe_i { + font-size: 16px; +} .openerp .oe_searchview .oe_searchview_facets .oe_searchview_facet .oe_facet_value { border-left: 1px solid #afafb6; + text-shadow: 0 1px 1px white; + 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_remove { position: absolute; - top: 0; - left: 2px; - color: white; + top: 3px; + right: 3px; + color: #8786b7; + line-height: 8px; + width: 12px; + height: 12px; + padding-top: 1px; + text-align: center; + font-weight: bold; + cursor: pointer; + text-shadow: 0 1px 1px white; } .openerp .oe_searchview .oe_searchview_facets .oe_searchview_facet .oe_facet_remove:hover { - color: #cccccc; + 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.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; + z-index: 100; + margin-top: 4px; top: 100%; - right: 0; + right: -1px; background-color: white; min-width: 100%; display: none; - border: 1px solid #cccccc; + border: 1px solid #afafb6; text-align: left; - padding-bottom: 0.5em; - -moz-border-radius: 1em; - -webkit-border-radius: 1em; - border-radius: 1em; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + border-radius: 4px; + -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_searchview_drawer > div { border-top: 1px solid #cccccc; - margin: 3px 0; + margin: 0; + padding: 8px; } .openerp .oe_searchview .oe_searchview_drawer > div:first-child { border-top: none; margin: 0; } +.openerp .oe_searchview .oe_searchview_drawer h3 { + margin: 8px 4px 4px 12px; + color: #7c7bad; + font-size: 13px; +} .openerp .oe_searchview .oe_searchview_drawer h4, .openerp .oe_searchview .oe_searchview_drawer h4 * { margin: 0; cursor: pointer; + font-weight: normal; + display: inline-block; +} +.openerp .oe_searchview .oe_searchview_drawer h4:hover, .openerp .oe_searchview .oe_searchview_drawer h4 *:hover { + background-color: #f0f0fa; } .openerp .oe_searchview .oe_searchview_drawer h4:before { content: "▸ "; + color: #a3a3a3; +} +.openerp .oe_searchview .oe_searchview_drawer button { + margin: 4px 0; } -.openerp .oe_searchview .oe_searchview_drawer button, .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; + box-shadow: none; -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; } -.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_filters { +.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_section { display: table; width: 100%; } -.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_filters > div { - -moz-box-sizing: border-box; +.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_section > div { -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; box-sizing: border-box; display: table-cell; width: 50%; } -.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_filters h3 { - margin: 2px 4px 2px 8px; - color: #8786b7; -} -.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_filters ul { - margin: 0 12px 3px; +.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_section ul { + margin: 0 8px 8px; padding: 0; list-style: none; } -.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_filters li { +.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_section li { list-style: none; - padding: 3px 6px 3px 18px; + padding: 2px 4px 2px 20px; line-height: 14px; color: inherit; cursor: pointer; + position: relative; } -.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_section li.oe_selected:before { + content: "W"; + font-family: "entypoRegular" !important; + font-size: 24px; + font-weight: 300 !important; + color: #a3a3a3; + position: absolute; + left: 4px; + top: -2px; } -.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_filters li:hover { +.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_section li:hover { background-color: #f0f0fa; } -.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_custom form { - display: none; +.openerp .oe_searchview .oe_searchview_drawer form { + margin-left: 12px; } -.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 form p { + margin: 4px 0; + line-height: 18px; } -.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 form button { + margin: 0 0 8px 0; +} +.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_custom { + padding: 0 8px 8px 8px; +} +.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_custom form { + display: none; } .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_custom li.oe_searchview_custom_private { - background: url(/web/static/src/img/icons/terp-locked.png) 5px center no-repeat; + line-height: 14px; + padding: 2px 4px 2px 20px; } .openerp .oe_searchview .oe_searchview_drawer .oe_searchview_custom li:hover { background-color: #f0f0fa; @@ -1383,89 +1829,211 @@ top: 0; right: 5px; } +.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_dashboard form { + display: none; + margin-top: 2px; +} .openerp .oe_searchview .oe_searchview_drawer .oe_searchview_advanced form { display: none; + margin-top: 8px; } .openerp .oe_searchview .oe_searchview_drawer .oe_searchview_advanced button.oe_add_condition:before { - content: "⊞ "; -} -.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_advanced button.oe_apply:before { - content: "⚡ "; + content: "Z"; + font-family: "entypoRegular" !important; + font-size: 24px; + font-weight: 300 !important; + margin-right: 4px; } .openerp .oe_searchview .oe_searchview_drawer .oe_searchview_advanced ul { list-style: none; padding: 0; } .openerp .oe_searchview .oe_searchview_drawer .oe_searchview_advanced li { + position: relative; list-style: none; margin: 0; white-space: nowrap; } +.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_advanced li:first-child .searchview_extended_prop_or { + visibility: hidden; +} +.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_advanced .searchview_extended_prop_or { + opacity: 0.5; + margin-left: -14px; +} .openerp .oe_searchview .oe_searchview_drawer .oe_opened h4:before { content: "▾ "; + position: relative; + top: -1px; } .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; + 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; } -.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_searchview .oe_searchview_drawer .oe_searchview_custom_delete:hover, .openerp .oe_searchview .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_view_nocontent > img { - float: left; - margin: 1.5em; +.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_custom_delete { + display: none; + position: absolute; + bottom: 1px; + right: 4px; } -.openerp .oe_view_nocontent > div { - overflow: hidden; - padding: 35px 0px 0px 0px; - max-width: 700px; +.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_custom_private:hover .oe_searchview_custom_delete, .openerp .oe_searchview .oe_searchview_drawer .oe_searchview_custom_public:hover .oe_searchview_custom_delete { + display: inline-block; +} +.openerp .oe_searchview .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 .oe_view_nocontent { + padding: 15px; + margin-top: 0; + color: #777777; font-size: 125%; + max-width: 700px; } -.openerp .oe_view_topbar { - border-bottom: 1px solid #cacaca; - background-color: #fcfcfc; - background-image: -webkit-gradient(linear, left top, left bottom, from(#fcfcfc), to(#dedede)); - background-image: -webkit-linear-gradient(top, #fcfcfc, #dedede); - background-image: -moz-linear-gradient(top, #fcfcfc, #dedede); - background-image: -ms-linear-gradient(top, #fcfcfc, #dedede); - background-image: -o-linear-gradient(top, #fcfcfc, #dedede); - background-image: linear-gradient(to bottom, #fcfcfc, #dedede); - padding: 0 8px; - line-height: 30px; +.openerp .oe_view_nocontent .oe_view_nocontent_create { + background: transparent url(/web/static/src/img/view_empty_arrow.png) no-repeat 7px 0; + margin-top: 0; + padding-top: 35px; + min-height: 28px; + color: #4c4c4c; +} +.openerp .oe_view_nocontent > p { + padding-left: 95px; +} +.openerp .oe_view_nocontent .oe_empty_custom_dashboard { + background: transparent url(/web/static/src/img/graph_background.png) no-repeat 0 0; + margin-top: -15px; + padding: 100px 0 0 137px; + min-height: 327px; + margin-left: -15px; +} +.openerp .oe_view.oe_cannot_create .oe_view_nocontent_create { + display: none; } .openerp .oe_formview { background: white; } -.openerp .oe_form_header { - padding: 8px; +.openerp .oe_form_dropdown_section { + position: relative; + display: inline-block; } .openerp .oe_form_invalid input, .openerp .oe_form_invalid select, .openerp .oe_form_invalid textarea { background-color: #ff6666 !important; border: 1px solid #dd0000 !important; } -.openerp .oe_form_button_save_dirty { - color: white; - background: #dc5f59; - background: -moz-linear-gradient(#dc5f59, #b33630); - background: -webkit-gradient(linear, left top, left bottom, from(#dc5f59), to(#b33630)); - background: -webkit-linear-gradient(#dc5f59, #b33630); +.openerp .oe_view_manager_current .oe_form_editable .oe_highlight { + color: #404040; + background: none; +} +.openerp .oe_view_manager_current .oe_form_editable button.oe_highlight { + background-color: #efefef; + background-image: -webkit-gradient(linear, left top, left bottom, from(#efefef), to(#d8d8d8)); + background-image: -webkit-linear-gradient(top, #efefef, #d8d8d8); + background-image: -moz-linear-gradient(top, #efefef, #d8d8d8); + background-image: -ms-linear-gradient(top, #efefef, #d8d8d8); + background-image: -o-linear-gradient(top, #efefef, #d8d8d8); + background-image: linear-gradient(to bottom, #efefef, #d8d8d8); + -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 1px rgba(255, 255, 255, 0.8) inset; + -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 1px rgba(255, 255, 255, 0.8) inset; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 1px rgba(255, 255, 255, 0.8) inset; +} +.openerp .oe_view_manager_current .oe_form_editable button.oe_highlight:active { + background-color: #e3e3e3; + background-image: -webkit-gradient(linear, left top, left bottom, from(#e3e3e3), to(#f6f6f6)); + background-image: -webkit-linear-gradient(top, #e3e3e3, #f6f6f6); + background-image: -moz-linear-gradient(top, #e3e3e3, #f6f6f6); + background-image: -ms-linear-gradient(top, #e3e3e3, #f6f6f6); + background-image: -o-linear-gradient(top, #e3e3e3, #f6f6f6); + background-image: linear-gradient(to bottom, #e3e3e3, #f6f6f6); -moz-box-shadow: none; -webkit-box-shadow: none; - -box-shadow: none; - font-weight: bold; + box-shadow: none; } -.openerp .oe_form_button_save_dirty:hover { - background: #ed6f6a; +.openerp .oe_view_manager_current .oe_form_editable button.oe_highlight:hover { + background-color: #f6f6f6; + background-image: -webkit-gradient(linear, left top, left bottom, from(#f6f6f6), to(#e3e3e3)); + background-image: -webkit-linear-gradient(top, #f6f6f6, #e3e3e3); + background-image: -moz-linear-gradient(top, #f6f6f6, #e3e3e3); + background-image: -ms-linear-gradient(top, #f6f6f6, #e3e3e3); + background-image: -o-linear-gradient(top, #f6f6f6, #e3e3e3); + background-image: linear-gradient(to bottom, #f6f6f6, #e3e3e3); + -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 1px rgba(255, 255, 255, 0.8) inset; + -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 1px rgba(255, 255, 255, 0.8) inset; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 1px rgba(255, 255, 255, 0.8) inset; +} +.openerp .oe_form_invisible { + display: none !important; +} +.openerp .oe_form_readonly .oe_edit_only, .openerp .oe_form_readonly .oe_form_field:empty { + display: none !important; +} +.openerp .oe_form_readonly .oe_form .oe_form_field_date { + width: auto; +} +.openerp .oe_form_nosheet { + margin: 16px; } -.openerp .oe_form_topbar { +.openerp .oe_form_nosheet > header { + margin: -16px -16px 0 -16px; + padding: 0; +} +.openerp .oe_form_sheetbg { + padding: 16px 0; +} +.openerp .oe_form_sheet_width { + min-width: 650px; + max-width: 860px; + margin: 0 auto; +} +.openerp .oe_form_sheet { + background: white; + min-height: 330px; + padding: 16px; +} +.openerp .oe_application .oe_form_sheetbg { + background: url(/web/static/src/img/form_sheetbg.png); + border-bottom: 1px solid #dddddd; +} +.openerp .oe_application .oe_form_sheet { + border: 1px solid #c8c8d3; + -moz-box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); + -webkit-box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); +} +.openerp .oe_application .oe_form_sheet .ui-tabs { + margin: 0 -16px; +} +.openerp .oe_application .oe_form_sheet .oe_notebook_page { + padding: 0 16px; +} +.openerp .oe_form header { + position: relative; border-bottom: 1px solid #cacaca; + padding-left: 2px; background-color: #fcfcfc; background-image: -webkit-gradient(linear, left top, left bottom, from(#fcfcfc), to(#dedede)); background-image: -webkit-linear-gradient(top, #fcfcfc, #dedede); @@ -1473,125 +2041,94 @@ background-image: -ms-linear-gradient(top, #fcfcfc, #dedede); background-image: -o-linear-gradient(top, #fcfcfc, #dedede); background-image: linear-gradient(to bottom, #fcfcfc, #dedede); - padding: 0 8px; - line-height: 30px; } -.openerp .oe_form_topbar button { - font-size: 12px; - height: 24px !important; - line-height: 24px; - vertical-align: top; - padding: 0 10px; - margin: 3px 4px 3px 0; +.openerp .oe_form header > span { + margin-left: 4px; } -.openerp .oe_form_topbar button span.i { - line-height: 20px; - height: 24px; +.openerp .oe_form header ul { + display: inline-block; + float: right; } -.openerp .oe_form_topbar ul { - height: 30px; - padding: 0; - margin: 0; - text-shadow: 0 1px 1px white; - border-left: 1px solid #cacaca; - border-right: 1px solid #cacaca; +.openerp .oe_form header .oe_button { + margin: 3px 2px 1px; } -.openerp .oe_form_topbar ul li { - padding: 0; - margin: 0; - float: left; - vertical-align: top; - border-right: 1px solid #cacaca; - height: 30px; - padding: 0 12px; +.openerp .oe_form header .oe_button:first-child { + margin-left: 6px; } -.openerp .oe_form_topbar ul li:first-child { - border-left: 1px solid #cacaca; +.openerp .oe_form header .oe_tags { + margin: 5px 0 0 5px; + width: 400px; + padding-bottom: 0; } -.openerp .oe_form_topbar ul li a { - color: #4c4c4c; +.openerp .oe_form div.oe_chatter { + box-sizing: border-box; + min-width: 682px; + max-width: 892px; + margin: 0 auto; + padding: 16px 16px 48px; } -.openerp .oe_form_topbar ul li a:hover { - color: black; +.openerp .oe_form div.oe_form_configuration p, .openerp .oe_form div.oe_form_configuration ul, .openerp .oe_form div.oe_form_configuration ol { + color: #aaaaaa; + max-width: 650px; } -.openerp .oe_form_topbar .oe_form_steps img { - margin: 0 8px; - vertical-align: top; +.openerp .oe_form div.oe_form_configuration label { + min-width: 150px; } -.openerp .oe_form_topbar .oe_form_steps li { - border-right: none; - padding: 0; +.openerp .oe_form div.oe_form_configuration .oe_form_group_cell_label { + padding: 1px 0; } -.openerp .oe_form_topbar .oe_form_steps li:first-child { - margin-left: 12px; - border-left: none; +.openerp .oe_form div.oe_form_configuration .oe_form_group_cell div div { + padding: 1px 0; +} +.openerp .oe_form .oe_subtotal_footer { + width: 1% !important; } -.openerp .oe_form_topbar .oe_form_steps li:last-child { - margin-right: 12px; +.openerp .oe_form .oe_subtotal_footer td.oe_form_group_cell { + text-align: right; + padding: 0 !important; +} +.openerp .oe_form .oe_subtotal_footer td.oe_form_group_cell_label { + border-right: none; } -.openerp .oe_form_topbar .oe_form_steps_active { +.openerp .oe_form .oe_subtotal_footer .oe_subtotal_footer_separator { + width: 108px; + border-top: 1px solid #cacaca; + margin-top: 4px; + padding-top: 4px; font-weight: bold; - color: #b33630; + font-size: 18px; } -.openerp .oe_form_topbar.oe_form_topbar_hifirst button:first-child, .openerp button.oe_form_button_hi { - color: white; - background: #dc5f59; - background: -moz-linear-gradient(#dc5f59, #b33630); - background: -webkit-gradient(linear, left top, left bottom, from(#dc5f59), to(#b33630)); - background: -webkit-linear-gradient(#dc5f59, #b33630); - -moz-box-shadow: none; - -webkit-box-shadow: none; - -box-shadow: none; +.openerp .oe_form .oe_subtotal_footer label:after { + content: ":"; } -.openerp .oe_form_topbar.oe_form_topbar_hifirst button:first-child:hover, .openerp button.oe_form_button_hi:hover { - background: #df6b66; - background: -moz-linear-gradient(#df6b66, #bf3a33); - background: -webkit-gradient(linear, left top, left bottom, from(#df6b66), to(#bf3a33)); - background: -webkit-linear-gradient(#df6b66, #bf3a33); - -moz-box-shadow: 0 0 1px rgba(0, 0, 0, 0.2); - -webkit-box-shadow: 0 0 1px rgba(0, 0, 0, 0.2); - -box-shadow: 0 0 1px rgba(0, 0, 0, 0.2); +.openerp .oe_form .oe_subtotal_footer label.oe_subtotal_footer_separator { + font-weight: bold !important; + padding: 2px 11px 2px 0px !important; } -.openerp .oe_application .oe_form_sheetbg { - background: url(/web/static/src/img/form_sheetbg.png); - padding: 8px 0; - border-bottom: 1px solid #dddddd; +.openerp .oe_form .oe_subtotal_footer label.oe_form_label_help { + font-weight: normal; } -.openerp .oe_application .oe_form_sheet_width, .openerp .oe_application .oe_form_bottom { - min-width: 650px; - max-width: 980px; - margin: 0 auto; +.openerp .oe_form .oe_form_button { + margin: 2px; } -.openerp .oe_application .oe_form_sheet { - padding: 8px 8px 8px 8px; - background: white; - min-height: 420px; - border: 1px solid #afafb6; - -moz-box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); - -webkit-box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); - -box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); +.openerp .oe_form td.oe_form_group_cell_label { + border-right: 1px solid #dddddd; + padding: 2px 0px; } -.openerp .oe_form .oe_form_group_cell { +.openerp .oe_form td.oe_form_group_cell_label label { line-height: 18px; - padding: 2px; - height: 20px; + display: block; + min-width: 140px; +} +.openerp .oe_form td.oe_form_group_cell + .oe_form_group_cell { + padding: 2px 0 2px 8px; } -.openerp .oe_form .oe_form_group_cell > .oe_form_field_many2one, -.openerp .oe_form .oe_form_group_cell > .oe_form_field_many2one input, -.openerp .oe_form .oe_form_group_cell > .oe_form_field_char input, -.openerp .oe_form .oe_form_group_cell > .oe_form_field_text textarea, -.openerp .oe_form .oe_form_group_cell > .oe_form_field_email, -.openerp .oe_form .oe_form_group_cell > .oe_form_field_email input, -.openerp .oe_form .oe_form_group_cell > .oe_form_field_url, -.openerp .oe_form .oe_form_group_cell > .oe_form_field_url input, -.openerp .oe_form .oe_form_group_cell > .oe_form_field_float input, -.openerp .oe_form .oe_form_group_cell > .oe_form_field_selection select, -.openerp .oe_form .oe_form_group_cell > div > .oe_datepicker_root, -.openerp .oe_form .oe_form_group_cell > div > .oe_datepicker_root > input.oe_datepicker_master { +.openerp .oe_form .oe_form_group { width: 100%; + margin: 9px 0 9px 0; } -.openerp .oe_form .oe_form_group_cell.oe_form_group_nested { - padding: 0; +.openerp .oe_form .oe_form_group .oe_form_group_cell.oe_group_right { + padding-left: 20px; } .openerp .oe_form .oe_form_label_help[for], .openerp .oe_form .oe_form_label[for] { font-weight: bold; @@ -1606,106 +2143,116 @@ top: -4px; padding: 0 2px; } -.openerp .oe_form textarea { - resize: vertical; +.openerp .oe_horizontal_border { + border-bottom: 1px solid black; } -.openerp .oe_form input[type="text"], -.openerp .oe_form input[type="password"], -.openerp .oe_form input[type="file"], -.openerp .oe_form select { - height: 22px; - padding-top: 2px; +.openerp .oe_horizontal_separator { + font-weight: bold; + font-size: 20px; + margin: 15px 0px 10px 0px; + color: #7c7bad; } -.openerp .oe_form input[type="text"], -.openerp .oe_form input[type="password"], -.openerp .oe_form input[type="file"], -.openerp .oe_form select, -.openerp .oe_form textarea { - -moz-box-sizing: border-box; +.openerp .oe_horizontal_separator:empty { + height: 5px; +} +.openerp .oe_vertical_separator { + border-left: 1px solid #666666; + padding: 0 4px 0 4px; +} +.openerp .oe_form_field_progressbar { + display: inline-block; + min-width: 70px; +} +.openerp .oe_form_field_progressbar.ui-progressbar { + height: 22px; + font-size: 10px; -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box; - padding: 0 2px; - margin: 0 2px; border: 1px solid #999999; -moz-border-radius: 3px; -webkit-border-radius: 3px; border-radius: 3px; background: white; - min-width: 90px; + min-width: 50px; +} +.openerp .oe_form_field_progressbar.ui-progressbar span { + position: absolute; + margin-left: 10px; + font-weight: bold; +} +.openerp .oe_form_field_progressbar.ui-progressbar .ui-widget-header { + background: #cccccc url(/web/static/lib/jquery.ui/css/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png) 50% 50% repeat-x; +} +.openerp .oe_form .oe_form_field_text { + width: 100%; +} +.openerp .oe_form .oe_form_field_char input, +.openerp .oe_form .oe_form_field_url input, +.openerp .oe_form .oe_form_field_email input, +.openerp .oe_form .oe_form_field_text textarea, +.openerp .oe_form .oe_form_field_selection select { + width: 100%; +} +.openerp .oe_form .oe_form_field_text.oe_inline, .openerp .oe_form .oe_form_field_text.oe_inline > textarea { + width: 500px; +} +.openerp .oe_form h1, .openerp .oe_form h2, .openerp .oe_form h3, .openerp .oe_form h4, .openerp .oe_form h5, .openerp .oe_form h6 { + margin: 0 0 4px 0; +} +.openerp .oe_form h1 input, .openerp .oe_form h2 input, .openerp .oe_form h3 input, .openerp .oe_form h4 input, .openerp .oe_form h5 input, .openerp .oe_form h6 input { + height: inherit !important; + font-size: inherit; +} +.openerp .oe_form .oe_form_field { + width: 100%; + display: inline-block; + padding: 2px 2px 2px 0px; +} +.openerp .oe_form .oe_form_field input { + margin: 0px; +} +.openerp .oe_form input[type="text"], .openerp .oe_form input[type="password"], .openerp .oe_form input[type="file"], .openerp .oe_form select { + height: 22px; + padding-top: 2px; +} +.openerp .oe_form input[type="text"], .openerp .oe_form input[type="password"], .openerp .oe_form input[type="file"], .openerp .oe_form select, .openerp .oe_form textarea { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; + box-sizing: border-box; + background: white; + min-width: 60px; color: #1f1f1f; } -.openerp .oe_form input[readonly], -.openerp .oe_form select[readonly], -.openerp .oe_form textarea[readonly], -.openerp .oe_form input[disabled], -.openerp .oe_form select[disabled] { +.openerp .oe_form input[readonly], .openerp .oe_form select[readonly], .openerp .oe_form textarea[readonly], .openerp .oe_form input[disabled], .openerp .oe_form select[disabled] { background: #e5e5e5 !important; color: #666666; } .openerp .oe_form textarea[disabled] { border: none; - border-left: 8px solid #eeeeee; padding-left: 8px; -moz-box-shadow: none; -webkit-box-shadow: none; - -box-shadow: none; + box-shadow: none; -moz-border-radius: 0px; -webkit-border-radius: 0px; border-radius: 0px; } -.openerp .oe_form .oe_form_field_many2one input, -.openerp .oe_form .oe_form_field_binary input, -.openerp .oe_form .oe_form_field_binary input, -.openerp .oe_form .oe_form_field_email input, -.openerp .oe_form .oe_form_field_url input { - border-right: none; - -webkit-border-top-right-radius: 0px; - -webkit-border-bottom-right-radius: 0px; - -moz-border-radius-topright: 0px; - -moz-border-radius-bottomright: 0px; - border-top-right-radius: 0px; - border-bottom-right-radius: 0px; -} -.openerp .oe_form .oe_form_field_email button img, +.openerp .oe_form textarea.oe_inline[disabled] { + border-left: 8px solid #eeeeee; +} .openerp .oe_form .oe_form_field_url button img { vertical-align: top; } -.openerp .oe_form .oe_form_field_translatable, -.openerp .oe_form .oe_form_field_many2one, .openerp .oe_form .oe_form_field_date, .openerp .oe_form .oe_form_field_datetime { white-space: nowrap; } .openerp .oe_form .oe_form_field_boolean { padding-top: 4px; -} -.openerp .oe_form .oe_form_field_datetime input { - min-width: 11em; -} -.openerp .oe_form .oe_form_field_many2manytags .text-wrap { - width: 100% !important; -} -.openerp .oe_form .oe_form_field_many2manytags .text-wrap textarea { - width: 100% !important; -} -.openerp .oe_form .oe_form_field_many2manytags .oe_form_field_many2manytags_box { - border-radius: 2px; - box-sizing: border-box; - position: relative; - float: left; - border: 1px solid #9daccc; - background: #e2e6f0; - color: black; - padding: 0px 3px 0px 3px; - margin: 0 2px 2px 0; - cursor: pointer; - height: 16px; - font: 11px "lucida grande", tahoma, verdana, arial, sans-serif; -} -.openerp .oe_form .oe_form_field_many2manytags .text-core .text-wrap .text-dropdown .text-list .text-suggestion em { - font-style: italic; - text-decoration: none; + width: auto; } .openerp .oe_form .oe_datepicker_container { display: none; @@ -1713,24 +2260,13 @@ .openerp .oe_form .oe_datepicker_root { display: inline-block; } -.openerp .oe_form .oe_form_required input, .openerp .oe_form .oe_form_required select, .openerp .oe_form .oe_form_required textarea { +.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; } .openerp .oe_form .oe_form_invalid input, .openerp .oe_form .oe_form_invalid select, .openerp .oe_form .oe_form_invalid textarea { background-color: #ff6666 !important; border: 1px solid #dd0000 !important; } -.openerp .oe_form .oe_button.oe_field_button { - display: block; - -webkit-border-top-left-radius: 0px; - -webkit-border-bottom-left-radius: 0px; - -moz-border-radius-topleft: 0px; - -moz-border-radius-bottomleft: 0px; - border-top-left-radius: 0px; - border-bottom-left-radius: 0px; - margin-right: -1px; - height: 22px; -} .openerp .oe_form .oe_input_icon { cursor: pointer; margin: 3px 0 0 -21px; @@ -1744,222 +2280,423 @@ right: 5px; top: 3px; } -.openerp .oe_form .oe_form_button.oe_button { +.openerp .oe_form .oe_form_field_with_button.oe_no_button > .oe_button { + display: none; +} +.openerp .oe_form .oe_form_field_with_button:not(.oe_no_button) > .oe_button { + float: right; + -moz-border-radius: 0; + -webkit-border-radius: 0; + border-radius: 0; + border-bottom-left-radius: 0px; height: 22px; - color: #4c4c4c; } -.openerp .oe_form .oe_form_button.oe_button span { +.openerp .oe_form .oe_form_field_with_button input { + width: 100%; +} +.openerp .oe_form .oe_form_field_with_button > div { position: relative; - vertical-align: top; + overflow: hidden; } -.openerp .oe_form .oe_form_button > img { - vertical-align: -3px; - padding: 0 2px; +.openerp .oe_form .oe_form_embedded_html { + position: relative; + width: 600px; + margin-left: 130px; + margin-top: 32px; + margin-bottom: 32px; + text-align: justify; } -.openerp .oe_form .oe_form_group_cell > .oe_form_button.oe_button { - display: block; - white-space: nowrap; - min-width: 100%; - width: 100%; +.openerp .oe_form_editable .oe_form .oe_form_field_integer input { + width: 6em !important; } -.openerp .oe_form .oe-binary-file-set { - overflow: hidden; +.openerp .oe_form_editable .oe_form .oe_form_field_float input { + width: 7em !important; +} +.openerp .oe_form_editable .oe_form .oe_form_field_date input { + width: 7.5em !important; +} +.openerp .oe_form_editable .oe_form .oe_form_field_datetime input { + width: 11.5em !important; +} +.openerp .oe_hidden_input_file { position: relative; - width: 45px; - height: 30px; } -.openerp .oe_form input.oe-binary-file { +.openerp .oe_hidden_input_file input.oe_form_binary_file { z-index: 0; line-height: 0; font-size: 12px; position: absolute; top: 1px; - right: 10px; + left: 0; + right: 0; opacity: 0; filter: alpha(opacity=0); -ms-filter: "alpha(opacity=0)"; margin: 0; padding: 0; } -.openerp .oe_horizontal_border { - border-bottom: 1px solid black; +.openerp .oe_form .oe_form_field_image { + padding: 0; + position: relative; + display: inline-block; + width: auto; + vertical-align: top; } -.openerp .oe_horizontal_separator { - font-weight: bold; - font-size: 16px; - margin: 4px 0; +.openerp .oe_form .oe_form_field_image .oe_form_field_image_controls { + position: absolute; + top: 1px; + padding: 4px; + width: 100%; + display: none; + text-align: center; + color: #eeeeee; + background: rgba(37, 37, 37, 0.9); + -moz-border-radius: 3px 3px 0 0; + -webkit-border-radius: 3px 3px 0 0; + border-radius: 3px 3px 0 0; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; + box-sizing: border-box; } -.openerp .oe_horizontal_separator:empty { - height: 5px; +.openerp .oe_form .oe_form_field_image:hover .oe_form_field_image_controls { + display: block; } -.openerp .oe_vertical_separator { - border-left: 1px solid #666666; - padding: 0 4px 0 4px; +.openerp .oe_form_field_many2one td:first-child { + position: relative; } -.openerp .oe_form_notebook { - margin: 8px 0; - padding: 0 8px; - list-style: none; - zoom: 1; +.openerp .oe_form_field_many2one span.oe_m2o_drop_down_button { + position: absolute; + top: 2px; + right: 0px; } -.openerp .oe_form_notebook:before, .openerp .oe_form_notebook:after { - display: table; - content: ""; - zoom: 1; +.openerp .oe_form_field_many2one .oe_m2o_cm_button { + line-height: 14px; + float: right; + padding-left: 2px; } -.openerp .oe_form_notebook:after { - clear: both; +.openerp.ui-autocomplete li.oe_m2o_dropdown_option a { + font-style: italic; + padding-left: 2em; } -.openerp .oe_form_notebook > li { - float: left; +.openerp.ui-autocomplete li:not(.oe_m2o_dropdown_option) + li.oe_m2o_dropdown_option { + margin-top: 10px; } -.openerp .oe_form_notebook > li > a { - display: block; - color: #4c4c4c; +.openerp ul.oe_form_status, .openerp ul.oe_form_status_clickable { + display: inline-block; + margin: 0; + padding: 0 18px 0 0; } -.openerp .oe_form_notebook { - border-color: #dddddd; - border-style: solid; - border-width: 0 0 1px; +.openerp ul.oe_form_status li, .openerp ul.oe_form_status_clickable li { + display: inline-block; + list-style-type: none; + margin: 0 -18px 0 0; + padding: 0; + background-color: #fcfcfc; + background-image: -webkit-gradient(linear, left top, left bottom, from(#fcfcfc), to(#dedede)); + background-image: -webkit-linear-gradient(top, #fcfcfc, #dedede); + background-image: -moz-linear-gradient(top, #fcfcfc, #dedede); + background-image: -ms-linear-gradient(top, #fcfcfc, #dedede); + background-image: -o-linear-gradient(top, #fcfcfc, #dedede); + background-image: linear-gradient(to bottom, #fcfcfc, #dedede); } -.openerp .oe_form_notebook > li { - position: relative; - margin-bottom: -1px; +.openerp ul.oe_form_status li:first-child .label, .openerp ul.oe_form_status_clickable li:first-child .label { + border-left: 1px solid #cacaca; + padding-left: 14px; } -.openerp .oe_form_notebook > li > a { - padding: 0 12px; - margin-right: 2px; - line-height: 30px; - border: 1px solid transparent; - -moz-border-radius: 4px 4px 0 0; - -webkit-border-radius: 4px 4px 0 0; - border-radius: 4px 4px 0 0; +.openerp ul.oe_form_status li:last-child, .openerp ul.oe_form_status_clickable li:last-child { + border-right: 1px solid #cacaca; } -.openerp .oe_form_notebook > li > a:hover { - text-decoration: none; - background-color: #eeeeee; - border-color: #eeeeee #eeeeee #dddddd; +.openerp ul.oe_form_status li:last-child .label, .openerp ul.oe_form_status_clickable li:last-child .label { + padding-right: 14px; } -.openerp .oe_form_notebook > li.ui-state-active > a, .openerp .oe_form_notebook > li.ui-state-active > a:hover { - background-color: white; - border: 1px solid #dddddd; - border-bottom-color: transparent; - cursor: default; +.openerp ul.oe_form_status li:last-child .arrow, .openerp ul.oe_form_status_clickable li:last-child .arrow { + display: none; } -.openerp .oe_form_notebook_page { - padding: 0; +.openerp ul.oe_form_status li .label, .openerp ul.oe_form_status_clickable li .label { + color: #4c4c4c; + text-shadow: 0 1px 1px #fcfcfc, 0 -1px 1px #dedede; + padding: 7px; + display: inline-block; + padding-left: 24px; + margin: 0; + position: relative; } -.openerp .ui-tabs-hide { - display: none; +.openerp ul.oe_form_status li .arrow, .openerp ul.oe_form_status_clickable li .arrow { + width: 17px; + display: inline-block; + vertical-align: top; + overflow: hidden; + margin-left: -5px; } -.openerp .oe_form .oe_form_field_progressbar.ui-progressbar { - height: 22px; - font-size: 10px; - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - -ms-box-sizing: border-box; - box-sizing: border-box; - border: 1px solid #999999; +.openerp ul.oe_form_status li .arrow span, .openerp ul.oe_form_status_clickable li .arrow span { + position: relative; + width: 24px; + height: 24px; + display: inline-block; + margin-left: -12px; + margin-top: 3px; + box-shadow: -1px 1px 2px rgba(255, 255, 255, 0.2), inset -1px 1px 1px rgba(0, 0, 0, 0.2); + background-color: #dedede; + background: -moz-linear-gradient(135deg, #dedede, #fcfcfc); + background: -o-linear-gradient(135deg, #fcfcfc, #dedede); + background: -webkit-gradient(linear, left top, right bottom, from(#fcfcfc), to(#dedede)); -moz-border-radius: 3px; -webkit-border-radius: 3px; border-radius: 3px; - background: white; - min-width: 90px; + -webkit-transform: rotate(45deg); + -moz-transform: rotate(45deg); + -ms-transform: rotate(45deg); + -o-transform: rotate(45deg); + transform: rotate(45deg); +} +.openerp ul.oe_form_status li.oe_active, .openerp ul.oe_form_status_clickable li.oe_active { + background-color: #729fcf; + background-image: -webkit-gradient(linear, left top, left bottom, from(#729fcf), to(#3465a4)); + background-image: -webkit-linear-gradient(top, #729fcf, #3465a4); + background-image: -moz-linear-gradient(top, #729fcf, #3465a4); + background-image: -ms-linear-gradient(top, #729fcf, #3465a4); + background-image: -o-linear-gradient(top, #729fcf, #3465a4); + background-image: linear-gradient(to bottom, #729fcf, #3465a4); +} +.openerp ul.oe_form_status li.oe_active .arrow span, .openerp ul.oe_form_status_clickable li.oe_active .arrow span { + background-color: #3465a4; + background: -moz-linear-gradient(135deg, #3465a4, #729fcf); + background: -o-linear-gradient(135deg, #729fcf, #3465a4); + background: -webkit-gradient(linear, left top, right bottom, from(#729fcf), to(#3465a4)); +} +.openerp ul.oe_form_status li.oe_active .label, .openerp ul.oe_form_status_clickable li.oe_active .label { + color: white; + text-shadow: 0 1px 1px #729fcf, 0 -1px 1px #3465a4; } -.openerp .oe_form .oe_form_field_progressbar.ui-progressbar span { - position: absolute; - margin-left: 10px; +.openerp ul.oe_form_status_clickable li { + cursor: pointer; +} +.openerp ul.oe_form_status_clickable li:hover { + background-color: #e8e8e8; + background-image: -webkit-gradient(linear, left top, left bottom, from(#e8e8e8), to(#cacaca)); + background-image: -webkit-linear-gradient(top, #e8e8e8, #cacaca); + background-image: -moz-linear-gradient(top, #e8e8e8, #cacaca); + background-image: -ms-linear-gradient(top, #e8e8e8, #cacaca); + background-image: -o-linear-gradient(top, #e8e8e8, #cacaca); + background-image: linear-gradient(to bottom, #e8e8e8, #cacaca); +} +.openerp ul.oe_form_status_clickable li:hover .label { + text-shadow: 0 -1px 1px #fcfcfc, 0 1px 1px #dedede; +} +.openerp ul.oe_form_status_clickable li:hover .arrow span { + background-color: #e8e8e8; + background-image: -webkit-gradient(linear, left top, left bottom, from(#e8e8e8), to(#cacaca)); + background-image: -webkit-linear-gradient(top, #e8e8e8, #cacaca); + background-image: -moz-linear-gradient(top, #e8e8e8, #cacaca); + background-image: -ms-linear-gradient(top, #e8e8e8, #cacaca); + background-image: -o-linear-gradient(top, #e8e8e8, #cacaca); + background-image: linear-gradient(to bottom, #e8e8e8, #cacaca); +} +.openerp ul.oe_form_status_clickable li .label { + color: #7c7bad; +} +.openerp ul.oe_form_status_clickable li.oe_active:hover { + background-color: #4c85c2; + background-image: -webkit-gradient(linear, left top, left bottom, from(#4c85c2), to(#284d7d)); + background-image: -webkit-linear-gradient(top, #4c85c2, #284d7d); + background-image: -moz-linear-gradient(top, #4c85c2, #284d7d); + background-image: -ms-linear-gradient(top, #4c85c2, #284d7d); + background-image: -o-linear-gradient(top, #4c85c2, #284d7d); + background-image: linear-gradient(to bottom, #4c85c2, #284d7d); +} +.openerp ul.oe_form_status_clickable li.oe_active:hover .label { + text-shadow: 0 -1px 1px #729fcf, 0 1px 1px #3465a4; +} +.openerp ul.oe_form_status_clickable li.oe_active:hover .arrow span { + background-color: #284d7d; + background: -moz-linear-gradient(135deg, #284d7d, #4c85c2); + background: -o-linear-gradient(135deg, #4c85c2, #284d7d); + background: -webkit-gradient(linear, left top, right bottom, from(#4c85c2), to(#284d7d)); +} +.openerp .oe_form .oe_form_field_one2many > .oe_view_manager .oe_list_pager_single_page { + display: none; +} +.openerp .oe_form_field_one2many > .oe_view_manager .oe_list_pager_single_page, .openerp .oe_form_field_many2many > .oe_view_manager .oe_list_pager_single_page { + display: none !important; +} +.openerp .oe_form_field_one2many > .oe_view_manager .oe_view_manager_view_list, .openerp .oe_form_field_many2many > .oe_view_manager .oe_view_manager_view_list { + min-height: 132px; +} +.openerp .oe_form_field_one2many .oe_form_field_one2many_list_row_add, .openerp .oe_form_field_many2many .oe_form_field_one2many_list_row_add { font-weight: bold; } -.openerp .oe_form .oe_form_field_progressbar.ui-progressbar .ui-widget-header { - background: #cccccc url(/web/static/lib/jquery.ui/css/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png) 50% 50% repeat-x; +.openerp .oe_form_field_one2many .oe_list_content > thead, .openerp .oe_form_field_many2many .oe_list_content > thead { + border-bottom: 1px; +} +.openerp .oe_form_field_one2many .oe_list_content > tbody tr:nth-child(odd), .openerp .oe_form_field_many2many .oe_list_content > tbody tr:nth-child(odd) { + background: transparent; } -.openerp .oe_form .oe_view_manager_header2 td { +.openerp .oe_form_field_one2many .oe_list .oe_list_edit_row_save, .openerp .oe_form_field_many2many .oe_list .oe_list_edit_row_save { + background: url(/web/static/src/img/iconset-b-remove.png) 50% 50% no-repeat; +} +.openerp .oe_form_field_one2many .oe_list .oe_list_edit_row_save:before, .openerp .oe_form_field_many2many .oe_list .oe_list_edit_row_save:before { + visibility: hidden; +} +.openerp .oe_form_field_one2many > .oe_view_manager .oe_header_row_top, .openerp .oe_form_field_many2many > .oe_view_manager .oe_header_row_top { + display: none; +} +.openerp .oe_form_field_one2many > .oe_view_manager .oe_view_manager_header2 td, .openerp .oe_form_field_many2many > .oe_view_manager .oe_view_manager_header2 td { padding: 0px 8px; line-height: 16px; } -.openerp .oe_form .oe_view_manager_header2 td .oe_i { +.openerp .oe_form_field_one2many > .oe_view_manager .oe_view_manager_header2 td .oe_i, .openerp .oe_form_field_many2many > .oe_view_manager .oe_view_manager_header2 td .oe_i { font-size: 13px; } -.openerp .oe_form .oe_view_manager_header2 td .oe_pager_group { +.openerp .oe_form_field_one2many > .oe_view_manager .oe_view_manager_header2 td .oe_pager_group, .openerp .oe_form_field_many2many > .oe_view_manager .oe_view_manager_header2 td .oe_pager_group { height: auto; line-height: 16px; } -.openerp .oe_form .oe_view_manager_header2 td .oe_pager_group li { +.openerp .oe_form_field_one2many > .oe_view_manager .oe_view_manager_header2 td .oe_pager_group li, .openerp .oe_form_field_many2many > .oe_view_manager .oe_view_manager_header2 td .oe_pager_group li { height: auto; line-height: 16px; } -.openerp .oe-select-create-popup-view-form > .oe_formview > .oe_form_pager { - display: none; +.openerp .oe_form_field_one2many .oe_list_buttons.oe_editing .oe_list_save, .openerp .oe_form_field_many2many .oe_list_buttons.oe_editing .oe_list_save { + visibility: hidden; } -.openerp .oe_form .oe_form_title, .openerp .oe_form .oe_form_title input { - font-size: 20px; - font-weight: bold; - height: 30px; +.openerp .oe_form_editable .oe_list_editable .oe_list_content td.oe_required { + background-color: #d2d2ff; } -.openerp .oe_form .oe_form_group_odd_border > tbody > tr.oe_form_group_row > td.oe_form_group_cell:nth-child(odd), -.openerp .oe_form .oe_form_group_label_border > tbody > tr.oe_form_group_row > td.oe_form_group_cell_label { - border-right: 1px solid #dddddd; +.openerp .oe_form_editable .oe_list_editable .oe_list_content td.oe_readonly { + background-color: #eeeeee; } -.openerp .oe_form .oe_form_group_odd_border > tbody > tr.oe_form_group_row > td.oe_form_group_cell:nth-child(odd) label, -.openerp .oe_form .oe_form_group_label_border > tbody > tr.oe_form_group_row > td.oe_form_group_cell_label label { - display: block; - min-width: 120px; +.openerp .oe_list_editable .oe_list_content td.oe_list_field_cell { + padding: 4px 6px 3px 6px; } -.openerp .oe_form .oe_form_group_odd_border > tbody > tr.oe_form_group_row > td.oe_form_group_cell, -.openerp .oe_form .oe_form_group_label_border > tbody > tr.oe_form_group_row > td.oe_form_group_cell { - padding-left: 8px; +.openerp .oe_list.oe_list_editable.oe_editing .oe_edition .oe_list_field_cell:not(.oe_readonly) { + color: transparent; + text-shadow: none; } -.openerp .oe_form .oe_form_subtotal_footer { - float: right; +.openerp .oe_list.oe_list_editable.oe_editing .oe_edition .oe_list_field_cell:not(.oe_readonly) * { + visibility: hidden; +} +.openerp .oe_list.oe_list_editable.oe_editing .oe_m2o_drop_down_button { + top: 5px; +} +.openerp .oe_list.oe_list_editable.oe_editing .oe_m2o_cm_button { + display: none; +} +.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field input, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field textarea { + height: 27px; } -.openerp .oe_form .oe_form_subtotal_footer > div { +.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field input, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field textarea { + -moz-border-radius: 0; + -webkit-border-radius: 0; + border-radius: 0; + border: 1px solid #aaaaff; + margin: 0; +} +.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_float input, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_view_integer input { text-align: right; - padding: 3px; + width: 100% !important; +} +.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_datetime > span, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_date > span { + width: 100% !important; +} +.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_datetime input.oe_datepicker_master, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_date input.oe_datepicker_master { + width: 100% !important; } -.openerp .oe_form .oe_form_subtotal_footer label { - font-weight: normal !important; +.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field .oe_form_field_float, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field .oe_form_view_integer, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_datetime, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_date { + min-width: 0 !important; + max-width: none !important; +} +.openerp .oe_list_group_name { + white-space: nowrap; +} +.openerp .oe_form .oe_form_field_many2many > .oe_list .oe_list_pager_single_page { + display: none; +} +.openerp .oe_list_buttons .oe_alternative { + visibility: hidden; } -.openerp .oe_form .oe_form_subtotal_footer span { +.openerp .oe_list_buttons .oe_list_save, .openerp .oe_list_buttons .oe_list_discard { + display: none; +} +.openerp .oe_list_buttons.oe_editing .oe_list_add { + display: none; +} +.openerp .oe_list_buttons.oe_editing .oe_list_save { display: inline-block; - min-width: 80px; - text-align: right; } -.openerp .oe_form .oe_form_subtotal_footer .oe_form_subtotal_footer_separator { - border-top: 1px solid #cacaca; - font-size: 120%; - font-weight: bold; +.openerp .oe_list_buttons.oe_editing .oe_list_discard { + display: inline; } -.openerp .oe_form .oe_form_subtotal_footer .oe_form_subtotal_footer_separator label { - font-weight: bold !important; +.openerp .oe_list_buttons.oe_editing .oe_alternative { + visibility: visible; +} +.openerp .oe_list.oe_cannot_edit .oe_list_header_handle, .openerp .oe_list.oe_cannot_edit .oe_list_field_handle { + display: none !important; + padding: 0 !important; +} +.openerp .oe_list.oe_cannot_delete .oe_list_record_delete { + display: none !important; } -.openerp .oe-listview-content { +.openerp .oe_list .oe_form .oe_form_nosheet { + margin: 0; + padding: 0; + border: none; +} +.openerp .oe_list .oe_form .oe_form_field { + width: auto; + position: absolute; + margin: 0 !important; + padding: 0; +} +.openerp .oe_list .oe_list_content .oe_group_header { + background-color: #fcfcfc; + background-image: -webkit-gradient(linear, left top, left bottom, from(#fcfcfc), to(#dedede)); + background-image: -webkit-linear-gradient(top, #fcfcfc, #dedede); + background-image: -moz-linear-gradient(top, #fcfcfc, #dedede); + background-image: -ms-linear-gradient(top, #fcfcfc, #dedede); + background-image: -o-linear-gradient(top, #fcfcfc, #dedede); + background-image: linear-gradient(to bottom, #fcfcfc, #dedede); +} +.openerp .oe_list_content { width: 100%; } -.openerp .oe-listview-content > thead { +.openerp .oe_list_content td:first-child:after, .openerp .oe_list_content th:first-child:after { + border-width: 0; +} +.openerp .oe_list_content td.oe_number { + text-align: right !important; + max-width: 100px; +} +.openerp .oe_list_content > thead { border-bottom: 2px solid #cacaca; background: #eeeeee; vertical-align: top; } -.openerp .oe-listview-content > thead td, .openerp .oe-listview-content > thead th { +.openerp .oe_list_content td, .openerp .oe_list_content th { padding: 3px 6px; line-height: 18px; } -.openerp .oe-listview-content > thead th:after { +.openerp .oe_list_content th.oe_sortable, .openerp .oe_list_content th.oe_sortable div { + cursor: pointer; +} +.openerp .oe_list_content th.oe_sortable div { + position: relative; +} +.openerp .oe_list_content th.oe_sortable div:after { + position: absolute; + right: 6px; content: ""; - float: right; margin-top: 7px; border-width: 0 4px 4px; border-style: solid; border-color: black transparent; visibility: hidden; } -.openerp .oe-listview-content > thead th.sortup:after { +.openerp .oe_list_content th.sortup div:after { visibility: visible; filter: alpha(opacity=60); opacity: 0.6; } -.openerp .oe-listview-content > thead th.sortdown:after { +.openerp .oe_list_content th.sortdown div:after { border-bottom: none; border-left: 4px solid transparent; border-right: 4px solid transparent; @@ -1967,39 +2704,36 @@ visibility: visible; -moz-box-shadow: none; -webkit-box-shadow: none; - -box-shadow: none; + box-shadow: none; filter: alpha(opacity=60); opacity: 0.6; } -.openerp .oe-listview-content > tbody { +.openerp .oe_list_content > tbody { cursor: pointer; - background: white; } -.openerp .oe-listview-content > tbody > tr { +.openerp .oe_list_content > tbody > tr { + height: 27px; border-top: 1px solid #dddddd; } -.openerp .oe-listview-content > tbody > tr > td, .openerp .oe-listview-content > tbody > tr > th { +.openerp .oe_list_content > tbody > tr > td.oe_list_field_cell { padding: 3px 6px; - line-height: 18px; + white-space: pre-line; } -.openerp .oe-listview-content > tbody > tr > td > button, .openerp .oe-listview-content > tbody > tr > th > button { +.openerp .oe_list_content > tbody > tr > td > button, .openerp .oe_list_content > tbody > tr > th > button { border: none; background: transparent; padding: 0; -moz-box-shadow: none; -webkit-box-shadow: none; - -box-shadow: none; + box-shadow: none; } -.openerp .oe-listview-content > tbody > tr > td.oe_list_checkbox:first-child, .openerp .oe-listview-content > tbody > tr th.oe_list_checkbox:first-child { +.openerp .oe_list_content > tbody > tr > td.oe_list_checkbox:first-child, .openerp .oe_list_content > tbody > tr th.oe_list_checkbox:first-child { width: 17px; } -.openerp .oe-listview-content > tbody > tr > td.oe_list_checkbox:first-child:after, .openerp .oe-listview-content > tbody > tr th.oe_list_checkbox:first-child:after { +.openerp .oe_list_content > tbody > tr > td.oe_list_checkbox:first-child:after, .openerp .oe_list_content > tbody > tr th.oe_list_checkbox:first-child:after { border-width: 0; } -.openerp .oe-listview-content > tbody > tr > td.oe-number { - text-align: right !important; -} -.openerp .oe-listview-content > tbody > tr:nth-child(odd) { +.openerp .oe_list_content > tbody > tr:nth-child(odd) { background-color: #f0f0fa; background-color: #f0f0fa; background-image: -webkit-gradient(linear, left top, left bottom, from(#f0f0fa), to(#eeeef6)); @@ -2009,104 +2743,101 @@ background-image: -o-linear-gradient(top, #f0f0fa, #eeeef6); background-image: linear-gradient(to bottom, #f0f0fa, #eeeef6); } -.openerp .oe-listview-content > tfoot { +.openerp .oe_list_content > tfoot { border-top: 2px solid #cacaca; border-bottom: 1px solid #cacaca; background: #eeeeee; font-weight: bold; } -.openerp .oe-listview-content > tbody tr:hover td, .openerp .oe-listview-content tbody tr:hover th { - background-color: #eeeeee; - background-image: -webkit-gradient(linear, left top, left bottom, from(#eeeeee), to(#dedede)); - background-image: -webkit-linear-gradient(top, #eeeeee, #dedede); - background-image: -moz-linear-gradient(top, #eeeeee, #dedede); - background-image: -ms-linear-gradient(top, #eeeeee, #dedede); - background-image: -o-linear-gradient(top, #eeeeee, #dedede); - background-image: linear-gradient(to bottom, #eeeeee, #dedede); -} -.openerp .oe-listview-content .numeric { +.openerp .oe_list_content .numeric { text-align: right; width: 82px; } -.openerp .oe-listview-content .numeric input { +.openerp .oe_list_content .numeric input { text-align: right; } -.openerp .oe_kanban_group_title { - margin: 1px 1px 4px; - font-size: 15px; - font-weight: bold; - text-shadow: 0 1px 0 white; +.openerp .oe_list_content th.oe_list_header_handle { + font-size: 1px; + overflow: hidden; + text-indent: -9001px; } -.openerp .oe_kanban_column, .openerp .oe_kanban_group_header { - width: 240px; - vertical-align: top; - padding: 6px 7px 6px 6px; - background: #f0eeee; - border-left: 1px solid #f0f8f8; - border-right: 1px solid #b9b9b9; +.openerp .oe_list_content td.oe_list_field_handle { + width: 1em; + padding: 0 !important; + cursor: ns-resize; } -.openerp .oe_kanban_record { - position: relative; - display: block; - min-height: 50px; - margin: 6px 0; - display: block; - -moz-border-radius: 4px; - -webkit-border-radius: 4px; - border-radius: 4px; +.openerp .oe_list_content td.oe_list_field_handle .oe_list_handle { + font-size: 1px; + letter-spacing: -1px; + color: transparent; + font-weight: normal; + margin-right: 7px; +} +.openerp .oe_list_content td.oe_list_field_handle .oe_list_handle:before { + font: 18px "entypoRegular"; + content: "}"; + color: #e0e0e0; +} +.openerp .oe_list_content .oe_list_field_progressbar progress { + width: 100%; +} +.openerp .tree_header { + background-color: #f0f0f0; + border-bottom: 1px solid #cacaca; + color: #4c4c4c; + padding: 5px; + height: 25px; +} +.openerp .tree_header button { + float: right; + height: 27px; + margin-right: 5px; } -.openerp .oe_kanban_record:last-child { - margin-bottom: 0; +.openerp .oe-treeview-table { + width: 100%; + background-color: white; + border-spacing: 0; + color: #4c4c4c; } -.openerp .oe_kanban_record .oe_kanban_title { +.openerp .oe-treeview-table th { + padding: 10px; font-weight: bold; - margin: 2px 4px; + background-color: #f0f0f0; + border-bottom: 2px solid #cacaca; } -.openerp .oe_kanban_gravatar { - width: 20px; +.openerp .oe-treeview-table td { + cursor: pointer; + vertical-align: middle; + text-align: left; + vertical-align: middle; height: 20px; - -moz-border-radius: 3px; - -webkit-border-radius: 3px; - border-radius: 3px; - -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); - -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); - -box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); + padding-left: 4px; + padding-right: 4px; + border-right: 1px solid #e7e7e7; } -.openerp .oe_kanban_avatar_toto { - height: 40px; - width: 40px; - border: 1px solid; - border-color: #e5e5e5 #dbdbdb #d2d2d2; - -moz-border-radius: 3px; - -webkit-border-radius: 3px; - border-radius: 3px; - -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); - -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); - -box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); +.openerp .oe-treeview-table td.oe_number { + text-align: right !important; } -.openerp .oe_trad_field.touched { - border: 1px solid green !important; +.openerp .oe-treeview-table tr { + border-bottom: 1px solid #d6d6d6; } -.openerp.oe_tooltip { - font-size: 12px; +.openerp .oe-treeview-table tr:hover { + background-color: #e7e7e7; } -.openerp.oe_tooltip .oe_tooltip_string { - color: #ffdd55; - font-weight: bold; - font-size: 13px; +.openerp .oe-treeview-table span { + font-size: 90%; + font-weight: normal; + white-space: nowrap; + display: block; } -.openerp.oe_tooltip .oe_tooltip_help { - white-space: pre-wrap; +.openerp .oe-treeview-table .treeview-tr.oe-treeview-first { + background: transparent url(/web/static/src/img/expand.gif) 0 50% no-repeat; } -.openerp.oe_tooltip .oe_tooltip_technical { - padding: 0 0 4px 0; - margin: 5px 0 0 15px; +.openerp .oe-treeview-table .oe_open .treeview-tr.oe-treeview-first { + background-image: url(/web/static/src/img/collapse.gif); } -.openerp.oe_tooltip .oe_tooltip_technical li { - list-style: circle; -} -.openerp.oe_tooltip .oe_tooltip_technical_title { - font-weight: bold; +.openerp .oe-treeview-table .treeview-tr.oe-treeview-first span, .openerp .oe-treeview-table .treeview-td.oe-treeview-first span { + margin-left: 16px; } .openerp .oe_layout_debugging .oe_form_group { outline: 2px dashed green; @@ -2149,11 +2880,166 @@ color: #333333; } -.openerp .oe_form_field_many2one td:first-child { +.openerp .oe_fileupload { + display: inline-block; + clear: both; + width: 100%; +} +.openerp .oe_fileupload .oe_add { + float: left; position: relative; + width: 100%; + left: 2px; + top: 7px; } -.openerp .oe_form_field_many2one span.oe-m2o-drop-down-button { - position: absolute; - top: 3px; - right: 2px; +.openerp .oe_fileupload .oe_add button { + display: inline; + height: 24px; + font-size: 12px; + line-height: 12px; + vertical-align: middle; +} +.openerp .oe_fileupload .oe_add button.oe_attach { + width: 24px; + overflow: hidden; + width: 24px; + overflow: hidden; + background: transparent; + color: #7c7bad; + box-shadow: none; + border: none; + text-shadow: none; +} +.openerp .oe_fileupload .oe_add button.oe_attach .oe_e { + position: relative; + top: -1px; + left: -9px; +} +.openerp .oe_fileupload .oe_add input.oe_form_binary_file { + display: inline-block; + margin-left: -5px; + height: 28px; + width: 52px; + margin-top: -26px; +} +.openerp .oe_fileupload .oe_add .oe_attach_label { + color: #7c7bad; + margin-left: -3px; +} +.openerp .oe_fileupload .oe_attachments { + margin-bottom: 4px; + margin-right: 0px; + font-size: 12px; + border-radius: 2px; + border: solid 1px rgba(124, 123, 173, 0.14); +} +.openerp .oe_fileupload .oe_attachments .oe_attachment { + padding: 2px; + padding-left: 4px; + padding-right: 4px; +} +.openerp .oe_fileupload .oe_attachments .oe_attachment .oe_e { + font-size: 23px; + margin-top: -5px; +} +.openerp .oe_fileupload .oe_attachments .oe_attachment .oe_e:hover { + text-decoration: none; +} +.openerp .oe_fileupload .oe_attachments .oe_attachment:nth-child(odd) { + background: white; +} +.openerp .oe_fileupload .oe_attachments .oe_attachment:nth-child(even) { + background: #f4f5fa; +} + +.kitten-mode-activated { + background-image: url(http://placekitten.com/g/1365/769); + background-size: cover; + background-attachment: fixed; +} +.kitten-mode-activated > * { + opacity: 0.7; +} + +div.ui-widget-overlay { + background: black; + filter: alpha(opacity=30); + opacity: 0.3; +} + +.ui-widget { + font-family: "Lucida Grande", Helvetica, Verdana, Arial, sans-serif; + color: #4c4c4c; + font-size: 13px; +} + +.ui-menu { + padding: 2px 0; + -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); + margin-top: 4px; + border: 1px solid #afafb6; +} +.ui-menu .ui-menu-item { + width: 100%; + padding: 0; +} +.ui-menu .ui-menu-item a { + padding: 1px 16px; +} +.ui-menu .ui-menu-item a.ui-corner-all { + -moz-border-radius: 0; + -webkit-border-radius: 0; + border-radius: 0; +} +.ui-menu .ui-menu-item a.ui-state-active { + background: #f0f0fa; +} +.ui-menu .ui-menu-item a.ui-state-hover, .ui-menu .ui-menu-item a.ui-state-active { + background: #7c7bad; +} + +.ui-corner-all { + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; +} + +@media print { + .openerp { + text-shadow: none; + } + .openerp .oe_header_row, .openerp ul.oe_header, .openerp div.oe_mail_thread_action, .openerp .oe_mail_recthread_actions, .openerp .oe_button_box, .openerp .oe_form button, .openerp button.oe_invite, .openerp .oe_form header, .openerp .openerp .oe_notebook > li.ui-state-default { + display: none; + } + .openerp .oe_list_content button, .openerp .oe_list_content input[type=checkbox] { + visibility: hidden; + } + .openerp .tree_header button, .openerp .oe_mail .oe_mail_thread_msg .oe_mail_unread, .openerp .oe_mail_fetch_more, .openerp .oe_m2o_drop_down_button img, .openerp .oe_form_field_one2many_list_row_add { + visibility: hidden; + } + .openerp a.oe_m2o_cm_button, .openerp a.oe_e { + visibility: hidden; + } + .openerp .oe_form .oe_form_field_date img, .openerp .oe_form .oe_form_field_datetime img { + visibility: hidden; + } + .openerp .oe_notebook > li.ui-tabs-selected { + display: block; + } + .openerp .oe_application .oe_form_sheet, .openerp .oe_application .oe_form_sheetbg { + border: 0px !important; + box-shadow: 0px 0px 0px; + } + .openerp .oe_view_manager_current > .oe_view_manager_header { + border: 0px !important; + box-shadow: 0px 0px 0px; + } + .openerp .text-core .text-wrap .text-arrow { + background: none; + } + .openerp .openerp div.oe_mail_wall { + overflow: hidden !important; + } } diff --git a/addons/web/static/src/css/base.sass b/addons/web/static/src/css/base.sass index 3dcde9c9f70..e388ad16727 100644 --- a/addons/web/static/src/css/base.sass +++ b/addons/web/static/src/css/base.sass @@ -1,11 +1,17 @@ @charset "utf-8" // Variables {{{ -$section-title-color: #8786b7 $facets-border: #afafb6 -$facets-border-selected: #a6a6fe +$section-title-color: #7C7BAD +$tag-bg-light: #f0f0fa +$tag-bg-dark: #7C7BAD +$tag-border: #afafb6 +$tag-border-selected: #a6a6fe $hover-background: #f0f0fa -$colour4: #8a89ba +$link-color: #7C7BAD +$sheet-max-width: 860px +$sheet-min-width: 650px +$sheet-padding: 16px // }}} // Mixins {{{ @font-face @@ -27,6 +33,14 @@ $colour4: #8a89ba font-weight: normal font-style: normal +@mixin reset() + border: none + padding: 0 + margin: 0 + background: none + @include radius(0) + @include box-shadow(none) + @mixin vertical-gradient($startColor: #555, $endColor: #333) background-color: $startColor background-image: -webkit-gradient(linear, left top, left bottom, from($startColor), to($endColor)) /* Saf4+, Chrome */ @@ -36,7 +50,6 @@ $colour4: #8a89ba background-image: -o-linear-gradient(top, $startColor, $endColor) /* Opera 11.10+ */ background-image: linear-gradient(to bottom, $startColor, $endColor) - @mixin radial-gradient($gradient) background-position: center center background-image: -webkit-radial-gradient(circle, $gradient) @@ -52,7 +65,7 @@ $colour4: #8a89ba @mixin box-shadow($bsval: 0px 1px 4px #777) -moz-box-shadow: $bsval -webkit-box-shadow: $bsval - -box-shadow: $bsval + box-shadow: $bsval @mixin transition($transval: (border linear 0.2s, box-shadow linear 0.2s)) -webkit-transition: $transval @@ -70,10 +83,38 @@ $colour4: #8a89ba -moz-background-clip: $clip background-clip: $clip -@mixin unscrew-box - -moz-box-sizing: border-box - -webkit-box-sizing: border-box - box-sizing: border-box +@mixin box-sizing($type: content) + // type = border || content || padding + -webkit-box-sizing: #{$type}-box + -moz-box-sizing: #{$type}-box + -ms-box-sizing: #{$type}-box + box-sizing: #{$type}-box + +@mixin skew-gradient($startColor: #555, $endColor: #333) + background-color: $endColor + background: -moz-linear-gradient(135deg, $endColor, $startColor) + background: -o-linear-gradient(135deg, $startColor, $endColor) + background: -webkit-gradient(linear, left top, right bottom, from($startColor), to($endColor)) + +@mixin transform($transform) + -webkit-transform: $transform + -moz-transform: $transform + -ms-transform: $transform + -o-transform: $transform + transform: $transform + +@mixin keyframes($name) + @-webkit-keyframes #{$name} + @content + @-moz-keyframes #{$name} + @content + @-ms-keyframes #{$name} + @content + @-o-keyframes #{$name} + @content + @keyframes #{$name} + @content + // 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 @@ -81,17 +122,35 @@ $colour4: #8a89ba font-size: 1px letter-spacing: -1px color: transparent + font-weight: normal &:before - font-family: "mnmliconsRegular" + font: 21px "mnmliconsRegular" + content: $icon-name + color: $color + +@mixin text-to-entypo-icon($icon-name, $color: #404040, $size: 21px) + font-size: 1px + letter-spacing: -1px + color: transparent + font-weight: normal + &:before + font: $size "entypoRegular" content: $icon-name - font-size: 20px color: $color // }}} -.openerp.openerp-web-client-container +@media print + .oe_topbar, .oe_leftbar, .oe_loading + display: none !important + +.openerp.openerp_webclient_container height: 100% - position: relative + +// jQueryUI css bug fixing +.text-tag .text-button + height: auto !important + min-height: 16px .openerp // Global style {{{ @@ -101,21 +160,32 @@ $colour4: #8a89ba color: #4c4c4c font-size: 13px background: white + text-shadow: 0 1px 1px rgba(255, 255, 255, 0.5) // }}} + //Placeholder style{{{ + \:-moz-placeholder + color: $tag-border !important + font-style: italic !important + \::-webkit-input-placeholder + color: $tag-border !important + font-style: italic !important + //}}} // Tag reset {{{ a text-decoration: none + cursor: pointer !important table padding: 0 - font-size: 13px border-collapse: collapse thead font-weight: bold background-color: #f0f0f0 th - border-right: 1px dotted #afafb6 - &:last-child - border-right: none + border-left: 1px solid #dfdfdf + &:first-child + border-left: none + &.null + border-left: none th, td padding: 0 text-align: left @@ -129,51 +199,23 @@ $colour4: #8a89ba @include vertical-gradient(#f0f0fa, #eeeef6) .zebra tbody tr:hover td @include vertical-gradient(#eee, #dedede) - .numeric - text-align: right - width: 82px - input - text-align: right - ul, li, ol - margin: 0 - padding: 0 - li - list-style-type: none - // }}} - // Generic classes {{{ - .oe_i - font-family: "mnmliconsRegular" !important - font-size: 21px - font-weight: 300 !important - .oe_e - font-family: "entypoRegular" !important - font-size: 34px - font-weight: 300 !important - .oe_left - float: left - margin-right: 8px - .oe_right - float: right - margin-left: 8px - .oe_text_right - text-align: right - .oe_clear - clear: both - .oe_wait - cursor: wait - .oe_fade - color: #888 - //font-size: 12px - .oe_bold - font-weight: bold + input, textarea, select + padding: 2px 4px + border: 1px solid #ccc + @include radius(3px) + background: white + img + vertical-align: middle + h4 + margin: 4px 0 // }}} // Button style {{{ - a.button:link, a.button:visited, button, input[type='submit'] + a.button:link, a.button:visited, button, input[type='submit'], .ui-dialog-buttonpane .ui-dialog-buttonset .ui-button display: inline-block - border: 1px solid #ababab - color: #404040 + border: 1px solid rgba(0,0,0,0.4) + color: #4c4c4c margin: 0 - padding: 4px 12px + padding: 3px 12px font-size: 13px text-align: center @include vertical-gradient(#efefef, #d8d8d8) @@ -183,20 +225,19 @@ $colour4: #8a89ba -webkit-font-smoothing: antialiased outline: none - a.button:hover, button:hover, input[type='submit']:hover + a.button:hover, button:hover, input[type='submit']:hover, .ui-dialog-buttonpane .ui-dialog-buttonset .ui-button.ui-state-hover @include vertical-gradient(#f6f6f6, #e3e3e3) cursor: pointer + background-position: 0 - a.button:focus, button:focus, input[type='submit']:focus + a.button:focus, button:focus, input[type='submit']:focus, .ui-dialog-buttonpane .ui-dialog-buttonset .ui-button.ui-state-focus border: 1px solid #80bfff + background-position: 0 @include vertical-gradient(#f6f6f6, #e3e3e3) @include box-shadow((0 0 3px #80bfff, 0 1px 1px rgba(255, 255, 255, .8) inset)) - a.button:active, a.button.active, button:active, button.active, input[type='submit']:active, input[type='submit'].active - background: #e3e3e3 - background: -moz-linear-gradient(top, #e3e3e3, #f6f6f6) #1b468f - background: -webkit-gradient(linear, left top, left bottom, from(#e3e3e3), to(#f6f6f6)) #1b468f - background: linear-gradient(top, #e3e3e3, #f6f6f6) #1b468f + a.button:active, a.button.active, button:active, button.active, input[type='submit']:active, input[type='submit'].active, .ui-dialog-buttonpane .ui-dialog-buttonset .ui-button.ui-state-active + @include vertical-gradient(#e3e3e3, #f6f6f6) @include box-shadow(none) a.button.disabled, button:disabled, input[type='submit']:disabled @@ -206,32 +247,417 @@ $colour4: #8a89ba color: #aaa !important cursor: default text-shadow: 0 1px 1px #fff !important + // }}} + // Jquery ui Overrides {{{ + .ui-widget-content a + color: $link-color + .oe_bounce_container + display: inline-block - button.primary - color: white - @include vertical-gradient(#dc5f59, #b33630) - @include box-shadow(none) + // bug noted in jquery ui CSS doesn't seem to occur in IE9, + // so remove position:relative + .ui-tabs + position: static - button.primary:hover - @include vertical-gradient(lighten(#dc5f59, 3%), lighten(#b33630, 3%)) - @include box-shadow(0 0 1px rgba(0,0,0,0.2)) + // Modal box + &.ui-dialog + display: none + padding: 6px + //overflow: hidden + background-color: rgba(60,60,60,0.7) + border: 1px solid + border-color: #888 #555 #444 + //overflow: hidden + @include radius(8px) + @include box-shadow(0 1px 12px rgba(0, 0, 0, 0.6)) + @include background-clip() + .ui-dialog-content + padding: 0 + .ui-dialog-titlebar, .ui-dialog-content, .ui-dialog-buttonpane + padding: 16px + .ui-dialog-titlebar + border-bottom: 1px solid #cacaca + @include radius(2px 2px 0 0) + @include vertical-gradient(#FCFCFC, #DEDEDE) + .ui-dialog-title + margin: 0 + padding: 0 + .ui-widget-header + border: none + .ui-dialog-content + background: white + .ui-dialog-buttonpane + border-top: 1px solid #e0e0e0 + background: #f5f7f9 + margin: 0 + @include radius(0 0 2px 2px) + button + margin: 0 4px 0 0 + .ui-dialog-buttonset + float: left + .ui-button + margin-right: 4px + .ui-dialog-titlebar-close + padding: 0 + .ui-icon-closethick + display: none + &:before + content: "×" + font-size: 18px + font-weight: bold + line-height: 16px + color: black + text-shadow: 0 1px 0 white + padding: 0 + cursor: pointer + background: transparent + border: 0 + &:hover + color: black + text-decoration: none + .oe_about + background-color: white + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAAAKUlEQVQIHWO8e/fufwYsgAUkJigoiCIF5DMyoYggcUiXgNnBiGQKmAkARpcEQeriln4AAAAASUVORK5CYII=) + @include radius(0 0 2px 2px) + a + color: $link-color + &:hover + text-decoration: underline + &:focus + outline: none + .oe_logo + margin-left: -6px + .oe_bottom + position: absolute + top: 50% + left: 0 + right: 0 + bottom: 0 + text-shadow: 0 1px 1px #999999 + @include vertical-gradient(#b41616, #600606) + color: #eee + padding: 0 16px + @include radius(0 0 2px 2px) + a + color: #eee - button.primary:active - @include vertical-gradient(#b33630, #dc5f59) - @include box-shadow(none) + &.ui-dialog.oe_act_window + .ui-dialog-content + padding: 0px - button.following - color: white - width: 120px - @include vertical-gradient($colour4, darken($colour4, 3%)) + .modal-backdrop + position: fixed + top: 0 + right: 0 + bottom: 0 + left: 0 + z-index: 1040 + background-color: black + @include opacity(.3) - button.unfollow - display: none + // }}} + // Generic classes {{{ + .oe_i + font-family: "mnmliconsRegular" !important + font-size: 21px + font-weight: 300 !important + .oe_e + font-family: "entypoRegular" !important + font-size: 34px + font-weight: 300 !important + .oe_left + float: left + margin-right: 8px + .oe_right + float: right + margin-left: 8px + .oe_text_right + text-align: right + .oe_clear + clear: both + .oe_wait + cursor: wait + .oe_fade + color: #888 + font-weight: normal + .oe_bold + font-weight: bold + .oe_inline + width: auto !important + .oe_highlight color: white - width: 120px - @include vertical-gradient(#dc5f59, #b33630) + background: #DC5F59 + button.oe_highlight + @include vertical-gradient(#df3f3f, #a21a1a) + @include box-shadow((0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 1px rgba(155, 155, 155, 0.4) inset)) + button.oe_highlight:hover + @include vertical-gradient(lighten(#e04f4f, 2%), lighten(#a21a1a, 2%)) + button.oe_highlight:active + @include vertical-gradient(lighten(#a21a1a, 8%), darken(#df3f3f, 8%)) + .oe_background_grey + background: #eee !important + + .oe_form_dirty + .oe_highlight_on_dirty + color: white + background: #dc5f59 + font-weight: bold + button.oe_highlight_on_dirty + @include vertical-gradient(#dc5f59, #b33630) + @include box-shadow(none) + &:hover + background: #ED6F6A + .oe_title + width: 50% + float: left + .oe_title:after + content: "." + display: block + height: 0 + clear: both + visibility: hidden + .oe_button_box + width: 270px + text-align: right + button + margin: 4px + .oe_avatar + > img + max-height: 90px + max-width: 90px + @include radius(3px) + @include box-shadow(0 1px 4px rgba(0, 0, 0, 0.4)) + border: none + margin-bottom: 10px + .oe_avatar + div + margin-left: 5px + .oe_image_small > img + max-width: 50px + max-height: 50px + .oe_image_medium > img + max-width: 180px + max-height: 180px + .oe_button.oe_link + @include reset() + img + display: none + span + @include reset() + color: $link-color + font-weight: bold + &:hover + text-decoration: underline + .oe_webclient + .oe_star_on, .oe_star_off + color: #cccccc + text-shadow: 0 0 2px black + vertical-align: top + position: relative + top: -5px + .oe_star_on:hover, .oe_star_off:hover + text-decoration: none + .oe_star_on + color: gold + p.oe_grey + max-width: 650px + .oe_grey + color: #aaa // }}} + + // Tags (for many2many tags, among others) {{{ + .oe_tag + border: 1px solid $tag-border + font-size: 11px + padding: 2px 4px + margin: 0 2px 2px 0 + @include radius(3px) + background: $tag-bg-light + color: #4C4C4C + .oe_tag_dark + background: $tag-bg-dark + color: #eee + + .oe_tags + &.oe_inline + min-width: 250px + .text-wrap + width: 100% !important + textarea + width: 100% !important + .text-core .text-wrap .text-dropdown .text-list .text-suggestion em + font-style: italic + text-decoration: none + margin-bottom: 1px + // }}} + // Tooltips {{{ + &.oe_tooltip + font-size: 12px + .oe_tooltip_string + color: #FD5 + font-weight: bold + font-size: 13px + .oe_tooltip_help + white-space: pre-wrap + .oe_tooltip_technical + padding: 0 0 4px 0 + margin: 5px 0 0 15px + li + list-style: circle + .oe_tooltip_technical_title + font-weight: bold + .oe_tooltip_close + margin: -5px 0 0 2px + cursor: default + float: right + color: white + &:hover + color: #999 + cursor: pointer + .oe_tooltip_message + max-width: 310px + // }}} + // Notebook {{{ + .oe_notebook + margin: 8px 0 + padding: 0 16px + list-style: none + zoom: 1 + .oe_notebook.ui-corner-all + @include radius(0) + .oe_notebook:before, .oe_notebook:after + display: table + content: "" + zoom: 1 + .oe_notebook:after + clear: both + .oe_notebook > li + float: left + .oe_notebook > li > a + display: block + color: #4c4c4c + .oe_notebook + border-color: #ddd + border-style: solid + border-width: 0 0 1px + .oe_notebook > li + position: relative + .oe_notebook > li > a + padding: 0 12px + margin-right: 2px + line-height: 30px + border: 1px solid transparent + @include radius(4px 4px 0 0) + .oe_notebook > li > a:hover + text-decoration: none + background-color: #eee + border-color: #eee #eee #ddd + .oe_notebook > li.ui-state-active > a, .oe_notebook > li.ui-state-active > a:hover + background-color: #ffffff + border: 1px solid #ddd + border-bottom-color: transparent + cursor: default + .oe_notebook_page + padding: 0 + div.ui-tabs + padding: 3px 0px 3px 0px + .ui-tabs-hide + display: none + // }}} + // Dropdown {{{ + .oe_dropdown, .oe_dropdown_hover, .oe_dropdown_toggle + position: relative + cursor: pointer + .oe_dropdown_toggle + color: rgba(0,0,0,0.3) + font-weight: normal + .oe_dropdown_hover:hover .oe_dropdown_menu, .oe_dropdown_menu.oe_opened + display: block + .oe_dropdown_menu + display: none + position: absolute + top: 26px + left: 0 + z-index: 3 + margin: 0 + padding: 0 + border: 1px solid $tag-border + background: white + padding: 4px 0 + min-width: 140px + text-align: left + @include radius(3px) + @include box-shadow(0 1px 4px rgba(0,0,0,0.3)) + > li + &:hover + @include vertical-gradient(#f0f0fa, #eeeef6) + @include box-shadow(none) + list-style-type: none + float: none + display: block + position: relative + margin: 0 + padding: 2px 8px + > a + white-space: nowrap + display: block + color: #4c4c4c + text-decoration: none + &:hover + text-decoration: none + .oe_dropdown_arrow:after + width: 0 + height: 0 + display: inline-block + content: "&darr" + text-indent: -99999px + vertical-align: top + margin-top: 8px + //margin-left set at 3px to avoid a strange overflow + margin-left: 3px + border-left: 4px solid transparent + 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 display: none @@ -256,13 +682,14 @@ $colour4: #8a89ba // }}} // Login {{{ .oe_login - background: url("/web/static/src/img/pattern.png") repeat + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAAAKUlEQVQIHWO8e/fufwYsgAUkJigoiCIF5DMyoYggcUiXgNnBiGQKmAkARpcEQeriln4AAAAASUVORK5CYII=) text-align: center font-size: 14px height: 100% - li + ul, li + margin: 0 list-style-type: none - padding-bottom: 4px + padding: 0 0 4px 0 button float: right display: inline-block @@ -279,7 +706,7 @@ $colour4: #8a89ba font-family: "Lucida Grande", Helvetica, Verdana, Arial border: 1px solid #999999 background: whitesmoke - @include box-shadow(inset 0 1px 4px rgba(0, 0, 0, 0.3)) + @include box-shadow(inset 0 1px 4px rgba(0, 0, 0, 0.4)) @include radius(3px) input margin-bottom: 9px @@ -379,63 +806,118 @@ $colour4: #8a89ba .oe_topbar, .oe_leftbar display: none // }}} - // WebClient.topbar {{{ + // Top Menu {{{ + .oe_topbar width: 100% - height: 31px - border-top: solid 1px #d3d3d3 + height: 32px + background-color: #414141 @include vertical-gradient(#646060, #262626) - .oe_topbar_item - li - float: left - a - display: block - padding: 5px 10px 7px - line-height: 20px - height: 20px - color: #eee - vertical-align: top - text-shadow: 0 1px 1px rgba(0,0,0,0.2) - &:hover - background: #303030 - color: white - @include box-shadow(0 1px 2px rgba(255,255,255,0.3) inset) - .oe_active - background: #303030 - font-weight: bold + display: block + padding: 5px 10px 7px + line-height: 20px + height: 20px + text-decoration: none + color: #eee + vertical-align: top + text-shadow: 0 1px 1px rgba(0,0,0,0.2) + @include transition(all 0.2s ease-out) + &:hover + background: rgba(0,0,0,0.2) + text-shadow: black 0px 0px 3px color: white - @include box-shadow(0 1px 2px rgba(255,255,255,0.3) inset) + @include box-shadow(0 1px 2px rgba(0,0,0,0.4) inset) .oe_topbar_avatar width: 24px height: 24px margin: -2px 2px 0 0 - @include radius(4px) + @include radius(3px) .oe_topbar_avatar vertical-align: top - .oe_dropdown_toggle:after - width: 0 - height: 0 - display: inline-block - content: "&darr" - text-indent: -99999px - vertical-align: top - margin-top: 8px - margin-left: 4px - border-left: 4px solid transparent - border-right: 4px solid transparent + .oe_dropdown_arrow:after border-top: 4px solid white - @include opacity(0.5) + .oe_dropdown_menu + top: 32px + background: #333 + background: rgba(37,37,37,0.9) + border-color: #999 + border-color: rgba(0, 0, 0, 0.2) + border-style: solid + border-width: 0 1px 1px + @include radius(0 0 6px 6px) + @include background-clip() + li + float: none + padding: 3px 12px + a + color: #eee + &:hover + @include vertical-gradient(#292929, #191919) + @include box-shadow(none) + + // oe menu is the list of the buttons on the left side of the bar. + // So why aren't the buttons oe_topbar_items ? This sad state of affairs + // is a leftover from an uncomplete refactoring when the left and top menu + // were merged. You are welcome to refactor and clean this up + .oe_menu + float: left + padding: 0 + margin: 0 + font-size: 13px + > li + list-style-type: none + padding: 0 + margin: 0 + float: left + display: block + color: #eeeeee + > a + display: block + padding: 5px 10px 7px + line-height: 20px + height: 20px + text-decoration: none + color: #eee + vertical-align: top + text-shadow: 0 1px 1px rgba(0,0,0,0.2) + @include transition(all 0.2s ease-out) + &:hover + background: rgba(0,0,0,0.2) + text-shadow: black 0px 0px 3px + color: white + @include box-shadow(0 1px 2px rgba(0,0,0,0.4) inset) + > .oe_active + background: rgba(0,0,0,0.30) + text-shadow: black 0px 0px 3px + @include box-shadow(0 1px 2px rgba(0,0,0,0.4) inset) + + .oe_user_menu + float: right + padding: 0 + margin: 0 + li + list-style-type: none + float: left + .oe_dropdown_menu + right: -1px + + .oe_systray > div + float: left + padding: 0 4px 0 4px + .oe_systray + float: right + // }}} // Webclient.leftbar {{{ .oe_leftbar + display: none width: 220px background: #f0eeee - border-right: 1px solid #afafb6 + border-right: 1px solid $tag-border text-shadow: 0 1px 1px white padding-bottom: 16px - line-height: 18px a.oe_logo width: 220px display: block @@ -462,149 +944,22 @@ $colour4: #8a89ba color: #c81010 font-style: italic // }}} - // UserMenu {{{ - .oe_user_menu - float: right - padding: 0 - margin: 0 - li - list-style-type: none - float: left - .oe_dropdown - position: relative - - .oe_dropdown_options - float: left - background: #333 - background: rgba(37,37,37,0.9) - display: none - position: absolute - top: 32px - right: -1px - border: 0 - z-index: 900 - margin-left: 0 - margin-right: 0 - padding: 6px 0 - zoom: 1 - border-color: #999 - border-color: rgba(0, 0, 0, 0.2) - border-style: solid - border-width: 0 1px 1px - @include radius(0 0 6px 6px) - @include box-shadow(0 1px 4px rgba(0,0,0,0.3)) - @include background-clip() - li - float: none - display: block - background-color: none - a - display: block - padding: 4px 15px - clear: both - font-weight: normal - line-height: 18px - color: #eee - &:hover - @include vertical-gradient(#292929, #191919) - @include box-shadow(none) - hr - border-top: 1px solid #999 - border-bottom: 0 - // }}} - // Systray {{{ - .oe_systray > div - float: left - padding: 0 4px 0 4px - .oe_systray - float: right + // Webclient.leftbar items {{{ - .oe_systray_dropdown - position: relative - .oe_systray_dropdown:hover .oe_systray_dropdown_options - display: block - .oe_systray_dropdown_options - background: #333 - background: rgba(37,37,37,0.9) - display: none - position: absolute - z-index: 900 - border: 0 - margin-left: 0 - margin-right: 0 - padding: 6px 0 - border-color: #999 - border-color: rgba(0, 0, 0, 0.2) - border-style: solid - border-width: 0 1px 1px - @include radius(0 0 6px 6px) - @include box-shadow(0 1px 4px rgba(0,0,0,0.3)) - @include background-clip() - li - float: none - display: block - background-color: none - a - display: block - padding: 4px 15px - clear: both - font-weight: normal - line-height: 18px - color: #eee - &:hover - @include vertical-gradient(#292929, #191919) - @include box-shadow(none) - // }}} - // Menu {{{ - .oe_menu - float: left - padding: 0 - margin: 0 - li - float: left - a - display: block - padding: 5px 10px 7px - line-height: 20px - height: 20px - color: #eee - vertical-align: top - text-shadow: 0 1px 1px rgba(0,0,0,0.2) - &:hover - background: #303030 - color: white - @include box-shadow(0 1px 2px rgba(255,255,255,0.3) inset) - .oe_active - background: #303030 - font-weight: bold - color: white - @include box-shadow(0 1px 2px rgba(255,255,255,0.3) inset) - .oe_menu_more_container - position: relative - .oe_menu_more - position: absolute - padding: 0 - background-color: #646060 - z-index: 1 - border: 1px solid black - border-bottom-left-radius: 5px - border-bottom-right-radius: 5px - li - float: none - a - white-space: nowrap .oe_secondary_menu_section font-weight: bold margin-left: 8px - color: $colour4 + color: $link-color .oe_secondary_submenu padding: 2px 0 8px 0 margin: 0 width: 100% display: inline-block - > li + li position: relative - padding: 1px 0 1px 20px + margin: 0 + padding: 1px 0 1px 20px !important + list-style-type: none a display: block color: #4c4c4c @@ -614,7 +969,7 @@ $colour4: #8a89ba top: 1px right: 1px font-size: 10px - background: $colour4 + background: $link-color color: white padding: 2px 4px margin: 1px 6px 0 0 @@ -622,32 +977,32 @@ $colour4: #8a89ba text-shadow: 0 1px 1px rgba(0,0,0,0.2) @include radius(4px) @include box-shadow(inset 0 1px 1px rgba(0, 0, 0, 0.2)) - .oe_menu_counter + .oe_menu_counter float: right - background: #8a89ba - color: #eee - font-size: 12px - border: 1px solid lightgray - padding: 0px 4px 0px 4px text-shadow: 0 1px 1px rgba(0,0,0,0.2) - margin: 0px - @include radius(4px) + margin: 1px + padding: 1px 4px + border: none + &:hover + cursor: pointer + @include transform(scale(1.1)) .oe_active - background: $colour4 border-top: 1px solid lightGray - border-bottom: 1px solid lightGray + border-bottom: 1px solid #dedede text-shadow: 0 1px 1px rgba(0,0,0,0.2) - @include box-shadow(inset 0 1px 1px rgba(0, 0, 0, 0.2)) + @include box-shadow((inset 0 1px 3px rgba(0, 0, 0, 0.2), inset 0 -1px 3px rgba(40, 40, 40, 0.2))) + background: $link-color a color: white .oe_menu_label background: #eee - color: $colour4 + color: $link-color text-shadow: 0 1px 1px white @include box-shadow(0 1px 1px rgba(0, 0, 0, 0.2)) - .oe_menu_counter + .oe_menu_counter background: #eee - color: #8a89ba + color: $link-color + @include box-shadow(inset 0 1px 2px rgba(0, 0, 0, 0.2)) .oe_menu_toggler:before width: 0 height: 0 @@ -669,123 +1024,139 @@ $colour4: #8a89ba border-left: 4px solid transparent border-right: 4px solid transparent border-top: 4px solid #4c4c4c + + // }}} + // About openerp {{{ + .oe_about + background-color: white + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAAAKUlEQVQIHWO8e/fufwYsgAUkJigoiCIF5DMyoYggcUiXgNnBiGQKmAkARpcEQeriln4AAAAASUVORK5CYII=) + @include radius(0 0 2px 2px) + a + color: $link-color + &:hover + text-decoration: underline + &:focus + outline: none + .oe_logo + margin-left: -6px + .oe_bottom + position: absolute + top: 50% + left: 0 + right: 0 + bottom: 0 + text-shadow: 0 1px 1px #999999 + @include vertical-gradient(#b41616, #600606) + color: #eee + padding: 0 16px + @include radius(0 0 2px 2px) + a + color: #eee // }}} // ActionManager {{{ .oe_application width: 100% a - color: $colour4 + color: $link-color &:hover text-decoration: underline - // }}} - // Content Header MIT {{{ - .oe-view-manager-header - border-top: 1px solid #cacaca - 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))) - h2 - float: left - font-size: 18px - margin: 1px 0 - a - color: $colour4 - .buttons - padding-right: 8px - li - float: left - margin-right: 12px - .filter - vertical-align: top - li - height: 24px - line-height: 24px - padding: 0 8px - .header-row - height: 26px - line-height: 26px - margin: 8px - clear: both - text-shadow: 0 1px 1px white - .dropdown-toggle:after - margin-top: 7px - margin-left: 6px - border-top-color: #404040 - .oe_vm_switch - li - padding-left: 1px - text-align: center - width: 24px - height: 24px - line-height: 24px - .pagination - li - height: 24px - line-height: 24px - padding: 0 8px - .button-group + > div + height: 100% + .oe_breadcrumb_item:not(:last-child) display: inline-block - border: 1px solid #ababab - @include radius(5px) - li - float: left - border-right: 1px solid #ababab - &:last-child - border: none - a - color: #4c4c4c - &:hover - text-decoration: none - .active - width: 100% - background: #999 - @include box-shadow(0 1px 4px rgba(0,0,0,0.3) inset) - color: #fff - text-shadow: 0 1px 2px rgba(0,0,0,0.4) + max-width: 7em + white-space: nowrap + overflow: hidden + text-overflow: ellipsis // }}} - // ViewManager.header {{{ - .oe_view_manager_header - width: 100% - border-top: 1px solid #cacaca - 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 - //min-height: 26px - //line-height: 26px - clear: both - text-shadow: 0 1px 1px white - td - padding: 8px - .oe_header_row:last-child - td - padding-top: 0 - .oe_header_row:first-child + // ViewManager common {{{ + .oe_view_manager + .oe_view_manager_body + height: inherit + .oe_view_manager_view_kanban + height: inherit + + table.oe_view_manager_header + width: 100% + table-layout: fixed + .oe_header_row + //min-height: 26px + //line-height: 26px + clear: both + text-shadow: 0 1px 1px white + .oe_header_row:last-child + td + padding-top: 0 + .oe_header_row:first-child + td + padding-top: 8px + .oe_view_manager_sidebar + margin: 0px auto + text-align: center td - padding-top: 8px - .oe_view_manager_sidebar - margin: 0px auto - width: 400px - text-align: center - td + line-height: 26px + h2 + font-size: 18px + margin: 0 + float: left + a + color: $link-color + .oe_dropdown_menu + line-height: normal + .oe_button_group + display: inline-block + border: 1px solid #ababab + @include radius(5px) + li + float: left + border-right: 1px solid #ababab + &:last-child + border: none + a + color: #4c4c4c + &:hover + text-decoration: none + .active + background: #999 + @include box-shadow(0 1px 4px rgba(0,0,0,0.3) inset) + a + color: #fff + text-shadow: 0 1px 2px rgba(0,0,0,0.4) + .oe_view_manager_buttons + white-space: nowrap + // }}} + // ViewManager.pager {{{ + .oe_view_manager_pager line-height: 26px - h2 - font-size: 18px + .oe_list_pager_single_page .oe_pager_group + display: none + .oe_pager_value + float: left + margin-right: 8px + ul.oe_pager_group + padding: 0 margin: 0 + .oe_pager_group float: left - a - color: $colour4 - .oe_button_group + height: 24px + line-height: 24px display: inline-block border: 1px solid #ababab + cursor: pointer @include radius(5px) li + height: 24px + line-height: 24px + padding: 0 + margin: 0 + list-style-type: none float: left border-right: 1px solid #ababab &:last-child border: none a color: #4c4c4c + padding: 0 8px &:hover text-decoration: none .active @@ -794,511 +1165,534 @@ $colour4: #8a89ba a color: #fff text-shadow: 0 1px 2px rgba(0,0,0,0.4) - .oe_view_manager_buttons - white-space: nowrap - // }}} - // ViewManager.pager {{{ - .oe_view_manager_pager - line-height: 26px - .oe_pager_value - float: left - margin-right: 8px - .oe_pager_group - float: left - height: 24px - line-height: 24px - display: inline-block - border: 1px solid #ababab - cursor: pointer - @include radius(5px) - li - height: 24px - line-height: 24px + // }}} + // ViewManager.switches {{{ + .oe_view_manager_switch padding: 0 - float: left - border-right: 1px solid #ababab - &:last-child - border: none - a - color: #4c4c4c - padding: 0 8px - &:hover - text-decoration: none - .active - background: #999 - @include box-shadow(0 1px 4px rgba(0,0,0,0.3) inset) - a - color: #fff - text-shadow: 0 1px 2px rgba(0,0,0,0.4) - // }}} - // ViewManager.switches {{{ - .oe_view_manager_switch - li - text-align: center - width: 24px - height: 24px - line-height: 16px - a + margin: 0 0 0 8px + li + margin: 0 + width: 24px + height: 24px + line-height: 16px + padding: 0 + text-align: center + list-style-type: none + a + position: relative + .oe_vm_switch_list:after, .oe_vm_switch_tree:after + padding: 2px + content: "i" + .oe_vm_switch_form:after + content: "m" + .oe_vm_switch_graph:after + font-family: "mnmliconsRegular" !important + font-size: 21px + font-weight: 300 !important + content: "}" + top: -2px position: relative - .oe_vm_switch_list:after, .oe_vm_switch_tree:after - padding: 2px - content: "i" - .oe_vm_switch_graph:after - font-family: "mnmliconsRegular" !important - font-size: 21px - font-weight: 300 !important - content: "}" - top: -2px - position: relative - .oe_vm_switch_gantt:after - font-family: "mnmliconsRegular" !important - font-size: 21px - font-weight: 300 !important - content: "y" - top: -2px - position: relative - .oe_vm_switch_calendar:after - content: "P" - .oe_vm_switch_kanban:after - content: "k" - .oe_vm_switch_diagram:after - content: "f" - //.oe_vm_switch_list:after, .oe_vm_switch_tree:after - //content: "ö" - //.oe_vm_switch_graph:after - //content: "ó" - //.oe_vm_switch_gantt:after - //content: "y" - //.oe_vm_switch_calendar:after - //content: "b" - //.oe_vm_switch_kanban:after - //content: "ó" - //.oe_vm_switch_diagram:after - //content: "}" + .oe_vm_switch_gantt:after + font-family: "mnmliconsRegular" !important + font-size: 21px + font-weight: 300 !important + content: "y" + top: -2px + position: relative + .oe_vm_switch_calendar:after + content: "P" + .oe_vm_switch_kanban:after + content: "k" + .oe_vm_switch_diagram:after + content: "f" + // }}} + // 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.sidebar {{{ - .oe_form_dropdown_section - position: relative - display: inline-block - .oe_dropdown_toggle:after - width: 0 - height: 0 - display: inline-block - content: "&darr" - text-indent: -99999px - vertical-align: top - border-left: 4px solid transparent - border-right: 4px solid transparent - border-top: 4px solid white - @include opacity(0.5) - margin-top: 7px - margin-left: 6px - border-top-color: #404040 - .oe_dropdown_menu - display: none - position: absolute - top: 28px - left: 0px - padding: 8px - border: 1px solid #afafb6 - width: 120px - overflow-x: hidden - z-index: 900 - text-align: left - background: white - @include radius(3px) - @include box-shadow(0 1px 4px rgba(0,0,0,0.3)) - li - list-style-type: none - float: none - display: block - background-color: none - a - display: block - padding: 3px 6px - clear: both - font-weight: normal - line-height: 14px - color: #4c4c4c - text-decoration: none - cursor: pointer - &:hover - text-decoration: none - @include vertical-gradient(#f0f0fa, #eeeef6) - @include box-shadow(none) - .oe_sidebar - white-space: nowrap + // ViewManager inline {{{ + .oe_view_manager_inline, .oe_view_manager_inlineview + height: 100% + > .oe_view_manager_header + display: none + // }}} + // Viewmanager popup {{{ + .oe_popup_form > .oe_formview > .oe_form_pager + display: none !important // }}} - // SearchView xmo {{{ + // SearchView {{{ .oe_searchview cursor: text position: relative float: right - padding-right: 20px - width: 410px + padding: 1px 0 + line-height: 18px + width: 400px border: 1px solid #ababab background: white - @include radius(1em) + @include radius(13px) @include box-shadow(0 1px 2px rgba(0,0,0,0.2) inset) - + input, textarea + padding: 3px + height: 14px + font-size: 12px + line-height: 18px &.oe_focused - border-color: $facets-border-selected - @include box-shadow(0 1px 2px $facets-border-selected inset) - + border-color: $tag-border-selected + @include box-shadow(0 1px 2px $tag-border-selected inset) .oe_searchview_clear cursor: pointer position: absolute top: 0 - right: 22px + right: 18px width: 15px height: 100% - background: url(../img/attachments-close.png) center center no-repeat - + background: url(../img/search_reset.gif) center center no-repeat .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: "◀" + position: absolute + top: 10px + right: 7px + 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() - .oe_searchview_facets - &:before - color: #ccc - font-family: "mnmliconsRegular" - content: "r" - font-size: 150% - padding: 0 1px 0 3px - display: inline + .oe_searchview_search + @include text-to-icon("r", #a3a3a3) + @include box-shadow(none) + @include radius(0) + position: absolute + left: 3px + top: 1px + padding: 0 + border: none + background: transparent + .oe_searchview_facets + min-height: 22px + margin-left: 15px * vertical-align: top display: inline-block - line-height: 26px - - .oe_searchview_input, .oe_searchview_facet - height: 26px + line-height: 17px + .oe_searchview_facet + height: 18px + margin: 1px 0 + font-size: 11px &:focus outline: none - .oe_searchview_input - padding: 0 3px - + padding: 0 0 0 6px + font-size: 12px + height: 16px + margin-top: 3px + &:focus + outline: none .oe_searchview_facet position: relative cursor: pointer - border: 1px solid $facets-border - @include radius(3px) - background: #8786b7 + padding: 0 -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) - + border-color: $tag-border-selected + @include box-shadow(0 0 3px 1px $tag-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 - + height: 18px + padding: 0 4px .oe_facet_category color: white text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4) - + .oe_facet_category.oe_i + font-size: 16px .oe_facet_value - border-left: 1px solid $facets-border - + border-left: 1px solid $tag-border + text-shadow: 0 1px 1px white + color: #4C4C4C + &:last-child + padding-right: 16px .oe_facet_remove position: absolute - top: 0 - left: 2px - color: white - + top: 3px + right: 3px + color: #8786b7 + line-height: 8px + width: 12px + height: 12px + padding-top: 1px + text-align: center + font-weight: bold + cursor: pointer + text-shadow: 0 1px 1px white &:hover - color: #ccc - + color: white + background: #8786b7 + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4) + @include radius(2px) &.oe_searchview_open_drawer .oe_searchview_drawer display: block - .oe_searchview_unfold_drawer:before - content: "▼" .oe_searchview_drawer position: absolute - z-index: 1 + z-index: 100 // detach drawer from field slightly - margin-top: 3px + margin-top: 4px top: 100% - right: 0 + right: -1px background-color: white min-width: 100% display: none - border: 1px solid #ccc + border: 1px solid $tag-border text-align: left - padding-bottom: 0.5em - @include radius(1em) - + @include radius(4px) + @include box-shadow(0 1px 4px rgba(0,0,0,0.3)) > div border-top: 1px solid #ccc - margin: 3px 0 + margin: 0 + padding: 8px > div:first-child border-top: none margin: 0 - + h3 + margin: 8px 4px 4px 12px + color: $section-title-color + font-size: 13px h4, h4 * margin: 0 cursor: pointer - + font-weight: normal + display: inline-block + &:hover + background-color: $hover-background h4:before content: "▸ " - - button, .button + color: #a3a3a3 + button + margin: 4px 0 + .button border: none background: transparent padding: 0 2px @include box-shadow(none) @include radius(0) - - .oe_searchview_filters + .oe_searchview_section display: table width: 100% - > div - @include unscrew-box + @include box-sizing(border) display: table-cell width: 50% - - h3 - margin: 2px 4px 2px 8px - color: $section-title-color - ul - margin: 0 12px 3px + margin: 0 8px 8px padding: 0 list-style: none - li list-style: none - padding: 3px 6px 3px 18px + padding: 2px 4px 2px 20px line-height: 14px color: inherit cursor: pointer - - &.oe_selected - background: url(/web/static/src/img/icons/gtk-apply.png) left 2px no-repeat + position: relative + &.oe_selected:before + content: "W" + font-family: "entypoRegular" !important + font-size: 24px + font-weight: 300 !important + color: #a3a3a3 + position: absolute + left: 4px + top: -2px // after oe_selected so background color is not overridden &:hover background-color: $hover-background - - + form + margin-left: 12px + p + margin: 4px 0 + line-height: 18px + button + margin: 0 0 8px 0 .oe_searchview_custom + padding: 0 8px 8px 8px form display: none - - button - @include text-to-icon("S") li cursor: pointer position: relative - line-height: 1.2em - padding: 2px 20px 2px 25px - - &.oe_searchview_custom_private - background: url(/web/static/src/img/icons/terp-locked.png) 5px center no-repeat - + line-height: 14px + padding: 2px 4px 2px 20px &:hover background-color: $hover-background - button position: absolute top: 0 right: 5px - + .oe_searchview_dashboard + form + display: none + margin-top: 2px .oe_searchview_advanced form display: none - + margin-top: 8px button.oe_add_condition:before - content: "⊞ " - button.oe_apply:before - content: "⚡ " - + 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 white-space: nowrap - + &:first-child .searchview_extended_prop_or + visibility: hidden + .searchview_extended_prop_or + opacity: 0.5 + margin-left: -14px .oe_opened h4:before content: "▾ " + position: relative + top: -1px form display: block // delete buttons .oe_searchview_custom_delete, .searchview_extended_delete_prop - @include text-to-icon("d") + 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 + 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 + + // }}} // Views Common {{{ .oe_view_nocontent - > img - float: left - margin: 1.5em - > div - // don't encroach on my arrow - overflow: hidden - padding: 35px 0px 0px 0px - max-width: 700px - font-size: 125% - .oe_view_topbar - border-bottom: 1px solid #cacaca - @include vertical-gradient(#fcfcfc, #dedede) - padding: 0 8px - line-height: 30px + padding: 15px + margin-top: 0 + color: #777777 + font-size: 125% + max-width: 700px + .oe_view_nocontent_create + background: transparent url(/web/static/src/img/view_empty_arrow.png) no-repeat 7px 0 + margin-top: 0 + padding-top: 35px + min-height: 28px + color: #4c4c4c + > p + padding-left: 95px + .oe_empty_custom_dashboard + background: transparent url(/web/static/src/img/graph_background.png) no-repeat 0 0 + margin-top: -15px + padding: 100px 0 0 137px + min-height: 327px + margin-left: -15px + .oe_view.oe_cannot_create + .oe_view_nocontent_create + display: none // }}} - // FormView.body {{{ + // FormView.base and dynamic tags {{{ .oe_formview background: white - .oe_form_header - padding: 8px + .oe_form_dropdown_section + position: relative + display: inline-block .oe_form_invalid input, select, textarea background-color: #F66 !important border: 1px solid #D00 !important - .oe_form_button_save_dirty - color: white - background: #dc5f59 - background: -moz-linear-gradient(#dc5f59, #b33630) - background: -webkit-gradient(linear, left top, left bottom, from(#dc5f59), to(#b33630)) - background: -webkit-linear-gradient(#dc5f59, #b33630) - -moz-box-shadow: none - -webkit-box-shadow: none - -box-shadow: none - font-weight: bold - &:hover - background: #ED6F6A - // }}} - // FormView.customdivs {{{ - .oe_form_topbar - border-bottom: 1px solid #cacaca - @include vertical-gradient(#fcfcfc, #dedede) - padding: 0 8px - line-height: 30px - button - font-size: 12px - height: 24px !important - line-height: 24px - vertical-align: top - padding: 0 10px - margin: 3px 4px 3px 0 - span.i - line-height: 20px - height: 24px - ul - height: 30px + .oe_view_manager_current .oe_form_editable + .oe_highlight + color: #404040 + background: none + button.oe_highlight + @include vertical-gradient(#efefef, #d8d8d8) + @include box-shadow((0 1px 2px rgba(0, 0, 0, .1), 0 1px 1px rgba(255, 255, 255, .8) inset)) + button.oe_highlight:active + @include vertical-gradient(#e3e3e3, #f6f6f6) + @include box-shadow(none) + button.oe_highlight:hover + @include vertical-gradient(#f6f6f6, #e3e3e3) + @include box-shadow((0 1px 2px rgba(0, 0, 0, .1), 0 1px 1px rgba(255, 255, 255, .8) inset)) + .oe_form_invisible + display: none !important + .oe_form_readonly + .oe_edit_only, .oe_form_field:empty + display: none !important + .oe_form .oe_form_field_date + width: auto + // Sheet and padding + .oe_form_nosheet + margin: 16px + > header + margin: -16px -16px 0 -16px padding: 0 - margin: 0 - text-shadow: 0 1px 1px white - border-left: 1px solid #cacaca - border-right: 1px solid #cacaca - li - padding: 0 - margin: 0 - float: left - vertical-align: top - border-right: 1px solid #cacaca - height: 30px - padding: 0 12px - &:first-child - border-left: 1px solid #cacaca - a - color: #4c4c4c - &:hover - color: black - .oe_form_steps - img - margin: 0 8px - vertical-align: top - li - border-right: none - padding: 0 - &:first-child - margin-left: 12px - border-left: none - &:last-child - margin-right: 12px - .oe_form_steps_active - font-weight: bold - color: #b33630 - .oe_form_topbar.oe_form_topbar_hifirst button:first-child, button.oe_form_button_hi - color: white - background: #DC5F59 - background: -moz-linear-gradient(#DC5F59, #B33630) - background: -webkit-gradient(linear, left top, left bottom, from(#DC5F59), to(#B33630)) - background: -webkit-linear-gradient(#DC5F59, #B33630) - -moz-box-shadow: none - -webkit-box-shadow: none - -box-shadow: none - .oe_form_topbar.oe_form_topbar_hifirst button:first-child:hover, button.oe_form_button_hi:hover - background: #DF6B66 - background: -moz-linear-gradient( #DF6B66, #BF3A33) - background: -webkit-gradient(linear, left top, left bottom, from( #DF6B66), to( #BF3A33)) - background: -webkit-linear-gradient( #DF6B66, #BF3A33) - -moz-box-shadow: 0 0 1px rgba(0, 0, 0, 0.2) - -webkit-box-shadow: 0 0 1px rgba(0, 0, 0, 0.2) - -box-shadow: 0 0 1px rgba(0, 0, 0, 0.2) + .oe_form_sheetbg + padding: 16px 0 + .oe_form_sheet_width + min-width: 650px + max-width: $sheet-max-width + margin: 0 auto + .oe_form_sheet + background: white + min-height: 330px + padding: 16px + // Sheet inline mode .oe_application .oe_form_sheetbg background: url(/web/static/src/img/form_sheetbg.png) - padding: 8px 0 - border-bottom: 1px solid #ddd - .oe_form_sheet_width, .oe_form_bottom - min-width: 650px - max-width: 980px - margin: 0 auto + border-bottom: 1px solid #ddd .oe_form_sheet - padding: 8px 8px 8px 8px - background: white - min-height: 420px - border: 1px solid #afafb6 - @include box-shadow(0 0 10px rgba(0,0,0,0.3)) + border: 1px solid #c8c8d3 //$tag-border + @include box-shadow(0 4px 20px rgba(0,0,0,0.15)) + .ui-tabs + margin: 0 -16px + .oe_notebook_page + padding: 0 16px // }}} - // FormView.layout {{{ + // FormView.header {{{ + .oe_form header + position: relative + border-bottom: 1px solid #cacaca + padding-left: 2px + @include vertical-gradient(#fcfcfc, #dedede) + > span + margin-left: 4px + ul + display: inline-block + float: right + .oe_button + margin: 3px 2px 1px + &:first-child + margin-left: 6px + // }}} + // FormView.custom tags and classes {{{ .oe_form - .oe_form_group_cell - line-height: 18px - padding: 2px - height: 20px - .oe_form_group_cell > .oe_form_field_many2one, - .oe_form_group_cell > .oe_form_field_many2one input, - .oe_form_group_cell > .oe_form_field_char input, - .oe_form_group_cell > .oe_form_field_text textarea, - .oe_form_group_cell > .oe_form_field_email, - .oe_form_group_cell > .oe_form_field_email input, - .oe_form_group_cell > .oe_form_field_url, - .oe_form_group_cell > .oe_form_field_url input, - .oe_form_group_cell > .oe_form_field_float input, - .oe_form_group_cell > .oe_form_field_selection select, - .oe_form_group_cell > div > .oe_datepicker_root, - .oe_form_group_cell > div > .oe_datepicker_root > input.oe_datepicker_master + header + .oe_tags + margin: 5px 0 0 5px + width: 400px + padding-bottom: 0 + div.oe_chatter + box-sizing: border-box + min-width: $sheet-min-width + 2* $sheet-padding + max-width: $sheet-max-width + 2* $sheet-padding + margin: 0 auto + padding: 16px 16px 48px + div.oe_form_configuration + p, ul, ol + color: #aaa + max-width: 650px + label + min-width: 150px + .oe_form_group_cell_label + padding: 1px 0 + .oe_form_group_cell div div + padding: 1px 0 + .oe_subtotal_footer + width: 1% !important + td.oe_form_group_cell + text-align: right + padding: 0 !important + td.oe_form_group_cell_label + border-right: none + .oe_subtotal_footer_separator + width: 108px + border-top: 1px solid #cacaca + margin-top: 4px + padding-top: 4px + font-weight: bold + font-size: 18px + label:after + content: ":" + label.oe_subtotal_footer_separator + font-weight: bold !important + padding: 2px 11px 2px 0px !important + label.oe_form_label_help + font-weight: normal + + // }}} + // FormView.group {{{ + .oe_form + .oe_form_button + margin: 2px + td.oe_form_group_cell_label + border-right: 1px solid #ddd + padding: 2px 0px + label + line-height: 18px + display: block + min-width: 140px + td.oe_form_group_cell + .oe_form_group_cell + padding: 2px 0 2px 8px + .oe_form_group width: 100% - .oe_form_group_cell.oe_form_group_nested - padding: 0 + margin: 9px 0 9px 0 + .oe_form_group_cell.oe_group_right + padding-left: 20px // }}} // FormView.label {{{ .oe_form @@ -1314,113 +1708,99 @@ $colour4: #8a89ba top: -4px padding: 0 2px // }}} + // FormView.separator {{{ + .oe_horizontal_border + border-bottom: 1px solid black + .oe_horizontal_separator + font-weight: bold + font-size: 20px + margin: 15px 0px 10px 0px + color: $section-title-color + .oe_horizontal_separator:empty + height: 5px + .oe_vertical_separator + border-left: 1px solid #666 + padding: 0 4px 0 4px + // }}} + // FormView.progressbar {{{ + .oe_form_field_progressbar + display: inline-block + min-width: 70px + .oe_form_field_progressbar.ui-progressbar + height: 22px + font-size: 10px + @include box-sizing(border) + border: 1px solid #999 + @include radius(3px) + background: white + min-width: 50px + span + position: absolute + margin-left: 10px + font-weight: bold + .ui-widget-header + background: #cccccc url(/web/static/lib/jquery.ui/css/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png) 50% 50% repeat-x + // }}} // FormView.fields {{{ .oe_form - textarea - resize: vertical - input[type="text"], - input[type="password"], - input[type="file"], - select + .oe_form_field_text + width: 100% + .oe_form_field_char input, + .oe_form_field_url input, + .oe_form_field_email input, + .oe_form_field_text textarea, + .oe_form_field_selection select + width: 100% + .oe_form_field_text.oe_inline, .oe_form_field_text.oe_inline > textarea + width: 500px + h1, h2, h3, h4, h5, h6 + margin: 0 0 4px 0 + input + height: inherit !important + font-size: inherit + .oe_form_field + width: 100% + display: inline-block + padding: 2px 2px 2px 0px + input + margin: 0px + input[type="text"], input[type="password"], input[type="file"], select height: 22px padding-top: 2px - input[type="text"], - input[type="password"], - input[type="file"], - select, - textarea - -moz-box-sizing: border-box - -webkit-box-sizing: border-box - -ms-box-sizing: border-box - box-sizing: border-box - padding: 0 2px - margin: 0 2px - border: 1px solid #999 - -moz-border-radius: 3px - -webkit-border-radius: 3px - border-radius: 3px + input[type="text"], input[type="password"], input[type="file"], select, textarea + @include box-sizing(border) background: white - min-width: 90px + min-width: 60px color: #1f1f1f - input[readonly], - select[readonly], - textarea[readonly], - input[disabled], - select[disabled], + input[readonly], select[readonly], textarea[readonly], input[disabled], select[disabled] background: #E5E5E5 !important color: #666 textarea[disabled] border: none - border-left: 8px solid #eee padding-left: 8px @include box-shadow(none) @include radius(0px) - .oe_form_field_many2one input, - .oe_form_field_binary input, - .oe_form_field_binary input, - .oe_form_field_email input, - .oe_form_field_url input - border-right: none - -webkit-border-top-right-radius: 0px - -webkit-border-bottom-right-radius: 0px - -moz-border-radius-topright: 0px - -moz-border-radius-bottomright: 0px - border-top-right-radius: 0px - border-bottom-right-radius: 0px - .oe_form_field_email button img, + textarea.oe_inline[disabled] + border-left: 8px solid #eee .oe_form_field_url button img vertical-align: top - .oe_form_field_translatable, - .oe_form_field_many2one, .oe_form_field_date, .oe_form_field_datetime white-space: nowrap .oe_form_field_boolean padding-top: 4px - .oe_form_field_datetime input - min-width: 11em - .oe_form_field_many2manytags - .text-wrap - width: 100% !important - textarea - width: 100% !important - .oe_form_field_many2manytags_box - border-radius: 2px - box-sizing: border-box - position: relative - float: left - border: 1px solid #9DACCC - background: #E2E6F0 - color: black - padding: 0px 3px 0px 3px - margin: 0 2px 2px 0 - cursor: pointer - height: 16px - font: 11px "lucida grande", tahoma, verdana, arial, sans-serif - .text-core .text-wrap .text-dropdown .text-list .text-suggestion em - font-style: italic - text-decoration: none + width: auto .oe_datepicker_container display: none .oe_datepicker_root display: inline-block .oe_form_required - input, select, textarea + input:not([disabled]):not([readonly]), select:not([disabled]):not([readonly]), textarea:not([disabled]):not([readonly]) background-color: #D2D2FF !important .oe_form_invalid input, select, textarea background-color: #F66 !important border: 1px solid #D00 !important - .oe_button.oe_field_button - display: block - -webkit-border-top-left-radius: 0px - -webkit-border-bottom-left-radius: 0px - -moz-border-radius-topleft: 0px - -moz-border-radius-bottomleft: 0px - border-top-left-radius: 0px - border-bottom-left-radius: 0px - margin-right: -1px - height: 22px .oe_input_icon cursor: pointer margin: 3px 0 0 -21px @@ -1432,227 +1812,351 @@ $colour4: #8a89ba filter: alpha(opacity=50) right: 5px top: 3px - // }}} - // FormView.buttons {{{ - .oe_form - .oe_form_button.oe_button - height: 22px - color: #4c4c4c - span + .oe_form_field_with_button.oe_no_button > .oe_button + display: none + .oe_form_field_with_button:not(.oe_no_button) + > .oe_button + float: right + @include radius(0) + border-bottom-left-radius: 0px + height: 22px + .oe_form_field_with_button + input + width: 100% + > div position: relative - vertical-align: top - .oe_form_button > img - vertical-align: -3px - padding: 0 2px - .oe_form_group_cell > .oe_form_button.oe_button - display: block - white-space: nowrap - min-width: 100% - width: 100% + overflow: hidden + .oe_form_embedded_html + position: relative + width: 600px + margin-left: 130px + margin-top: 32px + margin-bottom: 32px + text-align: justify + + .oe_form_editable + .oe_form + .oe_form_field_integer input + width: 6em !important + .oe_form_field_float input + width: 7em !important + .oe_form_field_date input + width: 7.5em !important + .oe_form_field_datetime input + width: 11.5em !important // }}} - // FormView.binaryfile {{{ + // FormView.fields_binary {{{ /* http://www.quirksmode.org/dom/inputfile.html * http://stackoverflow.com/questions/2855589/replace-input-type-file-by-an-image */ - .oe_form - .oe-binary-file-set - overflow: hidden - position: relative - width: 45px - height: 30px - input.oe-binary-file + .oe_hidden_input_file + // Position: relative is used for the hidden input[type=file] + // Do not remove it anymore ! + position: relative + input.oe_form_binary_file z-index: 0 line-height: 0 font-size: 12px position: absolute - // Should be adjusted for all browsers top: 1px - right: 10px + left: 0 + right: 0 opacity: 0 filter: alpha(opacity = 0) -ms-filter: "alpha(opacity=0)" margin: 0 padding: 0 + .oe_form + .oe_form_field_image + padding: 0 + position: relative + display: inline-block + width: auto + vertical-align: top + .oe_form_field_image_controls + position: absolute + top: 1px + padding: 4px + width: 100% + display: none + text-align: center + color: #eee + background: rgba(37,37,37,0.9) + @include radius(3px 3px 0 0) + @include box-sizing(border) + &:hover .oe_form_field_image_controls + display: block // }}} - // FormView.separator {{{ - .oe_horizontal_border - border-bottom: 1px solid black - .oe_horizontal_separator - font-weight: bold - font-size: 16px - margin: 4px 0 - .oe_horizontal_separator:empty - height: 5px - .oe_vertical_separator - border-left: 1px solid #666 - padding: 0 4px 0 4px + // FormView.many2one {{{ + .oe_form_field_many2one + td:first-child + position: relative + span.oe_m2o_drop_down_button + position: absolute + top: 2px + right: 0px + .oe_m2o_cm_button + line-height: 14px + float: right + padding-left: 2px + &.ui-autocomplete + li.oe_m2o_dropdown_option a + font-style: italic + padding-left: 2em + li:not(.oe_m2o_dropdown_option) + li.oe_m2o_dropdown_option + margin-top: 10px // }}} - // FormView.notebook {{{ - .oe_form_notebook - margin: 8px 0 - padding: 0 8px - list-style: none - zoom: 1 - .oe_form_notebook:before, .oe_form_notebook:after - display: table - content: "" - zoom: 1 - .oe_form_notebook:after - clear: both - .oe_form_notebook > li - float: left - .oe_form_notebook > li > a - display: block - color: #4c4c4c - .oe_form_notebook - border-color: #ddd - border-style: solid - border-width: 0 0 1px - .oe_form_notebook > li - position: relative - margin-bottom: -1px - .oe_form_notebook > li > a - padding: 0 12px - margin-right: 2px - line-height: 30px - border: 1px solid transparent - @include radius(4px 4px 0 0) - .oe_form_notebook > li > a:hover - text-decoration: none - background-color: #eee - border-color: #eee #eee #ddd - .oe_form_notebook > li.ui-state-active > a, .oe_form_notebook > li.ui-state-active > a:hover - background-color: #ffffff - border: 1px solid #ddd - border-bottom-color: transparent - cursor: default - .oe_form_notebook_page - padding: 0 - .ui-tabs-hide - display: none - + // FormView.fieldstatus {{{ + ul.oe_form_status, ul.oe_form_status_clickable + display: inline-block + margin: 0 + padding: 0 18px 0 0 + li + display: inline-block + list-style-type: none + // It is probably a bad idea to modify this margin + margin: 0 -18px 0 0 + padding: 0 + @include vertical-gradient(#fcfcfc, #dedede) + &:first-child .label + border-left: 1px solid #cacaca + padding-left: 14px + &:last-child + border-right: 1px solid #cacaca + .label + padding-right: 14px + .arrow + display: none + .label + color: #4c4c4c + text-shadow: 0 1px 1px #fcfcfc, 0 -1px 1px #dedede + padding: 7px + display: inline-block + padding-left: 24px + margin: 0 + position: relative + .arrow + width: 17px + display: inline-block + vertical-align: top + overflow: hidden + margin-left: -5px + span + position: relative + width: 24px + height: 24px + display: inline-block + margin-left: -12px + margin-top: 3px + box-shadow: -1px 1px 2px rgba(255,255,255,0.2), inset -1px 1px 1px rgba(0,0,0,0.2) + @include skew-gradient(#fcfcfc, #dedede) + @include radius(3px) + @include transform(rotate(45deg)) + li.oe_active + @include vertical-gradient(#729fcf, #3465a4) + .arrow span + @include skew-gradient(#729fcf, #3465a4) + .label + color: white + text-shadow: 0 1px 1px #729fcf, 0 -1px 1px #3465a4 + ul.oe_form_status_clickable + li + cursor: pointer + &:hover + @include vertical-gradient(darken(#fcfcfc, 8%), darken(#dedede, 8%)) + .label + text-shadow: 0 -1px 1px #fcfcfc, 0 1px 1px #dedede + .arrow span + @include vertical-gradient(darken(#fcfcfc, 8%), darken(#dedede, 8%)) + .label + color: $link-color + li.oe_active + &:hover + @include vertical-gradient(darken(#729fcf, 10%), darken(#3465a4, 10%)) + .label + text-shadow: 0 -1px 1px #729fcf, 0 1px 1px #3465a4 + .arrow span + @include skew-gradient(darken(#729fcf, 10%), darken(#3465a4, 10%)) // }}} - // FormView.progressbar {{{ - .oe_form - .oe_form_field_progressbar.ui-progressbar - height: 22px - font-size: 10px - -moz-box-sizing: border-box - -webkit-box-sizing: border-box - -ms-box-sizing: border-box - box-sizing: border-box - border: 1px solid #999 - -moz-border-radius: 3px - -webkit-border-radius: 3px - border-radius: 3px - background: white - min-width: 90px - - span - position: absolute - margin-left: 10px - font-weight: bold + // FormView.one2many {{{ + .oe_form .oe_form_field_one2many > .oe_view_manager + .oe_list_pager_single_page + display: none + .oe_form_field_one2many,.oe_form_field_many2many + // TODO: oe_form_field_one2many_list? + > .oe_view_manager + .oe_list_pager_single_page + display: none !important + .oe_view_manager_view_list + min-height: 132px + + .oe_form_field_one2many_list_row_add + font-weight: bold + .oe_list_content + > thead + border-bottom: 1px + > tbody + tr:nth-child(odd) + background: transparent + .oe_list .oe_list_edit_row_save + background: url(/web/static/src/img/iconset-b-remove.png) 50% 50% no-repeat + &:before + visibility: hidden - .ui-widget-header - background: #cccccc url(/web/static/lib/jquery.ui/css/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png) 50% 50% repeat-x - // }}} - // FormView.viewmanager {{{ - .oe_form - .oe_view_manager_header2 - td - padding: 0px 8px - line-height: 16px - .oe_i - font-size: 13px - .oe_pager_group - height: auto + &> .oe_view_manager + .oe_header_row_top + display: none + .oe_view_manager_header2 + td + padding: 0px 8px line-height: 16px - li + .oe_i + font-size: 13px + .oe_pager_group height: auto line-height: 16px - .oe-select-create-popup-view-form > .oe_formview > .oe_form_pager - display: none - + li + height: auto + line-height: 16px + .oe_list_buttons.oe_editing .oe_list_save + // keep "save row" button hidden in o2m + visibility: hidden + .oe_form_editable + .oe_list_editable + .oe_list_content + td.oe_required + background-color: #d2d2ff + td.oe_readonly + background-color: #eee + + $row-height: 27px + .oe_list_editable + .oe_list_content + td.oe_list_field_cell + padding: 4px 6px 3px 6px + .oe_list.oe_list_editable.oe_editing + .oe_edition .oe_list_field_cell:not(.oe_readonly) + * + visibility: hidden + color: transparent + text-shadow: none + .oe_m2o_drop_down_button + top: 5px + .oe_m2o_cm_button + display: none + .oe_form_field + input, textarea + height: $row-height + input, textarea + @include radius(0) + border: 1px solid #aaf + margin: 0 + &.oe_form_field_float,&.oe_form_view_integer + input + text-align: right + width: 100% !important + &.oe_form_field_datetime,&.oe_form_field_date + > span + width: 100% !important + input.oe_datepicker_master + width: 100% !important + .oe_form_field_float,.oe_form_view_integer,&.oe_form_field_datetime,&.oe_form_field_date + min-width: 0 !important + max-width: none !important + .oe_list_group_name + white-space: nowrap // }}} - // FormView.classes for openerp views {{{ - .oe_form - .oe_form_title, .oe_form_title input - font-size: 20px - font-weight: bold - height: 30px - .oe_form_group_odd_border > tbody > tr.oe_form_group_row > td.oe_form_group_cell:nth-child(odd), - .oe_form_group_label_border > tbody > tr.oe_form_group_row > td.oe_form_group_cell_label - border-right: 1px solid #ddd - label - display: block - min-width: 120px - .oe_form_group_odd_border > tbody > tr.oe_form_group_row > td.oe_form_group_cell, - .oe_form_group_label_border > tbody > tr.oe_form_group_row > td.oe_form_group_cell - padding-left: 8px - .oe_form_subtotal_footer - float: right - > div - text-align: right - padding: 3px - label - font-weight: normal !important - span - display: inline-block - min-width: 80px - text-align: right - .oe_form_subtotal_footer_separator - border-top: 1px solid #cacaca - font-size: 120% - font-weight: bold - label - font-weight: bold !important + // FormView.many2many {{{ + .oe_form .oe_form_field_many2many > .oe_list + .oe_list_pager_single_page + display: none // }}} // ListView {{{ - .oe-listview-content + .oe_list_buttons + .oe_alternative + visibility: hidden + .oe_list_save, .oe_list_discard + display: none + &.oe_editing + .oe_list_add + display: none + .oe_list_save + display: inline-block + .oe_list_discard + display: inline + .oe_alternative + visibility: visible + + .oe_list + &.oe_cannot_edit + .oe_list_header_handle, .oe_list_field_handle + display: none !important + padding: 0 !important + &.oe_cannot_delete + .oe_list_record_delete + display: none !important + + .oe_form + .oe_form_nosheet + margin: 0 // FIXME: either class or border should not be by default + padding: 0 + border: none + .oe_form_field + width: auto + position: absolute + margin: 0 !important // dammit + padding: 0 + .oe_list_content .oe_group_header + @include vertical-gradient(#fcfcfc, #dedede) + + .oe_list_content width: 100% + td:first-child, th:first-child + &:after + border-width: 0 + td.oe_number + text-align: right !important + max-width: 100px > thead border-bottom: 2px solid #cacaca background: #eee vertical-align: top - td, th - padding: 3px 6px - line-height: 18px - th:after - content: "" - float: right - margin-top: 7px - border-width: 0 4px 4px - border-style: solid - border-color: #000 transparent - visibility: hidden - th.sortup:after - visibility: visible - @include opacity(0.6) - th.sortdown:after - border-bottom: none - border-left: 4px solid transparent - border-right: 4px solid transparent - border-top: 4px solid #000 - visibility: visible - @include box-shadow(none) - @include opacity(0.6) - // TODO mit - //th:hover:after - // border-bottom: none - // border-left: 4px solid transparent - // border-right: 4px solid transparent - // border-top: 4px solid #000 - // visibility: visible - // @include box-shadow(none) - // @include opacity(0.6) + td, th + padding: 3px 6px + line-height: 18px + th.oe_sortable, th.oe_sortable div + cursor: pointer + th.oe_sortable div + position: relative + th.oe_sortable div:after + position: absolute + right: 6px + content: "" + margin-top: 7px + border-width: 0 4px 4px + border-style: solid + border-color: #000 transparent + visibility: hidden + th.sortup div:after + visibility: visible + @include opacity(0.6) + th.sortdown div:after + border-bottom: none + border-left: 4px solid transparent + border-right: 4px solid transparent + border-top: 4px solid #000 + visibility: visible + @include box-shadow(none) + @include opacity(0.6) > tbody cursor: pointer - background: white > tr + height: $row-height border-top: 1px solid #ddd - > td, > th + > td.oe_list_field_cell padding: 3px 6px - line-height: 18px + white-space: pre-line + > td, > th > button border: none background: transparent @@ -1662,84 +2166,85 @@ $colour4: #8a89ba width: 17px &:after border-width: 0 - > td.oe-number - text-align: right !important > tr:nth-child(odd) background-color: #f0f0fa @include vertical-gradient(#f0f0fa, #eeeef6) + > tfoot border-top: 2px solid #cacaca border-bottom: 1px solid #cacaca background: #eee font-weight: bold - > tbody tr:hover td, tbody tr:hover th - @include vertical-gradient(#eee, #dedede) .numeric text-align: right width: 82px input text-align: right + th.oe_list_header_handle + font-size: 1px + overflow: hidden + text-indent: -9001px + td.oe_list_field_handle + width: 1em + padding: 0 !important + cursor: ns-resize + .oe_list_handle + @include text-to-entypo-icon("}",#E0E0E0,18px) + margin-right: 7px + .oe_list_field_progressbar progress + width: 100% // }}} - // KanbanView {{{ - .oe_kanban_group_title - margin: 1px 1px 4px - font-size: 15px - font-weight: bold - text-shadow: 0 1px 0 white - .oe_kanban_column, .oe_kanban_group_header - width: 240px - vertical-align: top - padding: 6px 7px 6px 6px - background: #f0eeee - border-left: 1px solid #f0f8f8 - border-right: 1px solid #b9b9b9 - .oe_kanban_record - position: relative - display: block - min-height: 50px - margin: 6px 0 - display: block - @include radius(4px) - &:last-child - margin-bottom: 0 - .oe_kanban_title - font-weight: bold - margin: 2px 4px - .oe_kanban_gravatar - width: 20px - height: 20px - @include radius(3px) - @include box-shadow(0 1px 2px rgba(0,0,0,0.2)) - .oe_kanban_avatar_toto - height: 40px - width: 40px - border: 1px solid - border-color: #e5e5e5 #dbdbdb #d2d2d2 - @include radius(3px) - @include box-shadow(0 1px 2px rgba(0,0,0,0.2)) - // }}} - // Translation {{{ - .oe_trad_field.touched - border: 1px solid green !important - // }}} - // Tooltips {{{ - &.oe_tooltip - font-size: 12px - .oe_tooltip_string - color: #FD5 - font-weight: bold - font-size: 13px - .oe_tooltip_help - white-space: pre-wrap - .oe_tooltip_technical - padding: 0 0 4px 0 - margin: 5px 0 0 15px - li - list-style: circle - .oe_tooltip_technical_title + // Tree view {{{ + .tree_header + background-color: #f0f0f0 + border-bottom: 1px solid #cacaca + color: #4c4c4c + padding: 5px + height: 25px + button + float: right + height: 27px + margin-right: 5px + .oe-treeview-table + width: 100% + background-color: white + border-spacing: 0 + color: #4c4c4c + th + padding: 10px font-weight: bold + background-color: #f0f0f0 + border-bottom: 2px solid #cacaca + + td + cursor: pointer + vertical-align: middle + text-align: left + vertical-align: middle + height: 20px + padding-left: 4px + padding-right: 4px + border-right: 1px solid #e7e7e7 + + td.oe_number + text-align: right !important + tr + border-bottom: 1px solid #d6d6d6 + &:hover + background-color: #e7e7e7 + span + font-size: 90% + font-weight: normal + white-space: nowrap + display: block + .treeview-tr.oe-treeview-first + background: transparent url(/web/static/src/img/expand.gif) 0 50% no-repeat + .oe_open .treeview-tr.oe-treeview-first + background-image: url(/web/static/src/img/collapse.gif) + .treeview-tr.oe-treeview-first span, .treeview-td.oe-treeview-first span + margin-left: 16px // }}} // Debugging stuff {{{ .oe_layout_debugging @@ -1758,7 +2263,6 @@ $colour4: #8a89ba float: right .oe_form_group_row_incomplete.oe_form_group_row_newline > td:last-child:after content: "[newline]" - .oe_debug_view float: left .oe_debug_view_log @@ -1777,16 +2281,135 @@ $colour4: #8a89ba color: #333 // }}} -// Transitional overrides for old styles {{{ .openerp - .oe_form_field_many2one - td:first-child + .oe_fileupload + display: inline-block + clear: both + width: 100% + .oe_add + float: left position: relative - span.oe-m2o-drop-down-button - position: absolute - top: 3px - right: 2px + width: 100% + left: +2px + top: +7px + button + display: inline + height: 24px + font-size: 12px + line-height: 12px + vertical-align: middle + button.oe_attach + width: 24px + overflow: hidden + width: 24px + overflow: hidden + background: transparent + color: #7C7BAD + box-shadow: none + border: none + text-shadow: none + .oe_e + position: relative + top: -1px + left: -9px + input.oe_form_binary_file + display: inline-block + margin-left: -5px + height: 28px + width: 52px + margin-top: -26px + .oe_attach_label + color: #7C7BAD + margin-left: -3px + .oe_attachments + margin-bottom: 4px + margin-right: 0px + font-size: 12px + border-radius: 2px + border: solid 1px rgba(124,123,173,0.14) + .oe_attachment + padding: 2px + padding-left: 4px + padding-right: 4px + .oe_e + font-size: 23px + margin-top: -5px + .oe_e:hover + text-decoration: none + .oe_attachment:nth-child(odd) + background: white + .oe_attachment:nth-child(even) + background: #F4F5FA + +// Kitten Mode {{{ +.kitten-mode-activated + background-image: url(http://placekitten.com/g/1365/769) + background-size: cover + background-attachment: fixed + >* + opacity: 0.70 // }}} +// The jQuery-ui overlay and Autocomplete are outside the .openerp div, please don't add indentation !!! +div.ui-widget-overlay + background: black + @include opacity(0.3) + +.ui-widget + font-family: "Lucida Grande", Helvetica, Verdana, Arial, sans-serif + color: #4c4c4c + font-size: 13px + +.ui-menu + padding: 2px 0 + @include box-shadow(0 1px 4px rgba(0,0,0,0.3)) + margin-top: 4px + border: 1px solid $tag-border + .ui-menu-item + width: 100% + padding: 0 + a + padding: 1px 16px + a.ui-corner-all + @include radius(0) + a.ui-state-active + background: #f0f0fa + .ui-menu-item a.ui-state-hover, .ui-menu-item a.ui-state-active + background: $tag-bg-dark + //color: #4c4c4c + //@include vertical-gradient(#f0f0fa, #eeeef6) + +.ui-corner-all + @include radius(3px) + +@media print + .openerp + .oe_header_row, ul.oe_header, div.oe_mail_thread_action, .oe_mail_recthread_actions, .oe_button_box, .oe_form button, button.oe_invite, .oe_form header, .openerp .oe_notebook > li.ui-state-default + display: none + .oe_list_content + button, input[type=checkbox] + visibility: hidden + .tree_header button, .oe_mail .oe_mail_thread_msg .oe_mail_unread, .oe_mail_fetch_more, .oe_m2o_drop_down_button img, .oe_form_field_one2many_list_row_add + visibility: hidden + a + &.oe_m2o_cm_button, &.oe_e + visibility: hidden + .oe_form + .oe_form_field_date img, .oe_form_field_datetime img + visibility: hidden + .oe_notebook > li.ui-tabs-selected + display: block + .oe_application + .oe_form_sheet, .oe_form_sheetbg + border: 0px !important + box-shadow: 0px 0px 0px + .oe_view_manager_current > .oe_view_manager_header + border: 0px !important + box-shadow: 0px 0px 0px + text-shadow: none + .text-core .text-wrap .text-arrow + background: none + .openerp div.oe_mail_wall + overflow: hidden !important // au BufWritePost,FileWritePost *.sass :!sass --style expanded --line-numbers <afile> > "%:p:r.css" // vim:tabstop=4:shiftwidth=4:softtabstop=4:fdm=marker: diff --git a/addons/web/static/src/css/base_old.css b/addons/web/static/src/css/base_old.css deleted file mode 100644 index 74b660f7613..00000000000 --- a/addons/web/static/src/css/base_old.css +++ /dev/null @@ -1,999 +0,0 @@ -.openerp { - padding: 0; - margin: 0; - font-size: 80%; - font-family: Ubuntu, Helvetica, sans-serif; -} - -.openerp, .openerp textarea, .openerp input, .openerp select, .openerp option, -.openerp button, .openerp .ui-widget { - font-family: Ubuntu, Helvetica, sans-serif; - font-size:85%; -} - -.openerp .view-manager-main-content { - width: 100%; -} - -.openerp .oe_form_group_cell .view-manager-main-content { - padding: 0; -} - -.oe_box { - border: 1px solid #aaf; - padding: 2px; - margin: 2px; -} - - -#oe_errors pre { - margin: 0; -} - -.openerp .oe-listview .oe-number { - text-align: right !important; -} -.oe-listview-header-columns { - background: #d1d1d1; /* Old browsers */ - background: -moz-linear-gradient(top, #ffffff 0%, #d1d1d1 100%); /* FF3.6+ */ - background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ffffff), color-stop(100%,#d1d1d1)); /* Chrome,Safari4+ */ - background: -webkit-linear-gradient(top, #ffffff 0%,#d1d1d1 100%); /* Chrome10+,Safari5.1+ */ - background: -o-linear-gradient(top, #ffffff 0%,#d1d1d1 100%); /* Opera11.10+ */ - background: -ms-linear-gradient(top, #ffffff 0%,#d1d1d1 100%); /* IE10+ */ - filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#FFFFFF', endColorstr='#d1d1d1',GradientType=0 ); /* IE6-9 */ - background: linear-gradient(top, #ffffff 0%,#d1d1d1 100%); /* W3C */ -} - -.openerp .oe_hide { - display: none !important; -} - -/* STATES */ -.openerp .on_logged, -.openerp .db_options_row { - display: none; -} - - - -/* Database */ -.login .oe-database-manager { - display: none; - height: 100%; - width: 100%; - background-color: white; -} -.login.database_block .bottom, -.login.database_block .login_error_message, -.login.database_block .pane { - display: none; -} -.login.database_block .oe-database-manager { - display: block; -} - -.login .database { - float: left; - width: 202px; - height: 100%; - background: #666666; -} -.login .oe_db_options { - margin-left: 202px; - color: black; - padding-top: 20px; -} - -.login .database ul { - margin-top: 65px; -} - -ul.db_options li { - padding: 5px 0 10px 5px; - background: #949292; /* Old browsers */ - background: -moz-linear-gradient(top, #949292 30%, #6d6b6b 95%, #282828 100%); /* FF3.6+ */ - background: -webkit-gradient(linear, left top, left bottom, color-stop(30%,#949292), color-stop(95%,#6d6b6b), color-stop(100%,#282828)); /* Chrome,Safari4+ */ - background: -webkit-linear-gradient(top, #949292 30%,#6d6b6b 95%,#282828 100%); /* Chrome10+,Safari5.1+ */ - background: -o-linear-gradient(top, #949292 30%,#6d6b6b 95%,#282828 100%); /* Opera11.10+ */ - background: -ms-linear-gradient(top, #949292 30%,#6d6b6b 95%,#282828 100%); /* IE10+ */ - filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#949292', endColorstr='#282828',GradientType=0 ); /* IE6-9 */ - background: linear-gradient(top, #949292 30%,#6d6b6b 95%,#282828 100%); /* W3C */ - /* for ie9 */ - filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#949292', endColorstr='#5B5A5A',GradientType=0 ); /* IE6-9 */ - border: none; - /* overriding jquery ui */ - -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; - display: block; - font-weight: bold; - text-transform: uppercase; - margin: 1px; - color: #EEEEEE; - cursor: pointer; - width: 195px; - font-size: 12px; -} - -.db_option_table { - border: 1px solid #5A5858; - padding: 5px; - -moz-border-radius: 10px; -} - -table.db_option_table input.required { - background-color: #D2D2FF !important; -} - -.db_option_table label { - display: block; - text-align: right; -} - -.db_option_table input[type="text"], -.db_option_table input[type="password"], -.db_option_table input[type="file"], -.db_option_table select { - width: 300px; -} - -.option_string { - font-weight: bold; - color: #555; - width: 100%; - text-align: center; - padding: 10px 0; - font-size: large; -} - -label.error { - float: none; - color: red; - padding-left: .5em; - vertical-align: top; -} - -/* Main*/ -.openerp .main_table { - width: 100%; - height: 100%; - background: #f0eeee; -} -.openerp .oe-application { - height: 100%; -} -.openerp .oe-application-container { - width: 100%; - height: 100%; -} - -/* IE Hack - for IE < 9 - * Avoids footer to be placed statically at 100% cutting the middle of the views - * */ -.openerp .oe-application-container { - height: auto\9; - min-height: 100%\9; -} - - -/* Main Application */ -.openerp .oe-main-content { - padding: 0; - height: 100%; -} - -.openerp h2.oe_view_title { - font-size: 110%; - font-weight: normal; - margin: 2px 0; - color: #252424; - text-shadow: white 0 1px 0; -} -.openerp div[id^="notebook"] .oe_view_title { - font-size:85%; - padding-bottom:4px; -} - -/* List */ -.openerp table.oe-listview-content { - clear: right; - width: 100%; - border-spacing: 0; - border: 1px solid silver; -} - -.openerp .oe-listview thead table { - width: 100%; - border: none; -} -.openerp .oe-listview tr.odd { - background-color: #f3f3f3; -} -.openerp .oe-listview tbody tr:hover { - background-color: #ecebf2; -} -.openerp .oe-listview tbody tr:hover { - background-color: #eae9f0; -} -.openerp .oe-listview thead table tr, -.openerp .oe-listview thead table tr:hover { - background: none; -} - -.openerp .oe-listview > table > tbody > tr > td, -.openerp .oe-listview th { - vertical-align: middle; - text-align: left; - padding: 1px 2px; -} - -.openerp .oe-record-delete button, -.openerp button.oe-edit-row-save { - border: none; - height: 12px; - width: 12px; - background: url("/web/static/src/img/iconset-b-remove.png") no-repeat scroll center center transparent; - cursor: pointer; -} -.openerp button.oe-edit-row-save { - background-image: url('/web/static/src/img/icons/save-document.png'); -} - -/* Could use :not selectors if they were supported by MSIE8... */ -.openerp .oe-listview > table > tbody > tr > td { - border-left: 1px solid #dadada; /*currently commenting to test with no vertical lines in list view*/ -} -.openerp .oe-listview tbody td:first-child, -.openerp .oe-listview tbody td.oe-button, -.openerp .oe-listview tbody td.oe-button, -.openerp .oe-listview tbody th.oe-record-selector, -.openerp .oe-listview tbody td.oe-record-delete { - border-left: none; -} - -.openerp .oe-listview td.oe-record-delete { - text-align: right; -} -.openerp .oe-listview th.oe-sortable { - cursor: pointer; - font-size: 75%; - text-transform: uppercase; - padding: 0; - margin: 0; - padding-left: 3px; - color: #333; -} -.openerp .oe-listview th.oe-sortable .ui-icon { - height: 60%; - margin: -6px 0 0; - display: inline; - display: inline-block; - vertical-align: middle; -} - -.openerp .oe-listview > table > tbody > tr > td { - border-bottom: 1px solid #E3E3E3; -} - - -.openerp .oe-listview td.oe-actions { - border-bottom:none; -} - -.openerp .oe-listview .oe-record-selector, .openerp .oe-listview .oe-record-edit-link { - border-bottom: 1px solid #E3E3E3; -} -.openerp .oe-listview .oe-record-edit-link { - cursor: pointer; -} - -.openerp .oe-listview .oe-field-cell { - cursor: pointer; - margin-top: 0; - margin-bottom: 0; - padding-top: 3px; - padding-bottom: 3px; - font-size: 80%; -} -.openerp .oe-listview .oe-field-cell progress { - width: 100%; -} -.openerp .oe-listview .oe-field-cell.oe-button button, -.openerp .oe-listview .oe_form_button button { - margin: 0; - padding: 0; - border: none; - background: none; - width: 16px; - box-shadow: none; - -moz-box-shadow: none; - -webkit-box-shadow: none; -} -.openerp .oe-listview .oe-field-cell button:active { - opacity: 0.5; -} -.openerp .oe-listview .oe-field-cell button img { - cursor: pointer; -} -.openerp .oe-listview .oe-field-cell button img:hover { - opacity: 0.75; -} - -.openerp .oe-listview .oe-field-cell .oe-listview-button-disabled img { - opacity: 0.5; -} - -.openerp .oe-listview th.oe-actions { - text-align: left; - white-space: nowrap; -} -.openerp .oe-listview th.oe-list-pager { - text-align: right; - white-space: nowrap; -} -.openerp .oe-list-pager .oe-pager-state { - cursor: pointer; - font-size: 90%; - color: #555; -} - -.openerp .oe_button.oe_button_pager, -.openerp .oe-list-pager > span, -.openerp .oe_form_pager > span { - line-height: 17px; - height: 17px; - cursor: pointer; - color: gray; - font-weight: bold; - vertical-align: middle; -} -.openerp .oe_button.oe_button_pager, -.openerp .oe_button.oe_button_pager:disabled { - padding: 0 3px 0 3px; - margin: 0; - height: 17px; -} -.openerp .oe-listview .oe-group-name { - padding-right: 1em; -} -.openerp .oe-listview .oe-group-name, -.openerp .oe-listview .oe-group-pagination { - white-space: nowrap; -} - -.openerp .oe-listview tfoot td { - padding: 3px 3px 0; -} -.openerp .oe-listview .oe-list-footer { - text-align: center; - white-space: nowrap; - color: #444; - font-size: 85%; -} -.openerp .oe-listview .oe-list-footer span { - margin: 0 1em; -} -.openerp .oe-listview .oe-list-footer progress { - vertical-align:-10% !important; - width: 100%; -} - -/** list rounded corners - - rounded corners are a pain on tables: need to round not only table, but - also on the first and last children of the first and last row - */ -.openerp .oe-listview table.oe-listview-content { - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} -.openerp .oe-listview table.oe-listview-content thead tr:first-child th:first-child { - -webkit-border-top-left-radius: 4px; - -moz-border-radius-topleft: 4px; - border-top-left-radius: 4px; -} -.openerp .oe-listview table.oe-listview-content thead tr:first-child th:last-child { - -webkit-border-top-right-radius: 4px; - -moz-border-radius-topright: 4px; - border-top-right-radius: 4px; -} -.openerp .oe-listview table.oe-listview-content tfoot tr:last-child th:first-child, -.openerp .oe-listview table.oe-listview-content tfoot tr:last-child td:first-child, -.openerp .oe-listview table.oe-listview-content tbody:last-child tr:last-child th:first-child { - -webkit-border-bottom-left-radius: 4px; - -moz-border-radius-bottomleft: 4px; - border-bottom-left-radius: 4px; -} -.openerp .oe-listview table.oe-listview-content tfoot tr:last-child th:last-child, -.openerp .oe-listview table.oe-listview-content tfoot tr:last-child td:last-child, -.openerp .oe-listview table.oe-listview-content tbody:last-child tr:last-child td:last-child { - -webkit-border-bottom-right-radius: 4px; - -moz-border-radius-bottomright: 4px; - border-bottom-right-radius: 4px; -} - -/* Form */ -.openerp .oe_form_group_cell input[type="checkbox"] { - margin-top: 3px; - vertical-align: middle; -} -.openerp .oe_form_group_cell .input[type="text"] { - padding-bottom: 1px; -} - -.openerp table.oe_form_group td { - color: #4c4c4c; -} -.openerp table.oe_form_group { - color: #4c4c4c; -} -.openerp fieldset.oe_group_box { - border: 1px solid #AAAAAA; - moz-border-radius: 4px; - -webkit-border-radius: 4px; - border-radius: 4px; - background: #F9F9F9; - padding: 4px; -} -.openerp fieldset.oe_group_box legend { - font-weight: bold; -} -.openerp .required.error { - border: 1px solid #900; -} -.openerp .oe_form_buttons, .openerp .oe_list_buttons { - float: left; -} -.openerp .oe_form_pager, .openerp .oe_list_pager { - float: right; - font-size: 80%; - color: gray; - font-weight: bold; -} - -.openerp .oe_form_pager { - margin-right: 3px; -} - -.openerp .oe_align_left { - text-align: left; -} -.openerp .oe_align_right { - text-align: right; -} -.openerp .oe_align_center { - text-align: center; -} -.openerp .oe_form .oe_form_paragraph { - margin: 3px 0 0 0; - white-space: normal; -} - -.openerp .oe_form .oe_form_paragraph.oe_multilines { - white-space: pre; -} - -.openerp .oe_form_field_one2many .oe-actions h3.oe_view_title, -.openerp .oe_form_field_one2many_list .oe-actions h3.oe_view_title{ - display: inline; - margin: 0 0.5em 0 0; -} - -.openerp .oe_form .oe-listview th.oe-sortable .ui-icon, -.openerp .oe_form .oe-listview th.oe-sortable .ui-icon { - height: 100%; - margin-top: -9px; -} - -.openerp table.oe_form_group .oe-listview-content td { - color: inherit; -} - -/* Uneditable Form View */ -.openerp .oe_form_readonly { - -} -.openerp .oe_form_readonly .oe_form_group_cell .field_text, -.openerp .oe_form_readonly .field_char, -.openerp .oe_form_readonly .field_int, -.openerp .oe_form_readonly .field_float, -.openerp .oe_form_readonly .field_email, -.openerp .oe_form_readonly .field_date, -.openerp .oe_form_readonly .field_selection, -.openerp .oe_form_readonly .oe_form_field_many2one { - padding: 3px 2px 2px 2px; - background-color: white; - height: 17px; -} -.openerp .oe_form_readonly .oe_form_group_cell .field_text { - height: auto; -} -.openerp .oe_form_readonly .field_datetime { - padding: 1px 2px 2px 2px; - background-color: white; - height:19px; -} -.openerp .oe_form_readonly .oe_form_field_many2one div { - background-color:white; - height:18px; - margin-bottom:1px; - padding: 0px 2px 5px 2px; -} - -.openerp .oe_form_readonly .oe_form_field_email div { - background-color: white; - padding: 1px 2px 3px 2px; -} - - -.openerp .oe_form_readonly .oe_form_field_text div.field_text, -.openerp .oe_form_readonly .oe_form_field_text_html div.field_text_html { - white-space: pre-wrap; -} -.openerp .oe_form_readonly .oe_form_group_cell .field_text { - min-height:100px; -} -/* Inputs */ -/* vertically recentering filter management select tag */ -.openerp select.oe_search-view-filters-management { - margin-top:2px; -} - - -@-moz-document url-prefix() { - /* Strange firefox behaviour on width: 100% + white-space: nowrap */ - .openerp .oe_form .oe_form_button .oe_button { - width: auto; - } -} -/* IE Hack - for IE < 9 - * Avoids buttons overflow - * */ -.openerp .oe_form .oe_form_button .oe_button { - min-width: auto\9; -} - - -/* jQuery UI override */ -.openerp .ui-widget { - font-size: 1em; -} -.openerp tbody.ui-widget-content { - margin-bottom: 10px; - border-spacing: 4px; -} -.openerp .ui-widget-header { - background: white none; -} - -/* Sidebar */ -.openerp .view-manager-main-table { - margin: 0; - width:100%; - border-collapse:collapse; - height:100%; -} - -.openerp .view-manager-main-table tbody { - vertical-align: top; -} - -.openerp .oe-view-manager-header { - overflow: auto; - background: url("/web/static/src/img/sep-a.gif") 0 100% repeat-x; - margin:6px 0 6px 2px; -} -.openerp .oe_form_group_cell .oe-view-manager-header { /* Trick: remove the background when element is in a formular */ - background: none; -} - -.openerp .oe-view-manager-header h2 { - float: left; -} - -.openerp .oe_view_manager_menu_tips blockquote { - display: none; - font-size: 85%; - margin: 0; - background: #fff; - border-bottom: 1px solid #CECBCB; - padding: 1px 10px; - color: #4C4C4C; -} -.openerp .oe_view_manager_menu_tips blockquote p { - margin: 0; - padding: 6px 1px 4px; -} - -.openerp .oe_view_manager_menu_tips blockquote div { - text-align: right; - margin-right:10px; -} - -.openerp .oe_view_manager_menu_tips blockquote div button { - border: none; - background: none; - padding: 0 4px; - margin: 0; - display: inline; - text-decoration: underline; - color: inherit; -} - -.openerp .view-manager-main-sidebar { - width: 180px; - padding: 0; - margin: 0; -} - -.openerp .sidebar-main-div { - height: 100%; - border-left: 1px solid #D2CFCF; -} - -.openerp .sidebar-content { - padding: 0; - margin: 0; - width: 180px; - height: 100%; - font-size: 0.9em; -} - -.openerp .closed-sidebar .sidebar-content { - width: 22px; -} - -.openerp .closed-sidebar .sidebar-content { - display: none; -} - -.openerp .sidebar-main-div a { - color: #555; - text-decoration: none; -} - -.openerp .sidebar-main-div a:hover { - color: black; -} - -.openerp .oe-sidebar-attachments-toolbar { - margin: 4px 0 0 4px; -} -.openerp .oe-sidebar-attachments-items { - clear: both; - padding-top: 5px !important; -} -.openerp .oe-sidebar-attachments-items li { - position: relative; - padding: 0 0 3px 10px !important; -} -.openerp .oe-sidebar-attachments-items li:hover { - background: #ddd; -} -.openerp .oe-sidebar-attachments-link { - display: block; - margin-right: 15px; - overflow: hidden; -} -.openerp .oe-sidebar-attachment-delete { - position: absolute; - right: 2px; - top: 1px; - overflow: hidden; - width: 15px; - height: 15px; - padding: 1px; - border-radius: 7px; - -moz-border-radius: 7px; - -webkit-border-radius: 7px; -} -.openerp .oe-sidebar-attachment-delete:hover { - background-color: white; -} - -.openerp .view-manager-main-sidebar h2 { - margin:0; - font-size: 1.15em; - color: #8E8E8E; - text-shadow: white 0 1px 0; - padding-left: 10px; - padding-right: 21px; - height: 21px; - - background: #ffffff; /* Old browsers */ - background: -moz-linear-gradient(top, #ffffff 0%, #ebe9e9 100%); /* FF3.6+ */ - background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ffffff), color-stop(100%,#ebe9e9)); /* Chrome,Safari4+ */ - background: -webkit-linear-gradient(top, #ffffff 0%,#ebe9e9 100%); /* Chrome10+,Safari5.1+ */ - background: -o-linear-gradient(top, #ffffff 0%,#ebe9e9 100%); /* Opera11.10+ */ - background: -ms-linear-gradient(top, #ffffff 0%,#ebe9e9 100%); /* IE10+ */ - filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#FFFFFF', endColorstr='#EBE9E9',GradientType=0 ); /* IE6-9 */ - background: linear-gradient(top, #ffffff 0%,#ebe9e9 100%); /* W3C */ - - border: 1px solid #D2CFCF; - border-right-width: 0; - border-left-width: 0; -} -.openerp .view-manager-main-sidebar h2 { - border-top-width: 0; -} - -.openerp .view-manager-main-sidebar ul { - list-style-type: none; - margin: 0; - padding: 0; - display: block; -} - -.openerp .view-manager-main-sidebar li { - display: block; - padding: 3px 3px 3px 10px; -} - -.openerp .toggle-sidebar { - cursor: pointer; - border: 1px solid #D2CFCF; - border-top-width: 0; - display: block; - background: url(/web/static/src/img/toggle-a-bg.png); - width: 21px; - height: 21px; - z-index: 10; -} -.openerp .open-sidebar .toggle-sidebar { - margin-left: 158px; - background-position: 21px 0; - position: absolute; -} -.openerp .closed-sidebar .toggle-sidebar { - border-left: none; -} -.openerp li.oe_sidebar_print { - padding-left: 20px; - background: 1px 3px url(/web/static/src/img/icons/gtk-print.png) no-repeat; -} - -.openerp .oe_sidebar_print ul { - padding-left:8px; -} - -.openerp.kitten-mode-activated .main_table { - background: url(http://placekitten.com/g/1500/800) repeat; -} -.openerp.kitten-mode-activated.clark-gable .main_table { - background: url(http://amigrave.com/ClarkGable.jpg); - background-size: 100%; -} - -.openerp.kitten-mode-activated .header { - background: url(http://placekitten.com/g/211/65) repeat; -} - -.openerp.kitten-mode-activated .menu { - background: #828282; - background: -moz-linear-gradient(top, #828282 0%, #4D4D4D 100%); - background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#828282), color-stop(100%,#4D4D4D)); - filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#828282', endColorstr='#4D4D4D',GradientType=0 ); -} -.openerp.kitten-mode-activated .menu a { - background: none; -} -.openerp.kitten-mode-activated .menu span { - background: none; -} -.openerp.kitten-mode-activated .sidebar-content li a, -.openerp.kitten-mode-activated .oe-application .view-manager-main-content h2.oe_view_title, -.openerp.kitten-mode-activated .oe-application .view-manager-main-content a.searchview_group_string, -.openerp.kitten-mode-activated .oe-application .view-manager-main-content label { - color: white; -} -.openerp.kitten-mode-activated .menu, -.openerp.kitten-mode-activated .header_corner, -.openerp.kitten-mode-activated .header_title, -.openerp.kitten-mode-activated .oe-application, -.openerp.kitten-mode-activated .oe_footer, -.openerp.kitten-mode-activated .loading, -.openerp.kitten-mode-activated .ui-dialog { - opacity:0.8; - filter:alpha(opacity=80); -} -.openerp.kitten-mode-activated .header .company_logo { - background: url(http://placekitten.com/g/180/46); -} -.openerp.kitten-mode-activated .loading { - background: #828282; - border-color: #828282; -} - -.openerp .oe-m2o-drop-down-button { - margin-left: -24px; -} -.openerp .oe-m2o-drop-down-button img { - margin-bottom: -4px; - cursor: pointer; -} -.openerp .oe-m2o input { - border-right: none; - margin-right: 0px !important; - padding-bottom: 2px !important; -} -.openerp .oe-m2o-disabled-cm { - color: grey; -} -.openerp ul[role="listbox"] li a { - font-size:80%; -} -.parent_top { - vertical-align: text-top; -} - -.openerp .oe-dialog-warning p { - padding-left: 1em; - font-size: 1.2em; - font-weight: bold; -} - -.openerp .dhx_mini_calendar { - -moz-box-shadow: none; - -khtml-box-shadow: none; - -webkit-box-shadow: none; - box-shadow: none; -} -.openerp .oe-treeview-table { - width: 100%; - background-color : #FFFFFF; - border-spacing: 0; - -} -.openerp .oe-treeview-table tr:hover{ - color: blue; - background-color : #D8D8D8; -} -.treeview-tr, .treeview-td { - cursor: pointer; - vertical-align: top; - text-align: left; - border-bottom: 1px solid #CFCCCC; -} -.openerp .oe-treeview-table .oe-number { - text-align: right !important; -} -.treeview-tr span, .treeview-td span { - font-size: 90%; - font-weight: normal; - white-space: nowrap; - display: block; - } -.treeview-tr.oe-treeview-first { - background: transparent url(/web/static/src/img/expand.gif) 0 50% no-repeat; -} -.oe-open .treeview-tr.oe-treeview-first { - background-image: url(/web/static/src/img/collapse.gif); -} -.treeview-tr.oe-treeview-first span, -.treeview-td.oe-treeview-first span { - margin-left: 16px; -} - -.treeview-header { - vertical-align: top; - background-color : #D8D8D8; - white-space: nowrap; - text-align: left; - padding: 4px 5px; -} -ul.oe-arrow-list { - padding-left: 1.1em; - margin: 0; - white-space: nowrap; -} -ul.oe-arrow-list li { - display: inline-block; - margin-left: -1em; -} -ul.oe-arrow-list li span { - vertical-align: top; - display: inline-block; - border: 1em solid #DEDEDE; - line-height:0em; -} -ul.oe-arrow-list .oe-arrow-list-before { - border-left-color: rgba(0,0,0,0); - border-right-width:0; -} -ul.oe-arrow-list .oe-arrow-list-after { - border-color: rgba(0,0,0,0); - border-left-color: #DEDEDE; - border-right-width:0; -} -ul.oe-arrow-list li.oe-arrow-list-selected span { - border-color: #B5B9FF; -} -ul.oe-arrow-list li.oe-arrow-list-selected .oe-arrow-list-before { - border-left-color: rgba(0,0,0,0); -} -ul.oe-arrow-list li.oe-arrow-list-selected .oe-arrow-list-after { - border-color: rgba(0,0,0,0); - border-left-color: #B5B9FF; -} -.openerp ul.oe-arrow-list li:first-child span:first-child{ - -webkit-border-top-left-radius: 3px; - -moz-border-radius-topleft: 3px; - border-top-left-radius: 3px; - -webkit-border-bottom-left-radius: 3px; - -moz-border-radius-bottomleft: 3px; - border-bottom-left-radius: 3px; -} -.openerp ul.oe-arrow-list li:last-child span:last-child{ - -webkit-border-top-right-radius: 3px; - -moz-border-radius-topright: 3px; - border-top-right-radius: 3px; - -webkit-border-bottom-right-radius: 3px; - -moz-border-radius-bottomright: 3px; - border-bottom-right-radius: 3px; -} -.openerp .oe_view_editor { - width:100%; - border-collapse : collapse; - margin-left: -12px; - - width: 100%; - background-color : white; - border-spacing: 0; -} -.openerp .oe_view_editor td{ - text-align: center; - white-space: nowrap; - border: 1px solid #D8D8D8; - - cursor: pointer; - font-size: 90%; -} -.openerp .oe_view_editor_field td{ - border: 0px !important; -} - -.openerp .oe_view_editor tr:hover { - background-color: #ecebf2; -} - - -/* Dialog traceback cases */ -.openerp .oe_error_detail{ - display: block; -} -.openerp .oe_error_send{ - display:block; -} -.openerp .oe_fielddiv{ - display:inline-block; - width:100%; -} -.openerp .oe_fielddiv input[type=text],textarea{ - width:100%; -} -/* for Alignment center */ -.openerp .oe_centeralign{ - text-align:center; -} - -.openerp .oe_applications_tiles { - color: #4C4C4C; - text-shadow: #EEE 0 1px 0; - margin: 0 20px; -} - -.openerp select.oe_search-view-filters-management { - font-style: oblique; - color: #999999; -} - -.openerp .oe_search-view-filters-management option, -.openerp .oe_search-view-filters-management optgroup { - font-style: normal; - color: black; -} - - -/* Internet Explorer Fix */ -a img { - border: none; -} diff --git a/addons/web/static/src/css/data_export.css b/addons/web/static/src/css/data_export.css index 149fcea81e6..3fa24265b45 100644 --- a/addons/web/static/src/css/data_export.css +++ b/addons/web/static/src/css/data_export.css @@ -1,4 +1,4 @@ -.openerp .oe-export { +.openerp .oe_export { width: 100%; } .openerp .oe_export_row tr{ diff --git a/addons/web/static/src/css/data_import.css b/addons/web/static/src/css/data_import.css deleted file mode 100644 index a63a52802f0..00000000000 --- a/addons/web/static/src/css/data_import.css +++ /dev/null @@ -1,46 +0,0 @@ -.openerp .oe_import_grid { - border: none; - border-collapse: collapse; -} -.openerp .oe_import_grid-header .oe_import_grid-cell { - background: url(../img/gradientlinebg.gif) repeat-x #CCCCCC; - border-bottom: 1px solid #E3E3E3; - font-weight: bold; - text-align: left; -} -.openerp .oe_import_grid-row .oe_import_grid-cell { - border-bottom: 1px solid #E3E3E3; -} - -.openerp .oe-import-no-result .oe-import-result { - display: none; -} -.openerp .oe-import fieldset { - cursor: pointer; -} -.openerp .oe-import fieldset legend:before { - content: '\25BC '; -} -.openerp .oe-import fieldset.oe-closed legend:before { - content: '\25B6 '; -} -.openerp .oe-import fieldset.oe-closed table { - display: none; -} -.openerp .oe-import .separator.horizontal { - font-weight: bold; - border-bottom-width: 1px; - margin: 6px 4px 6px 1px; - height: 20px; -} -.openerp .duplicate_fld{ - background-color:#FF6666; -} -.openerp .select_fld{ - background: none repeat scroll 0 0 white; -} -.openerp .ui-autocomplete { - max-height: 300px; - overflow-y: auto; - padding-right: 20px; -} diff --git a/addons/web/static/src/img/graph_background.png b/addons/web/static/src/img/graph_background.png Binary files differnew file mode 100644 index 00000000000..530312b20bb --- /dev/null +++ b/addons/web/static/src/img/graph_background.png diff --git a/addons/web/static/src/img/pattern.png b/addons/web/static/src/img/pattern.png Binary files differdeleted file mode 100644 index 7e3a2b37834..00000000000 --- a/addons/web/static/src/img/pattern.png +++ /dev/null diff --git a/addons/web/static/src/img/search_reset.gif b/addons/web/static/src/img/search_reset.gif Binary files differnew file mode 100644 index 00000000000..9d4253e018b --- /dev/null +++ b/addons/web/static/src/img/search_reset.gif diff --git a/addons/web/static/src/img/throbber2.gif b/addons/web/static/src/img/throbber2.gif Binary files differdeleted file mode 100644 index 5d2057ae0e7..00000000000 --- a/addons/web/static/src/img/throbber2.gif +++ /dev/null diff --git a/addons/web/static/src/img/view_empty_arrow.png b/addons/web/static/src/img/view_empty_arrow.png Binary files differindex b0b1372adfd..ef8f3c820d7 100644 --- a/addons/web/static/src/img/view_empty_arrow.png +++ b/addons/web/static/src/img/view_empty_arrow.png diff --git a/addons/web/static/src/js/boot.js b/addons/web/static/src/js/boot.js index f83dbc97095..05ce709e80a 100644 --- a/addons/web/static/src/js/boot.js +++ b/addons/web/static/src/js/boot.js @@ -24,7 +24,7 @@ init: function(modules) { // By default only web will be loaded, the rest will be by loaded // by openerp.web.Session on the first session_authenticate - modules = modules || ["web"]; + modules = _.union(['web'], modules || []); var new_instance = { // links to the global openerp _openerp: openerp, @@ -37,7 +37,10 @@ }; openerp.instances[new_instance._session_id] = new_instance; for(var i=0; i < modules.length; i++) { - openerp[modules[i]](new_instance,new_instance[modules[i]]); + new_instance[modules[i]] = {}; + if (openerp[modules[i]]) { + openerp[modules[i]](new_instance,new_instance[modules[i]]); + } } return new_instance; } @@ -48,17 +51,12 @@ * OpenERP Web web module split *---------------------------------------------------------*/ openerp.web = function(session) { - var files = ["corelib","coresetup","dates","formats","chrome","data","views","search","list","form","list_editable","web_mobile","view_tree","data_export","data_import","view_editor"]; + var files = ["corelib","coresetup","dates","formats","chrome","data","views","search","list","form","list_editable","web_mobile","view_tree","data_export","data_import"]; for(var i=0; i<files.length; i++) { if(openerp.web[files[i]]) { openerp.web[files[i]](session); } } - session.log = function() { - if (session.connection.debug && window.console) { - console.log.apply(console, arguments); - } - } }; // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: diff --git a/addons/web/static/src/js/chrome.js b/addons/web/static/src/js/chrome.js index 47983ed322d..08ef8c4f9bf 100644 --- a/addons/web/static/src/js/chrome.js +++ b/addons/web/static/src/js/chrome.js @@ -9,12 +9,11 @@ instance.web.Notification = instance.web.Widget.extend({ template: 'Notification', init: function() { this._super.apply(this, arguments); - // move to instance.web.notification - instance.notification = this; + instance.web.notification = this; }, start: function() { this._super.apply(this, arguments); - this.$element.notify({ + this.$el.notify({ speed: 500, expires: 2500 }); @@ -25,7 +24,7 @@ instance.web.Notification = instance.web.Widget.extend({ if (sticky) { opts.expires = false; } - this.$element.notify('create', { + this.$el.notify('create', { title: title, text: text }, opts); @@ -36,7 +35,7 @@ instance.web.Notification = instance.web.Widget.extend({ if (sticky) { opts.expires = false; } - this.$element.notify('create', 'oe_notification_alert', { + this.$el.notify('create', 'oe_notification_alert', { title: title, text: text }, opts); @@ -58,22 +57,22 @@ instance.web.Dialog = instance.web.Widget.extend({ init: function (parent, options, content) { var self = this; this._super(parent); - if (content) { - this.$element = content instanceof $ ? content : $(content); - } + this.content_to_set = content; this.dialog_options = { modal: true, destroy_on_close: true, - width: $(window).width() * (($(window).width() > 1024) ? 0.5 : 0.75), + width: 900, min_width: 0, max_width: '95%', height: 'auto', min_height: 0, - max_height: this.get_height('100%') - 140, + max_height: this.get_height('100%') - 200, autoOpen: false, - position: [false, 50], + position: [false, 40], buttons: {}, - beforeClose: function () { self.on_close(); }, + beforeClose: function () { + self.trigger("closing"); + }, resizeStop: this.on_resized }; for (var f in this) { @@ -82,13 +81,12 @@ instance.web.Dialog = instance.web.Widget.extend({ } } if (options) { + if (options.buttons) { + this.params_buttons = true; + } _.extend(this.dialog_options, options); } - if (this.dialog_options.autoOpen) { - this.open(); - } else { - instance.web.dialog(this.$element, this.get_options()); - } + this.on("closing", this, this._closing); }, get_options: function(options) { var self = this, @@ -120,38 +118,72 @@ instance.web.Dialog = instance.web.Widget.extend({ return parseInt(val, 10); } }, - open: function(options) { - // TODO fme: bind window on resize - if (this.template) { - this.$element.html(this.renderElement()); + renderElement: function() { + if (this.content_to_set) { + this.setElement(this.content_to_set); + } else if (this.template) { + this._super(); } + }, + open: function(options) { + if (! this.dialog_inited) + this.init_dialog(); var o = this.get_options(options); - instance.web.dialog(this.$element, o).dialog('open'); + this.$buttons.appendTo($("body")); + instance.web.dialog(this.$el, o).dialog('open'); + this.$el.dialog("widget").find(".ui-dialog-buttonpane").remove(); + this.$buttons.appendTo(this.$el.dialog("widget")); if (o.height === 'auto' && o.max_height) { - this.$element.css({ 'max-height': o.max_height, 'overflow-y': 'auto' }); + this.$el.css({ 'max-height': o.max_height, 'overflow-y': 'auto' }); } return this; }, + init_dialog: function(options) { + this.renderElement(); + var o = this.get_options(options); + instance.web.dialog(this.$el, o); + if (! this.params_buttons) { + this.$buttons = $('<div class="ui-dialog-buttonpane ui-widget-content ui-helper-clearfix" />'); + this.$el.dialog("widget").append(this.$buttons); + } else { + this.$buttons = this.$el.dialog("widget").find(".ui-dialog-buttonpane"); + } + this.dialog_inited = true; + var res = this.start(); + return res; + }, close: function() { - this.$element.dialog('close'); + this.$el.dialog('close'); }, - on_close: function() { + _closing: function() { + if (this.__tmp_dialog_destroying) + return; if (this.dialog_options.destroy_on_close) { - this.$element.dialog('destroy'); + this.__tmp_dialog_closing = true; + this.destroy(); + this.__tmp_dialog_closing = undefined; } }, on_resized: function() { - //openerp.log("Dialog resized to %d x %d", this.$element.width(), this.$element.height()); }, destroy: function () { - this.close(); - this.$element.dialog('destroy'); + _.each(this.getChildren(), function(el) { + el.destroy(); + }); + if (! this.__tmp_dialog_closing) { + this.__tmp_dialog_destroying = true; + this.close(); + this.__tmp_dialog_destroying = undefined; + } + if (! this.isDestroyed()) { + this.$el.dialog('destroy'); + } this._super(); } }); instance.web.CrashManager = instance.web.CallbackEnabled.extend({ - on_rpc_error: function(error) { + rpc_error: function(error) { if (error.data.fault_code) { var split = ("" + error.data.fault_code).split('\n')[0].split(' -- '); if (split.length > 1) { @@ -160,12 +192,12 @@ instance.web.CrashManager = instance.web.CallbackEnabled.extend({ } } if (error.code === 200 && error.type) { - this.on_managed_error(error); + this.show_warning(error); } else { - this.on_traceback(error); + this.show_error(error); } }, - on_managed_error: function(error) { + show_warning: function(error) { instance.web.dialog($('<div>' + QWeb.render('CrashManager.warning', {error: error}) + '</div>'), { title: "OpenERP " + _.str.capitalize(error.type), buttons: [ @@ -173,32 +205,12 @@ instance.web.CrashManager = instance.web.CallbackEnabled.extend({ ] }); }, - on_traceback: function(error) { + show_error: function(error) { var self = this; var buttons = {}; - if (instance.connection.openerp_entreprise) { - buttons[_t("Send OpenERP Enterprise Report")] = function() { - var $this = $(this); - var issuename = $('#issuename').val(); - var explanation = $('#explanation').val(); - var remark = $('#remark').val(); - // Call the send method from server to send mail with details - new instance.web.DataSet(self, 'publisher_warranty.contract').call_and_eval('send', [error.data,explanation,remark,issuename]).then(function(result){ - if (result === false) { - alert('There was a communication error.'); - } else { - $this.dialog('close'); - } - }); - }; - buttons[_t("Dont send")] = function() { - $(this).dialog("close"); - }; - } else { - buttons[_t("Ok")] = function() { - $(this).dialog("close"); - }; - } + buttons[_t("Ok")] = function() { + $(this).dialog("close"); + }; var dialog = new instance.web.Dialog(this, { title: "OpenERP " + _.str.capitalize(error.type), width: '80%', @@ -207,8 +219,15 @@ instance.web.CrashManager = instance.web.CallbackEnabled.extend({ min_height: '600px', buttons: buttons }).open(); - dialog.$element.html(QWeb.render('CrashManager.error', {session: instance.connection, error: error})); - } + dialog.$el.html(QWeb.render('CrashManager.error', {session: instance.session, error: error})); + }, + show_message: function(exception) { + this.show_error({ + type: _t("Client Error"), + message: exception, + data: {debug: ""} + }); + }, }); instance.web.Loading = instance.web.Widget.extend({ @@ -217,47 +236,49 @@ instance.web.Loading = instance.web.Widget.extend({ this._super(parent); this.count = 0; this.blocked_ui = false; - var self = this; - this.request_call = function() { - self.on_rpc_event(1); - }; - this.response_call = function() { - self.on_rpc_event(-1); - }; - this.session.on_rpc_request.add_first(this.request_call); - this.session.on_rpc_response.add_last(this.response_call); + this.session.on("request", this, this.request_call); + this.session.on("response", this, this.response_call); + this.session.on("response_failed", this, this.response_call); }, destroy: function() { - this.session.on_rpc_request.remove(this.request_call); - this.session.on_rpc_response.remove(this.response_call); this.on_rpc_event(-this.count); this._super(); }, + request_call: function() { + this.on_rpc_event(1); + }, + response_call: function() { + this.on_rpc_event(-1); + }, on_rpc_event : function(increment) { var self = this; if (!this.count && increment === 1) { // Block UI after 3s this.long_running_timer = setTimeout(function () { self.blocked_ui = true; - $.blockUI(); + instance.web.blockUI(); }, 3000); } this.count += increment; if (this.count > 0) { - this.$element.text(_.str.sprintf( _t("Loading (%d)"), this.count)); - this.$element.show(); - this.getParent().$element.addClass('oe_wait'); + if (instance.session.debug) { + this.$el.text(_.str.sprintf( _t("Loading (%d)"), this.count)); + } else { + this.$el.text(_t("Loading")); + } + this.$el.show(); + this.getParent().$el.addClass('oe_wait'); } else { this.count = 0; clearTimeout(this.long_running_timer); // Don't unblock if blocked by somebody else if (self.blocked_ui) { this.blocked_ui = false; - $.unblockUI(); + instance.web.unblockUI(); } - this.$element.fadeOut(); - this.getParent().$element.removeClass('oe_wait'); + this.$el.fadeOut(); + this.getParent().$el.removeClass('oe_wait'); } } }); @@ -265,13 +286,14 @@ instance.web.Loading = instance.web.Widget.extend({ instance.web.DatabaseManager = instance.web.Widget.extend({ init: function(parent) { this._super(parent); - this.unblockUIFunction = $.unblockUI; + this.unblockUIFunction = instance.web.unblockUI; $.validator.addMethod('matches', function (s, _, re) { return new RegExp(re).test(s); }, _t("Invalid database name")); }, start: function() { var self = this; + $('.oe_secondary_menus_container,.oe_user_menu_placeholder').empty(); var fetch_db = this.rpc("/web/database/get_list", {}).pipe( function(result) { self.db_list = result.db_list; @@ -287,17 +309,27 @@ instance.web.DatabaseManager = instance.web.Widget.extend({ }, do_render: function() { var self = this; - self.$element.html(QWeb.render("DatabaseManager", { widget : self })); - self.$element.find(".oe_database_manager_menu").tabs({ - show: function(event, ui) { - $('*[autofocus]:first', ui.panel).focus(); - } + instance.webclient.toggle_bars(true); + self.$el.html(QWeb.render("DatabaseManager", { widget : self })); + $('.oe_user_menu_placeholder').append(QWeb.render("DatabaseManager.user_menu",{ widget : self })); + $('.oe_secondary_menus_container').append(QWeb.render("DatabaseManager.menu",{ widget : self })); + $('ul.oe_secondary_submenu > li:first').addClass('oe_active') + $('ul.oe_secondary_submenu > li').bind('click', function (event) { + var menuitem = $(this); + menuitem.addClass('oe_active').siblings().removeClass('oe_active'); + var form_id =menuitem.find('a').attr('href'); + $(form_id).show().siblings().hide(); + event.preventDefault(); }); - self.$element.find("form[name=create_db_form]").validate({ submitHandler: self.do_create }); - self.$element.find("form[name=drop_db_form]").validate({ submitHandler: self.do_drop }); - self.$element.find("form[name=backup_db_form]").validate({ submitHandler: self.do_backup }); - self.$element.find("form[name=restore_db_form]").validate({ submitHandler: self.do_restore }); - self.$element.find("form[name=change_pwd_form]").validate({ + $('#back-to-login').click(self.do_exit); + self.$el.find("td").addClass("oe_form_group_cell"); + self.$el.find("tr td:first-child").addClass("oe_form_group_cell_label"); + self.$el.find("label").addClass("oe_form_label"); + self.$el.find("form[name=create_db_form]").validate({ submitHandler: self.do_create }); + self.$el.find("form[name=drop_db_form]").validate({ submitHandler: self.do_drop }); + self.$el.find("form[name=backup_db_form]").validate({ submitHandler: self.do_backup }); + self.$el.find("form[name=restore_db_form]").validate({ submitHandler: self.do_restore }); + self.$el.find("form[name=change_pwd_form]").validate({ messages: { old_pwd: "Please enter your previous password", new_pwd: "Please enter your new password", @@ -308,10 +340,9 @@ instance.web.DatabaseManager = instance.web.Widget.extend({ }, submitHandler: self.do_change_password }); - self.$element.find("#back_to_login").click(self.do_exit); }, destroy: function () { - this.$element.find('#db-create, #db-drop, #db-backup, #db-restore, #db-change-password, #back-to-login').unbind('click').end().empty(); + this.$el.find('#db-create, #db-drop, #db-backup, #db-restore, #db-change-password, #back-to-login').unbind('click').end().empty(); this._super(); }, /** @@ -332,16 +363,16 @@ instance.web.DatabaseManager = instance.web.Widget.extend({ * from unblocking the UI */ blockUI: function () { - $.blockUI(); - $.unblockUI = function () {}; + instance.web.blockUI(); + instance.web.unblockUI = function () {}; }, /** * Reinstates $.unblockUI so third parties can play with blockUI, and * unblocks the UI */ unblockUI: function () { - $.unblockUI = this.unblockUIFunction; - $.unblockUI(); + instance.web.unblockUI = this.unblockUIFunction; + instance.web.unblockUI(); }, /** * Displays an error dialog resulting from the various RPC communications @@ -363,12 +394,22 @@ instance.web.DatabaseManager = instance.web.Widget.extend({ do_create: function(form) { var self = this; var fields = $(form).serializeArray(); - self.rpc("/web/database/create", {'fields': fields}, function(result) { + self.rpc("/web/database/create", {'fields': fields}).then(function(result) { var form_obj = self.to_object(fields); - self.getParent().do_login( form_obj['db_name'], 'admin', form_obj['create_admin_pwd']); - self.destroy(); + var client_action = { + type: 'ir.actions.client', + tag: 'login', + params: { + 'db': form_obj['db_name'], + 'login': 'admin', + 'password': form_obj['create_admin_pwd'], + 'login_successful': function() { + self.do_action("reload"); + }, + }, + }; + self.do_action(client_action); }); - }, do_drop: function(form) { var self = this; @@ -379,7 +420,7 @@ instance.web.DatabaseManager = instance.web.Widget.extend({ if (!db || !confirm("Do you really want to delete the database: " + db + " ?")) { return; } - self.rpc("/web/database/drop", {'fields': fields}, function(result) { + self.rpc("/web/database/drop", {'fields': fields}).then(function(result) { if (result.error) { self.display_error(result); return; @@ -396,7 +437,14 @@ instance.web.DatabaseManager = instance.web.Widget.extend({ success: function () { self.do_notify(_t("Backed"), _t("Database backed up successfully")); }, - error: instance.webclient.crashmanager.on_rpc_error, + error: function(error){ + if(error){ + self.display_error({ + title: 'Backup Database', + error: 'AccessDenied' + }); + } + }, complete: function() { self.unblockUI(); } @@ -435,26 +483,38 @@ instance.web.DatabaseManager = instance.web.Widget.extend({ var self = this; self.rpc("/web/database/change_password", { 'fields': $(form).serializeArray() - }, function(result) { + }).then(function(result) { if (result.error) { self.display_error(result); return; } + self.unblockUI(); self.do_notify("Changed Password", "Password has been changed successfully"); }); }, do_exit: function () { + this.$el.remove(); + instance.webclient.toggle_bars(false); + this.do_action('login'); } }); +instance.web.client_actions.add("database_manager", "instance.web.DatabaseManager"); instance.web.Login = instance.web.Widget.extend({ template: "Login", remember_credentials: true, - init: function(parent) { + + init: function(parent, action) { this._super(parent); this.has_local_storage = typeof(localStorage) != 'undefined'; + this.db_list = null; this.selected_db = null; this.selected_login = null; + this.params = action.params || {}; + + if (this.params.login_successful) { + this.on('login_successful', this, this.params.login_successful); + } if (this.has_local_storage && this.remember_credentials) { this.selected_db = localStorage.getItem('last_db_login_success'); @@ -466,48 +526,47 @@ instance.web.Login = instance.web.Widget.extend({ }, start: function() { var self = this; - - self.$element.find("form").submit(self.on_submit); - - self.$element.find('.oe_login_manage_db').click(function() { - self.$element.find('.oe_login_bottom').hide(); - self.$element.find('.oe_login_pane').hide(); - self.databasemanager = new instance.web.DatabaseManager(self); - self.databasemanager.appendTo(self.$element); - self.databasemanager.do_exit.add_last(function() { - self.databasemanager.destroy(); - self.$element.find('.oe_login_bottom').show(); - self.$element.find('.oe_login_pane').show(); - self.load_db_list(); - }); + self.$el.find("form").submit(self.on_submit); + self.$el.find('.oe_login_manage_db').click(function() { + self.do_action("database_manager"); }); - self.load_db_list(); - }, - load_db_list: function () { - var self = this; - self.rpc("/web/database/get_list", {}, function(result) { - self.set_db_list(result.db_list); - }, function(error, event) { - if (error.data.fault_code === 'AccessDenied') { - event.preventDefault(); + var d; + if (self.params.db) { + if (self.params.login && self.params.password) { + d = self.do_login(self.params.db, self.params.login, self.params.password); } - }); + } else { + d = self.rpc("/web/database/get_list", {}).done(self.on_db_loaded).fail(self.on_db_failed); + } + return d; + }, + on_db_loaded: function (result) { + this.db_list = result.db_list; + this.$("[name=db]").replaceWith(QWeb.render('Login.dblist', { db_list: this.db_list, selected_db: this.selected_db})); + if(this.db_list.length === 0) { + this.do_action("database_manager"); + } else if(this.db_list.length === 1) { + this.$('div.oe_login_dbpane').hide(); + } else { + this.$('div.oe_login_dbpane').show(); + } }, - set_db_list: function (list) { - this.$element.find("[name=db]").replaceWith(instance.web.qweb.render('Login.dblist', { db_list: list, selected_db: this.selected_db})); + on_db_failed: function (error, event) { + if (error.data.fault_code === 'AccessDenied') { + event.preventDefault(); + } }, on_submit: function(ev) { if(ev) { ev.preventDefault(); } - var $e = this.$element; - var db = $e.find("form [name=db]").val(); + var db = this.$("form [name=db]").val(); if (!db) { this.do_warn("Login", "No database selected !"); return false; } - var login = $e.find("form input[name=login]").val(); - var password = $e.find("form input[name=password]").val(); + var login = this.$("form input[name=login]").val(); + var password = this.$("form input[name=password]").val(); this.do_login(db, login, password); }, @@ -520,15 +579,9 @@ instance.web.Login = instance.web.Widget.extend({ */ do_login: function (db, login, password) { var self = this; - this.$element.removeClass('oe_login_invalid'); - this.session.on_session_invalid.add({ - callback: function () { - self.$element.addClass("oe_login_invalid"); - }, - unique: true - }); - this.session.session_authenticate(db, login, password).then(function() { - self.$element.removeClass("oe_login_invalid"); + self.hide_error(); + self.$(".oe_login_pane").fadeOut("slow"); + return this.session.session_authenticate(db, login, password).pipe(function() { if (self.has_local_storage) { if(self.remember_credentials) { localStorage.setItem('last_db_login_success', db); @@ -542,9 +595,97 @@ instance.web.Login = instance.web.Widget.extend({ localStorage.setItem('last_password_login_success', ''); } } + self.trigger('login_successful'); + }, function () { + self.$(".oe_login_pane").fadeIn("fast", function() { + self.show_error("Invalid username or password"); + }); }); + }, + show_error: function(message) { + this.$el.addClass("oe_login_invalid"); + this.$(".oe_login_error_message").text(message); + }, + hide_error: function() { + this.$el.removeClass('oe_login_invalid'); + }, +}); +instance.web.client_actions.add("login", "instance.web.Login"); + +/** + * Client action to reload the whole interface. + * If params has an entry 'menu_id', it opens the given menu entry. + */ +instance.web.Reload = function(parent, action) { + var params = action.params || {}; + var menu_id = params.menu_id || false; + var l = window.location; + + var sobj = $.deparam(l.search.substr(1)); + sobj.ts = new Date().getTime(); + var search = '?' + $.param(sobj); + + var hash = l.hash; + if (menu_id) { + hash = "#menu_id=" + menu_id; + } + var url = l.protocol + "//" + l.host + l.pathname + search + hash; + window.onerror = function() {}; + window.location = url; +}; +instance.web.client_actions.add("reload", "instance.web.Reload"); + +/** + * Client action to go back in breadcrumb history. + * If can't go back in history stack, will go back to home. + */ +instance.web.HistoryBack = function(parent) { + if (!parent.history_back()) { + window.location = '/' + (window.location.search || ''); + } +}; +instance.web.client_actions.add("history_back", "instance.web.HistoryBack"); + +/** + * Client action to go back home. + */ +instance.web.Home = instance.web.Widget.extend({ + init: function(parent) { + window.location = '/' + (window.location.search || ''); } }); +instance.web.client_actions.add("home", "instance.web.Home"); + +instance.web.ChangePassword = instance.web.Widget.extend({ + template: "ChangePassword", + start: function() { + var self = this; + self.$el.validate({ + submitHandler: function (form) { + self.rpc("/web/session/change_password",{ + 'fields': $(form).serializeArray() + }).then(function(result) { + if (result.error) { + self.display_error(result); + return; + } else { + instance.webclient.on_logout(); + } + }); + } + }); + }, + display_error: function (error) { + return instance.web.dialog($('<div>'), { + modal: true, + title: error.title, + buttons: [ + {text: _t("Ok"), click: function() { $(this).dialog("close"); }} + ] + }).html(error.error); + }, +}) +instance.web.client_actions.add("change_password", "instance.web.ChangePassword"); instance.web.Menu = instance.web.Widget.extend({ template: 'Menu', @@ -556,29 +697,34 @@ instance.web.Menu = instance.web.Widget.extend({ }, start: function() { this._super.apply(this, arguments); - this.$secondary_menus = this.getParent().$element.find('.oe_secondary_menus_container'); + this.$secondary_menus = this.getParent().$el.find('.oe_secondary_menus_container'); + this.$secondary_menus.on('click', 'a[data-menu]', this.on_menu_click); return this.do_reload(); }, do_reload: function() { - return this.rpc("/web/menu/load", {}).then(this.on_loaded); + var self = this; + return this.rpc("/web/menu/load", {}).then(function(r) { + self.menu_loaded(r); + }); }, - on_loaded: function(data) { + menu_loaded: function(data) { var self = this; this.data = data; this.renderElement(); this.limit_entries(); + // Hide toplevel item if there is only one + var $toplevel = this.$("li") + if($toplevel.length == 1) { + $toplevel.hide(); + } this.$secondary_menus.html(QWeb.render("Menu.secondary", { widget : this })); - this.$element.on('click', 'a.oe_menu_more_link', function() { - self.$element.find('.oe_menu_more').toggle(); - return false; - }); - this.$element.on('click', 'a[data-menu]', this.on_menu_click); - this.$secondary_menus.on('click', 'a[data-menu]', this.on_menu_click); + this.$el.on('click', 'a[data-menu]', this.on_menu_click); // Hide second level submenus this.$secondary_menus.find('.oe_menu_toggler').siblings('.oe_secondary_submenu').hide(); if (self.current_menu) { self.open_menu(self.current_menu); } + this.trigger('menu_loaded', data); this.has_been_loaded.resolve(); }, limit_entries: function() { @@ -588,20 +734,17 @@ instance.web.Menu = instance.web.Widget.extend({ } if (maximum_visible_links < this.data.data.children.length) { var $more = $(QWeb.render('Menu.more')), - $index = this.$element.find('li').eq(maximum_visible_links - 1); + $index = this.$el.find('li').eq(maximum_visible_links - 1); $index.after($more); + //$('.oe_topbar').append($more); $more.find('.oe_menu_more').append($index.next().nextAll()); } - this.do_hide_more(); }, auto_limit_entries: function() { // TODO: auto detect overflow and bind window on resize var width = $(window).width(); return Math.floor(width / 125); }, - do_hide_more: function() { - this.$element.find('.oe_menu_more').hide(); - }, /** * Opens a given menu by id, as if a user had browsed to that menu by hand * except does not trigger any event on the way @@ -609,20 +752,22 @@ instance.web.Menu = instance.web.Widget.extend({ * @param {Number} id database id of the terminal menu to select */ open_menu: function (id) { + this.current_menu = id; + this.session.active_id = id; var $clicked_menu, $sub_menu, $main_menu; - $clicked_menu = this.$element.add(this.$secondary_menus).find('a[data-menu=' + id + ']'); + $clicked_menu = this.$el.add(this.$secondary_menus).find('a[data-menu=' + id + ']'); this.trigger('open_menu', id, $clicked_menu); if (this.$secondary_menus.has($clicked_menu).length) { $sub_menu = $clicked_menu.parents('.oe_secondary_menu'); - $main_menu = this.$element.find('a[data-menu=' + $sub_menu.data('menu-parent') + ']'); + $main_menu = this.$el.find('a[data-menu=' + $sub_menu.data('menu-parent') + ']'); } else { $sub_menu = this.$secondary_menus.find('.oe_secondary_menu[data-menu-parent=' + $clicked_menu.attr('data-menu') + ']'); $main_menu = $clicked_menu; } // Activate current main menu - this.$element.find('.oe_active').removeClass('oe_active'); + this.$el.find('.oe_active').removeClass('oe_active'); $main_menu.addClass('oe_active'); // Show current sub menu @@ -649,7 +794,7 @@ instance.web.Menu = instance.web.Widget.extend({ * @param {Number} id the action_id to match */ open_action: function (id) { - var $menu = this.$element.add(this.$secondary_menus).find('a[data-action-id=' + id + ']'); + var $menu = this.$el.add(this.$secondary_menus).find('a[data-action-id="' + id + '"]'); var menu_id = $menu.data('menu'); if (menu_id) { this.open_menu(menu_id); @@ -659,34 +804,37 @@ instance.web.Menu = instance.web.Widget.extend({ * Process a click on a menu item * * @param {Number} id the menu_id + * @param {Boolean} [needaction=false] whether the triggered action should execute in a `needs action` context */ - menu_click: function(id) { - if (id) { - this.do_hide_more(); - // find back the menuitem in dom to get the action - var $item = this.$element.find('a[data-menu=' + id + ']'); - if (!$item.length) { - $item = this.$secondary_menus.find('a[data-menu=' + id + ']'); - } - var action_id = $item.data('action-id'); - // If first level menu doesnt have action trigger first leaf - if (!action_id) { - if(this.$element.has($item).length) { - $sub_menu = this.$secondary_menus.find('.oe_secondary_menu[data-menu-parent=' + id + ']'); - $items = $sub_menu.find('a[data-action-id]').filter('[data-action-id!=""]'); - if($items.length) { - action_id = $items.data('action-id'); - id = $items.data('menu'); - } + menu_click: function(id, needaction) { + if (!id) { return; } + + // find back the menuitem in dom to get the action + var $item = this.$el.find('a[data-menu=' + id + ']'); + if (!$item.length) { + $item = this.$secondary_menus.find('a[data-menu=' + id + ']'); + } + var action_id = $item.data('action-id'); + // If first level menu doesnt have action trigger first leaf + if (!action_id) { + if(this.$el.has($item).length) { + var $sub_menu = this.$secondary_menus.find('.oe_secondary_menu[data-menu-parent=' + id + ']'); + var $items = $sub_menu.find('a[data-action-id]').filter('[data-action-id!=""]'); + if($items.length) { + action_id = $items.data('action-id'); + id = $items.data('menu'); } } - this.open_menu(id); - this.current_menu = id; - this.session.active_id = id; - if (action_id) { - this.trigger('menu_click', action_id, id, $item); - } } + if (action_id) { + this.trigger('menu_click', { + action_id: action_id, + needaction: needaction, + id: id, + previous_menu_id: this.current_menu // Here we don't know if action will fail (in which case we have to revert menu) + }, $item); + } + this.open_menu(id); }, /** * Jquery event handler for menu click @@ -694,9 +842,9 @@ instance.web.Menu = instance.web.Widget.extend({ * @param {Event} ev the jquery event */ on_menu_click: function(ev) { - this.menu_click($(ev.currentTarget).data('menu')); - ev.stopPropagation(); - return false; + ev.preventDefault(); + var needaction = $(ev.target).is('div.oe_menu_counter'); + this.menu_click($(ev.currentTarget).data('menu'), needaction); }, }); @@ -709,118 +857,46 @@ instance.web.UserMenu = instance.web.Widget.extend({ start: function() { var self = this; this._super.apply(this, arguments); - $('html').bind('click', function() { - self.$element.find('.oe_dropdown_options').hide(); - }); - this.$element.find('.oe_dropdown_toggle').click(function() { - self.$element.find('.oe_dropdown_options').toggle(); - return false; - }); - this.$element.on('click', '.oe_dropdown_options li a[data-menu]', function() { + this.$el.on('click', '.oe_dropdown_menu li a[data-menu]', function(ev) { + ev.preventDefault(); var f = self['on_menu_' + $(this).data('menu')]; if (f) { f($(this)); } - self.$element.find('.oe_dropdown_options').hide(); - return false; - }); - }, - change_password :function() { - var self = this; - this.dialog = new instance.web.Dialog(this, { - title: _t("Change Password"), - width : 'auto' - }).open(); - this.dialog.$element.html(QWeb.render("UserMenu.password", self)); - this.dialog.$element.find("form[name=change_password_form]").validate({ - submitHandler: function (form) { - self.rpc("/web/session/change_password",{ - 'fields': $(form).serializeArray() - }, function(result) { - if (result.error) { - self.display_error(result); - return; - } else { - instance.webclient.on_logout(); - } - }); - } }); }, - display_error: function (error) { - return instance.web.dialog($('<div>'), { - modal: true, - title: error.title, - buttons: [ - {text: _("Ok"), click: function() { $(this).dialog("close"); }} - ] - }).html(error.error); - }, do_update: function () { var self = this; var fct = function() { - var $avatar = self.$element.find('.oe_topbar_avatar'); + var $avatar = self.$el.find('.oe_topbar_avatar'); $avatar.attr('src', $avatar.data('default-src')); if (!self.session.uid) return; var func = new instance.web.Model("res.users").get_func("read"); return func(self.session.uid, ["name", "company_id"]).pipe(function(res) { var topbar_name = res.name; - if(instance.connection.debug) - topbar_name = _.str.sprintf("%s (%s)", topbar_name, instance.connection.db); + if(instance.session.debug) + topbar_name = _.str.sprintf("%s (%s)", topbar_name, instance.session.db); if(res.company_id[0] > 1) topbar_name = _.str.sprintf("%s (%s)", topbar_name, res.company_id[1]); - self.$element.find('.oe_topbar_name').text(topbar_name); - var avatar_src = _.str.sprintf('%s/web/binary/image?session_id=%s&model=res.users&field=avatar&id=%s', self.session.prefix, self.session.session_id, self.session.uid); + self.$el.find('.oe_topbar_name').text(topbar_name); + var avatar_src = _.str.sprintf('%s/web/binary/image?session_id=%s&model=res.users&field=image_small&id=%s', self.session.prefix, self.session.session_id, self.session.uid); $avatar.attr('src', avatar_src); }); }; this.update_promise = this.update_promise.pipe(fct, fct); }, - on_action: function() { - }, on_menu_logout: function() { + this.trigger('user_logout'); }, on_menu_settings: function() { var self = this; - var action_manager = new instance.web.ActionManager(this); - var dataset = new instance.web.DataSet (this,'res.users',this.context); - dataset.call ('action_get','',function (result){ - self.rpc('/web/action/load', {action_id:result}, function(result){ - action_manager.do_action(_.extend(result['result'], { - target: 'inline', - res_id: self.session.uid, - res_model: 'res.users', - flags: { - action_buttons: false, - search_view: false, - sidebar: false, - views_switcher: false, - pager: false - } - })); + if (!this.getParent().has_uncommitted_changes()) { + self.rpc("/web/action/load", { action_id: "base.action_res_users_my" }, function(result) { + result.res_id = instance.session.uid; + self.getParent().action_manager.do_action(result); }); - }); - this.dialog = new instance.web.Dialog(this,{ - title: _t("Preferences"), - width: '700px', - buttons: [ - {text: _t("Change password"), click: function(){ self.change_password(); }}, - {text: _t("Cancel"), click: function(){ $(this).dialog('destroy'); }}, - {text: _t("Save"), click: function(){ - var inner_viewmanager = action_manager.inner_viewmanager; - inner_viewmanager.views[inner_viewmanager.active_view].controller.do_save() - .then(function() { - self.dialog.destroy(); - // needs to refresh interface in case language changed - window.location.reload(); - }); - } - } - ] - }).open(); - action_manager.appendTo(this.dialog.$element); - action_manager.renderElement(this.dialog); + } }, on_menu_about: function() { var self = this; @@ -828,105 +904,160 @@ instance.web.UserMenu = instance.web.Widget.extend({ var $help = $(QWeb.render("UserMenu.about", {version_info: res})); $help.find('a.oe_activate_debug_mode').click(function (e) { e.preventDefault(); - window.location = $.param.querystring( - window.location.href, 'debug'); + window.location = $.param.querystring( window.location.href, 'debug'); }); instance.web.dialog($help, {autoOpen: true, - modal: true, width: 960, title: _t("About")}); + modal: true, width: 507, height: 290, resizable: false, title: _t("About")}); }); }, }); -instance.web.WebClient = instance.web.Widget.extend({ - init: function(parent) { +instance.web.Client = instance.web.Widget.extend({ + init: function(parent, origin) { + instance.client = instance.webclient = this; + this._super(parent); + this.origin = origin; + }, + start: function() { var self = this; + return instance.session.session_bind(this.origin).pipe(function() { + var $e = $(QWeb.render(self._template, {})); + self.replaceElement($e); + self.bind_events(); + return self.show_common(); + }); + }, + bind_events: function() { + var self = this; + this.$el.on('mouseenter', '.oe_systray > div:not([data-tipsy=true])', function() { + $(this).attr('data-tipsy', 'true').tipsy().trigger('mouseenter'); + }); + this.$el.on('click', '.oe_dropdown_toggle', function(ev) { + ev.preventDefault(); + var $toggle = $(this); + var $menu = $toggle.siblings('.oe_dropdown_menu'); + $menu = $menu.size() >= 1 ? $menu : $toggle.find('.oe_dropdown_menu'); + var state = $menu.is('.oe_opened'); + setTimeout(function() { + // Do not alter propagation + $toggle.add($menu).toggleClass('oe_opened', !state); + if (!state) { + // Move $menu if outside window's edge + var doc_width = $(document).width(); + var offset = $menu.offset(); + var menu_width = $menu.width(); + var x = doc_width - offset.left - menu_width - 2; + if (x < 0) { + $menu.offset({ left: offset.left + x }).width(menu_width); + } + } + }, 0); + }); + instance.web.bus.on('click', this, function(ev) { + $.fn.tipsy.clear(); + if (!$(ev.target).is('input[type=file]')) { + self.$el.find('.oe_dropdown_menu.oe_opened, .oe_dropdown_toggle.oe_opened').removeClass('oe_opened'); + } + }); + }, + show_common: function() { + var self = this; + this.crashmanager = new instance.web.CrashManager(); + instance.session.on('error', this.crashmanager, this.crashmanager.rpc_error); + self.notification = new instance.web.Notification(this); + self.notification.appendTo(self.$el); + self.loading = new instance.web.Loading(self); + self.loading.appendTo(self.$el); + self.action_manager = new instance.web.ActionManager(self); + self.action_manager.appendTo(self.$('.oe_application')); + }, + toggle_bars: function(value) { + this.$('tr:has(td.oe_topbar),.oe_leftbar').toggle(value); + }, + has_uncommitted_changes: function() { + return false; + }, +}); + +instance.web.WebClient = instance.web.Client.extend({ + _template: 'WebClient', + init: function(parent) { this._super(parent); - instance.webclient = this; - this.querystring = '?' + jQuery.param.querystring(); this._current_state = null; }, start: function() { var self = this; - this.$element.addClass("openerp openerp-web-client-container"); - if (jQuery.param !== undefined && jQuery.deparam(jQuery.param.querystring()).kitten !== undefined) { - this.$element.addClass("kitten-mode-activated"); - this.$element.delegate('img.oe-record-edit-link-img', 'hover', function(e) { - self.$element.toggleClass('clark-gable'); + return $.when(this._super()).pipe(function() { + self.$el.on('click', '.oe_logo', function() { + self.action_manager.do_action('home'); }); - } - this.session.session_bind().then(function() { + if (jQuery.param !== undefined && jQuery.deparam(jQuery.param.querystring()).kitten !== undefined) { + $("body").addClass("kitten-mode-activated"); + if ($.blockUI) { + $.blockUI.defaults.message = '<img src="http://www.amigrave.com/kitten.gif">'; + } + } if (!self.session.session_is_valid()) { self.show_login(); + } else { + self.show_application(); } }); - this.session.on_session_valid.add(function() { - self.show_application(); - - if(self.action_manager) - self.action_manager.destroy(); - self.action_manager = new instance.web.ActionManager(self); - self.action_manager.appendTo(self.$element.find('.oe_application')); - self.bind_hashchange(); - var version_label = _t("OpenERP - Unsupported/Community Version"); - if (!self.session.openerp_entreprise) { - self.$element.find('.oe_footer_powered').append(_.str.sprintf('<span> - <a href="http://www.openerp.com/support-or-publisher-warranty-contract" target="_blank">%s</a></span>', version_label)); - document.title = version_label; - } - }); - this.$element.on('mouseenter', '.oe_systray > div:not([data-tipsy=true])', function() { - $(this).attr('data-tipsy', 'true').tipsy().trigger('mouseenter'); - }); }, - show_login: function() { + set_title: function(title) { + title = _.str.clean(title); + var sep = _.isEmpty(title) ? '' : ' - '; + document.title = title + sep + 'OpenERP'; + }, + show_common: function() { var self = this; - this.destroy_content(); - this.show_common(); - self.login = new instance.web.Login(self); - self.login.appendTo(self.$element); + this._super(); + window.onerror = function (message, file, line) { + self.crashmanager.show_error({ + type: _t("Client Error"), + message: message, + data: {debug: file + ':' + line} + }); + }; + }, + show_login: function() { + this.toggle_bars(false); + + var state = $.bbq.getState(true); + var action = { + type: 'ir.actions.client', + tag: 'login', + _push_me: false, + }; + + this.action_manager.do_action(action); + this.action_manager.inner_widget.on('login_successful', this, function() { + this.show_application(); // will load the state we just pushed + }); }, show_application: function() { var self = this; - this.destroy_content(); - this.show_common(); - self.$table = $(QWeb.render("WebClient", {})); - self.$element.append(self.$table); + self.toggle_bars(true); self.menu = new instance.web.Menu(self); - self.menu.replace(this.$element.find('.oe_menu_placeholder')); + self.menu.replace(this.$el.find('.oe_menu_placeholder')); self.menu.on('menu_click', this, this.on_menu_action); self.user_menu = new instance.web.UserMenu(self); - self.user_menu.replace(this.$element.find('.oe_user_menu_placeholder')); - self.user_menu.on_menu_logout.add(this.proxy('on_logout')); - self.user_menu.on_action.add(this.proxy('on_menu_action')); + self.user_menu.replace(this.$el.find('.oe_user_menu_placeholder')); + self.user_menu.on('user_logout', self, self.on_logout); self.user_menu.do_update(); - }, - show_common: function() { - var self = this; - if (!this.crashmanager) { - this.crashmanager = new instance.web.CrashManager(); - instance.connection.on_rpc_error.add(this.crashmanager.on_rpc_error); - window.onerror = function (message, file, line) { - self.crashmanager.on_traceback({ - type: _t("Client Error"), - message: message, - data: {debug: file + ':' + line} - }); - }; - } - this.notification = new instance.web.Notification(this); - this.notification.appendTo(this.$element); - this.loading = new instance.web.Loading(this); - this.loading.appendTo(this.$element); + self.bind_hashchange(); + self.set_title(); }, destroy_content: function() { _.each(_.clone(this.getChildren()), function(el) { el.destroy(); }); - this.$element.children().remove(); + this.$el.children().remove(); }, do_reload: function() { var self = this; return this.session.session_reload().pipe(function () { - instance.connection.load_modules(true).pipe( + instance.session.load_modules(true).pipe( self.menu.proxy('do_reload')); }); }, @@ -940,28 +1071,28 @@ instance.web.WebClient = instance.web.Widget.extend({ }, on_logout: function() { var self = this; - this.session.session_logout().then(function () { - $(window).unbind('hashchange', self.on_hashchange); - self.do_push_state({}); - //would be cool to be able to do this, but I think it will make addons do strange things - //this.show_login(); - window.location.reload(); - }); + if (!this.has_uncommitted_changes()) { + this.session.session_logout().then(function () { + $(window).unbind('hashchange', self.on_hashchange); + self.do_push_state({}); + window.location.reload(); + }); + } }, bind_hashchange: function() { var self = this; $(window).bind('hashchange', this.on_hashchange); var state = $.bbq.getState(true); - if (! _.isEmpty(state)) { - $(window).trigger('hashchange'); - } else { + if (_.isEmpty(state) || state.action == "login") { self.menu.has_been_loaded.then(function() { - var first_menu_id = self.menu.$element.find("a:first").data("menu"); + var first_menu_id = self.menu.$el.find("a:first").data("menu"); if(first_menu_id) { self.menu.menu_click(first_menu_id); } }); + } else { + $(window).trigger('hashchange'); } }, on_hashchange: function(event) { @@ -975,61 +1106,85 @@ instance.web.WebClient = instance.web.Widget.extend({ }); }); } else { + state._push_me = false; // no need to push state back... this.action_manager.do_load_state(state, !!this._current_state); } } this._current_state = state; }, do_push_state: function(state) { + this.set_title(state.title); + delete state.title; var url = '#' + $.param(state); this._current_state = _.clone(state); $.bbq.pushState(url); }, - on_menu_action: function(action) { - this.action_manager.do_action(action); - }, - do_action: function(action) { + on_menu_action: function(options) { var self = this; - // TODO replace by client action menuclick - if(action.menu_id) { - this.do_reload().then(function () { - self.menu.menu_click(action.menu_id); + return this.rpc("/web/action/load", { action_id: options.action_id }) + .pipe(function (result) { + var action = result; + if (options.needaction) { + action.context.search_default_message_unread = true; + } + return $.when(self.action_manager.do_action(action, { + clear_breadcrumbs: true, + action_menu_id: self.menu.current_menu, + })).fail(function() { + self.menu.open_menu(options.previous_menu_id); + }); }); - } }, set_content_full_screen: function(fullscreen) { - if (fullscreen) - $(".oe_webclient", this.$element).addClass("oe_content_full_screen"); - else - $(".oe_webclient", this.$element).removeClass("oe_content_full_screen"); - } + if (fullscreen) { + $(".oe_webclient", this.$el).addClass("oe_content_full_screen"); + $("body").css({'overflow-y':'hidden'}); + } else { + $(".oe_webclient", this.$el).removeClass("oe_content_full_screen"); + $("body").css({'overflow-y':'scroll'}); + } + }, + has_uncommitted_changes: function() { + var $e = $.Event('clear_uncommitted_changes'); + instance.web.bus.trigger('clear_uncommitted_changes', $e); + if ($e.isDefaultPrevented()) { + return true; + } else { + return this._super.apply(this, arguments); + } + }, }); -instance.web.EmbeddedClient = instance.web.Widget.extend({ - template: 'EmptyComponent', - init: function(parent, action_id, options) { - this._super(parent); - // TODO take the xmlid of a action instead of its id +instance.web.EmbeddedClient = instance.web.Client.extend({ + _template: 'EmbedClient', + init: function(parent, origin, dbname, login, key, action_id, options) { + this._super(parent, origin); + + this.dbname = dbname; + this.login = login; + this.key = key; this.action_id = action_id; this.options = options || {}; - this.am = new instance.web.ActionManager(this); }, start: function() { var self = this; - this.am.appendTo(this.$element.addClass('openerp')); - return this.rpc("/web/action/load", { action_id: this.action_id }, function(result) { - var action = result.result; - action.flags = _.extend({ - //views_switcher : false, - search_view : false, - action_buttons : false, - sidebar : false - //pager : false - }, self.options, action.flags || {}); + return $.when(this._super()).pipe(function() { + return instance.session.session_authenticate(self.dbname, self.login, self.key, true).pipe(function() { + return self.rpc("/web/action/load", { action_id: self.action_id }).then(function(result) { + var action = result; + action.flags = _.extend({ + //views_switcher : false, + search_view : false, + action_buttons : false, + sidebar : false + //pager : false + }, self.options, action.flags || {}); - self.am.do_action(action); + self.action_manager.do_action(action); + }); + }); }); - } + }, }); instance.web.embed = function (origin, dbname, login, key, action, options) { @@ -1043,13 +1198,8 @@ instance.web.embed = function (origin, dbname, login, key, action, options) { var sc = document.getElementsByTagName('script'); currentScript = sc[sc.length-1]; } - instance.connection.session_bind(origin).then(function () { - instance.connection.session_authenticate(dbname, login, key, true).then(function () { - var client = new instance.web.EmbeddedClient(null, action, options); - client.insertAfter(currentScript); - }); - }); - + var client = new instance.web.EmbeddedClient(null, origin, dbname, login, key, action, options); + client.insertAfter(currentScript); }; }; diff --git a/addons/web/static/src/js/corelib.js b/addons/web/static/src/js/corelib.js index 93796ab4282..6ecbfdd01ac 100644 --- a/addons/web/static/src/js/corelib.js +++ b/addons/web/static/src/js/corelib.js @@ -99,7 +99,6 @@ openerp.web.corelib = function(instance) { for (var name in prop) { // Check if we're overwriting an existing function prototype[name] = typeof prop[name] == "function" && - typeof _super[name] == "function" && fnTest.test(prop[name]) ? (function(name, fn) { return function() { @@ -122,6 +121,10 @@ openerp.web.corelib = function(instance) { // The dummy class constructor function Class() { + if(this.constructor !== instance.web.Class){ + throw new Error("You can only instanciate objects with the 'new' operator"); + return null; + } // All construction is actually done in the init method if (!initializing && this.init) { var ret = this.init.apply(this, arguments); @@ -288,6 +291,17 @@ var Events = instance.web.Class.extend({ return this; }, + callbackList: function() { + var lst = []; + _.each(this._callbacks || {}, function(el, eventName) { + var node = el; + while ((node = node.next) && node.next) { + lst.push([eventName, node.callback, node.context]); + } + }); + return lst; + }, + trigger : function(events) { var event, node, calls, tail, args, all, rest; if (!(calls = this._callbacks)) @@ -331,6 +345,9 @@ instance.web.EventDispatcherMixin = _.extend({}, instance.web.ParentedMixin, { }, on: function(events, dest, func) { var self = this; + if (!(func instanceof Function)) { + throw new Error("Event handler must be a function."); + } events = events.split(/\s+/); _.each(events, function(eventName) { self.__edispatcherEvents.on(eventName, func, dest); @@ -363,9 +380,9 @@ instance.web.EventDispatcherMixin = _.extend({}, instance.web.ParentedMixin, { event.source.__edispatcherEvents.off(event.name, event.func, self); }); this.__edispatcherRegisteredEvents = []; - if(!this.__edispatcherEvents) { - debugger; - } + _.each(this.__edispatcherEvents.callbackList(), function(cal) { + this.off(cal[0], cal[2], cal[1]); + }, this); this.__edispatcherEvents.off(); instance.web.ParentedMixin.destroy.call(this); } @@ -376,7 +393,17 @@ instance.web.PropertiesMixin = _.extend({}, instance.web.EventDispatcherMixin, { instance.web.EventDispatcherMixin.init.call(this); this.__getterSetterInternalMap = {}; }, - set: function(map) { + set: function(arg1, arg2, arg3) { + var map; + var options; + if (typeof arg1 === "string") { + map = {}; + map[arg1] = arg2; + options = arg3 || {}; + } else { + map = arg1; + options = arg2 || {}; + } var self = this; var changed = false; _.each(map, function(val, key) { @@ -385,10 +412,11 @@ instance.web.PropertiesMixin = _.extend({}, instance.web.EventDispatcherMixin, { return; changed = true; self.__getterSetterInternalMap[key] = val; - self.trigger("change:" + key, self, { - oldValue: tmp, - newValue: val - }); + if (! options.silent) + self.trigger("change:" + key, self, { + oldValue: tmp, + newValue: val + }); }); if (changed) self.trigger("change", self); @@ -402,71 +430,16 @@ instance.web.CallbackEnabledMixin = _.extend({}, instance.web.PropertiesMixin, { init: function() { instance.web.PropertiesMixin.init.call(this); var self = this; - var callback_maker = function(obj, name, method) { - var callback = function() { - var args = Array.prototype.slice.call(arguments); - self.trigger.apply(self, [name].concat(args)); - var r; - for(var i = 0; i < callback.callback_chain.length; i++) { - var c = callback.callback_chain[i]; - if(c.unique) { - callback.callback_chain.splice(i, 1); - i -= 1; - } - var result = c.callback.apply(c.self, c.args.concat(args)); - if (c.callback === method) { - // return the result of the original method - r = result; - } - // TODO special value to stop the chain - // instance.web.callback_stop - } - return r; - }; - callback.callback_chain = []; - callback.add = function(f) { - if(typeof(f) == 'function') { - f = { callback: f, args: Array.prototype.slice.call(arguments, 1) }; - } - f.self = f.self || null; - f.args = f.args || []; - f.unique = !!f.unique; - if(f.position == 'last') { - callback.callback_chain.push(f); - } else { - callback.callback_chain.unshift(f); - } - return callback; - }; - callback.add_first = function(f) { - return callback.add.apply(null,arguments); - }; - callback.add_last = function(f) { - return callback.add({ - callback: f, - args: Array.prototype.slice.call(arguments, 1), - position: "last" - }); - }; - callback.remove = function(f) { - callback.callback_chain = _.difference(callback.callback_chain, _.filter(callback.callback_chain, function(el) { - return el.callback === f; - })); - return callback; - }; - - return callback.add({ - callback: method, - self:obj, - args:Array.prototype.slice.call(arguments, 3) - }); - }; // Transform on_/do_* methods into callbacks + var callback_maker = function(fn) { + return function() { + return fn.apply(self, arguments); + } + }; for (var name in this) { if(typeof(this[name]) == "function") { - this[name].debug_name = name; if((/^on_|^do_/).test(name)) { - this[name] = callback_maker(this, name, this[name]); + this[name] = callback_maker(this[name]); } } } @@ -487,24 +460,20 @@ instance.web.CallbackEnabledMixin = _.extend({}, instance.web.PropertiesMixin, { * * The semantics of this precisely replace closing over the method call. * - * @param {String} method_name name of the method to invoke + * @param {String|Function} method function or name of the method to invoke * @returns {Function} proxied method */ - proxy: function (method_name) { + proxy: function (method) { var self = this; return function () { - return self[method_name].apply(self, arguments); + var fn = (typeof method === 'string') ? self[method] : method; + return fn.apply(self, arguments); } } }); instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, { /** - * Tag name when creating a default $element. - * @type string - */ - tagName: 'div', - /** * Constructs the widget and sets its parent if a parent is given. * * @constructs instance.web.Widget @@ -513,14 +482,9 @@ instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, { * @param {instance.web.Widget} parent Binds the current instance to the given Widget instance. * When that widget is destroyed by calling destroy(), the current instance will be * destroyed too. Can be null. - * @param {String} element_id Deprecated. Sets the element_id. Only useful when you want - * to bind the current Widget to an already existing part of the DOM, which is not compatible - * with the DOM insertion methods provided by the current implementation of Widget. So - * for new components this argument should not be provided any more. */ init: function(parent) { instance.web.CallbackEnabledMixin.init.call(this); - this.$element = $(document.createElement(this.tagName)); this.setParent(parent); }, /** @@ -530,8 +494,8 @@ instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, { _.each(this.getChildren(), function(el) { el.destroy(); }); - if(this.$element != null) { - this.$element.remove(); + if(this.$el) { + this.$el.remove(); } instance.web.PropertiesMixin.destroy.call(this); }, @@ -543,7 +507,7 @@ instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, { appendTo: function(target) { var self = this; return this.__widgetRenderAndInsert(function(t) { - self.$element.appendTo(t); + self.$el.appendTo(t); }, target); }, /** @@ -554,7 +518,7 @@ instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, { prependTo: function(target) { var self = this; return this.__widgetRenderAndInsert(function(t) { - self.$element.prependTo(t); + self.$el.prependTo(t); }, target); }, /** @@ -565,7 +529,7 @@ instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, { insertAfter: function(target) { var self = this; return this.__widgetRenderAndInsert(function(t) { - self.$element.insertAfter(t); + self.$el.insertAfter(t); }, target); }, /** @@ -576,7 +540,7 @@ instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, { insertBefore: function(target) { var self = this; return this.__widgetRenderAndInsert(function(t) { - self.$element.insertBefore(t); + self.$el.insertBefore(t); }, target); }, /** @@ -586,7 +550,7 @@ instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, { */ replace: function(target) { return this.__widgetRenderAndInsert(_.bind(function(t) { - this.$element.replaceAll(t); + this.$el.replaceAll(t); }, this), target); }, __widgetRenderAndInsert: function(insertion, target) { @@ -609,6 +573,7 @@ instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, { * @returns {jQuery.Deferred} */ start: function() { + return $.when(); } }); @@ -644,8 +609,8 @@ instance.web.CallbackEnabled = instance.web.Class.extend(instance.web.CallbackEn * // stuff that you want to init before the rendering * }, * start: function() { - * // stuff you want to make after the rendering, `this.$element` holds a correct value - * this.$element.find(".my_button").click(/* an example of event binding * /); + * // stuff you want to make after the rendering, `this.$el` holds a correct value + * this.$el.find(".my_button").click(/* an example of event binding * /); * * // if you have some asynchronous operations, it's a good idea to return * // a promise in start() @@ -669,6 +634,12 @@ instance.web.CallbackEnabled = instance.web.Class.extend(instance.web.CallbackEn * That will kill the widget in a clean way and erase its content from the dom. */ instance.web.Widget = instance.web.Class.extend(instance.web.WidgetMixin, { + // Backbone-ish API + tagName: 'div', + id: null, + className: null, + attributes: {}, + events: {}, /** * The name of the QWeb template that will be used for rendering. Must be * redefined in subclasses or the default render() method can not be used. @@ -685,14 +656,12 @@ instance.web.Widget = instance.web.Class.extend(instance.web.WidgetMixin, { * @param {instance.web.Widget} parent Binds the current instance to the given Widget instance. * When that widget is destroyed by calling destroy(), the current instance will be * destroyed too. Can be null. - * @param {String} element_id Deprecated. Sets the element_id. Only useful when you want - * to bind the current Widget to an already existing part of the DOM, which is not compatible - * with the DOM insertion methods provided by the current implementation of Widget. So - * for new components this argument should not be provided any more. */ init: function(parent) { instance.web.WidgetMixin.init.call(this,parent); - this.session = instance.connection; + // FIXME: this should not be + this.setElement(this._make_descriptive()); + this.session = instance.session; }, /** * Renders the element. The default implementation renders the widget using QWeb, @@ -700,29 +669,130 @@ instance.web.Widget = instance.web.Class.extend(instance.web.WidgetMixin, { * key that references `this`. */ renderElement: function() { - var rendered = null; - if (this.template) - rendered = instance.web.qweb.render(this.template, {widget: this}); - if (_.str.trim(rendered)) { - var elem = $(rendered); - this.$element.replaceWith(elem); - this.$element = elem; + var $el; + if (this.template) { + $el = $(_.str.trim(instance.web.qweb.render( + this.template, {widget: this}))); + } else { + $el = this._make_descriptive(); + } + this.replaceElement($el); + }, + + /** + * Re-sets the widget's root element and replaces the old root element + * (if any) by the new one in the DOM. + * + * @param {HTMLElement | jQuery} $el + * @returns {*} this + */ + replaceElement: function ($el) { + var $oldel = this.$el; + this.setElement($el); + if ($oldel && !$oldel.is(this.$el)) { + $oldel.replaceWith(this.$el); } + return this; }, /** - * Shortcut for $element.find() like backbone + * Re-sets the widget's root element (el/$el/$el). + * + * Includes: + * * re-delegating events + * * re-binding sub-elements + * * if the widget already had a root element, replacing the pre-existing + * element in the DOM + * + * @param {HTMLElement | jQuery} element new root element for the widget + * @return {*} this */ - "$": function() { - return this.$element.find.apply(this.$element,arguments); + setElement: function (element) { + // NB: completely useless, as WidgetMixin#init creates a $el + // always + if (this.$el) { + this.undelegateEvents(); + } + + this.$el = (element instanceof $) ? element : $(element); + this.el = this.$el[0]; + + this.delegateEvents(); + + return this; + }, + /** + * Utility function to build small DOM elements. + * + * @param {String} tagName name of the DOM element to create + * @param {Object} [attributes] map of DOM attributes to set on the element + * @param {String} [content] HTML content to set on the element + * @return {Element} + */ + make: function (tagName, attributes, content) { + var el = document.createElement(tagName); + if (!_.isEmpty(attributes)) { + $(el).attr(attributes); + } + if (content) { + $(el).html(content); + } + return el; + }, + /** + * Makes a potential root element from the declarative builder of the + * widget + * + * @return {jQuery} + * @private + */ + _make_descriptive: function () { + var attrs = _.extend({}, this.attributes || {}); + if (this.id) { attrs.id = this.id; } + if (this.className) { attrs['class'] = this.className; } + return $(this.make(this.tagName, attrs)); + }, + delegateEvents: function () { + var events = this.events; + if (_.isEmpty(events)) { return; } + + for(var key in events) { + if (!events.hasOwnProperty(key)) { continue; } + + var method = this.proxy(events[key]); + + var match = /^(\S+)(\s+(.*))?$/.exec(key); + var event = match[1]; + var selector = match[3]; + + event += '.widget_events'; + if (!selector) { + this.$el.on(event, method); + } else { + this.$el.on(event, selector, method); + } + } + }, + undelegateEvents: function () { + this.$el.off('.widget_events'); + }, + /** + * Shortcut for ``this.$el.find(selector)`` + * + * @param {String} selector CSS selector, rooted in $el + * @returns {jQuery} selector match + */ + $: function(selector) { + return this.$el.find(selector); }, /** * Informs the action manager to do an action. This supposes that * the action manager can be found amongst the ancestors of the current widget. * If that's not the case this method will simply return `false`. */ - do_action: function(action, on_finished) { - if (this.getParent()) { - return this.getParent().do_action(action, on_finished); + do_action: function() { + var parent = this.getParent(); + if (parent) { + return parent.do_action.apply(parent, arguments); } return false; }, @@ -741,7 +811,7 @@ instance.web.Widget = instance.web.Class.extend(instance.web.WidgetMixin, { rpc: function(url, data, success, error) { var def = $.Deferred().then(success, error); var self = this; - instance.connection.rpc(url, data). then(function() { + instance.session.rpc(url, data).then(function() { if (!self.isDestroyed()) def.resolve.apply(def, arguments); }, function() { @@ -763,7 +833,7 @@ instance.web.Registry = instance.web.Class.extend({ * * An object path is simply a dotted name from the instance root to the * object pointed to (e.g. ``"instance.web.Session"`` for an OpenERP - * connection object). + * session object). * * @constructs instance.web.Registry * @param {Object} mapping a mapping of keys to object-paths @@ -872,6 +942,12 @@ instance.web.Registry = instance.web.Class.extend({ }); instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ + triggers: { + 'request': 'Request sent', + 'response': 'Response received', + 'response_failed': 'HTTP Error response or timeout received', + 'error': 'The received response is an JSON-RPC error', + }, /** * @constructs instance.web.JsonRPC * @extends instance.web.CallbackEnabled @@ -1035,7 +1111,8 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ uid: new py.float(this.uid), datetime: datetime, time: time, - relativedelta: relativedelta + relativedelta: relativedelta, + current_date: date.today.__call__().strftime(['%Y-%m-%d']) }; }, /** @@ -1193,7 +1270,7 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ * @param {Function} error_callback function to execute on RPC call failure * @returns {jQuery.Deferred} jquery-provided ajax deferred */ - rpc: function(url, params, success_callback, error_callback) { + rpc: function(url, params) { var self = this; // url can be an $.ajax option object if (_.isString(url)) { @@ -1209,12 +1286,10 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ id: _.uniqueId('r') }; var deferred = $.Deferred(); - this.on_rpc_request(); - var aborter = params.aborter; - delete params.aborter; + this.trigger('request', url, payload); var request = this.rpc_function(url, payload).then( function (response, textStatus, jqXHR) { - self.on_rpc_response(); + self.trigger('response', response); if (!response.error) { if (url.url === '/web/session/eval_domain_and_context') { self.test_eval(params, response.result); @@ -1222,18 +1297,12 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ deferred.resolve(response["result"], textStatus, jqXHR); } else if (response.error.data.type === "session_invalid") { self.uid = false; - // TODO deprecate or use a deferred on login.do_ask_login() - self.on_session_invalid(function() { - self.rpc(url, payload.params, - function() { deferred.resolve.apply(deferred, arguments); }, - function() { deferred.reject.apply(deferred, arguments); }); - }); } else { deferred.reject(response.error, $.Event()); } }, function(jqXHR, textStatus, errorThrown) { - self.on_rpc_response(); + self.trigger('response_failed', jqXHR); var error = { code: -32098, message: "XmlHttpRequestError " + errorThrown, @@ -1241,24 +1310,14 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ }; deferred.reject(error, $.Event()); }); - if (aborter) { - aborter.abort_last = function () { - if (!(request.isResolved() || request.isRejected())) { - deferred.fail(function (error, event) { - event.preventDefault(); - }); - request.abort(); - } - }; - } // Allow deferred user to disable on_rpc_error in fail deferred.fail(function() { deferred.fail(function(error, event) { if (!event.isDefaultPrevented()) { - self.on_rpc_error(error, event); + self.trigger('error', error, event); } }); - }).then(success_callback, error_callback).promise(); + }); return deferred; }, /** @@ -1276,7 +1335,7 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ processData: false }, url); if (this.synch) - ajax.async = false; + ajax.async = false; return $.ajax(ajax); }, rpc_jsonp: function(url, payload) { @@ -1295,7 +1354,7 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ data: data }, url); if (this.synch) - ajax.async = false; + ajax.async = false; var payload_str = JSON.stringify(payload); var payload_url = $.param({r:payload_str}); if(payload_url.length < 2000) { @@ -1305,19 +1364,19 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ } else { // Indirect jsonp request var ifid = _.uniqueId('oe_rpc_iframe'); - var display = options.openerp.debug ? 'block' : 'none'; + var display = self.debug ? 'block' : 'none'; var $iframe = $(_.str.sprintf("<iframe src='javascript:false;' name='%s' id='%s' style='display:%s'></iframe>", ifid, ifid, display)); var $form = $('<form>') .attr('method', 'POST') .attr('target', ifid) .attr('enctype', "multipart/form-data") - .attr('action', ajax.url + '?' + $.param(data)) + .attr('action', ajax.url + '?jsonp=1&' + $.param(data)) .append($('<input type="hidden" name="r" />').attr('value', payload_str)) .hide() .appendTo($('body')); var cleanUp = function() { if ($iframe) { - $iframe.unbind("load").attr("src", "javascript:false;").remove(); + $iframe.unbind("load").remove(); } $form.remove(); }; @@ -1341,14 +1400,15 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ return deferred; } }, - on_rpc_request: function() { - }, - on_rpc_response: function() { - }, - on_rpc_error: function(error) { + get_url: function (file) { + return this.prefix + file; }, }); +instance.web.py_eval = function(expr, context) { + return py.eval(expr, _.extend({}, context || {}, {"true": true, "false": false, "null": null})); +}; + } // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: diff --git a/addons/web/static/src/js/coresetup.js b/addons/web/static/src/js/coresetup.js index 5a8a2b50722..220b9b63d5c 100644 --- a/addons/web/static/src/js/coresetup.js +++ b/addons/web/static/src/js/coresetup.js @@ -11,33 +11,6 @@ if (!console.debug) { openerp.web.coresetup = function(instance) { -/** - * @deprecated use :class:`instance.web.Widget` - */ -instance.web.OldWidget = instance.web.Widget.extend({ - init: function(parent, element_id) { - this._super(parent); - this.element_id = element_id; - this.element_id = this.element_id || _.uniqueId('widget-'); - var tmp = document.getElementById(this.element_id); - this.$element = tmp ? $(tmp) : $(document.createElement(this.tagName)); - }, - renderElement: function() { - var rendered = this.render(); - if (rendered) { - var elem = $(rendered); - this.$element.replaceWith(elem); - this.$element = elem; - } - return this; - }, - render: function (additional) { - if (this.template) - return instance.web.qweb.render(this.template, _.extend({widget: this}, additional || {})); - return null; - } -}); - /** Session openerp specific RPC class */ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Session# */{ init: function() { @@ -46,9 +19,9 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess this.name = instance._session_id; this.qweb_mutex = new $.Mutex(); }, - rpc: function(url, params, success_callback, error_callback) { + rpc: function(url, params) { params.session_id = this.session_id; - return this._super(url, params, success_callback, error_callback); + return this._super(url, params); }, /** * Setup a sessionm @@ -62,7 +35,6 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess this.username = false; this.user_context= {}; this.db = false; - this.openerp_entreprise = false; this.module_list = instance._modules.slice(); this.module_loaded = {}; _(this.module_list).each(function (mod) { @@ -85,7 +57,12 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess if(self.session_is_valid()) { return deferred.pipe(function() { return self.load_modules(); }); } - return deferred; + return $.when( + deferred, + self.rpc('/web/webclient/bootstrap_translations', {mods: instance._modules}).pipe(function(trans) { + instance.web._t.database.set_bundle(trans); + }) + ); }); }, /** @@ -105,8 +82,7 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess db: result.db, username: result.login, uid: result.uid, - user_context: result.context, - openerp_entreprise: result.openerp_entreprise + user_context: result.context }); }); }, @@ -121,13 +97,16 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess var base_location = document.location.protocol + '//' + document.location.host; var params = { db: db, login: login, password: password, base_location: base_location }; return this.rpc("/web/session/authenticate", params).pipe(function(result) { + if (!result.uid) { + return $.Deferred().reject(); + } + _.extend(self, { session_id: result.session_id, db: result.db, username: result.login, uid: result.uid, - user_context: result.context, - openerp_entreprise: result.openerp_entreprise + user_context: result.context }); if (!_volatile) { self.set_cookie('session_id', self.session_id); @@ -139,20 +118,6 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess this.set_cookie('session_id', ''); return this.rpc("/web/session/destroy", {}); }, - on_session_valid: function() { - }, - /** - * Called when a rpc call fail due to an invalid session. - * By default, it's a noop - */ - on_session_invalid: function(retry_callback) { - }, - /** - * Fetches a cookie stored by an openerp session - * - * @private - * @param name the cookie's name - */ get_cookie: function (name) { if (!this.name) { return null; } var nameEQ = this.name + '|' + name + '='; @@ -186,9 +151,8 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess /** * Load additional web addons of that instance and init them * - * @param {Boolean} [no_session_valid_signal=false] prevents load_module from triggering ``on_session_valid``. */ - load_modules: function(no_session_valid_signal) { + load_modules: function() { var self = this; return this.rpc('/web/session/modules', {}).pipe(function(result) { var lang = self.user_context.lang, @@ -197,31 +161,31 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess var to_load = _.difference(result, self.module_list).join(','); self.module_list = all_modules; - var loaded = $.Deferred().resolve().promise(); - if (to_load.length) { + var loaded = self.rpc('/web/webclient/translations', params).then(function(trans) { + instance.web._t.database.set_bundle(trans); + }); + var file_list = ["/web/static/lib/datejs/globalization/" + lang.replace("_", "-") + ".js"]; + if(to_load.length) { loaded = $.when( - self.rpc('/web/webclient/csslist', {mods: to_load}, self.do_load_css), + loaded, + self.rpc('/web/webclient/csslist', {mods: to_load}).then(self.do_load_css), self.rpc('/web/webclient/qweblist', {mods: to_load}).pipe(self.do_load_qweb), - self.rpc('/web/webclient/translations', params).pipe(function(trans) { - instance.web._t.database.set_bundle(trans); - var file_list = ["/web/static/lib/datejs/globalization/" + lang.replace("_", "-") + ".js"]; - return self.rpc('/web/webclient/jslist', {mods: to_load}).pipe(function(files) { - return self.do_load_js(file_list.concat(files)); - }).then(function () { - if (!Date.CultureInfo.pmDesignator) { - // If no am/pm designator is specified but the openerp - // datetime format uses %i, date.js won't be able to - // correctly format a date. See bug#938497. - Date.CultureInfo.amDesignator = 'AM'; - Date.CultureInfo.pmDesignator = 'PM'; - } - }); - })) + self.rpc('/web/webclient/jslist', {mods: to_load}).then(function(files) { + file_list = file_list.concat(files); + }) + ); } - return loaded.then(function() { + return loaded.pipe(function () { + return self.do_load_js(file_list); + }).then(function() { self.on_modules_loaded(); - if (!no_session_valid_signal) { - self.on_session_valid(); + self.trigger('module_loaded'); + if (!Date.CultureInfo.pmDesignator) { + // If no am/pm designator is specified but the openerp + // datetime format uses %i, date.js won't be able to + // correctly format a date. See bug#938497. + Date.CultureInfo.amDesignator = 'AM'; + Date.CultureInfo.pmDesignator = 'PM'; } }); }); @@ -284,9 +248,6 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess } } }, - get_url: function (file) { - return this.prefix + file; - }, /** * Cooperative file download implementation, for ajaxy APIs. * @@ -332,11 +293,14 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess .appendTo(document.body) .load(function () { try { - if (options.error) { - options.error(JSON.parse( - this.contentDocument.body.childNodes[1].textContent - )); - } + if (options.error) { + if (!this.contentDocument.body.childNodes[1]) { + options.error(this.contentDocument.body.childNodes); + } + else { + options.error(JSON.parse(this.contentDocument.body.childNodes[1].textContent)); + } + } } finally { complete(); } @@ -389,16 +353,41 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess timer = setTimeout(waitLoop, CHECK_INTERVAL); }, synchronized_mode: function(to_execute) { - var synch = this.synch; - this.synch = true; - try { - return to_execute(); - } finally { - this.synch = synch; - } + var synch = this.synch; + this.synch = true; + try { + return to_execute(); + } finally { + this.synch = synch; + } } }); + +/** + * Event Bus used to bind events scoped in the current instance + */ +instance.web.Bus = instance.web.Class.extend(instance.web.EventDispatcherMixin, { + init: function() { + instance.web.EventDispatcherMixin.init.call(this, parent); + var self = this; + // TODO fme: allow user to bind keys for some global actions. + // check gtk bindings + // http://unixpapa.com/js/key.html + _.each('click,dblclick,keydown,keypress,keyup'.split(','), function(evtype) { + $('html').on(evtype, function(ev) { + self.trigger(evtype, ev); + }); + }); + _.each('resize,scroll'.split(','), function(evtype) { + $(window).on(evtype, function(ev) { + self.trigger(evtype, ev); + }); + }); + } +}) +instance.web.bus = new instance.web.Bus(); + /** OpenERP Translations */ instance.web.TranslationDataBase = instance.web.Class.extend(/** @lends instance.web.TranslationDataBase# */{ /** @@ -453,64 +442,6 @@ instance.web.TranslationDataBase = instance.web.Class.extend(/** @lends instance } }); -/** Configure default qweb */ -instance.web._t = new instance.web.TranslationDataBase().build_translation_function(); -/** - * Lazy translation function, only performs the translation when actually - * printed (e.g. inserted into a template) - * - * Useful when defining translatable strings in code evaluated before the - * translation database is loaded, as class attributes or at the top-level of - * an OpenERP Web module - * - * @param {String} s string to translate - * @returns {Object} lazy translation object - */ -instance.web._lt = function (s) { - return {toString: function () { return instance.web._t(s); }} -}; -instance.web.qweb = new QWeb2.Engine(); -instance.web.qweb.debug = ($.deparam($.param.querystring()).debug != undefined); -instance.web.qweb.default_dict = { - '_' : _, - '_t' : instance.web._t -}; -instance.web.qweb.preprocess_node = function() { - // Note that 'this' is the Qweb Node - switch (this.node.nodeType) { - case 3: - case 4: - // Text and CDATAs - var translation = this.node.parentNode.attributes['t-translation']; - if (translation && translation.value === 'off') { - return; - } - var ts = _.str.trim(this.node.data); - if (ts.length === 0) { - return; - } - var tr = instance.web._t(ts); - if (tr !== ts) { - this.node.data = tr; - } - break; - case 1: - // Element - var attr, attrs = ['label', 'title', 'alt']; - while (attr = attrs.pop()) { - if (this.attributes[attr]) { - this.attributes[attr] = instance.web._t(this.attributes[attr]); - } - } - } -}; - -/** Configure blockui */ -if ($.blockUI) { - $.blockUI.defaults.baseZ = 1100; - $.blockUI.defaults.message = '<img src="/web/static/src/img/throbber2.gif">'; -} - /** Custom jQuery plugins */ $.fn.getAttributes = function() { var o = {}; @@ -540,10 +471,6 @@ $.Mutex = (function() { return Mutex; })(); -/** Setup default session */ -instance.connection = new instance.web.Session(); -instance.web.qweb.default_dict['__debug__'] = instance.connection.debug; - $.async_when = function() { var async = false; var def = $.Deferred(); @@ -573,38 +500,176 @@ $.async_when = function() { // special tweak for the web client var old_async_when = $.async_when; $.async_when = function() { - if (instance.connection.synch) - return $.when.apply(this, arguments); - else - return old_async_when.apply(this, arguments); + if (instance.session.synch) + return $.when.apply(this, arguments); + else + return old_async_when.apply(this, arguments); }; +/** Setup default session */ +instance.session = new instance.web.Session(); + +/** Configure default qweb */ +instance.web._t = new instance.web.TranslationDataBase().build_translation_function(); /** - * Registry for all the client actions key: tag value: widget + * Lazy translation function, only performs the translation when actually + * printed (e.g. inserted into a template) + * + * Useful when defining translatable strings in code evaluated before the + * translation database is loaded, as class attributes or at the top-level of + * an OpenERP Web module + * + * @param {String} s string to translate + * @returns {Object} lazy translation object */ -instance.web.client_actions = new instance.web.Registry(); +instance.web._lt = function (s) { + return {toString: function () { return instance.web._t(s); }} +}; +instance.web.qweb = new QWeb2.Engine(); +instance.web.qweb.default_dict['__debug__'] = instance.session.debug; // Which one ? +instance.web.qweb.debug = instance.session.debug; +instance.web.qweb.default_dict = { + '_' : _, + '_t' : instance.web._t, + 'JSON': JSON, +}; +instance.web.qweb.preprocess_node = function() { + // Note that 'this' is the Qweb Node + switch (this.node.nodeType) { + case 3: + case 4: + // Text and CDATAs + var translation = this.node.parentNode.attributes['t-translation']; + if (translation && translation.value === 'off') { + return; + } + var match = /^(\s*)(.+?)(\s*)$/.exec(this.node.data); + if (match) { + this.node.data = match[1] + instance.web._t(match[2]) + match[3]; + } + break; + case 1: + // Element + var attr, attrs = ['label', 'title', 'alt', 'placeholder']; + while (attr = attrs.pop()) { + if (this.attributes[attr]) { + this.attributes[attr] = instance.web._t(this.attributes[attr]); + } + } + } +}; -/** - * Client action to reload the whole interface. - * If params has an entry 'menu_id', it opens the given menu entry. +/** Setup jQuery timeago */ +var _t = instance.web._t; +/* + * Strings in timeago are "composed" with prefixes, words and suffixes. This + * makes their detection by our translating system impossible. Use all literal + * strings we're using with a translation mark here so the extractor can do its + * job. */ -instance.web.client_actions.add("reload", "instance.web.Reload"); +{ + _t('less than a minute ago'); + _t('about a minute ago'); + _t('%d minutes ago'); + _t('about an hour ago'); + _t('%d hours ago'); + _t('a day ago'); + _t('%d days ago'); + _t('about a month ago'); + _t('%d months ago'); + _t('about a year ago'); + _t('%d years ago'); +} -instance.web.Reload = instance.web.Widget.extend({ - init: function(parent, params) { - this._super(parent); - this.menu_id = (params && params.menu_id) || false; - }, +instance.session.on('module_loaded', this, function () { + // provide timeago.js with our own translator method + $.timeago.settings.translator = instance.web._t; +}); + +/** Setup blockui */ +if ($.blockUI) { + $.blockUI.defaults.baseZ = 1100; + $.blockUI.defaults.message = '<div class="openerp oe_blockui_spin_container" style="background-color: transparent;">'; + $.blockUI.defaults.css.border = '0'; + $.blockUI.defaults.css["background-color"] = ''; +} + +var messages_by_seconds = function() { + return [ + [0, _t("Loading...")], + [20, _t("Still loading...")], + [60, _t("Still loading...<br />Please be patient.")], + [120, _t("Don't leave yet,<br />it's still loading...")], + [300, _t("You may not believe it,<br />but the application is actually loading...")], + [420, _t("Take a minute to get a coffee,<br />because it's loading...")], + [3600, _t("Maybe you should consider reloading the application by pressing F5...")], + ]; +}; + +instance.web.Throbber = instance.web.Widget.extend({ + template: "Throbber", start: function() { - if (this.menu_id) { - // open the given menu id - var url_without_fragment = window.location.toString().split("#", 1)[0]; - window.location = url_without_fragment + "#menu_id=" + this.menu_id; - } else { - window.location.reload(); - } - } + var opts = { + lines: 13, // The number of lines to draw + length: 7, // The length of each line + width: 4, // The line thickness + radius: 10, // The radius of the inner circle + rotate: 0, // The rotation offset + color: '#FFF', // #rgb or #rrggbb + speed: 1, // Rounds per second + trail: 60, // Afterglow percentage + shadow: false, // Whether to render a shadow + hwaccel: false, // Whether to use hardware acceleration + className: 'spinner', // The CSS class to assign to the spinner + zIndex: 2e9, // The z-index (defaults to 2000000000) + top: 'auto', // Top position relative to parent in px + left: 'auto' // Left position relative to parent in px + }; + this.spin = new Spinner(opts).spin(this.$el[0]); + this.start_time = new Date().getTime(); + this.act_message(); + }, + act_message: function() { + var self = this; + setTimeout(function() { + if (self.isDestroyed()) + return; + var seconds = (new Date().getTime() - self.start_time) / 1000; + var mes; + _.each(messages_by_seconds(), function(el) { + if (seconds >= el[0]) + mes = el[1]; + }); + self.$(".oe_throbber_message").html(mes); + self.act_message(); + }, 1000); + }, + destroy: function() { + if (this.spin) + this.spin.stop(); + this._super(); + }, }); +instance.web.Throbber.throbbers = []; + +instance.web.blockUI = function() { + var tmp = $.blockUI.apply($, arguments); + var throbber = new instance.web.Throbber(); + instance.web.Throbber.throbbers.push(throbber); + throbber.appendTo($(".oe_blockui_spin_container")); + return tmp; +} +instance.web.unblockUI = function() { + _.each(instance.web.Throbber.throbbers, function(el) { + el.destroy(); + }); + return $.unblockUI.apply($, arguments); +} + +/** + * Registry for all the client actions key: tag value: widget + */ +instance.web.client_actions = new instance.web.Registry(); }; diff --git a/addons/web/static/src/js/data.js b/addons/web/static/src/js/data.js index fdb15d598af..63c90f53758 100644 --- a/addons/web/static/src/js/data.js +++ b/addons/web/static/src/js/data.js @@ -58,7 +58,7 @@ instance.web.Query = instance.web.Class.extend({ }, _execute: function () { var self = this; - return instance.connection.rpc('/web/dataset/search_read', { + return instance.session.rpc('/web/dataset/search_read', { model: this._model.name, fields: this._fields || false, domain: this._model.domain(this._filter), @@ -107,7 +107,7 @@ instance.web.Query = instance.web.Class.extend({ * Performs a groups read according to the provided grouping criterion * * @param {String|Array<String>} grouping - * @returns {jQuery.Deferred<Array<openerp.web.data.Group>> | null} + * @returns {jQuery.Deferred<Array<openerp.web.QueryGroup>> | null} */ group_by: function (grouping) { if (grouping === undefined) { @@ -120,6 +120,10 @@ instance.web.Query = instance.web.Class.extend({ if (_.isEmpty(grouping)) { return null; } var self = this; + + // FIXME: when pyeval is merged + var ctx = instance.session.test_eval_contexts( + [this._model.context(this._context)]); return this._model.call('read_group', { groupby: grouping, fields: _.uniq(grouping.concat(this._fields || [])), @@ -130,7 +134,11 @@ instance.web.Query = instance.web.Class.extend({ orderby: instance.web.serialize_sort(this._order_by) || false }).pipe(function (results) { return _(results).map(function (result) { - return new instance.web.data.Group( + // FIX: querygroup initialization + result.__context = result.__context || {}; + result.__context.group_by = result.__context.group_by || []; + _.defaults(result.__context, ctx); + return new instance.web.QueryGroup( self._model.name, grouping[0], result); }); }); @@ -194,7 +202,54 @@ instance.web.Query = instance.web.Class.extend({ } }); -instance.web.Model = instance.web.Class.extend(/** @lends openerp.web.Model# */{ +instance.web.QueryGroup = instance.web.Class.extend({ + init: function (model, grouping_field, read_group_group) { + // In cases where group_by_no_leaf and no group_by, the result of + // read_group has aggregate fields but no __context or __domain. + // Create default (empty) values for those so that things don't break + var fixed_group = _.extend( + {__context: {group_by: []}, __domain: []}, + read_group_group); + + var aggregates = {}; + _(fixed_group).each(function (value, key) { + if (key.indexOf('__') === 0 + || key === grouping_field + || key === grouping_field + '_count') { + return; + } + aggregates[key] = value || 0; + }); + + this.model = new instance.web.Model( + model, fixed_group.__context, fixed_group.__domain); + + var group_size = fixed_group[grouping_field + '_count'] || fixed_group.__count || 0; + var leaf_group = fixed_group.__context.group_by.length === 0; + this.attributes = { + folded: !!(fixed_group.__fold), + grouped_on: grouping_field, + // if terminal group (or no group) and group_by_no_leaf => use group.__count + length: group_size, + value: fixed_group[grouping_field], + // A group is open-able if it's not a leaf in group_by_no_leaf mode + has_children: !(leaf_group && fixed_group.__context['group_by_no_leaf']), + + aggregates: aggregates + }; + }, + get: function (key) { + return this.attributes[key]; + }, + subgroups: function () { + return this.model.query().group_by(this.model.context().group_by); + }, + query: function () { + return this.model.query.apply(this.model, arguments); + } +}); + +instance.web.Model = instance.web.Class.extend({ /** * @constructs instance.web.Model * @extends instance.web.Class @@ -233,7 +288,8 @@ instance.web.Model = instance.web.Class.extend(/** @lends openerp.web.Model# */{ kwargs = args; args = []; } - return instance.connection.rpc('/web/dataset/call_kw', { + var debug = instance.session.debug ? '/'+this.name+':'+method : ''; + return instance.session.rpc('/web/dataset/call_kw' + debug, { model: this.name, method: method, args: args, @@ -256,7 +312,7 @@ instance.web.Model = instance.web.Class.extend(/** @lends openerp.web.Model# */{ * @param {String} signal signal to trigger on the workflow */ exec_workflow: function (id, signal) { - return instance.connection.rpc('/web/dataset/exec_workflow', { + return instance.session.rpc('/web/dataset/exec_workflow', { model: this.name, id: id, signal: signal @@ -282,7 +338,7 @@ instance.web.Model = instance.web.Class.extend(/** @lends openerp.web.Model# */{ */ context: function (context) { return new instance.web.CompoundContext( - instance.connection.user_context, this._context, context || {}); + instance.session.user_context, this._context, context || {}); }, /** * Button action caller, needs to perform cleanup if an action is returned @@ -292,7 +348,7 @@ instance.web.Model = instance.web.Class.extend(/** @lends openerp.web.Model# */{ * FIXME: remove when evaluator integrated */ call_button: function (method, args) { - return instance.connection.rpc('/web/dataset/call_button', { + return instance.session.rpc('/web/dataset/call_button', { model: this.name, method: method, domain_id: null, @@ -302,235 +358,12 @@ instance.web.Model = instance.web.Class.extend(/** @lends openerp.web.Model# */{ }, }); -instance.web.Traverser = instance.web.Class.extend(/** @lends openerp.web.Traverser# */{ - /** - * @constructs instance.web.Traverser - * @extends instance.web.Class - * - * @param {instance.web.Model} model instance this traverser is bound to - */ - init: function (model) { - this._model = model; - this._index = 0; - }, - - /** - * Gets and sets the current index - * - * @param {Number} [idx] - * @returns {Number} current index - */ - index: function (idx) { - if (idx) { this._index = idx; } - return this._index; - }, - /** - * Returns the model this traverser is currently bound to - * - * @returns {openerp.web.Model} - */ - model: function () { - return this._model; - }, - /** - * Fetches the size of the backing model's match - * - * @returns {Deferred<Number>} deferred count - */ - size: function () { - return this._model.query().count(); - }, - - /** - * Record at the current index for the collection, fails if there is no - * record at the current index. - * - * @returns {Deferred<>} - */ - current: function (fields) { - return this._model.query(fields).first().pipe(function (record) { - if (record == null) { - return $.Deferred() - .reject('No record at index' + this._index) - .promise(); - } - return record; - }); - }, - next: function (fields) { - var self = this; - this._index++; - return this.size().pipe(function (s) { - if (self._index >= s) { - self._index = 0; - } - return self.current(fields); - }); - }, - previous: function (fields) { - var self = this; - this._index--; - if (this._index < 0) { - return this.size().pipe(function (s) { - self._index = s-1; - return self.current(fields); - }); - } - return this.current(fields); - } - -}); - -/** - * Utility objects, should never need to be instantiated from outside of this - * module - * - * @namespace - */ -instance.web.data = { - Group: instance.web.Class.extend(/** @lends openerp.web.data.Group# */{ - /** - * @constructs instance.web.data.Group - * @extends instance.web.Class - */ - init: function (model, grouping_field, read_group_group) { - // In cases where group_by_no_leaf and no group_by, the result of - // read_group has aggregate fields but no __context or __domain. - // Create default (empty) values for those so that things don't break - var fixed_group = _.extend( - {__context: {group_by: []}, __domain: []}, - read_group_group); - - var aggregates = {}; - _(fixed_group).each(function (value, key) { - if (key.indexOf('__') === 0 - || key === grouping_field - || key === grouping_field + '_count') { - return; - } - aggregates[key] = value || 0; - }); - - this.model = new instance.web.Model( - model, fixed_group.__context, fixed_group.__domain); - - var group_size = fixed_group[grouping_field + '_count'] || fixed_group.__count || 0; - var leaf_group = fixed_group.__context.group_by.length === 0; - this.attributes = { - grouped_on: grouping_field, - // if terminal group (or no group) and group_by_no_leaf => use group.__count - length: group_size, - value: fixed_group[grouping_field], - // A group is open-able if it's not a leaf in group_by_no_leaf mode - has_children: !(leaf_group && fixed_group.__context['group_by_no_leaf']), - - aggregates: aggregates - }; - }, - get: function (key) { - return this.attributes[key]; - }, - subgroups: function () { - return this.model.query().group_by(this.model.context().group_by); - }, - query: function () { - return this.model.query.apply(this.model, arguments); - } - }) -}; - -instance.web.DataGroup = instance.web.OldWidget.extend( /** @lends openerp.web.DataGroup# */{ - /** - * Management interface between views and grouped collections of OpenERP - * records. - * - * The root DataGroup is instantiated with the relevant information - * (a session, a model, a domain, a context and a group_by sequence), the - * domain and context may be empty. It is then interacted with via - * :js:func:`~instance.web.DataGroup.list`, which is used to read the - * content of the current grouping level. - * - * @constructs instance.web.DataGroup - * @extends instance.web.OldWidget - * - * @param {instance.web.OldWidget} parent widget - * @param {String} model name of the model managed by this DataGroup - * @param {Array} domain search domain for this DataGroup - * @param {Object} context context of the DataGroup's searches - * @param {Array} group_by sequence of fields by which to group - * @param {Number} [level=0] nesting level of the group - */ - init: function(parent, model, domain, context, group_by, level) { - this._super(parent, null); - this.model = new instance.web.Model(model, context, domain); - this.group_by = group_by; - this.context = context; - this.domain = domain; - - this.level = level || 0; - }, - list: function (fields, ifGroups, ifRecords) { - var self = this; - $.when(this.model.query(fields) - .order_by(this.sort) - .group_by(this.group_by)).then(function (groups) { - if (!groups) { - ifRecords(_.extend( - new instance.web.DataSetSearch( - self, self.model.name, - self.model.context(), - self.model.domain()), - {_sort: self.sort})); - return; - } - ifGroups(_(groups).map(function (group) { - var child_context = _.extend( - {}, self.model.context(), group.model.context()); - return _.extend( - new instance.web.DataGroup( - self, self.model.name, group.model.domain(), - child_context, group.model._context.group_by, - self.level + 1), - { - __context: child_context, - __domain: group.model.domain(), - grouped_on: group.get('grouped_on'), - length: group.get('length'), - value: group.get('value'), - openable: group.get('has_children'), - aggregates: group.get('aggregates') - }, {sort: self.sort}); - })); - }); - } -}); -instance.web.ContainerDataGroup = instance.web.DataGroup.extend({ }); -instance.web.GrouplessDataGroup = instance.web.DataGroup.extend({ }); - -instance.web.StaticDataGroup = instance.web.GrouplessDataGroup.extend( /** @lends openerp.web.StaticDataGroup# */ { - /** - * A specialization of groupless data groups, relying on a single static - * dataset as its records provider. - * - * @constructs instance.web.StaticDataGroup - * @extends instance.web.GrouplessDataGroup - * @param {openep.web.DataSetStatic} dataset a static dataset backing the groups - */ - init: function (dataset) { - this.dataset = dataset; - }, - list: function (fields, ifGroups, ifRecords) { - ifRecords(this.dataset); - } -}); - -instance.web.DataSet = instance.web.OldWidget.extend( /** @lends openerp.web.DataSet# */{ +instance.web.DataSet = instance.web.CallbackEnabled.extend({ /** - * DateaManagement interface between views and the collection of selected - * OpenERP records (represents the view's state?) + * Collection of OpenERP records, used to share records and the current selection between views. * * @constructs instance.web.DataSet - * @extends instance.web.OldWidget + * @extends instance.web.CallbackEnabled * * @param {String} model the OpenERP model this dataset will manage */ @@ -638,21 +471,16 @@ instance.web.DataSet = instance.web.OldWidget.extend( /** @lends openerp.web.Da default_get: function(fields, options) { options = options || {}; return this._model.call('default_get', - [fields], {context: this._model.context(options.context)}); + [fields], {context: this.get_context(options.context)}); }, /** * Creates a new record in db * * @param {Object} data field values to set on the new record - * @param {Function} callback function called with operation result - * @param {Function} error_callback function called in case of creation error * @returns {$.Deferred} */ - create: function(data, callback, error_callback) { - return this._model.call('create', - [data], {context: this._model.context()}) - .pipe(function (r) { return {result: r}; }) - .then(callback, error_callback); + create: function(data) { + return this._model.call('create', [data], {context: this.get_context()}); }, /** * Saves the provided data in an existing db record @@ -663,24 +491,17 @@ instance.web.DataSet = instance.web.OldWidget.extend( /** @lends openerp.web.Da * @param {Function} error_callback function called in case of write error * @returns {$.Deferred} */ - write: function (id, data, options, callback, error_callback) { + write: function (id, data, options) { options = options || {}; - return this._model.call('write', - [[id], data], {context: this._model.context(options.context)}) - .pipe(function (r) { return {result: r}}) - .then(callback, error_callback); + return this._model.call('write', [[id], data], {context: this.get_context(options.context)}).then(this.trigger('dataset_changed', id, data, options)); }, /** * Deletes an existing record from the database * * @param {Number|String} ids identifier of the record to delete - * @param {Function} callback function called with operation result - * @param {Function} error_callback function called in case of deletion error */ - unlink: function(ids, callback, error_callback) { - return this._model.call('unlink', - [ids], {context: this._model.context()}) - .then(callback, error_callback); + unlink: function(ids) { + return this._model.call('unlink', [ids], {context: this.get_context()}).then(this.trigger('dataset_changed', ids)); }, /** * Calls an arbitrary RPC method @@ -691,53 +512,27 @@ instance.web.DataSet = instance.web.OldWidget.extend( /** @lends openerp.web.Da * @param {Function} error_callback * @returns {$.Deferred} */ - call: function (method, args, callback, error_callback) { - return this._model.call(method, args).then(callback, error_callback); - }, - /** - * Calls an arbitrary method, with more crazy - * - * @param {String} method - * @param {Array} [args] - * @param {Number} [domain_index] index of a domain to evaluate in the args array - * @param {Number} [context_index] index of a context to evaluate in the args array - * @param {Function} callback - * @param {Function} error_callback - * @returns {$.Deferred} - */ - call_and_eval: function (method, args, domain_index, context_index, callback, error_callback) { - return this.rpc('/web/dataset/call', { - model: this.model, - method: method, - domain_id: domain_index == undefined ? null : domain_index, - context_id: context_index == undefined ? null : context_index, - args: args || [] - }, callback, error_callback); + call: function (method, args) { + return this._model.call(method, args); }, /** * Calls a button method, usually returning some sort of action * * @param {String} method * @param {Array} [args] - * @param {Function} callback - * @param {Function} error_callback * @returns {$.Deferred} */ - call_button: function (method, args, callback, error_callback) { - return this._model.call_button(method, args) - .then(callback, error_callback); + call_button: function (method, args) { + return this._model.call_button(method, args); }, /** * Fetches the "readable name" for records, based on intrinsic rules * * @param {Array} ids - * @param {Function} callback * @returns {$.Deferred} */ - name_get: function(ids, callback) { - return this._model.call('name_get', - [ids], {context: this._model.context()}) - .then(callback); + name_get: function(ids) { + return this._model.call('name_get', [ids], {context: this.get_context()}); }, /** * @@ -748,28 +543,23 @@ instance.web.DataSet = instance.web.OldWidget.extend( /** @lends openerp.web.Da * @param {Function} callback function to call with name_search result * @returns {$.Deferred} */ - name_search: function (name, domain, operator, limit, callback) { + name_search: function (name, domain, operator, limit) { return this._model.call('name_search', { name: name || '', args: domain || false, operator: operator || 'ilike', context: this._model.context(), limit: limit || 0 - }).then(callback); + }); }, /** * @param name - * @param callback */ - name_create: function(name, callback) { - return this._model.call('name_create', - [name], {context: this._model.context()}) - .then(callback); + name_create: function(name) { + return this._model.call('name_create', [name], {context: this.get_context()}); }, - exec_workflow: function (id, signal, callback) { - return this._model.exec_workflow(id, signal) - .pipe(function (result) { return { result: result }; }) - .then(callback); + exec_workflow: function (id, signal) { + return this._model.exec_workflow(id, signal); }, get_context: function(request_context) { return this._model.context(request_context); @@ -803,11 +593,29 @@ instance.web.DataSet = instance.web.OldWidget.extend( /** @lends openerp.web.Da return this.ids.length; }, alter_ids: function(n_ids) { - this.ids = n_ids; + this.ids = n_ids; + }, + /** + * Resequence records. + * + * @param {Array} ids identifiers of the records to resequence + * @returns {$.Deferred} + */ + resequence: function (ids, options) { + options = options || {}; + return instance.session.rpc('/web/dataset/resequence', { + model: this.model, + ids: ids, + context: this.get_context(options.context), + }).pipe(function (results) { + return results; + }); }, }); + instance.web.DataSetStatic = instance.web.DataSet.extend({ init: function(parent, model, context, ids) { + var self = this; this._super(parent, model, context); // all local records this.ids = ids || []; @@ -829,14 +637,13 @@ instance.web.DataSetStatic = instance.web.DataSet.extend({ } }, unlink: function(ids) { - this.on_unlink(ids); + this.set_ids(_.without.apply(null, [this.ids].concat(ids))); + this.trigger('unlink', ids); return $.Deferred().resolve({result: true}); }, - on_unlink: function(ids) { - this.set_ids(_.without.apply(null, [this.ids].concat(ids))); - } }); -instance.web.DataSetSearch = instance.web.DataSet.extend(/** @lends openerp.web.DataSetSearch */{ + +instance.web.DataSetSearch = instance.web.DataSet.extend({ /** * @constructs instance.web.DataSetSearch * @extends instance.web.DataSet @@ -886,18 +693,17 @@ instance.web.DataSetSearch = instance.web.DataSet.extend(/** @lends openerp.web }, unlink: function(ids, callback, error_callback) { var self = this; - return this._super(ids, function(result) { + return this._super(ids).then(function(result) { self.ids = _(self.ids).difference(ids); if (self._length) { self._length -= 1; } - if (this.index !== null) { + if (self.index !== null) { self.index = self.index <= self.ids.length - 1 ? self.index : (self.ids.length > 0 ? self.ids.length -1 : 0); } - if (callback) - callback(result); - }, error_callback); + self.trigger("dataset_changed", ids, callback, error_callback); + }); }, size: function () { if (this._length !== undefined) { @@ -906,6 +712,7 @@ instance.web.DataSetSearch = instance.web.DataSet.extend(/** @lends openerp.web return this._super(); } }); + instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({ virtual_id_prefix: "one2many_v_id_", debug_mode: true, @@ -920,16 +727,14 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({ on_default_get: function(res) { this.last_default_get = res; }, - create: function(data, callback, error_callback) { + create: function(data) { var cached = {id:_.uniqueId(this.virtual_id_prefix), values: data, defaults: this.last_default_get}; this.to_create.push(_.extend(_.clone(cached), {values: _.clone(cached.values)})); this.cache.push(cached); - var prom = $.Deferred().then(callback); - prom.resolve({result: cached.id}); - return prom.promise(); + return $.Deferred().resolve(cached.id).promise(); }, - write: function (id, data, options, callback) { + write: function (id, data, options) { var self = this; var record = _.detect(this.to_create, function(x) {return x.id === id;}); record = record || _.detect(this.to_write, function(x) {return x.id === id;}); @@ -954,10 +759,8 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({ } $.extend(cached.values, record.values); if (dirty) - this.on_change(); - var to_return = $.Deferred().then(callback); - to_return.resolve({result: true}); - return to_return.promise(); + this.trigger("dataset_changed", id, data, options); + return $.Deferred().resolve(true).promise(); }, unlink: function(ids, callback, error_callback) { var self = this; @@ -970,7 +773,7 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({ this.to_write = _.reject(this.to_write, function(x) { return _.include(ids, x.id);}); this.cache = _.reject(this.cache, function(x) { return _.include(ids, x.id);}); this.set_ids(_.without.apply(_, [this.ids].concat(ids))); - this.on_change(); + this.trigger("dataset_changed", ids, callback, error_callback); return $.async_when({result: true}).then(callback); }, reset_ids: function(ids) { @@ -981,7 +784,6 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({ this.cache = []; this.delete_all = false; }, - on_change: function() {}, read_ids: function (ids, fields, options) { var self = this; var to_get = []; @@ -1042,7 +844,7 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({ self.cache.push({id: id, values: record}); } else { // I assume cache value is prioritary - cached.values = _.defaults(_.clone(cached.values), record); + cached.values = _.defaults(_.clone(cached.values), record); } }); return_records(); @@ -1053,7 +855,7 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({ } return completion.promise(); }, - call_button: function (method, args, callback, error_callback) { + call_button: function (method, args) { var id = args[0][0], index; for(var i=0, len=this.cache.length; i<len; ++i) { var record = this.cache[i]; @@ -1064,11 +866,11 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({ break; } } - return this._super(method, args, callback, error_callback); + return this._super(method, args); }, alter_ids: function(n_ids) { - this._super(n_ids); - this.on_change(); + this._super(n_ids); + this.trigger("dataset_changed", n_ids); }, }); instance.web.BufferedDataSet.virtual_id_regex = /^one2many_v_id_.*$/; @@ -1096,23 +898,23 @@ instance.web.ProxyDataSet = instance.web.DataSetSearch.extend({ return this._super.apply(this, arguments); } }, - create: function(data, callback, error_callback) { + create: function(data) { if (this.create_function) { - return this.create_function(data, this._super).then(callback, error_callback); + return this.create_function(data, this._super); } else { return this._super.apply(this, arguments); } }, - write: function (id, data, options, callback, error_callback) { + write: function (id, data, options) { if (this.write_function) { - return this.write_function(id, data, options, this._super).then(callback, error_callback); + return this.write_function(id, data, options, this._super); } else { return this._super.apply(this, arguments); } }, - unlink: function(ids, callback, error_callback) { + unlink: function(ids) { if (this.unlink_function) { - return this.unlink_function(ids, this._super).then(callback, error_callback); + return this.unlink_function(ids, this._super); } else { return this._super.apply(this, arguments); } @@ -1165,7 +967,7 @@ instance.web.CompoundDomain = instance.web.Class.extend({ } }); -instance.web.DropMisordered = instance.web.Class.extend(/** @lends openerp.web.DropMisordered# */{ +instance.web.DropMisordered = instance.web.Class.extend({ /** * @constructs instance.web.DropMisordered * @extends instance.web.Class diff --git a/addons/web/static/src/js/data_export.js b/addons/web/static/src/js/data_export.js index ebe82ddb4b4..b4beb4c3a58 100644 --- a/addons/web/static/src/js/data_export.js +++ b/addons/web/static/src/js/data_export.js @@ -5,7 +5,15 @@ instance.web.DataExport = instance.web.Dialog.extend({ template: 'ExportTreeView', dialog_title: {toString: function () { return _t("Export Data"); }}, init: function(parent, dataset) { - this._super(parent); + var self = this; + options = { + buttons : [ + {text: _t("Close"), click: function() { self.close(); }}, + {text: _t("Export To File"), click: function() { self.on_click_export_data(); }} + ], + close: function(event, ui){ self.close();} + } + this._super(parent, options); this.records = {}; this.dataset = dataset; this.exports = new instance.web.DataSetSearch( @@ -14,42 +22,36 @@ instance.web.DataExport = instance.web.Dialog.extend({ start: function() { var self = this; this._super.apply(this, arguments); - this.open({ - buttons : [ - {text: _t("Close"), click: function() { self.close(); }}, - {text: _t("Export To File"), click: function() { self.on_click_export_data(); }} - ], - close: function(event, ui){ self.close();} - }); - self.$element.removeClass('ui-dialog-content ui-widget-content'); - self.$element.find('#add_field').click(function() { + this.open(); + self.$el.removeClass('ui-dialog-content ui-widget-content'); + self.$el.find('#add_field').click(function() { if ($('#field-tree-structure tr.ui-selected')) { - var fld = self.$element.find('#field-tree-structure tr.ui-selected').find('a'); + var fld = self.$el.find('#field-tree-structure tr.ui-selected').find('a'); for (var i = 0; i < fld.length; i++) { var id = $(fld[i]).attr('id').split('-')[1]; var string = $(fld[i]).attr('string'); self.add_field(id, string); } - self.$element.find('#field-tree-structure tr').removeClass('ui-selected'); + self.$el.find('#field-tree-structure tr').removeClass('ui-selected'); } }); - self.$element.find('#remove_field').click(function() { - self.$element.find('#fields_list option:selected').remove(); + self.$el.find('#remove_field').click(function() { + self.$el.find('#fields_list option:selected').remove(); }); - self.$element.find('#remove_all_field').click(function() { - self.$element.find('#fields_list').empty(); + self.$el.find('#remove_all_field').click(function() { + self.$el.find('#fields_list').empty(); }); - this.$element.find('#export_new_list').click(this.on_show_save_list); + this.$el.find('#export_new_list').click(this.on_show_save_list); var got_fields = new $.Deferred(); - this.$element.find('#import_compat').change(function() { - self.$element.find('#fields_list').empty(); - self.$element.find('#field-tree-structure').remove(); - var import_comp = self.$element.find("#import_compat").val(); + this.$el.find('#import_compat').change(function() { + self.$el.find('#fields_list').empty(); + self.$el.find('#field-tree-structure').remove(); + var import_comp = self.$el.find("#import_compat").val(); self.rpc("/web/export/get_fields", { model: self.dataset.model, import_compat: Boolean(import_comp) - }, function (records) { + }).then(function (records) { got_fields.resolve(); self.on_show_data(records); }); @@ -57,11 +59,11 @@ instance.web.DataExport = instance.web.Dialog.extend({ return $.when( got_fields, - this.rpc('/web/export/formats', {}, this.do_setup_export_formats), + this.rpc('/web/export/formats', {}).then(this.do_setup_export_formats), this.show_exports_list()); }, do_setup_export_formats: function (formats) { - var $fmts = this.$element.find('#export_format'); + var $fmts = this.$el.find('#export_format'); _(formats).each(function (format) { var opt = new Option(format.label, format.tag); if (format.error) { @@ -76,8 +78,8 @@ instance.web.DataExport = instance.web.Dialog.extend({ }, show_exports_list: function() { var self = this; - if (self.$element.find('#saved_export_list').is(':hidden')) { - self.$element.find('#ExistsExportList').show(); + if (self.$el.find('#saved_export_list').is(':hidden')) { + self.$el.find('#ExistsExportList').show(); return; } return this.exports.read_slice(['name'], { @@ -86,35 +88,35 @@ instance.web.DataExport = instance.web.Dialog.extend({ if (!export_list.length) { return; } - self.$element.find('#ExistsExportList').append(QWeb.render('Exists.ExportList', {'existing_exports': export_list})); - self.$element.find('#saved_export_list').change(function() { - self.$element.find('#fields_list option').remove(); - var export_id = self.$element.find('#saved_export_list option:selected').val(); + self.$el.find('#ExistsExportList').append(QWeb.render('Exists.ExportList', {'existing_exports': export_list})); + self.$el.find('#saved_export_list').change(function() { + self.$el.find('#fields_list option').remove(); + var export_id = self.$el.find('#saved_export_list option:selected').val(); if (export_id) { - self.rpc('/web/export/namelist', {'model': self.dataset.model, export_id: parseInt(export_id)}, self.do_load_export_field); + self.rpc('/web/export/namelist', {'model': self.dataset.model, export_id: parseInt(export_id)}).then(self.do_load_export_field); } }); - self.$element.find('#delete_export_list').click(function() { - var select_exp = self.$element.find('#saved_export_list option:selected'); + self.$el.find('#delete_export_list').click(function() { + var select_exp = self.$el.find('#saved_export_list option:selected'); if (select_exp.val()) { self.exports.unlink([parseInt(select_exp.val(), 10)]); select_exp.remove(); - if (self.$element.find('#saved_export_list option').length <= 1) { - self.$element.find('#ExistsExportList').hide(); + if (self.$el.find('#saved_export_list option').length <= 1) { + self.$el.find('#ExistsExportList').hide(); } } }); }); }, do_load_export_field: function(field_list) { - var export_node = this.$element.find("#fields_list"); + var export_node = this.$el.find("#fields_list"); _(field_list).each(function (field) { export_node.append(new Option(field.label, field.name)); }); }, on_show_save_list: function() { var self = this; - var current_node = self.$element.find("#savenewlist"); + var current_node = self.$el.find("#savenewlist"); if (!(current_node.find("label")).length) { current_node.append(QWeb.render('ExportNewList')); current_node.find("#add_export_list").click(function() { @@ -150,14 +152,14 @@ instance.web.DataExport = instance.web.Dialog.extend({ if (!export_list_id.result) { return; } - self.$element.find("#saved_export_list").append( + self.$el.find("#saved_export_list").append( new Option(value, export_list_id.result)); - if (self.$element.find("#saved_export_list").is(":hidden")) { + if (self.$el.find("#saved_export_list").is(":hidden")) { self.show_exports_list(); } }); this.on_show_save_list(); - this.$element.find("#fields_list option").remove(); + this.$el.find("#fields_list option").remove(); }, on_click: function(id, record) { var self = this; @@ -173,7 +175,7 @@ instance.web.DataExport = instance.web.Dialog.extend({ } if (!record.loaded) { - var import_comp = self.$element.find("#import_compat").val(); + var import_comp = self.$el.find("#import_compat").val(); self.rpc("/web/export/get_fields", { model: model, prefix: prefix, @@ -181,7 +183,7 @@ instance.web.DataExport = instance.web.Dialog.extend({ import_compat: Boolean(import_comp), parent_field_type : record['field_type'], exclude: exclude_fields - }, function(results) { + }).then(function(results) { record.loaded = true; self.on_show_data(results, record.id); }); @@ -191,44 +193,44 @@ instance.web.DataExport = instance.web.Dialog.extend({ }, on_show_data: function(result, after) { var self = this; - var imp_cmpt = Boolean(self.$element.find("#import_compat").val()); + var imp_cmpt = Boolean(self.$el.find("#import_compat").val()); if (after) { - var current_tr = self.$element.find("tr[id='treerow-" + after + "']"); + var current_tr = self.$el.find("tr[id='treerow-" + after + "']"); current_tr.addClass('open'); current_tr.find('img').attr('src','/web/static/src/img/collapse.gif'); current_tr.after(QWeb.render('ExportTreeView-Secondary.children', {'fields': result})); } else { - self.$element.find('#left_field_panel').append(QWeb.render('ExportTreeView-Secondary', {'fields': result})); + self.$el.find('#left_field_panel').append(QWeb.render('ExportTreeView-Secondary', {'fields': result})); } _.each(result, function(record) { self.records[record.id] = record.value; if (record.required) { - var required_fld = self.$element.find("tr[id='treerow-" + record.id + "']").find('#tree-column'); + var required_fld = self.$el.find("tr[id='treerow-" + record.id + "']").find('#tree-column'); required_fld.addClass("oe_export_requiredfield"); } - self.$element.find("img[id='parentimg-" + record.id +"']").click(function() { + self.$el.find("img[id='parentimg-" + record.id +"']").click(function() { self.on_click(this.id, record); }); - self.$element.find("tr[id='treerow-" + record.id + "']").click(function(e) { + self.$el.find("tr[id='treerow-" + record.id + "']").click(function(e) { if (e.shiftKey) { var frst_click, scnd_click = ''; if (self.row_index == 0) { self.row_index = this.rowIndex; - frst_click = self.$element.find("tr[id^='treerow-']")[self.row_index-1]; + frst_click = self.$el.find("tr[id^='treerow-']")[self.row_index-1]; $(frst_click).addClass("ui-selected"); } else { if (this.rowIndex >=self.row_index) { for (var i = (self.row_index-1); i < this.rowIndex; i++) { - scnd_click = self.$element.find("tr[id^='treerow-']")[i]; + scnd_click = self.$el.find("tr[id^='treerow-']")[i]; if (!$(scnd_click).find('#tree-column').hasClass("oe_export_readonlyfield")) { $(scnd_click).addClass("ui-selected"); } } } else { for (var i = (self.row_index-1); i >= (this.rowIndex-1); i--) { - scnd_click = self.$element.find("tr[id^='treerow-']")[i]; + scnd_click = self.$el.find("tr[id^='treerow-']")[i]; if (!$(scnd_click).find('#tree-column').hasClass("oe_export_readonlyfield")) { $(scnd_click).addClass("ui-selected"); } @@ -238,10 +240,10 @@ instance.web.DataExport = instance.web.Dialog.extend({ } self.row_index = this.rowIndex; - self.$element.find("tr[id='treerow-" + record.id + "']").keyup(function() { + self.$el.find("tr[id='treerow-" + record.id + "']").keyup(function() { self.row_index = 0; }); - var o2m_selection = self.$element.find("tr[id='treerow-" + record.id + "']").find('#tree-column'); + var o2m_selection = self.$el.find("tr[id='treerow-" + record.id + "']").find('#tree-column'); if ($(o2m_selection).hasClass("oe_export_readonlyfield")) { return false; } @@ -252,14 +254,14 @@ instance.web.DataExport = instance.web.Dialog.extend({ $(this).addClass('ui-selected').find('a').focus(); } } else if (!e.shiftKey) { - self.$element.find("tr.ui-selected") + self.$el.find("tr.ui-selected") .removeClass("ui-selected").find('a').blur(); $(this).addClass("ui-selected").find('a').focus(); } return false; }); - self.$element.find("tr[id='treerow-" + record.id + "']").keydown(function(e) { + self.$el.find("tr[id='treerow-" + record.id + "']").keydown(function(e) { var keyCode = e.keyCode || e.which; var arrow = {left: 37, up: 38, right: 39, down: 40 }; switch (keyCode) { @@ -297,19 +299,19 @@ instance.web.DataExport = instance.web.Dialog.extend({ break; } }); - self.$element.find("tr[id='treerow-" + record.id + "']").dblclick(function() { - var $o2m_selection = self.$element.find("tr[id^='treerow-" + record.id + "']").find('#tree-column'); + self.$el.find("tr[id='treerow-" + record.id + "']").dblclick(function() { + var $o2m_selection = self.$el.find("tr[id^='treerow-" + record.id + "']").find('#tree-column'); if (!$o2m_selection.hasClass("oe_export_readonlyfield")) { self.add_field(record.id, $(this).find("a").attr("string")); } }); }); - self.$element.find('#fields_list').mouseover(function(event) { + self.$el.find('#fields_list').mouseover(function(event) { if (event.relatedTarget) { if (event.relatedTarget.attributes['id'] && event.relatedTarget.attributes['string']) { var field_id = event.relatedTarget.attributes["id"]["value"]; if (field_id && field_id.split("-")[0] === 'export') { - if (!self.$element.find("tr[id='treerow-" + field_id.split("-")[1] + "']").find('#tree-column').hasClass("oe_export_readonlyfield")) { + if (!self.$el.find("tr[id='treerow-" + field_id.split("-")[1] + "']").find('#tree-column').hasClass("oe_export_readonlyfield")) { self.add_field(field_id.split("-")[1], event.relatedTarget.attributes["string"]["value"]); } } @@ -319,7 +321,7 @@ instance.web.DataExport = instance.web.Dialog.extend({ }, showcontent: function(id) { // show & hide the contents - var $this = this.$element.find("tr[id='treerow-" + id + "']"); + var $this = this.$el.find("tr[id='treerow-" + id + "']"); var is_open = $this.hasClass('open'); $this.toggleClass('open'); @@ -329,7 +331,7 @@ instance.web.DataExport = instance.web.Dialog.extend({ } else { first_child.attr('src', '/web/static/src/img/collapse.gif'); } - var child_field = this.$element.find("tr[id^='treerow-" + id +"/']"); + var child_field = this.$el.find("tr[id^='treerow-" + id +"/']"); var child_len = (id.split("/")).length + 1; for (var i = 0; i < child_field.length; i++) { var $child = $(child_field[i]); @@ -345,15 +347,15 @@ instance.web.DataExport = instance.web.Dialog.extend({ } }, add_field: function(field_id, string) { - var field_list = this.$element.find('#fields_list'); - if (this.$element.find("#fields_list option[value='" + field_id + "']") - && !this.$element.find("#fields_list option[value='" + field_id + "']").length) { + var field_list = this.$el.find('#fields_list'); + if (this.$el.find("#fields_list option[value='" + field_id + "']") + && !this.$el.find("#fields_list option[value='" + field_id + "']").length) { field_list.append(new Option(string, field_id)); } }, get_fields: function() { var export_field = []; - this.$element.find("#fields_list option").each(function() { + this.$el.find("#fields_list option").each(function() { export_field.push($(this).val()); }); if (!export_field.length) { @@ -363,7 +365,7 @@ instance.web.DataExport = instance.web.Dialog.extend({ }, on_click_export_data: function() { var self = this; - var exported_fields = this.$element.find('#fields_list option').map(function () { + var exported_fields = this.$el.find('#fields_list option').map(function () { // DOM property is textContent, but IE8 only knows innerText return {name: self.records[this.value] || this.value, label: this.textContent || this.innerText}; @@ -375,8 +377,8 @@ instance.web.DataExport = instance.web.Dialog.extend({ } exported_fields.unshift({name: 'id', label: 'External ID'}); - var export_format = this.$element.find("#export_format").val(); - $.blockUI(); + var export_format = this.$el.find("#export_format").val(); + instance.web.blockUI(); this.session.get_file({ url: '/web/export/' + export_format, data: {data: JSON.stringify({ @@ -385,13 +387,13 @@ instance.web.DataExport = instance.web.Dialog.extend({ ids: this.dataset.ids, domain: this.dataset.domain, import_compat: Boolean( - this.$element.find("#import_compat").val()) + this.$el.find("#import_compat").val()) })}, - complete: $.unblockUI + complete: instance.web.unblockUI }); }, close: function() { - this.$element.remove(); + this.$el.remove(); this._super(); } }); diff --git a/addons/web/static/src/js/data_import.js b/addons/web/static/src/js/data_import.js deleted file mode 100644 index 30f9323f723..00000000000 --- a/addons/web/static/src/js/data_import.js +++ /dev/null @@ -1,366 +0,0 @@ -openerp.web.data_import = function(instance) { -var QWeb = instance.web.qweb, - _t = instance.web._t; -/** - * Safari does not deal well at all with raw JSON data being returned. As a - * result, we're going to cheat by using a pseudo-jsonp: instead of getting - * JSON data in the iframe, we're getting a ``script`` tag which consists of a - * function call and the returned data (the json dump). - * - * The function is an auto-generated name bound to ``window``, which calls - * back into the callback provided here. - * - * @param {Object} form the form element (DOM or jQuery) to use in the call - * @param {Object} attributes jquery.form attributes object - * @param {Function} callback function to call with the returned data - */ -function jsonp(form, attributes, callback) { - attributes = attributes || {}; - var options = {jsonp: _.uniqueId('import_callback_')}; - window[options.jsonp] = function () { - delete window[options.jsonp]; - callback.apply(null, arguments); - }; - if ('data' in attributes) { - _.extend(attributes.data, options); - } else { - _.extend(attributes, {data: options}); - } - $(form).ajaxSubmit(attributes); -} - -instance.web.DataImport = instance.web.Dialog.extend({ - template: 'ImportDataView', - dialog_title: {toString: function () { return _t("Import Data"); }}, - init: function(parent, dataset){ - var self = this; - this._super(parent, {}); - this.model = parent.model; - this.fields = []; - this.all_fields = []; - this.fields_with_defaults = []; - this.required_fields = null; - - var convert_fields = function (root, prefix) { - prefix = prefix || ''; - _(root.fields).each(function (f) { - self.all_fields.push(prefix + f.name); - if (f.fields) { - convert_fields(f, prefix + f.id + '/'); - } - }); - }; - this.ready = $.Deferred.queue().then(function () { - self.required_fields = _(self.fields).chain() - .filter(function (field) { - return field.required && - !_.include(self.fields_with_defaults, field.id); }) - .pluck('name') - .value(); - convert_fields(self); - self.all_fields.sort(); - }); - }, - start: function() { - var self = this; - this._super(); - this.open({ - buttons: [ - {text: _t("Close"), click: function() { self.destroy(); }}, - {text: _t("Import File"), click: function() { self.do_import(); }, 'class': 'oe-dialog-import-button'} - ], - close: function(event, ui) { - self.destroy(); - } - }); - this.toggle_import_button(false); - this.$element.find('#csvfile').change(this.on_autodetect_data); - this.$element.find('fieldset').change(this.on_autodetect_data); - this.$element.delegate('fieldset legend', 'click', function() { - $(this).parent().toggleClass('oe-closed'); - }); - this.ready.push(new instance.web.DataSet(this, this.model).call( - 'fields_get', [], function (fields) { - self.graft_fields(fields); - self.ready.push(new instance.web.DataSet(self, self.model) - .default_get(_.pluck(self.fields, 'id')).then(function (fields) { - _.each(fields, function(val, key) { - if (val) { - self.fields_with_defaults.push(key); - } - }); - }) - ) - })); - }, - graft_fields: function (fields, parent, level) { - parent = parent || this; - level = level || 0; - - var self = this; - if (level === 0) { - parent.fields.push({ - id: 'id', - name: 'id', - string: _t('External ID'), - required: false - }); - } - _(fields).each(function (field, field_name) { - // Ignore spec for id field - // Don't import function fields (function and related) - if (field_name === 'id') { - return; - } - // Skip if there's no state which could disable @readonly, - // if a field is ever always readonly we can't import it - if (field.readonly) { - // no states at all - if (_.isEmpty(field.states)) { return; } - // no state altering @readonly - if (!_.any(field.states, function (modifiers) { - return _(modifiers).chain().pluck(0).contains('readonly').value(); - })) { return; } - } - - var f = { - id: field_name, - name: field_name, - string: field.string, - required: field.required - }; - - switch (field.type) { - case 'many2many': - case 'many2one': - f.name += '/id'; - break; - case 'one2many': - f.name += '/id'; - f.fields = []; - // only fetch sub-fields to a depth of 2 levels - if (level < 2) { - self.ready.push(new instance.web.DataSet(self, field.relation).call( - 'fields_get', [], function (fields) { - self.graft_fields(fields, f, level+1); - })); - } - break; - } - parent.fields.push(f); - }); - }, - toggle_import_button: function (newstate) { - instance.web.dialog(this.$element, 'widget') - .find('.oe-dialog-import-button') - .button('option', 'disabled', !newstate); - }, - do_import: function() { - if(!this.$element.find('#csvfile').val()) { return; } - var lines_to_skip = parseInt(this.$element.find('#csv_skip').val(), 10); - var with_headers = this.$element.find('#file_has_headers').prop('checked'); - if (!lines_to_skip && with_headers) { - lines_to_skip = 1; - } - var indices = [], fields = []; - this.$element.find(".sel_fields").each(function(index, element) { - var val = element.value; - if (!val) { - return; - } - indices.push(index); - fields.push(val); - }); - - jsonp(this.$element.find('#import_data'), { - url: '/web/import/import_data', - data: { - model: this.model, - meta: JSON.stringify({ - skip: lines_to_skip, - indices: indices, - fields: fields - }) - } - }, this.on_import_results); - }, - on_autodetect_data: function() { - if(!this.$element.find('#csvfile').val()) { return; } - jsonp(this.$element.find('#import_data'), { - url: '/web/import/detect_data' - }, this.on_import_results); - }, - on_import_results: function(results) { - this.$element.find('#result').empty(); - var headers, result_node = this.$element.find("#result"); - - if (results['error']) { - result_node.append(QWeb.render('ImportView.error', { - 'error': results['error']})); - this.$element.find('fieldset').removeClass('oe-closed'); - return; - } - if (results['success']) { - if (this.getParent().getParent().active_view == "list") { - this.getParent().reload_content(); - } - this.destroy(); - return; - } - - if (results['records']) { - var lines_to_skip = parseInt(this.$element.find('#csv_skip').val(), 10), - with_headers = this.$element.find('#file_has_headers').prop('checked'); - headers = with_headers ? results.records[0] : null; - - result_node.append(QWeb.render('ImportView.result', { - 'headers': headers, - 'records': lines_to_skip ? results.records.slice(lines_to_skip) - : with_headers ? results.records.slice(1) - : results.records - })); - this.$element.find('fieldset').addClass('oe-closed'); - } - this.$element.find('form').removeClass('oe-import-no-result'); - - this.$element.delegate('.oe-m2o-drop-down-button', 'click', function () { - $(this).prev('input').focus(); - }); - - var self = this; - this.ready.then(function () { - var $fields = self.$element.find('.sel_fields').bind('blur', function () { - if (this.value && !_(self.all_fields).contains(this.value)) { - this.value = ''; - } - }).autocomplete({ - minLength: 0, - source: self.all_fields, - change: self.on_check_field_values - }).focus(function () { - $(this).autocomplete('search'); - }); - // Column auto-detection - _(headers).each(function (header, index) { - var field_name = self.match_column_to_field(header); - if (field_name) { - $fields.eq(index).val(field_name); - } - }); - self.on_check_field_values(); - }); - }, - /** - * Returns the name of the field (nested) matching the provided column name - * - * @param {String} name column name to look for - * @param {Array} [fields] fields to look into for the provided name - * @returns {String|undefined} - */ - match_column_to_field: function (name, fields) { - fields = fields || this.fields; - var f; - f = _(fields).detect(function (field) { - // TODO: levenshtein between header and field.string - return field.name === name - || field.string.toLowerCase() === name.toLowerCase(); - }); - if (f) { return f.name; } - - // if ``name`` is a path (o2m), we need to recurse through its .fields - var index = name.indexOf('/'); - if (index === -1) { return undefined; } - // Get the first path section, try to find the matching field - var column_name = name.substring(0, index); - f = _(fields).detect(function (field) { - // field.name for o2m is $foo/id, so we want to match on id - return field.id === column_name - || field.string.toLowerCase() === column_name.toLowerCase() - }); - if (!f) { return undefined; } - - // if we found a matching field for the first path section, recurse in - // its own .fields to try and get the rest of the path matched - var rest = this.match_column_to_field( - name.substring(index+1), f.fields); - if (!rest) { return undefined; } - return f.id + '/' + rest; - }, - /** - * Looks through all the field selections, and tries to find if two - * (or more) columns were matched to the same model field. - * - * Returns a map of the multiply-mapped fields to an array of offending - * columns (not actually columns, but the inputs containing the same field - * names). - * - * Also has the side-effect of marking the discovered inputs with the class - * ``duplicate_fld``. - * - * @returns {Object<String, Array<String>>} map of duplicate field matches to same-valued inputs - */ - find_duplicate_fields: function() { - // Maps values to DOM nodes, in order to discover duplicates - var values = {}, duplicates = {}; - this.$element.find(".sel_fields").each(function(index, element) { - var value = element.value; - var $element = $(element).removeClass('duplicate_fld'); - if (!value) { return; } - - if (!(value in values)) { - values[value] = element; - } else { - var same_valued_field = values[value]; - if (value in duplicates) { - duplicates[value].push(element); - } else { - duplicates[value] = [same_valued_field, element]; - } - $element.add(same_valued_field).addClass('duplicate_fld'); - } - }); - return duplicates; - }, - on_check_field_values: function () { - this.$element.find("#message, #msg").remove(); - - var required_valid = this.check_required(); - - var duplicates = this.find_duplicate_fields(); - if (_.isEmpty(duplicates)) { - this.toggle_import_button(required_valid); - } else { - var $err = $('<div id="msg" style="color: red;">Destination fields should only be selected once, some fields are selected more than once:</div>').insertBefore(this.$element.find('#result')); - var $dupes = $('<dl>').appendTo($err); - _(duplicates).each(function(elements, value) { - $('<dt>').text(value).appendTo($dupes); - _(elements).each(function(element) { - var cell = $(element).closest('td'); - $('<dd>').text(cell.parent().children().index(cell)).appendTo($dupes); - }); - }); - this.toggle_import_button(false); - } - - }, - check_required: function() { - if (!this.required_fields.length) { return true; } - - var selected_fields = _(this.$element.find('.sel_fields').get()).chain() - .pluck('value') - .compact() - .value(); - - var missing_fields = _.difference(this.required_fields, selected_fields); - if (missing_fields.length) { - this.$element.find("#result").before('<div id="message" style="color:red">*Required Fields are not selected : ' + missing_fields + '.</div>'); - return false; - } - return true; - }, - destroy: function() { - this.$element.remove(); - this._super(); - } -}); -}; diff --git a/addons/web/static/src/js/formats.js b/addons/web/static/src/js/formats.js index 2b527686ddd..03b2c5b775d 100644 --- a/addons/web/static/src/js/formats.js +++ b/addons/web/static/src/js/formats.js @@ -118,7 +118,9 @@ instance.web.format_value = function (value, descriptor, value_if_empty) { return instance.web.insert_thousand_seps( _.str.sprintf('%d', value)); case 'float': - var precision = descriptor.digits ? descriptor.digits[1] : 2; + var digits = descriptor.digits ? descriptor.digits : [69,2]; + digits = typeof digits === "string" ? py.eval(digits) : digits; + var precision = digits[1]; var formatted = _.str.sprintf('%.' + precision + 'f', value).split('.'); formatted[0] = instance.web.insert_thousand_seps(formatted[0]); return formatted.join(l10n.decimal_point); @@ -133,7 +135,13 @@ instance.web.format_value = function (value, descriptor, value_if_empty) { Math.round((value % 1) * 60)); case 'many2one': // name_get value format - return value[1]; + return value[1] ? value[1].split("\n")[0] : value[1]; + case 'one2many': + case 'many2many': + if (typeof value === 'string') { + return value; + } + return _.str.sprintf(_t("(%d records)"), value.length); case 'datetime': if (typeof(value) == "string") value = instance.web.auto_str_to_date(value); @@ -266,82 +274,4 @@ instance.web.auto_date_to_str = function(value, type) { } }; -/** - * Formats a provided cell based on its field type. Most of the field types - * return a correctly formatted value, but some tags and fields are - * special-cased in their handling: - * - * * buttons will return an actual ``<button>`` tag with a bunch of error handling - * - * * boolean fields will return a checkbox input, potentially disabled - * - * * binary fields will return a link to download the binary data as a file - * - * @param {Object} row_data record whose values should be displayed in the cell - * @param {Object} column column descriptor - * @param {"button"|"field"} column.tag base control type - * @param {String} column.type widget type for a field control - * @param {String} [column.string] button label - * @param {String} [column.icon] button icon - * @param {Object} [options] - * @param {String} [options.value_if_empty=''] what to display if the field's value is ``false`` - * @param {Boolean} [options.process_modifiers=true] should the modifiers be computed ? - * @param {String} [options.model] current record's model - * @param {Number} [options.id] current record's id - * - */ -instance.web.format_cell = function (row_data, column, options) { - options = options || {}; - var attrs = {}; - if (options.process_modifiers !== false) { - attrs = column.modifiers_for(row_data); - } - if (attrs.invisible) { return ''; } - - if (column.tag === 'button') { - return _.template('<button type="button" title="<%-title%>" <%=additional_attributes%> >' + - '<img src="<%-prefix%>/web/static/src/img/icons/<%-icon%>.png" alt="<%-alt%>"/>' + - '</button>', { - title: column.string || '', - additional_attributes: isNaN(row_data["id"].value) && instance.web.BufferedDataSet.virtual_id_regex.test(row_data["id"].value) ? - 'disabled="disabled" class="oe-listview-button-disabled"' : '', - prefix: instance.connection.prefix, - icon: column.icon, - alt: column.string || '' - }); - } - if (!row_data[column.id]) { - return options.value_if_empty === undefined ? '' : options.value_if_empty; - } - - switch (column.widget || column.type) { - case "boolean": - return _.str.sprintf('<input type="checkbox" %s disabled="disabled"/>', - row_data[column.id].value ? 'checked="checked"' : ''); - case "binary": - var text = _t("Download"), - download_url = _.str.sprintf('/web/binary/saveas?session_id=%s&model=%s&field=%s&id=%d', instance.connection.session_id, options.model, column.id, options.id); - if (column.filename) { - download_url += '&filename_field=' + column.filename; - if (row_data[column.filename]) { - text = _.str.sprintf(_t("Download \"%s\""), instance.web.format_value( - row_data[column.filename].value, {type: 'char'})); - } - } - return _.template('<a href="<%-href%>"><%-text%></a> (%<-size%>)', { - text: text, - href: download_url, - size: row_data[column.id].value - }); - case 'progressbar': - return _.template( - '<progress value="<%-value%>" max="100"><%-value%>%</progress>', { - value: _.str.sprintf("%.0f", row_data[column.id].value || 0) - }); - } - - return _.escape(instance.web.format_value( - row_data[column.id].value, column, options.value_if_empty)); -} - }; diff --git a/addons/web/static/src/js/search.js b/addons/web/static/src/js/search.js index 689f315fa63..8da5eb0ca55 100644 --- a/addons/web/static/src/js/search.js +++ b/addons/web/static/src/js/search.js @@ -61,7 +61,7 @@ my.SearchQuery = B.Collection.extend({ this.on('change', function (facet) { if(!facet.values.isEmpty()) { return; } - this.remove(facet); + this.remove(facet, {silent: true}); }, this); }, add: function (values, options) { @@ -124,23 +124,14 @@ function assert(condition, 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); + events: { + focus: function () { this.trigger('focused', this); }, + blur: function () { this.$el.text(''); this.trigger('blurred', this); }, + keydown: 'onKeydown' }, getSelection: function () { // get Text node - var root = this.$element[0].childNodes[0]; + var root = this.$el[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) @@ -202,7 +193,7 @@ my.InputView = instance.web.Widget.extend({ break; case $.ui.keyCode.RIGHT: sel = this.getSelection(); - var len = this.$element.text().length; + var len = this.$el.text().length; if (sel.start !== len || sel.start !== sel.end) { e.stopPropagation(); } @@ -212,6 +203,27 @@ my.InputView = instance.web.Widget.extend({ }); my.FacetView = instance.web.Widget.extend({ template: 'SearchView.FacetView', + events: { + 'focus': function () { this.trigger('focused', this); }, + 'blur': function () { this.trigger('blurred', this); }, + 'click': function (e) { + if ($(e.target).is('.oe_facet_remove')) { + this.model.destroy(); + return false; + } + this.$el.focus(); + e.stopPropagation(); + }, + 'keydown': function (e) { + var keys = $.ui.keyCode; + switch (e.which) { + case keys.BACKSPACE: + case keys.DELETE: + this.model.destroy(); + return false; + } + } + }, init: function (parent, model) { this._super(parent); this.model = model; @@ -223,37 +235,15 @@ my.FacetView = instance.web.Widget.extend({ }, 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) { + var $e = this.$('> span:last-child'); + return $.when(this._super()).pipe(function () { + return $.when.apply(null, 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() + '*'); + this.$el.text(this.$el.text() + '*'); } }); my.FacetValueView = instance.web.Widget.extend({ @@ -268,12 +258,54 @@ my.FacetValueView = instance.web.Widget.extend({ this._super(); }, model_changed: function () { - this.$element.text(this.$element.text() + '*'); + this.$el.text(this.$el.text() + '*'); } }); instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.SearchView# */{ template: "SearchView", + events: { + // focus last input if view itself is clicked + 'click': function (e) { + if (e.target === this.$('.oe_searchview_facets')[0]) { + this.$('.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 + 'autocompleteopen': function () { + var menu = this.$el.data('autocomplete').menu; + menu.activate( + $.Event({ type: "mouseenter" }), + menu.element.children().first()); + }, + // search button + 'click button.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(); + this.$el.toggleClass('oe_searchview_open_drawer'); + }, + 'keydown .oe_searchview_input, .oe_searchview_facet': function (e) { + switch(e.which) { + case $.ui.keyCode.LEFT: + this.focusPreceding(this); + e.preventDefault(); + break; + case $.ui.keyCode.RIGHT: + this.focusFollowing(this); + e.preventDefault(); + break; + } + } + }, /** * @constructs instance.web.SearchView * @extends instance.web.Widget @@ -313,7 +345,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea .on('add change reset remove', this.proxy('renderFacets')); if (this.hidden) { - this.$element.hide(); + this.$el.hide(); } if (this.headless) { this.ready.resolve(); @@ -324,63 +356,31 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea context: this.dataset.get_context() }); $.when(load_view) - .pipe(this.on_loaded) - .fail(function () { + .pipe(function(r) { + self.search_view_loaded(r) + }).fail(function () { self.ready.reject.apply(null, arguments); }); } - 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(); + instance.web.bus.on('click', this, function(ev) { + if ($(ev.target).parents('.oe_searchview').length === 0) { + self.$el.removeClass('oe_searchview_open_drawer'); } }); - // 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); }, show: function () { - this.$element.show(); + this.$el.show(); }, hide: function () { - this.$element.hide(); + this.$el.hide(); }, subviewForRoot: function (subview_root) { return _(this.input_subviews).detect(function (subview) { - return subview.$element[0] === subview_root; + return subview.$el[0] === subview_root; }); }, siblingSubview: function (subview, direction, wrap_around) { @@ -395,12 +395,12 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea focusPreceding: function (subview_root) { return this.siblingSubview( this.subviewForRoot(subview_root), -1, true) - .$element.focus(); + .$el.focus(); }, focusFollowing: function (subview_root) { return this.siblingSubview( this.subviewForRoot(subview_root), +1, true) - .$element.focus(); + .$el.focus(); }, /** @@ -419,19 +419,19 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea // 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 () { + // input or textarea), cheat by setting val() on $el + this.$el.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()); + self.$el.val(self.currentInputValue()); }, 0); }); - this.$element.autocomplete({ + this.$el.autocomplete({ source: this.proxy('complete_global_search'), select: this.proxy('select_completion'), focus: function (e) { e.preventDefault(); }, @@ -468,7 +468,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea * div[contenteditable].oe_searchview_input) */ currentInputValue: function () { - return this.$element.find('div.oe_searchview_input:focus').text(); + return this.$('div.oe_searchview_input:focus').text(); }, /** * Provide auto-completion result for req.term (an array to `resp`) @@ -498,14 +498,23 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea var input_index = _(this.input_subviews).indexOf( this.subviewForRoot( - this.$element.find('div.oe_searchview_input:focus')[0])); + this.$('div.oe_searchview_input:focus')[0])); this.query.add(ui.item.facet, {at: input_index / 2}); }, childFocused: function () { - this.$element.addClass('oe_focused'); + this.$el.addClass('oe_focused'); }, childBlurred: function () { - this.$element.removeClass('oe_focused'); + var val = this.$el.val(); + this.$el.val(''); + var complete = this.$el.data('autocomplete'); + if ((val && complete.term === undefined) || complete.previous !== undefined) { + throw new Error("new jquery.ui version altering implementation" + + " details relied on"); + } + delete complete.term; + this.$el.removeClass('oe_focused') + .trigger('blur'); }, /** * @@ -518,7 +527,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea // _2: undefined if event=change, otherwise model var self = this; var started = []; - var $e = this.$element.find('div.oe_searchview_facets'); + var $e = this.$('div.oe_searchview_facets'); _.invoke(this.input_subviews, 'destroy'); this.input_subviews = []; @@ -549,7 +558,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea input_to_focus = self.input_subviews[(options.at + 1) * 2]; } - input_to_focus.$element.focus(); + input_to_focus.$el.focus(); }); }, @@ -615,7 +624,17 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea return null; } }, - on_loaded: function(data) { + + add_common_inputs: function() { + // add Filters to this.inputs, need view.controls filled + (new instance.web.search.Filters(this)); + // add custom filters to this.inputs + this.custom_filters = new instance.web.search.CustomFilters(this); + // add Advanced to this.inputs + (new instance.web.search.Advanced(this)); + }, + + search_view_loaded: function(data) { var self = this; this.fields_view = data.fields_view; if (data.fields_view.type !== 'search' || @@ -624,30 +643,28 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea "Got non-search view after asking for a search view: type %s, arch root %s", data.fields_view.type, data.fields_view.arch.tag)); } - this.make_widgets( 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 - this.custom_filters = new instance.web.search.CustomFilters(this); - // add Advanced to this.inputs - (new instance.web.search.Advanced(this)); + this.add_common_inputs(); // build drawer var drawer_started = $.when.apply( null, _(this.select_for_drawer()).invoke( - 'appendTo', this.$element.find('.oe_searchview_drawer'))); - + 'appendTo', this.$('.oe_searchview_drawer'))); + // load defaults var defaults_fetched = $.when.apply(null, _(this.inputs).invoke( - 'facet_for_defaults', this.defaults)) - .pipe(this.proxy('setup_default_query')); + '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(); }) + .then(function () { + self.trigger("search_view_loaded", data); + self.ready.resolve(); + }); }, setup_default_query: function () { // Hacky implementation of CustomFilters#facet_for_defaults ensure @@ -678,95 +695,6 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea this.query.reset(_(arguments).compact(), {preventSearch: true}); }, /** - * Handle event when the user make a selection in the filters management select box. - */ - on_filters_management: function(e) { - var self = this; - var select = this.$element.find(".oe_search-view-filters-management"); - var val = select.val(); - switch(val) { - case 'advanced_filter': - this.extended_search.on_activate(); - break; - case 'add_to_dashboard': - this.on_add_to_dashboard(); - break; - case '': - this.do_clear(); - } - if (val.slice(0, 4) == "get:") { - val = val.slice(4); - val = parseInt(val, 10); - var filter = this.managed_filters[val]; - this.do_clear(false).then(_.bind(function() { - select.val('get:' + val); - - var groupbys = []; - var group_by = filter.context.group_by; - if (group_by) { - groupbys = _.map( - group_by instanceof Array ? group_by : group_by.split(','), - function (el) { return { group_by: el }; }); - } - this.filter_data = { - domains: [filter.domain], - contexts: [filter.context], - groupbys: groupbys - }; - this.do_search(); - }, this)); - } else { - select.val(''); - } - }, - on_add_to_dashboard: function() { - this.$element.find(".oe_search-view-filters-management")[0].selectedIndex = 0; - var self = this, - menu = instance.webclient.menu, - $dialog = $(QWeb.render("SearchView.add_to_dashboard", { - dashboards : menu.data.data.children, - selected_menu_id : menu.$element.find('a.active').data('menu') - })); - $dialog.find('input').val(this.fields_view.name); - instance.web.dialog($dialog, { - modal: true, - title: _t("Add to Dashboard"), - buttons: [ - {text: _t("Cancel"), click: function() { - $(this).dialog("close"); - }}, - {text: _t("OK"), click: function() { - $(this).dialog("close"); - var menu_id = $(this).find("select").val(), - title = $(this).find("input").val(), - data = self.build_search_data(), - context = new instance.web.CompoundContext(), - domain = new instance.web.CompoundDomain(); - _.each(data.contexts, function(x) { - context.add(x); - }); - _.each(data.domains, function(x) { - domain.add(x); - }); - self.rpc('/web/searchview/add_to_dashboard', { - menu_id: menu_id, - action_id: self.getParent().action.id, - context_to_save: context, - domain: domain, - view_mode: self.getParent().active_view, - name: title - }, function(r) { - if (r === false) { - self.do_warn("Could not add filter to dashboard"); - } else { - self.do_notify("Filter added to dashboard", ''); - } - }); - }} - ] - }); - }, - /** * Extract search data from the view's facets. * * Result is an object with 4 (own) properties: @@ -815,7 +743,8 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea groupbys: groupbys, errors: errors }; - }, /** + }, + /** * Performs the search view collection of widget data. * * If the collection went well (all fields are valid), then triggers @@ -824,7 +753,8 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea * 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 + * @param [_query] + * @param {Object} [options] */ do_search: function (_query, options) { if (options && options.preventSearch) { @@ -835,7 +765,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea this.on_invalid(search.errors); return; } - return this.on_search(search.domains, search.contexts, search.groupbys); + this.trigger('search_data', search.domains, search.contexts, search.groupbys); }, /** * Triggered after the SearchView has collected all relevant domains and @@ -853,8 +783,6 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea * @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 */ - on_search: function (domains, contexts, groupbys) { - }, /** * Triggered after a validation error in the SearchView fields. * @@ -868,6 +796,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea */ on_invalid: function (errors) { this.do_notify(_t("Invalid Search"), _t("triggered from search view")); + this.trigger('invalid_search', errors); } }); @@ -878,6 +807,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea instance.web.search.fields = new instance.web.Registry({ 'char': 'instance.web.search.CharField', 'text': 'instance.web.search.CharField', + 'html': 'instance.web.search.CharField', 'boolean': 'instance.web.search.BooleanField', 'integer': 'instance.web.search.IntegerField', 'id': 'instance.web.search.IntegerField', @@ -913,13 +843,13 @@ instance.web.search.Invalid = instance.web.Class.extend( /** @lends instance.web ); } }); -instance.web.search.Widget = instance.web.OldWidget.extend( /** @lends instance.web.search.Widget# */{ +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.OldWidget + * @extends instance.web.Widget * * @param view the ancestor view of this widget */ @@ -1041,7 +971,7 @@ instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends in this.view.query.on('add remove change reset', this.proxy('search_change')); }, start: function () { - this.$element.on('click', 'li', this.proxy('toggle_filter')); + this.$el.on('click', 'li', this.proxy('toggle_filter')); return $.when(null); }, /** @@ -1050,7 +980,7 @@ instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends in */ search_change: function () { var self = this; - var $filters = this.$element.find('> li').removeClass('oe_selected'); + var $filters = this.$('> li').removeClass('oe_selected'); var facet = this.view.query.find(_.bind(this.match_facet, this)); if (!facet) { return; } facet.values.each(function (v) { @@ -1261,7 +1191,7 @@ instance.web.search.Field = instance.web.search.Input.extend( /** @lends instanc var self = this; // A field needs a context to send when active var context = this.attrs.context; - if (!context || !facet.values.length) { + if (_.isEmpty(context) || !facet.values.length) { return; } var contexts = facet.values.map(function (facetValue) { @@ -1351,17 +1281,17 @@ instance.web.search.CharField = instance.web.search.Field.extend( /** @lends ins }); instance.web.search.NumberField = instance.web.search.Field.extend(/** @lends instance.web.search.NumberField# */{ value_from: function () { - if (!this.$element.val()) { + if (!this.$el.val()) { return null; } - var val = this.parse(this.$element.val()), - check = Number(this.$element.val()); + var val = this.parse(this.$el.val()), + check = Number(this.$el.val()); if (isNaN(val) || val !== check) { - this.$element.addClass('error'); + this.$el.addClass('error'); throw new instance.web.search.Invalid( - this.attrs.name, this.$element.val(), this.error_message); + this.attrs.name, this.$el.val(), this.error_message); } - this.$element.removeClass('error'); + this.$el.removeClass('error'); return val; } }); @@ -1542,9 +1472,16 @@ instance.web.search.ManyToOneField = instance.web.search.CharField.extend({ facet_for: function (value) { var self = this; if (value instanceof Array) { - return $.when(facet_from(this, value)); + if (value.length === 2 && _.isString(value[1])) { + return $.when(facet_from(this, value)); + } + assert(value.length <= 1, + _t("M2O search fields do not currently handle multiple default values")); + // there are many cases of {search_default_$m2ofield: [id]}, need + // to handle this as if it were a single value. + value = value[0]; } - return this.model.call('name_get', [value], {}).pipe(function (names) { + return this.model.call('name_get', [value]).pipe(function (names) { if (_(names).isEmpty()) { return null; } return facet_from(self, names[0]); }) @@ -1557,6 +1494,15 @@ instance.web.search.ManyToOneField = instance.web.search.CharField.extend({ return [[name, '=', facetValue.get('value')]]; } return this._super(name, operator, facetValue); + }, + get_context: function (facet) { + var values = facet.values; + if (_.isEmpty(this.attrs.context) && values.length === 1) { + var c = {}; + c['default_' + this.attrs.name] = values.at(0).get('value'); + return c; + } + return this._super(facet); } }); @@ -1572,9 +1518,17 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({ this.model = new instance.web.Model('ir.filters'); this.filters = {}; this.$filters = {}; - this.$element.on('submit', 'form', this.proxy('save_current')); - this.$element.on('click', 'h4', function () { - self.$element.toggleClass('oe_opened'); + this.view.query + .on('remove', function (facet) { + if (!facet.get('is_custom_filter')) { + return; + } + self.clear_selection(); + }) + .on('reset', this.proxy('clear_selection')); + this.$el.on('submit', 'form', this.proxy('save_current')); + this.$el.on('click', 'h4', function () { + self.$el.toggleClass('oe_opened'); }); // FIXME: local eval of domain and context to get rid of special endpoint return this.rpc('/web/searchview/get_filters', { @@ -1621,18 +1575,20 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({ */ facet_for: function (filter) { return { - category: _("Custom Filter"), + category: _t("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} - ] + is_custom_filter: true, + values: [{label: filter.name, value: null}] }; }, + clear_selection: function () { + this.$('li.oe_selected').removeClass('oe_selected'); + }, append_filter: function (filter) { var self = this; var key = this.key_for(filter); @@ -1644,13 +1600,12 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({ var id = filter.id; this.filters[key] = filter; $filter = this.$filters[key] = $('<li></li>') - .appendTo(this.$element.find('.oe_searchview_custom_list')) + .appendTo(this.$('.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")) + $('<a class="oe_searchview_custom_delete">x</a>') .click(function (e) { e.stopPropagation(); self.model.call('unlink', [id]).then(function () { @@ -1664,6 +1619,7 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({ $filter.unbind('click').click(function () { self.view.query.reset([self.facet_for(filter)]); + $filter.addClass('oe_selected'); }); }, set_filters: function (filters) { @@ -1671,11 +1627,9 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({ }, save_current: function () { var self = this; - var $name = this.$element.find('input:first'); - var private_filter = !this.$element.find( - 'input#oe_searchview_custom_public').prop('checked'); - var set_as_default = this.$element.find( - 'input#oe_searchview_custom_default').prop('checked'); + 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'); var search = this.view.build_search_data(); this.rpc('/web/session/eval_domain_and_context', { @@ -1688,7 +1642,7 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({ } var filter = { name: $name.val(), - user_id: private_filter ? instance.connection.uid : false, + user_id: private_filter ? instance.session.uid : false, model_id: self.view.model, context: results.context, domain: results.domain, @@ -1698,7 +1652,7 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({ return self.model.call('create_or_replace', [filter]).then(function (id) { filter.id = id; self.append_filter(filter); - self.$element + self.$el .removeClass('oe_opened') .find('form')[0].reset(); }); @@ -1725,7 +1679,7 @@ instance.web.search.Filters = instance.web.search.Input.extend({ var col1 = [], col2 = _(this.view.controls).map(function (inputs, group) { var filters = _(inputs).filter(is_group); return { - name: group === 'null' ? _t("Filters") : group, + name: group === 'null' ? "<span class='oe_i'>q</span> " + _t("Filters") : "<span class='oe_i'>w</span> " + group, filters: filters, length: _(filters).chain().map(function (i) { return i.filters.length; }).sum().value() @@ -1743,26 +1697,27 @@ instance.web.search.Filters = instance.web.search.Input.extend({ } return $.when( - this.render_column(col1, $('<div>').appendTo(this.$element)), - this.render_column(col2, $('<div>').appendTo(this.$element))); + this.render_column(col1, $('<div>').appendTo(this.$el)), + this.render_column(col2, $('<div>').appendTo(this.$el))); }, render_column: function (column, $el) { return $.when.apply(null, _(column).map(function (group) { - $('<h3>').text(group.name).appendTo($el); + $('<h3>').html(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 + this.$el .on('keypress keydown keyup', function (e) { e.stopPropagation(); }) .on('click', 'h4', function () { - self.$element.toggleClass('oe_opened'); + self.$el.toggleClass('oe_opened'); }).on('click', 'button.oe_add_condition', function () { self.append_proposition(); }).on('submit', 'form', function (e) { @@ -1771,7 +1726,7 @@ instance.web.search.Advanced = instance.web.search.Input.extend({ }); return $.when( this._super(), - this.rpc("/web/searchview/fields_get", {model: this.view.model}, function(data) { + this.rpc("/web/searchview/fields_get", {model: this.view.model}).then(function(data) { self.fields = _.extend({ id: { string: 'ID', type: 'id' } }, data.fields); @@ -1780,11 +1735,20 @@ instance.web.search.Advanced = instance.web.search.Input.extend({ }); }, append_proposition: function () { + var self = this; return (new instance.web.search.ExtendedSearchProposition(this, this.fields)) - .appendTo(this.$element.find('ul')); + .appendTo(this.$('ul')).then(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 () { - var self = this; // Get domain sections from all propositions var children = this.getChildren(); var propositions = _.invoke(children, 'get_proposition'); @@ -1808,15 +1772,22 @@ instance.web.search.Advanced = instance.web.search.Input.extend({ // add new empty proposition this.append_proposition(); // TODO: API on searchview - this.view.$element.removeClass('oe_searchview_open_drawer'); + this.view.$el.removeClass('oe_searchview_open_drawer'); } }); -instance.web.search.ExtendedSearchProposition = instance.web.OldWidget.extend(/** @lends instance.web.search.ExtendedSearchProposition# */{ +instance.web.search.ExtendedSearchProposition = instance.web.Widget.extend(/** @lends instance.web.search.ExtendedSearchProposition# */{ template: 'SearchView.extended_search.proposition', + events: { + 'change .searchview_extended_prop_field': 'changed', + 'click .searchview_extended_delete_prop': function (e) { + e.stopPropagation(); + this.getParent().remove_proposition(this); + } + }, /** * @constructs instance.web.search.ExtendedSearchProposition - * @extends instance.web.OldWidget + * @extends instance.web.Widget * * @param parent * @param fields @@ -1831,17 +1802,10 @@ instance.web.search.ExtendedSearchProposition = instance.web.OldWidget.extend(/* this.value = null; }, start: function () { - var _this = this; - this.$element.find(".searchview_extended_prop_field").change(function() { - _this.changed(); - }); - this.$element.find('.searchview_extended_delete_prop').click(function () { - _this.destroy(); - }); - this.changed(); + return this._super().then(this.proxy('changed')); }, changed: function() { - var nval = this.$element.find(".searchview_extended_prop_field").val(); + var nval = this.$(".searchview_extended_prop_field").val(); if(this.attrs.selected == null || nval != this.attrs.selected.name) { this.select_field(_.detect(this.fields, function(x) {return x.name == nval;})); } @@ -1856,7 +1820,7 @@ instance.web.search.ExtendedSearchProposition = instance.web.OldWidget.extend(/* if(this.attrs.selected != null) { this.value.destroy(); this.value = null; - this.$element.find('.searchview_extended_prop_op').html(''); + this.$('.searchview_extended_prop_op').html(''); } this.attrs.selected = field; if(field == null) { @@ -1872,9 +1836,9 @@ instance.web.search.ExtendedSearchProposition = instance.web.OldWidget.extend(/* _.each(this.value.operators, function(operator) { $('<option>', {value: operator.value}) .text(String(operator.text)) - .appendTo(self.$element.find('.searchview_extended_prop_op')); + .appendTo(self.$('.searchview_extended_prop_op')); }); - var $value_loc = this.$element.find('.searchview_extended_prop_value').empty(); + var $value_loc = this.$('.searchview_extended_prop_value').empty(); this.value.appendTo($value_loc); }, @@ -1882,7 +1846,7 @@ instance.web.search.ExtendedSearchProposition = instance.web.OldWidget.extend(/* if ( this.attrs.selected == null) return null; var field = this.attrs.selected; - var op = this.$element.find('.searchview_extended_prop_op')[0]; + var op = this.$('.searchview_extended_prop_op')[0]; var operator = op.options[op.selectedIndex]; return { label: _.str.sprintf(_t('%(field)s %(operator)s "%(value)s"'), { @@ -1926,7 +1890,7 @@ instance.web.search.ExtendedSearchProposition.Char = instance.web.search.Extende {value: "!=", text: _lt("is not equal to")} ], get_value: function() { - return this.$element.val(); + return this.$el.val(); } }); instance.web.search.ExtendedSearchProposition.DateTime = instance.web.search.ExtendedSearchProposition.Field.extend({ @@ -1950,7 +1914,7 @@ instance.web.search.ExtendedSearchProposition.DateTime = instance.web.search.Ext start: function() { var ready = this._super(); this.datewidget = new (this.widget())(this); - this.datewidget.appendTo(this.$element); + this.datewidget.appendTo(this.$el); return ready; } }); @@ -1968,11 +1932,11 @@ instance.web.search.ExtendedSearchProposition.Integer = instance.web.search.Exte {value: "<=", text: _lt("less or equal than")} ], toString: function () { - return this.$element.val(); + return this.$el.val(); }, get_value: function() { try { - return instance.web.parse_value(this.$element.val(), {'widget': 'integer'}); + return instance.web.parse_value(this.$el.val(), {'widget': 'integer'}); } catch (e) { return ""; } @@ -1992,11 +1956,11 @@ instance.web.search.ExtendedSearchProposition.Float = instance.web.search.Extend {value: "<=", text: _lt("less or equal than")} ], toString: function () { - return this.$element.val(); + return this.$el.val(); }, get_value: function() { try { - return instance.web.parse_value(this.$element.val(), {'widget': 'float'}); + return instance.web.parse_value(this.$el.val(), {'widget': 'float'}); } catch (e) { return ""; } @@ -2009,12 +1973,12 @@ instance.web.search.ExtendedSearchProposition.Selection = instance.web.search.Ex {value: "!=", text: _lt("is not")} ], toString: function () { - var select = this.$element[0]; + var select = this.$el[0]; var option = select.options[select.selectedIndex]; return option.label || option.text; }, get_value: function() { - return this.$element.val(); + return this.$el.val(); } }); instance.web.search.ExtendedSearchProposition.Boolean = instance.web.search.ExtendedSearchProposition.Field.extend({ diff --git a/addons/web/static/src/js/test_support.js b/addons/web/static/src/js/test_support.js index d46366326b7..8e1317ba2b0 100644 --- a/addons/web/static/src/js/test_support.js +++ b/addons/web/static/src/js/test_support.js @@ -1,18 +1,17 @@ openerp.test_support = { - setup_connection: function (connection) { + setup_session: function (session) { var origin = location.protocol+"//"+location.host; - _.extend(connection, { + _.extend(session, { origin: origin, prefix: origin, server: origin, // keep chs happy //openerp.web.qweb.default_dict['_s'] = this.origin; - rpc_function: connection.rpc_json, + rpc_function: session.rpc_json, session_id: false, uid: false, username: false, user_context: {}, db: false, - openerp_entreprise: false, // this.module_list = openerp._modules.slice(); // this.module_loaded = {}; // _(this.module_list).each(function (mod) { @@ -22,7 +21,7 @@ openerp.test_support = { shortcuts: [], active_id: null }); - return connection.session_reload(); + return session.session_reload(); }, module: function (title, tested_core, nonliterals) { var conf = QUnit.config.openerp = {}; @@ -31,10 +30,10 @@ openerp.test_support = { QUnit.stop(); var oe = conf.openerp = window.openerp.init(); window.openerp.web[tested_core](oe); - var done = openerp.test_support.setup_connection(oe.connection); + var done = openerp.test_support.setup_session(oe.session); if (nonliterals) { done = done.pipe(function () { - return oe.connection.rpc('/tests/add_nonliterals', { + return oe.session.rpc('/tests/add_nonliterals', { domains: nonliterals.domains || [], contexts: nonliterals.contexts || [] }).then(function (r) { diff --git a/addons/web/static/src/js/view_editor.js b/addons/web/static/src/js/view_editor.js deleted file mode 100644 index f169887cc54..00000000000 --- a/addons/web/static/src/js/view_editor.js +++ /dev/null @@ -1,1207 +0,0 @@ -openerp.web.view_editor = function(instance) { -var _t = instance.web._t; -var QWeb = instance.web.qweb; -instance.web.ViewEditor = instance.web.OldWidget.extend({ - init: function(parent, element_id, dataset, view, options) { - this._super(parent); - this.element_id = element_id; - this.parent = parent; - this.dataset = new instance.web.DataSetSearch(this, 'ir.ui.view', null, null), - this.model = dataset.model; - this.xml_element_id = 0; - this.property = instance.web.ViewEditor.property_widget; - this.one_object = false; - }, - start: function() { - this.init_view_editor(); - }, - init_view_editor: function() { - var self = this, - action_title = _.str.sprintf(_t("Manage Views (%s)"), this.model); - var action = { - name: action_title, - context: this.session.user_context, - domain: [["model", "=", this.model]], - res_model: 'ir.ui.view', - views: [[false, 'list']], - type: 'ir.actions.act_window', - target: "current", - limit: this.dataset.limit || 80, - auto_search : true, - flags: { - sidebar: false, - deletable: false, - views_switcher: false, - action_buttons: false, - search_view: false, - pager: false, - radio: true, - select_view_id: self.parent.fields_view.view_id - } - }; - this.view_edit_dialog = new instance.web.Dialog(this, { - title: action_title, - width: 850, - buttons: [ - {text: _t("Create"), click: function() { self.on_create_view(); }}, - {text: _t("Edit"), click: function() { self.xml_element_id = 0; self.get_arch(); }}, - {text: _t("Remove"), click: function() { self.do_delete_view(); }}, - {text: _t("Close"), click: function() { self.view_edit_dialog.close(); window.location.reload(); }} - ] - }).open(); - this.view_edit_dialog.on_close.add_last(function(){window.location.reload();}); - this.main_view_id = this.parent.fields_view.view_id; - this.action_manager = new instance.web.ActionManager(this); - this.action_manager.appendTo(this.view_edit_dialog); - $.when(this.action_manager.do_action(action)).then(function() { - var viewmanager = self.action_manager.inner_viewmanager, - controller = viewmanager.views[viewmanager.active_view].controller; - controller.on_loaded.add_last(function(){ - $(controller.groups).bind({ - 'selected': function(e, ids, records) { - self.main_view_id = ids[0]; - } - }) - }); - }); - }, - on_create_view: function() { - var self = this; - this.create_view_dialog = new instance.web.Dialog(this, { - title: _.str.sprintf(_t("Create a view (%s)"), self.model), - buttons: [ - {text: _t("Save"), click: function () { - var view_values = {}; - var warn = false; - _.each(self.create_view_widget, function(widget) { - if (widget.is_invalid) { - warn = true; - return false; - } - if (widget.dirty && !widget.is_invalid) { - view_values[widget.name] = widget.get_value(); - } - }); - if (warn) { - self.on_valid_create_view(self.create_view_widget); - } else { - $.when(self.do_save_view(view_values)).then(function() { - self.create_view_dialog.close(); - var controller = self.action_manager.inner_viewmanager.views[self.action_manager.inner_viewmanager.active_view].controller; - controller.reload_content(); - }); - } - }}, - {text: _t("Cancel"), click: function () { self.create_view_dialog.close(); }} - ] - }).open(); - var view_widget = [{'name': 'view_name', 'string':'View Name', 'type': 'char', 'required': true, 'value' : this.model + '.custom_' + Math.round(Math.random() * 1000)}, - {'name': 'view_type', 'string': 'View Type', 'type': 'selection', 'required': true, 'value': 'Form', 'selection': [['',''],['tree', 'Tree'],['form', 'Form'],['graph', 'Graph'],['calendar', 'Calender']]}, - {'name': 'proirity', 'string': 'Priority', 'type': 'float', 'required': true, 'value':'16'}]; - this.create_view_dialog.$element.append('<table id="create_view" style="width:400px" class="oe_form"></table>'); - this.create_view_widget = []; - _.each(view_widget, function(widget) { - var type_widget = new (self.property.get_any([widget.type])) (self.create_view_dialog, widget); - self.create_view_dialog.$element.find('table[id=create_view]').append('<tr><td width="100px" align="right">' + widget.string + ':</td>' + type_widget.render()+'</tr>'); - var value = null; - if (widget.value) { - value = widget.value; - type_widget.dirty = true; - } - type_widget.start(); - type_widget.set_value(value); - self.create_view_widget.push(type_widget); - }); - }, - do_save_view: function(values) { - def = $.Deferred(); - var field_dataset = new instance.web.DataSetSearch(this, this.model, null, null); - var model_dataset = new instance.web.DataSetSearch(this, 'ir.model', null, null); - var view_string = "", field_name = false, self = this; - field_dataset.call( 'fields_get', [], function(fields) { - _.each(['name', 'x_name'], function(value) { - if (_.include(_.keys(fields), value)) { - field_name = value; - return false; - } - }); - if (field_name) { - model_dataset.read_slice(['name','field_id'], {"domain": [['model','=',self.model]]}).then(function(records) { - if (records) {view_string = records[0].name;} - var arch = _.str.sprintf("<?xml version='1.0'?>\n<%s string='%s'>\n\t<field name='%s'/>\n</%s>", values.view_type, view_string, field_name, values.view_type); - var vals = {'model': self.model, 'name': values.view_name, 'priority': values.priority, 'type': values.view_type, 'arch': arch}; - self.dataset.create(vals, function(suc) { - def.resolve(); - }); - }); - } - }); - return def.promise(); - }, - on_valid_create_view: function(widgets) { - var msg = "<ul>"; - _.each(widgets, function(widget) { - if (widget.is_invalid) { - msg += "<li>" + widget.name + "</li>"; - } - }); - msg += "</ul>"; - this.do_warn("The following fields are invalid :", msg); - }, - add_node_name : function(node) { - if(node.tagName.toLowerCase() == "button" || node.tagName.toLowerCase() == "field"){ - return (node.getAttribute('name'))? - _.str.sprintf( "<%s name='%s'>",node.tagName.toLowerCase(), node.getAttribute('name')): - _.str.sprintf( "<%s>",node.tagName.toLowerCase()); - }else if(node.tagName.toLowerCase() == "group"){ - return (node.getAttribute('string'))? - _.str.sprintf( "<%s>",node.getAttribute('string')): - _.str.sprintf( "<%s>",node.tagName.toLowerCase()); - }else{ - return (node.getAttribute('string'))? - _.str.sprintf( "<%s string='%s'>",node.tagName.toLowerCase(), node.getAttribute('string')): - _.str.sprintf( "<%s>",node.tagName.toLowerCase()); - } - }, - do_delete_view: function() { - var self = this; - if (confirm(_t("Do you really want to remove this view?"))) { - var controller = this.action_manager.inner_viewmanager.views[this.action_manager.inner_viewmanager.active_view].controller; - this.dataset.unlink([this.main_view_id]).then(function() { - controller.reload_content(); - self.main_view_id = self.parent.fields_view.view_id; - }); - } - }, - create_View_Node: function(node){ - ViewNode = { - 'level': ($(node).parents()).length + 1, - 'id': this.xml_element_id += 1, - 'att_list': [], - 'name': this.add_node_name(node), - 'child_id': [] - }; - ViewNode.att_list.push(node.tagName.toLowerCase()); - _.each(node.attributes, function(att) { - ViewNode.att_list.push([att.nodeName, att.nodeValue]); - }); - return ViewNode; - }, - append_child_object: function(main_object, parent_id, child_obj_list) { - var self = this; - if (main_object.id == parent_id) { - main_object.child_id = child_obj_list; - return main_object; - } else { - _.each(main_object.child_id, function(child_object) { - self.append_child_object(child_object, parent_id, child_obj_list); - }); - } - }, - convert_arch_to_obj: function(xml_Node, main_object, parent_id) { - var self = this; - var child_obj_list = []; - _.each(xml_Node, function(element) { - child_obj_list.push(self.create_View_Node(element)); - }); - this.append_child_object(main_object, parent_id, child_obj_list); - var obj_xml_list = _.zip(xml_Node, child_obj_list); - _.each(obj_xml_list, function(node) { - var children = _.filter(node[0].childNodes, function(child) { - return child.nodeType == 1; - }); - if (children) { - self.convert_arch_to_obj(children, main_object, node[1].id); - } - }); - return main_object; - }, - parse_xml: function(arch, view_id) { - main_object = { - 'level': 0, - 'id': this.xml_element_id +=1, - 'att_list': [], - 'name': _.str.sprintf("<view view_id = %s>", view_id), - 'child_id': [] - }; - var xml_arch = QWeb.load_xml(arch); - return [this.convert_arch_to_obj(xml_arch.childNodes, main_object, this.xml_element_id)]; - }, - get_arch: function() { - var self = this; - var view_arch_list = []; - this.dataset.read_ids([parseInt(self.main_view_id)], ['arch', 'type','priority']).then(function(arch) { - if (arch.length) { - var arch_object = self.parse_xml(arch[0].arch, self.main_view_id); - self.main_view_type = arch[0].type == 'tree'? 'list': arch[0].type; - view_arch_list.push({"view_id": self.main_view_id, "arch": arch[0].arch,"priority":arch[0].priority}); - self.dataset.read_slice([], {domain: [['inherit_id','=', parseInt(self.main_view_id)]]}).then(function(result) { - _.each(result, function(res) { - view_arch_list.push({"view_id": res.id, "arch": res.arch,"priority":res.priority}); - self.inherit_view(arch_object, res); - }); - return self.edit_view({"main_object": arch_object, - "parent_child_id": self.parent_child_list(arch_object, []), - "arch": view_arch_list}); - }); - } else { - self.do_warn("Please select view in list :"); - } - }); - }, - parent_child_list : function(one_object, parent_list) { - var self = this; - _.each(one_object , function(element) { - if (element.child_id.length != 0) { - parent_list.push({"key": element.id, "value": _.pluck(element.child_id, 'id')}); - self.parent_child_list(element.child_id, parent_list); - } - }); - return parent_list; - }, - inherit_view : function(arch_object, result) { - var self = this, xml_list = [], xml_arch = QWeb.load_xml(result.arch); - if (xml_arch.childNodes[0].tagName == "data") { - xml_list = _.filter(xml_arch.childNodes[0].childNodes, function(child) { - return child.nodeType == 1; - }); - } else { - xml_list.push( xml_arch.childNodes[0]); - } - _.each(xml_list, function(xml) { - var expr_to_list = [], xpath_arch_object = self.parse_xml(QWeb.tools.xml_node_to_string(xml), result.id); - if (xml.tagName == "xpath") { - var part_expr = _.without(xml.getAttribute('expr').split("/"), ""); - _.each(part_expr, function(part) { - expr_to_list.push(_.without($.trim(part.replace(/[^a-zA-Z 0-9 _]+/g,'!')).split("!"), "")); - }); - } else { - var temp = _.reject(xpath_arch_object[0].child_id[0].att_list, function(list) { - return list instanceof Array? _.include(list, "position"): false; - }); - expr_to_list = [_.flatten(temp)]; - } - self.inherit_apply(expr_to_list, arch_object ,xpath_arch_object); - }); - }, - inherit_apply: function(expr_list ,arch_object ,xpath_arch_object) { - var self = this; - if (xpath_arch_object.length) { - var check = expr_list[0], obj = false; - switch (check.length) { - case 2: - if (parseInt(check[1])) { - //for field[3] - var temp_list = _.select(arch_object, function(element) { - return _.include(_.flatten(element.att_list), check[0]); - }); - obj = arch_object[_.indexOf(arch_object, temp_list[parseInt(check[1]) - 1])]; - } else { - //for notebook[last()] - obj = _.detect(arch_object, function(element) { - return _.include(_.flatten(element.att_list), check[0]); - }); - } - break; - case 3: - //for field[@name='type'] - obj = _.detect(arch_object, function(element){ - if ((_.intersection(_.flatten(element.att_list), _.uniq(check))).length == _.uniq(check).length) { - return element; - } - }); - break; - case 1: - //for /form/notebook - var temp_list = _.select(arch_object, function(element) { - return _.include(_.flatten(element.att_list), check[0]); - }); - if (temp_list.length != 0) { - expr_list.length == 1 ? obj = temp_list[0] : expr_list.shift(); - } - break; - } - if (obj) { - expr_list.shift(); - if (expr_list.length) { - self.inherit_apply(expr_list, obj.child_id, xpath_arch_object); - } else { - self.increase_level(xpath_arch_object[0], obj.level + 1); - obj.child_id.push(xpath_arch_object[0]); - xpath_arch_object.pop(); - } - } else { - _.each(arch_object, function(element) { - self.inherit_apply(expr_list, element.child_id, xpath_arch_object); - }); - } - } - }, - increase_level: function(val, level) { - var self = this; - val.level = level; - _.each(val.child_id, function(val, key) { - self.increase_level(val, level + 1); - }); - }, - do_select_row: function(row_id) { - this.edit_xml_dialog.$element.find("tr[id^='viewedit-']").removeClass('ui-selected'); - this.edit_xml_dialog.$element.find("tr[id=viewedit-" + row_id + "]").addClass('ui-selected'); - }, - do_parent_img_hide_show: function(img) { - if ($(img).attr('src') == '/web/static/src/img/collapse.gif') { - $(img).attr('src', '/web/static/src/img/expand.gif'); - this.on_expand(img); - } else { - $(img).attr('src', '/web/static/src/img/collapse.gif'); - this.on_collapse(img); - } - }, - edit_view: function(one_object) { - var self = this; - this.one_object = one_object; - this.edit_xml_dialog = new instance.web.Dialog(this, { - title: _.str.sprintf(_t("View Editor %d - %s"), self.main_view_id, self.model), - height: '90%', - buttons: [ - {text: _t("Inherited View"), click: function(){ - var selected_row = self.edit_xml_dialog.$element.find('.ui-selected'); - if (selected_row.length) { - if(selected_row.find('a').text().search("field") != -1){ - if (confirm(_t("Do you really wants to create an inherited view here?"))) { - self.inherited_view(selected_row); - } - }else{ - alert("Can't Update View"); - } - }else{ - alert("Select an element"); - } - }}, - {text: _t("Preview"), click: function() { - var action = { - context: self.session.user_context, - res_model: self.model, - views: [[self.main_view_id, self.main_view_type]], - type: 'ir.actions.act_window', - target: "new", - auto_search: true, - flags: { - sidebar: false, - views_switcher: false, - action_buttons: false - } - }; - var action_manager = new instance.web.ActionManager(self); - action_manager.do_action(action); - }}, - {text: _t("Close"), click: function(){ - self.action_manager.inner_viewmanager.views[self.action_manager.inner_viewmanager.active_view].controller.reload_content(); - self.edit_xml_dialog.close(); - }} - ] - }).open(); - var no_property_att = []; - _.each(_PROPERTIES, function(val, key) { - if (! val.length) no_property_att.push(key); - }); - this.edit_xml_dialog.$element.html(QWeb.render('view_editor', {'data': one_object['main_object'], 'no_properties': no_property_att})); - this.edit_xml_dialog.$element.find("tr[id^='viewedit-']").click(function() { - self.do_select_row(this.id.split('-')[1]); - }); - this.edit_xml_dialog.$element.find("img[id^='parentimg-']").click(function() { - self.do_parent_img_hide_show(this); - }); - this.edit_xml_dialog.$element.find("img[id^='side-']").click(function() { - self.on_select_img(this); - }); - }, - inherited_view: function(selected_row){ - var self = this; - var row_id = parseInt((selected_row.attr('id')).split('-')[1]); - var obj = self.get_object_by_id(row_id,self.one_object['main_object'], [])[0]; - var view_name = this.model + '.inherit_' + Math.round(Math.random() * 1000); - var view_find = selected_row; - var view_id; - var min_level = parseInt(selected_row.attr('level')); - while (1) { - view_find = view_find.prev(); - if (view_find.length == 0 || - self.edit_xml_dialog.$element.find(view_find).find('a').text().search("view_id") != -1 && - parseInt(view_find.attr('level')) < min_level ) { - view_id = parseInt($(view_find).find('a').text().replace(/[^0-9]+/g, '')); - break; - } - if (view_find.attr('level') < min_level) { - min_level = parseInt(view_find.attr('level')); - } - } - var val = _.detect(obj.att_list, function(val) {return val[0] == "name";}); - var priority = _.detect(self.one_object['arch'], function(val) {return val.view_id == view_id;}); - var arch = _.str.sprintf("<?xml version='1.0'?>\n\t <field name='%s' position='after'> </field>", val[1]); - var vals = {'model': self.model, 'name': view_name, 'priority': priority.priority + 1, 'type': "form", 'arch': arch,'inherit_id':self.main_view_id}; - this.dataset.create(vals, function(suc) { - var arch_to_obj = self.parse_xml(arch,suc.result); - obj.child_id.push(arch_to_obj[0]); - self.one_object['parent_child_id'] = self.parent_child_list(self.one_object['main_object'],[]); - self.one_object['arch'].push({'view_id':suc.result,"arch":arch,'priority': priority.priority + 1}); - self.increase_level(arch_to_obj[0],obj.level+1); - self.render_inherited_view(selected_row,arch_to_obj[0]); - }); - }, - render_inherited_view: function(selected_row,obj){ - var self = this,row_id = parseInt((selected_row.attr('id')).split('-')[1]); - var clone = this.create_clone(selected_row.clone(),obj); - if (selected_row.find("img[id^='parentimg-']").length == 0) { - ($(selected_row.find('a').parent()).siblings('td')) - .append($('<img width="16" height="16"></img>').attr('src', '/web/static/src/img/collapse.gif'). - attr('id','parentimg-'+ row_id).click(function(){ - self.do_parent_img_hide_show(this); - })); - } - self.edit_xml_dialog.$element. - find("tr[id='viewedit-"+row_id+"']").after(clone.removeClass('ui-selected')); - _.each(obj.child_id,function(obj){self.render_inherited_view(clone,obj);}); - }, - on_select_img: function(element_img) { - var self = this; - var side = $(element_img).closest("tr[id^='viewedit-']"); - this.one_object.clicked_tr_id = parseInt((side.attr('id')).split('-')[1]); - this.one_object.clicked_tr_level = parseInt(side.attr('level')); - var img = side.find("img[id='parentimg-" + this.one_object.clicked_tr_id + "']").attr('src'); - var view_id = 0, view_xml_id = 0, view_find = side; - //for view id finding - var min_level = this.one_object.clicked_tr_id; - if (($(side).find('a').text()).search("view_id") != -1) { - view_id = parseInt(($(view_find).find('a').text()).replace(/[^0-9]+/g, '')); - view_xml_id = (view_find.attr('id')).split('-')[1]; - this.one_object.clicked_tr_id += 1; - this.one_object.clicked_tr_level += 1; - }else{ - while (1) { - view_find = view_find.prev(); - if (view_find.length == 0 || - (self.edit_xml_dialog.$element.find(view_find).find('a').text()).search("view_id") != -1 - && parseInt(view_find.attr('level')) < min_level ) { - view_id = parseInt(($(view_find).find('a').text()).replace(/[^0-9]+/g, '')); - view_xml_id = parseInt((view_find.attr('id')).split('-')[1]); - break; - } - if (view_find.attr('level') < min_level) { - min_level = parseInt(view_find.attr('level')); - } - } - } - this.one_object.clicked_tr_view = [view_id, view_xml_id]; - switch (element_img.id) { - case "side-add": - self.do_node_add(side); - break; - case "side-remove": - if (confirm(_t("Do you really want to remove this node?"))) { - self.do_save_update_arch("remove_node"); - } - break; - case "side-edit": - self.do_node_edit(side); - break; - case "side-up": - self.do_node_up(side, img); - break; - case "side-down": - self.do_node_down(side, img); - break; - } - }, - do_node_add: function(side){ - var self = this; - var tr = self.get_object_by_id(this.one_object.clicked_tr_id, this.one_object['main_object'], [])[0].att_list[0]; - var parent_tr = ($(side).prevAll("tr[level=" + String(this.one_object.clicked_tr_level - 1) + "]"))[0]; - var field_dataset = new instance.web.DataSetSearch(this, this.model, null, null); - parent_tr = $(parent_tr).find('a').text(); - field_dataset.call( 'fields_get', [], function(result) { - var fields = _.keys(result); - fields.push(" "),fields.sort(); - var property_to_check = []; - _.each([tr, parent_tr],function(element) { - property_to_check.push( - _.detect(_.keys(_CHILDREN),function(res){ - return _.str.include(element, res); - })); - }); - self.on_add_node(property_to_check, fields); - }); - }, - do_node_edit: function(side) { - var self = this; - var result = self.get_object_by_id(this.one_object.clicked_tr_id, this.one_object['main_object'], []); - if (result.length && result[0] && result[0].att_list) { - var properties = _PROPERTIES[result[0].att_list[0]]; - self.on_edit_node(properties); - } - }, - do_node_down: function(cur_tr, img) { - var self = this, next_tr, last_tr, tr_to_move = []; - tr_to_move.push(cur_tr); - if (img) { - while (1) { - next_tr = cur_tr.next(); - if ( parseInt(next_tr.attr('level')) <= this.one_object.clicked_tr_level || next_tr.length == 0) { - last_tr = next_tr; - break; - } else { - tr_to_move.push(next_tr); - cur_tr = next_tr; - } - } - } else { - last_tr = cur_tr.next(); - } - if ((self.edit_xml_dialog.$element.find(last_tr).find('a').text()).search("view_id") != -1) { - return false; - } - if (last_tr.length != 0 && parseInt(last_tr.attr('level')) == this.one_object.clicked_tr_level) { - var last_tr_id = (last_tr.attr('id')).split('-')[1]; - img = last_tr.find("img[id='parentimg-" + last_tr_id + "']").attr('src'); - if (img) { - self.edit_xml_dialog.$element.find("img[id='parentimg-" + last_tr_id + "']"). - attr('src', '/web/static/src/img/expand.gif'); - while (1) { - var next_tr = last_tr.next(); - if (next_tr.attr('level') <= this.one_object.clicked_tr_level || next_tr.length == 0) break; - next_tr.hide(); - last_tr = next_tr; - } - } - tr_to_move.reverse(); - _.each(tr_to_move, function(rec) { - $(last_tr).after(rec); - }); - self.do_save_update_arch("down"); - } - }, - do_node_up: function(cur_tr, img) { - var self = this, side = cur_tr, tr_to_move = []; - tr_to_move.push(side); - while (1) { - var prev_tr = cur_tr.prev(); - if (this.one_object.clicked_tr_level >= parseInt(prev_tr.attr('level')) || prev_tr.length == 0) { - last_tr = prev_tr; - break; - } - cur_tr = prev_tr; - } - if (img) { - self.edit_xml_dialog.$element.find("img[id='parentimg-" + this.one_object.clicked_tr_id + "']"). - attr('src', '/web/static/src/img/expand.gif'); - while (1) { - next_tr = side.next(); - if (parseInt(next_tr.attr('level')) <= this.one_object.clicked_tr_level || next_tr.length == 0) { - break; - } else { - next_tr.hide(); - tr_to_move.push(next_tr); - side = next_tr; - } - } - } - if (last_tr.length != 0 && parseInt(last_tr.attr('level')) == this.one_object.clicked_tr_level && - (self.edit_xml_dialog.$element.find(last_tr).find('a').text()).search("view_id") == -1) { - _.each(tr_to_move, function(rec) { - $(last_tr).before(rec); - }); - self.do_save_update_arch("up"); - } - }, - do_save_update_arch: function(move_direct, update_values) { - var self = this; - var arch = _.detect(self.one_object['arch'], function(element) - {return element.view_id == self.one_object.clicked_tr_view[0]}); - var obj = self.get_object_by_id(this.one_object.clicked_tr_view[1],this.one_object['main_object'], []); - //for finding xpath tag from inherit view - var xml_arch = QWeb.load_xml(arch.arch); - if (xml_arch.childNodes[0].tagName == "data") { - var check_list = _.flatten(obj[0].child_id[0].att_list); - var children = _.filter(xml_arch.childNodes[0].childNodes, function (child) { - return child.nodeType == 1; - }); - arch.arch = _.detect(children, function(xml_child) { - var temp_obj = self.create_View_Node(xml_child), - insert = _.intersection(_.flatten(temp_obj.att_list),_.uniq(check_list)); - if (insert.length == _.uniq(check_list).length ) {return xml_child;} - }); - } - arch_to_pass = _.filter($(arch.arch), function (child) { - return child.nodeType == 1; - }); - return self.do_save_xml(arch_to_pass[0], obj[0].child_id[0],obj[0].child_id, move_direct, update_values,arch); - }, - get_object_by_id: function(id, one_object, result) { - var self = this; - if (result.length == 0 ) { - var check = _.detect(one_object , function(obj) { - return id == obj.id; - }); - if (check) {result.push(check);} - _.each(one_object, function(obj) { - self.get_object_by_id(id,obj.child_id, result); - }); - } - return result; - }, - create_clone: function(clone, new_node_obj){ - var self = this; - clone.find('a').text(new_node_obj.name); - ($(clone.find('a').parent()).siblings('td')).css( "padding-left", 20 * new_node_obj.level); - clone.attr("id", "viewedit-" + new_node_obj.id); - clone.attr("level", new_node_obj.level); - clone.find("img[id^='parentimg-']").remove(); - clone.bind("click",function(){ - self.do_select_row(this.id.split('-')[1]); - }); - clone.find("img[id^='side-']").click(function() { - self.on_select_img(this); - }); - return clone; - }, - do_save_xml: function(arch1, obj, child_list, move_direct, update_values, arch){ - var self = this, children_list = $(arch1).children(),list_obj_xml; - try{list_obj_xml = _.zip(children_list, obj.child_id);}catch(err){return;} - if (this.one_object.clicked_tr_id) { - if (obj.id == this.one_object.clicked_tr_id) { - var parent = false, index = _.indexOf(child_list, obj); - if (move_direct == "down") { - var next = $(arch1).next(); - $(next).after(arch1); - var re_insert_obj = child_list.splice(index, 1); - child_list.splice(index+1, 0, re_insert_obj[0]); - parent = $(arch1).parents(); - } else if (move_direct == "up") { - var prev = $(arch1).prev(); - $(prev).before(arch1); - var re_insert_obj = child_list.splice(index, 1); - child_list.splice(index-1, 0, re_insert_obj[0]); - parent = $(arch1).parents(); - } else if (move_direct == "update_node") { - _.each(update_values, function(val){ - if (val[1]) $(arch1)[0].setAttribute(val[0], val[1]); - else $(arch1)[0].removeAttribute(val[0]); - }); - var new_obj = self.create_View_Node(arch1); - new_obj.id = obj.id,new_obj.child_id = obj.child_id; - self.edit_xml_dialog.$element. - find("tr[id='viewedit-"+this.one_object.clicked_tr_id+"']"). - find('a').text(new_obj.name); - child_list.splice(index, 1, new_obj); - parent = $(arch1).parents(); - } else if(move_direct == "add_node") { - var tr_click = self.edit_xml_dialog.$element.find("tr[id='viewedit-"+self.one_object.clicked_tr_id+"']"), - temp_xml = QWeb.load_xml(update_values[0]), - object_xml = self.create_View_Node(temp_xml.childNodes[0]); - (update_values[1] == "Inside")? object_xml.level = obj.level + 1:object_xml.level = obj.level; - var clone = self.create_clone(tr_click.clone(),object_xml), - after_append = _.detect(self.one_object['parent_child_id'],function(ele){ - return self.one_object.clicked_tr_id == ele.key; - }); - after_append = (after_append)?_.last(after_append.value):self.one_object.clicked_tr_id; - switch (update_values[1]) { - case "After": - self.edit_xml_dialog.$element. - find("tr[id='viewedit-"+after_append+"']").after(clone); - $(arch1).after(update_values[0]); - child_list.splice(index + 1, 0, object_xml); - break; - case "Before": - tr_click.before(clone); - $(arch1).before(update_values[0]); - child_list.splice(index - 1, 0, object_xml); - break; - case "Inside": - if (tr_click.find("img[id^='parentimg-']").length == 0) { - ($(tr_click.find('a').parent()).siblings('td')) - .append($('<img width="16" height="16"></img>').attr('src', '/web/static/src/img/collapse.gif'). - attr('id','parentimg-'+ self.one_object.clicked_tr_id).click(function(){ - self.do_parent_img_hide_show(this); - })); - } - $(arch1).append(update_values[0]); - self.edit_xml_dialog.$element. - find("tr[id='viewedit-"+after_append+"']").after(clone); - obj.child_id.push(object_xml); - break; - } - self.edit_xml_dialog.$element. - find("tr[id='viewedit-" + object_xml.id + "']").removeClass('ui-selected'); - parent = $(arch1).parents(); - } else if (move_direct == "remove_node") { - parent = $(arch1).parents(); - if (parent.length == 0 || (parent[0].tagName.toLowerCase() == "data")) { - self.one_object.clicked_tr_id = self.one_object.clicked_tr_id -1; - self.one_object.clicked_tr_level = self.one_object.clicked_tr_level - 1; - (parent.length == 0)?parent.push("remove_view"):false; - } - $(arch1).remove(); - child_list.splice(index,1); - var cur_tr = self.edit_xml_dialog.$element. - find("tr[id='viewedit-" + self.one_object.clicked_tr_id + "']"); - _.each(self.get_list_tr(cur_tr,self.one_object.clicked_tr_level), function(tr_element){ - tr_element.remove(); - }); - cur_tr.remove(); - var parent_img = _.detect(self.one_object['parent_child_id'],function(element){ - return _.include(element.value, self.one_object.clicked_tr_id); - }); - if(parent_img.value.length == 1){ - self.edit_xml_dialog.$element. - find("tr[id='viewedit-"+parent_img.key+"']"). - find("img[id^='parentimg-']").remove(); - } - self.one_object['parent_child_id'] = self.parent_child_list(self.one_object['main_object'],[]); - } - var convert_to_utf = (parent.length != 0)? parent[parent.length-1]: arch1; - if (convert_to_utf != "remove_view") { - convert_to_utf = QWeb.tools.xml_node_to_string(convert_to_utf); - convert_to_utf = convert_to_utf.replace('xmlns="http://www.w3.org/1999/xhtml"', ""); - convert_to_utf = '<?xml version="1.0"?>' + convert_to_utf; - arch.arch = convert_to_utf; - this.dataset.write(this.one_object.clicked_tr_view[0] ,{"arch":convert_to_utf}, function(r) {}); - } else { - this.dataset.unlink([this.one_object.clicked_tr_view[0]],function(res) {}); - } - if(move_direct == "add_node"){ - self.add_node_dialog.close(); - self.on_select_img(clone.find("img[id='side-edit']")[0]); - self.one_object['parent_child_id'] = self.parent_child_list(self.one_object['main_object'],[]); - } - } - if (obj.level <= this.one_object.clicked_tr_level) { - _.each(list_obj_xml, function(child_node) { - self.do_save_xml(child_node[0], child_node[1], obj.child_id, move_direct, update_values, arch); - }); - } - } - }, - on_expand: function(expand_img){ - var level = parseInt($(expand_img).closest("tr[id^='viewedit-']").attr('level')); - var cur_tr = $(expand_img).closest("tr[id^='viewedit-']"); - _.each(this.get_list_tr(cur_tr,level), function(tr_element){ - tr_element.hide(); - }); - }, - get_list_tr: function(cur_tr,level){ - tr_list = []; - while (1) { - var nxt_tr = cur_tr.next(); - if (parseInt(nxt_tr.attr('level')) > level) { - cur_tr = nxt_tr; - tr_list.push(nxt_tr); - } else return tr_list; - } - }, - on_collapse: function(collapse_img) { - var self = this, id = collapse_img.id.split('-')[1]; - var datas = _.detect(self.one_object['parent_child_id'] , function(res) { - return res.key == id; - }); - _.each(datas.value, function (rec) { - var tr = self.edit_xml_dialog.$element.find("tr[id='viewedit-" + rec + "']"); - tr.find("img[id='parentimg-" + rec + "']").attr('src', '/web/static/src/img/expand.gif'); - tr.show(); - }); - }, - on_edit_node: function(properties){ - var self = this; - this.edit_node_dialog = new instance.web.Dialog(this,{ - title: _t("Properties"), - width: 450, - buttons: [ - {text: _t("Update"), click: function () { - var warn = false, update_values = []; - _.each(self.edit_widget, function(widget) { - if (widget.is_invalid) { - warn = true; - return false; - } - if (widget.dirty && !widget.is_invalid) { - update_values.push([widget.name, widget.get_value()]); - } - }); - if (warn) { - self.on_valid_create_view(self.edit_widget); - } else { - self.do_save_update_arch("update_node", update_values); - self.edit_node_dialog.close(); - } - }}, - {text: _t("Cancel"), click: function () { self.edit_node_dialog.close(); }} - ] - }).open(); - var _PROPERTIES_ATTRIBUTES = { - 'name' : {'name':'name', 'string': 'Name', 'type': 'char'}, - 'string' : {'name':'string', 'string': 'String', 'type': 'char'}, - 'required' : {'name':'required', 'string': 'Required', 'type': 'boolean'}, - 'readonly' : {'name':'readonly', 'string': 'Readonly', 'type': 'boolean'}, - 'domain' : {'name':'domain', 'string': 'Domain', 'type': 'char'}, - 'context' : {'name':'context', 'string': 'Context', 'type': 'char'}, - 'limit' : {'name':'limit', 'string': 'Limit', 'type': 'float'}, - 'min_rows' : {'name':'min_rows', 'string': 'Minimum rows', 'type': 'float'}, - 'date_start' : {'name':'date_start', 'string': 'Start date', 'type': 'char'}, - 'date_delay' : {'name':'date_delay', 'string': 'Delay date', 'type': 'char'}, - 'day_length' : {'name':'day_length', 'string': 'Day length', 'type': 'char'}, - 'mode' : {'name':'mode', 'string': 'Mode', 'type': 'char'}, - 'align' : {'name':'align', 'string': 'Alignment ', 'type': 'selection', 'selection': [['', ''], ['0.0', 'Left'], ['0.5', 'Center'], ['1.0', 'Right']]}, - 'icon' : {'name':'icon', 'string': 'Icon', 'type': 'selection', 'selection': _ICONS}, - 'type' : {'name':'type', 'string': 'Type', 'type': 'selection', 'selection': [['', ''], ['action', 'Action'], ['object', 'Object'], ['workflow', 'Workflow'], ['server_action', 'Server Action']]}, - 'special' : {'name':'special', 'string': 'Special', 'type': 'selection', 'selection': [['',''],['save', 'Save Button'], ['cancel', 'Cancel Button'], ['open', 'Open Button']]}, - 'target' : {'name':'target', 'string': 'Target', 'type': 'selection', 'selection': [['', ''], ['new', 'New Window']]}, - 'confirm' : {'name':'confirm', 'string': 'Confirm', 'type': 'char'}, - 'style' : {'name':'style', 'string': 'Style', 'type': 'selection', 'selection':[["",""],["1", "1"],["1-1", "1-1"],["1-2", "1-2"],["2-1", "2-1"],["1-1-1", "1-1-1"]]}, - 'filename' : {'name':'filename', 'string': 'File Name', 'type': 'char'}, - 'width' : {'name':'width', 'string': 'Width', 'type': 'float'}, - 'height' : {'name':'height', 'string': 'Height', 'type': 'float'}, - 'attrs' : {'name':'attrs', 'string': 'Attrs', 'type': 'char'}, - 'col' : {'name':'col', 'string': 'col', 'type': 'float'}, - 'link' : {'name':'link', 'string': 'Link', 'type': 'char'}, - 'position' : {'name':'position', 'string': 'Position', 'type': 'selection', 'selection': [['',''],['after', 'After'],['before', 'Before'],['inside', 'Inside'],['replace', 'Replace']]}, - 'states' : {'name':'states', 'string': 'states', 'type': 'char'}, - 'eval' : {'name':'eval', 'string': 'Eval', 'type': 'char'}, - 'ref' : {'name':'ref', 'string': 'Ref', 'type': 'char'}, - 'on_change' : {'name':'on_change', 'string': 'On change', 'type': 'char'}, - 'nolabel' : {'name':'nolabel', 'string': 'No label', 'type': 'boolean'}, - 'completion' : {'name':'completion', 'string': 'Completion', 'type': 'boolean'}, - 'colspan' : {'name':'colspan', 'string': 'Colspan', 'type': 'float'}, - 'widget' : {'name':'widget', 'string': 'widget', 'type': 'selection'}, - 'colors' : {'name':'colors', 'string': 'Colors', 'type': 'char'}, - 'editable' : {'name':'editable', 'string': 'Editable', 'type': 'selection', 'selection': [["",""],["top","Top"],["bottom", "Bottom"]]}, - 'groups' : {'name':'groups', 'string': 'Groups', 'type': 'selection_multi'}, - 'fonts' : {'name':'fonts', 'string': 'fonts', 'type': 'char'}, - }; - var arch_val = self.get_object_by_id(this.one_object.clicked_tr_id,this.one_object['main_object'], []); - this.edit_node_dialog.$element.append('<table id="rec_table" style="width:400px" class="oe_form"></table>'); - this.edit_widget = []; - self.ready = $.when(self.on_groups(properties)).then(function () { - _PROPERTIES_ATTRIBUTES['groups']['selection'] = self.groups; - var values = _.keys( instance.web.form.widgets.map); - values.push(''); - values.sort(); - _PROPERTIES_ATTRIBUTES['widget']['selection'] = values; - var widgets = _.filter(_PROPERTIES_ATTRIBUTES, function (property) { return _.include(properties, property.name)}); - _.each(widgets, function(widget) { - var type_widget = new (self.property.get_any([widget.type])) (self.edit_node_dialog, widget); - var value = _.detect(arch_val[0]['att_list'],function(res) { - return res instanceof Array? _.include(res, widget.name): false; - }); - - value = value instanceof Array ? value[1] : value; - self.edit_node_dialog.$element.find('table[id=rec_table]').append('<tr><td align="right">' + widget.string + ':</td>' + type_widget.render() + '</tr>'); - type_widget.start(); - type_widget.set_value(value); - self.edit_widget.push(type_widget); - }); - }); - }, - //for getting groups - on_groups: function(properties){ - var self = this, - def = $.Deferred(); - if (!_.include(properties, 'groups')) { - self.groups = false; - def.resolve(); - } - var group_ids = [], group_names = {}, groups = []; - var res_groups = new instance.web.DataSetSearch(this,'res.groups', null, null), - model_data = new instance.web.DataSetSearch(self,'ir.model.data', null, null); - res_groups.read_slice([], {}).done(function (res_grp) { - _.each(res_grp, function (res) { - var key = res.id; - group_names[key]=res.full_name; - group_ids.push(res.id); - }); - model_data.read_slice([], {domain: [ - ['res_id', 'in', group_ids], - ['model', '=', 'res.groups'] - ]}).done(function (model_grp) { - _.each(model_grp, function (res_group) { - groups.push([res_group.module + "." + res_group.name, group_names[res_group.res_id]]); - }); - self.groups = groups; - def.resolve(); - }); - }); - return def.promise(); - }, - on_add_node: function(properties, fields){ - var self = this; - var render_list = [{'name': 'node_type','selection': _.keys(_CHILDREN).sort(), 'value': 'field', 'string': 'Node Type','type': 'selection'}, - {'name': 'field_value','selection': fields, 'value': false, 'string': '','type': 'selection'}, - {'name': 'position','selection': ['After','Before','Inside'], 'value': false, 'string': 'Position','type': 'selection'}]; - this.add_widget = []; - this.add_node_dialog = new instance.web.Dialog(this,{ - title: _t("Properties"), - width: 450, - buttons: [ - {text: _t("Update"), click: function() { - var check_add_node = true, values = {}; - _.each(self.add_widget, function(widget) { - values[widget.name] = widget.get_value() || false; - }); - (values.position == "Inside")? - check_add_node =(_.include(_CHILDREN[properties[0]],values.node_type))?true:false: - check_add_node =(_.include(_CHILDREN[properties[1]],values.node_type))?true:false; - if(values.node_type == "field" && check_add_node ) - {check_add_node = (values.field_value != " ")?true:false; - } - if(check_add_node){ - var tag = (values.node_type == "field")? - _.str.sprintf("<%s name='%s'> </%s>",values.node_type,values.field_value,values.node_type): - _.str.sprintf("<%s> </%s>",values.node_type,values.node_type); - self.do_save_update_arch("add_node", [tag, values.position]); - } else { - alert("Can't Update View"); - } - }}, - {text: _t("Cancel"), click: function() { self.add_node_dialog.close(); }} - ] - }).open(); - this.add_node_dialog.$element.append('<table id="rec_table" style="width:420px" class="oe_form"><tbody><tr></tbody></table>'); - var table_selector = self.add_node_dialog.$element.find('table[id=rec_table] tbody'); - _.each(render_list, function(node) { - type_widget = new (self.property.get_any([node.type])) (self.add_node_dialog, node); - if (node.name == "position") { - table_selector.append('</tr><tr><td align="right" width="100px">' + node.string + '</td>' + type_widget.render() + '</tr>'); - } else { - table_selector.append('<td align="right">' + node.string + '</td>' + type_widget.render() ); - if (node.name == "field_value") { - table_selector.append('<td id="new_field" align="right" width="100px"> <button>New Field</button></td>'); - } - } - type_widget.start(); - type_widget.set_value(node.value); - self.add_widget.push(type_widget); - }); - table_selector.find("td[id^=]").attr("width","100px"); - self.add_node_dialog.$element.find('#new_field').click(function() { - model_data = new instance.web.DataSetSearch(self,'ir.model', null, null); - model_data.read_slice([], {domain: [['model','=', self.model]]}).then(function(result) { - self.render_new_field(result[0].id); - }); - }); - }, - render_new_field :function(id){ - var self = this; - var action = { - context: {'default_model_id': id, 'manual': true}, - res_model: "ir.model.fields", - views: [[false, 'form']], - type: 'ir.actions.act_window', - target: "new", - flags: { - action_buttons: true - } - }; - var action_manager = new instance.web.ActionManager(self); - $.when(action_manager.do_action(action)).then(function() { - var controller = action_manager.dialog_viewmanager.views['form'].controller; - controller.on_button_cancel.add_last(function(){ - action_manager.destroy() - }); - controller.do_save.add_last(function(){ - action_manager.destroy(); - var value =controller.fields.name.value; - self.add_node_dialog.$element.find('select[id=field_value]').append($("<option selected></option>").attr("value",value).text(value)); - _.detect(self.add_widget,function(widget){ - widget.name == "field_value"? widget.selection.push(value): false; - }); - }); - }); - } -}); -instance.web.ViewEditor.Field = instance.web.Class.extend({ - init: function(view, widget) { - this.$element = view.$element; - this.dirty = false; - this.name = widget.name; - this.selection = widget.selection || []; - this.required = widget.required || false; - this.string = widget.string || ""; - this.type = widget.type; - this.is_invalid = false; - }, - start: function () { - this.update_dom(); - }, - update_dom: function() { - this.$element.find("td[id=" + this.name + "]").toggleClass('invalid', this.is_invalid); - this.$element.find("td[id=" + this.name + "]").toggleClass('required', this.required); - }, - on_ui_change: function() { - this.validate(); - this.dirty = true; - this.update_dom(); - }, - validate: function() { - this.is_invalid = false; - try { - var value = instance.web.parse_value(this.get_value(), this, ''); - this.is_invalid = this.required && value === ''; - } catch(e) { - this.is_invalid = true; - } - }, - render: function() { - return _.str.sprintf("<td id = %s>%s</td>", this.name, QWeb.render(this.template, {widget: this})) - } -}); -instance.web.ViewEditor.FieldBoolean = instance.web.ViewEditor.Field.extend({ - template : "vieweditor_boolean", - start: function() { - var self = this; - this._super(); - this.$element.find("input[id="+ self.name+"]").change(function() { - self.on_ui_change(); - }); - }, - set_value: function(value) { - if (value) { - this.$element.find("input[id=" + this.name+ "]").attr('checked', true); - } - }, - get_value: function() { - return this.$element.find("input[id=" + this.name + "]").is(':checked')? "1" : null; - } -}); -instance.web.ViewEditor.FieldChar = instance.web.ViewEditor.Field.extend({ - template : "vieweditor_char", - start: function () { - var self = this; - this._super(); - this.$element.find("input[id="+ this.name+"]").css('width','100%').change(function() { - self.on_ui_change(); - }); - }, - set_value: function(value) { - this.$element.find("input[id=" + this.name + "]").val(value); - }, - get_value: function() { - return this.$element.find("input[id=" + this.name + "]").val(); - } -}); -instance.web.ViewEditor.FieldSelect = instance.web.ViewEditor.Field.extend({ - template : "vieweditor_selection", - start: function () { - var self = this; - this._super(); - this.$element.find("select[id=" + this.name + "]").css('width', '100%').change(function() { - self.on_ui_change(); - if (self.name == "node_type") { - if (self.get_value() == "field") { - self.$element.find('#new_field').show(); - self.$element.find("select[id=field_value]").show(); - } else { - self.$element.find('#new_field').hide(); - self.$element.find("select[id=field_value]").hide(); - } - } - }); - }, - set_value: function(value) { - var index = 0; - value = value === null? false: value; - for (var i = 0, ii = this.selection.length; i < ii; i++) { - if ((this.selection[i] instanceof Array && this.selection[i][0] === value) || this.selection[i] === value) index = i; - } - this.$element.find("select[id=" + this.name + "]")[0].selectedIndex = index; - }, - get_value: function() { - return this.$element.find("select[id=" + this.name + "]").val(); - } -}); -instance.web.ViewEditor.FieldSelectMulti = instance.web.ViewEditor.FieldSelect.extend({ - start: function () { - this._super(); - this.$element.find("select[id=" + this.name + "]").css('height', '100px').attr("multiple", true); - }, - set_value: function(value) { - var self = this; - self.$element.find("#groups option").attr("selected",false); - if (!value) return false; - _.each(this.selection, function(item) { - if (_.include(value.split(','), item[0])) { - self.$element.find("select[id="+self.name+"] option[value='" + item[0] +"']").attr("selected",1) - } - }); - } -}); -instance.web.ViewEditor.FieldFloat = instance.web.ViewEditor.FieldChar.extend({ -}); - -var _PROPERTIES = { - 'field' : ['name', 'string', 'required', 'readonly', 'domain', 'context', 'nolabel', 'completion', - 'colspan', 'widget', 'eval', 'ref', 'on_change', 'attrs', 'groups'], - 'form' : ['string', 'col', 'link'], - 'notebook' : ['colspan', 'position', 'groups'], - 'page' : ['string', 'states', 'attrs', 'groups'], - 'group' : ['string', 'col', 'colspan', 'states', 'attrs', 'groups'], - 'image' : ['filename', 'width', 'height', 'groups'], - 'separator' : ['string', 'colspan', 'groups'], - 'label': ['string', 'align', 'colspan', 'groups'], - 'button': ['name', 'string', 'icon', 'type', 'states', 'readonly', 'special', 'target', 'confirm', 'context', 'attrs', 'colspan', 'groups'], - 'newline' : [], - 'board': ['style'], - 'column' : [], - 'action' : ['name', 'string', 'colspan', 'groups'], - 'tree' : ['string', 'colors', 'editable', 'link', 'limit', 'min_rows', 'fonts'], - 'graph' : ['string', 'type'], - 'calendar' : ['string', 'date_start', 'date_stop', 'date_delay', 'day_length', 'color', 'mode'] -}; -var _CHILDREN = { - 'form': ['notebook', 'group', 'field', 'label', 'button','board', 'newline', 'separator'], - 'tree': ['field'], - 'graph': ['field'], - 'calendar': ['field'], - 'notebook': ['page'], - 'page': ['notebook', 'group', 'field', 'label', 'button', 'newline', 'separator'], - 'group': ['field', 'label', 'button', 'separator', 'newline','group'], - 'board': ['column'], - 'action': [], - 'field': ['form', 'tree', 'graph','field'], - 'label': [], - 'button' : [], - 'newline': [], - 'separator': [] -}; -var _ICONS = ['','STOCK_ABOUT', 'STOCK_ADD', 'STOCK_APPLY', 'STOCK_BOLD', - 'STOCK_CANCEL', 'STOCK_CDROM', 'STOCK_CLEAR', 'STOCK_CLOSE', 'STOCK_COLOR_PICKER', - 'STOCK_CONNECT', 'STOCK_CONVERT', 'STOCK_COPY', 'STOCK_CUT', 'STOCK_DELETE', - 'STOCK_DIALOG_AUTHENTICATION', 'STOCK_DIALOG_ERROR', 'STOCK_DIALOG_INFO', - 'STOCK_DIALOG_QUESTION', 'STOCK_DIALOG_WARNING', 'STOCK_DIRECTORY', 'STOCK_DISCONNECT', - 'STOCK_DND', 'STOCK_DND_MULTIPLE', 'STOCK_EDIT', 'STOCK_EXECUTE', 'STOCK_FILE', - 'STOCK_FIND', 'STOCK_FIND_AND_REPLACE', 'STOCK_FLOPPY', 'STOCK_GOTO_BOTTOM', - 'STOCK_GOTO_FIRST', 'STOCK_GOTO_LAST', 'STOCK_GOTO_TOP', 'STOCK_GO_BACK', - 'STOCK_GO_DOWN', 'STOCK_GO_FORWARD', 'STOCK_GO_UP', 'STOCK_HARDDISK', - 'STOCK_HELP', 'STOCK_HOME', 'STOCK_INDENT', 'STOCK_INDEX', 'STOCK_ITALIC', - 'STOCK_JUMP_TO', 'STOCK_JUSTIFY_CENTER', 'STOCK_JUSTIFY_FILL', - 'STOCK_JUSTIFY_LEFT', 'STOCK_JUSTIFY_RIGHT', 'STOCK_MEDIA_FORWARD', - 'STOCK_MEDIA_NEXT', 'STOCK_MEDIA_PAUSE', 'STOCK_MEDIA_PLAY', - 'STOCK_MEDIA_PREVIOUS', 'STOCK_MEDIA_RECORD', 'STOCK_MEDIA_REWIND', - 'STOCK_MEDIA_STOP', 'STOCK_MISSING_IMAGE', 'STOCK_NETWORK', 'STOCK_NEW', - 'STOCK_NO', 'STOCK_OK', 'STOCK_OPEN', 'STOCK_PASTE', 'STOCK_PREFERENCES', - 'STOCK_PRINT', 'STOCK_PRINT_PREVIEW', 'STOCK_PROPERTIES', 'STOCK_QUIT', - 'STOCK_REDO', 'STOCK_REFRESH', 'STOCK_REMOVE', 'STOCK_REVERT_TO_SAVED', - 'STOCK_SAVE', 'STOCK_SAVE_AS', 'STOCK_SELECT_COLOR', 'STOCK_SELECT_FONT', - 'STOCK_SORT_ASCENDING', 'STOCK_SORT_DESCENDING', 'STOCK_SPELL_CHECK', - 'STOCK_STOP', 'STOCK_STRIKETHROUGH', 'STOCK_UNDELETE', 'STOCK_UNDERLINE', - 'STOCK_UNDO', 'STOCK_UNINDENT', 'STOCK_YES', 'STOCK_ZOOM_100', - 'STOCK_ZOOM_FIT', 'STOCK_ZOOM_IN', 'STOCK_ZOOM_OUT', - 'terp-account', 'terp-crm', 'terp-mrp', 'terp-product', 'terp-purchase', - 'terp-sale', 'terp-tools', 'terp-administration', 'terp-hr', 'terp-partner', - 'terp-project', 'terp-report', 'terp-stock', 'terp-calendar', 'terp-graph' -]; -instance.web.ViewEditor.property_widget = new instance.web.Registry({ - 'boolean' : 'instance.web.ViewEditor.FieldBoolean', - 'selection_multi' : 'instance.web.ViewEditor.FieldSelectMulti', - 'selection' : 'instance.web.ViewEditor.FieldSelect', - 'char' : 'instance.web.ViewEditor.FieldChar', - 'float' : 'instance.web.ViewEditor.FieldFloat' -}); -}; diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index 3430f1dd703..bbb2234e3bd 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -9,31 +9,65 @@ instance.web.form = {}; /** * Interface implemented by the form view or any other object * able to provide the features necessary for the fields to work. - * + * * Properties: * - display_invalid_fields : if true, all fields where is_valid() return true should * be displayed as invalid. + * - actual_mode : the current mode of the field manager. Can be "view", "edit" or "create". * Events: * - view_content_has_changed : when the values of the fields have changed. When * this event is triggered all fields should reprocess their modifiers. + * - field_changed:<field_name> : when the value of a field change, an event is triggered + * named "field_changed:<field_name>" with <field_name> replaced by the name of the field. + * This event is not related to the on_change mechanism of OpenERP and is always called + * when the value of a field is setted or changed. This event is only triggered when the + * value of the field is syntactically valid, but it can be triggered when the value + * is sematically invalid (ie, when a required field is false). It is possible that an event + * about a precise field is never triggered even if that field exists in the view, in that + * case the value of the field is assumed to be false. */ instance.web.form.FieldManagerMixin = { /** * Must return the asked field as in fields_get. */ - get_field: function(field_name) {}, + get_field_desc: function(field_name) {}, /** - * Called by the field when the translate button is clicked. + * Returns the current value of a field present in the view. See the get_value() method + * method in FieldInterface for further information. */ - open_translate_dialog: function(field) {}, + get_field_value: function(field_name) {}, /** - * Returns true when the view is in create mode. - */ - is_create_mode: function() {}, + Gives new values for the fields contained in the view. The new values could not be setted + right after the call to this method. Setting new values can trigger on_changes. + + @param (dict) values A dictonnary with key = field name and value = new value. + @return (Deferred) Is resolved after all the values are setted. + */ + set_values: function(values) {}, + /** + Computes an OpenERP domain. + + @param (list) expression An OpenERP domain. + @return (boolean) The computed value of the domain. + */ + compute_domain: function(expression) {}, + /** + Builds an evaluation context for the resolution of the fields' contexts. Please note + the field are only supposed to use this context to evualuate their own, they should not + extend it. + + @return (CompoundContext) An OpenERP context. + */ + build_eval_context: function() {}, }; instance.web.views.add('form', 'instance.web.FormView'); -instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form.FieldManagerMixin, { +/** + * Properties: + * - actual_mode: always "view", "edit" or "create". Read-only property. Determines + * the mode used by the view. + */ +instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerMixin, { /** * Indicates that this view is not searchable, and thus that no search * view should be displayed (if there is one active). @@ -55,6 +89,7 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. * @property {instance.web.Registry} registry=instance.web.form.widgets widgets registry for this form view instance */ init: function(parent, dataset, view_id, options) { + var self = this; this._super(parent); this.set_default_options(options); this.dataset = dataset; @@ -68,135 +103,142 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. this.default_focus_button = null; this.fields_registry = instance.web.form.widgets; this.tags_registry = instance.web.form.tags; + this.widgets_registry = instance.web.form.custom_widgets; this.has_been_loaded = $.Deferred(); this.translatable_fields = []; _.defaults(this.options, { "not_interactible_on_create": false, "initial_mode": "view", + "disable_autofocus": false, + "footer_to_buttons": false, }); this.is_initialized = $.Deferred(); this.mutating_mutex = new $.Mutex(); this.on_change_mutex = new $.Mutex(); this.reload_mutex = new $.Mutex(); - this.rendering_engine = new instance.web.form.FormRenderingEngineReadonly(this); - this.qweb = null; // A QWeb instance will be created if the view is a QWeb template + this.__clicked_inside = false; + this.__blur_timeout = null; + this.rendering_engine = new instance.web.form.FormRenderingEngine(this); + self.set({actual_mode: self.options.initial_mode}); + this.has_been_loaded.then(function() { + self.on("change:actual_mode", self, self.check_actual_mode); + self.check_actual_mode(); + self.on("change:actual_mode", self, self.init_pager); + self.init_pager(); + }); + self.on("load_record", self, self.load_record); + this.on('view_loaded', self, self.load_form); + instance.web.bus.on('clear_uncommitted_changes', this, function(e) { + if (!this.can_be_discarded()) { + e.preventDefault(); + } + }); }, destroy: function() { _.each(this.get_widgets(), function(w) { + w.off('focused blurred'); w.destroy(); }); + if (this.$el) { + this.$el.off('.formBlur'); + } this._super(); }, - on_loaded: function(data) { + load_form: function(data) { var self = this; if (!data) { throw new Error("No data provided."); } if (this.arch) { - throw "Form view does not support multiple calls to on_loaded"; + throw "Form view does not support multiple calls to load_form"; } this.fields_order = []; this.fields_view = data; this.rendering_engine.set_fields_registry(this.fields_registry); this.rendering_engine.set_tags_registry(this.tags_registry); - if (!this.extract_qweb_template(data)) { - this.rendering_engine.set_fields_view(data); - var $dest = this.$element.hasClass("oe_form_container") ? this.$element : this.$element.find('.oe_form_container'); - this.rendering_engine.render_to($dest); - } + this.rendering_engine.set_widgets_registry(this.widgets_registry); + this.rendering_engine.set_fields_view(data); + var $dest = this.$el.hasClass("oe_form_container") ? this.$el : this.$el.find('.oe_form_container'); + this.rendering_engine.render_to($dest); + this.$el.on('mousedown.formBlur', function () { + self.__clicked_inside = true; + }); this.$buttons = $(QWeb.render("FormView.buttons", {'widget':self})); if (this.options.$buttons) { this.$buttons.appendTo(this.options.$buttons); } else { - this.$element.find('.oe_form_buttons').replaceWith(this.$buttons); + this.$el.find('.oe_form_buttons').replaceWith(this.$buttons); } - this.$buttons.on('click','.oe_form_button_create',this.on_button_create); - this.$buttons.on('click','.oe_form_button_edit',this.on_button_edit); - this.$buttons.on('click','.oe_form_button_save',this.on_button_save); - this.$buttons.on('click','.oe_form_button_cancel',this.on_button_cancel); - - this.$pager = $(QWeb.render("FormView.pager", {'widget':self})); - if (this.options.$pager) { - this.$pager.appendTo(this.options.$pager); - } else { - this.$element.find('.oe_form_pager').replaceWith(this.$pager); + this.$buttons.on('click', '.oe_form_button_create', this.on_button_create); + this.$buttons.on('click', '.oe_form_button_edit', this.on_button_edit); + this.$buttons.on('click', '.oe_form_button_save', this.on_button_save); + this.$buttons.on('click', '.oe_form_button_cancel', this.on_button_cancel); + if (this.options.footer_to_buttons) { + this.$el.find('footer').appendTo(this.$buttons); } - this.$pager.on('click','a[data-pager-action]',function() { - var action = $(this).data('pager-action'); - self.on_pager_action(action); - }); - this.$sidebar = this.options.$sidebar || this.$element.find('.oe_form_sidebar'); + this.$sidebar = this.options.$sidebar || this.$el.find('.oe_form_sidebar'); if (!this.sidebar && this.options.$sidebar) { this.sidebar = new instance.web.Sidebar(this); this.sidebar.appendTo(this.$sidebar); - if(this.fields_view.toolbar) { + if (this.fields_view.toolbar) { this.sidebar.add_toolbar(this.fields_view.toolbar); } - this.sidebar.add_items('other', [ - { label: _t('Delete'), callback: self.on_button_delete }, - { label: _t('Duplicate'), callback: self.on_button_duplicate }, - { label: _t('Set Default'), callback: function (item) { self.open_defaults_dialog(); } }, - ]); + this.sidebar.add_items('other', _.compact([ + self.is_action_enabled('delete') && { label: _t('Delete'), callback: self.on_button_delete }, + self.is_action_enabled('create') && { label: _t('Duplicate'), callback: self.on_button_duplicate }, + { label: _t('Set Default'), callback: function (item) { self.open_defaults_dialog(); } } + ])); } - this.on("change:mode", this, this.switch_mode); - this.set({mode: this.options.initial_mode}); + this.has_been_loaded.resolve(); + + // Add bounce effect on button 'Edit' when click on readonly page view. + this.$el.find(".oe_form_group_row,.oe_form_field,label").on('click', function (e) { + if(self.get("actual_mode") == "view") { + var $button = self.options.$buttons.find(".oe_form_button_edit"); + $button.effect('bounce', {distance: 18, times: 5}, 150); + e.stopPropagation(); + instance.web.bus.trigger('click', e); + } + }); + //bounce effect on red button when click on statusbar. + this.$el.find(".oe_form_field_status:not(.oe_form_status_clickable)").on('click', function (e) { + if((self.get("actual_mode") == "view")) { + var $button = self.$el.find(".oe_highlight:not(.oe_form_invisible)").css({'float':'left','clear':'none'}); + $button.effect('bounce', {distance:18, times: 5}, 150); + e.stopPropagation(); + } + }); + this.trigger('form_view_loaded', data); return $.when(); }, - extract_qweb_template: function(fvg) { - for (var i=0, ii=fvg.arch.children.length; i < ii; i++) { - var child = fvg.arch.children[i]; - if (child.tag === "templates") { - this.qweb = new QWeb2.Engine(); - this.qweb.add_template(instance.web.json_node_to_xml(child)); - if (!this.qweb.has_template('form')) { - throw new Error("No QWeb template found for form view"); - } - return true; - } + widgetFocused: function() { + // Clear click flag if used to focus a widget + this.__clicked_inside = false; + if (this.__blur_timeout) { + clearTimeout(this.__blur_timeout); + this.__blur_timeout = null; } - this.qweb = null; - return false; - }, - get_fvg_from_qweb: function(record) { - var view = this.qweb.render('form', this.get_qweb_context(record)); - var fvg = _.clone(this.fields_view); - fvg.arch = instance.web.xml_to_json(instance.web.str_to_xml(view).firstChild); - return fvg; - }, - get_qweb_context: function(record) { - var self = this, - new_record = {}; - _.each(record, function(value_, name) { - var r = _.clone(self.fields_view.fields[name] || {}); - if ((r.type === 'date' || r.type === 'datetime') && value_) { - r.raw_value = instance.web.auto_str_to_date(value_); - } else { - r.raw_value = value_; - } - r.value = instance.web.format_value(value_, r); - new_record[name] = r; - }); - return { - record : new_record, - new_record : !record.id - }; }, - kill_current_form: function() { - _.each(this.getChildren(), function(el) { - el.destroy(); - }); - this.fields = {}; - this.fields_order = []; - this.default_focus_field = null; - this.default_focus_button = null; - this.translatable_fields = []; - this.$element.find('.oe_form_container').empty(); + widgetBlurred: function() { + if (this.__clicked_inside) { + // clicked in an other section of the form (than the currently + // focused widget) => just ignore the blurring entirely? + this.__clicked_inside = false; + return; + } + var self = this; + // clear timeout, if any + this.widgetFocused(); + this.__blur_timeout = setTimeout(function () { + self.trigger('blurred'); + }, 0); }, + do_load_state: function(state, warm) { if (state.id && this.datarecord.id != state.id) { if (!this.dataset.get_id_index(state.id)) { @@ -208,39 +250,55 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. } } }, - do_show: function () { + /** + * + * @param {Object} [options] + * @param {Boolean} [mode=undefined] If specified, switch the form to specified mode. Can be "edit" or "view". + * @param {Boolean} [reload=true] whether the form should reload its content on show, or use the currently loaded record + * @return {$.Deferred} + */ + do_show: function (options) { var self = this; + options = options || {}; if (this.sidebar) { - this.sidebar.$element.show(); + this.sidebar.$el.show(); } if (this.$buttons) { this.$buttons.show(); - this.$buttons.find('.oe_form_button_save').removeClass('oe_form_button_save_dirty'); } - if (this.$pager) { - this.$pager.show(); - } - this.$element.show().css('visibility', 'hidden'); - this.$element.removeClass('oe_form_dirty'); - return this.has_been_loaded.pipe(function() { - var result; - if (self.dataset.index === null) { - // null index means we should start a new record - result = self.on_button_new(); - } else { - result = self.dataset.read_index(_.keys(self.fields_view.fields), { - context : { 'bin_size' : true } - }).pipe(self.on_record_loaded); - } - result.pipe(function() { - self.$element.css('visibility', 'visible'); + this.$el.show().css({ + opacity: '0', + filter: 'alpha(opacity = 0)' + }); + this.$el.add(this.$buttons).removeClass('oe_form_dirty'); + + var shown = this.has_been_loaded; + if (options.reload !== false) { + shown = shown.pipe(function() { + if (self.dataset.index === null) { + // null index means we should start a new record + return self.on_button_new(); + } + var fields = _.keys(self.fields_view.fields); + fields.push('display_name'); + return self.dataset.read_index(fields, { + context: { 'bin_size': true, 'future_display_name' : true } + }).pipe(function(r) { + self.trigger('load_record', r); + }); + }); + } + return shown.pipe(function() { + self._actualize_mode(options.mode || self.options.initial_mode); + self.$el.css({ + opacity: '1', + filter: 'alpha(opacity = 100)' }); - return result; }); }, do_hide: function () { if (this.sidebar) { - this.sidebar.$element.hide(); + this.sidebar.$el.hide(); } if (this.$buttons) { this.$buttons.hide(); @@ -250,24 +308,22 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. } this._super(); }, - on_record_loaded: function(record) { + load_record: function(record) { var self = this, set_values = []; if (!record) { + this.set({ 'title' : undefined }); this.do_warn("Form", "The record could not be found in the database.", true); return $.Deferred().reject(); } this.datarecord = record; - - if (this.qweb) { - this.kill_current_form(); - this.rendering_engine.set_fields_view(this.get_fvg_from_qweb(record)); - var $dest = this.$element.hasClass("oe_form_container") ? this.$element : this.$element.find('.oe_form_container'); - this.rendering_engine.render_to($dest); - } + this._actualize_mode(); + this.set({ 'title' : record.id ? record.display_name : "New" }); _(this.fields).each(function (field, f) { field._dirty_flag = false; + field._inhibit_on_change_flag = true; var result = field.set_value(self.datarecord[f] || false); + field._inhibit_on_change_flag = false; set_values.push(result); }); return $.when.apply(null, set_values).pipe(function() { @@ -283,29 +339,44 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. }); } self.on_form_changed(); + self.rendering_engine.init_fields(); self.is_initialized.resolve(); self.do_update_pager(record.id == null); if (self.sidebar) { self.sidebar.do_attachement_update(self.dataset, self.datarecord.id); } - if (self.default_focus_field) { - self.default_focus_field.focus(); - } if (record.id) { self.do_push_state({id:record.id}); + } else { + self.do_push_state({}); } - self.$element.removeClass('oe_form_dirty'); - self.$buttons.find('.oe_form_button_save').removeClass('oe_form_button_save_dirty'); + self.$el.add(self.$buttons).removeClass('oe_form_dirty'); + self.autofocus(); }); }, + /** + * Loads and sets up the default values for the model as the current + * record + * + * @return {$.Deferred} + */ + load_defaults: function () { + var self = this; + var keys = _.keys(this.fields_view.fields); + if (keys.length) { + return this.dataset.default_get(keys).pipe(function(r) { + self.trigger('load_record', r); + }); + } + return self.trigger('load_record', {}); + }, on_form_changed: function() { this.trigger("view_content_has_changed"); }, do_notify_change: function() { - this.$element.addClass('oe_form_dirty'); - this.$buttons.find('.oe_form_button_save').addClass('oe_form_button_save_dirty'); + this.$el.add(this.$buttons).addClass('oe_form_dirty'); }, - on_pager_action: function(action) { + execute_pager_action: function(action) { if (this.can_be_discarded()) { switch (action) { case 'first': @@ -322,20 +393,41 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. break; } this.reload(); + this.trigger('pager_action_executed'); + } + }, + init_pager: function() { + var self = this; + if (this.$pager) + this.$pager.remove(); + if (this.get("actual_mode") === "create") + return; + this.$pager = $(QWeb.render("FormView.pager", {'widget':self})).hide(); + if (this.options.$pager) { + this.$pager.appendTo(this.options.$pager); + } else { + this.$el.find('.oe_form_pager').replaceWith(this.$pager); } + this.$pager.on('click','a[data-pager-action]',function() { + var action = $(this).data('pager-action'); + self.execute_pager_action(action); + }); + this.do_update_pager(); }, do_update_pager: function(hide_index) { - var index = hide_index ? '-' : this.dataset.index + 1; - this.$pager.find('button').prop('disabled', this.dataset.ids.length < 2).end() - .find('span.oe_pager_index').html(index).end() - .find('span.oe_pager_count').html(this.dataset.ids.length); + this.$pager.toggle(this.dataset.ids.length > 1); + if (hide_index) { + $(".oe_form_pager_state", this.$pager).html(""); + } else { + $(".oe_form_pager_state", this.$pager).html(_.str.sprintf(_t("%d / %d"), this.dataset.index + 1, this.dataset.ids.length)); + } }, parse_on_change: function (on_change, widget) { var self = this; var onchange = _.str.trim(on_change); var call = onchange.match(/^\s?(.*?)\((.*?)\)\s?$/); if (!call) { - return null; + throw new Error("Wrong on change format: " + onchange); } var method = call[1]; @@ -402,100 +494,74 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. var self = this; return this.on_change_mutex.exec(function() { try { - var response = {}, can_process_onchange = $.Deferred(); + var def; processed = processed || []; processed.push(widget.name); var on_change = widget.node.attrs.on_change; if (on_change) { var change_spec = self.parse_on_change(on_change, widget); - if (change_spec) { - var ajax = { - url: '/web/dataset/onchange', - async: false - }; - can_process_onchange = self.rpc(ajax, { - model: self.dataset.model, - method: change_spec.method, - args: [(self.datarecord.id == null ? [] : [self.datarecord.id])].concat(change_spec.args), - context_id: change_spec.context_index == undefined ? null : change_spec.context_index + 1 - }).then(function(r) { - _.extend(response, r); - }); - } else { - console.warn("Wrong on_change format", on_change); - } - } - // fail if onchange failed - if (can_process_onchange.isRejected()) { - return can_process_onchange; + def = self.rpc('/web/dataset/onchange', { + model: self.dataset.model, + method: change_spec.method, + args: [(self.datarecord.id == null ? [] : [self.datarecord.id])].concat(change_spec.args), + context_id: change_spec.context_index == undefined ? null : change_spec.context_index + 1 + }); + } else { + def = $.when({}); } + return def.pipe(function(response) { + if (widget.field['change_default']) { + var fieldname = widget.name + var value_; + if (response.value && (fieldname in response.value)) { + // Use value from onchange if onchange executed + value_ = response.value[fieldname]; + } else { + // otherwise get form value for field + value_ = self.fields[fieldname].get_value(); + } + var condition = fieldname + '=' + value_; - if (widget.field['change_default']) { - var fieldname = widget.name, value_; - if (response.value && (fieldname in response.value)) { - // Use value from onchange if onchange executed - value_ = response.value[fieldname]; - } else { - // otherwise get form value for field - value_ = self.fields[fieldname].get_value(); - } - var condition = fieldname + '=' + value_; - - if (value_) { - can_process_onchange = self.rpc({ - url: '/web/dataset/call', - async: false - }, { - model: 'ir.values', - method: 'get_defaults', - args: [self.model, condition] - }).then(function (results) { - if (!results.length) { return; } - if (!response.value) { - response.value = {}; - } - for(var i=0; i<results.length; ++i) { - // [whatever, key, value] - var triplet = results[i]; - response.value[triplet[1]] = triplet[2]; - } - }); + if (value_) { + return self.rpc('/web/dataset/call', { + model: 'ir.values', + method: 'get_defaults', + args: [self.model, condition] + }).pipe(function (results) { + if (!results.length) { + return response; + } + if (!response.value) { + response.value = {}; + } + for(var i=0; i<results.length; ++i) { + // [whatever, key, value] + var triplet = results[i]; + response.value[triplet[1]] = triplet[2]; + } + return response; + }); + } } - } - if (can_process_onchange.isRejected()) { - return can_process_onchange; - } - - return self.on_processed_onchange(response, processed); + return response; + }).pipe(function(response) { + return self.on_processed_onchange(response, processed); + }); } catch(e) { console.error(e); + instance.webclient.crashmanager.show_message(e); return $.Deferred().reject(); } }); }, - on_processed_onchange: function(response, processed) { + on_processed_onchange: function(result, processed) { try { - var result = response; if (result.value) { - for (var f in result.value) { - if (!result.value.hasOwnProperty(f)) { continue; } - var field = this.fields[f]; - // If field is not defined in the view, just ignore it - if (field) { - var value_ = result.value[f]; - if (field.get_value() != value_) { - field.set_value(value_); - field._dirty_flag = true; - if (!_.contains(processed, field.name)) { - this.do_onchange(field, processed); - } - } - } - } - this.on_form_changed(); + this._internal_set_values(result.value, processed); } if (!_.isEmpty(result.warning)) { - instance.web.dialog($(QWeb.render("CrashManagerWarning", result.warning)), { + instance.web.dialog($(QWeb.render("CrashManager.warning", result.warning)), { + title:result.warning.title, modal: true, buttons: [ {text: _t("Ok"), click: function() { $(this).dialog("close"); }} @@ -504,6 +570,9 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. } if (result.domain) { function edit_domain(node) { + if (typeof node !== "object") { + return; + } var new_domain = result.domain[node.attrs.name]; if (new_domain) { node.attrs.domain = new_domain; @@ -515,61 +584,131 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. return $.Deferred().resolve(); } catch(e) { console.error(e); + instance.webclient.crashmanager.show_message(e); return $.Deferred().reject(); } }, - switch_mode: function() { + _internal_set_values: function(values, exclude) { + exclude = exclude || []; + for (var f in values) { + if (!values.hasOwnProperty(f)) { continue; } + var field = this.fields[f]; + // If field is not defined in the view, just ignore it + if (field) { + var value_ = values[f]; + if (field.get_value() != value_) { + field._inhibit_on_change_flag = true; + field.set_value(value_); + field._inhibit_on_change_flag = false; + field._dirty_flag = true; + if (!_.contains(exclude, field.name)) { + this.do_onchange(field, exclude); + } + } + } + } + this.on_form_changed(); + }, + set_values: function(values) { + var self = this; + return this.on_change_mutex.exec(function() { + self._internal_set_values(values); + }); + }, + /** + * Ask the view to switch to view mode if possible. The view may not do it + * if the current record is not yet saved. It will then stay in create mode. + */ + to_view_mode: function() { + this._actualize_mode("view"); + }, + /** + * Ask the view to switch to edit mode if possible. The view may not do it + * if the current record is not yet saved. It will then stay in create mode. + */ + to_edit_mode: function() { + this._actualize_mode("edit"); + }, + /** + * Ask the view to switch to a precise mode if possible. The view is free to + * not respect this command if the state of the dataset is not compatible with + * the new mode. For example, it is not possible to switch to edit mode if + * the current record is not yet saved in database. + * + * @param {string} [new_mode] Can be "edit", "view", "create" or undefined. If + * undefined the view will test the actual mode to check if it is still consistent + * with the dataset state. + */ + _actualize_mode: function(switch_to) { + var mode = switch_to || this.get("actual_mode"); + if (! this.datarecord.id) { + mode = "create"; + } else if (mode === "create") { + mode = "edit"; + } + this.set({actual_mode: mode}); + }, + check_actual_mode: function(source, options) { var self = this; - if(this.get("mode") == "view") { + if(this.get("actual_mode") === "view") { + self.$el.removeClass('oe_form_editable').addClass('oe_form_readonly'); self.$buttons.find('.oe_form_buttons_edit').hide(); self.$buttons.find('.oe_form_buttons_view').show(); self.$sidebar.show(); - _.each(this.fields,function(field){ - field.set({"force_readonly": true}); - }); } else { + self.$el.removeClass('oe_form_readonly').addClass('oe_form_editable'); self.$buttons.find('.oe_form_buttons_edit').show(); self.$buttons.find('.oe_form_buttons_view').hide(); self.$sidebar.hide(); - _.each(this.fields,function(field){ - field.set({"force_readonly": false}); - }); + this.autofocus(); + } + }, + autofocus: function() { + if (this.get("actual_mode") !== "view" && !this.options.disable_autofocus) { + var fields_order = this.fields_order.slice(0); + if (this.default_focus_field) { + fields_order.unshift(this.default_focus_field.name); + } + for (var i = 0; i < fields_order.length; i += 1) { + var field = this.fields[fields_order[i]]; + if (!field.get('effective_invisible') && !field.get('effective_readonly') && field.$label) { + if (field.focus() !== false) { + break; + } + } + } } }, on_button_save: function() { var self = this; - return this.do_save().then(function(result) { - self.set({mode: "view"}); + return this.save().then(function(result) { + self.trigger("save", result); + self.to_view_mode(); }); }, on_button_cancel: function(event) { if (this.can_be_discarded()) { - this.set({mode: "view"}); + if (this.get('actual_mode') === 'create') { + this.trigger('history_back'); + } else { + this.to_view_mode(); + this.trigger('load_record', this.datarecord); + } } + this.trigger('on_button_cancel'); return false; }, on_button_new: function() { var self = this; - this.set({mode: "edit"}); - var def = $.Deferred(); - $.when(this.has_been_loaded).then(function() { + this.to_edit_mode(); + return $.when(this.has_been_loaded).pipe(function() { if (self.can_be_discarded()) { - var keys = _.keys(self.fields_view.fields); - if (keys.length) { - self.dataset.default_get(keys).pipe(self.on_record_loaded).then(function() { - def.resolve(); - }); - } else { - self.on_record_loaded({}).then(function() { - def.resolve(); - }); - } + return self.load_defaults(); } }); - return def.promise(); }, on_button_edit: function() { - return this.set({mode: "edit"}); + return this.to_edit_mode(); }, on_button_create: function() { this.dataset.index = null; @@ -580,9 +719,9 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. var def = $.Deferred(); $.when(this.has_been_loaded).then(function() { self.dataset.call('copy', [self.datarecord.id, {}, self.dataset.context]).then(function(new_id) { - return self.on_created({ result : new_id }); + return self.record_created(new_id); }).then(function() { - return self.set({mode: "edit"}); + return self.to_edit_mode(); }).then(function() { def.resolve(); }); @@ -595,7 +734,7 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. $.when(this.has_been_loaded).then(function() { if (self.datarecord.id && confirm(_t("Do you really want to delete this record?"))) { self.dataset.unlink([self.datarecord.id]).then(function() { - self.on_pager_action('next'); + self.execute_pager_action('next'); def.resolve(); }); } else { @@ -607,17 +746,24 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. return def.promise(); }, can_be_discarded: function() { - return !this.$element.is('.oe_form_dirty') || confirm(_t("Warning, the record has been modified, your changes will be discarded.")); + if (this.$el.is('.oe_form_dirty')) { + if (!confirm(_t("Warning, the record has been modified, your changes will be discarded.\n\nAre you sure you want to leave this page ?"))) { + return false; + } + this.$el.removeClass('oe_form_dirty'); + } + return true; }, /** * Triggers saving the form's record. Chooses between creating a new * record or saving an existing one depending on whether the record * already has an id property. * - * @param {Function} success callback on save success - * @param {Boolean} [prepend_on_create=false] if ``do_save`` creates a new record, should that record be inserted at the start of the dataset (by default, records are added at the end) + * @param {Boolean} [prepend_on_create=false] if ``save`` creates a new + * record, should that record be inserted at the start of the dataset (by + * default, records are added at the end) */ - do_save: function(success, prepend_on_create) { + save: function(prepend_on_create) { var self = this; return this.mutating_mutex.exec(function() { return self.is_initialized.pipe(function() { try { @@ -625,6 +771,7 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. values = {}, first_invalid_field = null; for (var f in self.fields) { + if (!self.fields.hasOwnProperty(f)) { continue; } f = self.fields[f]; if (!f.is_valid()) { form_invalid = true; @@ -647,21 +794,21 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. self.set({'display_invalid_fields': false}); var save_deferral; if (!self.datarecord.id) { - //console.log("FormView(", self, ") : About to create", values); + // Creation save save_deferral = self.dataset.create(values).pipe(function(r) { - return self.on_created(r, undefined, prepend_on_create); + return self.record_created(r, prepend_on_create); }, null); } else if (_.isEmpty(values) && ! self.force_dirty) { - //console.log("FormView(", self, ") : Nothing to save"); + // Not dirty, noop save save_deferral = $.Deferred().resolve({}).promise(); } else { self.force_dirty = false; - //console.log("FormView(", self, ") : About to save", values); + // Write save save_deferral = self.dataset.write(self.datarecord.id, values, {}).pipe(function(r) { - return self.on_saved(r); + return self.record_saved(r); }, null); } - return save_deferral.then(success); + return save_deferral; } } catch (e) { console.error(e); @@ -670,22 +817,32 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. });}); }, on_invalid: function() { - var msg = "<ul>"; - _.each(this.fields, function(f) { - if (!f.is_valid()) { - msg += "<li>" + f.node.attrs.string + "</li>"; - } - }); - msg += "</ul>"; - this.do_warn("The following fields are invalid :", msg); + var warnings = _(this.fields).chain() + .filter(function (f) { return !f.is_valid(); }) + .map(function (f) { + return _.str.sprintf('<li>%s</li>', + _.escape(f.string)); + }).value(); + warnings.unshift('<ul>'); + warnings.push('</ul>'); + this.do_warn("The following fields are invalid :", warnings.join('')); }, - on_saved: function(r, success) { - if (!r.result) { + /** + * Reload the form after saving + * + * @param {Object} r result of the write function. + */ + record_saved: function(r) { + var self = this; + if (!r) { // should not happen in the server, but may happen for internal purpose + this.trigger('record_saved', r); return $.Deferred().reject(); } else { return $.when(this.reload()).pipe(function () { - return $.when(r).then(success); }, null); + self.trigger('record_saved', r); + return r; + }); } }, /** @@ -698,15 +855,17 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. * * Updates the pager and sidebar displays * * @param {Object} r - * @param {Function} success callback to execute after having updated the dataset - * @param {Boolean} [prepend_on_create=false] adds the newly created record at the beginning of the dataset instead of the end + * @param {Boolean} [prepend_on_create=false] adds the newly created record + * at the beginning of the dataset instead of the end */ - on_created: function(r, success, prepend_on_create) { - if (!r.result) { + record_created: function(r, prepend_on_create) { + var self = this; + if (!r) { // should not happen in the server, but may happen for internal purpose + this.trigger('record_created', r); return $.Deferred().reject(); } else { - this.datarecord.id = r.result; + this.datarecord.id = r; if (!prepend_on_create) { this.dataset.alter_ids(this.dataset.ids.concat([this.datarecord.id])); this.dataset.index = this.dataset.ids.length - 1; @@ -717,10 +876,12 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. this.do_update_pager(); if (this.sidebar) { this.sidebar.do_attachement_update(this.dataset, this.datarecord.id); - } - //instance.log("The record has been created with id #" + this.datarecord.id); - this.reload(); - return $.when(_.extend(r, {created: true})).then(success); + } + //openerp.log("The record has been created with id #" + this.datarecord.id); + return $.when(this.reload()).pipe(function () { + self.trigger('record_created', r); + return _.extend(r, {created: true}); + }); } }, on_action: function (action) { @@ -730,15 +891,23 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. var self = this; return this.reload_mutex.exec(function() { if (self.dataset.index == null) { - self.do_prev_view(); + self.trigger("previous_view"); return $.Deferred().reject().promise(); } if (self.dataset.index == null || self.dataset.index < 0) { return $.when(self.on_button_new()); } else { - return self.dataset.read_index(_.keys(self.fields_view.fields), { - context : { 'bin_size' : true } - }).pipe(self.on_record_loaded); + var fields = _.keys(self.fields_view.fields); + fields.push('display_name'); + return self.dataset.read_index(fields, + { + context: { + 'bin_size': true, + 'future_display_name': true + } + }).pipe(function(r) { + self.trigger('load_record', r); + }); } }); }, @@ -748,15 +917,15 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. }); }, get_fields_values: function(blacklist) { - blacklist = blacklist || []; + blacklist = blacklist || []; var values = {}; var ids = this.get_selected_ids(); values["id"] = ids.length > 0 ? ids[0] : false; _.each(this.fields, function(value_, key) { - if (_.include(blacklist, key)) - return; - var val = value_.get_value(); - values[key] = val; + if (_.include(blacklist, key)) { + return; + } + values[key] = value_.get_value(); }); return values; }, @@ -766,11 +935,20 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. }, recursive_save: function() { var self = this; - return $.when(this.do_save()).pipe(function(res) { + return $.when(this.save()).pipe(function(res) { if (self.dataset.parent_view) return self.dataset.parent_view.recursive_save(); }); }, + recursive_reload: function() { + var self = this; + var pre = $.when(); + if (self.dataset.parent_view) + pre = self.dataset.parent_view.recursive_reload(); + return pre.pipe(function() { + return self.reload(); + }); + }, is_dirty: function() { return _.any(this.fields, function (value_) { return value_._dirty_flag; @@ -788,10 +966,20 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. return true; }, sidebar_context: function () { - return this.do_save().pipe(_.bind(function() {return this.get_fields_values();}, this)); + return this.save().pipe(_.bind(function() {return this.get_fields_values();}, this)); }, open_defaults_dialog: function () { var self = this; + var display = function (field, value) { + if (field instanceof instance.web.form.FieldSelection) { + return _(field.values).find(function (option) { + return option[0] === value; + })[1]; + } else if (field instanceof instance.web.form.FieldMany2One) { + return field.get_displayed(); + } + return value; + } var fields = _.chain(this.fields) .map(function (field, name) { var value = field.get_value(); @@ -801,32 +989,32 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. || field.get('invisible') || field.get("readonly") || field.field.type === 'one2many' - || field.field.type === 'many2many') { + || field.field.type === 'many2many' + || field.field.type === 'binary') { return false; } - var displayed = value; - switch (field.field.type) { - case 'selection': - displayed = _(field.values).find(function (option) { - return option[0] === value; - })[1]; - break; - } return { name: name, - string: field.node.attrs.string || field.field.string, + string: field.string, value: value, - displayed: displayed, - // convert undefined to false - change_default: !!field.field.change_default + displayed: display(field, value), } }) .compact() .sortBy(function (field) { return field.string; }) .value(); - var conditions = _.chain(fields) - .filter(function (field) { return field.change_default; }) + var conditions = _.chain(self.fields) + .filter(function (field) { return field.field.change_default; }) + .map(function (field, name) { + var value = field.get_value(); + return { + name: name, + string: field.string, + value: value, + displayed: display(field, value), + } + }) .value(); var d = new instance.web.Dialog(this, { @@ -838,21 +1026,21 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. buttons: [ {text: _t("Close"), click: function () { d.close(); }}, {text: _t("Save default"), click: function () { - var $defaults = d.$element.find('#formview_default_fields'); + var $defaults = d.$el.find('#formview_default_fields'); var field_to_set = $defaults.val(); if (!field_to_set) { $defaults.parent().addClass('oe_form_invalid'); return; } - var condition = d.$element.find('#formview_default_conditions').val(), - all_users = d.$element.find('#formview_default_all').is(':checked'); + var condition = d.$el.find('#formview_default_conditions').val(), + all_users = d.$el.find('#formview_default_all').is(':checked'); new instance.web.DataSet(self, 'ir.values').call( 'set_default', [ self.dataset.model, field_to_set, self.fields[field_to_set].get_value(), all_users, - false, + true, condition || false ]).then(function () { d.close(); }); }} @@ -864,10 +1052,22 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. register_field: function(field, name) { this.fields[name] = field; this.fields_order.push(name); - if (this.get_field(name).translate) { + if (JSON.parse(field.node.attrs.default_focus || "0")) { + this.default_focus_field = field; + } + + field.on('focused', null, this.proxy('widgetFocused')) + .on('blurred', null, this.proxy('widgetBlurred')); + if (this.get_field_desc(name).translate) { this.translatable_fields.push(field); } field.on('changed_value', this, function() { + if (field.is_syntax_valid()) { + this.trigger('field_changed:' + name); + } + if (field._inhibit_on_change_flag) { + return; + } field._dirty_flag = true; if (field.is_syntax_valid()) { this.do_onchange(field); @@ -876,13 +1076,35 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. } }); }, - get_field: function(field_name) { + get_field_desc: function(field_name) { return this.fields_view.fields[field_name]; }, - is_create_mode: function() { - return !this.datarecord.id; + get_field_value: function(field_name) { + return this.fields[field_name].get_value(); }, -})); + compute_domain: function(expression) { + return instance.web.form.compute_domain(expression, this.fields); + }, + _build_view_fields_values: function(blacklist) { + var a_dataset = this.dataset; + var fields_values = this.get_fields_values(blacklist); + var active_id = a_dataset.ids[a_dataset.index]; + _.extend(fields_values, { + active_id: active_id || false, + active_ids: active_id ? [active_id] : [], + active_model: a_dataset.model, + parent: {} + }); + if (a_dataset.parent_view) { + fields_values.parent = a_dataset.parent_view.get_fields_values([a_dataset.child_name]); + } + return fields_values; + }, + build_eval_context: function(blacklist) { + var a_dataset = this.dataset; + return new instance.web.CompoundContext(a_dataset.get_context(), this._build_view_fields_values(blacklist)); + }, +}); /** * Interface to be implemented by rendering engines for the form view. @@ -890,12 +1112,12 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. instance.web.form.FormRenderingEngineInterface = instance.web.Class.extend({ set_fields_view: function(fields_view) {}, set_fields_registry: function(fields_registry) {}, - render_to: function($element) {}, + render_to: function($el) {}, }); /** * Default rendering engine for the form view. - * + * * It is necessary to set the view using set_view() before usage. */ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInterface.extend({ @@ -904,6 +1126,10 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt }, set_fields_view: function(fvg) { this.fvg = fvg; + this.version = parseFloat(this.fvg.arch.attrs.version); + if (isNaN(this.version)) { + this.version = 6.1; + } }, set_tags_registry: function(tags_registry) { this.tags_registry = tags_registry; @@ -911,6 +1137,20 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt set_fields_registry: function(fields_registry) { this.fields_registry = fields_registry; }, + set_widgets_registry: function(widgets_registry) { + this.widgets_registry = widgets_registry; + }, + // Backward compatibility tools, current default version: v6.1 + process_version: function() { + if (this.version < 7.0) { + this.$form.find('form:first').wrapInner('<group col="4"/>'); + this.$form.find('page').each(function() { + if (!$(this).parents('field').length) { + $(this).wrapInner('<group col="4"/>'); + } + }); + } + }, render_to: function($target) { var self = this; this.$target = $target; @@ -919,17 +1159,19 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt // but one day, we will have to get rid of xml2json var xml = instance.web.json_node_to_xml(this.fvg.arch); this.$form = $('<div class="oe_form">' + xml + '</div>'); - if (this.fvg.arch.attrs && this.fvg.arch.attrs['layout'] !== 'manual') { - this.$form.attr('layout', 'auto'); - } + + this.process_version(); this.fields_to_init = []; this.tags_to_init = []; + this.widgets_to_init = []; this.labels = {}; this.process(this.$form); this.$form.appendTo(this.$target); + this.to_replace = []; + _.each(this.fields_to_init, function($elem) { var name = $elem.attr("name"); if (!self.fvg.fields[name]) { @@ -937,7 +1179,7 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt } var obj = self.fields_registry.get_any([$elem.attr('widget'), self.fvg.fields[name].type]); if (!obj) { - throw new Error("Widget type '"+ key + "' is not implemented"); + throw new Error("Widget type '"+ $elem.attr('widget') + "' is not implemented"); } var w = new (obj)(self.view, instance.web.xml_to_json($elem[0])); var $label = self.labels[$elem.attr("name")]; @@ -946,23 +1188,33 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt } self.alter_field(w); self.view.register_field(w, $elem.attr("name")); - w.replace($elem); + self.to_replace.push([w, $elem]); }); _.each(this.tags_to_init, function($elem) { var tag_name = $elem[0].tagName.toLowerCase(); var obj = self.tags_registry.get_object(tag_name); var w = new (obj)(self.view, instance.web.xml_to_json($elem[0])); - w.replace($elem); - }) - // TODO: return a deferred + self.to_replace.push([w, $elem]); + }); + _.each(this.widgets_to_init, function($elem) { + var widget_type = $elem.attr("type"); + var obj = self.widgets_registry.get_object(widget_type); + var w = new (obj)(self.view, instance.web.xml_to_json($elem[0])); + self.to_replace.push([w, $elem]); + }); + }, + init_fields: function() { + var defs = []; + _.each(this.to_replace, function(el) { + defs.push(el[0].replace(el[1])); + }); + this.to_replace = []; + return $.when.apply($, defs); }, - render_element: function(template, layout/* dictionaries */) { - var dicts = [].slice.call(arguments).slice(2); - dicts.unshift({ 'layout' : layout }); + render_element: function(template /* dictionaries */) { + var dicts = [].slice.call(arguments).slice(1); var dict = _.extend.apply(_, dicts); dict['classnames'] = dict['class'] || ''; // class is a reserved word and might caused problem to Safari when used from QWeb - var alt_template = template + '.' + layout; - template = QWeb.has_template(alt_template) ? alt_template : template; return $(QWeb.render(template, dict)); }, alter_field: function(field) { @@ -977,10 +1229,8 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt } this.$target.toggleClass('oe_layout_debugging'); }, - process: function($tag, layout) { + process: function($tag) { var self = this; - layout = $tag.attr('layout') || layout || 'auto'; - $tag.removeAttr('layout'); var tagname = $tag[0].nodeName.toLowerCase(); if (this.tags_registry.contains(tagname)) { this.tags_to_init.push($tag); @@ -990,39 +1240,49 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt if (fn) { var args = [].slice.call(arguments); args[0] = $tag; - args[1] = layout; return fn.apply(self, args); } else { // generic tag handling, just process children $tag.children().each(function() { - self.process($(this), layout); + self.process($(this)); }); self.handle_common_properties($tag, $tag); $tag.removeAttr("modifiers"); return $tag; } }, - process_sheet: function($sheet, layout) { - var $new_sheet = this.render_element('FormRenderingSheet', layout, $sheet.getAttributes()); + process_widget: function($widget) { + this.widgets_to_init.push($widget); + return $widget; + }, + process_sheet: function($sheet) { + var $new_sheet = this.render_element('FormRenderingSheet', $sheet.getAttributes()); this.handle_common_properties($new_sheet, $sheet); - var $dst = (layout === 'auto') ? $new_sheet.find('group:first') : $new_sheet.find('.oe_form_sheet'); - $sheet.children().appendTo($dst); + var $dst = $new_sheet.find('.oe_form_sheet'); + $sheet.contents().appendTo($dst); $sheet.before($new_sheet).remove(); - this.process($new_sheet, layout); + this.process($new_sheet); }, - process_form: function($form, layout) { - var $new_form = this.render_element('FormRenderingForm', layout, $form.getAttributes()); + process_form: function($form) { + if ($form.find('> sheet').length === 0) { + $form.addClass('oe_form_nosheet'); + } + var $new_form = this.render_element('FormRenderingForm', $form.getAttributes()); this.handle_common_properties($new_form, $form); - var $dst = (layout === 'auto') ? $new_form.find('group:first') : $new_form; - $form.children().appendTo($dst); + $form.contents().appendTo($new_form); if ($form[0] === this.$form[0]) { // If root element, replace it this.$form = $new_form; } else { $form.before($new_form).remove(); } - this.process($new_form, layout); + this.process($new_form); }, + /* + * Used by direct <field> children of a <group> tag only + * This method will add the implicit <label...> for every field + * in the <group> + */ preprocess_field: function($field) { var self = this; var name = $field.attr('name'), @@ -1043,11 +1303,12 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt if (found) return; - $label = $('<label/>').attr({ + var $label = $('<label/>').attr({ 'for' : name, "modifiers": JSON.stringify({invisible: field_modifiers.invisible}), "string": $field.attr('string'), "help": $field.attr('help'), + "class": $field.attr('class'), }); $label.insertBefore($field); if (field_colspan > 1) { @@ -1055,31 +1316,34 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt } return $label; }, - process_field: function($field, layout) { - var $label = this.preprocess_field($field); - if ($label) - this.process($label, layout); + process_field: function($field) { + if ($field.parent().is('group')) { + // No implicit labels for normal fields, only for <group> direct children + var $label = this.preprocess_field($field); + if ($label) { + this.process($label); + } + } this.fields_to_init.push($field); return $field; }, - process_group: function($group, layout) { + process_group: function($group) { var self = this; - if ($group.parent().is('.oe_form_group_cell')) { - $group.parent().addClass('oe_form_group_nested'); - } $group.children('field').each(function() { self.preprocess_field($(this)); }); - var $new_group = this.render_element('FormRenderingGroup', layout, $group.getAttributes()), - $table; - if ($new_group.is('table')) { + var $new_group = this.render_element('FormRenderingGroup', $group.getAttributes()); + var $table; + if ($new_group.first().is('table.oe_form_group')) { $table = $new_group; + } else if ($new_group.filter('table.oe_form_group').length) { + $table = $new_group.filter('table.oe_form_group').first(); } else { - $table = $new_group.find('table:first'); + $table = $new_group.find('table.oe_form_group').first(); } - $table.addClass('oe_form_group'); + var $tr, $td, - cols = parseInt($group.attr('col') || 4, 10), + cols = parseInt($group.attr('col') || 2, 10), row_cols = cols; var children = []; @@ -1089,6 +1353,8 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt var tagName = $child[0].tagName.toLowerCase(); var $td = $('<td/>').addClass('oe_form_group_cell').attr('colspan', colspan); var newline = tagName === 'newline'; + + // Note FME: those classes are used in layout debug mode if ($tr && row_cols > 0 && (newline || row_cols < colspan)) { $tr.addClass('oe_form_group_row_incomplete'); if (newline) { @@ -1102,6 +1368,9 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt if (!$tr || row_cols < colspan) { $tr = $('<tr/>').addClass('oe_form_group_row').appendTo($table); row_cols = cols; + } else if (tagName==='group') { + // When <group> <group/><group/> </group>, we need a spacing between the two groups + $td.addClass('oe_group_right') } row_cols -= colspan; @@ -1118,7 +1387,6 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt } $group.before($new_group).remove(); - // Now compute width of cells $table.find('> tbody > tr').each(function() { var to_compute = [], row_cols = cols, @@ -1128,16 +1396,11 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt $child = $td.children(':first'); switch ($child[0].tagName.toLowerCase()) { case 'separator': - if ($child.attr('orientation') === 'vertical') { - $td.addClass('oe_vertical_separator').attr('width', '1'); - $td.empty(); - row_cols--; - } break; case 'label': if ($child.attr('for')) { $td.attr('width', '1%').addClass('oe_form_group_cell_label'); - row_cols--; + row_cols-= $td.attr('colspan') || 1; total--; } break; @@ -1154,20 +1417,22 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt } $td.attr('width', width); $child.removeAttr('width'); - row_cols--; + row_cols-= $td.attr('colspan') || 1; } else { to_compute.push($td); } } }); - var unit = Math.floor(total / row_cols); - if (!$(this).is('.oe_form_group_row_incomplete')) { - _.each(to_compute, function($td, i) { - var width = parseInt($td.attr('colspan'), 10) * unit; - $td.attr('width', ((i == to_compute.length - 1) ? total : width) + '%'); - total -= width; - }); + if (row_cols) { + var unit = Math.floor(total / row_cols); + if (!$(this).is('.oe_form_group_row_incomplete')) { + _.each(to_compute, function($td, i) { + var width = parseInt($td.attr('colspan'), 10) * unit; + $td.attr('width', width + '%'); + total -= width; + }); + } } }); _.each(children, function(el) { @@ -1176,37 +1441,61 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt this.handle_common_properties($new_group, $group); return $new_group; }, - process_notebook: function($notebook, layout) { + process_notebook: function($notebook) { var self = this; var pages = []; $notebook.find('> page').each(function() { var $page = $(this); var page_attrs = $page.getAttributes(); page_attrs.id = _.uniqueId('notebook_page_'); - pages.push(page_attrs); - var $new_page = self.render_element('FormRenderingNotebookPage', layout, page_attrs); - var $dst = (layout === 'auto') ? $new_page.find('group:first') : $new_page; - $page.children().appendTo($dst); + var $new_page = self.render_element('FormRenderingNotebookPage', page_attrs); + $page.contents().appendTo($new_page); $page.before($new_page).remove(); - self.handle_common_properties($new_page, $page); + var ic = self.handle_common_properties($new_page, $page).invisibility_changer; + page_attrs.__page = $new_page; + page_attrs.__ic = ic; + pages.push(page_attrs); + + $new_page.children().each(function() { + self.process($(this)); + }); }); - var $new_notebook = this.render_element('FormRenderingNotebook', layout, { pages : pages }); - $notebook.children().appendTo($new_notebook); + var $new_notebook = this.render_element('FormRenderingNotebook', { pages : pages }); + $notebook.contents().appendTo($new_notebook); $notebook.before($new_notebook).remove(); - $new_notebook.children().each(function() { - self.process($(this)); - }); + self.process($($new_notebook.children()[0])); + //tabs and invisibility handling $new_notebook.tabs(); + _.each(pages, function(page, i) { + if (! page.__ic) + return; + page.__ic.on("change:effective_invisible", null, function() { + if (!page.__ic.get('effective_invisible')) { + $new_notebook.tabs('select', i); + return; + } + var current = $new_notebook.tabs("option", "selected"); + if (! pages[current].__ic || ! pages[current].__ic.get("effective_invisible")) + return; + var first_visible = _.find(_.range(pages.length), function(i2) { + return (! pages[i2].__ic) || (! pages[i2].__ic.get("effective_invisible")); + }); + if (first_visible !== undefined) { + $new_notebook.tabs('select', first_visible); + } + }); + }); + this.handle_common_properties($new_notebook, $notebook); return $new_notebook; }, - process_separator: function($separator, layout) { - var $new_separator = this.render_element('FormRenderingSeparator', layout, $separator.getAttributes()); + process_separator: function($separator) { + var $new_separator = this.render_element('FormRenderingSeparator', $separator.getAttributes()); $separator.before($new_separator).remove(); this.handle_common_properties($new_separator, $separator); return $new_separator; }, - process_label: function($label, layout) { + process_label: function($label) { var name = $label.attr("for"), field_orm = this.fvg.fields[name]; var dict = { @@ -1223,7 +1512,7 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt align = 'center'; } dict.align = align; - var $new_label = this.render_element('FormRenderingLabel', layout, dict); + var $new_label = this.render_element('FormRenderingLabel', dict); $label.before($new_label).remove(); this.handle_common_properties($new_label, $label); if (name) { @@ -1232,52 +1521,66 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt return $new_label; }, handle_common_properties: function($new_element, $node) { - var str_modifiers = $node.attr("modifiers") || "{}" + var str_modifiers = $node.attr("modifiers") || "{}"; var modifiers = JSON.parse(str_modifiers); + var ic = null; if (modifiers.invisible !== undefined) - new instance.web.form.InvisibilityChanger(this.view, this.view, modifiers.invisible, $new_element); + ic = new instance.web.form.InvisibilityChanger(this.view, this.view, modifiers.invisible, $new_element); $new_element.addClass($node.attr("class") || ""); $new_element.attr('style', $node.attr('style')); + return {invisibility_changer: ic,}; }, }); -instance.web.form.FormRenderingEngineReadonly = instance.web.form.FormRenderingEngine.extend({ - alter_field: function(field) { - field.set({"force_readonly": true}); - }, -}); +/** + Welcome. -instance.web.form.FormDialog = instance.web.Dialog.extend({ - init: function(parent, options, view_id, dataset) { - this._super(parent, options); - this.dataset = dataset; - this.view_id = view_id; - return this; - }, - start: function() { - this._super(); - this.form = new instance.web.FormView(this, this.dataset, this.view_id, { - pager: false + If you read this documentation, it probably means that you were asked to use a form view widget outside of + a form view. Before going further, you must understand that those fields were never really created for + that usage. Don't think that this class will hold the answer to all your problems, at best it will allow + you to hack the system with more style. +*/ +instance.web.form.DefaultFieldManager = instance.web.Widget.extend({ + init: function(parent, eval_context) { + this._super(parent); + this.field_descs = {}; + this.eval_context = eval_context || {}; + this.set({ + display_invalid_fields: false, + actual_mode: 'create', }); - this.form.appendTo(this.$element); - this.form.on_created.add_last(this.on_form_dialog_saved); - this.form.on_saved.add_last(this.on_form_dialog_saved); - return this; }, - select_id: function(id) { - if (this.form.dataset.select_id(id)) { - return this.form.do_show(); - } else { - this.do_warn("Could not find id in dataset"); - return $.Deferred().reject(); + get_field_desc: function(field_name) { + if (this.field_descs[field_name] === undefined) { + this.field_descs[field_name] = { + string: field_name, + }; } + return this.field_descs[field_name]; + }, + extend_field_desc: function(fields) { + var self = this; + _.each(fields, function(v, k) { + _.extend(self.get_field_desc(k), v); + }); + }, + get_field_value: function(field_name) { + return false; + }, + set_values: function(values) { + // nothing + }, + compute_domain: function(expression) { + return instance.web.form.compute_domain(expression, {}); + }, + build_eval_context: function() { + return new instance.web.CompoundContext(this.eval_context); }, - on_form_dialog_saved: function(r) { - this.close(); - } }); instance.web.form.compute_domain = function(expr, fields) { + if (! (expr instanceof Array)) + return !! expr; var stack = []; for (var i = expr.length - 1; i >= 0; i--) { var ex = expr[i]; @@ -1351,83 +1654,118 @@ instance.web.form.compute_domain = function(expr, fields) { /** * Must be applied over an class already possessing the PropertiesMixin. * - * Apply the result of the "invisible" domain to this.$element. + * Apply the result of the "invisible" domain to this.$el. */ instance.web.form.InvisibilityChangerMixin = { init: function(field_manager, invisible_domain) { - this._ic_field_manager = field_manager + var self = this; + this._ic_field_manager = field_manager; this._ic_invisible_modifier = invisible_domain; this._ic_field_manager.on("view_content_has_changed", this, function() { - var result = this._ic_invisible_modifier === undefined ? false : - instance.web.form.compute_domain(this._ic_invisible_modifier, this._ic_field_manager.fields); - this.set({"invisible": result}); + var result = self._ic_invisible_modifier === undefined ? false : + self._ic_field_manager.compute_domain(self._ic_invisible_modifier); + self.set({"invisible": result}); }); this.set({invisible: this._ic_invisible_modifier === true, force_invisible: false}); var check = function() { - if (this.get("invisible") || this.get('force_invisible')) { - this.set({"effective_invisible": true}); + if (self.get("invisible") || self.get('force_invisible')) { + self.set({"effective_invisible": true}); } else { - this.set({"effective_invisible": false}); + self.set({"effective_invisible": false}); } }; this.on('change:invisible', this, check); this.on('change:force_invisible', this, check); - _.bind(check, this)(); + check.call(this); }, start: function() { - var check_visibility = function() { - if (this.get("effective_invisible")) { - this.$element.hide(); - } else { - this.$element.show(); - } - }; - this.on("change:effective_invisible", this, check_visibility); - _.bind(check_visibility, this)(); + this.on("change:effective_invisible", this, this._check_visibility); + this._check_visibility(); + }, + _check_visibility: function() { + this.$el.toggleClass('oe_form_invisible', this.get("effective_invisible")); }, }; -instance.web.form.InvisibilityChanger = instance.web.Class.extend(_.extend({}, instance.web.PropertiesMixin, instance.web.form.InvisibilityChangerMixin, { - init: function(parent, field_manager, invisible_domain, $element) { +instance.web.form.InvisibilityChanger = instance.web.Class.extend(instance.web.PropertiesMixin, instance.web.form.InvisibilityChangerMixin, { + init: function(parent, field_manager, invisible_domain, $el) { this.setParent(parent); instance.web.PropertiesMixin.init.call(this); instance.web.form.InvisibilityChangerMixin.init.call(this, field_manager, invisible_domain); - this.$element = $element; + this.$el = $el; this.start(); }, -})); +}); + +/** + Base class for all fields, custom widgets and buttons to be displayed in the form view. -instance.web.form.FormWidget = instance.web.Widget.extend(_.extend({}, instance.web.form.InvisibilityChangerMixin, { + Properties: + - effective_readonly: when it is true, the widget is displayed as readonly. Vary depending + the values of the "readonly" property and the "mode" property on the field manager. +*/ +instance.web.form.FormWidget = instance.web.Widget.extend(instance.web.form.InvisibilityChangerMixin, { /** * @constructs instance.web.form.FormWidget * @extends instance.web.Widget * - * @param view + * @param field_manager * @param node */ - init: function(view, node) { - this._super(view); - this.view = view; + init: function(field_manager, node) { + this._super(field_manager); + this.field_manager = field_manager; + if (this.field_manager instanceof instance.web.FormView) + this.view = this.field_manager; this.node = node; this.modifiers = JSON.parse(this.node.attrs.modifiers || '{}'); - instance.web.form.InvisibilityChangerMixin.init.call(this, view, this.modifiers.invisible); + instance.web.form.InvisibilityChangerMixin.init.call(this, this.field_manager, this.modifiers.invisible); - this.view.on("view_content_has_changed", this, this.process_modifiers); + this.field_manager.on("view_content_has_changed", this, this.process_modifiers); + + this.set({ + required: false, + readonly: false, + }); + // some events to make the property "effective_readonly" sync automatically with "readonly" and + // "mode" on field_manager + var self = this; + var test_effective_readonly = function() { + self.set({"effective_readonly": self.get("readonly") || self.field_manager.get("actual_mode") === "view"}); + }; + this.on("change:readonly", this, test_effective_readonly); + this.field_manager.on("change:actual_mode", this, test_effective_readonly); + test_effective_readonly.call(this); }, renderElement: function() { + this.process_modifiers(); this._super(); - this.$element.addClass(this.node.attrs["class"] || ""); + this.$el.addClass(this.node.attrs["class"] || ""); }, destroy: function() { $.fn.tipsy.clear(); this._super.apply(this, arguments); }, + /** + * Sets up blur/focus forwarding from DOM elements to a widget (`this`). + * + * This method is an utility method that is meant to be called by child classes. + * + * @param {jQuery} $e jQuery object of elements to bind focus/blur on + */ + setupFocus: function ($e) { + var self = this; + $e.on({ + focus: function () { self.trigger('focused'); }, + blur: function () { self.trigger('blurred'); } + }); + }, process_modifiers: function() { - var compute_domain = instance.web.form.compute_domain; var to_set = {}; for (var a in this.modifiers) { + if (!this.modifiers.hasOwnProperty(a)) { continue; } if (!_.include(["invisible"], a)) { - var val = compute_domain(this.modifiers[a], this.view.fields); + var val = this.field_manager.compute_domain(this.modifiers[a]); to_set[a] = val; } } @@ -1435,7 +1773,7 @@ instance.web.form.FormWidget = instance.web.Widget.extend(_.extend({}, instance. }, do_attach_tooltip: function(widget, trigger, options) { widget = widget || this; - trigger = trigger || this.$element; + trigger = trigger || this.$el; options = _.extend({ delayIn: 500, delayOut: 0, @@ -1446,7 +1784,7 @@ instance.web.form.FormWidget = instance.web.Widget.extend(_.extend({}, instance. template = 'WidgetLabel.tooltip'; } return QWeb.render(template, { - debug: instance.connection.debug, + debug: instance.session.debug, widget: widget })}, gravity: $.fn.tipsy.autoBounds(50, 'nw'), @@ -1456,25 +1794,6 @@ instance.web.form.FormWidget = instance.web.Widget.extend(_.extend({}, instance. }, options || {}); $(trigger).tipsy(options); }, - _build_view_fields_values: function(blacklist) { - var a_dataset = this.view.dataset; - var fields_values = this.view.get_fields_values(blacklist); - var active_id = a_dataset.ids[a_dataset.index]; - _.extend(fields_values, { - active_id: active_id || false, - active_ids: active_id ? [active_id] : [], - active_model: a_dataset.model, - parent: {} - }); - if (a_dataset.parent_view) { - fields_values.parent = a_dataset.parent_view.get_fields_values([a_dataset.child_name]); - } - return fields_values; - }, - _build_eval_context: function(blacklist) { - var a_dataset = this.view.dataset; - return new instance.web.CompoundContext(a_dataset.get_context(), this._build_view_fields_values(blacklist)); - }, /** * Builds a new context usable for operations related to fields by merging * the fields'context with the action's context. @@ -1485,9 +1804,9 @@ instance.web.form.FormWidget = instance.web.Widget.extend(_.extend({}, instance. if (! v_context) { v_context = (this.field || {}).context || {}; } - + if (v_context.__ref || true) { //TODO: remove true - var fields_values = this._build_eval_context(blacklist); + var fields_values = this.field_manager.build_eval_context(blacklist); v_context = new instance.web.CompoundContext(v_context).set_eval_context(fields_values); } return v_context; @@ -1498,31 +1817,35 @@ instance.web.form.FormWidget = instance.web.Widget.extend(_.extend({}, instance. // if there is a domain on the node, overrides the model's domain var final_domain = n_domain !== null ? n_domain : f_domain; if (!(final_domain instanceof Array) || true) { //TODO: remove true - var fields_values = this._build_eval_context(); + var fields_values = this.field_manager.build_eval_context(); final_domain = new instance.web.CompoundDomain(final_domain).set_eval_context(fields_values); } return final_domain; } -})); +}); instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({ template: 'WidgetButton', - init: function(view, node) { - this._super(view, node); + init: function(field_manager, node) { + this._super(field_manager, node); this.force_disabled = false; this.string = (this.node.attrs.string || '').replace(/_/g, ''); - if (this.node.attrs.default_focus == '1') { + if (JSON.parse(this.node.attrs.default_focus || "0")) { // TODO fme: provide enter key binding to widgets this.view.default_focus_button = this; } + if (this.node.attrs.icon && (! /\//.test(this.node.attrs.icon))) { + this.node.attrs.icon = '/web/static/src/img/icons/' + this.node.attrs.icon + '.png'; + } this.view.on('view_content_has_changed', this, this.check_disable); }, start: function() { this._super.apply(this, arguments); - this.$element.click(this.on_click); - if (this.node.attrs.help || instance.connection.debug) { + this.$el.click(this.on_click); + if (this.node.attrs.help || instance.session.debug) { this.do_attach_tooltip(); } + this.setupFocus(this.$el); }, on_click: function() { var self = this; @@ -1571,37 +1894,30 @@ instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({ on_confirmed: function() { var self = this; - var context = this.node.attrs.context; - if (context && context.__ref) { - context = new instance.web.CompoundContext(context); - context.set_eval_context(this._build_eval_context()); - } + var context = this.build_context(); return this.view.do_execute_action( _.extend({}, this.node.attrs, {context: context}), this.view.dataset, this.view.datarecord.id, function () { - self.view.reload(); + self.view.recursive_reload(); }); }, check_disable: function() { var disabled = (this.force_disabled || !this.view.is_interactible_record()); - this.$element.prop('disabled', disabled); - this.$element.css('color', disabled ? 'grey' : ''); + this.$el.prop('disabled', disabled); + this.$el.css('color', disabled ? 'grey' : ''); } }); /** * Interface to be implemented by fields. - * - * Properties: - * - readonly: boolean. If set to true the field should appear in readonly mode. - * - force_readonly: boolean, When it is true, the field should always appear - * in read only mode, no matter what the value of the "readonly" property can be. + * * Events: - * - changed_value: triggered to inform the view to check on_changes - * + * - changed_value: triggered when the value of the field has changed. This can be due + * to a user interaction or a call to set_value(). + * */ -instance.web.form.FieldMixin = { +instance.web.form.FieldInterface = { /** * Constructor takes 2 arguments: * - field_manager: Implements FieldManagerMixin @@ -1610,14 +1926,11 @@ instance.web.form.FieldMixin = { init: function(field_manager, node) {}, /** * Called by the form view to indicate the value of the field. - * - * set_value() may return an object that can be passed to $.when() that represents the moment when - * the field has finished all operations necessary before the user can effectively use the widget. - * + * * Multiple calls to set_value() can occur at any time and must be handled correctly by the implementation, - * regardless of any asynchronous operation currently running and the status of any promise that a - * previous call to set_value() could have returned. - * + * regardless of any asynchronous operation currently running. Calls to set_value() can and will also occur + * before the widget is inserted into the DOM. + * * set_value() must be able, at any moment, to handle the syntax returned by the "read" method of the * osv class in the OpenERP server as well as the syntax used by the set_value() (see below). It must * also be able to handle any other format commonly used in the _defaults key on the models in the addons @@ -1627,19 +1940,19 @@ instance.web.form.FieldMixin = { set_value: function(value_) {}, /** * Get the current value of the widget. - * - * Must always return a syntaxically correct value to be passed to the "write" method of the osv class in + * + * Must always return a syntactically correct value to be passed to the "write" method of the osv class in * the OpenERP server, although it is not assumed to respect the constraints applied to the field. - * For example if the field is marqued as "required", a call to get_value() can return false. - * + * For example if the field is marked as "required", a call to get_value() can return false. + * * get_value() can also be called *before* a call to set_value() and, in that case, is supposed to - * return a defaut value according to the type of field. - * + * return a default value according to the type of field. + * * This method is always assumed to perform synchronously, it can not return a promise. - * + * * If there was no user interaction to modify the value of the field, it is always assumed that * get_value() return the same semantic value than the one passed in the last call to set_value(), - * altough the syntax can be different. This can be the case for type of fields that have a different + * although the syntax can be different. This can be the case for type of fields that have a different * syntax for "read" and "write" (example: m2o: set_value([0, "Administrator"]), get_value() => 0). */ get_value: function() {}, @@ -1654,28 +1967,30 @@ instance.web.form.FieldMixin = { */ is_valid: function() {}, /** - * Returns true if the field holds a value which is syntaxically correct, ignoring + * Returns true if the field holds a value which is syntactically correct, ignoring * the potential semantic restrictions applied to the field. */ is_syntax_valid: function() {}, /** - * Must set the focus on the field. + * Must set the focus on the field. Return false if field is not focusable. */ focus: function() {}, + /** + * Called when the translate button is clicked. + */ + on_translate: function() {}, }; /** - * Abstract class for classes implementing FieldMixin. - * + * Abstract class for classes implementing FieldInterface. + * * Properties: - * - effective_readonly: when it is true, the widget is displayed as readonly. Vary depending - * the values of the "readonly" property and the "force_readonly" property on the field manager. * - value: useful property to hold the value of the field. By default, set_value() and get_value() * set and retrieve the value property. Changing the value property also triggers automatically * a 'changed_value' event that inform the view to trigger on_changes. - * + * */ -instance.web.form.AbstractField = instance.web.form.FormWidget.extend(_.extend({}, instance.web.form.FieldMixin, { +instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.web.form.FieldInterface, { /** * @constructs instance.web.form.AbstractField * @extends instance.web.form.FormWidget @@ -1684,41 +1999,30 @@ instance.web.form.AbstractField = instance.web.form.FormWidget.extend(_.extend({ * @param node */ init: function(field_manager, node) { + var self = this this._super(field_manager, node); - this.field_manager = field_manager; this.name = this.node.attrs.name; + this.field = this.field_manager.get_field_desc(this.name); + this.widget = this.node.attrs.widget; + this.string = this.node.attrs.string || this.field.string || this.name; + this.options = instance.web.py_eval(this.node.attrs.options || '{}'); this.set({'value': false}); - this.field = this.field_manager.get_field(this.name); - this.set({required: this.modifiers['required'] === true}); - - // some events to make the property "effective_readonly" sync automatically with "readonly" and - // "force_readonly" - this.set({"readonly": this.modifiers['readonly'] === true}); - var test_effective_readonly = function() { - this.set({"effective_readonly": this.get("readonly") || !!this.get("force_readonly")}); - }; - this.on("change:readonly", this, test_effective_readonly); - this.on("change:force_readonly", this, test_effective_readonly); - _.bind(test_effective_readonly, this)(); - + this.on("change:value", this, function() { - if (! this._inhibit_on_change) - this.trigger('changed_value'); + this.trigger('changed_value'); this._check_css_flags(); }); }, renderElement: function() { var self = this; this._super(); - if (this.field.translate) { - this.$element.addClass('oe_form_field_translatable'); - this.$element.find('.oe_field_translate').click(_.bind(function() { - this.field_manager.open_translate_dialog(this); - }, this)); + if (this.field.translate && this.view) { + this.$el.addClass('oe_form_field_translatable'); + this.$el.find('.oe_field_translate').click(this.on_translate); } - this.$label = this.view.$element.find('label[for=' + this.id_for_label + ']'); - if (instance.connection.debug) { - this.do_attach_tooltip(this, this.$label[0] || this.$element); + this.$label = this.view ? this.view.$el.find('label[for=' + this.id_for_label + ']') : $(); + if (instance.session.debug) { + this.do_attach_tooltip(this, this.$label[0] || this.$el); this.$label.off('dblclick').on('dblclick', function() { console.log("Field '%s' of type '%s' in View: %o", self.name, (self.node.attrs.widget || self.field.type), self.view); window.w = self; @@ -1730,23 +2034,47 @@ instance.web.form.AbstractField = instance.web.form.FormWidget.extend(_.extend({ this.on("change:required", this, this._set_required); this._set_required(); } + this._check_visibility(); + this.field_manager.off("change:display_invalid_fields", this, this._check_css_flags); + this.field_manager.on("change:display_invalid_fields", this, this._check_css_flags); + this._check_css_flags(); + }, + start: function() { + var tmp = this._super(); + this.on("change:value", this, function() { + if (! this.no_rerender) + this.render_value(); + }); + this.render_value(); }, /** * Private. Do not use. */ _set_required: function() { - this.$element.toggleClass('oe_form_required', this.get("required")); + this.$el.toggleClass('oe_form_required', this.get("required")); }, set_value: function(value_) { - this._inhibit_on_change = true; this.set({'value': value_}); - this._inhibit_on_change = false; }, get_value: function() { return this.get('value'); }, + /** + Utility method that all implementations should use to change the + value without triggering a re-rendering. + */ + internal_set_value: function(value_) { + var tmp = this.no_render; + this.no_rerender = true; + this.set({'value': value_}); + this.no_rerender = tmp; + }, + /** + This method is called each time the value is modified. + */ + render_value: function() {}, is_valid: function() { - return this.is_syntax_valid() && (! this.get('required') || ! this.is_false()); + return this.is_syntax_valid() && !(this.get('required') && this.is_false()); }, is_syntax_valid: function() { return true; @@ -1758,62 +2086,65 @@ instance.web.form.AbstractField = instance.web.form.FormWidget.extend(_.extend({ is_false: function() { return this.get('value') === false; }, - _check_css_flags: function(show_invalid) { + _check_css_flags: function() { if (this.field.translate) { - this.$element.find('.oe_field_translate').toggle(!this.field_manager.is_create_mode()); + this.$el.find('.oe_field_translate').toggle(this.field_manager.get('actual_mode') !== "create"); } if (!this.disable_utility_classes) { if (this.field_manager.get('display_invalid_fields')) { - this.$element.toggleClass('oe_form_invalid', !this.is_valid()); + this.$el.toggleClass('oe_form_invalid', !this.is_valid()); } } }, focus: function() { - }, - /** - * Utility method to focus an element, but only after a small amount of time. - */ - delay_focus: function($elem) { - setTimeout(function() { - $elem.focus(); - }, 50); - }, - /** - * Utility method to get the widget options defined in the field xml description. - */ - get_definition_options: function() { - if (!this.definition_options) { - var str = this.node.attrs.options || '{}'; - this.definition_options = JSON.parse(str); - } - return this.definition_options; + return false; }, set_input_id: function(id) { this.id_for_label = id; }, -})); + on_translate: function() { + var self = this; + var trans = new instance.web.DataSet(this, 'ir.translation'); + return trans.call_button('translate_fields', [this.view.dataset.model, this.view.datarecord.id, this.name, this.view.dataset.get_context()]).then(function(r) { + self.do_action(r); + }); + }, + + set_dimensions: function (height, width) { + // remove width css property + this.$el.css('width', ''); + // extract style (without width) + var old_style = this.$el.attr('style'); + // jQuery doesn't understand/use !important + var style = 'width:' + width + 'px !important;'; + if (old_style) { + style += old_style + } + this.$el.attr('style', style); + this.$el.css('minHeight', height); + } +}); /** - * A mixin to apply on any field that has to completely re-render when its readonly state + * A mixin to apply on any FormWidget that has to completely re-render when its readonly state * switch. */ -instance.web.form.ReinitializeFieldMixin = { +instance.web.form.ReinitializeWidgetMixin = { /** - * Default implementation of start(), use it or call explicitly initialize_field(). + * Default implementation of, you should not override it, use initialize_field() instead. */ start: function() { - this._super(); this.initialize_field(); + this._super(); }, initialize_field: function() { - this.on("change:effective_readonly", this, function() { - this.destroy_content(); - this.renderElement(); - this.initialize_content(); - this.render_value(); - }); + this.on("change:effective_readonly", this, this.reinitialize); + this.initialize_content(); + }, + reinitialize: function() { + this.destroy_content(); + this.renderElement(); this.initialize_content(); - this.render_value(); }, /** * Called to destroy anything that could have been created previously, called before a @@ -1824,43 +2155,49 @@ instance.web.form.ReinitializeFieldMixin = { * Called to initialize the content. */ initialize_content: function() {}, - /** - * Called to render the value. Should also be explicitly called at the end of a set_value(). - */ - render_value: function() {}, }; -instance.web.form.FieldChar = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.ReinitializeFieldMixin, { +/** + * A mixin to apply on any field that has to completely re-render when its readonly state + * switch. + */ +instance.web.form.ReinitializeFieldMixin = _.extend({}, instance.web.form.ReinitializeWidgetMixin, { + reinitialize: function() { + instance.web.form.ReinitializeWidgetMixin.reinitialize.call(this); + this.render_value(); + }, +}); + +instance.web.form.FieldChar = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, { template: 'FieldChar', + widget_class: 'oe_form_field_char', init: function (field_manager, node) { this._super(field_manager, node); this.password = this.node.attrs.password === 'True' || this.node.attrs.password === '1'; }, initialize_content: function() { var self = this; - this.$element.find('input').change(function() { - self.set({'value': instance.web.parse_value(self.$element.find('input').val(), self)}); + var $input = this.$el.find('input'); + $input.change(function() { + self.internal_set_value(self.parse_value($input.val())); }); - }, - set_value: function(value_) { - this._super(value_); - this.render_value(); + this.setupFocus($input); }, render_value: function() { - var show_value = instance.web.format_value(this.get('value'), this, ''); + var show_value = this.format_value(this.get('value'), ''); if (!this.get("effective_readonly")) { - this.$element.find('input').val(show_value); + this.$el.find('input').val(show_value); } else { if (this.password) { show_value = new Array(show_value.length + 1).join('*'); } - this.$element.text(show_value); + this.$(".oe_form_char_content").text(show_value); } }, is_syntax_valid: function() { - if (!this.get("effective_readonly")) { + if (!this.get("effective_readonly") && this.$("input").size() > 0) { try { - var value_ = instance.web.parse_value(this.$element.find('input').val(), this, ''); + var value_ = this.parse_value(this.$el.find('input').val(), ''); return true; } catch(e) { return false; @@ -1868,31 +2205,39 @@ instance.web.form.FieldChar = instance.web.form.AbstractField.extend(_.extend({} } return true; }, + parse_value: function(val, def) { + return instance.web.parse_value(val, this, def); + }, + format_value: function(val, def) { + return instance.web.format_value(val, this, def); + }, is_false: function() { - return this.get('value') === ''; + return this.get('value') === '' || this._super(); }, focus: function() { - this.delay_focus(this.$element.find('input:first')); + this.$('input:first').focus(); } -})); +}); instance.web.form.FieldID = instance.web.form.FieldChar.extend({ - + }); instance.web.form.FieldEmail = instance.web.form.FieldChar.extend({ template: 'FieldEmail', initialize_content: function() { this._super(); - this.$element.find('button').click(this.on_button_clicked); + var $button = this.$el.find('button'); + $button.click(this.on_button_clicked); + this.setupFocus($button); }, render_value: function() { if (!this.get("effective_readonly")) { this._super(); } else { - this.$element.find('a') + this.$el.find('a') .attr('href', 'mailto:' + this.get('value')) - .text(this.get('value')); + .text(this.get('value') || ''); } }, on_button_clicked: function() { @@ -1908,7 +2253,9 @@ instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({ template: 'FieldUrl', initialize_content: function() { this._super(); - this.$element.find('button').click(this.on_button_clicked); + var $button = this.$el.find('button'); + $button.click(this.on_button_clicked); + this.setupFocus($button); }, render_value: function() { if (!this.get("effective_readonly")) { @@ -1919,7 +2266,7 @@ instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({ if (!s) { tmp = "http://" + this.get('value'); } - this.$element.find('a').attr('href', tmp).text(tmp); + this.$el.find('a').attr('href', tmp).text(this.get('value') ? tmp : ''); } }, on_button_clicked: function() { @@ -1936,11 +2283,12 @@ instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({ instance.web.form.FieldFloat = instance.web.form.FieldChar.extend({ is_field_number: true, + widget_class: 'oe_form_field_float', init: function (field_manager, node) { this._super(field_manager, node); - this.set({'value': 0}); + this.internal_set_value(0); if (this.node.attrs.digits) { - this.digits = py.eval(node.attrs.digits); + this.digits = this.node.attrs.digits; } else { this.digits = this.field.digits; } @@ -1951,11 +2299,14 @@ instance.web.form.FieldFloat = instance.web.form.FieldChar.extend({ value_ = 0; } this._super.apply(this, [value_]); + }, + focus: function () { + this.$('input:first').select(); } }); -instance.web.DateTimeWidget = instance.web.OldWidget.extend({ - template: "web.datetimepicker", +instance.web.DateTimeWidget = instance.web.Widget.extend({ + template: "web.datepicker", jqueryui_object: 'datetimepicker', type_of_date: "datetime", init: function(parent) { @@ -1964,23 +2315,30 @@ instance.web.DateTimeWidget = instance.web.OldWidget.extend({ }, start: function() { var self = this; - this.$input = this.$element.find('input.oe_datepicker_master'); - this.$input_picker = this.$element.find('input.oe_datepicker_container'); - this.$input.change(this.on_change); + this.$input = this.$el.find('input.oe_datepicker_master'); + this.$input_picker = this.$el.find('input.oe_datepicker_container'); + this.$input.change(function(){ + self.change_datetime(); + }); + this.picker({ + onClose: this.on_picker_select, onSelect: this.on_picker_select, changeMonth: true, changeYear: true, showWeek: true, - showButtonPanel: true + showButtonPanel: true, + firstDay: Date.CultureInfo.firstDayOfWeek }); - this.$element.find('img.oe_datepicker_trigger').click(function() { - if (!self.get("effective_readonly") && !self.picker('widget').is(':visible')) { - self.picker('setDate', self.get('value') ? instance.web.auto_str_to_date(self.get('value')) : new Date()); - self.$input_picker.show(); - self.picker('show'); - self.$input_picker.hide(); + this.$el.find('img.oe_datepicker_trigger').click(function() { + if (self.get("effective_readonly") || self.picker('widget').is(':visible')) { + self.$input.focus(); + return; } + self.picker('setDate', self.value ? instance.web.auto_str_to_date(self.value) : new Date()); + self.$input_picker.show(); + self.picker('show'); + self.$input_picker.hide(); }); this.set_readonly(false); this.set({'value': false}); @@ -1990,7 +2348,10 @@ instance.web.DateTimeWidget = instance.web.OldWidget.extend({ }, on_picker_select: function(text, instance_) { var date = this.picker('getDate'); - this.$input.val(date ? this.format_client(date) : '').change(); + this.$input + .val(date ? this.format_client(date) : '') + .change() + .focus(); }, set_value: function(value_) { this.set({'value': value_}); @@ -2006,7 +2367,7 @@ instance.web.DateTimeWidget = instance.web.OldWidget.extend({ set_readonly: function(readonly) { this.readonly = readonly; this.$input.prop('readonly', this.readonly); - this.$element.find('img.oe_datepicker_trigger').toggleClass('oe_input_icon_disabled', readonly); + this.$el.find('img.oe_datepicker_trigger').toggleClass('oe_input_icon_disabled', readonly); }, is_valid_: function() { var value_ = this.$input.val(); @@ -2027,9 +2388,10 @@ instance.web.DateTimeWidget = instance.web.OldWidget.extend({ format_client: function(v) { return instance.web.format_value(v, {"widget": this.type_of_date}); }, - on_change: function() { + change_datetime: function() { if (this.is_valid_()) { this.set_value_from_ui_(); + this.trigger("datetime_changed"); } } }); @@ -2039,8 +2401,8 @@ instance.web.DateWidget = instance.web.DateTimeWidget.extend({ type_of_date: "date" }); -instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.ReinitializeFieldMixin, { - template: "EmptyComponent", +instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, { + template: "FieldDatetime", build_widget: function() { return new instance.web.DateTimeWidget(this); }, @@ -2053,69 +2415,77 @@ instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(_.exten initialize_content: function() { if (!this.get("effective_readonly")) { this.datewidget = this.build_widget(); - this.datewidget.on_change.add_last(_.bind(function() { - this.set({'value': this.datewidget.get_value()}); + this.datewidget.on('datetime_changed', this, _.bind(function() { + this.internal_set_value(this.datewidget.get_value()); }, this)); - this.datewidget.appendTo(this.$element); + this.datewidget.appendTo(this.$el); + this.setupFocus(this.datewidget.$input); } }, - set_value: function(value_) { - this._super(value_); - this.render_value(); - }, render_value: function() { if (!this.get("effective_readonly")) { this.datewidget.set_value(this.get('value')); } else { - this.$element.text(instance.web.format_value(this.get('value'), this, '')); + this.$el.text(instance.web.format_value(this.get('value'), this, '')); } }, is_syntax_valid: function() { - if (!this.get("effective_readonly")) { + if (!this.get("effective_readonly") && this.datewidget) { return this.datewidget.is_valid_(); } return true; }, is_false: function() { - return this.get('value') === ''; + return this.get('value') === '' || this._super(); }, focus: function() { - if (this.datewidget && this.datewidget.$input) - this.delay_focus(this.datewidget.$input); + if (this.datewidget && this.datewidget.$input) { + this.datewidget.$input.focus(); + } } -})); +}); instance.web.form.FieldDate = instance.web.form.FieldDatetime.extend({ + template: "FieldDate", build_widget: function() { return new instance.web.DateWidget(this); } }); -instance.web.form.FieldText = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.ReinitializeFieldMixin, { +instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, { template: 'FieldText', + init: function (field_manager, node) { + this._super(field_manager, node); + }, initialize_content: function() { - this.$textarea = this.$element.find('textarea'); + var self = this; + this.$textarea = this.$el.find('textarea'); + this.default_height = this.$textarea.css('height'); if (!this.get("effective_readonly")) { this.$textarea.change(_.bind(function() { - this.set({'value': instance.web.parse_value(this.$textarea.val(), this)}); + self.internal_set_value(instance.web.parse_value(self.$textarea.val(), self)); }, this)); } else { this.$textarea.attr('disabled', 'disabled'); } - }, - set_value: function(value_) { - this._super.apply(this, arguments); - this.render_value(); + this.$el.keyup(function (e) { + if (e.which === $.ui.keyCode.ENTER) { + e.stopPropagation(); + } + }); + this.setupFocus(this.$textarea); }, render_value: function() { + $(window).resize(); var show_value = instance.web.format_value(this.get('value'), this, ''); - this.$textarea.val(show_value); - if (show_value && this.view.options.resize_textareas) { - this.do_resize(this.view.options.resize_textareas); + if (show_value === '') { + this.$textarea.css('height', parseInt(this.default_height)+"px"); } + this.$textarea.val(show_value); + this.$textarea.autosize(); }, is_syntax_valid: function() { - if (!this.get("effective_readonly")) { + if (!this.get("effective_readonly") && this.$textarea) { try { var value_ = instance.web.parse_value(this.$textarea.val(), this, ''); return true; @@ -2126,53 +2496,91 @@ instance.web.form.FieldText = instance.web.form.AbstractField.extend(_.extend({} return true; }, is_false: function() { - return this.get('value') === ''; + return this.get('value') === '' || this._super(); }, - focus: function($element) { - this.delay_focus(this.$textarea); + focus: function($el) { + this.$textarea.focus(); }, - do_resize: function(max_height) { - max_height = parseInt(max_height, 10); - var $input = this.$textarea, - $div = $('<div style="position: absolute; z-index: 1000; top: 0"/>').width($input.width()), - new_height; - $div.text($input.val()); - _.each('font-family,font-size,white-space'.split(','), function(style) { - $div.css(style, $input.css(style)); + set_dimensions: function (height, width) { + this._super(); + this.$textarea.css({ + width: width, + minHeight: height }); - $div.appendTo($('body')); - new_height = $div.height(); - if (new_height < 90) { - new_height = 90; + }, +}); + +/** + * FieldTextHtml Widget + * Intended for FieldText widgets meant to display HTML content. This + * widget will instantiate the CLEditor (see cleditor in static/src/lib) + * To find more information about CLEditor configutation: go to + * http://premiumsoftware.net/cleditor/docs/GettingStarted.html + */ +instance.web.form.FieldTextHtml = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, { + template: 'FieldTextHtml', + init: function() { + this._super.apply(this, arguments); + }, + initialize_content: function() { + var self = this; + if (! this.get("effective_readonly")) { + self._updating_editor = false; + this.$textarea = this.$el.find('textarea'); + var width = ((this.node.attrs || {}).editor_width || '100%'); + var height = ((this.node.attrs || {}).editor_height || 250); + this.$textarea.cleditor({ + width: width, // width not including margins, borders or padding + height: height, // height not including margins, borders or padding + controls: // controls to add to the toolbar + "bold italic underline strikethrough " + + "| removeformat | bullets numbering | outdent " + + "indent | link unlink | source", + bodyStyle: // style to assign to document body contained within the editor + "margin:4px; font:12px monospace; cursor:text; color:#1F1F1F" + }); + this.$cleditor = this.$textarea.cleditor()[0]; + this.$cleditor.change(function() { + if (! self._updating_editor) { + self.$cleditor.updateTextArea(); + self.internal_set_value(self.$textarea.val()); + } + }); } - if (!isNaN(max_height) && new_height > max_height) { - new_height = max_height; + }, + render_value: function() { + if (! this.get("effective_readonly")) { + this.$textarea.val(this.get('value') || ''); + this._updating_editor = true; + this.$cleditor.updateFrame(); + this._updating_editor = false; + } else { + this.$el.html(this.get('value')); } - $div.remove(); - $input.height(new_height); }, -})); +}); instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({ template: 'FieldBoolean', start: function() { - this._super.apply(this, arguments); - this.$checkbox = $("input", this.$element); - this.$element.click(_.bind(function() { - this.set({'value': this.$checkbox.is(':checked')}); + var self = this; + this.$checkbox = $("input", this.$el); + this.setupFocus(this.$checkbox); + this.$el.click(_.bind(function() { + this.internal_set_value(this.$checkbox.is(':checked')); }, this)); var check_readonly = function() { - this.$checkbox.prop('disabled', this.get("effective_readonly")); + self.$checkbox.prop('disabled', self.get("effective_readonly")); }; this.on("change:effective_readonly", this, check_readonly); - _.bind(check_readonly, this)(); - }, - set_value: function(value_) { + check_readonly.call(this); this._super.apply(this, arguments); - this.$checkbox[0].checked = value_; + }, + render_value: function() { + this.$checkbox[0].checked = this.get('value'); }, focus: function() { - this.delay_focus(this.$checkbox); + this.$checkbox.focus(); } }); @@ -2180,27 +2588,23 @@ instance.web.form.FieldProgressBar = instance.web.form.AbstractField.extend({ template: 'FieldProgressBar', start: function() { this._super.apply(this, arguments); - this.$element.progressbar({ + this.$el.progressbar({ value: this.get('value'), disabled: this.get("effective_readonly") }); }, - set_value: function(value_) { - this._super.apply(this, arguments); - var show_value = Number(value_); + render_value: function() { + var show_value = Number(this.get('value')); if (isNaN(show_value)) { show_value = 0; } var formatted_value = instance.web.format_value(show_value, { type : 'float' }, '0'); - this.$element.progressbar('option', 'value', show_value).find('span').html(formatted_value + '%'); + this.$el.progressbar('option', 'value', show_value).find('span').html(formatted_value + '%'); } }); -instance.web.form.FieldTextXml = instance.web.form.AbstractField.extend({ -// to replace view editor -}); -instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.ReinitializeFieldMixin, { +instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, { template: 'FieldSelection', init: function(field_manager, node) { var self = this; @@ -2226,9 +2630,9 @@ instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(_.exte // changing the selected value), takes the action as validating the // row var ischanging = false; - this.$element.find('select') + var $select = this.$el.find('select') .change(_.bind(function() { - this.set({'value': this.values[this.$element.find('select')[0].selectedIndex][0]}); + this.internal_set_value(this.values[this.$el.find('select')[0].selectedIndex][0]); }, this)) .change(function () { ischanging = true; }) .click(function () { ischanging = false; }) @@ -2237,12 +2641,12 @@ instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(_.exte e.stopPropagation(); ischanging = false; }); + this.setupFocus($select); }, set_value: function(value_) { value_ = value_ === null ? false : value_; value_ = value_ instanceof Array ? value_[0] : value_; this._super(value_); - this.render_value(); }, render_value: function() { if (!this.get("effective_readonly")) { @@ -2250,27 +2654,20 @@ instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(_.exte for (var i = 0, ii = this.values.length; i < ii; i++) { if (this.values[i][0] === this.get('value')) index = i; } - this.$element.find('select')[0].selectedIndex = index; + this.$el.find('select')[0].selectedIndex = index; } else { var self = this; var option = _(this.values) - .detect(function (record) { return record[0] === self.get('value'); }); - this.$element.text(option ? option[1] : this.values[0][1]); + .detect(function (record) { return record[0] === self.get('value'); }); + this.$el.text(option ? option[1] : this.values[0][1]); } }, - is_syntax_valid: function() { - if (this.get("effective_readonly")) { - return true; - } - var value_ = this.values[this.$element.find('select')[0].selectedIndex]; - return !! value_; - }, focus: function() { - this.delay_focus(this.$element.find('select:first')); + this.$el.find('select:first').focus(); } -})); +}); -// jquery autocomplete tweak to allow html +// jquery autocomplete tweak to allow html and classnames (function() { var proto = $.ui.autocomplete.prototype, initSource = proto._initSource; @@ -2297,7 +2694,8 @@ instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(_.exte return $( "<li></li>" ) .data( "item.autocomplete", item ) .append( $( "<a></a>" )[ this.options.html ? "html" : "text" ]( item.label ) ) - .appendTo( ul ); + .appendTo( ul ) + .addClass(item.classname); } }); })(); @@ -2317,54 +2715,72 @@ instance.web.form.CompletionFieldMixin = { var self = this; var dataset = new instance.web.DataSet(this, this.field.relation, self.build_context()); + var blacklist = this.get_search_blacklist(); + this.last_query = search_val; return this.orderer.add(dataset.name_search( - search_val, self.build_domain(), 'ilike', this.limit + 1)).pipe(function(data) { + search_val, new instance.web.CompoundDomain(self.build_domain(), [["id", "not in", blacklist]]), + 'ilike', this.limit + 1, self.build_context())).pipe(function(data) { self.last_search = data; // possible selections for the m2o var values = _.map(data, function(x) { + x[1] = x[1].split("\n")[0]; return { label: _.str.escapeHTML(x[1]), - value:x[1], - name:x[1], - id:x[0] + value: x[1], + name: x[1], + id: x[0], }; }); // search more... if more results that max if (values.length > self.limit) { values = values.slice(0, self.limit); - values.push({label: _t("<em> Search More...</em>"), action: function() { - dataset.name_search(search_val, self.build_domain(), 'ilike' - , false, function(data) { - self._search_create_popup("search", data); - }); - }}); + values.push({ + label: _t("Search More..."), + action: function() { + dataset.name_search(search_val, self.build_domain(), 'ilike', false).then(function(data) { + self._search_create_popup("search", data); + }); + }, + classname: 'oe_m2o_dropdown_option' + }); } // quick create var raw_result = _(data.result).map(function(x) {return x[1];}); if (search_val.length > 0 && !_.include(raw_result, search_val)) { - values.push({label: _.str.sprintf(_t('<em> Create "<strong>%s</strong>"</em>'), - $('<span />').text(search_val).html()), action: function() { - self._quick_create(search_val); - }}); + values.push({ + label: _.str.sprintf(_t('Create "<strong>%s</strong>"'), + $('<span />').text(search_val).html()), + action: function() { + self._quick_create(search_val); + }, + classname: 'oe_m2o_dropdown_option' + }); } // create... - values.push({label: _t("<em> Create and Edit...</em>"), action: function() { - self._search_create_popup("form", undefined, {"default_name": search_val}); - }}); + values.push({ + label: _t("Create and Edit..."), + action: function() { + self._search_create_popup("form", undefined, self._create_context(search_val)); + }, + classname: 'oe_m2o_dropdown_option' + }); return values; }); }, + get_search_blacklist: function() { + return []; + }, _quick_create: function(name) { var self = this; var slow_create = function () { - self._search_create_popup("form", undefined, {"default_name": name}); + self._search_create_popup("form", undefined, self._create_context(name)); }; - if (self.get_definition_options().quick_create === undefined || self.get_definition_options().quick_create) { + if (self.options.quick_create === undefined || self.options.quick_create) { new instance.web.DataSet(this, this.field.relation, self.build_context()) - .name_create(name, function(data) { + .name_create(name).then(function(data) { self.add_id(data[0]); }).fail(function(error, event) { event.preventDefault(); @@ -2380,7 +2796,7 @@ instance.web.form.CompletionFieldMixin = { pop.select_element( self.field.relation, { - title: (view === 'search' ? _t("Search: ") : _t("Create: ")) + (this.string || this.name), + title: (view === 'search' ? _t("Search: ") : _t("Create: ")) + this.string, initial_ids: ids ? _.map(ids, function(x) {return x[0]}) : undefined, initial_view: view, disable_multiple_selection: true @@ -2388,18 +2804,53 @@ instance.web.form.CompletionFieldMixin = { self.build_domain(), new instance.web.CompoundContext(self.build_context(), context || {}) ); - pop.on_select_elements.add(function(element_ids) { + pop.on("elements_selected", self, function(element_ids) { self.add_id(element_ids[0]); + self.focus(); }); }, /** * To implement. */ add_id: function(id) {}, + _create_context: function(name) { + var tmp = {}; + var field = (this.options || {}).create_name_field; + if (field === undefined) + field = "name"; + if (field !== false && name && (this.options || {}).quick_create !== false) + tmp["default_" + field] = name; + return tmp; + }, }; -instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.ReinitializeFieldMixin, - instance.web.form.CompletionFieldMixin, { +instance.web.form.M2ODialog = instance.web.Dialog.extend({ + template: "M2ODialog", + init: function(parent) { + this._super(parent, { + title: _.str.sprintf(_t("Add %s"), parent.string), + width: 312, + }); + }, + start: function() { + var self = this; + this.$buttons.html(QWeb.render("M2ODialog.buttons")); + this.$("input").val(this.getParent().last_query); + this.$buttons.find(".oe_form_m2o_qc_button").click(function(){ + self.getParent()._quick_create(self.$("input").val()); + self.destroy(); + }); + this.$buttons.find(".oe_form_m2o_sc_button").click(function(){ + self.getParent()._search_create_popup("form", undefined, self.getParent()._create_context(self.$("input").val())); + self.destroy(); + }); + this.$buttons.find(".oe_form_m2o_cancel_button").click(function(){ + self.destroy(); + }); + }, +}); + +instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, { template: "FieldMany2One", init: function(field_manager, node) { this._super(field_manager, node); @@ -2408,114 +2859,159 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten this.display_value = {}; this.last_search = []; this.floating = false; - this.inhibit_on_change = false; + this.current_display = null; + this.is_started = false; }, - start: function() { - this._super(); - instance.web.form.ReinitializeFieldMixin.start.call(this); - this.on("change:value", this, function() { - this.floating = false; + reinit_value: function(val) { + this.internal_set_value(val); + this.floating = false; + if (this.is_started) this.render_value(); + }, + initialize_field: function() { + this.is_started = true; + instance.web.bus.on('click', this, function() { + if (!this.get("effective_readonly") && this.$input && this.$input.autocomplete('widget').is(':visible')) { + this.$input.autocomplete("close"); + } }); + instance.web.form.ReinitializeFieldMixin.initialize_field.call(this); }, initialize_content: function() { if (!this.get("effective_readonly")) this.render_editable(); - this.render_value(); + }, + init_error_displayer: function() { + // nothing + }, + hide_error_displayer: function() { + // doesn't work + }, + show_error_displayer: function() { + new instance.web.form.M2ODialog(this).open(); }, render_editable: function() { var self = this; - this.$input = this.$element.find("input"); - - self.$input.tipsy({ - title: function() { - return "No element was selected, you should create or select one from the dropdown list."; - }, - trigger:'manual', - fade: true, + this.$input = this.$el.find("input"); + + this.init_error_displayer(); + + self.$input.on('focus', function() { + self.hide_error_displayer(); }); - - this.$drop_down = this.$element.find(".oe-m2o-drop-down-button"); - this.$follow_button = $(".oe-m2o-cm-button", this.$element); - - this.$follow_button.click(function() { + + this.$drop_down = this.$el.find(".oe_m2o_drop_down_button"); + this.$follow_button = $(".oe_m2o_cm_button", this.$el); + + this.$follow_button.click(function(ev) { + ev.preventDefault(); if (!self.get('value')) { + self.focus(); return; } - var pop = new instance.web.form.FormOpenPopup(self.view); + var pop = new instance.web.form.FormOpenPopup(self); pop.show_element( self.field.relation, self.get("value"), self.build_context(), { - title: _t("Open: ") + (self.string || self.name) + title: _t("Open: ") + self.string } ); - pop.on_write_completed.add_last(function() { + pop.on('write_completed', self, function(){ self.display_value = {}; self.render_value(); + self.focus(); }); }); // some behavior for input - this.$input.keyup(function() { - if (self.$input.val() === "") { - self.set({value: false}); - } else { - self.floating = true; + var input_changed = function() { + if (self.current_display !== self.$input.val()) { + self.current_display = self.$input.val(); + if (self.$input.val() === "") { + self.internal_set_value(false); + self.floating = false; + } else { + self.floating = true; + } } - }); + }; + this.$input.keydown(input_changed); + this.$input.change(input_changed); this.$drop_down.click(function() { if (self.$input.autocomplete("widget").is(":visible")) { self.$input.autocomplete("close"); + self.$input.focus(); } else { if (self.get("value") && ! self.floating) { self.$input.autocomplete("search", ""); } else { self.$input.autocomplete("search"); } - self.$input.focus(); } }); - var tip_def = $.Deferred(); - var untip_def = $.Deferred(); - var tip_delay = 200; - var tip_duration = 3000; - var anyoneLoosesFocus = function() { + self.ed_def = $.Deferred(); + self.uned_def = $.Deferred(); + var ed_delay = 200; + var ed_duration = 15000; + var anyoneLoosesFocus = function (e) { + var used = false; if (self.floating) { if (self.last_search.length > 0) { if (self.last_search[0][0] != self.get("value")) { self.display_value = {}; self.display_value["" + self.last_search[0][0]] = self.last_search[0][1]; - self.set({value: self.last_search[0][0]}); + self.reinit_value(self.last_search[0][0]); } else { + used = true; self.render_value(); } } else { - self.set({value: false}); + used = true; + self.reinit_value(false); } + self.floating = false; } - if (! self.get("value")) { - tip_def.reject(); - untip_def.reject(); - tip_def = $.Deferred(); - tip_def.then(function() { - self.$input.tipsy("show"); + if (used && self.get("value") === false && ! self.no_ed) { + self.ed_def.reject(); + self.uned_def.reject(); + self.ed_def = $.Deferred(); + self.ed_def.then(function() { + self.show_error_displayer(); + ignore_blur = false; + self.trigger('focused'); }); + ignore_blur = true; setTimeout(function() { - tip_def.resolve(); - untip_def.reject(); - untip_def = $.Deferred(); - untip_def.then(function() { - self.$input.tipsy("hide"); + self.ed_def.resolve(); + self.uned_def.reject(); + self.uned_def = $.Deferred(); + self.uned_def.then(function() { + self.hide_error_displayer(); }); - setTimeout(function() {untip_def.resolve();}, tip_duration); - }, tip_delay); + setTimeout(function() {self.uned_def.resolve();}, ed_duration); + }, ed_delay); } else { - tip_def.reject(); + self.no_ed = false; + self.ed_def.reject(); } }; - this.$input.focusout(anyoneLoosesFocus); + var ignore_blur = false; + this.$input.on({ + focusout: anyoneLoosesFocus, + focus: function () { self.trigger('focused'); }, + autocompleteopen: function () { ignore_blur = true; }, + autocompleteclose: function () { ignore_blur = false; }, + blur: function () { + // autocomplete open + if (ignore_blur) { return; } + if (_(self.getChildren()).any(function (child) { + return child instanceof instance.web.form.AbstractFormPopup; + })) { return; } + self.trigger('blurred'); + } + }); var isSelecting = false; // autocomplete @@ -2531,10 +3027,11 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten if (item.id) { self.display_value = {}; self.display_value["" + item.id] = item.name; - self.set({value: item.id}); + self.reinit_value(item.id); } else if (item.action) { - self.floating = true; item.action(); + // Cancel widget blurring, to avoid form blur event + self.trigger('focused'); return false; } }, @@ -2542,21 +3039,22 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten e.preventDefault(); }, html: true, - close: anyoneLoosesFocus, + // disabled to solve a bug, but may cause others + //close: anyoneLoosesFocus, minLength: 0, delay: 0 }); this.$input.autocomplete("widget").addClass("openerp"); // used to correct a bug when selecting an element by pushing 'enter' in an editable list this.$input.keyup(function(e) { - if (e.which === 13) { + if (e.which === 13) { // ENTER if (isSelecting) e.stopPropagation(); } isSelecting = false; }); + this.setupFocus(this.$follow_button); }, - render_value: function(no_recurse) { var self = this; if (! this.get("value")) { @@ -2569,8 +3067,8 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten return; } if (! no_recurse) { - var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.view.dataset.get_context()); - dataset.name_get([self.get("value")], function(data) { + var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context()); + dataset.name_get([self.get("value")]).then(function(data) { self.display_value["" + self.get("value")] = data[0][1]; self.render_value(true); }); @@ -2579,47 +3077,76 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten display_string: function(str) { var self = this; if (!this.get("effective_readonly")) { - this.$input.val(str); + this.$input.val(str.split("\n")[0]); + this.current_display = this.$input.val(); + if (this.is_false()) { + this.$('.oe_m2o_cm_button').css({'display':'none'}); + } else { + this.$('.oe_m2o_cm_button').css({'display':'inline'}); + } } else { - this.$element.find('a') + var lines = _.escape(str).split("\n"); + var link = ""; + var follow = ""; + link = lines[0]; + follow = _.rest(lines).join("<br />"); + if (follow) + link += "<br />"; + var $link = this.$el.find('.oe_form_uri') .unbind('click') - .text(str) - .click(function () { + .html(link); + if (! this.options.no_open) + $link.click(function () { self.do_action({ type: 'ir.actions.act_window', res_model: self.field.relation, res_id: self.get("value"), - context: self.build_context(), views: [[false, 'form']], target: 'current' }); return false; }); + $(".oe_form_m2o_follow", this.$el).html(follow); } }, set_value: function(value_) { var self = this; if (value_ instanceof Array) { this.display_value = {}; - this.display_value["" + value_[0]] = value_[1]; + if (! this.options.always_reload) { + this.display_value["" + value_[0]] = value_[1]; + } value_ = value_[0]; } value_ = value_ || false; - this.inhibit_on_change = true; - this._super(value_); - this.inhibit_on_change = false; + this.reinit_value(value_); + }, + get_displayed: function() { + return this.display_value["" + this.get("value")]; }, add_id: function(id) { this.display_value = {}; - this.set({value: id}); + this.reinit_value(id); }, is_false: function() { return ! this.get("value"); }, focus: function () { - this.delay_focus(this.$input); - } -})); + if (!this.get('effective_readonly')) { + this.$input.focus(); + } + }, + _quick_create: function() { + this.no_ed = true; + this.ed_def.reject(); + return instance.web.form.CompletionFieldMixin._quick_create.apply(this, arguments); + }, + _search_create_popup: function() { + this.no_ed = true; + this.ed_def.reject(); + return instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments); + }, +}); /* # Values: (0, 0, { fields }) create @@ -2674,27 +3201,26 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({ lazy_build_o2m_kanban_view(); this.is_loaded = $.Deferred(); this.initial_is_loaded = this.is_loaded; - this.is_setted = $.Deferred(); this.form_last_update = $.Deferred(); this.init_form_last_update = this.form_last_update; - }, - start: function() { - this._super.apply(this, arguments); - - var self = this; - + this.is_started = false; this.dataset = new instance.web.form.One2ManyDataSet(this, this.field.relation); this.dataset.o2m = this; this.dataset.parent_view = this.view; this.dataset.child_name = this.name; - //this.dataset.child_name = - this.dataset.on_change.add_last(function() { + var self = this; + this.dataset.on('dataset_changed', this, function() { self.trigger_on_change(); }); + this.set_value([]); + }, + start: function() { + this._super.apply(this, arguments); + this.$el.addClass('oe_form_field oe_form_field_one2many'); - this.is_setted.then(function() { - self.load_views(); - }); + var self = this; + + self.load_views(); this.is_loaded.then(function() { self.on("change:effective_readonly", self, function() { self.is_loaded = self.is_loaded.pipe(function() { @@ -2705,6 +3231,8 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({ }); }); }); + this.is_started = true; + this.reload_current_view(); }, trigger_on_change: function() { var tmp = this.doing_on_change; @@ -2714,11 +3242,14 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({ }, load_views: function() { var self = this; - + var modes = this.node.attrs.mode; modes = !!modes ? modes.split(",") : ["tree"]; var views = []; _.each(modes, function(mode) { + if (! _.include(["list", "tree", "graph", "kanban"], mode)) { + throw new Error(_.str.sprintf("View type '%s' is not supported in One2Many.", mode)); + } var view = { view_id: false, view_type: mode == "tree" ? "list" : mode, @@ -2728,20 +3259,37 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({ view.embedded_view = self.field.views[mode]; } if(view.view_type === "list") { - view.options.selectable = self.multi_selection; + _.extend(view.options, { + addable: null, + selectable: self.multi_selection, + sortable: false, + import_enabled: false, + deletable: true + }); if (self.get("effective_readonly")) { - view.options.addable = null; - view.options.deletable = null; + _.extend(view.options, { + deletable: null, + reorderable: false, + }); } } else if (view.view_type === "form") { if (self.get("effective_readonly")) { view.view_type = 'form'; } - view.options.not_interactible_on_create = true; + _.extend(view.options, { + not_interactible_on_create: true, + }); } else if (view.view_type === "kanban") { + _.extend(view.options, { + confirm_on_delete: false, + }); if (self.get("effective_readonly")) { - view.options.action_buttons = false; - view.options.quick_creatable = false; + _.extend(view.options, { + action_buttons: false, + quick_creatable: false, + creatable: false, + read_only_mode: true, + }); } } views.push(view); @@ -2756,36 +3304,45 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({ var def = $.Deferred().then(function() { self.initial_is_loaded.resolve(); }); - this.viewmanager.on_controller_inited.add_last(function(view_type, controller) { + this.viewmanager.on("controller_inited", self, function(view_type, controller) { controller.o2m = self; if (view_type == "list") { - if (self.get("effective_readonly")) - controller.set_editable(false); + if (self.get("effective_readonly")) { + controller.on('edit:before', self, function (e) { + e.cancel = true; + }); + _(controller.columns).find(function (column) { + if (!(column instanceof instance.web.list.Handle)) { + return false; + } + column.modifiers.invisible = true; + return true; + }); + } } else if (view_type === "form") { if (self.get("effective_readonly")) { - $(".oe_form_buttons", controller.$element).children().remove(); + $(".oe_form_buttons", controller.$el).children().remove(); } - controller.on_record_loaded.add_last(function() { - once.resolve(); - }); - controller.on_pager_action.add_first(function() { - self.save_any_view(); - }); + controller.on("load_record", self, function(){ + once.resolve(); + }); + controller.on('pager_action_executed',self,self.save_any_view); } else if (view_type == "graph") { self.reload_current_view() } def.resolve(); }); - this.viewmanager.on_mode_switch.add_first(function(n_mode, b, c, d, e) { + this.viewmanager.on("switch_mode", self, function(n_mode, b, c, d, e) { $.when(self.save_any_view()).then(function() { - if(n_mode === "list") - $.async_when().then(function() {self.reload_current_view();}); + if (n_mode === "list") { + $.async_when().then(function() { + self.reload_current_view(); + }); + } }); }); - this.is_setted.then(function() { - $.async_when().then(function () { - self.viewmanager.appendTo(self.$element); - }); + $.async_when().then(function () { + self.viewmanager.appendTo(self.$el); }); return def; }, @@ -2865,8 +3422,12 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({ if (this.dataset.index === null && this.dataset.ids.length > 0) { this.dataset.index = 0; } - self.is_setted.resolve(); - return self.reload_current_view(); + this.trigger_on_change(); + if (this.is_started) { + return self.reload_current_view(); + } else { + return $.when(); + } }, get_value: function() { var self = this; @@ -2892,42 +3453,44 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({ save_any_view: function() { if (this.doing_on_change) return false; - return this.session.synchronized_mode(_.bind(function() { - if (this.viewmanager && this.viewmanager.views && this.viewmanager.active_view && - this.viewmanager.views[this.viewmanager.active_view] && - this.viewmanager.views[this.viewmanager.active_view].controller) { - var view = this.viewmanager.views[this.viewmanager.active_view].controller; - if (this.viewmanager.active_view === "form") { - if (!view.is_initialized.isResolved()) { - return false; - } - var res = $.when(view.do_save()); - if (!res.isResolved() && !res.isRejected()) { - console.warn("Asynchronous get_value() is not supported in form view."); - } - return res; - } else if (this.viewmanager.active_view === "list") { - var res = $.when(view.ensure_saved()); - if (!res.isResolved() && !res.isRejected()) { - console.warn("Asynchronous get_value() is not supported in list view."); - } - return res; - } - } - return false; - }, this)); + return this.session.synchronized_mode(_.bind(function() { + if (this.viewmanager && this.viewmanager.views && this.viewmanager.active_view && + this.viewmanager.views[this.viewmanager.active_view] && + this.viewmanager.views[this.viewmanager.active_view].controller) { + var view = this.viewmanager.views[this.viewmanager.active_view].controller; + if (this.viewmanager.active_view === "form") { + if (!view.is_initialized.isResolved()) { + return false; + } + var res = $.when(view.save()); + if (!res.isResolved() && !res.isRejected()) { + console.warn("Asynchronous get_value() is not supported in form view."); + } + return res; + } else if (this.viewmanager.active_view === "list") { + var res = $.when(view.ensure_saved()); + if (!res.isResolved() && !res.isRejected()) { + console.warn("Asynchronous get_value() is not supported in list view."); + } + return res; + } + } + return false; + }, this)); }, is_syntax_valid: function() { - if (!this.viewmanager.views[this.viewmanager.active_view]) + if (! this.viewmanager || ! this.viewmanager.views[this.viewmanager.active_view]) return true; var view = this.viewmanager.views[this.viewmanager.active_view].controller; - if (this.viewmanager.active_view === "form") { - for (var f in view.fields) { - f = view.fields[f]; - if (!f.is_valid()) { - return false; - } - } + switch (this.viewmanager.active_view) { + case 'form': + return _(view.fields).chain() + .invoke('is_valid') + .all(_.identity) + .value(); + break; + case 'list': + return view.is_valid(); } return true; }, @@ -2942,16 +3505,23 @@ instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({ form: 'instance.web.form.One2ManyFormView', kanban: 'instance.web.form.One2ManyKanbanView', }); + this.__ignore_blur = false; }, - switch_view: function(mode, unused) { + switch_mode: function(mode, unused) { if (mode !== 'form') { return this._super(mode, unused); } var self = this; var id = self.o2m.dataset.index !== null ? self.o2m.dataset.ids[self.o2m.dataset.index] : null; - var pop = new instance.web.form.FormOpenPopup(self.o2m.view); + var pop = new instance.web.form.FormOpenPopup(this); pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), { - title: _t("Open: ") + self.name, + title: _t("Open: ") + self.o2m.string, + create_function: function(data) { + return self.o2m.dataset.create(data).then(function(r) { + self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r])); + self.o2m.dataset.trigger("dataset_changed", r); + }); + }, write_function: function(id, data, options) { return self.o2m.dataset.write(id, data, {}).then(function() { self.o2m.reload_current_view(); @@ -2966,6 +3536,9 @@ instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({ form_view_options: {'not_interactible_on_create':true}, readonly: self.o2m.get("effective_readonly") }); + pop.on("elements_selected", self, function() { + self.o2m.reload_current_view(); + }); }, }); @@ -2978,8 +3551,52 @@ instance.web.form.One2ManyDataSet = instance.web.BufferedDataSet.extend({ instance.web.form.One2ManyListView = instance.web.ListView.extend({ _template: 'One2Many.listview', + init: function (parent, dataset, view_id, options) { + this._super(parent, dataset, view_id, _.extend(options || {}, { + GroupsType: instance.web.form.One2ManyGroups, + ListType: instance.web.form.One2ManyList + })); + this.on('edit:before', this, this.proxy('_before_edit')); + this.on('edit:after', this, this.proxy('_after_edit')); + this.on('save:before cancel:before', this, this.proxy('_before_unedit')); + + this.records + .bind('add', this.proxy("changed_records")) + .bind('edit', this.proxy("changed_records")) + .bind('remove', this.proxy("changed_records")); + }, + start: function () { + var ret = this._super(); + this.$el + .off('mousedown.handleButtons') + .on('mousedown.handleButtons', 'table button', this.proxy('_button_down')); + return ret; + }, + changed_records: function () { + this.o2m.trigger_on_change(); + }, + is_valid: function () { + var form = this.editor.form; + + // If the form has not been modified, the view can only be valid + // NB: is_dirty will also be set on defaults/onchanges/whatever? + // oe_form_dirty seems to only be set on actual user actions + if (!form.$el.is('.oe_form_dirty')) { + return true; + } + this.o2m._dirty_flag = true; + + // Otherwise validate internal form + return _(form.fields).chain() + .invoke(function () { + this._check_css_flags(); + return this.is_valid(); + }) + .all(_.identity) + .value(); + }, do_add_record: function () { - if (this.options.editable) { + if (this.editable()) { this._super.apply(this, arguments); } else { var self = this; @@ -2987,13 +3604,13 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({ pop.select_element( self.o2m.field.relation, { - title: _t("Create: ") + self.name, + title: _t("Create: ") + self.o2m.string, initial_view: "form", alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined, create_function: function(data, callback, error_callback) { return self.o2m.dataset.create(data).then(function(r) { - self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r.result])); - self.o2m.dataset.on_change(); + self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r])); + self.o2m.dataset.trigger("dataset_changed", r); }).then(callback, error_callback); }, read_function: function() { @@ -3006,18 +3623,18 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({ self.o2m.build_domain(), self.o2m.build_context() ); - pop.on_select_elements.add_last(function() { + pop.on("elements_selected", self, function() { self.o2m.reload_current_view(); }); } }, do_activate_record: function(index, id) { var self = this; - var pop = new instance.web.form.FormOpenPopup(self.o2m.view); + var pop = new instance.web.form.FormOpenPopup(self); pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), { - title: _t("Open: ") + self.name, + title: _t("Open: ") + self.o2m.string, write_function: function(id, data) { - return self.o2m.dataset.write(id, data, {}, function(r) { + return self.o2m.dataset.write(id, data, {}).then(function() { self.o2m.reload_current_view(); }); }, @@ -3028,23 +3645,167 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({ return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments); }, form_view_options: {'not_interactible_on_create':true}, - readonly: self.o2m.get("effective_readonly") + readonly: !this.is_action_enabled('edit') || self.o2m.get("effective_readonly") }); }, do_button_action: function (name, id, callback) { + if (!_.isNumber(id)) { + instance.webclient.notification.warn( + _t("Action Button"), + _t("The o2m record must be saved before an action can be used")); + return; + } + var parent_form = this.o2m.view; var self = this; - var def = $.Deferred().then(callback).then(function() {self.o2m.view.reload();}); - return this._super(name, id, _.bind(def.resolve, def)); + this.ensure_saved().pipe(function () { + if (parent_form) + return parent_form.save(); + else + return $.when(); + }).then(function () { + self.handle_button(name, id, callback); + }); + }, + + _before_edit: function () { + this.__ignore_blur = false; + this.editor.form.on('blurred', this, this._on_form_blur); + }, + _after_edit: function () { + // The form's blur thing may be jiggered during the edition setup, + // potentially leading to the o2m instasaving the row. Cancel any + // blurring triggered the edition startup here + this.editor.form.widgetFocused(); + }, + _before_unedit: function () { + this.editor.form.off('blurred', this, this._on_form_blur); + }, + _button_down: function () { + // If a button is clicked (usually some sort of action button), it's + // the button's responsibility to ensure the editable list is in the + // correct state -> ignore form blurring + this.__ignore_blur = true; + }, + /** + * Handles blurring of the nested form (saves the currently edited row), + * unless the flag to ignore the event is set to ``true`` + * + * Makes the internal form go away + */ + _on_form_blur: function () { + if (this.__ignore_blur) { + this.__ignore_blur = false; + return; + } + // FIXME: why isn't there an API for this? + if (this.editor.form.$el.hasClass('oe_form_dirty')) { + this.save_edition(); + return; + } + this.cancel_edition(); + }, + keyup_ENTER: function () { + // blurring caused by hitting the [Return] key, should skip the + // autosave-on-blur and let the handler for [Return] do its thing (save + // the current row *anyway*, then create a new one/edit the next one) + this.__ignore_blur = true; + this._super.apply(this, arguments); + }, + do_delete: function (ids) { + var self = this; + var next = $.when(); + var _super = this._super; + // handle deletion of an item which does not exist + // TODO: better handle that in the editable list? + var false_id_index = _(ids).indexOf(false); + if (false_id_index !== -1) { + ids.splice(false_id_index, 1); + next = this.cancel_edition(true); + } + return next.pipe(function () { + // wheeee + var confirm = window.confirm; + window.confirm = function () { return true; }; + try { + return _super.call(self, ids); + } finally { + window.confirm = confirm; + } + }); + } +}); +instance.web.form.One2ManyGroups = instance.web.ListView.Groups.extend({ + setup_resequence_rows: function () { + if (!this.view.o2m.get('effective_readonly')) { + this._super.apply(this, arguments); + } + } +}); +instance.web.form.One2ManyList = instance.web.ListView.List.extend({ + pad_table_to: function (count) { + if (!this.view.is_action_enabled('create')) { + this._super(count); + } else { + this._super(count > 0 ? count - 1 : 0); + } + + // magical invocation of wtf does that do + if (this.view.o2m.get('effective_readonly')) { + return; + } + + var self = this; + var columns = _(this.columns).filter(function (column) { + return column.invisible !== '1'; + }).length; + if (this.options.selectable) { columns++; } + if (this.options.deletable) { columns++; } + + if (!this.view.is_action_enabled('create')) { + return; + } + + var $cell = $('<td>', { + colspan: columns, + 'class': 'oe_form_field_one2many_list_row_add' + }).append( + $('<a>', {href: '#'}).text(_t("Add an item")) + .mousedown(function () { + // FIXME: needs to be an official API somehow + if (self.view.editor.is_editing()) { + self.view.__ignore_blur = true; + } + }) + .click(function (e) { + e.preventDefault(); + e.stopPropagation(); + // FIXME: there should also be an API for that one + if (self.view.editor.form.__blur_timeout) { + clearTimeout(self.view.editor.form.__blur_timeout); + self.view.editor.form.__blur_timeout = false; + } + self.view.ensure_saved().then(function () { + self.view.do_add_record(); + }); + })); + + var $padding = this.$current.find('tr:not([data-id]):first'); + var $newrow = $('<tr>').append($cell); + if ($padding.length) { + $padding.before($newrow); + } else { + this.$current.append($newrow) + } } }); instance.web.form.One2ManyFormView = instance.web.FormView.extend({ form_template: 'One2Many.formview', - on_loaded: function(data) { + load_form: function(data) { this._super(data); var self = this; this.$buttons.find('button.oe_form_button_create').click(function() { - self.do_save().then(self.on_button_new); + self.save().then(self.on_button_new); }); }, do_notify_change: function() { @@ -3063,8 +3824,7 @@ instance.web.form.One2ManyKanbanView = instance.web_kanban.KanbanView.extend({ }); } -instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.CompletionFieldMixin, - instance.web.form.ReinitializeFieldMixin, { +instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, { template: "FieldMany2ManyTags", init: function() { this._super.apply(this, arguments); @@ -3073,16 +3833,11 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(_. this._display_orderer = new instance.web.DropMisordered(); this._drop_shown = false; }, - start: function() { - this._super(); - instance.web.form.ReinitializeFieldMixin.start.call(this); - this.on("change:value", this, this.render_value); - }, initialize_content: function() { if (this.get("effective_readonly")) return; var self = this; - self. $text = $("textarea", this.$element); + self.$text = $("textarea", this.$el); self.$text.textext({ plugins : 'tags arrow autocomplete', autocomplete: { @@ -3134,29 +3889,16 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(_. return _.extend(el, {index:i}); })}); }); - }).bind('tagClick', function(e, tag, value, callback) { - var pop = new instance.web.form.FormOpenPopup(self.view); - pop.show_element( - self.field.relation, - value.id, - self.build_context(), - { - title: _t("Open: ") + (self.string || self.name) - } - ); - pop.on_write_completed.add_last(function() { - self.render_value(); - }); }).bind('hideDropdown', function() { self._drop_shown = false; - }).bind('hideDropdown', function() { + }).bind('showDropdown', function() { self._drop_shown = true; }); self.tags = self.$text.textext()[0].tags(); - $("textarea", this.$element).focusout(function() { - $("textarea", this.$element).val(""); + $("textarea", this.$el).focusout(function() { + self.$text.trigger("setInputData", ""); }).keydown(function(e) { - if (event.keyCode === 9 && self._drop_shown) { + if (e.which === $.ui.keyCode.TAB && self._drop_shown) { self.$text.textext()[0].autocomplete().selectFromDropdown(); } }); @@ -3172,36 +3914,31 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(_. var tmp = [commands.replace_with(this.get("value"))]; return tmp; }, + get_search_blacklist: function() { + return this.get("value"); + }, render_value: function() { var self = this; - var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.view.dataset.get_context()); + var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context()); + var values = self.get("value") var handle_names = function(data) { + if (self.isDestroyed()) + return; var indexed = {}; _.each(data, function(el) { indexed[el[0]] = el; }); - data = _.map(self.get("value"), function(el) { return indexed[el]; }); + data = _.map(values, function(el) { return indexed[el]; }); if (! self.get("effective_readonly")) { self.tags.containerElement().children().remove(); - $("textarea", self.$element).css("padding-left", "3px"); + $("textarea", self.$el).css("padding-left", "3px"); self.tags.addTags(_.map(data, function(el) {return {name: el[1], id:el[0]};})); } else { - self.$element.html(QWeb.render("FieldMany2ManyTags.box", {elements: data})); - $(".oe_form_field_many2manytags_box", self.$element).click(function() { - var index = Number($(this).data("index")); - self.do_action({ - type: 'ir.actions.act_window', - res_model: self.field.relation, - res_id: self.get("value")[index], - context: self.build_context(), - views: [[false, 'form']], - target: 'current' - }); - }); + self.$el.html(QWeb.render("FieldMany2ManyTag", {elements: data})); } }; - if (! self.get('values') || self.get('values').length > 0) { - this._display_orderer.add(dataset.name_get(self.get("value"))).then(handle_names); + if (! values || values.length > 0) { + this._display_orderer.add(dataset.name_get(values)).then(handle_names); } else { handle_names([]); } @@ -3209,11 +3946,14 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(_. add_id: function(id) { this.set({'value': _.uniq(this.get('value').concat([id]))}); }, -})); +}); -/* - * TODO niv: clean those deferred stuff, it could be better - */ +/** + widget options: + - reload_on_button: Reload the whole form view if click on a button in a list view. + If you see this options, do not use it, it's basically a dirty hack to make one + precise o2m to behave the way we want. +*/ instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({ multi_selection: false, disable_utility_classes: true, @@ -3221,32 +3961,31 @@ instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({ this._super(field_manager, node); this.is_loaded = $.Deferred(); this.initial_is_loaded = this.is_loaded; - this.is_setted = $.Deferred(); + this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation); + this.dataset.m2m = this; + var self = this; + this.dataset.on('unlink', self, function(ids) { + self.dataset_changed(); + }); + this.set_value([]); }, start: function() { - this._super.apply(this, arguments); + this.$el.addClass('oe_form_field oe_form_field_many2many'); var self = this; - this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation); - this.dataset.m2m = this; - this.dataset.on_unlink.add_last(function(ids) { - self.dataset_changed(); - }); - - this.is_setted.then(function() { - self.load_view(); - }); + self.load_view(); this.is_loaded.then(function() { self.on("change:effective_readonly", self, function() { self.is_loaded = self.is_loaded.pipe(function() { self.list_view.destroy(); return $.when(self.load_view()).then(function() { - self.reload_content(); + self.render_value(); }); }); }); - }) + }); + this._super.apply(this, arguments); }, set_value: function(value_) { value_ = value_ || []; @@ -3254,10 +3993,12 @@ instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({ value_ = value_[0][2]; } this._super(value_); - this.dataset.set_ids(value_); - var self = this; - self.reload_content(); - this.is_setted.resolve(); + }, + get_value: function() { + return [commands.replace_with(this.get('value'))]; + }, + is_false: function () { + return _(this.get("value")).isEmpty(); }, load_view: function() { var self = this; @@ -3265,6 +4006,9 @@ instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({ 'addable': self.get("effective_readonly") ? null : _t("Add"), 'deletable': self.get("effective_readonly") ? false : true, 'selectable': self.multi_selection, + 'sortable': false, + 'reorderable': false, + 'import_enabled': false, }); var embedded = (this.field.views || {}).tree; if (embedded) { @@ -3272,23 +4016,24 @@ instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({ } this.list_view.m2m_field = this; var loaded = $.Deferred(); - this.list_view.on_loaded.add_last(function() { + this.list_view.on("list_view_loaded", self, function() { self.initial_is_loaded.resolve(); loaded.resolve(); }); $.async_when().then(function () { - self.list_view.appendTo(self.$element); + self.list_view.appendTo(self.$el); }); return loaded; }, - reload_content: function() { + render_value: function() { var self = this; + this.dataset.set_ids(this.get("value")); this.is_loaded = this.is_loaded.pipe(function() { return self.list_view.reload_content(); }); }, dataset_changed: function() { - this.set({'value': [commands.replace_with(this.dataset.ids)]}); + this.internal_set_value(this.dataset.ids); }, }); @@ -3309,39 +4054,257 @@ instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends in pop.select_element( this.model, { - title: _t("Add: ") + this.name + title: _t("Add: ") + this.m2m_field.string }, new instance.web.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]), this.m2m_field.build_context() ); var self = this; - pop.on_select_elements.add(function(element_ids) { - _.each(element_ids, function(one_id) { - if(! _.detect(self.dataset.ids, function(x) {return x == one_id;})) { - self.dataset.set_ids([].concat(self.dataset.ids, [one_id])); + pop.on("elements_selected", self, function(element_ids) { + var reload = false; + _(element_ids).each(function (id) { + if(! _.detect(self.dataset.ids, function(x) {return x == id;})) { + self.dataset.set_ids(self.dataset.ids.concat([id])); self.m2m_field.dataset_changed(); - self.reload_content(); + reload = true; } }); + if (reload) { + self.reload_content(); + } }); }, do_activate_record: function(index, id) { var self = this; var pop = new instance.web.form.FormOpenPopup(this); pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), { - title: _t("Open: ") + this.name, + title: _t("Open: ") + this.m2m_field.string, readonly: this.getParent().get("effective_readonly") }); - pop.on_write_completed.add_last(function() { - self.reload_content(); + pop.on('write_completed', self, self.reload_content); + }, + do_button_action: function(name, id, callback) { + var self = this; + var _sup = _.bind(this._super, this); + if (! this.m2m_field.options.reload_on_button) { + return _sup(name, id, callback); + } else { + return this.m2m_field.view.save().pipe(function() { + return _sup(name, id, function() { + self.m2m_field.view.reload(); + }); + }); + } + }, +}); + +instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, { + disable_utility_classes: true, + init: function(field_manager, node) { + this._super(field_manager, node); + instance.web.form.CompletionFieldMixin.init.call(this); + m2m_kanban_lazy_init(); + this.is_loaded = $.Deferred(); + this.initial_is_loaded = this.is_loaded; + + var self = this; + this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation); + this.dataset.m2m = this; + this.dataset.on('unlink', self, function(ids) { + self.dataset_changed(); }); - } + }, + start: function() { + this._super.apply(this, arguments); + + var self = this; + + self.load_view(); + this.is_loaded.then(function() { + self.on("change:effective_readonly", self, function() { + self.is_loaded = self.is_loaded.pipe(function() { + self.kanban_view.destroy(); + return $.when(self.load_view()).then(function() { + self.render_value(); + }); + }); + }); + }) + }, + set_value: function(value_) { + value_ = value_ || []; + if (value_.length >= 1 && value_[0] instanceof Array) { + value_ = value_[0][2]; + } + this._super(value_); + }, + get_value: function() { + return [commands.replace_with(this.get('value'))]; + }, + load_view: function() { + var self = this; + this.kanban_view = new instance.web.form.Many2ManyKanbanView(this, this.dataset, false, { + 'create_text': _t("Add"), + 'creatable': self.get("effective_readonly") ? false : true, + 'quick_creatable': self.get("effective_readonly") ? false : true, + 'read_only_mode': self.get("effective_readonly") ? true : false, + 'confirm_on_delete': false, + }); + var embedded = (this.field.views || {}).kanban; + if (embedded) { + this.kanban_view.set_embedded_view(embedded); + } + this.kanban_view.m2m = this; + var loaded = $.Deferred(); + this.kanban_view.on("kanban_view_loaded",self,function() { + self.initial_is_loaded.resolve(); + loaded.resolve(); + }); + this.kanban_view.on('switch_mode', this, this.open_popup); + $.async_when().then(function () { + self.kanban_view.appendTo(self.$el); + }); + return loaded; + }, + render_value: function() { + var self = this; + this.dataset.set_ids(this.get("value")); + this.is_loaded = this.is_loaded.pipe(function() { + return self.kanban_view.do_search(self.build_domain(), self.dataset.get_context(), []); + }); + }, + dataset_changed: function() { + this.set({'value': this.dataset.ids}); + }, + open_popup: function(type, unused) { + if (type !== "form") + return; + var self = this; + if (this.dataset.index === null) { + var pop = new instance.web.form.SelectCreatePopup(this); + pop.select_element( + this.field.relation, + { + title: _t("Add: ") + this.string + }, + new instance.web.CompoundDomain(this.build_domain(), ["!", ["id", "in", this.dataset.ids]]), + this.build_context() + ); + pop.on("elements_selected", self, function(element_ids) { + _.each(element_ids, function(one_id) { + if(! _.detect(self.dataset.ids, function(x) {return x == one_id;})) { + self.dataset.set_ids([].concat(self.dataset.ids, [one_id])); + self.dataset_changed(); + self.render_value(); + } + }); + }); + } else { + var id = self.dataset.ids[self.dataset.index]; + var pop = new instance.web.form.FormOpenPopup(this); + pop.show_element(self.field.relation, id, self.build_context(), { + title: _t("Open: ") + self.string, + write_function: function(id, data, options) { + return self.dataset.write(id, data, {}).then(function() { + self.render_value(); + }); + }, + alternative_form_view: self.field.views ? self.field.views["form"] : undefined, + parent_view: self.view, + child_name: self.name, + readonly: self.get("effective_readonly") + }); + } + }, + add_id: function(id) { + this.quick_create.add_id(id); + }, +}); + +function m2m_kanban_lazy_init() { +if (instance.web.form.Many2ManyKanbanView) + return; +instance.web.form.Many2ManyKanbanView = instance.web_kanban.KanbanView.extend({ + quick_create_class: 'instance.web.form.Many2ManyQuickCreate', + _is_quick_create_enabled: function() { + return this._super() && ! this.group_by; + }, +}); +instance.web.form.Many2ManyQuickCreate = instance.web.Widget.extend({ + template: 'Many2ManyKanban.quick_create', + + /** + * close_btn: If true, the widget will display a "Close" button able to trigger + * a "close" event. + */ + init: function(parent, dataset, context, buttons) { + this._super(parent); + this.m2m = this.getParent().view.m2m; + this.m2m.quick_create = this; + this._dataset = dataset; + this._buttons = buttons || false; + this._context = context || {}; + }, + start: function () { + var self = this; + self.$text = this.$el.find('input').css("width", "200px"); + self.$text.textext({ + plugins : 'arrow autocomplete', + autocomplete: { + render: function(suggestion) { + return $('<span class="text-label"/>'). + data('index', suggestion['index']).html(suggestion['label']); + } + }, + ext: { + autocomplete: { + selectFromDropdown: function() { + $(this).trigger('hideDropdown'); + var index = Number(this.selectedSuggestionElement().children().children().data('index')); + var data = self.search_result[index]; + if (data.id) { + self.add_id(data.id); + } else { + data.action(); + } + }, + }, + itemManager: { + itemToString: function(item) { + return item.name; + }, + }, + }, + }).bind('getSuggestions', function(e, data) { + var _this = this; + var str = !!data ? data.query || '' : ''; + self.m2m.get_search_result(str).then(function(result) { + self.search_result = result; + $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) { + return _.extend(el, {index:i}); + })}); + }); + }); + self.$text.focusout(function() { + self.$text.val(""); + }); + }, + focus: function() { + this.$text.focus(); + }, + add_id: function(id) { + var self = this; + self.$text.val(""); + self.trigger('added', id); + this.m2m.dataset_changed(); + }, }); +} /** * Class with everything which is common between FormOpenPopup and SelectCreatePopup. */ -instance.web.form.AbstractFormPopup = instance.web.OldWidget.extend({ +instance.web.form.AbstractFormPopup = instance.web.Widget.extend({ template: "AbstractFormPopup.render", /** * options: @@ -3371,12 +4334,15 @@ instance.web.form.AbstractFormPopup = instance.web.OldWidget.extend({ this.dataset.create_function = function(data, sup) { var fct = self.options.create_function || sup; return fct.call(this, data).then(function(r) { - self.created_elements.push(r.result); + self.trigger('create_completed saved', r); + self.created_elements.push(r); }); }; this.dataset.write_function = function(id, data, options, sup) { var fct = self.options.write_function || sup; - return fct.call(this, id, data, options).then(self.on_write_completed); + return fct.call(this, id, data, options).then(function() { + self.trigger('write_completed saved'); + }); }; this.dataset.parent_view = this.options.parent_view; this.dataset.child_name = this.options.child_name; @@ -3384,17 +4350,17 @@ instance.web.form.AbstractFormPopup = instance.web.OldWidget.extend({ display_popup: function() { var self = this; this.renderElement(); - new instance.web.Dialog(this, { - width: '90%', + var dialog = new instance.web.Dialog(this, { min_width: '800px', + dialogClass: 'oe_act_window', close: function() { - self.check_exit(); + self.check_exit(true); }, title: this.options.title || "", - }, this.$element).open(); + }, this.$el).open(); + this.$buttonpane = dialog.$buttons; this.start(); }, - on_write_completed: function() {}, setup_form_view: function() { var self = this; if (this.row_id) { @@ -3407,53 +4373,57 @@ instance.web.form.AbstractFormPopup = instance.web.OldWidget.extend({ if (this.row_id !== null) { options.initial_mode = this.options.readonly ? "view" : "edit"; } + _.extend(options, { + $buttons: this.$buttonpane, + }); this.view_form = new instance.web.FormView(this, this.dataset, false, options); if (this.options.alternative_form_view) { this.view_form.set_embedded_view(this.options.alternative_form_view); } - this.view_form.appendTo(this.$element.find(".oe-form-view-popup-form-placeholder")); - this.view_form.on_loaded.add_last(function() { - var $buttons = self.view_form.$element.find(".oe_form_buttons"); + this.view_form.appendTo(this.$el.find(".oe_popup_form")); + this.view_form.on("form_view_loaded", self, function() { var multi_select = self.row_id === null && ! self.options.disable_multiple_selection; - $buttons.html(QWeb.render("AbstractFormPopup.buttons", {multi_select: multi_select})); - var $snbutton = $buttons.find(".oe_abstractformpopup-form-save-new"); + self.$buttonpane.html(QWeb.render("AbstractFormPopup.buttons", { + multi_select: multi_select, + readonly: self.row_id !== null && self.options.readonly, + })); + var $snbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save-new"); $snbutton.click(function() { - $.when(self.view_form.do_save()).then(function() { + $.when(self.view_form.save()).then(function() { self.view_form.reload_mutex.exec(function() { self.view_form.on_button_new(); }); }); }); - var $sbutton = $buttons.find(".oe_abstractformpopup-form-save"); + var $sbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save"); $sbutton.click(function() { - $.when(self.view_form.do_save()).then(function() { + $.when(self.view_form.save()).then(function() { self.view_form.reload_mutex.exec(function() { self.check_exit(); }); }); }); - var $cbutton = $buttons.find(".oe_abstractformpopup-form-close"); + var $cbutton = self.$buttonpane.find(".oe_abstractformpopup-form-close"); $cbutton.click(function() { self.check_exit(); }); - if (self.row_id !== null && self.options.readonly) { - $snbutton.hide(); - $sbutton.hide(); - $cbutton.text(_t("Close")); - } self.view_form.do_show(); }); }, - on_select_elements: function(element_ids) { + select_elements: function(element_ids) { + this.trigger("elements_selected", element_ids); }, - check_exit: function() { + check_exit: function(no_destroy) { if (this.created_elements.length > 0) { - this.on_select_elements(this.created_elements); + this.select_elements(this.created_elements); + this.created_elements = []; } + this.trigger('closed'); this.destroy(); }, destroy: function () { - this.$element.dialog('close'); + this.trigger('closed'); + this.$el.dialog('close'); this._super(); }, }); @@ -3503,7 +4473,7 @@ instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend self.rpc('/web/session/eval_domain_and_context', { domains: [], contexts: [this.context] - }, function (results) { + }).then(function (results) { var search_defaults = {}; _.each(results.context, function (value_, key) { var match = /^search_default_(.*)$/.exec(key); @@ -3524,7 +4494,7 @@ instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend } this.searchview = new instance.web.SearchView(this, this.dataset, false, search_defaults); - this.searchview.on_search.add(function(domains, contexts, groupbys) { + this.searchview.on('search_data', self, function(domains, contexts, groupbys) { if (self.initial_ids) { self.do_search(domains.concat([[["id", "in", self.initial_ids]], self.domain]), contexts, groupbys); @@ -3533,36 +4503,41 @@ instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend self.do_search(domains.concat([self.domain]), contexts.concat(self.context), groupbys); } }); - this.searchview.on_loaded.add_last(function () { + this.searchview.on("search_view_loaded", self, function() { self.view_list = new instance.web.form.SelectCreateListView(self, self.dataset, false, _.extend({'deletable': false, - 'selectable': !self.options.disable_multiple_selection + 'selectable': !self.options.disable_multiple_selection, + 'import_enabled': false, + '$buttons': self.$buttonpane, }, self.options.list_view_options || {})); + self.view_list.on('edit:before', self, function (e) { + e.cancel = true; + }); self.view_list.popup = self; - self.view_list.appendTo($(".oe-select-create-popup-view-list", self.$element)).pipe(function() { + self.view_list.appendTo($(".oe_popup_list", self.$el)).pipe(function() { self.view_list.do_show(); }).pipe(function() { self.searchview.do_search(); }); - self.view_list.on_loaded.add_last(function() { - var $buttons = self.view_list.$element.find(".oe-actions"); - $buttons.prepend(QWeb.render("SelectCreatePopup.search.buttons")); - var $cbutton = $buttons.find(".oe_selectcreatepopup-search-close"); + self.view_list.on("list_view_loaded", self, function() { + self.$buttonpane.html(QWeb.render("SelectCreatePopup.search.buttons", {widget:self})); + var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-close"); $cbutton.click(function() { self.destroy(); }); - var $sbutton = $buttons.find(".oe_selectcreatepopup-search-select"); - if(self.options.disable_multiple_selection) { - $sbutton.hide(); - } + var $sbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-select"); $sbutton.click(function() { - self.on_select_elements(self.selected_ids); + self.select_elements(self.selected_ids); self.destroy(); }); + var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-create"); + $cbutton.click(function() { + self.new_object(); + }); }); }); - this.searchview.appendTo($(".oe-select-create-popup-view-list", self.$element)); + this.searchview.appendTo($(".oe_popup_search", self.$el)); }, do_search: function(domains, contexts, groupbys) { var self = this; @@ -3570,16 +4545,17 @@ instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend domains: domains || [], contexts: contexts || [], group_by_seq: groupbys || [] - }, function (results) { + }).then(function (results) { self.view_list.do_search(results.domain, results.context, results.group_by); }); }, on_click_element: function(ids) { + var self = this; this.selected_ids = ids || []; if(this.selected_ids.length > 0) { - this.$element.find(".oe_selectcreatepopup-search-select").removeAttr('disabled'); + self.$buttonpane.find(".oe_selectcreatepopup-search-select").removeAttr('disabled'); } else { - this.$element.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled"); + self.$buttonpane.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled"); } }, new_object: function() { @@ -3587,7 +4563,7 @@ instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend this.searchview.hide(); } if (this.view_list) { - this.view_list.$element.hide(); + this.view_list.$el.hide(); } this.setup_form_view(); }, @@ -3598,7 +4574,7 @@ instance.web.form.SelectCreateListView = instance.web.ListView.extend({ this.popup.new_object(); }, select_record: function(index) { - this.popup.on_select_elements([this.dataset.ids[index]]); + this.popup.select_elements([this.dataset.ids[index]]); this.popup.destroy(); }, do_select: function(ids, records) { @@ -3607,110 +4583,110 @@ instance.web.form.SelectCreateListView = instance.web.ListView.extend({ } }); -instance.web.form.FieldReference = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.ReinitializeFieldMixin, { +instance.web.form.FieldReference = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, { template: 'FieldReference', init: function(field_manager, node) { this._super(field_manager, node); this.reference_ready = true; }, - on_nop: function() { - }, - on_selection_changed: function() { - if (this.reference_ready) { - var sel = this.selection.get_value(); - this.m2o.field.relation = sel; - this.m2o.set_value(false); - this.m2o.$element.toggle(sel !== false); - } - }, destroy_content: function() { - if (this.selection) { - this.selection.destroy(); - this.selection = undefined; - } - if (this.m2o) { - this.m2o.destroy(); - this.m2o = undefined; + if (this.fm) { + this.fm.destroy(); + this.fm = undefined; } }, initialize_content: function() { - this.selection = new instance.web.form.FieldSelection(this, { attrs: { - name: 'selection' + var self = this; + var fm = new instance.web.form.DefaultFieldManager(this); + this.fm = fm; + fm.extend_field_desc({ + "selection": { + selection: this.field_manager.get_field_desc(this.name).selection, + type: "selection", + }, + "m2o": { + relation: null, + type: "many2one", + }, + }); + this.selection = new instance.web.form.FieldSelection(fm, { attrs: { + name: 'selection', + modifiers: JSON.stringify({readonly: this.get('effective_readonly')}), }}); - this.selection.view = this.view; - this.selection.set({force_readonly: this.get('effective_readonly')}); this.selection.on("change:value", this, this.on_selection_changed); - this.selection.$element = $(".oe_form_view_reference_selection", this.$element); - this.selection.renderElement(); - this.selection.start(); + this.selection.appendTo(this.$(".oe_form_view_reference_selection")); + this.selection + .on('focused', null, function () {self.trigger('focused')}) + .on('blurred', null, function () {self.trigger('blurred')}); - this.m2o = new instance.web.form.FieldMany2One(this, { attrs: { - name: 'm2o' + this.m2o = new instance.web.form.FieldMany2One(fm, { attrs: { + name: 'm2o', + modifiers: JSON.stringify({readonly: this.get('effective_readonly')}), }}); - this.m2o.view = this.view; - this.m2o.set({force_readonly: this.get("effective_readonly")}); this.m2o.on("change:value", this, this.data_changed); - this.m2o.$element = $(".oe_form_view_reference_m2o", this.$element); - this.m2o.renderElement(); - this.m2o.start(); + this.m2o.appendTo(this.$(".oe_form_view_reference_m2o")); + this.m2o + .on('focused', null, function () {self.trigger('focused')}) + .on('blurred', null, function () {self.trigger('blurred')}); }, - is_false: function() { - return typeof(this.get_value()) !== 'string'; + on_selection_changed: function() { + if (this.reference_ready) { + this.internal_set_value([this.selection.get_value(), false]); + this.render_value(); + } }, - set_value: function(value_) { - this._super(value_); - this.render_value(); + data_changed: function() { + if (this.reference_ready) { + this.internal_set_value([this.selection.get_value(), this.m2o.get_value()]); + } + }, + set_value: function(val) { + if (val) { + val = val.split(','); + val[0] = val[0] || false; + val[1] = val[0] ? (val[1] ? parseInt(val[1], 10) : val[1]) : false; + } + this._super(val || [false, false]); + }, + get_value: function() { + return this.get('value')[0] && this.get('value')[1] ? (this.get('value')[0] + ',' + this.get('value')[1]) : false; }, render_value: function() { this.reference_ready = false; - var vals = [], sel_val, m2o_val; - if (typeof(this.get('value')) === 'string') { - vals = this.get('value').split(','); - } - sel_val = vals[0] || false; - m2o_val = vals[1] ? parseInt(vals[1], 10) : vals[1]; if (!this.get("effective_readonly")) { - this.selection.set_value(sel_val); + this.selection.set_value(this.get('value')[0]); } - this.m2o.field.relation = sel_val; - this.m2o.set_value(m2o_val); + this.m2o.field.relation = this.get('value')[0]; + this.m2o.set_value(this.get('value')[1]); + this.m2o.$el.toggle(!!this.get('value')[0]); this.reference_ready = true; }, - data_changed: function() { - var model = this.selection.get_value(), - id = this.m2o.get_value(); - if (typeof(model) === 'string' && typeof(id) === 'number') { - this.set({'value': model + ',' + id}); - } else { - this.set({'value': false}); - } - }, - get_field: function(name) { - if (name === "selection") { - return { - selection: this.view.fields_view.fields[this.name].selection, - type: "selection", - }; - } else if (name === "m2o") { - return { - relation: null, - type: "many2one", - }; - } - throw Exception("Should not happen"); - }, -})); +}); -instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.ReinitializeFieldMixin, { +instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, { init: function(field_manager, node) { + var self = this; this._super(field_manager, node); - this.iframe = this.element_id + '_iframe'; this.binary_value = false; + this.useFileAPI = !!window.FileReader; + if (!this.useFileAPI) { + this.fileupload_id = _.uniqueId('oe_fileupload'); + $(window).on(this.fileupload_id, function() { + var args = [].slice.call(arguments).slice(1); + self.on_file_uploaded.apply(self, args); + }); + } + }, + stop: function() { + if (!this.useFileAPI) { + $(window).off(this.fileupload_id); + } + this._super.apply(this, arguments); }, initialize_content: function() { - this.$element.find('input.oe-binary-file').change(this.on_file_change); - this.$element.find('button.oe-binary-file-save').click(this.on_save_as); - this.$element.find('.oe-binary-file-clear').click(this.on_clear); + this.$el.find('input.oe_form_binary_file').change(this.on_file_change); + this.$el.find('button.oe_form_binary_file_save').click(this.on_save_as); + this.$el.find('.oe_form_binary_file_clear').click(this.on_clear); }, human_filesize : function(size) { var units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; @@ -3722,54 +4698,84 @@ instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(_.extend( return size.toFixed(2) + ' ' + units[i]; }, on_file_change: function(e) { - // TODO: on modern browsers, we could directly read the file locally on client ready to be used on image cropper - // http://www.html5rocks.com/tutorials/file/dndfiles/ - // http://deepliquid.com/projects/Jcrop/demos.php?demo=handler - window[this.iframe] = this.on_file_uploaded; - if ($(e.target).val() != '') { - this.$element.find('form.oe-binary-form input[name=session_id]').val(this.session.session_id); - this.$element.find('form.oe-binary-form').submit(); - this.$element.find('.oe-binary-progress').show(); - this.$element.find('.oe-binary').hide(); + var self = this; + var file_node = e.target; + if ((this.useFileAPI && file_node.files.length) || (!this.useFileAPI && $(file_node).val() !== '')) { + if (this.useFileAPI) { + var file = file_node.files[0]; + var filereader = new FileReader(); + filereader.readAsDataURL(file); + filereader.onloadend = function(upload) { + var data = upload.target.result; + data = data.split(',')[1]; + self.on_file_uploaded(file.size, file.name, file.type, data); + }; + } else { + this.$el.find('form.oe_form_binary_form input[name=session_id]').val(this.session.session_id); + this.$el.find('form.oe_form_binary_form').submit(); + } + this.$el.find('.oe_form_binary_progress').show(); + this.$el.find('.oe_form_binary').hide(); } }, on_file_uploaded: function(size, name, content_type, file_base64) { - delete(window[this.iframe]); if (size === false) { this.do_warn("File Upload", "There was a problem while uploading your file"); // TODO: use openerp web crashmanager console.warn("Error while uploading file : ", name); } else { + this.filename = name; this.on_file_uploaded_and_valid.apply(this, arguments); } - this.$element.find('.oe-binary-progress').hide(); - this.$element.find('.oe-binary').show(); + this.$el.find('.oe_form_binary_progress').hide(); + this.$el.find('.oe_form_binary').show(); }, on_file_uploaded_and_valid: function(size, name, content_type, file_base64) { }, - on_save_as: function() { - $.blockUI(); - this.session.get_file({ - url: '/web/binary/saveas_ajax', - data: {data: JSON.stringify({ - model: this.view.dataset.model, - id: (this.view.datarecord.id || ''), - field: this.name, - filename_field: (this.node.attrs.filename || ''), - context: this.view.dataset.get_context() - })}, - complete: $.unblockUI, - error: instance.webclient.crashmanager.on_rpc_error - }); + on_save_as: function(ev) { + var value = this.get('value'); + if (!value) { + this.do_warn(_t("Save As..."), _t("The field is empty, there's nothing to save !")); + ev.stopPropagation(); + } else if (this._dirty_flag) { + var link = this.$('.oe_form_binary_file_save_data')[0]; + link.download = this.filename || "download.bin"; // Works on only on Google Chrome + //link.target = '_blank'; + link.href = "data:application/octet-stream;base64," + value; + } else { + instance.web.blockUI(); + this.session.get_file({ + url: '/web/binary/saveas_ajax', + data: {data: JSON.stringify({ + model: this.view.dataset.model, + id: (this.view.datarecord.id || ''), + field: this.name, + filename_field: (this.node.attrs.filename || ''), + context: this.view.dataset.get_context() + })}, + complete: instance.web.unblockUI, + error: instance.webclient.crashmanager.on_rpc_error + }); + ev.stopPropagation(); + return false; + } + }, + set_filename: function(value) { + var filename = this.node.attrs.filename; + if (filename) { + var tmp = {}; + tmp[filename] = value; + this.field_manager.set_values(tmp); + } }, on_clear: function() { if (this.get('value') !== false) { this.binary_value = false; - this.set({'value': false}); + this.internal_set_value(false); } return false; } -})); +}); instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({ template: 'FieldBinaryFile', @@ -3777,18 +4783,14 @@ instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({ this._super(); if (this.get("effective_readonly")) { var self = this; - this.$element.find('a').click(function() { + this.$el.find('a').click(function(ev) { if (self.get('value')) { - self.on_save_as(); + self.on_save_as(ev); } return false; }); } }, - set_value: function(value_) { - this._super.apply(this, arguments); - this.render_value(); - }, render_value: function() { if (!this.get("effective_readonly")) { var show_value; @@ -3797,146 +4799,334 @@ instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({ } else { show_value = (this.get('value') != null && this.get('value') !== false) ? this.get('value') : ''; } - this.$element.find('input').eq(0).val(show_value); + this.$el.find('input').eq(0).val(show_value); } else { - this.$element.find('a').show(!!this.get('value')); + this.$el.find('a').show(!!this.get('value')); if (this.get('value')) { - var show_value = _t("Download") + " " + (this.view.datarecord[this.node.attrs.filename] || ''); - this.$element.find('a').text(show_value); + var show_value = _t("Download") + if (this.view) + show_value += " " + (this.view.datarecord[this.node.attrs.filename] || ''); + this.$el.find('a').text(show_value); } } }, on_file_uploaded_and_valid: function(size, name, content_type, file_base64) { this.binary_value = true; - this.set({'value': file_base64}); + this.internal_set_value(file_base64); var show_value = name + " (" + this.human_filesize(size) + ")"; - this.$element.find('input').eq(0).val(show_value); + this.$el.find('input').eq(0).val(show_value); this.set_filename(name); }, - set_filename: function(value_) { - var filename = this.node.attrs.filename; - if (this.view.fields[filename]) { - this.view.fields[filename].set({value: value_}); - } - }, on_clear: function() { this._super.apply(this, arguments); - this.$element.find('input').eq(0).val(''); + this.$el.find('input').eq(0).val(''); this.set_filename(''); } }); instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({ template: 'FieldBinaryImage', - initialize_content: function() { - this._super(); - this.$placeholder = $(".oe_form_field-binary-image-placeholder", this.$element); - if (!this.get("effective_readonly")) - this.$element.find('.oe-binary').show(); - else - this.$element.find('.oe-binary').hide(); - }, - set_value: function(value_) { - this._super.apply(this, arguments); - this.render_value(); - }, + placeholder: "/web/static/src/img/placeholder.png", render_value: function() { + var self = this; var url; - if (this.get('value') && this.get('value').substr(0, 10).indexOf(' ') == -1) { + if (this.get('value') && ! /^\d+(\.\d*)? \w+$/.test(this.get('value'))) { url = 'data:image/png;base64,' + this.get('value'); } else if (this.get('value')) { + var id = escape(JSON.stringify(this.view.datarecord.id || null)); + var field = this.name; + if (this.options.preview_image) + field = this.options.preview_image; url = '/web/binary/image?session_id=' + this.session.session_id + '&model=' + - this.view.dataset.model +'&id=' + (this.view.datarecord.id || '') + '&field=' + this.name + '&t=' + (new Date().getTime()); + this.view.dataset.model +'&id=' + id + '&field=' + field + '&t=' + (new Date().getTime()); } else { - url = "/web/static/src/img/placeholder.png"; + url = this.placeholder; } - var rendered = QWeb.render("FieldBinaryImage-img", {widget: this, url: url});; - this.$placeholder.html(rendered); + var $img = $(QWeb.render("FieldBinaryImage-img", { widget: this, url: url })); + this.$el.find('> img').remove(); + this.$el.prepend($img); + $img.load(function() { + if (! self.options.size) + return; + $img.css("max-width", "" + self.options.size[0] + "px"); + $img.css("max-height", "" + self.options.size[1] + "px"); + $img.css("margin-left", "" + (self.options.size[0] - $img.width()) / 2 + "px"); + $img.css("margin-top", "" + (self.options.size[1] - $img.height()) / 2 + "px"); + }); + $img.on('error', function() { + $img.attr('src', self.placeholder); + instance.webclient.notification.warn(_t("Image"), _t("Could not display the selected image.")); + }); }, on_file_change: function() { this.render_value(); this._super.apply(this, arguments); }, on_file_uploaded_and_valid: function(size, name, content_type, file_base64) { - this.set({'value': file_base64}); + this.internal_set_value(file_base64); this.binary_value = true; this.render_value(); + this.set_filename(name); }, on_clear: function() { this._super.apply(this, arguments); this.render_value(); + this.set_filename(''); } }); +/** + * Widget for (one2many field) to upload one or more file in same time and display in list. + * The user can delete his files. + * Options on attribute ; "blockui" {Boolean} block the UI or not + * during the file is uploading + */ +instance.web.form.FieldOne2ManyBinaryMultiFiles = instance.web.form.AbstractField.extend({ + template: "FieldBinaryFileUploader", + init: function(field_manager, node) { + this._super(field_manager, node); + this.field_manager = field_manager; + this.node = node; + if(this.field.type != "one2many" || this.field.relation != 'ir.attachment') { + throw "The type of the field '"+this.field.string+"' must be a one2many field with a relation to 'ir.attachment' model."; + } + this.ds_file = new instance.web.DataSetSearch(this, 'ir.attachment'); + this.fileupload_id = _.uniqueId('oe_fileupload_temp'); + $(window).on(this.fileupload_id, _.bind(this.on_file_loaded, this)); + }, + start: function() { + this._super(this); + this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change ); + }, + get_value: function() { + return _.map(this.get('value'), function (value) { return commands.link_to( value.id ); }); + }, + get_file_url: function (attachment) { + return instance.origin + '/web/binary/saveas?session_id=' + this.session.session_id + '&model=ir.attachment&field=datas&filename_field=datas_fname&id=' + attachment['id']; + }, + render_value: function () { + var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': this})); + render.on('click', '.oe_delete', _.bind(this.on_file_delete, this)); + this.$('.oe_placeholder_files, .oe_attachments').replaceWith( render ); + + // reinit input type file + var $input = this.$('input.oe_form_binary_file'); + $input.after($input.clone(true)).remove(); + this.$(".oe_fileupload").show(); + }, + on_file_change: function (event) { + event.stopPropagation(); + var self = this; + var $target = $(event.target); + if ($target.val() !== '') { + + var filename = $target.val().replace(/.*[\\\/]/,''); + + // if the files is currently uploded, don't send again + if( !isNaN(_.find(this.get('value'), function (file) { return (file.filename || file.name) == filename && file.upload; } )) ) { + return false; + } + + // block UI or not + if(this.node.attrs.blockui) { + instance.web.blockUI(); + } + + // if the files exits for this answer, delete the file before upload + var files = _.filter(this.get('value'), function (file) { + if((file.filename || file.name) == filename) { + self.ds_file.unlink([file.id]); + return false; + } else { + return true; + } + }); + + // TODO : unactivate send on wizard and form + + // submit file + this.$('form.oe_form_binary_form').submit(); + this.$(".oe_fileupload").hide(); + + // add file on result + files.push({ + 'id': 0, + 'name': filename, + 'filename': filename, + 'url': '', + 'upload': true + }); + + this.set({'value': files}); + } + }, + on_file_loaded: function (event, result) { + // unblock UI + if(this.node.attrs.blockui) { + instance.web.unblockUI(); + } + + // TODO : activate send on wizard and form + + var files = this.get('value'); + for(var i in files){ + if(files[i].filename == result.filename && files[i].upload) { + files[i] = { + 'id': result.id, + 'name': result.name, + 'filename': result.filename, + 'url': this.get_file_url(result) + }; + } + } + + this.set({'value': files}); + this.render_value() + }, + on_file_delete: function (event) { + event.stopPropagation(); + var file_id=$(event.target).data("id"); + if (file_id) { + var files=[]; + for(var i in this.get('value')){ + if(file_id != this.get('value')[i].id){ + files.push(this.get('value')[i]); + } + else { + this.ds_file.unlink([file_id]); + } + } + this.set({'value': files}); + } + }, +}); + instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({ - template: "EmptyComponent", + template: "FieldStatus", + init: function(field_manager, node) { + this._super(field_manager, node); + this.options.clickable = this.options.clickable || (this.node.attrs || {}).clickable || false; + this.options.visible = this.options.visible || (this.node.attrs || {}).statusbar_visible || false; + this.set({value: false}); + }, start: function() { + if (this.options.clickable) { + this.$el.on('click','li',this.on_click_stage); + } + if (this.$el.parent().is('header')) { + this.$el.after('<div class="oe_clear"/>'); + } this._super(); - this.selected_value = null; - - this.render_list(); }, set_value: function(value_) { + if (value_ instanceof Array) { + value_ = value_[0]; + } this._super(value_); - this.selected_value = value_; - - this.render_list(); }, - render_list: function() { + render_value: function() { var self = this; - var shown = _.map(((this.node.attrs || {}).statusbar_visible || "").split(","), - function(x) { return _.str.trim(x); }); - shown = _.select(shown, function(x) { return x.length > 0; }); - - if (shown.length == 0) { - this.to_show = this.field.selection; + self.get_selection().then(function() { + var content = QWeb.render("FieldStatus.content", {widget: self}); + self.$el.html(content); + var colors = JSON.parse((self.node.attrs || {}).statusbar_colors || "{}"); + var color = colors[self.get('value')]; + if (color) { + self.$("oe_active").css("color", color); + } + }); + }, + /** Get the selection and render it + * selection: [[identifier, value_to_display], ...] + * For selection fields: this is directly given by this.field.selection + * For many2one fields: perform a search on the relation of the many2one field + */ + get_selection: function() { + var self = this; + self.selection = []; + if (this.field.type == "many2one") { + var domain = []; + if(!_.isEmpty(this.field.domain) || !_.isEmpty(this.node.attrs.domain)) { + domain = new instance.web.CompoundDomain(['|'], self.build_domain(), [['id', '=', self.get('value')]]); + } + var ds = new instance.web.DataSetSearch(this, this.field.relation, self.build_context(), domain); + return ds.read_slice(['name'], {}).pipe(function (records) { + for(var i = 0; i < records.length; i++) { + self.selection.push([records[i].id, records[i].name]); + } + }); } else { - this.to_show = _.select(this.field.selection, function(x) { - return _.indexOf(shown, x[0]) !== -1 || x[0] === self.selected_value; + // For field type selection filter values according to + // statusbar_visible attribute of the field. For example: + // statusbar_visible="draft,open". + var selection = this.field.selection; + for(var i=0; i < selection.length; i++) { + var key = selection[i][0]; + if(key == this.get('value') || !this.options.visible || this.options.visible.indexOf(key) != -1) { + this.selection.push(selection[i]); + } + } + return $.when(); + } + }, + on_click_stage: function (ev) { + var self = this; + var $li = $(ev.currentTarget); + var val = parseInt($li.data("id")); + if (val != self.get('value')) { + this.view.recursive_save().then(function() { + var change = {}; + change[self.name] = val; + self.view.dataset.write(self.view.datarecord.id, change).then(function() { + self.view.reload(); + }); }); } + }, +}); - var content = instance.web.qweb.render("FieldStatus.content", {widget: this, _:_}); - this.$element.html(content); - - var colors = JSON.parse((this.node.attrs || {}).statusbar_colors || "{}"); - var color = colors[this.selected_value]; - if (color) { - var elem = this.$element.find("li.oe-arrow-list-selected span"); - elem.css("border-color", color); - if (this.check_white(color)) - elem.css("color", "white"); - elem = this.$element.find("li.oe-arrow-list-selected .oe-arrow-list-before"); - elem.css("border-left-color", "rgba(0,0,0,0)"); - elem = this.$element.find("li.oe-arrow-list-selected .oe-arrow-list-after"); - elem.css("border-color", "rgba(0,0,0,0)"); - elem.css("border-left-color", color); - } - }, - check_white: function(color) { - var div = $("<div></div>"); - div.css("display", "none"); - div.css("color", color); - div.appendTo($("body")); - var ncolor = div.css("color"); - div.remove(); - var res = /^\s*rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)\s*$/.exec(ncolor); - if (!res) { - return false; +instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({ + template: "FieldMonetary", + init: function() { + this._super.apply(this, arguments); + this.set({"currency": false}); + if (this.options.currency_field) { + this.field_manager.on("field_changed:" + this.options.currency_field, this, function() { + this.set({"currency": this.field_manager.get_field_value(this.options.currency_field)}); + }); } - var comps = [parseInt(res[1]), parseInt(res[2]), parseInt(res[3])]; - var lum = comps[0] * 0.3 + comps[1] * 0.59 + comps[1] * 0.11; - if (lum < 128) { - return true; + this.on("change:currency", this, this.get_currency_info); + this.get_currency_info(); + this.ci_dm = new instance.web.DropMisordered(); + }, + start: function() { + var tmp = this._super(); + this.on("change:currency_info", this, this.reinitialize); + return tmp; + }, + get_currency_info: function() { + var self = this; + if (this.get("currency") === false) { + this.set({"currency_info": null}); + return; } - return false; - } + return this.ci_dm.add(new instance.web.Model("res.currency").query(["symbol", "position"]) + .filter([["id", "=", self.get("currency")]]).first()).pipe(function(res) { + self.set({"currency_info": res}); + }); + }, + parse_value: function(val, def) { + return instance.web.parse_value(val, {type: "float"}, def); + }, + format_value: function(val, def) { + return instance.web.format_value(val, {type: "float"}, def); + }, }); /** * Registry of form fields, called by :js:`instance.web.FormView`. * - * All referenced classes must implement FieldMixin. Those represent the classes whose instances + * All referenced classes must implement FieldInterface. Those represent the classes whose instances * will substitute to the <field> tags as defined in OpenERP's views. */ instance.web.form.widgets = new instance.web.Registry({ @@ -3945,12 +5135,14 @@ instance.web.form.widgets = new instance.web.Registry({ 'email' : 'instance.web.form.FieldEmail', 'url' : 'instance.web.form.FieldUrl', 'text' : 'instance.web.form.FieldText', + 'html' : 'instance.web.form.FieldTextHtml', 'date' : 'instance.web.form.FieldDate', 'datetime' : 'instance.web.form.FieldDatetime', 'selection' : 'instance.web.form.FieldSelection', 'many2one' : 'instance.web.form.FieldMany2One', 'many2many' : 'instance.web.form.FieldMany2Many', - 'many2manytags' : 'instance.web.form.FieldMany2ManyTags', + 'many2many_tags' : 'instance.web.form.FieldMany2ManyTags', + 'many2many_kanban' : 'instance.web.form.FieldMany2ManyKanban', 'one2many' : 'instance.web.form.FieldOne2Many', 'one2many_list' : 'instance.web.form.FieldOne2Many', 'reference' : 'instance.web.form.FieldReference', @@ -3961,7 +5153,9 @@ instance.web.form.widgets = new instance.web.Registry({ 'progressbar': 'instance.web.form.FieldProgressBar', 'image': 'instance.web.form.FieldBinaryImage', 'binary': 'instance.web.form.FieldBinaryFile', - 'statusbar': 'instance.web.form.FieldStatus' + 'one2many_binary': 'instance.web.form.FieldOne2ManyBinaryMultiFiles', + 'statusbar': 'instance.web.form.FieldStatus', + 'monetary': 'instance.web.form.FieldMonetary', }); /** @@ -3974,6 +5168,9 @@ instance.web.form.tags = new instance.web.Registry({ 'button' : 'instance.web.form.WidgetButton', }); +instance.web.form.custom_widgets = new instance.web.Registry({ +}); + }; // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: diff --git a/addons/web/static/src/js/view_list.js b/addons/web/static/src/js/view_list.js index 67c8ce0239e..ab50ae933d9 100644 --- a/addons/web/static/src/js/view_list.js +++ b/addons/web/static/src/js/view_list.js @@ -22,6 +22,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi 'reorderable': true, 'action_buttons': true, }, + view_type: 'tree', /** * Core class for list-type displays. * @@ -61,12 +62,12 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi this.records = new Collection(); - this.set_groups(new instance.web.ListView.Groups(this)); + this.set_groups(new (this.options.GroupsType)(this)); if (this.dataset instanceof instance.web.DataSetStatic) { - this.groups.datagroup = new instance.web.StaticDataGroup(this.dataset); + this.groups.datagroup = new StaticDataGroup(this.dataset); } else { - this.groups.datagroup = new instance.web.DataGroup( + this.groups.datagroup = new DataGroup( this, this.model, dataset.get_domain(), dataset.get_context()); @@ -83,7 +84,17 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi }); this.no_leaf = false; + this.grouped = false; + this.on('view_loaded', self, self.load_list); + }, + set_default_options: function (options) { + this._super(options); + _.defaults(this.options, { + GroupsType: instance.web.ListView.Groups, + ListType: instance.web.ListView.List + }); }, + /** * Retrieves the view's number of records per page (|| section) * @@ -129,14 +140,14 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi }); }, /** - * View startup method, the default behavior is to set the ``oe-listview`` + * View startup method, the default behavior is to set the ``oe_list`` * class on its root element and to perform an RPC load call. * * @returns {$.Deferred} loading promise */ start: function() { - this.$element.addClass('oe-listview'); - return this.reload_view(null, null, true); + this.$el.addClass('oe_list'); + return this._super(); }, /** * Returns the style for the provided record in the current view (from the @@ -210,7 +221,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi * @param {Object} data.fields_view.arch current list view descriptor * @param {Boolean} grouped Is the list view grouped */ - on_loaded: function(data, grouped) { + load_list: function(data) { var self = this; this.fields_view = data; this.name = "" + this.fields_view.arch.attrs.string; @@ -236,21 +247,23 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi }).value(); } - this.setup_columns(this.fields_view.fields, grouped); + this.setup_columns(this.fields_view.fields, this.grouped); + + this.$el.html(QWeb.render(this._template, this)); + this.$el.addClass(this.fields_view.arch.attrs['class']); - this.$element.html(QWeb.render(this._template, this)); // Head hook // Selecting records - this.$element.find('.all-record-selector').click(function(){ - self.$element.find('.oe-record-selector input').prop('checked', - self.$element.find('.all-record-selector').prop('checked') || false); + this.$el.find('.oe_list_record_selector').click(function(){ + self.$el.find('.oe_list_record_selector input').prop('checked', + self.$el.find('.oe_list_record_selector').prop('checked') || false); var selection = self.groups.get_selection(); $(self.groups).trigger( 'selected', [selection.ids, selection.records]); }); // Sorting columns - this.$element.find('thead').delegate('th.oe-sortable[data-id]', 'click', function (e) { + this.$el.find('thead').delegate('th.oe_sortable[data-id]', 'click', function (e) { e.stopPropagation(); var $this = $(this); self.dataset.sort($this.data('id')); @@ -259,26 +272,22 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi } else { $this.toggleClass("sortdown"); } - $this.siblings('.oe-sortable').removeClass("sortup sortdown"); + $this.siblings('.oe_sortable').removeClass("sortup sortdown"); self.reload_content(); }); - // Add button and Import link + // Add button if (!this.$buttons) { this.$buttons = $(QWeb.render("ListView.buttons", {'widget':self})); if (this.options.$buttons) { this.$buttons.appendTo(this.options.$buttons); } else { - this.$element.find('.oe_list_buttons').replaceWith(this.$buttons); + this.$el.find('.oe_list_buttons').replaceWith(this.$buttons); } this.$buttons.find('.oe_list_add') .click(this.proxy('do_add_record')) - .prop('disabled', grouped && this.options.editable); - this.$buttons.on('click', '.oe_list_button_import', function() { - self.on_sidebar_import(); - return false; - }); + .prop('disabled', this.grouped); } // Pager @@ -287,7 +296,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi if (this.options.$buttons) { this.$pager.appendTo(this.options.$pager); } else { - this.$element.find('.oe_list_pager').replaceWith(this.$pager); + this.$el.find('.oe_list_pager').replaceWith(this.$pager); } this.$pager @@ -311,7 +320,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi self.page = 0; } self.reload_content(); - }).find('.oe-pager-state') + }).find('.oe_list_pager_state') .click(function (e) { e.stopPropagation(); var $this = $(this); @@ -329,6 +338,8 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi self._limit = (isNaN(val) ? null : val); self.page = 0; self.reload_content(); + }).blur(function() { + $(this).trigger('change'); }) .val(self._limit || 'NaN'); }); @@ -338,11 +349,14 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi if (!this.sidebar && this.options.$sidebar) { this.sidebar = new instance.web.Sidebar(this); this.sidebar.appendTo(this.options.$sidebar); - this.sidebar.add_items('other', [ - { label: _t('Delete'), callback: this.do_delete_selected }, - ]); + this.sidebar.add_items('other', _.compact([ + { label: _t("Export"), callback: this.on_sidebar_export }, + self.is_action_enabled('delete') && { label: _t('Delete'), callback: this.do_delete_selected } + ])); this.sidebar.add_toolbar(this.fields_view.toolbar); + this.sidebar.$el.hide(); } + this.trigger('list_view_loaded', data, this.grouped); }, /** * Configures the ListView pager based on the provided dataset's information @@ -359,17 +373,23 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi } var total = dataset.size(); + var limit = this.limit() || total; + if (total == 0) + this.$pager.hide(); + else + this.$pager.css("display", ""); + this.$pager.toggleClass('oe_list_pager_single_page', (total <= limit)); var spager = '-'; if (total) { - var range_start = this.page * this.limit() + 1; - var range_stop = range_start - 1 + this.limit(); + var range_start = this.page * limit + 1; + var range_stop = range_start - 1 + limit; if (range_stop > total) { range_stop = total; } spager = _.str.sprintf('%d-%d of %d', range_start, range_stop, total); } - this.$pager.find('.oe-pager-state').text(spager); + this.$pager.find('.oe_list_pager_state').text(spager); }, /** * Sets up the listview's columns: merges view and fields data, move @@ -380,68 +400,23 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi * @param {Boolean} [grouped] Should the grouping columns (group and count) be displayed */ setup_columns: function (fields, grouped) { - var domain_computer = instance.web.form.compute_domain; - - var noop = function () { return {}; }; - var field_to_column = function (field) { - var name = field.attrs.name; - var column = _.extend({id: name, tag: field.tag}, - fields[name], field.attrs); - // modifiers computer - if (column.modifiers) { - var modifiers = JSON.parse(column.modifiers); - column.modifiers_for = function (fields) { - if (!modifiers.invisible) { - return {}; - } - return { - 'invisible': domain_computer(modifiers.invisible, fields) - }; - }; - if (modifiers['tree_invisible']) { - column.invisible = '1'; - } else { - delete column.invisible; - } - } else { - column.modifiers_for = noop; - } - return column; - }; - + var registry = instance.web.list.columns; this.columns.splice(0, this.columns.length); - this.columns.push.apply( - this.columns, - _(this.fields_view.arch.children).map(field_to_column)); + this.columns.push.apply(this.columns, + _(this.fields_view.arch.children).map(function (field) { + var id = field.attrs.name; + return registry.for_(id, fields[id], field); + })); if (grouped) { - this.columns.unshift({ - id: '_group', tag: '', string: _t("Group"), meta: true, - modifiers_for: function () { return {}; } - }, { - id: '_count', tag: '', string: '#', meta: true, - modifiers_for: function () { return {}; } - }); + this.columns.unshift( + new instance.web.list.MetaColumn('_group', _t("Group"))); } this.visible_columns = _.filter(this.columns, function (column) { return column.invisible !== '1'; }); - this.aggregate_columns = _(this.visible_columns) - .map(function (column) { - if (column.type !== 'integer' && column.type !== 'float') { - return {}; - } - var aggregation_func = column['group_operator'] || 'sum'; - if (!(aggregation_func in column)) { - return {}; - } - - return _.extend({}, column, { - 'function': aggregation_func, - label: column[aggregation_func] - }); - }); + this.aggregate_columns = _(this.visible_columns).invoke('to_aggregate'); }, /** * Used to handle a click on a table row, if no other handler caught the @@ -467,9 +442,6 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi }, do_show: function () { this._super(); - if (this.sidebar) { - this.sidebar.$element.show(); - } if (this.$buttons) { this.$buttons.show(); } @@ -479,7 +451,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi }, do_hide: function () { if (this.sidebar) { - this.sidebar.$element.hide(); + this.sidebar.$el.hide(); } if (this.$buttons) { this.$buttons.hide(); @@ -492,25 +464,12 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi /** * Reloads the list view based on the current settings (dataset & al) * + * @deprecated * @param {Boolean} [grouped] Should the list be displayed grouped * @param {Object} [context] context to send the server while loading the view */ reload_view: function (grouped, context, initial) { - var self = this; - var callback = function (field_view_get) { - self.on_loaded(field_view_get, grouped); - }; - if (this.embedded_view) { - return $.Deferred().then(callback).resolve(this.embedded_view); - } else { - return this.rpc('/web/listview/load', { - model: this.model, - view_id: this.view_id, - view_type: "tree", - context: this.dataset.get_context(context), - toolbar: !!this.options.$sidebar - }, callback); - } + return this.load_view(context); }, /** * re-renders the content of the list view @@ -519,10 +478,10 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi */ reload_content: function () { var self = this; - self.$element.find('.all-record-selector').prop('checked', false); + self.$el.find('.oe_list_record_selector').prop('checked', false); this.records.reset(); var reloaded = $.Deferred(); - this.$element.find('.oe-listview-content').append( + this.$el.find('.oe_list_content').append( this.groups.render(function () { if (self.dataset.index == null) { var has_one = false; @@ -543,6 +502,20 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi reload: function () { return this.reload_content(); }, + reload_record: function (record) { + return this.dataset.read_ids( + [record.get('id')], + _.pluck(_(this.columns).filter(function (r) { + return r.tag === 'field'; + }), 'name') + ).then(function (records) { + _(records[0]).each(function (value, key) { + record.set(key, value, {silent: true}); + }); + record.trigger('change', record); + }); + }, + do_load_state: function(state, warm) { var reload = false; if (state.page && this.page !== state.page) { @@ -570,7 +543,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi */ do_search: function (domain, context, group_by) { this.page = 0; - this.groups.datagroup = new instance.web.DataGroup( + this.groups.datagroup = new DataGroup( this, this.model, domain, context, group_by); this.groups.datagroup.sort = this.dataset._sort; @@ -578,8 +551,9 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi group_by = null; } this.no_leaf = !!context['group_by_no_leaf']; + this.grouped = !!group_by; - this.reload_view(!!group_by, context).then( + return this.load_view(context).pipe( this.proxy('reload_content')); }, /** @@ -610,7 +584,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi if (!ids.length) { this.dataset.index = 0; if (this.sidebar) { - this.sidebar.$element.hide(); + this.sidebar.$el.hide(); } this.compute_aggregates(); return; @@ -618,7 +592,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi this.dataset.index = _(this.dataset.ids).indexOf(ids[0]); if (this.sidebar) { - this.sidebar.$element.show(); + this.sidebar.$el.show(); } this.compute_aggregates(_(records).map(function (record) { @@ -633,6 +607,23 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi * @param {Function} callback should be called after the action is executed, if non-null */ do_button_action: function (name, id, callback) { + this.handle_button(name, id, callback); + }, + /** + * Base handling of buttons, can be called when overriding do_button_action + * in order to bypass parent overrides. + * + * The callback will be provided with the ``id`` as its parameter, in case + * handle_button's caller had to alter the ``id`` (or even create one) + * while not being ``callback``'s creator. + * + * This method should not be overridden. + * + * @param {String} name action name + * @param {Object} id id of the record the action should be called on + * @param {Function} callback should be called after the action is executed, if non-null + */ + handle_button: function (name, id, callback) { var action = _.detect(this.columns, function (field) { return field.name === name; }); @@ -651,7 +642,8 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi c.add(action.context); } action.context = c; - this.do_execute_action(action, this.dataset, id, callback); + this.do_execute_action( + action, this.dataset, id, _.bind(callback, null, id)); }, /** * Handles the activation of a record (clicking on it) @@ -760,16 +752,14 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi }, display_aggregates: function (aggregation) { var self = this; - var $footer_cells = this.$element.find('.oe-list-footer'); + var $footer_cells = this.$el.find('.oe_list_footer'); _(this.aggregate_columns).each(function (column) { if (!column['function']) { return; } $footer_cells.filter(_.str.sprintf('[data-field=%s]', column.id)) - .html(instance.web.format_cell(aggregation, column, { - process_modifiers: false - })); + .html(column.format(aggregation, { process_modifiers: false })); }); }, get_selected_ids: function() { @@ -788,7 +778,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi pad_columns: function (count, options) { options = options || {}; // padding for action/pager header - var $first_header = this.$element.find('thead tr:first th'); + var $first_header = this.$el.find('thead tr:first th'); var colspan = $first_header.attr('colspan'); if (colspan) { if (!this.previous_colspan) { @@ -797,10 +787,10 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi $first_header.attr('colspan', parseInt(colspan, 10) + count); } // Padding for column titles, footer and data rows - var $rows = this.$element - .find('.oe-listview-header-columns, tr:not(thead tr)') + var $rows = this.$el + .find('.oe_list_header_columns, tr:not(thead tr)') .not(options['except']); - var newcols = new Array(count+1).join('<td class="oe-listview-padding"></td>'); + var newcols = new Array(count+1).join('<td class="oe_list_padding"></td>'); if (options.position === 'before') { $rows.prepend(newcols); } else { @@ -811,27 +801,29 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi * Removes all padding columns of the table */ unpad_columns: function () { - this.$element.find('.oe-listview-padding').remove(); + this.$el.find('.oe_list_padding').remove(); if (this.previous_colspan) { - this.$element + this.$el .find('thead tr:first th') .attr('colspan', this.previous_colspan); this.previous_colspan = null; } }, no_result: function () { - this.$element.find('.oe_view_nocontent').remove(); + this.$el.find('.oe_view_nocontent').remove(); if (this.groups.group_by || !this.options.action || !this.options.action.help) { return; } - this.$element.find('table:first').hide(); - this.$element.prepend( - $('<div class="oe_view_nocontent">') - .append($('<img>', { src: '/web/static/src/img/view_empty_arrow.png' })) - .append($('<div>').html(this.options.action.help)) + this.$el.find('table:first').hide(); + this.$el.prepend( + $('<div class="oe_view_nocontent">').html(this.options.action.help) ); + var create_nocontent = this.$buttons; + this.$el.find('.oe_view_nocontent').click(function() { + create_nocontent.effect('bounce', {distance: 18, times: 5}, 150); + }); } }); instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.ListView.List# */{ @@ -877,52 +869,63 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web. this.record_callbacks = { 'remove': function (event, record) { - var $row = self.$current.find( - '[data-id=' + record.get('id') + ']'); + var $row = self.$current.children( + '[data-id=' + record.get('id') + ']'); var index = $row.data('index'); $row.remove(); - self.refresh_zebra(index); }, 'reset': function () { return self.on_records_reset(); }, - 'change': function (event, record) { - var $row = self.$current.find('[data-id=' + record.get('id') + ']'); + 'change': function (event, record, attribute, value, old_value) { + var $row; + if (attribute === 'id') { + if (old_value) { + throw new Error("Setting 'id' attribute on existing record " + + JSON.stringify(record.attributes)); + } + if (!_.contains(self.dataset.ids, value)) { + // add record to dataset if not already in (added by + // the form view?) + self.dataset.ids.splice( + self.records.indexOf(record), 0, value); + } + // Set id on new record + $row = self.$current.children('[data-id=false]'); + } else { + $row = self.$current.children( + '[data-id=' + record.get('id') + ']'); + } $row.replaceWith(self.render_record(record)); }, 'add': function (ev, records, record, index) { - var $new_row = $('<tr>').attr({ - 'data-id': record.get('id') - }); + var $new_row = $(self.render_record(record)); if (index === 0) { $new_row.prependTo(self.$current); } else { var previous_record = records.at(index-1), - $previous_sibling = self.$current.find( + $previous_sibling = self.$current.children( '[data-id=' + previous_record.get('id') + ']'); $new_row.insertAfter($previous_sibling); } - - self.refresh_zebra(index, 1); } }; _(this.record_callbacks).each(function (callback, event) { this.records.bind(event, callback); }, this); - this.$_element = $('<tbody>') - .appendTo(document.body) - .delegate('th.oe-record-selector', 'click', function (e) { + this.$current = $('<tbody>') + .delegate('th.oe_list_record_selector', 'click', function (e) { e.stopPropagation(); var selection = self.get_selection(); $(self).trigger( 'selected', [selection.ids, selection.records]); }) - .delegate('td.oe-record-delete button', 'click', function (e) { + .delegate('td.oe_list_record_delete button', 'click', function (e) { e.stopPropagation(); var $row = $(e.target).closest('tr'); $(self).trigger('deleted', [[self.row_id($row)]]); }) - .delegate('td.oe-field-cell button', 'click', function (e) { + .delegate('td.oe_list_field_cell button', 'click', function (e) { e.stopPropagation(); var $target = $(e.currentTarget), field = $target.closest('td').data('field'), @@ -933,19 +936,19 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web. // of digits, nice when storing actual numbers, not nice when // storing strings composed only of digits. Force the action // name to be a string - $(self).trigger('action', [field.toString(), record_id, function () { - return self.reload_record(self.records.get(record_id)); + $(self).trigger('action', [field.toString(), record_id, function (id) { + return self.reload_record(self.records.get(id)); }]); }) .delegate('a', 'click', function (e) { e.stopPropagation(); }) .delegate('tr', 'click', function (e) { - e.stopPropagation(); var row_id = self.row_id(e.currentTarget); - if (row_id !== undefined) { + if (row_id) { + e.stopPropagation(); if (!self.dataset.select_id(row_id)) { - throw "Could not find id in dataset" + throw new Error("Could not find id in dataset"); } self.row_clicked(e); } @@ -967,13 +970,13 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web. // set) a human-readable version. m2o does not have this issue // because the non-human-readable is just a number, where the // human-readable version is a pair - if (value && (ref_match = /([\w\.]+),(\d+)/.exec(value))) { + if (value && (ref_match = /^([\w\.]+),(\d+)$/.exec(value))) { // reference values are in the shape "$model,$id" (as a // string), we need to split and name_get this pair in order // to get a correctly displayable value in the field var model = ref_match[1], id = parseInt(ref_match[2], 10); - new instance.web.DataSet(this.view, model).name_get([id], function(names) { + new instance.web.DataSet(this.view, model).name_get([id]).then(function(names) { if (!names.length) { return; } record.set(column.id, names[0][1]); }); @@ -989,29 +992,46 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web. // and let the various registered events handle refreshing the // row new instance.web.DataSet(this.view, column.relation) - .name_get([value], function (names) { + .name_get([value]).then(function (names) { if (!names.length) { return; } record.set(column.id, names[0]); }); } + } else if (column.type === 'many2many') { + value = record.get(column.id); + // non-resolved (string) m2m values are arrays + if (value instanceof Array && !_.isEmpty(value)) { + var ids; + // they come in two shapes: + if (value[0] instanceof Array) { + var command = value[0]; + // 1. an array of m2m commands (usually (6, false, ids)) + if (command[0] !== 6) { + throw new Error(_t("Unknown m2m command ") + command[0]); + } + ids = command[2]; + } else { + // 2. an array of ids + ids = value; + } + new instance.web.Model(column.relation) + .call('name_get', [ids]).then(function (names) { + record.set(column.id, _(names).pluck(1).join(', ')); + }) + } } - return instance.web.format_cell(record.toForm().data, column, { + return column.format(record.toForm().data, { model: this.dataset.model, id: record.get('id') }); }, render: function () { - var self = this; - if (this.$current) { - this.$current.remove(); - } - this.$current = this.$_element.clone(true); this.$current.empty().append( QWeb.render('ListView.rows', _.extend({ render_cell: function () { return self.render_cell.apply(self, arguments); } }, this))); - this.pad_table_to(5); + this.pad_table_to(4); }, pad_table_to: function (count) { if (this.records.length >= count || @@ -1020,20 +1040,20 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web. } var cells = []; if (this.options.selectable) { - cells.push('<th class="oe-record-selector"></td>'); + cells.push('<th class="oe_list_record_selector"></td>'); } _(this.columns).each(function(column) { if (column.invisible === '1') { return; } if (column.tag === 'button') { - cells.push('<td class="oe-button" title="' + column.string + '"> </td>'); + cells.push('<td class="oe_button" title="' + column.string + '"> </td>'); } else { cells.push('<td title="' + column.string + '"> </td>'); } }); if (this.options.deletable) { - cells.push('<td class="oe-record-delete"><button type="button" style="visibility: hidden"> </button></td>'); + cells.push('<td class="oe_list_record_delete"><button type="button" style="visibility: hidden"> </button></td>'); } cells.unshift('<tr>'); cells.push('</tr>'); @@ -1042,19 +1062,18 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web. this.$current .children('tr:not([data-id])').remove().end() .append(new Array(count - this.records.length + 1).join(row)); - this.refresh_zebra(this.records.length); }, /** * Gets the ids of all currently selected records, if any * @returns {Object} object with the keys ``ids`` and ``records``, holding respectively the ids of all selected records and the records themselves. */ get_selection: function () { + var result = {ids: [], records: []}; if (!this.options.selectable) { - return []; + return result; } var records = this.records; - var result = {ids: [], records: []}; - this.$current.find('th.oe-record-selector input:checked') + this.$current.find('th.oe_list_record_selector input:checked') .closest('tr').each(function () { var record = records.get($(this).data('id')); result.ids.push(record.get('id')); @@ -1081,8 +1100,6 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web. }, this); if (!this.$current) { return; } this.$current.remove(); - this.$current = null; - this.$_element.remove(); }, get_records: function () { return this.records.map(function (record) { @@ -1096,17 +1113,7 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web. * @returns {$.Deferred} promise to the finalization of the reloading */ reload_record: function (record) { - return this.dataset.read_ids( - [record.get('id')], - _.pluck(_(this.columns).filter(function (r) { - return r.tag === 'field'; - }), 'name') - ).then(function (records) { - _(records[0]).each(function (value, key) { - record.set(key, value, {silent: true}); - }); - record.trigger('change', record); - }); + return this.view.reload_record(record); }, /** * Renders a list record to HTML @@ -1115,6 +1122,7 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web. * @returns {String} QWeb rendering of the selected record */ render_record: function (record) { + var self = this; var index = this.records.indexOf(record); return QWeb.render('ListView.row', { columns: this.columns, @@ -1123,26 +1131,7 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web. row_parity: (index % 2 === 0) ? 'even' : 'odd', view: this.view, render_cell: function () { - return this.render_cell.apply(this, arguments); } - }); - }, - /** - * Fixes fixes the even/odd classes - * - * @param {Number} [from_index] index from which to resequence - * @param {Number} [offset = 0] selection offset for DOM, in case there are rows to ignore in the table - */ - refresh_zebra: function (from_index, offset) { - offset = offset || 0; - from_index = from_index || 0; - var dom_offset = offset + from_index; - var sel = dom_offset ? ':gt(' + (dom_offset - 1) + ')' : null; - this.$current.children(sel).each(function (i, e) { - var index = from_index + i; - // reset record-index accelerators on rows and even/odd - var even = index%2 === 0; - $(e).toggleClass('even', even) - .toggleClass('odd', !even); + return self.render_cell.apply(self, arguments); } }); } }); @@ -1150,7 +1139,7 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we passtrough_events: 'action deleted row_link', /** * Grouped display for the ListView. Handles basic DOM events and interacts - * with the :js:class:`~instance.web.DataGroup` bound to it. + * with the :js:class:`~DataGroup` bound to it. * * Provides events similar to those of * :js:class:`~instance.web.ListView.List` @@ -1230,8 +1219,9 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we .replaceWith(self.render()); }); this.$row.children().last() + .addClass('oe_list_group_pagination') .append($prev) - .append('<span class="oe-pager-state"></span>') + .append('<span class="oe_list_pager_state"></span>') .append($next); }, open: function (point_insertion) { @@ -1269,7 +1259,7 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we self.records.proxy(group.value).reset(); delete self.children[group.value]; } - var child = self.children[group.value] = new instance.web.ListView.Groups(self.view, { + var child = self.children[group.value] = new (self.view.options.GroupsType)(self.view, { records: self.records.proxy(group.value), options: self.options, columns: self.columns @@ -1277,7 +1267,7 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we self.bind_child_events(child); child.datagroup = group; - var $row = child.$row = $('<tr>'); + var $row = child.$row = $('<tr class="oe_group_header">'); if (group.openable && group.length) { $row.click(function (e) { if (!$row.data('open')) { @@ -1297,22 +1287,32 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we } placeholder.appendChild($row[0]); - var $group_column = $('<th class="oe-group-name">').appendTo($row); + var $group_column = $('<th class="oe_list_group_name">').appendTo($row); // Don't fill this if group_by_no_leaf but no group_by if (group.grouped_on) { var row_data = {}; row_data[group.grouped_on] = group; var group_column = _(self.columns).detect(function (column) { return column.id === group.grouped_on; }); + if (! group_column) { + throw new Error(_.str.sprintf( + _t("Grouping on field '%s' is not possible because that field does not appear in the list view."), + group.grouped_on)); + } + var group_label; try { - $group_column.html(instance.web.format_cell( - row_data, group_column, { - value_if_empty: _t("Undefined"), - process_modifiers: false - })); + group_label = group_column.format(row_data, { + value_if_empty: _t("Undefined"), + process_modifiers: false + }); } catch (e) { - $group_column.html(row_data[group_column.id].value); + group_label = _.str.escapeHTML(row_data[group_column.id].value); } + // group_label is html-clean (through format or explicit + // escaping if format failed), can inject straight into HTML + $group_column.html(_.str.sprintf("%s (%d)", + group_label, group.length)); + if (group.length && group.openable) { // Make openable if not terminal group & group_by_no_leaf $group_column.prepend('<span class="ui-icon ui-icon-triangle-1-e" style="float: left;">'); @@ -1324,30 +1324,27 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we } } self.indent($group_column, group.level); - // count column - $('<td>').text(group.length).appendTo($row); if (self.options.selectable) { $row.append('<td>'); } _(self.columns).chain() - .filter(function (column) {return !column.invisible;}) + .filter(function (column) { return column.invisible !== '1'; }) .each(function (column) { if (column.meta) { // do not do anything } else if (column.id in group.aggregates) { var r = {}; r[column.id] = {value: group.aggregates[column.id]}; - $('<td class="oe-number">') - .html(instance.web.format_cell( - r, column, {process_modifiers: false})) + $('<td class="oe_number">') + .html(column.format(r, {process_modifiers: false})) .appendTo($row); } else { $row.append('<td>'); } }); if (self.options.deletable) { - $row.append('<td class="oe-group-pagination">'); + $row.append('<td class="oe_list_group_pagination">'); } }); return placeholder; @@ -1372,7 +1369,7 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we }, render_dataset: function (dataset) { var self = this, - list = new instance.web.ListView.List(this, { + list = new (this.view.options.ListType)(this, { options: this.options, columns: this.columns, dataset: dataset, @@ -1398,21 +1395,23 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we } else { if (dataset.size() == records.length) { // only one page - self.$row.find('td.oe-group-pagination').empty(); + self.$row.find('td.oe_list_group_pagination').empty(); } else { var pages = Math.ceil(dataset.size() / limit); self.$row - .find('.oe-pager-state') + .find('.oe_list_pager_state') .text(_.str.sprintf(_t("%(page)d/%(page_count)d"), { page: page + 1, page_count: pages })) .end() .find('button[data-pager-action=previous]') - .attr('disabled', page === 0) + .css('visibility', + page === 0 ? 'hidden' : '') .end() .find('button[data-pager-action=next]') - .attr('disabled', page === pages - 1); + .css('visibility', + page === pages - 1 ? 'hidden' : ''); } } @@ -1427,18 +1426,31 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we }, setup_resequence_rows: function (list, dataset) { // drag and drop enabled if list is not sorted and there is a - // "sequence" column in the view. + // visible column with @widget=handle or "sequence" column in the view. if ((dataset.sort && dataset.sort()) || !_(this.columns).any(function (column) { - return column.name === 'sequence'; })) { + return column.widget === 'handle' + || column.name === 'sequence'; })) { return; } + var sequence_field = _(this.columns).find(function (c) { + return c.widget === 'handle'; + }); + var seqname = sequence_field ? sequence_field.name : 'sequence'; + // ondrop, move relevant record & fix sequences list.$current.sortable({ axis: 'y', items: '> tr[data-id]', - containment: 'parent', - helper: 'clone', + helper: 'clone' + }); + if (sequence_field) { + list.$current.sortable('option', 'handle', '.oe_list_field_handle'); + } + list.$current.sortable('option', { + start: function (e, ui) { + ui.placeholder.height(ui.item.height()); + }, stop: function (event, ui) { var to_move = list.records.get(ui.item.data('id')), target_id = ui.item.prev().data('id'), @@ -1456,7 +1468,7 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we var record, index = to, // if drag to 1st row (to = 0), start sequencing from 0 // (exclusive lower bound) - seq = to ? list.records.at(to - 1).get('sequence') : 0; + seq = to ? list.records.at(to - 1).get(seqname) : 0; while (++seq, record = list.records.at(index++)) { // write are independent from one another, so we can just // launch them all at the same time and we don't really @@ -1466,39 +1478,39 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we // when synchronous (without setTimeout) (function (dataset, id, seq) { $.async_when().then(function () { - dataset.write(id, {sequence: seq}); + var attrs = {}; + attrs[seqname] = seq; + dataset.write(id, attrs); }); }(dataset, record.get('id'), seq)); - record.set('sequence', seq); + record.set(seqname, seq); } - - list.refresh_zebra(); } }); }, render: function (post_render) { var self = this; - var $element = $('<tbody>'); - this.elements = [$element[0]]; + var $el = $('<tbody>'); + this.elements = [$el[0]]; this.datagroup.list( _(this.view.visible_columns).chain() .filter(function (column) { return column.tag === 'field' }) .pluck('name').value(), function (groups) { - $element[0].appendChild( + $el[0].appendChild( self.render_groups(groups)); if (post_render) { post_render(); } }, function (dataset) { self.render_dataset(dataset).then(function (list) { self.children[null] = list; self.elements = - [list.$current.replaceAll($element)[0]]; + [list.$current.replaceAll($el)[0]]; self.setup_resequence_rows(list, dataset); if (post_render) { post_render(); } }); }); - return $element; + return $el; }, /** * Returns the ids of all selected records for this group, and the records @@ -1537,6 +1549,60 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we } }); +var DataGroup = instance.web.CallbackEnabled.extend({ + init: function(parent, model, domain, context, group_by, level) { + this._super(parent, null); + this.model = new instance.web.Model(model, context, domain); + this.group_by = group_by; + this.context = context; + this.domain = domain; + + this.level = level || 0; + }, + list: function (fields, ifGroups, ifRecords) { + var self = this; + var query = this.model.query(fields).order_by(this.sort).group_by(this.group_by); + $.when(query).then(function (querygroups) { + // leaf node + if (!querygroups) { + var ds = new instance.web.DataSetSearch(self, self.model.name, self.model.context(), self.model.domain()); + ds._sort = self.sort; + ifRecords(ds); + return; + } + // internal node + var child_datagroups = _(querygroups).map(function (group) { + var child_context = _.extend( + {}, self.model.context(), group.model.context()); + var child_dg = new DataGroup( + self, self.model.name, group.model.domain(), + child_context, group.model._context.group_by, + self.level + 1); + child_dg.sort = self.sort; + // copy querygroup properties + child_dg.__context = child_context; + child_dg.__domain = group.model.domain(); + child_dg.folded = group.get('folded'); + child_dg.grouped_on = group.get('grouped_on'); + child_dg.length = group.get('length'); + child_dg.value = group.get('value'); + child_dg.openable = group.get('has_children'); + child_dg.aggregates = group.get('aggregates'); + return child_dg; + }); + ifGroups(child_datagroups); + }); + } +}); +var StaticDataGroup = DataGroup.extend({ + init: function (dataset) { + this.dataset = dataset; + }, + list: function (fields, ifGroups, ifRecords) { + ifRecords(this.dataset); + } +}); + /** * @mixin Events */ @@ -1802,7 +1868,7 @@ var Collection = instance.web.Class.extend(/** @lends Collection# */{ * @returns this */ remove: function (record) { - var index = _(this.records).indexOf(record); + var index = this.indexOf(record); if (index === -1) { _(this._proxies).each(function (proxy) { proxy.remove(record); @@ -1818,13 +1884,42 @@ var Collection = instance.web.Class.extend(/** @lends Collection# */{ return this; }, - _onRecordEvent: function (event, record, options) { + _onRecordEvent: function (event) { + switch(event) { // don't propagate reset events - if (event === 'reset') { return; } + case 'reset': return; + case 'change:id': + var record = arguments[1]; + var new_value = arguments[2]; + var old_value = arguments[3]; + // [change:id, record, new_value, old_value] + if (this._byId[old_value] === record) { + delete this._byId[old_value]; + this._byId[new_value] = record; + } + break; + } this.trigger.apply(this, arguments); }, // underscore-type methods + find: function (callback) { + var record; + for(var section in this._proxies) { + if (!this._proxies.hasOwnProperty(section)) { + continue + } + if ((record = this._proxies[section].find(callback))) { + return record; + } + } + for(var i=0; i<this.length; ++i) { + record = this.records[i]; + if (callback(record)) { + return record; + } + } + }, each: function (callback) { for(var section in this._proxies) { if (this._proxies.hasOwnProperty(section)) { @@ -1849,6 +1944,46 @@ var Collection = instance.web.Class.extend(/** @lends Collection# */{ }, indexOf: function (record) { return _(this.records).indexOf(record); + }, + succ: function (record, options) { + options = options || {wraparound: false}; + var result; + for(var section in this._proxies) { + if (!this._proxies.hasOwnProperty(section)) { + continue; + } + if ((result = this._proxies[section].succ(record, options))) { + return result; + } + } + var index = this.indexOf(record); + if (index === -1) { return null; } + var next_index = index + 1; + if (options.wraparound && (next_index === this.length)) { + return this.at(0); + } + return this.at(next_index); + }, + pred: function (record, options) { + options = options || {wraparound: false}; + + var result; + for (var section in this._proxies) { + if (!this._proxies.hasOwnProperty(section)) { + continue; + } + if ((result = this._proxies[section].pred(record, options))) { + return result; + } + } + + var index = this.indexOf(record); + if (index === -1) { return null; } + var next_index = index - 1; + if (options.wraparound && (next_index === -1)) { + return this.at(this.length - 1); + } + return this.at(next_index); } }); Collection.include(Events); @@ -1856,6 +1991,213 @@ instance.web.list = { Events: Events, Record: Record, Collection: Collection -} +}; +/** + * Registry for column objects used to format table cells (and some other tasks + * e.g. aggregation computations). + * + * Maps a field or button to a Column type via its ``$tag.$widget``, + * ``$tag.$type`` or its ``$tag`` (alone). + * + * This specific registry has a dedicated utility method ``for_`` taking a + * field (from fields_get/fields_view_get.field) and a node (from a view) and + * returning the right object *already instantiated from the data provided*. + * + * @type {instance.web.Registry} + */ +instance.web.list.columns = new instance.web.Registry({ + 'field': 'instance.web.list.Column', + 'field.boolean': 'instance.web.list.Boolean', + 'field.binary': 'instance.web.list.Binary', + 'field.progressbar': 'instance.web.list.ProgressBar', + 'field.handle': 'instance.web.list.Handle', + 'button': 'instance.web.list.Button', +}); +instance.web.list.columns.for_ = function (id, field, node) { + var description = _.extend({tag: node.tag}, field, node.attrs); + var tag = description.tag; + var Type = this.get_any([ + tag + '.' + description.widget, + tag + '.'+ description.type, + tag + ]); + return new Type(id, node.tag, description) +}; + +instance.web.list.Column = instance.web.Class.extend({ + init: function (id, tag, attrs) { + _.extend(attrs, { + id: id, + tag: tag + }); + + this.modifiers = attrs.modifiers ? JSON.parse(attrs.modifiers) : {}; + delete attrs.modifiers; + _.extend(this, attrs); + + if (this.modifiers['tree_invisible']) { + this.invisible = '1'; + } else { delete this.invisible; } + }, + modifiers_for: function (fields) { + var out = {}; + var domain_computer = instance.web.form.compute_domain; + + for (var attr in this.modifiers) { + if (!this.modifiers.hasOwnProperty(attr)) { continue; } + var modifier = this.modifiers[attr]; + out[attr] = _.isBoolean(modifier) + ? modifier + : domain_computer(modifier, fields); + } + + return out; + }, + to_aggregate: function () { + if (this.type !== 'integer' && this.type !== 'float') { + return {}; + } + var aggregation_func = this['group_operator'] || 'sum'; + if (!(aggregation_func in this)) { + return {}; + } + var C = function (fn, label) { + this['function'] = fn; + this.label = label; + }; + C.prototype = this; + return new C(aggregation_func, this[aggregation_func]); + }, + /** + * + * @param row_data record whose values should be displayed in the cell + * @param {Object} [options] + * @param {String} [options.value_if_empty=''] what to display if the field's value is ``false`` + * @param {Boolean} [options.process_modifiers=true] should the modifiers be computed ? + * @param {String} [options.model] current record's model + * @param {Number} [options.id] current record's id + * @return {String} + */ + format: function (row_data, options) { + options = options || {}; + var attrs = {}; + if (options.process_modifiers !== false) { + attrs = this.modifiers_for(row_data); + } + if (attrs.invisible) { return ''; } + + if (!row_data[this.id]) { + return options.value_if_empty === undefined + ? '' + : options.value_if_empty; + } + return this._format(row_data, options); + }, + /** + * Method to override in order to provide alternative HTML content for the + * cell. Column._format will simply call ``instance.web.format_value`` and + * escape the output. + * + * The output of ``_format`` will *not* be escaped by ``format``, any + * escaping *must be done* by ``format``. + * + * @private + */ + _format: function (row_data, options) { + return _.escape(instance.web.format_value( + row_data[this.id].value, this, options.value_if_empty)); + } +}); +instance.web.list.MetaColumn = instance.web.list.Column.extend({ + meta: true, + init: function (id, string) { + this._super(id, '', {string: string}); + } +}); +instance.web.list.Button = instance.web.list.Column.extend({ + /** + * Return an actual ``<button>`` tag + */ + format: function (row_data, options) { + options = options || {}; + var attrs = {}; + if (options.process_modifiers !== false) { + attrs = this.modifiers_for(row_data); + } + if (attrs.invisible) { return ''; } + + return QWeb.render('ListView.row.button', { + widget: this, + prefix: instance.session.prefix, + disabled: attrs.readonly + || isNaN(row_data.id.value) + || instance.web.BufferedDataSet.virtual_id_regex.test(row_data.id.value) + }); + } +}); +instance.web.list.Boolean = instance.web.list.Column.extend({ + /** + * Return a potentially disabled checkbox input + * + * @private + */ + _format: function (row_data, options) { + return _.str.sprintf('<input type="checkbox" %s disabled="disabled"/>', + row_data[this.id].value ? 'checked="checked"' : ''); + } +}); +instance.web.list.Binary = instance.web.list.Column.extend({ + /** + * Return a link to the binary data as a file + * + * @private + */ + _format: function (row_data, options) { + var text = _t("Download"); + var download_url = _.str.sprintf( + '/web/binary/saveas?session_id=%s&model=%s&field=%s&id=%d', + instance.session.session_id, options.model, this.id, options.id); + if (this.filename) { + download_url += '&filename_field=' + this.filename; + if (row_data[this.filename]) { + text = _.str.sprintf(_t("Download \"%s\""), instance.web.format_value( + row_data[this.filename].value, {type: 'char'})); + } + } + return _.template('<a href="<%-href%>"><%-text%></a> (%<-size%>)', { + text: text, + href: download_url, + size: row_data[this.id].value + }); + } +}); +instance.web.list.ProgressBar = instance.web.list.Column.extend({ + /** + * Return a formatted progress bar display + * + * @private + */ + _format: function (row_data, options) { + return _.template( + '<progress value="<%-value%>" max="100"><%-value%>%</progress>', { + value: _.str.sprintf("%.0f", row_data[this.id].value || 0) + }); + } +}); +instance.web.list.Handle = instance.web.list.Column.extend({ + init: function () { + this._super.apply(this, arguments); + // Handle overrides the field to not be form-editable. + this.modifiers.readonly = true; + }, + /** + * Return styling hooks for a drag handle + * + * @private + */ + _format: function (row_data, options) { + return '<div class="oe_list_handle">'; + } +}); }; // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: diff --git a/addons/web/static/src/js/view_list_editable.js b/addons/web/static/src/js/view_list_editable.js index c918012e60d..021e5549c18 100644 --- a/addons/web/static/src/js/view_list_editable.js +++ b/addons/web/static/src/js/view_list_editable.js @@ -3,10 +3,6 @@ * @namespace */ openerp.web.list_editable = function (instance) { - var KEY_RETURN = 13, - KEY_ESCAPE = 27; - var QWeb = instance.web.qweb; - // editability status of list rows instance.web.ListView.prototype.defaults.editable = null; @@ -15,6 +11,15 @@ openerp.web.list_editable = function (instance) { init: function () { var self = this; this._super.apply(this, arguments); + + this._force_editability = null; + this._context_editable = false; + this.editor = this.make_editor(); + // Stores records of {field, cell}, allows for re-rendering fields + // depending on cell state during and after resize events + this.fields_for_resize = []; + instance.web.bus.on('resize', this, this.resize_fields); + $(this.groups).bind({ 'edit': function (e, id, dataset) { self.do_edit(dataset.index, id, dataset); @@ -26,7 +31,35 @@ openerp.web.list_editable = function (instance) { self.configure_pager(self.dataset); self.compute_aggregates(); } - }) + }); + + this.records.bind('remove', function () { + if (self.editor.is_editing()) { + self.cancel_edition(); + } + }); + + this.on('edit:before', this, function (event) { + if (!self.editable() || self.editor.is_editing()) { + event.cancel = true; + } + }); + this.on('edit:after', this, function () { + self.$el.add(self.$buttons).addClass('oe_editing'); + }); + this.on('save:after cancel:after', this, function () { + self.$el.add(self.$buttons).removeClass('oe_editing'); + }); + }, + destroy: function () { + instance.web.bus.off('resize', this, this.resize_fields); + this._super(); + }, + do_hide: function () { + if (this.editor.is_editing()) { + this.cancel_edition(true); + } + this._super(); }, /** * Handles the activation of a record in editable mode (making a record @@ -42,26 +75,16 @@ openerp.web.list_editable = function (instance) { do_edit: function (index, id, dataset) { _.extend(this.dataset, dataset); }, - /** - * Sets editability status for the list, based on defaults, view - * architecture and the provided flag, if any. - * - * @param {Boolean} [force] forces the list to editability. Sets new row edition status to "bottom". - */ - set_editable: function (force) { - // If ``force``, set editability to bottom - // otherwise rely on view default - // view' @editable is handled separately as we have not yet - // fetched and processed the view at this point. - this.options.editable = ( - (force && "bottom") - || this.defaults.editable); + editable: function () { + return this.fields_view.arch.attrs.editable + || this._context_editable + || this.options.editable; }, /** * Replace do_search to handle editability process */ do_search: function(domain, context, group_by) { - this.set_editable(context['set_editable']); + this._context_editable = !!context.set_editable; this._super.apply(this, arguments); }, /** @@ -69,18 +92,66 @@ openerp.web.list_editable = function (instance) { * as an editable row at the top or bottom of the list) */ do_add_record: function () { - if (this.options.editable) { - this.$element.find('table:first').show(); - this.$element.find('.oe_view_nocontent').remove(); - this.groups.new_record(); + if (this.editable()) { + this.$el.find('table:first').show(); + this.$el.find('.oe_view_nocontent').remove(); + this.start_edition(); } else { this._super(); } }, - on_loaded: function (data, grouped) { + load_list: function (data, grouped) { + var self = this; // tree/@editable takes priority on everything else if present. - this.options.editable = data.arch.attrs.editable || this.options.editable; - return this._super(data, grouped); + var result = this._super(data, grouped); + if (this.editable()) { + this.$el.addClass('oe_list_editable'); + // FIXME: any hook available to ensure this is only done once? + this.$buttons + .off('click', '.oe_list_save') + .on('click', '.oe_list_save', this.proxy('save_edition')) + .off('click', '.oe_list_discard') + .on('click', '.oe_list_discard', function (e) { + e.preventDefault(); + self.cancel_edition(); + }); + this.$el + .off('click', 'tbody td:not(.oe_list_field_cell)') + .on('click', 'tbody td:not(.oe_list_field_cell)', function () { + if (!self.editor.is_editing()) { + self.start_edition(); + } + }); + this.editor.destroy(); + // Editor is not restartable due to formview not being + // restartable + this.editor = this.make_editor(); + var editor_ready = this.editor.prependTo(this.$el) + .then(this.proxy('setup_events')); + + return $.when(result, editor_ready); + } else { + this.$el.removeClass('oe_list_editable'); + } + + return result; + }, + /** + * Builds a new editor object + * + * @return {instance.web.list.Editor} + */ + make_editor: function () { + return new instance.web.list.Editor(this); + }, + do_button_action: function (name, id, callback) { + var self = this, args = arguments; + this.ensure_saved().then(function (done) { + if (!id && done.created) { + id = done.record.get('id'); + } + self.handle_button(name, id, callback); + }); }, /** * Ensures the editable list is saved (saves any pending edition if @@ -91,337 +162,639 @@ openerp.web.list_editable = function (instance) { * @returns {$.Deferred} */ ensure_saved: function () { - return this.groups.ensure_saved(); - } - }); - - instance.web.ListView.Groups.include(/** @lends instance.web.ListView.Groups# */{ - passtrough_events: instance.web.ListView.Groups.prototype.passtrough_events + " edit saved", - new_record: function () { - // TODO: handle multiple children - this.children[null].new_record(); + if (!this.editor.is_editing()) { + return $.when(); + } + return this.save_edition(); }, /** - * Ensures descendant editable List instances are all saved if they have - * pending editions. + * Set up the edition of a record of the list view "inline" * - * @returns {$.Deferred} + * @param {instance.web.list.Record} [record] record to edit, leave empty to create a new record + * @param {Object} [options] + * @param {String} [options.focus_field] field to focus at start of edition + * @return {jQuery.Deferred} */ - ensure_saved: function () { - return $.when.apply(null, - _.invoke( - _.values(this.children), - 'ensure_saved')); - } - }); - - instance.web.ListView.List.include(/** @lends instance.web.ListView.List# */{ - row_clicked: function (event) { - if (!this.options.editable) { - return this._super.apply(this, arguments); + start_edition: function (record, options) { + var self = this; + var item = false; + if (record) { + item = record.attributes; + } else { + var attrs = {id: false}; + _(this.columns).chain() + .filter(function (x) { return x.tag === 'field'}) + .pluck('name') + .each(function (field) { attrs[field] = false; }); + record = new instance.web.list.Record(attrs); + this.records.add(record, { + at: this.prepends_on_create() ? 0 : null}); } - this.edit_record($(event.currentTarget).data('id')); + + return this.ensure_saved().pipe(function () { + var $recordRow = self.groups.get_row_for(record); + var cells = self.get_cells_for($recordRow); + self.fields_for_resize.splice(0, self.fields_for_resize.length); + return self.with_event('edit', { + record: record.attributes, + cancel: false + }, function () { + return self.editor.edit(item, function (field_name, field) { + var cell = cells[field_name]; + if (!cell) { + return; + } + + // FIXME: need better way to get the field back from bubbling (delegated) DOM events somehow + field.$el.attr('data-fieldname', field_name); + self.fields_for_resize.push({field: field, cell: cell}); + }, options).pipe(function () { + $recordRow.addClass('oe_edition'); + self.resize_fields(); + return record.attributes; + }); + }).fail(function () { + // if the start_edition event is cancelled and it was a + // creation, remove the newly-created empty record + if (!record.get('id')) { + self.records.remove(record); + } + }); + }); + }, + get_cells_for: function ($row) { + var cells = {}; + $row.children('td').each(function (index, el) { + cells[el.getAttribute('data-field')] = el + }); + return cells; }, /** - * Checks if a record is being edited, and if so cancels it + * If currently editing a row, resizes all registered form fields based + * on the corresponding row cell */ - cancel_pending_edition: function () { - var self = this, cancelled = $.Deferred(); - if (!this.edition) { - cancelled.resolve(); - return cancelled.promise(); - } - - if (this.edition_id != null) { - this.reload_record(self.records.get(this.edition_id)).then(function () { - cancelled.resolve(); - }); - } else { - cancelled.resolve(); + resize_fields: function () { + if (!this.editor.is_editing()) { return; } + for(var i=0, len=this.fields_for_resize.length; i<len; ++i) { + var item = this.fields_for_resize[i]; + this.resize_field(item.field, item.cell); } - cancelled.then(function () { - self.view.unpad_columns(); - self.edition_form.destroy(); - self.edition_form.$element.remove(); - delete self.edition_form; - delete self.edition_id; - delete self.edition; - }); - this.pad_table_to(5); - return cancelled.promise(); }, /** - * Adapts this list's view description to be suitable to the inner form - * view of a row being edited. + * Resizes a field's root element based on the corresponding cell of + * a listview row * - * @returns {Object} fields_view_get's view section suitable for putting into form view of editable rows. + * @param {instance.web.form.AbstractField} field + * @param {jQuery} cell */ - get_form_fields_view: function () { - // deep copy of view - var view = $.extend(true, {}, this.group.view.fields_view); - _(view.arch.children).each(function (widget) { - widget.attrs.nolabel = true; - if (widget.tag === 'button') { - delete widget.attrs.string; - } + resize_field: function (field, cell) { + var $cell = $(cell); + var position = $cell.position(); + + field.set_dimensions($cell.outerHeight(), $cell.outerWidth()); + field.$el.position({ + my: 'left top', + at: 'left top', + of: $cell }); - view.arch.attrs.col = 2 * view.arch.children.length; - return view; }, - on_row_keyup: function (e) { + /** + * @return {jQuery.Deferred} + */ + save_edition: function () { var self = this; - switch (e.which) { - case KEY_RETURN: - this.save_row().then(function (result) { - if (result.created) { - self.new_record(); - return; - } - - var next_record_id, - next_record = self.records.at( - self.records.indexOf(result.edited_record) + 1); - if (next_record) { - next_record_id = next_record.get('id'); - self.dataset.index = _(self.dataset.ids) - .indexOf(next_record_id); - } else { - self.dataset.index = 0; - next_record_id = self.records.at(0).get('id'); + return this.with_event('save', { + editor: this.editor, + form: this.editor.form, + cancel: false + }, function () { + return this.editor.save().pipe(function (attrs) { + var created = false; + var record = self.records.get(attrs.id); + if (!record) { + // new record + created = true; + record = self.records.find(function (r) { + return !r.get('id'); + }).set('id', attrs.id); } - self.edit_record(next_record_id); + // onwrite callback could be altering & reloading the + // record which has *just* been saved, so first perform all + // onwrites then do a final reload of the record + return self.handle_onwrite(record) + .pipe(function () { + return self.reload_record(record); }) + .pipe(function () { + return { created: created, record: record }; }); }); - break; - case KEY_ESCAPE: - this.cancel_edition(); - break; - } + }); }, - render_row_as_form: function (row) { + /** + * @param {Boolean} [force=false] discards the data even if the form has been edited + * @return {jQuery.Deferred} + */ + cancel_edition: function (force) { var self = this; - this.cancel_pending_edition().then(function () { - var record_id = $(row).data('id'); - var $new_row = $('<tr>', { - id: _.uniqueId('oe-editable-row-'), - 'data-id': record_id, - 'class': (row ? $(row).attr('class') : '') + ' oe_form', - click: function (e) {e.stopPropagation();} - }) - .delegate('button.oe-edit-row-save', 'click', function () { - self.save_row(); - }) - .delegate('button', 'keyup', function (e) { - e.stopImmediatePropagation(); - }) - .keyup(function () { - return self.on_row_keyup.apply(self, arguments); }) - .keydown(function (e) { e.stopPropagation(); }); - if (row) { - $new_row.replaceAll(row); - } else if (self.options.editable) { - var $last_child = self.$current.children('tr:last'); - if (self.records.length) { - if (self.options.editable === 'top') { - $new_row.insertBefore( - self.$current.children('[data-id]:first')); - } else { - $new_row.insertAfter( - self.$current.children('[data-id]:last')); + return this.with_event('cancel', { + editor: this.editor, + form: this.editor.form, + cancel: false + }, function () { + return this.editor.cancel(force).pipe(function (attrs) { + if (attrs.id) { + var record = self.records.get(attrs.id); + if (!record) { + // Record removed by third party during edition + return } - } else { - $new_row.prependTo(self.$current); - } - if ($last_child.is(':not([data-id])')) { - $last_child.remove(); + return self.reload_record(record); } - } - self.edition = true; - self.edition_id = record_id; - $new_row.addClass("oe_form_container"); - self.edition_form = new instance.web.ListEditableFormView(self.view, self.dataset, false); - self.edition_form.$element = $new_row; - self.edition_form.editable_list = self; - // HO HO - // empty - $.when(self.edition_form.on_loaded(self.get_form_fields_view())).then(function () { - // put in $.when just in case FormView.on_loaded becomes asynchronous - $new_row.find('> td') - .end() - .find('td:last').removeClass('oe-field-cell').end(); - // pad in case of groupby - _(self.columns).each(function (column) { - if (column.meta) { - $new_row.prepend('<td>'); - } + var to_delete = self.records.find(function (r) { + return !r.get('id'); }); - // Add column for the save, if - // there is none in the list - if (!self.options.deletable) { - self.view.pad_columns( - 1, {except: $new_row}); + if (to_delete) { + self.records.remove(to_delete); } - - self.edition_form.do_show(); }); }); }, - handle_onwrite: function (source_record_id) { + /** + * Executes an action on the view's editor bracketed by a cancellable + * event of the name provided. + * + * The event name provided will be post-fixed with ``:before`` and + * ``:after``, the ``event`` parameter will be passed alongside the + * ``:before`` variant and if the parameter's ``cancel`` key is set to + * ``true`` the action *will not be called* and the method will return + * a rejection + * + * @param {String} event_name name of the event + * @param {Object} event event object, provided to ``:before`` sub-event + * @param {Function} action callable, called with the view's editor as its context + * @param {Array} [args] supplementary arguments provided to the action + * @param {Array} [trigger_params] supplementary arguments provided to the ``:after`` sub-event, before anything fetched by the ``action`` function + * @return {jQuery.Deferred} + */ + with_event: function (event_name, event, action) { var self = this; - var on_write_callback = self.view.fields_view.arch.attrs.on_write; - if (!on_write_callback) { return; } - this.dataset.call(on_write_callback, [source_record_id], function (ids) { - _(ids).each(function (id) { - var record = self.records.get(id); - if (!record) { - // insert after the source record - var index = self.records.indexOf( - self.records.get(source_record_id)) + 1; - record = new instance.web.list.Record({id: id}); - self.records.add(record, {at: index}); - self.dataset.ids.splice(index, 0, id); + event = event || {}; + this.trigger(event_name + ':before', event); + if (event.cancel) { + return $.Deferred().reject({ + message: _.str.sprintf("Event %s:before cancelled", + event_name)}); + } + return $.when(action.call(this)).then(function () { + self.trigger.apply(self, [event_name + ':after'] + .concat(_.toArray(arguments))); + }); + }, + edition_view: function (editor) { + var view = $.extend(true, {}, this.fields_view); + view.arch.tag = 'form'; + _.extend(view.arch.attrs, { + 'class': 'oe_form_container', + version: '7.0' + }); + _(view.arch.children).chain() + .zip(this.columns) + .each(function (ar) { + var widget = ar[0], column = ar[1]; + var modifiers = _.extend({}, column.modifiers); + widget.attrs.nolabel = true; + if (modifiers['tree_invisible'] || widget.tag === 'button') { + modifiers.invisible = true; } - self.reload_record(record); + widget.attrs.modifiers = JSON.stringify(modifiers); + }); + return view; + }, + handle_onwrite: function (source_record) { + var self = this; + var on_write_callback = self.fields_view.arch.attrs.on_write; + if (!on_write_callback) { return $.when(); } + return this.dataset.call(on_write_callback, [source_record.get('id')]) + .pipe(function (ids) { + return $.when.apply( + null, _(ids).map( + _.bind(self.handle_onwrite_record, self, source_record))); }); + }, + handle_onwrite_record: function (source_record, id) { + var record = this.records.get(id); + if (!record) { + // insert after the source record + var index = this.records.indexOf(source_record) + 1; + record = new instance.web.list.Record({id: id}); + this.records.add(record, {at: index}); + this.dataset.ids.splice(index, 0, id); + } + return this.reload_record(record); + }, + prepends_on_create: function () { + return this.editable() === 'top'; + }, + setup_events: function () { + var self = this; + _.each(this.editor.form.fields, function(field, field_name) { + var setting = false; + var set_invisible = function() { + if (!setting && field.get("effective_readonly")) { + setting = true; + field.set({invisible: true}); + setting = false; + } + }; + field.on("change:effective_readonly", self, set_invisible); + field.on("change:invisible", self, set_invisible); + set_invisible(); + }); + + this.editor.$el.on('keyup keydown', function (e) { + if (!self.editor.is_editing()) { return true; } + var key = _($.ui.keyCode).chain() + .map(function (v, k) { return {name: k, code: v}; }) + .find(function (o) { return o.code === e.which; }) + .value(); + if (!key) { return true; } + var method = e.type + '_' + key.name; + if (!(method in self)) { return true; } + return self[method](e); }); }, /** - * Saves the current row, and returns a Deferred resolving to an object - * with the following properties: - * - * ``created`` - * Boolean flag indicating whether the record saved was being created - * (``true`` or edited (``false``) - * ``edited_record`` - * The result of saving the record (either the newly created record, - * or the post-edition record), after insertion in the Collection if - * needs be. + * Saves the current record, and goes to the next one (creation or + * edition) * - * @returns {$.Deferred<{created: Boolean, edited_record: Record}>} + * @private + * @param {String} [next_record='succ'] method to call on the records collection to get the next record to edit + * @param {Object} [options] + * @param {String} [options.focus_field] + * @return {*} */ - save_row: function () { - //noinspection JSPotentiallyInvalidConstructorUsage - var self = this, done = $.Deferred(); - return this.edition_form - .do_save(null, this.options.editable === 'top') - .pipe(function (result) { - if (result.created && !self.edition_id) { - self.records.add({id: result.result}, - {at: self.options.editable === 'top' ? 0 : null}); - self.edition_id = result.result; - } - var edited_record = self.records.get(self.edition_id); - - return $.when( - self.handle_onwrite(self.edition_id), - self.cancel_pending_edition().then(function () { - $(self).trigger('saved', [self.dataset]); - })).pipe(function () { - return { - created: result.created || false, - edited_record: edited_record - }; - }, null); - }, null); + _next: function (next_record, options) { + next_record = next_record || 'succ'; + var self = this; + return this.save_edition().pipe(function (saveInfo) { + if (saveInfo.created) { + return self.start_edition(); + } + var record = self.records[next_record]( + saveInfo.record, {wraparound: true}); + return self.start_edition(record, options); + }); + }, + keyup_ENTER: function () { + return this._next(); + }, + keydown_ESCAPE: function (e) { + return false; + }, + keyup_ESCAPE: function (e) { + return this.cancel_edition(); }, /** - * If the current list is being edited, ensures it's saved + * Gets the selection range (start, end) for the provided element, + * returns ``null`` if it can't get a range. + * + * @private */ - ensure_saved: function () { - if (this.edition) { - return this.save_row(); + _text_selection_range: function (el) { + var selectionStart; + try { + selectionStart = el.selectionStart; + } catch (e) { + // radio or checkbox throw on selectionStart access + return null; } - //noinspection JSPotentiallyInvalidConstructorUsage - return $.Deferred().resolve().promise(); + if (selectionStart !== undefined) { + return { + start: selectionStart, + end: el.selectionEnd + }; + } else if (document.body.createTextRange) { + throw new Error("Implement text range handling for MSIE"); + var sel = document.body.createTextRange(); + if (sel.parentElement() === el) { + + } + } + // Element without selection ranges (select, div/@contenteditable) + return null; + }, + _text_cursor: function (el) { + var selection = this._text_selection_range(el); + if (!selection) { + return null; + } + if (selection.start !== selection.end) { + return {position: null, collapsed: false}; + } + return {position: selection.start, collapsed: true}; + }, + /** + * Checks if the cursor is at the start of the provided el + * + * @param {HTMLInputElement | HTMLTextAreaElement} + * @returns {Boolean} + * @private + */ + _at_start: function (cursor, el) { + return cursor.collapsed && (cursor.position === 0); }, /** - * Cancels the edition of the row for the current dataset index + * Checks if the cursor is at the end of the provided el + * + * @param {HTMLInputElement | HTMLTextAreaElement} + * @returns {Boolean} + * @private */ - cancel_edition: function () { - this.cancel_pending_edition(); + _at_end: function (cursor, el) { + return cursor.collapsed && (cursor.position === el.value.length); }, /** - * Edits record currently selected via dataset + * @param DOMEvent event + * @param {String} record_direction direction to move into to get the next record (pred | succ) + * @param {Function} is_valid_move whether the edition should be moved to the next record + * @private */ - edit_record: function (record_id) { - this.render_row_as_form( - this.$current.find('[data-id=' + record_id + ']')); - $(this).trigger( - 'edit', - [record_id, this.dataset]); - }, - new_record: function () { - this.dataset.index = null; - this.render_row_as_form(); - }, - render_record: function (record) { - var index = this.records.indexOf(record), - self = this; - // FIXME: context dict should probably be extracted cleanly - return QWeb.render('ListView.row', { - columns: this.columns, - options: this.options, - record: record, - row_parity: (index % 2 === 0) ? 'even' : 'odd', - view: this.view, - render_cell: function () { - return self.render_cell.apply(self, arguments); }, - edited: !!this.edition_form + _key_move_record: function (event, record_direction, is_valid_move) { + if (!this.editor.is_editing('edit')) { return $.when(); } + var cursor = this._text_cursor(event.target); + // if text-based input (has a cursor) + // and selecting (not collapsed) or not at a field boundary + // don't move to the next record + if (cursor && !is_valid_move(event.target, cursor)) { return $.when(); } + + event.preventDefault(); + var source_field = $(event.target).closest('[data-fieldname]') + .attr('data-fieldname'); + return this._next(record_direction, {focus_field: source_field}); + + }, + keydown_UP: function (e) { + var self = this; + return this._key_move_record(e, 'pred', function (el, cursor) { + return self._at_start(cursor, el); }); + }, + keydown_DOWN: function (e) { + var self = this; + return this._key_move_record(e, 'succ', function (el, cursor) { + return self._at_end(cursor, el); + }); + }, + + keydown_LEFT: function (e) { + // If the cursor is at the beginning of the field + var source_field = $(e.target).closest('[data-fieldname]') + .attr('data-fieldname'); + var cursor = this._text_cursor(e.target); + if (cursor && !this._at_start(cursor, e.target)) { return $.when(); } + + var fields_order = this.editor.form.fields_order; + var field_index = _(fields_order).indexOf(source_field); + + // Look for the closest visible form field to the left + var fields = this.editor.form.fields; + var field; + do { + if (--field_index < 0) { return $.when(); } + + field = fields[fields_order[field_index]]; + } while (!field.$el.is(':visible')); + + // and focus it + field.focus(); + return $.when(); + }, + keydown_RIGHT: function (e) { + // same as above, but with cursor at the end of the field and + // looking for new fields at the right + var source_field = $(e.target).closest('[data-fieldname]') + .attr('data-fieldname'); + var cursor = this._text_cursor(e.target); + if (cursor && !this._at_end(cursor, e.target)) { return $.when(); } + + var fields_order = this.editor.form.fields_order; + var field_index = _(fields_order).indexOf(source_field); + + var fields = this.editor.form.fields; + var field; + do { + if (++field_index >= fields_order.length) { return $.when(); } + + field = fields[fields_order[field_index]]; + } while (!field.$el.is(':visible')); + + field.focus(); + return $.when(); + }, + keydown_TAB: function (e) { + var form = this.editor.form; + var last_field = _(form.fields_order).chain() + .map(function (name) { return form.fields[name]; }) + .filter(function (field) { return field.$el.is(':visible'); }) + .last() + .value(); + // tabbed from last field in form + if (last_field && last_field.$el.has(e.target).length) { + e.preventDefault(); + return this._next(); + } + return $.when(); } }); - - instance.web.ListEditableFormView = instance.web.FormView.extend({ - init: function() { - this._super.apply(this, arguments); - this.rendering_engine = new instance.web.ListEditableRenderingEngine(this); - this.options.initial_mode = "edit"; + + instance.web.list.Editor = instance.web.Widget.extend({ + /** + * @constructs instance.web.list.Editor + * @extends instance.web.Widget + * + * Adapter between listview and formview for editable-listview purposes + * + * @param {instance.web.Widget} parent + * @param {Object} options + * @param {instance.web.FormView} [options.formView=instance.web.FormView] + * @param {Object} [options.delegate] + */ + init: function (parent, options) { + this._super(parent); + this.options = options || {}; + _.defaults(this.options, { + formView: instance.web.FormView, + delegate: this.getParent() + }); + this.delegate = this.options.delegate; + + this.record = null; + + this.form = new (this.options.formView)( + this, this.delegate.dataset, false, { + initial_mode: 'edit', + disable_autofocus: true, + $buttons: $(), + $pager: $() + }); }, - renderElement: function() {} - }); - - instance.web.ListEditableRenderingEngine = instance.web.form.FormRenderingEngineInterface.extend({ - init: function(view) { - this.view = view; + start: function () { + var self = this; + var _super = this._super(); + this.form.embedded_view = this._validate_view( + this.delegate.edition_view(this)); + var form_ready = this.form.appendTo(this.$el).then( + self.form.proxy('do_hide')); + return $.when(_super, form_ready); }, - set_fields_view: function(fields_view) { - this.fvg = fields_view; + _validate_view: function (edition_view) { + if (!edition_view) { + throw new Error("editor delegate's #edition_view must return " + + "a view descriptor"); + } + var arch = edition_view.arch; + if (!(arch && arch.children instanceof Array)) { + throw new Error("Editor delegate's #edition_view must have a" + + " non-empty arch") + } + if (!(arch.tag === "form")) { + throw new Error("Editor delegate's #edition_view must have a" + + " 'form' root node"); + } + if (!(arch.attrs && arch.attrs.version === "7.0")) { + throw new Error("Editor delegate's #edition_view must be a" + + " version 7 view"); + } + if (!/\boe_form_container\b/.test(arch.attrs['class'])) { + throw new Error("Editor delegate's #edition_view must have the" + + " class 'oe_form_container' on its root" + + " element"); + } + + return edition_view; }, - set_tags_registry: function(tags_registry) { - this.tags_registry = tags_registry; + + /** + * + * @param {String} [state] either ``new`` or ``edit`` + * @return {Boolean} + */ + is_editing: function (state) { + if (!this.record) { + return false; + } + switch(state) { + case null: case undefined: + return true; + case 'new': return !this.record.id; + case 'edit': return !!this.record.id; + } + throw new Error("is_editing's state filter must be either `new` or" + + " `edit` if provided"); }, - set_fields_registry: function(fields_registry) { - this.fields_registry = fields_registry; + _focus_setup: function (focus_field) { + var form = this.form; + + var field; + // If a field to focus was specified + if (focus_field + // Is actually in the form + && (field = form.fields[focus_field]) + // And is visible + && field.$el.is(':visible')) { + // focus it + field.focus(); + return; + } + + _(form.fields_order).detect(function (name) { + // look for first visible field in fields_order, focus it + var field = form.fields[name]; + if (!field.$el.is(':visible')) { + return false; + } + // Stop as soon as a field got focused + return field.focus() !== false; + }); }, - render_to: function($element) { + edit: function (record, configureField, options) { + // TODO: specify sequence of edit calls var self = this; - - var xml = instance.web.json_node_to_xml(this.fvg.arch); - var $xml = $(xml); - - if (this.view.editable_list.options.selectable) - $("<td>").appendTo($element); - - $xml.children().each(function(i, el) { - var modifiers = JSON.parse($(el).attr("modifiers") || "{}"); - var $td = $("<td>"); - if (modifiers.tree_invisible === true) - $td.hide(); - var tag_name = el.tagName.toLowerCase(); - var w; - if (tag_name === "field") { - var name = $(el).attr("name"); - var key = $(el).attr('widget') || self.fvg.fields[name].type; - var obj = self.view.fields_registry.get_object(key); - w = new (obj)(self.view, instance.web.xml_to_json(el)); - self.view.register_field(w, $(el).attr("name")); - } else { - var obj = self.tags_registry.get_object(tag_name); - w = new (obj)(self.view, instance.web.xml_to_json(el)); - } - w.appendTo($td); - $td.appendTo($element); + var form = self.form; + var loaded = record + ? form.trigger('load_record', _.extend({}, record)) + : form.load_defaults(); + return $.when(loaded).pipe(function () { + return form.do_show({reload: false}); + }).pipe(function () { + self.record = form.datarecord; + _(form.fields).each(function (field, name) { + configureField(name, field); + }); + self._focus_setup(options && options.focus_field); + return form; }); - $("<td><button class='oe-edit-row-save' type='button'>Save</button></td>").appendTo($element); }, + save: function () { + var self = this; + return this.form + .save(this.delegate.prepends_on_create()) + .pipe(function (result) { + var created = result.created && !self.record.id; + if (created) { + self.record.id = result.result; + } + return self.cancel(); + }); + }, + cancel: function (force) { + if (!(force || this.form.can_be_discarded())) { + return $.Deferred().reject({ + message: "The form's data can not be discarded"}).promise(); + } + var record = this.record; + this.record = null; + this.form.do_hide(); + return $.when(record); + } + }); + + instance.web.ListView.Groups.include(/** @lends instance.web.ListView.Groups# */{ + passtrough_events: instance.web.ListView.Groups.prototype.passtrough_events + " edit saved", + get_row_for: function (record) { + return _(this.children).chain() + .invoke('get_row_for', record) + .compact() + .first() + .value(); + } + }); + + instance.web.ListView.List.include(/** @lends instance.web.ListView.List# */{ + row_clicked: function (event) { + if (!this.view.editable() || ! this.view.is_action_enabled('edit')) { + return this._super.apply(this, arguments); + } + var record_id = $(event.currentTarget).data('id'); + return this.view.start_edition( + record_id ? this.records.get(record_id) : null, { + focus_field: $(event.target).data('field') + }); + }, + /** + * If a row mapping to the record (@data-id matching the record's id or + * no @data-id if the record has no id), returns it. Otherwise returns + * ``null``. + * + * @param {Record} record the record to get a row for + * @return {jQuery|null} + */ + get_row_for: function (record) { + var id; + var $row = this.$current.children('[data-id=' + record.get('id') + ']'); + if ($row.length) { + return $row; + } + return null; + } }); }; diff --git a/addons/web/static/src/js/view_tree.js b/addons/web/static/src/js/view_tree.js index 9e938fe293a..05760420ba8 100644 --- a/addons/web/static/src/js/view_tree.js +++ b/addons/web/static/src/js/view_tree.js @@ -45,7 +45,7 @@ instance.web.TreeView = instance.web.View.extend(/** @lends instance.web.TreeVie view_type: "tree", toolbar: this.view_manager ? !!this.view_manager.sidebar : false, context: this.dataset.get_context() - }, this.on_loaded); + }).then(this.on_loaded); }, /** * Returns the list of fields needed to correctly read objects. @@ -63,6 +63,12 @@ instance.web.TreeView = instance.web.View.extend(/** @lends instance.web.TreeVie } return fields; }, + store_record:function(records){ + var self = this; + _(records).each(function (record) { + self.records[record.id] = record; + }); + }, on_loaded: function (fields_view) { var self = this; var has_toolbar = !!fields_view.arch.attrs.toolbar; @@ -78,25 +84,28 @@ instance.web.TreeView = instance.web.View.extend(/** @lends instance.web.TreeVie }); this.fields = fields_view.fields; this.hook_row_click(); - this.$element.html(QWeb.render('TreeView', { + this.$el.html(QWeb.render('TreeView', { 'title': this.fields_view.arch.attrs.string, 'fields_view': this.fields_view.arch.children, 'fields': this.fields, 'toolbar': has_toolbar })); + this.$el.addClass(this.fields_view.arch.attrs['class']); this.dataset.read_slice(this.fields_list()).then(function(records) { + self.store_record(records); if (!has_toolbar) { // WARNING: will do a second read on the same ids, but only on // first load so not very important - self.getdata(null, _(records).pluck('id')); + self.render_data({'null':records}) + self.getdata(_.pluck(records,"id")); return; } - var $select = self.$element.find('select') + var $select = self.$el.find('select') .change(function () { var $option = $(this).find(':selected'); - self.getdata($option.val(), $option.data('children')); + self.getdata($option.val()); }); _(records).each(function (record) { self.records[record.id] = record; @@ -111,7 +120,12 @@ instance.web.TreeView = instance.web.View.extend(/** @lends instance.web.TreeVie $select.change(); } }); - + this.$el.find("#tree_view_expand").click(function(){ + self.expand_all(); + }); + this.$el.find("#tree_view_collapse").click(function(){ + self.collpase_all(); + }); // TODO store open nodes in url ?... this.do_push_state({}); @@ -127,6 +141,22 @@ instance.web.TreeView = instance.web.View.extend(/** @lends instance.web.TreeVie return [color, py.parse(py.tokenize(expr)), expr]; }).value(); }, + expand_all: function(){ + var self = this; + var tr = this.$el.find(".oe-treeview-table tbody tr[id^='treerow_']"); + _.each(tr,function(rec){ + self.showcontent($(rec).attr('data-id'),true); + }); + }, + collpase_all: function(){ + var self = this; + var root_tr = this.$el.find(".oe-treeview-table tbody tr[data-level='"+1+"']"); + _.each(root_tr,function(rec){ + if($(rec).hasClass('oe_open')){ + self.showcontent($(rec).attr('data-id'),false); + } + }); + }, /** * Returns the color for the provided record in the current view (from the * ``@colors`` attribute) @@ -157,65 +187,61 @@ instance.web.TreeView = instance.web.View.extend(/** @lends instance.web.TreeVie */ hook_row_click: function () { var self = this; - this.$element.delegate('.treeview-td span, .treeview-tr span', 'click', function (e) { + this.$el.delegate('.treeview-td span, .treeview-tr span', 'click', function (e) { e.stopImmediatePropagation(); self.activate($(this).closest('tr').data('id')); }); - this.$element.delegate('.treeview-tr', 'click', function () { - var is_loaded = 0, - $this = $(this), + this.$el.delegate('.treeview-tr', 'click', function () { + var $this = $(this), record_id = $this.data('id'), - record = self.records[record_id], - children_ids = record[self.children_field]; - - _(children_ids).each(function(childid) { - if (self.$element.find('#treerow_' + childid).length) { - if (self.$element.find('#treerow_' + childid).is(':hidden')) { - is_loaded = -1; - } else { - is_loaded++; - } - } - }); - if (is_loaded === 0) { - if (!$this.parent().hasClass('oe-open')) { - self.getdata(record_id, children_ids); - } - } else { - self.showcontent(record_id, is_loaded < 0); - } + bool = $this.parent().hasClass('oe_open'); + self.showcontent(record_id, !bool); }); }, // get child data of selected value - getdata: function (id, children_ids) { + getdata: function (id) { var self = this; - - self.dataset.read_ids(children_ids, this.fields_list()).then(function(records) { - _(records).each(function (record) { - self.records[record.id] = record; - }); - - var $curr_node = self.$element.find('#treerow_' + id); + var parent_child ={}; + id = _.isArray(id)?id:parseInt(id); + var ir_model_data = new instance.web.Model(this.model,self.dataset.get_context() || {},[['id','child_of',id]]).query(); + ir_model_data._execute().then(function(records){ + self.store_record(records); + _.each(records,function(rec){ + if(rec[self.children_field].length === 0)return; + parent_child[rec.id] = []; + _.each(rec[self.children_field],function(key){ + parent_child[rec.id].push(self.records[key]); + }); + }) + self.render_data(parent_child); + }); + }, + render_data: function(groupby){ + var self = this; + _.each(_.keys(groupby),function(key){ + var $curr_node = self.$el.find('#treerow_' + key); + var record = groupby[key]; var children_rows = QWeb.render('TreeView.rows', { - 'records': records, + 'records': record, 'children_field': self.children_field, 'fields_view': self.fields_view.arch.children, 'fields': self.fields, - 'level': $curr_node.data('level') || 0, + 'level': ($curr_node.data('level') || 0) + 1, 'render': instance.web.format_value, 'color_for': self.color_for }); - if ($curr_node.length) { - $curr_node.addClass('oe-open'); + $curr_node.addClass('oe_open'); $curr_node.after(children_rows); } else { - self.$element.find('tbody').html(children_rows); + self.$el.find('tbody').html(children_rows); } }); + self.collpase_all(); }, + // Get details in listview activate: function(id) { var self = this; @@ -246,25 +272,17 @@ instance.web.TreeView = instance.web.View.extend(/** @lends instance.web.TreeVie // show & hide the contents showcontent: function (record_id, show) { - this.$element.find('#treerow_' + record_id) - .toggleClass('oe-open', show); + this.$el.find('#treerow_' + record_id) + .toggleClass('oe_open', show); _(this.records[record_id][this.children_field]).each(function (child_id) { - var $child_row = this.$element.find('#treerow_' + child_id); - if ($child_row.hasClass('oe-open')) { + var $child_row = this.$el.find('#treerow_' + child_id); + if ($child_row.hasClass('oe_open')) { this.showcontent(child_id, false); } $child_row.toggle(show); }, this); }, - do_show: function () { - this.$element.show(); - }, - - do_hide: function () { - this.$element.hide(); - this.hidden = true; - } }); }; diff --git a/addons/web/static/src/js/views.js b/addons/web/static/src/js/views.js index 6bf7e273eab..a4fe86c516d 100644 --- a/addons/web/static/src/js/views.js +++ b/addons/web/static/src/js/views.js @@ -10,51 +10,208 @@ instance.web.ActionManager = instance.web.Widget.extend({ init: function(parent) { this._super(parent); this.inner_action = null; - this.inner_viewmanager = null; + this.inner_widget = null; this.dialog = null; - this.dialog_viewmanager = null; - this.client_widget = null; + this.dialog_widget = null; + this.breadcrumbs = []; + this.on('history_back', this, function() { + return this.history_back(); + }); + }, + start: function() { + this._super.apply(this, arguments); + this.$el.on('click', 'a.oe_breadcrumb_item', this.on_breadcrumb_clicked); }, dialog_stop: function () { if (this.dialog) { - this.dialog_viewmanager.destroy(); - this.dialog_viewmanager = null; this.dialog.destroy(); - this.dialog = null; } }, - content_stop: function () { - if (this.inner_viewmanager) { - this.inner_viewmanager.destroy(); - this.inner_viewmanager = null; + /** + * Add a new item to the breadcrumb + * + * If the title of an item is an array, the multiple title mode is in use. + * (eg: a widget with multiple views might need to display a title for each view) + * In multiple title mode, the show() callback can check the index it receives + * in order to detect which of its titles has been clicked on by the user. + * + * @param {Object} item breadcrumb item + * @param {Object} item.widget widget containing the view(s) to be added to the breadcrumb added + * @param {Function} [item.show] triggered whenever the widget should be shown back + * @param {Function} [item.hide] triggered whenever the widget should be shown hidden + * @param {Function} [item.destroy] triggered whenever the widget should be destroyed + * @param {String|Array} [item.title] title(s) of the view(s) to be displayed in the breadcrumb + * @param {Function} [item.get_title] should return the title(s) of the view(s) to be displayed in the breadcrumb + */ + push_breadcrumb: function(item) { + var last = this.breadcrumbs.slice(-1)[0]; + if (last) { + last.hide(); } - if (this.client_widget) { - this.client_widget.destroy(); - this.client_widget = null; + var item = _.extend({ + show: function(index) { + this.widget.$el.show(); + }, + hide: function() { + this.widget.$el.hide(); + }, + destroy: function() { + this.widget.destroy(); + }, + get_title: function() { + return this.title || this.widget.get('title'); + } + }, item); + item.id = _.uniqueId('breadcrumb_'); + this.breadcrumbs.push(item); + }, + 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); } }, + on_breadcrumb_clicked: function(ev) { + var $e = $(ev.target); + var id = $e.data('id'); + var index; + for (var i = this.breadcrumbs.length - 1; i >= 0; i--) { + if (this.breadcrumbs[i].id == id) { + index = i; + break; + } + } + var subindex = $e.parent().find('a.oe_breadcrumb_item[data-id=' + $e.data('id') + ']').index($e); + this.select_breadcrumb(index, subindex); + }, + select_breadcrumb: function(index, subindex) { + var next_item = this.breadcrumbs[index + 1]; + if (next_item && next_item.on_reverse_breadcrumb) { + next_item.on_reverse_breadcrumb(this.breadcrumbs[index].widget); + } + for (var i = this.breadcrumbs.length - 1; i >= 0; i--) { + if (i > index) { + if (this.remove_breadcrumb(i) === false) { + return false; + } + } + } + var item = this.breadcrumbs[index]; + item.show(subindex); + this.inner_widget = item.widget; + this.inner_action = item.action; + return true; + }, + clear_breadcrumbs: function() { + for (var i = this.breadcrumbs.length - 1; i >= 0; i--) { + if (this.remove_breadcrumb(0) === false) { + break; + } + } + }, + remove_breadcrumb: function(index) { + var item = this.breadcrumbs.splice(index, 1)[0]; + if (item) { + var dups = _.filter(this.breadcrumbs, function(it) { + return item.widget === it.widget; + }); + if (!dups.length) { + if (this.getParent().has_uncommitted_changes()) { + this.inner_widget = item.widget; + this.inner_action = item.action; + this.breadcrumbs.splice(index, 0, item); + return false; + } else { + item.destroy(); + } + } + } + var last_widget = this.breadcrumbs.slice(-1)[0]; + if (last_widget) { + this.inner_widget = last_widget.widget; + this.inner_action = last_widget.action; + } + }, + get_title: function() { + var titles = []; + for (var i = 0; i < this.breadcrumbs.length; i += 1) { + var item = this.breadcrumbs[i]; + var tit = item.get_title(); + if (!_.isArray(tit)) { + tit = [tit]; + } + for (var j = 0; j < tit.length; j += 1) { + var label = _.escape(tit[j]); + if (i === this.breadcrumbs.length - 1 && j === tit.length - 1) { + titles.push(_.str.sprintf('<span class="oe_breadcrumb_item">%s</span>', label)); + } else { + titles.push(_.str.sprintf('<a href="#" class="oe_breadcrumb_item" data-id="%s">%s</a>', item.id, label)); + } + } + } + return titles.join(' <span class="oe_fade">/</span> '); + }, do_push_state: function(state) { + state = state || {}; if (this.getParent() && this.getParent().do_push_state) { if (this.inner_action) { - state['model'] = this.inner_action.res_model; + if (this.inner_action._push_me === false) { + // this action has been explicitly marked as not pushable + return; + } + state['title'] = this.inner_action.name; + if(this.inner_action.type == 'ir.actions.act_window') { + state['model'] = this.inner_action.res_model; + } + if (this.inner_action.menu_id) { + state['menu_id'] = this.inner_action.menu_id; + } if (this.inner_action.id) { - state['action_id'] = this.inner_action.id; + state['action'] = this.inner_action.id; + } else if (this.inner_action.type == 'ir.actions.client') { + state['action'] = this.inner_action.tag; + var params = {}; + _.each(this.inner_action.params, function(v, k) { + if(_.isString(v) || _.isNumber(v)) { + params[k] = v; + } + }); + state = _.extend(params || {}, state); } } - this.getParent().do_push_state(state); + if(!this.dialog) { + this.getParent().do_push_state(state); + } } }, do_load_state: function(state, warm) { var self = this, action_loaded; - if (state.action_id) { - var run_action = (!this.inner_viewmanager) || this.inner_viewmanager.action.id !== state.action_id; - if (run_action) { + if (state.action) { + if (_.isString(state.action) && instance.web.client_actions.contains(state.action)) { + var action_client = {type: "ir.actions.client", tag: state.action, params: state}; this.null_action(); - action_loaded = this.do_action(state.action_id); - instance.webclient.menu.has_been_loaded.then(function() { - instance.webclient.menu.open_action(state.action_id); - }); + action_loaded = this.do_action(action_client); + } else { + var run_action = (!this.inner_widget || !this.inner_widget.action) || this.inner_widget.action.id !== state.action; + if (run_action) { + this.null_action(); + action_loaded = this.do_action(state.action); + instance.webclient.menu.has_been_loaded.then(function() { + instance.webclient.menu.open_action(state.action); + }); + } } } else if (state.model && state.id) { // TODO handle context & domain ? @@ -68,38 +225,43 @@ instance.web.ActionManager = instance.web.Widget.extend({ action_loaded = this.do_action(action); } else if (state.sa) { // load session action - var self = this; this.null_action(); action_loaded = this.rpc('/web/session/get_session_action', {key: state.sa}).pipe(function(action) { if (action) { return self.do_action(action); } }); - } else if (state.client_action) { - this.null_action(); - this.ir_actions_client(state.client_action); } $.when(action_loaded || null).then(function() { - if (self.inner_viewmanager) { - self.inner_viewmanager.do_load_state(state, warm); + if (self.inner_widget && self.inner_widget.do_load_state) { + self.inner_widget.do_load_state(state, warm); } }); }, - do_action: function(action, on_close) { - if (_.isNumber(action)) { + do_action: function(action, options) { + options = _.defaults(options || {}, { + clear_breadcrumbs: false, + on_reverse_breadcrumb: function() {}, + on_close: function() {}, + action_menu_id: null, + }); + if (_.isString(action) && instance.web.client_actions.contains(action)) { + var action_client = { type: "ir.actions.client", tag: action, params: {} }; + return this.do_action(action_client, options); + } else if (_.isNumber(action) || _.isString(action)) { var self = this; - return self.rpc("/web/action/load", { action_id: action }, function(result) { - self.do_action(result.result, on_close); + return self.rpc("/web/action/load", { action_id: action }).pipe(function(result) { + return self.do_action(result, options); }); } if (!action.type) { console.error("No type for action", action); - return; + return $.Deferred().reject(); } var type = action.type.replace(/\./g,'_'); var popup = action.target === 'new'; - var inline = action.target === 'inline'; + var inline = action.target === 'inline' || action.target === 'inlineview'; action.flags = _.extend({ views_switcher : !popup && !inline, search_view : !popup && !inline, @@ -108,74 +270,122 @@ instance.web.ActionManager = instance.web.Widget.extend({ pager : !popup && !inline, display_title : !popup }, action.flags || {}); + action.menu_id = options.action_menu_id; if (!(type in this)) { console.error("Action manager can't handle action of type " + action.type, action); - return; + return $.Deferred().reject(); } - return this[type](action, on_close); + return this[type](action, options); }, null_action: function() { this.dialog_stop(); - this.content_stop(); + this.clear_breadcrumbs(); }, - ir_actions_act_window: function (action, on_close) { - var self = this; - if (_(['base.module.upgrade', 'base.setup.installer']) - .contains(action.res_model)) { - var old_close = on_close; - on_close = function () { - instance.webclient.do_reload().then(old_close); - }; + /** + * + * @param {Object} executor + * @param {Object} executor.action original action + * @param {Function<instance.web.Widget>} executor.widget function used to fetch the widget instance + * @param {String} executor.klass CSS class to add on the dialog root, if action.target=new + * @param {Function<instance.web.Widget, undefined>} executor.post_process cleanup called after a widget has been added as inner_widget + * @param {Object} options + * @return {*} + */ + ir_actions_common: function(executor, options) { + if (this.inner_widget && executor.action.target !== 'new') { + if (this.getParent().has_uncommitted_changes()) { + return $.Deferred().reject(); + } else if (options.clear_breadcrumbs) { + this.clear_breadcrumbs(); + } } - if (action.target === 'new') { - if (this.dialog == null) { - this.dialog = new instance.web.Dialog(this, { width: '80%' }); - if(on_close) - this.dialog.on_close.add(on_close); - } else { - this.dialog_viewmanager.destroy(); + var widget = executor.widget(); + if (executor.action.target === 'new') { + if (this.dialog_widget && ! this.dialog_widget.isDestroyed()) + this.dialog_widget.destroy(); + if (this.dialog === null || this.dialog.isDestroyed()) { + this.dialog = new instance.web.Dialog(this, { + dialogClass: executor.klass, + }); + this.dialog.on("closing", null, options.on_close); + this.dialog.init_dialog(); + } + this.dialog.dialog_title = executor.action.name; + if (widget instanceof instance.web.ViewManager) { + _.extend(widget.flags, { + $buttons: this.dialog.$buttons, + footer_to_buttons: true, + }); } - this.dialog.dialog_title = action.name; - this.dialog_viewmanager = new instance.web.ViewManagerAction(this, action); - this.dialog_viewmanager.appendTo(this.dialog.$element); + this.dialog_widget = widget; + this.dialog_widget.setParent(this.dialog); + var initialized = this.dialog_widget.appendTo(this.dialog.$el); this.dialog.open(); + return initialized; } else { this.dialog_stop(); - this.content_stop(); - if(action.menu_id) { - return this.getParent().do_action(action, function () { - instance.webclient.menu.open_menu(action.menu_id); - }); + this.inner_action = executor.action; + this.inner_widget = widget; + executor.post_process(widget); + return this.inner_widget.appendTo(this.$el); + } + }, + ir_actions_act_window: function (action, options) { + var self = this; + + return this.ir_actions_common({ + widget: function () { return new instance.web.ViewManagerAction(self, action); }, + action: action, + klass: 'oe_act_window', + post_process: function (widget) { widget.add_breadcrumb(options.on_reverse_breadcrumb); } + }, options); + }, + ir_actions_client: function (action, options) { + var self = this; + var ClientWidget = instance.web.client_actions.get_object(action.tag); + + if (!(ClientWidget.prototype instanceof instance.web.Widget)) { + var next; + if (next = ClientWidget(this, action)) { + return this.do_action(next, options); } - this.inner_action = action; - this.inner_viewmanager = new instance.web.ViewManagerAction(this, action); - this.inner_viewmanager.appendTo(this.$element); + return $.when(); } + + return this.ir_actions_common({ + 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, + }); + if (action.tag !== 'reload') { + self.do_push_state({}); + } + } + }, options); }, - ir_actions_act_window_close: function (action, on_closed) { - if (!this.dialog && on_closed) { - on_closed(); + ir_actions_act_window_close: function (action, options) { + if (!this.dialog) { + options.on_close(); } this.dialog_stop(); }, - ir_actions_server: function (action, on_closed) { + ir_actions_server: function (action, options) { var self = this; this.rpc('/web/action/run', { action_id: action.id, context: action.context || {} }).then(function (action) { - self.do_action(action, on_closed) + self.do_action(action, options) }); }, - ir_actions_client: function (action) { - this.content_stop(); - this.dialog_stop(); - var ClientWidget = instance.web.client_actions.get_object(action.tag); - (this.client_widget = new ClientWidget(this, action.params)).appendTo(this.$element); - }, - ir_actions_report_xml: function(action, on_closed) { + ir_actions_report_xml: function(action, options) { var self = this; - $.blockUI(); + instance.web.blockUI(); self.rpc("/web/session/eval_domain_and_context", { contexts: [action.context], domains: [] @@ -185,10 +395,10 @@ instance.web.ActionManager = instance.web.Widget.extend({ self.session.get_file({ url: '/web/report', data: {action: JSON.stringify(action)}, - complete: $.unblockUI, + complete: instance.web.unblockUI, success: function(){ - if (!self.dialog && on_closed) { - on_closed(); + if (!self.dialog) { + options.on_close(); } self.dialog_stop(); }, @@ -199,15 +409,13 @@ instance.web.ActionManager = instance.web.Widget.extend({ ir_actions_act_url: function (action) { window.open(action.url, action.target === 'self' ? '_self' : '_blank'); }, - ir_ui_menu: function (action) { - this.getParent().do_action(action); - } }); instance.web.ViewManager = instance.web.Widget.extend({ template: "ViewManager", init: function(parent, dataset, views, flags) { this._super(parent); + this.url_states = {}; this.model = dataset ? dataset.model : undefined; this.dataset = dataset; this.searchview = null; @@ -235,8 +443,8 @@ instance.web.ViewManager = instance.web.Widget.extend({ start: function() { this._super(); var self = this; - this.$element.find('.oe_view_manager_switch a').click(function() { - self.on_mode_switch($(this).data('view-type')); + this.$el.find('.oe_view_manager_switch a').click(function() { + self.switch_mode($(this).data('view-type')); }).tipsy(); var views_ids = {}; _.each(this.views_src, function(view) { @@ -244,9 +452,9 @@ instance.web.ViewManager = instance.web.Widget.extend({ deferred : $.Deferred(), controller : null, options : _.extend({ - $buttons : self.$element.find('.oe_view_manager_buttons'), - $sidebar : self.flags.sidebar ? self.$element.find('.oe_view_manager_sidebar') : undefined, - $pager : self.$element.find('.oe_view_manager_pager'), + $buttons : self.$el.find('.oe_view_manager_buttons'), + $sidebar : self.flags.sidebar ? self.$el.find('.oe_view_manager_sidebar') : undefined, + $pager : self.$el.find('.oe_view_manager_pager'), action : self.action, action_views_ids : views_ids }, self.flags, self.flags[view.view_type] || {}, view.options || {}) @@ -254,61 +462,28 @@ instance.web.ViewManager = instance.web.Widget.extend({ views_ids[view.view_type] = view.view_id; }); if (this.flags.views_switcher === false) { - this.$element.find('.oe_view_manager_switch').hide(); + this.$el.find('.oe_view_manager_switch').hide(); } // If no default view defined, switch to the first one in sequence var default_view = this.flags.default_view || this.views_src[0].view_type; - return this.on_mode_switch(default_view); + return this.switch_mode(default_view); }, - /** - * Asks the view manager to switch visualization mode. - * - * @param {String} view_type type of view to display - * @param {Boolean} [no_store=false] don't store the view being switched to on the switch stack - * @returns {jQuery.Deferred} new view loading promise - */ - on_mode_switch: function(view_type, no_store) { + switch_mode: function(view_type, no_store, view_options) { var self = this; var view = this.views[view_type]; var view_promise; - if(!view) + var form = this.views['form']; + if (!view || (form && form.controller && !form.controller.can_be_discarded())) { + self.trigger('switch_mode', view_type, no_store, view_options); return $.Deferred().reject(); - + } if (!no_store) { this.views_history.push(view_type); } this.active_view = view_type; if (!view.controller) { - // Lazy loading of views - var controllerclass = this.registry.get_object(view_type); - var options = _.clone(view.options); - if (view_type === "form" && this.action) { - switch (this.action.target) { - case 'new': - case 'inline': - options.initial_mode = 'edit'; - break; - } - } - var controller = new controllerclass(this, this.dataset, view.view_id, options); - if (view.embedded_view) { - controller.set_embedded_view(view.embedded_view); - } - controller.do_switch_view.add_last(_.bind(this.switch_view, this)); - controller.do_prev_view.add_last(this.on_prev_view); - var container = this.$element.find(".oe_view_manager_view_" + view_type); - view_promise = controller.appendTo(container); - this.views[view_type].controller = controller; - this.views[view_type].deferred.resolve(view_type); - $.when(view_promise).then(function() { - self.on_controller_inited(view_type, controller); - if (self.searchview - && self.flags.auto_search - && view.controller.searchable !== false) { - self.searchview.ready.then(self.searchview.do_search); - } - }); + view_promise = this.do_create_view(view_type); } else if (this.searchview && self.flags.auto_search && view.controller.searchable !== false) { @@ -319,60 +494,148 @@ instance.web.ViewManager = instance.web.Widget.extend({ this.searchview[(view.controller.searchable === false || this.searchview.hidden) ? 'hide' : 'show'](); } - this.$element - .find('.oe_view_manager_switch a').parent().removeClass('active') - this.$element + this.$el.find('.oe_view_manager_switch a').parent().removeClass('active'); + this.$el .find('.oe_view_manager_switch a').filter('[data-view-type="' + view_type + '"]') .parent().addClass('active'); - $.when(view_promise).then(function () { + r = $.when(view_promise).then(function () { _.each(_.keys(self.views), function(view_name) { var controller = self.views[view_name].controller; if (controller) { + var container = self.$el.find(".oe_view_manager_view_" + view_name + ":first"); if (view_name === view_type) { - controller.do_show(); + container.show(); + controller.do_show(view_options || {}); } else { + container.hide(); controller.do_hide(); } } }); + self.trigger('switch_mode', view_type, no_store, view_options); + }); + return r; + }, + do_create_view: function(view_type) { + // Lazy loading of views + var self = this; + var view = this.views[view_type]; + var viewclass = this.registry.get_object(view_type); + var options = _.clone(view.options); + if (view_type === "form" && this.action && (this.action.target == 'new' || this.action.target == 'inline')) { + options.initial_mode = 'edit'; + } + var controller = new viewclass(this, this.dataset, view.view_id, options); + + controller.on('history_back', this, function() { + var am = self.getParent(); + if (am && am.trigger) { + return am.trigger('history_back'); + } + }); - self.$element.find('.oe_view_title_text:first').text( - self.display_title()); + controller.on("change:title", this, function() { + if (self.active_view === view_type) { + self.set_title(controller.get('title')); + } + }); + + if (view.embedded_view) { + controller.set_embedded_view(view.embedded_view); + } + controller.on('switch_mode', self, this.switch_mode); + controller.on('previous_view', self, this.prev_view); + + var container = this.$el.find(".oe_view_manager_view_" + view_type); + var view_promise = controller.appendTo(container); + this.views[view_type].controller = controller; + this.views[view_type].deferred.resolve(view_type); + return $.when(view_promise).then(function() { + if (self.searchview + && self.flags.auto_search + && view.controller.searchable !== false) { + self.searchview.ready.then(self.searchview.do_search); + } + self.trigger("controller_inited",view_type,controller); }); - return view_promise; }, - /** - * Method used internally when a view asks to switch view. This method is meant - * to be extended by child classes to change the default behavior, which simply - * consist to switch to the asked view. - */ - switch_view: function(view_type, no_store) { - return this.on_mode_switch(view_type, no_store); + set_title: function(title) { + this.$el.find('.oe_view_title_text:first').text(title); + }, + add_breadcrumb: function(on_reverse_breadcrumb) { + var self = this; + var views = [this.active_view || this.views_src[0].view_type]; + this.on('switch_mode', self, function(mode) { + var last = views.slice(-1)[0]; + if (mode !== last) { + if (mode !== 'form') { + views.length = 0; + } + views.push(mode); + } + }); + this.getParent().push_breadcrumb({ + widget: this, + action: this.action, + show: function(index) { + var view_to_select = views[index]; + var state = self.url_states[view_to_select]; + self.do_push_state(state || {}); + $.when(self.switch_mode(view_to_select)).then(function() { + self.$el.show(); + }); + }, + get_title: function() { + var id; + var currentIndex; + _.each(self.getParent().breadcrumbs, function(bc, i) { + if (bc.widget === self) { + currentIndex = i; + } + }); + var next = self.getParent().breadcrumbs.slice(currentIndex + 1)[0]; + var titles = _.map(views, function(v) { + var controller = self.views[v].controller; + if (v === 'form') { + id = controller.datarecord.id; + } + return controller.get('title'); + }); + if (next && next.action && next.action.res_id && self.dataset && + self.active_view === 'form' && self.dataset.model === next.action.res_model && id === next.action.res_id) { + // If the current active view is a formview and the next item in the breadcrumbs + // is an action on same object (model / res_id), then we omit the current formview's title + titles.pop(); + } + return titles; + }, + on_reverse_breadcrumb: on_reverse_breadcrumb, + }); }, /** * Returns to the view preceding the caller view in this manager's * navigation history (the navigation history is appended to via - * on_mode_switch) + * switch_mode) * * @param {Object} [options] * @param {Boolean} [options.created=false] resource was created * @param {String} [options.default=null] view to switch to if no previous view * @returns {$.Deferred} switching end signal */ - on_prev_view: function (options) { + prev_view: function (options) { options = options || {}; var current_view = this.views_history.pop(); var previous_view = this.views_history[this.views_history.length - 1] || options['default']; if (options.created && current_view === 'form' && previous_view === 'list') { // APR special case: "If creation mode from list (and only from a list), // after saving, go to page view (don't come back in list)" - return this.on_mode_switch('form'); + return this.switch_mode('form'); } else if (options.created && !previous_view && this.action && this.action.flags.default_view === 'form') { // APR special case: "If creation from dashboard, we have no previous view - return this.on_mode_switch('form'); + return this.switch_mode('form'); } - return this.on_mode_switch(previous_view, true); + return this.switch_mode(previous_view, true); }, /** * Sets up the current viewmanager's search view. @@ -387,8 +650,8 @@ instance.web.ViewManager = instance.web.Widget.extend({ } this.searchview = new instance.web.SearchView(this, this.dataset, view_id, search_defaults, this.flags.search_view === false); - this.searchview.on_search.add(this.do_searchview_search); - return this.searchview.appendTo(this.$element.find(".oe_view_manager_view_search")); + this.searchview.on('search_data', self, this.do_searchview_search); + return this.searchview.appendTo(this.$el.find(".oe_view_manager_view_search")); }, do_searchview_search: function(domains, contexts, groupbys) { var self = this, @@ -398,7 +661,7 @@ instance.web.ViewManager = instance.web.Widget.extend({ domains: [this.action.domain || []].concat(domains || []), contexts: [action_context].concat(contexts || []), group_by_seq: groupbys || [] - }, function (results) { + }).then(function (results) { self.dataset._model = new instance.web.Model( self.dataset.model, results.context, results.domain); var groupby = results.group_by.length @@ -411,14 +674,6 @@ instance.web.ViewManager = instance.web.Widget.extend({ }); }, /** - * Event launched when a controller has been inited. - * - * @param {String} view_type type of view - * @param {String} view the inited controller - */ - on_controller_inited: function(view_type, view) { - }, - /** * Called when one of the view want to execute an action */ on_action: function(action) { @@ -434,14 +689,6 @@ instance.web.ViewManager = instance.web.Widget.extend({ */ on_action_executed: function () { }, - display_title: function () { - var view = this.views[this.active_view]; - if (view) { - // ick - return view.controller.fields_view.arch.attrs.string; - } - return ''; - } }); instance.web.ViewManagerAction = instance.web.ViewManager.extend({ @@ -486,7 +733,7 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({ if (this.session.hidden_menutips) { return; } - this.session.hidden_menutips = {} + this.session.hidden_menutips = {}; }, /** * Initializes the ViewManagerAction: sets up the searchview (if the @@ -513,33 +760,8 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({ var manager_ready = $.when(searchview_loaded, main_view_loaded); - this.$element.find('.oe_debug_view').change(this.on_debug_changed); - - if (this.action.help && !this.flags.low_profile) { - var Users = new instance.web.DataSet(self, 'res.users'), - $tips = this.$element.find('.oe_view_manager_menu_tips'); - $tips.delegate('blockquote button', 'click', function() { - var $this = $(this); - //noinspection FallthroughInSwitchStatementJS - switch ($this.attr('name')) { - case 'disable': - Users.write(self.session.uid, {menu_tips:false}); - case 'hide': - $this.closest('blockquote').hide(); - self.session.hidden_menutips[self.action.id] = true; - } - }); - if (!(self.action.id in self.session.hidden_menutips)) { - Users.read_ids([this.session.uid], ['menu_tips']).then(function(users) { - var user = users[0]; - if (!(user && user.id === self.session.uid)) { - return; - } - $tips.find('blockquote').toggle(user.menu_tips); - }); - } - } - + this.$el.find('.oe_debug_view').change(this.on_debug_changed); + this.$el.addClass("oe_view_manager_" + (this.action.target || 'current')); return manager_ready; }, on_debug_changed: function (evt) { @@ -551,7 +773,15 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({ switch (val) { case 'fvg': var dialog = new instance.web.Dialog(this, { title: _t("Fields View Get"), width: '95%' }).open(); - $('<pre>').text(instance.web.json_node_to_xml(current_view.fields_view.arch, true)).appendTo(dialog.$element); + $('<pre>').text(instance.web.json_node_to_xml(current_view.fields_view.arch, true)).appendTo(dialog.$el); + break; + case 'tests': + this.do_action({ + name: "JS Tests", + target: 'new', + type : 'ir.actions.act_url', + url: '/web/static/test/test.html' + }) break; case 'perm_read': var ids = current_view.get_selected_ids(); @@ -570,14 +800,23 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({ case 'toggle_layout_outline': current_view.rendering_engine.toggle_layout_debugging(); break; + case 'translate': + this.do_action({ + name: "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_and_eval( - 'fields_get', [false, {}], null, 1).then(function (fields) { + this.dataset.call('fields_get', [false, {}]).then(function (fields) { var $root = $('<dl>'); _(fields).each(function (attributes, name) { $root.append($('<dt>').append($('<h4>').text(name))); - var $attrs = $('<dl>').appendTo( - $('<dd>').appendTo($root)); + var $attrs = $('<dl>').appendTo($('<dd>').appendTo($root)); _(attributes).each(function (def, name) { if (def instanceof Object) { def = JSON.stringify(def); @@ -593,15 +832,6 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({ width: '95%'}, $root).open(); }); break; - case 'manage_views': - if (current_view.fields_view && current_view.fields_view.arch) { - var view_editor = new instance.web.ViewEditor(current_view, current_view.$element, this.dataset, current_view.fields_view.arch); - view_editor.start(); - } else { - this.do_warn(_t("Manage Views"), - _t("Could not find current view declaration")); - } - break; case 'edit_workflow': return this.do_action({ res_model : 'workflow', @@ -626,6 +856,21 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({ } }); 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; default: if (val) { console.log("No debug handler for ", val); @@ -651,26 +896,40 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({ }, action || {}); this.do_action(action); }, - on_mode_switch: function (view_type, no_store) { + switch_mode: function (view_type, no_store, options) { var self = this; - return $.when(this._super(view_type, no_store)).then(function () { - var controller = self.views[self.active_view].controller, - fvg = controller.fields_view, - view_id = (fvg && fvg.view_id) || '--'; - self.$element.find('.oe_debug_view').html(QWeb.render('ViewManagerDebug', { + return $.when(this._super.apply(this, arguments)).then(function () { + var controller = self.views[self.active_view].controller; + self.$el.find('.oe_debug_view').html(QWeb.render('ViewManagerDebug', { view: controller, view_manager: self })); - if (!self.action.name && fvg) { - self.$element.find('.oe_view_title_text').text(fvg.arch.attrs.string || fvg.name); - } - + self.set_title(); }); }, + do_create_view: function(view_type) { + var r = this._super.apply(this, arguments); + var view = this.views[view_type].controller; + view.set({ 'title': this.action.name }); + return r; + }, + get_action_manager: function() { + var cur = this; + while (cur = cur.getParent()) { + if (cur instanceof instance.web.ActionManager) { + return cur; + } + } + return undefined; + }, + set_title: function(title) { + this.$el.find('.oe_breadcrumb_title:first').html(this.get_action_manager().get_title()); + }, do_push_state: function(state) { if (this.getParent() && this.getParent().do_push_state) { state["view_type"] = this.active_view; + this.url_states[this.active_view] = state; this.getParent().do_push_state(state); } }, @@ -680,7 +939,7 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({ if (state.view_type && state.view_type !== this.active_view) { defs.push( this.views[this.active_view].deferred.pipe(function() { - return self.on_mode_switch(state.view_type, true); + return self.switch_mode(state.view_type, true); }) ); } @@ -689,45 +948,39 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({ self.views[self.active_view].controller.do_load_state(state, warm); }); }, - display_title: function () { - return this.action.name; - } }); instance.web.Sidebar = instance.web.Widget.extend({ init: function(parent) { + var self = this; this._super(parent); var view = this.getParent(); this.sections = [ { 'name' : 'print', 'label' : _t('Print'), }, - { 'name' : 'files', 'label' : _t('Attachement'), }, { 'name' : 'other', 'label' : _t('More'), } ]; this.items = { 'print' : [], - 'files' : [], - 'other' : [ - { label: _t("Import"), callback: view.on_sidebar_import }, - { label: _t("Export"), callback: view.on_sidebar_export } - ] - } - if (this.session.uid === 1) { - var item = { label: _t("Translate"), callback: view.on_sidebar_translate, title: _t("Technical translation") }; - this.items.other.push(item); - } + 'other' : [] + }; + this.fileupload_id = _.uniqueId('oe_fileupload'); + $(window).on(this.fileupload_id, function() { + var args = [].slice.call(arguments).slice(1); + if (args[0] && args[0].error) { + alert(args[0].error); + } else { + self.do_attachement_update(self.dataset, self.model_id); + } + instance.web.unblockUI(); + }); }, start: function() { var self = this; this._super(this); this.redraw(); - this.$element.on('click','.oe_dropdown_toggle',function(event) { - $(this).parent().find('ul').toggle(); - return false; - }); - this.$element.on('click','.oe_dropdown_menu li a', function(event) { + this.$el.on('click','.oe_dropdown_menu li a', function(event) { var section = $(this).data('section'); var index = $(this).data('index'); - $(this).closest('ul').hide(); var item = self.items[section][index]; if (item.callback) { item.callback.apply(self, [item]); @@ -736,16 +989,17 @@ instance.web.Sidebar = instance.web.Widget.extend({ } else if (item.url) { return true; } - return false; + event.preventDefault(); }); - //this.$div.html(QWeb.render('FormView.sidebar.attachments', this)); - //this.$element.find('.oe-binary-file').change(this.on_attachment_changed); - //this.$element.find('.oe-sidebar-attachment-delete').click(this.on_attachment_delete); }, redraw: function() { var self = this; - self.$element.html(QWeb.render('Sidebar', {widget: self})); - this.$element.find('ul').hide(); + self.$el.html(QWeb.render('Sidebar', {widget: self})); + + // Hides Sidebar sections when item list is empty + this.$('.oe_form_dropdown_section').each(function() { + $(this).toggle(!!$(this).find('li').length); + }); }, /** * For each item added to the section: @@ -811,19 +1065,23 @@ instance.web.Sidebar = instance.web.Widget.extend({ self.rpc("/web/action/load", { action_id: item.action.id, context: additional_context - }, function(result) { - result.result.context = _.extend(result.result.context || {}, + }).then(function(result) { + result.context = _.extend(result.context || {}, additional_context); - result.result.flags = result.result.flags || {}; - result.result.flags.new_window = true; - self.do_action(result.result, function () { - // reload view - self.getParent().reload(); + result.flags = result.flags || {}; + result.flags.new_window = true; + self.do_action(result, { + on_close: function() { + // reload view + self.getParent().reload(); + }, }); }); }); }, do_attachement_update: function(dataset, model_id) { + this.dataset = dataset; + this.model_id = model_id; if (!model_id) { this.on_attachments_loaded([]); } else { @@ -835,165 +1093,43 @@ instance.web.Sidebar = instance.web.Widget.extend({ on_attachments_loaded: function(attachments) { var self = this; var items = []; - // TODO: preprend: _s + - var prefix = '/web/binary/saveas?session_id=' + self.session.session_id + '&model=ir.attachment&field=datas&filename_field=name&id='; + var prefix = this.session.origin + '/web/binary/saveas?session_id=' + self.session.session_id + '&model=ir.attachment&field=datas&filename_field=name&id='; _.each(attachments,function(a) { a.label = a.name; if(a.type === "binary") { a.url = prefix + a.id + '&t=' + (new Date().getTime()); } }); - attachments.push( { label: _t("Add..."), callback: self.on_attachment_add } ); self.items['files'] = attachments; self.redraw(); - }, - on_attachment_add: function(e) { - this.$element.find('.oe_sidebar_add').show(); + this.$('.oe_sidebar_add_attachment .oe_form_binary_file').change(this.on_attachment_changed); + this.$el.find('.oe_sidebar_delete_item').click(this.on_attachment_delete); }, on_attachment_changed: function(e) { - return; - window[this.element_id + '_iframe'] = this.do_update; var $e = $(e.target); - if ($e.val() != '') { - this.$element.find('form.oe-binary-form').submit(); + if ($e.val() !== '') { + this.$el.find('form.oe_form_binary_form').submit(); $e.parent().find('input[type=file]').prop('disabled', true); $e.parent().find('button').prop('disabled', true).find('img, span').toggle(); + this.$('.oe_sidebar_add_attachment span').text(_t('Uploading...')); + instance.web.blockUI(); } }, on_attachment_delete: function(e) { - return; - var self = this, $e = $(e.currentTarget); - var name = _.str.trim($e.parent().find('a.oe-sidebar-attachments-link').text()); - if (confirm(_.str.sprintf(_t("Do you really want to delete the attachment %s?"), name))) { - this.rpc('/web/dataset/unlink', { - model: 'ir.attachment', - ids: [parseInt($e.attr('data-id'))] - }, function(r) { - $e.parent().remove(); - self.do_notify("Delete an attachment", "The attachment '" + name + "' has been deleted"); - }); - } - } -}); - -instance.web.TranslateDialog = instance.web.Dialog.extend({ - dialog_title: {toString: function () { return _t("Translations"); }}, - init: function(view) { - // TODO fme: should add the language to fields_view_get because between the fields view get - // and the moment the user opens the translation dialog, the user language could have been changed - this.view_language = view.session.user_context.lang; - this['on_button' + _t("Save")] = this.on_button_Save; - this['on_button' + _t("Close")] = this.on_button_Close; - this._super(view, { - width: '80%', - height: '80%' - }); - this.view = view; - this.view_type = view.fields_view.type || ''; - this.$fields_form = null; - this.$view_form = null; - this.$sidebar_form = null; - this.translatable_fields_keys = _.map(this.view.translatable_fields || [], function(i) { return i.name }); - this.languages = null; - this.languages_loaded = $.Deferred(); - (new instance.web.DataSetSearch(this, 'res.lang', this.view.dataset.get_context(), - [['translatable', '=', '1']])).read_slice(['code', 'name'], { sort: 'id' }).then(this.on_languages_loaded); - }, - start: function() { var self = this; - this._super(); - $.when(this.languages_loaded).then(function() { - self.$element.html(instance.web.qweb.render('TranslateDialog', { widget: self })); - self.$fields_form = self.$element.find('.oe_translation_form'); - self.$fields_form.find('.oe_trad_field').change(function() { - $(this).toggleClass('touched', ($(this).val() != $(this).attr('data-value'))); - }); - }); - return this; - }, - on_languages_loaded: function(langs) { - this.languages = langs; - this.languages_loaded.resolve(); - }, - do_load_fields_values: function(callback) { - var self = this, - deffered = []; - this.$fields_form.find('.oe_trad_field').val('').removeClass('touched'); - _.each(self.languages, function(lg) { - var deff = $.Deferred(); - deffered.push(deff); - var callback = function(values) { - _.each(self.translatable_fields_keys, function(f) { - self.$fields_form.find('.oe_trad_field[name="' + lg.code + '-' + f + '"]').val(values[0][f] || '').attr('data-value', values[0][f] || ''); - }); - deff.resolve(); - }; - if (lg.code === self.view_language) { - var values = {}; - _.each(self.translatable_fields_keys, function(field) { - values[field] = self.view.fields[field].get_value(); - }); - callback([values]); - } else { - self.rpc('/web/dataset/get', { - model: self.view.dataset.model, - ids: [self.view.datarecord.id], - fields: self.translatable_fields_keys, - context: self.view.dataset.get_context({ - 'lang': lg.code - })}, callback); - } - }); - $.when.apply(null, deffered).then(callback); - }, - open: function(field) { - var self = this, - sup = this._super; - $.when(this.languages_loaded).then(function() { - if (self.view.translatable_fields && self.view.translatable_fields.length) { - self.do_load_fields_values(function() { - sup.call(self); - if (field) { - var $field_input = self.$element.find('tr[data-field="' + field.name + '"] td:nth-child(2) *:first-child'); - self.$element.scrollTo($field_input); - $field_input.focus(); - } - }); - } else { - sup.call(self); - } - }); - }, - on_button_Save: function() { - var trads = {}, - self = this, - trads_mutex = new $.Mutex(); - self.$fields_form.find('.oe_trad_field.touched').each(function() { - var field = $(this).attr('name').split('-'); - if (!trads[field[0]]) { - trads[field[0]] = {}; - } - trads[field[0]][field[1]] = $(this).val(); - }); - _.each(trads, function(data, code) { - if (code === self.view_language) { - _.each(data, function(value, field) { - self.view.fields[field].set_value(value); - }); - } - trads_mutex.exec(function() { - return self.view.dataset.write(self.view.datarecord.id, data, { context : { 'lang': code } }); + e.preventDefault(); + e.stopPropagation(); + var self = this; + var $e = $(e.currentTarget); + if (confirm(_t("Do you really want to delete this attachment ?"))) { + (new instance.web.DataSet(this, 'ir.attachment')).unlink([parseInt($e.attr('data-id'), 10)]).then(function() { + self.do_attachement_update(self.dataset, self.model_id); }); - }); - this.close(); - }, - on_button_Close: function() { - this.close(); + } } }); instance.web.View = instance.web.Widget.extend({ - template: "EmptyComponent", // name displayed in view switchers display_name: '', /** @@ -1006,33 +1142,36 @@ instance.web.View = instance.web.Widget.extend({ this.view_id = view_id; this.set_default_options(options); }, - start: function() { + start: function () { return this.load_view(); }, - load_view: function() { + load_view: function(context) { + var self = this; + var view_loaded; if (this.embedded_view) { - var def = $.Deferred(); - var self = this; - $.async_when().then(function() {def.resolve(self.embedded_view);}); - return def.pipe(this.on_loaded); + view_loaded = $.Deferred(); + $.async_when().then(function() { + view_loaded.resolve(self.embedded_view); + }); } else { - var context = new instance.web.CompoundContext(this.dataset.get_context()); if (! this.view_type) console.warn("view_type is not defined", this); - return this.rpc("/web/view/load", { + view_loaded = this.rpc("/web/view/load", { "model": this.dataset.model, "view_id": this.view_id, "view_type": this.view_type, toolbar: !!this.options.$sidebar, - context: context - }).pipe(this.on_loaded); + context: this.dataset.get_context(context) + }); } - }, - /** - * Called after a successful call to fields_view_get. - * Must return a promise. - */ - on_loaded: function(fields_view_get) { + return view_loaded.pipe(function(r) { + self.trigger('view_loaded', r); + // add css classes that reflect the (absence of) access rights + self.$el.addClass('oe_view') + .toggleClass('oe_cannot_create', !self.is_action_enabled('create')) + .toggleClass('oe_cannot_edit', !self.is_action_enabled('edit')) + .toggleClass('oe_cannot_delete', !self.is_action_enabled('delete')); + }); }, set_default_options: function(options) { this.options = options || {}; @@ -1044,12 +1183,6 @@ instance.web.View = instance.web.Widget.extend({ action_views_ids: {} }); }, - open_translate_dialog: function(field) { - if (!this.translate_dialog) { - this.translate_dialog = new instance.web.TranslateDialog(this).start(); - } - this.translate_dialog.open(field); - }, /** * Fetches and executes the action identified by ``action_data``. * @@ -1073,7 +1206,7 @@ instance.web.View = instance.web.Widget.extend({ var context = new instance.web.CompoundContext(dataset.get_context(), action_data.context || {}); var handler = function (r) { - var action = r.result; + var action = r; if (action && action.constructor == Object) { var ncontext = new instance.web.CompoundContext(context); if (record_id) { @@ -1092,7 +1225,9 @@ instance.web.View = instance.web.Widget.extend({ /* niv: previously we were overriding once more with action_data.context, * I assumed this was not a correct behavior and removed it */ - return self.do_action(action, result_handler); + return self.do_action(action, { + on_close: result_handler, + }); }, null); } else { return result_handler(); @@ -1100,7 +1235,7 @@ instance.web.View = instance.web.Widget.extend({ }; if (action_data.special) { - return handler({result: {"type":"ir.actions.act_window_close"}}); + return handler({"type":"ir.actions.act_window_close"}); } else if (action_data.type=="object") { var args = [[record_id]], additional_args = []; if (action_data.args) { @@ -1114,11 +1249,11 @@ instance.web.View = instance.web.Widget.extend({ } } args.push(context); - return dataset.call_button(action_data.name, args, handler); + return dataset.call_button(action_data.name, args).then(handler); } else if (action_data.type=="action") { - return this.rpc('/web/action/load', { action_id: parseInt(action_data.name, 10), context: context, do_not_eval: true}, handler); + return this.rpc('/web/action/load', { action_id: action_data.name, context: context, do_not_eval: true}).then(handler); } else { - return dataset.exec_workflow(record_id, action_data.name, handler); + return dataset.exec_workflow(record_id, action_data.name).then(handler); } }, /** @@ -1132,10 +1267,10 @@ instance.web.View = instance.web.Widget.extend({ this.embedded_view = embedded_view; }, do_show: function () { - this.$element.show(); + this.$el.show(); }, do_hide: function () { - this.$element.hide(); + this.$el.hide(); }, do_push_state: function(state) { if (this.getParent() && this.getParent().do_push_state) { @@ -1146,10 +1281,9 @@ instance.web.View = instance.web.Widget.extend({ }, /** * Switches to a specific view type - * - * @param {String} view view type to switch to */ - do_switch_view: function(view) { + do_switch_view: function() { + this.trigger.apply(this, ['switch_mode'].concat(_.toArray(arguments))); }, /** * Cancels the switch to the current view, switches to the previous one @@ -1158,27 +1292,11 @@ instance.web.View = instance.web.Widget.extend({ * @param {Boolean} [options.created=false] resource was created * @param {String} [options.default=null] view to switch to if no previous view */ - do_prev_view: function (options) { - }, + do_search: function(view) { }, - on_sidebar_import: function() { - var import_view = new instance.web.DataImport(this, this.dataset); - import_view.start(); - }, on_sidebar_export: function() { - var export_view = new instance.web.DataExport(this, this.dataset); - export_view.start(); - }, - on_sidebar_translate: function() { - return this.do_action({ - 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" - }); + new instance.web.DataExport(this, this.dataset).open(); }, sidebar_context: function () { return $.when(); @@ -1189,6 +1307,15 @@ instance.web.View = instance.web.Widget.extend({ */ reload: function () { return $.when(); + }, + /** + * Return whether the user can perform the action ('create', 'edit', 'delete') in this view. + * An action is disabled by setting the corresponding attribute in the view's main element, + * like: <form string="" create="false" edit="false" delete="false"> + */ + is_action_enabled: function(action) { + var attrs = this.fields_view.arch.attrs; + return (action in attrs) ? JSON.parse(attrs[action]) : true; } }); @@ -1216,7 +1343,6 @@ instance.web.xml_to_json = function(node) { } instance.web.json_node_to_xml = function(node, human_readable, indent) { // For debugging purpose, this function will convert a json node back to xml - // Maybe useful for xml view editor indent = indent || 0; var sindent = (human_readable ? (new Array(indent + 1).join('\t')) : ''), r = sindent + '<' + node.tag, @@ -1225,7 +1351,9 @@ instance.web.json_node_to_xml = function(node, human_readable, indent) { if (typeof(node) === 'string') { return sindent + node; } else if (typeof(node.tag) !== 'string' || !node.children instanceof Array || !node.attrs instanceof Object) { - throw("Node a json node"); + throw new Error( + _.str.sprintf("Node [%s] is not a JSONified XML node", + JSON.stringify(node))); } for (var attr in node.attrs) { var vattr = node.attrs[attr]; diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index 14f6f727daf..2ab51a32f03 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -25,11 +25,18 @@ </div> </div> </t> +<t t-name="Tipsy.alert"> + <a class="oe_tooltip_close oe_e">[</a> + <span style="float:left; margin:2px 5px 0 0;" class="ui-icon ui-icon-alert ui-state-error"></span> + <div class="oe_tooltip_message"> + <t t-esc="message"/> + </div> +</t> <t t-name="CrashManager.warning"> - <table cellspacing="0" cellpadding="0" border="0" class="oe-dialog-warning"> + <table cellspacing="0" cellpadding="0" border="0" class="oe_dialog_warning"> <tr> - <td><img t-att-src='_s + "/web/static/src/img/warning.png"' class="oe-dialog-icon"/></td> + <td><img t-att-src='_s + "/web/static/src/img/warning.png"' class="oe_dialog_icon"/></td> <td> <p> <t t-js="d"> @@ -44,29 +51,6 @@ </table> </t> <t t-name="CrashManager.error"> - <t t-if="!session.openerp_entreprise"> - <span>Your version of OpenERP is unsupported. Support & maintenance services are available here: <a href="http://www.openerp.com/support-or-publisher-warranty-contract" target="_blank">OpenERP Entreprise</a>.</span> - </t> - <t t-if="session.openerp_entreprise"> - <div class="oe_error_send"> - <div> - <div class="oe_centeralign"><b>OpenERP Enterprise Contract.</b></div> - <div><br/>Your report will be sent to the OpenERP Enterprise team.<br/></div><br/> - <div> - <label>Summary:</label><br/> - <input id="issuename" type="text" class="oe_fielddiv"/> - </div><br/><br/> - <div> - <label>Description:</label><br/> - <textarea id="explanation" rows="6"></textarea> - </div><br/><br/> - <div> - <label>What you did:</label><br/> - <textarea id="remark" rows="6" ></textarea> - </div> - </div> - </div><br/> - </t> <div class="oe_error_detail"> <pre><t t-esc="error.message"/></pre> <hr/> @@ -77,7 +61,7 @@ <t t-name="Login"> <div class="oe_login"> <div class="oe_login_bottom"> </div> - <div class="oe_login_error_message">Invalid username or password</div> + <div class="oe_login_error_message"/> <div class="oe_login_pane"> <div class="oe_login_logo"><img src='/web/static/src/img/logo2.png'/></div> <form action="" method="post"> @@ -95,7 +79,7 @@ </form> <div class="oe_login_footer"> <a href="#" class="oe_login_manage_db">Manage Databases</a> | - <a href="http://www.openerp.com">Powered by <span>OpenERP</span></a> + <a href="http://www.openerp.com" target="_blank">Powered by <span>OpenERP</span></a> </div> </div> </div> @@ -114,68 +98,80 @@ </select> </t> + <t t-name="DatabaseManager"> - <div class="oe_database_manager"> - <div class="oe_database_manager_menu"> - <ul class="oe_form_notebook"> - <li><a href="#db_create">Create</a></li> - <li><a href="#db_drop">Drop</a></li> - <li><a href="#db_backup">Backup</a></li> - <li><a href="#db_restore">Restore</a></li> - <li><a href="#db_change_password">Password</a></li> - <li><a id="back_to_login" href="#">Back to Login</a></li> - </ul> - <form id="db_create" name="create_db_form" method="POST"> + <div class="oe_view_manager_view_form"> + <div class="oe_form"> + <form id="db_create" name="create_db_form" style="display: block;"> + <div class="oe_view_manager oe_view_manager_current"> + <div class="oe_view_manager_header" style="padding: 8px;"> + <div class="oe_header_row"> + <h2 class="oe_view_title"> + <span class="oe_view_title_text oe_breadcrumb_title">Create Database</span> + </h2> + <button type="submit" class="oe_button oe_highlight db_create">Create</button> + </div> + </div> + </div> <table align="center" class="db_option_table"> <tr> - <th colspan="2" class="option_string"> CREATE DATABASE </th> - </tr> - <tr> <td><label for="super_admin_pwd">Master password:</label></td> - <td><input type="password" name="super_admin_pwd" class="required" value="admin"/></td> + <td><input type="password" name="super_admin_pwd" class="required" value="admin" /></td> </tr> <tr> <td><label for="db_name">New database name:</label></td> - <td><input type="text" name="db_name" class="required" matches="^[a-zA-Z][a-zA-Z0-9_]+$" autofocus="true"/></td> + <td><input type="text" name="db_name" class="required" matches="^[a-zA-Z][a-zA-Z0-9_-]+$" autofocus="true"/></td> </tr> <tr> <td><label for="demo_data">Load Demonstration data:</label></td> - <td><input type="checkbox" name="demo_data"/></td> + <td class="oe_form_group_cell"> + <span class="oe_form_field oe_form_field_boolean"> + <input type="checkbox" name="demo_data" /> + </span> + </td> </tr> <tr> <td><label for="db_lang">Default language:</label></td> - <td> + <td class="oe_form_field oe_form_field_selection "> <select name="db_lang" t-if="widget.lang_list"> <t t-foreach="widget.lang_list" t-as="lang"> - <option t-att-value="lang[0]" t-att-selected="lang[0] === 'en_US' ? 'selected' : undefined"><t t-esc="lang[1]"/></option> + <option t-att-value="lang[0]" t-att-selected="lang[0] === 'en_US' ? 'selected' : undefined"> + <t t-esc="lang[1]" /> + </option> </t> </select> </td> </tr> <tr> <td><label for="create_admin_pwd">Admin password:</label></td> - <td><input type="password" name="create_admin_pwd" class="required"/></td> + <td><input type="password" name="create_admin_pwd" class="required" /></td> </tr> <tr> <td><label for="create_confirm_pwd">Confirm password:</label></td> <td><input type="password" name="create_confirm_pwd" class="required" equalTo="input[name=create_admin_pwd]"/></td> </tr> - <tr> - <td colspan="2" align="right"><button class="oe_button">Create</button></td> - </tr> </table> </form> - <form id="db_drop" name="drop_db_form" method="POST"> + <form id="db_drop" name="drop_db_form" style="display: none; "> + <div class="oe_view_manager oe_view_manager_current"> + <div class="oe_view_manager_header" style="padding: 8px;"> + <div class="oe_header_row"> + <h2 class="oe_view_title"> + <span class="oe_view_title_text oe_breadcrumb_title">Drop Database</span> + </h2> + <button type="submit" class="oe_button oe_highlight db_drop">Drop</button> + </div> + </div> + </div> <table align="center" class="db_option_table"> <tr> - <th colspan="2" class="option_string"> DROP DATABASE </th> - </tr> - <tr> <td><label for="drop_db">Database:</label></td> - <td> + <td class="oe_form_field oe_form_field_selection"> <select t-if="widget.db_list" name="drop_db" autofocus="autofocus"> <t t-foreach="widget.db_list" t-as="db"> - <option t-att-value="db"><t t-esc="db"/></option> + <option t-att-value="db"> + <t t-esc="db" /> + </option> </t> </select> <input t-if="!widget.db_list" name="drop_db" class="required" type="text" autofocus="autofocus"/> @@ -183,25 +179,31 @@ </tr> <tr> <td><label for="drop_password">Master Password:</label></td> - <td><input type="password" name="drop_pwd" class="required"/></td> - </tr> - <tr> - <td colspan="2" align="right"><button class="oe_button">Drop</button></td> + <td><input type="password" name="drop_pwd" class="required" /></td> </tr> </table> </form> - <form id="db_backup" name="backup_db_form" method="POST" target="backup-target" action="/web/database/backup"> - <input type="hidden" name="token"/> + <form id="db_backup" name="backup_db_form" target="backup-target" action="/web/database/backup" style="display: none;"> + <div class="oe_view_manager oe_view_manager_current"> + <div class="oe_view_manager_header" style="padding: 8px;"> + <div class="oe_header_row"> + <h2 class="oe_view_title"> + <span class="oe_view_title_text oe_breadcrumb_title">Backup Database</span> + </h2> + <button type="submit" class="oe_button oe_highlight db_backup">Backup</button> + </div> + </div> + </div> + <input type="hidden" name="token" /> <table align="center" class="db_option_table"> <tr> - <th colspan="2" class="option_string"> BACKUP DATABASE </th> - </tr> - <tr> <td><label for="backup_db">Database:</label></td> - <td> + <td class="oe_form_field oe_form_field_selection "> <select t-if="widget.db_list" name="backup_db" autofocus="autofocus"> <t t-foreach="widget.db_list" t-as="db"> - <option t-att-value="db"><t t-esc="db"/></option> + <option t-att-value="db"> + <t t-esc="db" /> + </option> </t> </select> <input t-if="!widget.db_list" name="backup_db" class="required" type="text" autofocus="autofocus"/> @@ -209,19 +211,23 @@ </tr> <tr> <td><label for="backup_pwd">Master Password:</label></td> - <td><input type="password" name="backup_pwd" class="required"/></td> - </tr> - <tr> - <td colspan="2" align="right"><button class="oe_button">Backup</button></td> + <td><input type="password" name="backup_pwd" class="required" /></td> </tr> </table> </form> - <form id="db_restore" name="restore_db_form" method="POST"> + <form id="db_restore" name="restore_db_form" style="display: none; "> + <div class="oe_view_manager oe_view_manager_current"> + <div class="oe_view_manager_header" style="padding: 8px;"> + <div class="oe_header_row"> + <h2 class="oe_view_title"> + <span class="oe_view_title_text oe_breadcrumb_title">Restore Database</span> + </h2> + <button type="submit" class="oe_button oe_highlight db_restore">Restore</button> + </div> + </div> + </div> <table align="center" class="db_option_table"> <tr> - <th colspan="2" class="option_string"> RESTORE DATABASE </th> - </tr> - <tr> <td><label for="restore_db">File:</label></td> <td><input type="file" name="db_file" class="required" autofocus="autofocus"/></td> </tr> @@ -233,17 +239,21 @@ <td><label for="new_db">New database name:</label></td> <td><input type="text" name="new_db" class="required"/></td> </tr> - <tr> - <td colspan="2" align="right"><button class="oe_button">Restore</button></td> - </tr> </table> </form> - <form id="db_change_password" name="change_pwd_form" method="POST"> + <form id="db_change_password" name="change_pwd_form" style="display: none;"> + <div class="oe_view_manager oe_view_manager_current"> + <div class="oe_view_manager_header" style="padding: 8px;"> + <div class="oe_header_row"> + <h2 class="oe_view_title"> + <span class="oe_view_title_text oe_breadcrumb_title">Change Master Password</span> + </h2> + <button type="submit" class="oe_button oe_highlight db-change-password">Change Password</button> + </div> + </div> + </div> <table align="center" class="db_option_table"> <tr> - <th colspan="2" class="option_string"> CHANGE MASTER PASSWORD </th> - </tr> - <tr> <td><label for="old_pwd">Master password:</label></td> <td><input type="password" name="old_pwd" class="required" minlength="1" autofocus="autofocus"/></td> </tr> @@ -253,28 +263,67 @@ </tr> <tr> <td><label for="confirm_pwd">Confirm new master password:</label></td> - <td><input type="password" name="confirm_pwd" class="required" equalTo="input[name=new_pwd]" minlength="1"/></td> - </tr> - <tr> - <td colspan="2" align="right"><button class="oe_button">Change Password</button></td> + <td><input type="password" name="confirm_pwd" class="required" equalTo="input[name=new_pwd]" minlength="1"/> </td> </tr> </table> </form> </div> </div> </t> +<t t-name="DatabaseManager.user_menu"> + <span class="oe_right"> + <a id="back-to-login" href="#"><span class="oe_topbar_item oe_topbar_name">Back to Login</span></a> + </span> +</t> +<t t-name="DatabaseManager.menu"> + <div class="oe_secondary_menu_section">Database Management</div> + <ul class="oe_secondary_submenu"> + <li><a href="#db_create">Create</a></li> + <li><a href="#db_drop">Drop</a></li> + <li><a href="#db_backup">Backup</a></li> + <li><a href="#db_restore">Restore</a></li> + <li><a href="#db_change_password">Password</a></li> + </ul> +</t> + +<t t-name="ChangePassword"> + <form name="change_password_form" method="POST"> + <table align="center"> + <tr> + <td><label for="old_pwd">Old Password:</label></td> + <td><input type="password" name="old_pwd" + minlength="1" autofocus="autofocus"/></td> + </tr> + <tr> + <td><label for="new_password">New Password:</label></td> + <td><input type="password" name="new_password" + minlength="1"/></td> + </tr> + <tr> + <td><label for="confirm_pwd">Confirm Password:</label></td> + <td><input type="password" name="confirm_pwd" + minlength="1"/></td> + </tr> + <tr> + <td colspan="2" align="right"><button class="oe_button">Change Password</button></td> + </tr> + </table> + </form> +</t> <t t-name="Menu"> <ul class="oe_menu" t-if="widget.data"> <li t-foreach="widget.data.data.children" t-as="menu"> - <t t-call="Menu.secondary.link"/> + <t t-call="Menu.link"/> </li> </ul> </t> <t t-name="Menu.more"> <li class="oe_menu_more_container"> - <a href="#" class="oe_menu_more_link oe_dropdown_toggle">More</a> - <ul class="oe_menu_more" style="display: none;"/> + <span class="oe_topbar_item oe_menu_more_link oe_dropdown_toggle oe_dropdown_arrow"> + More + <ul class="oe_menu_more oe_dropdown_menu"/> + </span> </li> </t> <t t-name="Menu.secondary"> @@ -284,7 +333,7 @@ <t t-esc="menu.name"/> <!-- Shall the section be still clickable ? - <t t-call="Menu.secondary.link"/> + <t t-call="Menu.link"/> --> </div> <t t-call="Menu.secondary.submenu"/> @@ -294,21 +343,21 @@ <t t-name="Menu.secondary.submenu"> <ul t-if="menu.children.length" class="oe_secondary_submenu"> <li t-foreach="menu.children" t-as="menu"> - <t t-call="Menu.secondary.link"/> + <t t-call="Menu.link"/> <!--<span class="oe_menu_label">8</span>--> <t t-call="Menu.secondary.submenu"/> </li> </ul> </t> -<t t-name="Menu.secondary.link"> - <a t-attf-href="#menu_id=#{menu.id}&action_id=#{menu.action ? menu.action.split(',')[1] : ''}" +<t t-name="Menu.link"> + <a t-attf-href="#menu_id=#{menu.id}&action=#{menu.action ? menu.action.split(',')[1] : ''}" t-att-class="menu.children.length ? 'oe_menu_toggler' : 'oe_menu_leaf'" t-att-data-menu="menu.id" t-att-data-action-model="menu.action ? menu.action.split(',')[0] : ''" t-att-data-action-id="menu.action ? menu.action.split(',')[1] : ''"> <t t-esc="menu.name"/> <t t-if="menu.needaction_enabled and menu.needaction_counter"> - <div class="oe_menu_counter"> + <div class="oe_tag oe_tag_dark oe_menu_counter"> <t t-esc="menu.needaction_counter"/> </div> </t> @@ -316,63 +365,35 @@ </t> <t t-name="UserMenu"> - <ul class="oe_user_menu oe_topbar_item"> - <li class="oe_dropdown"> - <a href="#" class="oe_dropdown_toggle"> - <img class="oe_topbar_avatar" t-att-data-default-src="_s + '/web/static/src/img/user_menu_avatar.png'"/> - <span class="oe_topbar_name"/> - </a> - <ul class="oe_dropdown_options"> - <li><a href="#" data-menu="about">About OpenERP</a></li> - <li><a href="#" data-menu="settings">Preferences</a></li> - <li><a href="#" data-menu="logout">Log out</a></li> - </ul> - </li> - </ul> + <span class="oe_user_menu oe_topbar_item oe_dropdown_toggle oe_dropdown_arrow"> + <img class="oe_topbar_avatar" t-att-data-default-src="_s + '/web/static/src/img/user_menu_avatar.png'"/> + <span class="oe_topbar_name"/> + <ul class="oe_dropdown_menu"> + <li><a href="#" data-menu="about">About OpenERP</a></li> + <li><a href="#" data-menu="settings">Preferences</a></li> + <li><a href="#" data-menu="logout">Log out</a></li> + </ul> + </span> </t> <t t-name="UserMenu.about"> - <div> - <a class="oe_activate_debug_mode" href="?debug" style="float:right; font-size: 80%;">Activate the developper mode</a> - <h1 style="margin:0;">OpenERP</h1> - <h3 style="margin:15px 0;padding:0;">Version <t t-esc="version_info.version"/></h3> - <p> - Copyright © 2004-TODAY OpenERP SA. All Rights Reserved.<br /> - OpenERP is a trademark of the <a target="_blank" href="http://openerp.com/" style="text-decoration: underline;">OpenERP SA Company</a>. - </p> - <p> - Licenced under the terms of <a target="_blank" href="http://www.gnu.org/licenses/agpl.html" style="text-decoration: underline;">GNU Affero General Public License</a> - </p> - <p> - For more information visit <a target="_blank" href="http://openerp.com/" style="text-decoration: underline;">OpenERP.com</a> - </p> + + <div class="oe_about"> + <a class="oe_activate_debug_mode oe_right" href="?debug" style="background-color: white; padding:2px 6px; border-radius: 10px;">Activate the developer mode</a> + <img class="oe_logo" src="/web/static/src/img/logo2.png"/> + <h3>Version <t t-esc="version_info.version"/></h3> + + <div class="oe_bottom"> + <p>Copyright © 2004-TODAY OpenERP SA. All Rights Reserved.<br /> + OpenERP is a trademark of the <a target="_blank" href="http://openerp.com/" style="text-decoration: underline;">OpenERP SA Company</a>.</p> + <p>Licenced under the terms of <a target="_blank" href="http://www.gnu.org/licenses/agpl.html" style="text-decoration: underline;">GNU Affero General Public License</a></p> + <p>For more information visit <a target="_blank" href="http://openerp.com/" style="text-decoration: underline;">OpenERP.com</a></p> + </div> </div> -</t> -<t t-name="UserMenu.password"> - <form name="change_password_form" method="POST"> - <table align="center"> - <tr> - <td><label for="old_pwd">Old Password:</label></td> - <td><input type="password" name="old_pwd" - minlength="1" autofocus="autofocus"/></td> - </tr> - <tr> - <td><label for="new_password">New Password:</label></td> - <td><input type="password" name="new_password" - minlength="1" autofocus="autofocus"/></td> - </tr> - <tr> - <td><label for="confirm_pwd">Confirm Password:</label></td> - <td><input type="password" name="confirm_pwd" - minlength="1"/></td> - </tr> - <tr> - <td colspan="2" align="right"><button class="oe_button">Change Password</button></td> - </tr> - </table> - </form> + </t> <t t-name="WebClient"> + <div class="openerp openerp_webclient_container"> <table class="oe_webclient"> <tr> <td colspan="2" class="oe_topbar"> @@ -383,34 +404,39 @@ </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> + <a class="oe_logo" href="#"><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> + Powered by <a href="http://www.openerp.com" target="_blank"><span>OpenERP</span></a> </div> </td> <td class="oe_application"> </td> </tr> </table> + </div> +</t> + +<t t-name="EmbedClient"> + <div class="openerp"> + <div class="oe_application"></div> + </div> </t> <t t-name="ViewManager"> <div class="oe_view_manager"> <table class="oe_view_manager_header"> - <col width="33%"/> - <col width="33%"/> - <col width="34%"/> - <tr class="oe_header_row"> + <col width="20%"/> + <col width="35%"/> + <col width="15%"/> + <col width="30%"/> + <tr class="oe_header_row oe_header_row_top"> <td colspan="2"> <h2 class="oe_view_title" t-if="widget.flags.display_title !== false"> - <span class="oe_view_title_text"><t t-esc="widget.display_title()"/></span> + <span class="oe_view_title_text oe_breadcrumb_title"/> </h2> </td> - <td> + <td colspan="2"> <div class="oe_view_manager_view_search" t-opentag="true"/> </td> </tr> @@ -418,13 +444,13 @@ <td> <div class="oe_view_manager_buttons"/> </td> - <td> + <td colspan="2"> <div class="oe_view_manager_sidebar"/> </td> <td> <ul class="oe_view_manager_switch oe_button_group oe_right"> - <t t-if="widget.views_src.length > 2" t-foreach="widget.views_src" t-as="view"> - <li t-if="view.view_type != 'form'" class="oe_e"><a t-attf-class="oe_vm_switch_#{view.view_type}" t-att-data-view-type="view.view_type" t-att-title="view.label || view.view_type"> + <t t-if="widget.views_src.length > 1" t-foreach="widget.views_src" t-as="view"> + <li class="oe_e"><a t-attf-class="oe_vm_switch_#{view.view_type}" t-att-data-view-type="view.view_type" t-att-title="_t('Switch to ') + (view.label || view.view_type)"> </a></li> </t> </ul> @@ -433,7 +459,6 @@ </tr> </table> - <div class="oe_view_manager_body"> <t t-foreach="widget.views_src" t-as="view"> <div t-attf-class="oe_view_manager_view_#{view.view_type}"/> @@ -451,15 +476,18 @@ <option value="">Debug View#<t t-esc="view.fields_view.view_id"/></option> <option t-if="view_manager.active_view === 'form'" value="perm_read" data-views="form,page">View Log (perm_read)</option> <option t-if="view_manager.active_view === 'form'" value="toggle_layout_outline">Toggle Form Layout Outline</option> + <option value="tests">JS Tests</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="translate">Technical translation</option> <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> <option t-if="view_manager.searchview" value="edit" data-model="ir.ui.view" t-att-data-id="view_manager.searchview.view_id">Edit SearchView</option> <option t-if="view_manager.action" value="edit" t-att-data-model="view_manager.action.type" t-att-data-id="view_manager.action.id">Edit Action</option> <option value="edit_workflow">Edit Workflow</option> + <option value="print_workflow">Print Workflow</option> </t> </t> <t t-name="ViewManagerDebugViewLog"> @@ -511,66 +539,45 @@ <div class="oe_sidebar"> <t t-foreach="widget.sections" t-as="section"> <div class="oe_form_dropdown_section"> - <button class="oe_dropdown_toggle"><t t-esc="section.label"/></button> + <button class="oe_dropdown_toggle oe_dropdown_arrow"> + <t t-if="section.name == 'files'" t-raw="widget.items[section.name].length || ''"/> + <t t-esc="section.label"/> + </button> <ul class="oe_dropdown_menu"> <li t-foreach="widget.items[section.name]" t-as="item" t-att-class="item.classname"> <a class="oe_sidebar_action_a" t-att-title="item.title" t-att-data-section="section.name" t-att-data-index="item_index" t-att-href="item.url" target="_blank"> <t t-raw="item.label"/> </a> + <a t-if="section.name == 'files' and !item.callback" class="oe_sidebar_delete_item" t-att-data-id="item.id" title="Delete this attachment">x</a> </li> - <li t-if="section.name == 'files'" class="oe_sidebar_add" style="display:none;"> - Input type file stuff + <li t-if="section.name == 'files'" class="oe_sidebar_add_attachment"> + <t t-call="HiddenInputFile"> + <t t-set="fileupload_id" t-value="widget.fileupload_id"/> + <t t-set="fileupload_action">/web/binary/upload_attachment</t> + <input type="hidden" name="model" t-att-value="widget.dataset and widget.dataset.model"/> + <input type="hidden" name="id" t-att-value="widget.model_id"/> + <input type="hidden" name="session_id" t-att-value="widget.session.session_id"/> + <span>Add...</span> + </t> </li> </ul> </div> </t> </div> </t> -<t t-name="Sidebar.attachments"> - <div class="oe-sidebar-attachments-toolbar"> - <div class="oe-binary-file-set" style="float: right"> - <form class="oe-binary-form" t-attf-target="#{element_id}_iframe" - method="post" enctype="multipart/form-data" action="/web/binary/upload_attachment"> - <input type="hidden" name="session_id" t-att-value="session.session_id"/> - <input type="hidden" name="callback" t-attf-value="#{element_id}_iframe"/> - <input type="hidden" name="model" t-att-value="view.dataset.model"/> - <input type="hidden" name="id" t-att-value="view.datarecord.id"/> - <button class="oe_button" type="button"> - <img t-att-src='_s + "/web/static/src/img/throbber.gif"' width="16" height="16" style="display: none"/> - <span>Add</span> - </button> - <input type="file" class="oe-binary-file" name="ufile" title="Add attachment" - t-att-onclick="view.datarecord.id ? null : 'alert(\'No record selected ! You can only attach to existing record.\'); return false;'"/> - </form> - <iframe t-attf-id="#{element_id}_iframe" t-attf-name="#{element_id}_iframe" style="display: none"/> - </div> - </div> - <br style="clear: both"/> - <ul class="oe-sidebar-attachments-items"> - <li t-foreach="attachments" t-as="attachment"> - <t t-if="attachment.type == 'binary'" t-set="attachment.url" t-value="_s + '/web/binary/saveas?session_id=' - + session.session_id + '&model=ir.attachment&id=' + attachment.id - + '&field=datas&filename_field=name&t=' + (new Date().getTime())"/> - <a class="oe-sidebar-attachments-link" t-att-href="attachment.url" target="_blank"> - <t t-esc="attachment.name"/> - </a> - <a href="#" class="oe-sidebar-attachment-delete" t-att-data-id="attachment.id" t-attf-title="Delete the attachment #{attachment.name}"> - <img t-att-src='_s + "/web/static/src/img/attachments-close.png"' width="15" height="15" border="0"/> - </a> - </li> - </ul> -</t> <t t-name="TreeView"> - <select t-if="toolbar" style="width: 30%"> - </select> + <div class = "tree_header"> + <select t-if="toolbar" ></select> + <button id = "tree_view_collapse">Collapse All</button> + <button id = "tree_view_expand">Expand All</button> + </div> <table class="oe-treeview-table"> <thead> <tr> <th t-foreach="fields_view" t-as="field" - t-if="!field.attrs.modifiers.tree_invisible" - class="treeview-header"> - <t t-esc="fields[field.attrs.name].string" /> + t-if="!field.attrs.modifiers.tree_invisible"> + <t t-esc="field_value.attrs.string || fields[field.attrs.name].string" /> </th> </tr> </thead> @@ -581,17 +588,17 @@ <tr t-name="TreeView.rows" t-foreach="records" t-as="record" t-att-id="'treerow_' + record.id" - t-att-data-id="record.id" t-att-data-level="level + 1"> + t-att-data-id="record.id" t-att-data-level="level"> <t t-set="children" t-value="record[children_field]"/> <t t-set="class" t-value="children and children.length ? 'treeview-tr' : 'treeview-td'"/> <t t-set="rank" t-value="'oe-treeview-first'"/> - <t t-set="style" t-value="'background-position: ' + 19*level + 'px; padding-left: ' + 19*level + 'px;'"/> + <t t-set="style" t-value="'background-position: ' + 19*(level-1) + 'px; padding-left: ' + (4 + 19*(level-1)) + 'px;'"/> <td t-foreach="fields_view" t-as="field" t-if="!field.attrs.modifiers.tree_invisible" t-att-data-id="record.id" t-att-style="color_for(record) + style " - t-attf-class="#{class} #{rank} #{(fields[field.attrs.name].type === 'float') or (fields[field.attrs.name].type === 'integer') ? 'oe-number' : ''}"> + t-attf-class="#{class} #{rank} #{(fields[field.attrs.name].type === 'float') or (fields[field.attrs.name].type === 'integer') ? 'oe_number' : ''}"> <span t-if="!field.attrs.modifiers.invisible" > <t t-esc="render(record[field.attrs.name], fields[field.attrs.name])" /> @@ -603,7 +610,7 @@ </td> </tr> -<table t-name="ListView" class="oe-listview-content"> +<table t-name="ListView" class="oe_list_content"> <t t-set="columns_count" t-value="visible_columns.length + (options.selectable ? 1 : 0) + (options.deletable ? 1 : 0)"/> <thead> <tr t-if="(!!options.action_buttons and !options.$buttons) or (!!options.pager and !options.$pager)"> @@ -613,28 +620,28 @@ <div class="oe_list_pager"/> </th> </tr> - <tr t-if="options.header" class="oe-listview-header-columns"> + <tr t-if="options.header" class="oe_list_header_columns"> <t t-foreach="columns" t-as="column"> <th t-if="column.meta"> <t t-esc="column.string"/> </th> </t> <th t-if="options.selectable" width="1" > - <input type="checkbox" class="all-record-selector"/> + <input type="checkbox" class="oe_list_record_selector"/> </th> <t t-foreach="columns" t-as="column"> <th t-if="!column.meta and column.invisible !== '1'" t-att-data-id="column.id" - t-att-class="((options.sortable and column.tag !== 'button') ? 'oe-sortable' : null)"> + t-attf-class="oe_list_header_#{column.widget or column.type} #{((options.sortable and column.tag !== 'button') ? 'oe_sortable' : null)}"><div> <t t-if="column.tag !== 'button'"><t t-esc="column.string"/></t> - </th> + </div></th> </t> - <th t-if="options.deletable" width="1"/> + <th t-if="options.deletable" class="oe_list_record_delete" width="13px"/> </tr> </thead> <tfoot> <tr> <td t-if="options.selectable"/> - <td t-foreach="aggregate_columns" t-as="column" class="oe-list-footer oe-number" + <td t-foreach="aggregate_columns" t-as="column" class="oe_list_footer oe_number" t-att-data-field="column.id" t-att-title="column.label"> </td> <td t-if="options.deletable"/> @@ -642,17 +649,16 @@ </tfoot> </table> <div t-name="ListView.buttons" class="oe_list_buttons"> - <t t-if="!widget.no_leaf and widget.options.action_buttons !== false"> - <button type="button" class="oe_button oe_list_add oe_form_button_hi" t-if="widget.options.addable"> + <t t-if="!widget.no_leaf and widget.options.action_buttons !== false and widget.options.addable and widget.is_action_enabled('create')"> + <button type="button" class="oe_button oe_list_add oe_highlight"> <t t-esc="widget.options.addable"/> </button> - <span class="oe_fade">or</span> <a href="#" class="oe_bold oe_list_button_import">Import</a> </t> </div> <t t-name="ListView.pager"> <div class="oe_list_pager" t-att-colspan="widget.columns_count"> <t t-if="!widget.no_leaf and widget.options.pager !== false" t-call="ViewPager"> - <span class="oe-pager-state"> + <span class="oe_list_pager_state"> </span> </t> </div> @@ -660,35 +666,56 @@ <t t-name="ListView.rows" t-foreach="records.length" t-as="index"> <t t-call="ListView.row"> <t t-set="record" t-value="records.at(index)"/> - <t t-set="row_parity" t-value="index_parity"/> </t> </t> -<tr t-name="ListView.row" t-att-class="row_parity" +<tr t-name="ListView.row" t-att-data-id="record.get('id')" t-att-style="view.style_for(record)"> + <t t-set="asData" t-value="record.toForm().data"/> <t t-foreach="columns" t-as="column"> - <td t-if="column.meta"> - - </td> + <td t-if="column.meta"> </td> </t> - <th t-if="options.selectable" class="oe-record-selector" width="1"> + <th t-if="options.selectable" class="oe_list_record_selector" width="1"> <t t-set="checked" t-value="options.select_view_id == record.get('id') ? 'checked' : null"/> <input t-if="options.radio" type="radio" name="radiogroup" t-att-checked="checked"/> <input t-if="!options.radio" type="checkbox" name="radiogroup" t-att-checked="checked"/> </th> <t t-foreach="columns" t-as="column"> - <t t-set="align" t-value="column.type === 'integer' or column.type == 'float'"/> + <t t-set="number" t-value="column.type === 'integer' or column.type == 'float'"/> + <t t-set="modifiers" t-value="column.modifiers_for(asData)"/> <td t-if="!column.meta and column.invisible !== '1'" t-att-title="column.help" - t-att-class="'oe-field-cell' + (align ? ' oe-number' : '') - + (column.tag === 'button' ? ' oe-button' : '')" - t-att-data-field="column.id"> - <t t-raw="render_cell(record, column)"/> - </td> + t-attf-class="oe_list_field_cell oe_list_field_#{column.widget or column.type} #{number ? 'oe_number' : ''} #{column.tag === 'button' ? 'oe-button' : ''} #{modifiers.readonly ? 'oe_readonly' : ''} #{modifiers.required ? 'oe_required' : ''}" + t-att-data-field="column.id" + ><t t-raw="render_cell(record, column)"/></td> </t> - <td t-if="options.deletable" class='oe-record-delete' width="1"> + <td t-if="options.deletable" class='oe_list_record_delete' width="13px"> <button type="button" name="delete" class="oe_i">d</button> </td> </tr> +<t t-extend="ListView.buttons"> + <t t-jquery="button.oe_list_add" t-operation="after"> + <button class="oe_button oe_list_save oe_highlight" + type="button">Save</button> + <span class="oe_alternative"> + <span class="oe_fade">or</span> + <a href="#" class="oe_bold oe_list_discard">Discard</a> + </span> + </t> +</t> +<button t-name="ListView.row.button" type="button" + t-att-title="widget.string" t-att-disabled="disabled || undefined" + t-att-class="disabled ? 'oe_list_button_disabled' : undefined" + ><img t-attf-src="#{prefix}/web/static/src/img/icons/#{widget.icon}.png" + t-att-alt="widget.string" /></button> +<t t-extend="ListView.row"> + <!-- adds back padding to row being rendered after edition, if necessary + (if not deletable add back padding), otherwise the row being added is + missing columns + --> + <t t-jquery="> :last" t-operation="after"> + <td t-if="edited and !options.deletable" class="oe-listview-padding"/> + </t> +</t> <t t-name="FormView"> <div class="oe_formview"> @@ -701,18 +728,24 @@ <div t-name="FormView.buttons" class="oe_form_buttons"> <t t-if="widget.options.action_buttons !== false"> <span class="oe_form_buttons_view"> - <button type="button" class="oe_button oe_form_button_edit">Edit</button> - <button type="button" class="oe_button oe_form_button_create">Create</button> + <!-- required for the bounce effect on button --> + <div t-if="widget.is_action_enabled('edit')" style="display: inline-block;"> + <button type="button" class="oe_button oe_form_button_edit" accesskey="E">Edit</button> + </div> + <button t-if="widget.is_action_enabled('create')" + type="button" class="oe_button oe_form_button_create" accesskey="C">Create</button> </span> <span class="oe_form_buttons_edit"> - <button type="button" class="oe_button oe_form_button_save">Save</button> <span class="oe_fade">or</span> <a href="#" class="oe_bold oe_form_button_cancel">Discard</a> + <button type="button" class="oe_button oe_form_button_save oe_highlight" accesskey="S">Save</button> + <span class="oe_fade">or</span> + <a href="#" class="oe_bold oe_form_button_cancel" accesskey="D">Discard</a> </span> </t> </div> <t t-name="FormView.pager"> <div class="oe_form_pager"> <t t-if="widget.options.pager !== false" t-call="ViewPager"> - <span class="oe_pager_index">0</span><span class="oe_pager_separator"> / </span><span class="oe_pager_count">0</span> + <span class="oe_form_pager_state"></span> </t> </div> </t> @@ -773,37 +806,25 @@ </table> </form> -<t t-name="FormRenderingForm.manual"> - <div class="oe_form_manual_layout"/> -</t> <t t-name="FormRenderingForm"> + <div t-attf-class="#{classnames}"> + </div> +</t> +<t t-name="FormRenderingSheet"> <div class="oe_form_sheetbg"> <div t-attf-class="oe_form_sheet oe_form_sheet_width #{classnames}"> - <group t-if="layout === 'auto'" col="4"/> </div> </div> </t> -<t t-name="FormRenderingSheet" t-extend="FormRenderingForm"> -</t> <t t-name="FormRenderingGroup"> - <t t-set="table"> - <table border="0" cellpadding="0" cellspacing="0" width="100%" t-att-class="classnames"/> - </t> - <t t-if="string"> - <fieldset class="oe_group_box"> - <legend><t t-esc="string"/></legend> - <t t-raw="table"/> - </fieldset> - </t> - <t t-if="!string"> - <t t-raw="table"/> - </t> + <t t-if="string" t-call="FormRenderingSeparator"/> + <table border="0" cellpadding="0" cellspacing="0" t-attf-class="oe_form_group #{classnames}"/> </t> <t t-name="FormRenderingNotebook"> - <div> - <ul t-attf-class="oe_form_notebook #{classnames}"> + <div class="oe_clear"> + <ul t-attf-class="oe_notebook #{classnames}"> <li t-foreach="pages" t-as="page" t-att-modifiers="page.modifiers"> - <a t-attf-href="##{page.id}"> + <a t-attf-href="##{page.id}" t-att-accesskey="page.accesskey"> <t t-esc="page.string"/> </a> </li> @@ -811,12 +832,11 @@ </div> </t> <t t-name="FormRenderingNotebookPage"> - <div t-attf-class="oe_form_notebook_page #{classnames}" t-att-id="id"> - <group t-if="layout === 'auto'" col="4"/> + <div t-attf-class="oe_notebook_page #{classnames}" t-att-id="id"> </div> </t> <t t-name="FormRenderingSeparator"> - <div t-attf-class="oe_horizontal_separator #{classnames}"> + <div t-attf-class="oe_horizontal_separator oe_clear #{classnames}"> <t t-esc="string"/> </div> </t> @@ -895,172 +915,151 @@ </ul> </t> <t t-name="FieldChar"> - <t t-if="!widget.get('effective_readonly')"> - <div class="oe_form_field_char"> + <span t-att-class="'oe_form_field '+widget.widget_class" t-att-style="widget.node.attrs.style"> + <t t-if="!widget.get('effective_readonly')"> <input t-att-type="widget.password ? 'password' : 'text'" t-att-id="widget.id_for_label" t-att-tabindex="widget.node.attrs.tabindex" t-att-autofocus="widget.node.attrs.autofocus" - t-attf-class="field_char #{widget.is_field_number ? 'oe-number' : ''}" t-att-placeholder="widget.node.attrs.placeholder" + t-att-maxlength="widget.field.size" /><img class="oe_field_translate oe_input_icon" t-if="widget.field.translate" t-att-src='_s + "/web/static/src/img/icons/terp-translate.png"' width="16" height="16" border="0"/> - </div> - </t> - <t t-if="widget.get('effective_readonly')"> - <span t-attf-class="oe_form_field_char #{widget.is_field_number ? 'oe-number' : ''}"/> - </t> + </t> + <t t-if="widget.get('effective_readonly')"> + <span class="oe_form_char_content"></span> + </t> + </span> </t> <t t-name="FieldEmail"> - <t t-if="!widget.get('effective_readonly')"> - <table class="oe_form_field_email" cellpadding="0" cellspacing="0" border="0"> - <tr> - <td> + <span class="oe_form_field oe_form_field_email" t-att-style="widget.node.attrs.style"> + <a t-if="widget.get('effective_readonly')" href="#" class="oe_form_uri" target="_blank"/> + <t t-if="!widget.get('effective_readonly')"> + <div> <input type="text" t-att-id="widget.id_for_label" t-att-tabindex="widget.node.attrs.tabindex" t-att-autofocus="widget.node.attrs.autofocus" t-att-placeholder="widget.node.attrs.placeholder" + t-att-maxlength="widget.field.size" /> - </td> - <td width="16"> - <button class="oe_button oe_field_button" tabindex="-1" type="button" title="Send an e-mail with your default e-mail client"> - <img t-att-src='_s + "/web/static/src/img/icons/terp-mail-message-new.png"'/> - </button> - </td> - </tr> - </table> - </t> - <t t-if="widget.get('effective_readonly')"> - <span> - <a href="#" class="oe_form_uri"/> - </span> - </t> + </div> + </t> + </span> </t> -<t t-name="FieldUrl"> - <t t-if="!widget.get('effective_readonly')"> - <table class="oe_form_field_url" cellpadding="0" cellspacing="0" border="0"> - <tr> - <td> - <input type="text" - t-att-id="widget.id_for_label" - t-att-tabindex="widget.node.attrs.tabindex" - t-att-autofocus="widget.node.attrs.autofocus" - t-att-placeholder="widget.node.attrs.placeholder" - /> - </td> - <td width="16"> - <button class="oe_button oe_field_button" tabindex="-1" type="button" title="Open this resource"> - <img t-att-src='_s + "/web/static/src/img/icons/gtk-ok.png"'/> - </button> - </td> - </tr> - </table> - </t> - <t t-if="widget.get('effective_readonly')"> - <span> - <a href="#" class="oe_form_uri"/> - </span> +<t t-name="FieldUrl" t-extend="FieldEmail"> + <t t-jquery="span:first"> + this.removeClass('oe_form_field_email').addClass('oe_form_field_url'); </t> </t> <t t-name="FieldText"> - <div class="oe_form_field_text"> + <div class="oe_form_field oe_form_field_text" t-att-style="widget.node.attrs.style"> <textarea rows="6" t-att-name="widget.name" class="field_text" t-att-tabindex="widget.node.attrs.tabindex" t-att-autofocus="widget.node.attrs.autofocus" - t-att-placeholder="widget.node.attrs.placeholder" - ></textarea> - <img class="oe_field_translate oe_input_icon" t-if="widget.field.translate" - t-att-src='_s + "/web/static/src/img/icons/terp-translate.png"' width="16" height="16" border="0"/> + t-att-placeholder="! widget.get('effective_readonly') ? widget.node.attrs.placeholder : ''" + t-att-maxlength="widget.field.size" + ></textarea><img class="oe_field_translate oe_input_icon" + t-if="widget.field.translate and !widget.get('effective_readonly')" + t-att-src='_s + "/web/static/src/img/icons/terp-translate.png"' width="16" height="16" border="0" + /> + </div> +</t> +<t t-name="FieldTextHtml"> + <div t-att-class="'oe_form_field oe_form_field_html' + (widget.get('effective_readonly') ? ' oe_form_embedded_html' : '')" + t-att-style="widget.node.attrs.style"> + <t t-if="! widget.get('effective_readonly')"> + <textarea/> + </t> </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"> +<t t-name="web.datepicker"> + <span> + <t t-set="placeholder" t-value="widget.getParent().node and widget.getParent().node.attrs.placeholder"/> <input type="text" class="oe_datepicker_container" disabled="disabled" style="display: none;"/> <input type="text" t-att-name="widget.name" t-att-placeholder="placeholder" - t-attf-class="oe_datepicker_master field_#{widget.type_of_date} oe_form_field_#{widget.type_of_date}" + class="oe_datepicker_master" /><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"/> - </div> + </span> +</t> +<t t-name="FieldDate"> + <span class="oe_form_field oe_datepicker_root oe_form_field_date" t-att-style="widget.node.attrs.style"/> +</t> +<t t-name="FieldDatetime"> + <span class="oe_form_field oe_datepicker_root oe_form_field_datetime" t-att-style="widget.node.attrs.style"/> </t> <t t-name="FieldSelection"> - <t t-if="!widget.get('effective_readonly')"> - <div class="oe_form_field_selection"> - <select - t-att-name="widget.name" - t-att-tabindex="widget.node.attrs.tabindex" - t-att-autofocus="widget.node.attrs.autofocus" - t-att-id="widget.id_for_label"> - <t t-foreach="widget.values" t-as="option"> - <option><t t-esc="option[1]"/></option> - </t> - </select> - </div> - </t> - <t t-if="widget.get('effective_readonly')"> - <span class="field_selection"/> - </t> + <span class="oe_form_field oe_form_field_selection" t-att-style="widget.node.attrs.style"> + <select t-if="!widget.get('effective_readonly')" + t-att-name="widget.name" + t-att-tabindex="widget.node.attrs.tabindex" + t-att-autofocus="widget.node.attrs.autofocus" + t-att-id="widget.id_for_label"> + <t t-foreach="widget.values" t-as="option"> + <option><t t-esc="option[1]"/></option> + </t> + </select> + </span> </t> <t t-name="FieldMany2One"> - <t t-if="!widget.get('effective_readonly')"> - <table class="oe_form_field_many2one" cellpadding="0" cellspacing="0" border="0"> - <tr> - <td valign="top"> + <span class="oe_form_field oe_form_field_many2one oe_form_field_with_button" t-att-style="widget.node.attrs.style"> + <t t-if="widget.get('effective_readonly')"> + <a t-if="! widget.options.no_open" href="#" class="oe_form_uri"/> + <span t-if="widget.options.no_open" href="#" class="oe_form_uri"/> + <span class="oe_form_m2o_follow"/> + </t> + <t t-if="!widget.get('effective_readonly')"> + <a t-if="! widget.options.no_open" href="#" tabindex="-1" + class="oe_m2o_cm_button oe_e">/</a> + <div> <input type="text" t-att-id="widget.id_for_label" t-att-tabindex="widget.node.attrs.tabindex" t-att-autofocus="widget.node.attrs.autofocus" t-att-placeholder="widget.node.attrs.placeholder" /> - <span class="oe-m2o-drop-down-button"> - <img t-att-src='_s + "/web/static/src/img/down-arrow.png"' /></span> - </td> - <td width="16"> - <button class="oe_button oe_field_button oe-m2o-cm-button" title="Open Resource"> - <img t-att-src='_s + "/web/static/src/img/icons/terp-folder-yellow.png"'/> - </button> - </td> - </tr> - </table> - </t> - <t t-if="widget.get('effective_readonly')"> - <span> - <a href="#" class="oe_form_uri"/> - </span> - </t> + <span class="oe_m2o_drop_down_button"> + <img t-att-src='_s + "/web/static/src/img/down-arrow.png"'/> + </span> + </div> + </t> + </span> </t> <t t-name="FieldMany2ManyTags"> - <div class="oe_form_field_many2manytags"> + <div class="oe_form_field oe_tags" t-att-style="widget.node.attrs.style"> <t t-if="! widget.get('effective_readonly')"> - <textarea rows="1" style="width: 100%"></textarea> + <textarea rows="1" style="width: 100%" + t-att-placeholder="widget.node.attrs.placeholder"></textarea> </t> </div> </t> -<t t-name="FieldMany2ManyTags.box"> +<t t-name="FieldMany2ManyTag"> <t t-set="i" t-value="0"/> <t t-foreach="elements" t-as="el"> - <span class="oe_form_field_many2manytags_box" t-att-data-index="i"> + <span class="oe_tag" t-att-data-index="i"> <t t-esc="el[1]"/> </span> <t t-set="i" t-value="i + 1"/> </t> </t> <t t-name="FieldReference"> - <table class="oe_form_field_reference oe_form" border="0" cellpadding="0" cellspacing="0"> + <table class="oe_form_field oe_form_field_reference" border="0" cellpadding="0" cellspacing="0" t-att-style="widget.node.attrs.style"> <tr> - <td t-attf-class="oe_form_frame_cell oe_form_selection oe_form_view_reference_selection"> + <td t-attf-class="oe_form_frame_cell oe_form_selection"> + <span t-attf-class="oe_form_view_reference_selection"/> </td> - <td t-attf-class="oe_form_frame_cell oe_form_many2one oe_form_view_reference_m2o" nowrap="true" style="display: none"> + <td t-attf-class="oe_form_frame_cell oe_form_many2one" nowrap="true"> + <span t-attf-class="oe_form_view_reference_m2o"/> </td> </tr> </table> </t> <t t-name="FieldBoolean"> - <span class="oe_form_field_boolean"> + <span class="oe_form_field oe_form_field_boolean" t-att-style="widget.node.attrs.style"> <input type="checkbox" t-att-id="widget.id_for_label" t-att-name="widget.name" @@ -1070,88 +1069,47 @@ </span> </t> <t t-name="FieldProgressBar"> - <div class="oe_form_field_progressbar" t-opentag="true"> + <span class="oe_form_field oe_form_field_progressbar" t-att-style="widget.node.attrs.style"> <span></span> - </div> + </span> </t> -<t t-name="FieldStatus.content"> - <ul class="oe-arrow-list"> - <t t-set="size" t-value="widget.to_show.length"/> - <t t-foreach="_.range(size)" t-as="i"> - <li t-att-class="widget.to_show[i][0] === widget.selected_value ? 'oe-arrow-list-selected' : ''"> - <span class="oe-arrow-list-before" t-if="i > 0"></span><span><t t-esc="widget.to_show[i][1]"/></span><span class="oe-arrow-list-after" t-if="i < size - 1"></span> - </li> - </t> - </ul> +<t t-name="FieldStatus"> + <ul t-att-class="'oe_form_field_status ' + (widget.options.clickable ? 'oe_form_status_clickable' : 'oe_form_status')" t-att-style="widget.node.attrs.style"/> </t> <t t-name="FieldStatus.content"> - <ul class="oe_form_steps"> - <t t-set="size" t-value="widget.to_show.length"/> - <t t-foreach="_.range(size)" t-as="i"> - <li t-att-class="widget.to_show[i][0] === widget.selected_value ? 'oe_form_steps_active' : ''"> - <t t-esc="widget.to_show[i][1]"/> - <img t-att-src='_s + "/web/static/src/img/form_steps.png"' class="oe_form_steps_arrow" t-if="i < size - 1"/> - </li> - </t> - </ul> + <t t-foreach="widget.selection" t-as="i"> + <li t-att-class="i[0] === widget.get('value') ? 'oe_active' : ''" t-att-data-id="i[0]"> + <span class="label"><t t-esc="i[1]"/></span> + <!-- are you mit ? --> + <span class="arrow"><span></span></span> + </li> + </t> </t> <t t-name="FieldBinaryImage"> - <table cellpadding="0" cellspacing="0" border="0" width="100%"> - <tr> - <td class="oe_form_field-binary-image-placeholder" align="center"> - - </td> - </tr> - <tr> - <td align="center" valign="bottom" height="25"> - <div class="oe-binary"> - <table cellspacing="0" cellpadding="0" border="0"> - <tr> - <td> - <div class="oe-binary-file-set"> - <form class="oe-binary-form" t-att-target="widget.iframe" - method="post" enctype="multipart/form-data" action="/web/binary/upload"> - <input type="hidden" name="session_id" value=""/> - <input type="hidden" name="callback" t-att-value="widget.iframe"/> - <button class="oe_button" type="button" title="Set Image"> - <img t-att-src='_s + "/web/static/src/img/icons/STOCK_DIRECTORY.png"'/> - </button> - <input type="file" class="oe-binary-file" name="ufile" - t-att-tabindex="widget.node.attrs.tabindex" - t-att-autofocus="widget.node.attrs.autofocus" - /> - </form> - </div> - </td> - <td> - <button class="oe_button oe-binary-file-clear" type="button" title="Clear"> - <img t-att-src='_s + "/web/static/src/img/icons/STOCK_MISSING_IMAGE.png"'/> - </button> - </td> - </tr> - </table> - </div> - <div class="oe-binary-progress" style="display: none"> + <span class="oe_form_field oe_form_field_image" t-att-style="widget.node.attrs.style"> + <div class="oe_form_field_image_controls oe_edit_only"> + <t t-call="HiddenInputFile"> + <t t-set="fileupload_id" t-value="widget.fileupload_id"/> + Edit + </t> + <div class="oe_form_binary_progress" style="display: none"> <img t-att-src='_s + "/web/static/src/img/throbber.gif"' width="16" height="16"/> <b>Uploading ...</b> </div> - <iframe t-att-id="widget.iframe" t-att-name="widget.iframe" style="display: none"/> - </td> - </tr> - </table> + </div> + </span> </t> <t t-name="FieldBinaryImage-img"> - <img t-att-src='url' class="oe-binary-image field_image" + <img t-att-src='url' t-att-border="widget.readonly ? 0 : 1" t-att-name="widget.name" t-att-width="widget.node.attrs.img_width || widget.node.attrs.width" t-att-height="widget.node.attrs.img_height || widget.node.attrs.height" - t-att-style="'max-width: ' + (widget.$element.width() > 60 ? widget.$element.width() : 60) + 'px'" /> </t> <t t-name="FieldBinaryFile"> <t t-if="!widget.get('effective_readonly')"> - <table class="oe_form_field_binary" cellpadding="0" cellspacing="0" border="0"> + <table class="oe_form_field oe_form_field_binary" cellpadding="0" cellspacing="0" border="0" t-att-style="widget.node.attrs.style"> <tr> <td> <input type="text" size="1" readonly="readonly" @@ -1161,31 +1119,29 @@ class="field_binary" /> </td> - <td class="oe-binary" nowrap="true"> + <td class="oe_form_binary" nowrap="true"> <table cellspacing="0" cellpadding="0" border="0"> <tr> <td> - <div class="oe-binary-file-set" style="width: 80px; height:22px;"> - <form class="oe-binary-form" t-att-target="widget.iframe" - method="post" enctype="multipart/form-data" action="/web/binary/upload"> - <input type="hidden" name="session_id" value=""/> - <input type="hidden" name="callback" t-att-value="widget.iframe"/> - <button class="oe_button oe_field_button" type="button" title="Set Image"> - <img t-att-src='_s + "/web/static/src/img/icons/STOCK_DIRECTORY.png"'/> - <span>Select</span> - </button> - <input type="file" class="oe-binary-file" name="ufile"/> - </form> - </div> + <t t-call="HiddenInputFile"> + <t t-set="fileupload_id" t-value="widget.fileupload_id"/> + <t t-set="fileupload_style" t-translation="off">width: 83px;</t> + <button class="oe_button oe_field_button" type="button"> + <img t-att-src='_s + "/web/static/src/img/icons/STOCK_DIRECTORY.png"'/> + <span>Select</span> + </button> + </t> </td> <td> - <button class="oe_button oe-binary-file-save" type="button" title="Save As"> - <img t-att-src='_s + "/web/static/src/img/icons/gtk-save.png"'/> - <span>Save As</span> - </button> + <a class="oe_form_binary_file_save_data"> + <button class="oe_button oe_form_binary_file_save" type="button" title="Save As"> + <img t-att-src='_s + "/web/static/src/img/icons/gtk-save.png"'/> + <span>Save As</span> + </button> + </a> </td> <td> - <button class="oe_button oe-binary-file-clear" type="button" title="Clear"> + <button class="oe_button oe_form_binary_file_clear" type="button" title="Clear"> <img t-att-src='_s + "/web/static/src/img/icons/STOCK_MISSING_IMAGE.png"'/> <span>Clear</span> </button> @@ -1193,25 +1149,81 @@ </tr> </table> </td> - <td class="oe-binary-progress" style="display: none" nowrap="true"> + <td class="oe_form_binary_progress" style="display: none" nowrap="true"> <img t-att-src='_s + "/web/static/src/img/throbber.gif"' width="16" height="16"/> <b>Uploading ...</b> - <iframe t-att-id="widget.iframe" t-att-name="widget.iframe" style="display: none"/> </td> </tr> </table> </t> <t t-if="widget.get('effective_readonly')"> - <span> + <span t-att-style="widget.node.attrs.style"> <a href="javascript:void(0)" class="oe_form_uri"/> </span> </t> </t> +<t t-name="HiddenInputFile"> + <div t-attf-class="oe_hidden_input_file #{fileupload_class or ''}" t-att-style="fileupload_style"> + <form class="oe_form_binary_form" t-att-target="fileupload_id" + method="post" enctype="multipart/form-data" t-att-action="fileupload_action || '/web/binary/upload'"> + <input type="hidden" name="session_id" value=""/> + <input type="hidden" name="callback" t-att-value="fileupload_id"/> + <t t-raw="__content__"/> + <input type="file" class="oe_form_binary_file" name="ufile" t-if="widget.widget!='image'"/> + <input type="file" class="oe_form_binary_file" name="ufile" accept="image/*" t-if="widget.widget=='image'"/> + </form> + <iframe t-att-id="fileupload_id" t-att-name="fileupload_id" style="display: none"/> + </div> +</t> +<t t-name="FieldBinaryFileUploader.files"> + <div class="oe_attachments"> + <t t-if="!widget.get('effective_readonly')" t-foreach="widget.get('value')" t-as="file"> + <div class="oe_attachment"> + <span t-if="(file.upload or file.percent_loaded<100)" t-attf-title="{(file.name || file.filename) + (file.date?' \n('+file.date+')':'' )}" t-attf-name="{file.name || file.filename}"> + <span class="oe_fileuploader_in_process">...Upload in progress...</span> + <t t-raw="file.name || file.filename"/> + </span> + <a t-if="(!file.upload or file.percent_loaded>=100)" t-att-href="file.url" t-attf-title="{(file.name || file.filename) + (file.date?' \n('+file.date+')':'' )}"> + <t t-raw="file.name || file.filename"/> + </a> + <t t-if="(!file.upload or file.percent_loaded>=100)"> + <a class="oe_right oe_delete oe_e" title="Delete this file" t-attf-data-id="{file.id}">[</a> + </t> + </div> + </t> + <t t-if="widget.get('effective_readonly')" t-foreach="widget.get('value')" t-as="file"> + <div> + <a t-att-href="file.url" t-attf-title="{(file.name || file.filename) + (file.date?' \n('+file.date+')':'' )}"> + <t t-raw="file.name || file.filename"/> + </a> + </div> + </t> + </div> +</t> +<t t-name="FieldBinaryFileUploader"> + <div t-att-style="widget.node.attrs.style" t-attf-class="oe_fileupload #{widget.node.attrs.class ? widget.node.attrs.class :''}"> + <div class="oe_placeholder_files"/> + <div class="oe_add" t-if="!widget.get('effective_readonly')"> + <!-- uploader of file --> + <button class="oe_attach"><span class="oe_e">'</span></button> + <span class='oe_attach_label'>File</span> + <t t-call="HiddenInputFile"> + <t t-set="fileupload_id" t-value="widget.fileupload_id"/> + <t t-set="fileupload_action">/web/binary/upload_attachment</t> + <input type="hidden" name="model" t-att-value="widget.view.model"/> + <input type="hidden" name="id" value="0"/> + <input type="hidden" name="session_id" t-att-value="widget.session.session_id"/> + </t> + </div> + </div> +</t> <t t-name="WidgetButton"> <button type="button" class="oe_button oe_form_button" + t-att-style="widget.node.attrs.style" t-att-tabindex="widget.node.attrs.tabindex" - t-att-autofocus="widget.node.attrs.autofocus"> - <img t-if="widget.node.attrs.icon" t-att-src="_s + '/web/static/src/img/icons/' + widget.node.attrs.icon + '.png'" width="16" height="16"/> + t-att-autofocus="widget.node.attrs.autofocus" + t-att-accesskey="widget.node.attrs.accesskey"> + <img t-if="widget.node.attrs.icon" t-att-src="_s + widget.node.attrs.icon" width="16" height="16"/> <span t-if="widget.string"><t t-esc="widget.string"/></span> </button> </t> @@ -1247,31 +1259,52 @@ </t> </t> -<t t-name="TranslateDialog"> - <table t-if="widget.view.translatable_fields" class="oe_form oe_translation_form" border="0" cellpadding="0" cellspacing="0" width="100%"> - <tr> - <td class="oe_form_separator" width="1%" nowrap="nowrap"> - <div class="oe_horizontal_separator">Field</div> - </td> - <th t-foreach="widget.languages" align="left"> - <div class="oe_horizontal_separator"><t t-esc="name"/></div> - </th> - </tr> - <tr t-foreach="widget.view.translatable_fields" t-as="field" t-att-data-field="field.name"> - <td class="oe_form_group_cell" width="1%" nowrap="nowrap"> - <label class="oe_label"><t t-esc="field.node.attrs.string"/>:</label> - </td> - <td t-foreach="widget.languages" t-as="lg" class="oe_form_group_cell"> - <input t-if="field.type == 'char'" type="text" t-attf-name="#{lg.code}-#{field.name}" value="" data-value="" class="oe_trad_field" style="width: 100%"/> - <textarea t-if="field.type == 'text'" t-attf-name="#{lg.code}-#{field.name}" data-value="" class="oe_trad_field" style="width: 100%"></textarea> - </td> - </tr> - </table> +<t t-name="AbstractFormPopup.render"> + <div> + <table style="width:100%"> + <tr style="width:100%"> + <td style="width:100%"> + <div class="oe_popup_search" style="width:100%"></div> + </td> + </tr> + <tr style="width:100%"> + <td style="width:100%"> + <div class="oe_popup_list" style="width:100%"></div> + </td> + </tr> + </table> + <div class="oe_popup_form" style="width:100%"></div> + </div> +</t> +<t t-name="SelectCreatePopup.search.buttons"> + <t t-if="! widget.options.disable_multiple_selection"> + <button type="button" class="oe_button oe_selectcreatepopup-search-select" disabled="disabled">Select</button> + </t> + <button type="button" class="oe_button oe_selectcreatepopup-search-create">Create</button> + or <a class="oe_button oe_selectcreatepopup-search-close oe_bold oe_form_button_cancel" href="javascript:void(0)">Cancel</a> +</t> +<t t-name="AbstractFormPopup.buttons"> + <t t-if="! readonly"> + <t t-if="! multi_select"> + <button type="button" class="oe_button oe_abstractformpopup-form-save oe_highlight">Save</button> + </t> + <t t-if="multi_select"> + <button type="button" class="oe_button oe_abstractformpopup-form-save oe_highlight">Save & Close</button> + <button type="button" class="oe_button oe_abstractformpopup-form-save-new oe_highlight">Save & New</button> + </t> + or + </t> + <a class="oe_button oe_abstractformpopup-form-close oe_bold oe_form_button_cancel" href="javascript:void(0)"> + <t t-if="! readonly"> + Discard + </t> + <t t-if="readonly"> + Close + </t> + </a> </t> <t t-name="One2Many.viewmanager" t-extend="ViewManager"> - <t t-jquery="span.oe_view_title_text" t-operation="replace"/> - <t t-jquery=".oe-view-manager-header"> this.attr('t-if', 'views.length != 1'); </t> @@ -1299,18 +1332,21 @@ <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_unfold_drawer" title="Advanced Search..."/> <div class="oe_searchview_drawer"/> + <button type="button" class="oe_searchview_search" + title="Search Again">Search</button> </div> <div t-name="SearchView.InputView" class="oe_searchview_input" - contenteditable="true"/> + contenteditable="true" + tabindex="0"/> <!-- tabindex: makes div focusable --> <div t-name="SearchView.FacetView" - class="oe_searchview_facet" + class="oe_tag oe_tag_dark oe_searchview_facet" tabindex="0" - ><span class="oe_facet_remove">✖</span + ><span class="oe_facet_remove">x</span ><span class="oe_facet_category oe_i" t-if="widget.model.has('icon')"> <t t-esc="widget.model.get('icon')"/> </span @@ -1321,9 +1357,8 @@ <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> + <option class="oe_search_filters_title" value="">Filters</option> <optgroup label="-- Filters --"> <t t-foreach="filters" t-as="filter"> <option t-attf-value="get:#{filter_index}" @@ -1336,7 +1371,6 @@ <optgroup label="-- Actions --"> <option value="advanced_filter">Add Advanced Filter</option> <option value="save_filter">Save Filter</option> - <option value="add_to_dashboard">Add to Dashboard</option> <option value="manage_filters">Manage Filters</option> </optgroup> </t> @@ -1347,18 +1381,8 @@ <p>(Any existing filter with the same name will be replaced)</p> </div> </t> -<t t-name="SearchView.add_to_dashboard"> - <div class="oe_form"> - <p><b>Select Dashboard to add this filter to:</b></p> - <select style="width: 100%; margin-right: 1em;"> - <option t-foreach="dashboards" t-as="menu" t-att-value="menu.id" t-att-selected="(menu.id == selected_menu_id) || undefined"><t t-esc="menu.name"/></option> - </select> - <p><b>Title of new Dashboard item:</b></p> - <input type="text" style="width: 100%; margin-right: 1em;"/> - </div> -</t> <t t-name="SearchView.render_lines"> - <table class="oe-searchview-render-line" border="0" cellspacing="0" cellpadding="0" + <table class="oe_search_render_line" border="0" cellspacing="0" cellpadding="0" t-foreach="lines" t-as="line"> <tr> <td t-foreach="line" t-as="widget" class="oe_searchview_field"> @@ -1475,38 +1499,45 @@ </t> </t> </t> -<div t-name="SearchView.Filters" class="oe_searchview_filters"> +<div t-name="SearchView.Filters" class="oe_searchview_filters oe_searchview_section"> </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"/> - <label for="oe_searchview_custom_default">Use by default:</label> - <input id="oe_searchview_custom_default" type="checkbox"/> - </form> +<div t-name="SearchView.CustomFilters" class="oe_searchview_custom oe_searchview_section"> + <div> + <h3><span class="oe_i">M</span> Custom Filters</h3> + <ul class="oe_searchview_custom_list"/> + <div class="oe_searchview_custom"> + <h4>Save current filter</h4> + <form> + <p><input id="oe_searchview_custom_input" placeholder="Filter name"/></p> + <p> + <input id="oe_searchview_custom_public" type="checkbox"/> + <label for="oe_searchview_custom_public">Share with all users</label> + <input id="oe_searchview_custom_default" type="checkbox"/> + <label for="oe_searchview_custom_default">Use by default</label> + </p> + <button>Save</button> + </form> + </div> + </div> </div> + <div t-name="SearchView.advanced" class="oe_searchview_advanced"> <h4>Advanced Search</h4> <form> <ul> </ul> - <button class="oe_add_condition" type="button">Add a condition</button> + <button class="oe_add_condition button" type="button">Add a condition</button><br/> <button class="oe_apply" type="submit">Apply</button> </form> </div> <t t-name="SearchView.extended_search.proposition"> <li> + <span class="searchview_extended_prop_or">or</span> <select class="searchview_extended_prop_field"> <t t-foreach="widget.attrs.fields" t-as="field"> - <option t-if="typeof field.store === 'undefined' || field.store || field.fnct_search" - t-att="{'selected': field === widget.attrs.selected ? 'selected' : null}" + <option t-att="{'selected': field === widget.attrs.selected ? 'selected' : null}" t-att-value="field.name"> <t t-esc="field.string"/> </option> @@ -1514,7 +1545,7 @@ </select> <select class="searchview_extended_prop_op"/> <span class="searchview_extended_prop_value"/> - <button type="button" class="searchview_extended_delete_prop">Delete</button> + <a class="searchview_extended_delete_prop button">x</a> </li> </t> <t t-name="SearchView.extended_search.proposition.char"> @@ -1536,115 +1567,11 @@ </t> </select> </t> -<t t-name="AbstractFormPopup.render"> - <div> - <table style="width:100%"> - <tr style="width:100%"> - <td style="width:100%"> - <div class="oe-select-create-popup-view-search" style="width:100%"></div> - </td> - </tr> - <tr style="width:100%"> - <td style="width:100%"> - <div class="oe-select-create-popup-view-list" style="width:100%"></div> - </td> - </tr> - </table> - <div class="oe-form-view-popup-form-placeholder oe-select-create-popup-view-form" style="width:100%"></div> - </div> -</t> -<t t-name="SelectCreatePopup.search.buttons"> - <button type="button" class="oe_button oe_selectcreatepopup-search-select" disabled="disabled">Select</button> - <button type="button" class="oe_button oe_selectcreatepopup-search-close">Cancel</button> -</t> -<t t-name="AbstractFormPopup.buttons"> - <t t-if="! multi_select"> - <button type="button" class="oe_button oe_abstractformpopup-form-save">Save</button> - </t> - <t t-if="multi_select"> - <button type="button" class="oe_button oe_abstractformpopup-form-save-new">Save & New</button> - <button type="button" class="oe_button oe_abstractformpopup-form-save">Save & Close</button> - </t> - <button type="button" class="oe_button oe_abstractformpopup-form-close">Cancel</button> -</t> -<t t-extend="ListView.row"> - <!-- adds back padding to row being rendered after edition, if necessary - (if not deletable add back padding), otherwise the row being added is - missing columns - --> - <t t-jquery="> :last" t-operation="after"> - <td t-if="edited and !options.deletable" class="oe-listview-padding"/> - </t> -</t> - -<t t-name="view_editor"> - <table class="oe_view_editor"> - <t t-call="view_editor.row"/> - </table> -</t> -<t t-name="view_editor.row"> - <tr t-att-id="'viewedit-' + rec.id" t-att-level="rec.level" t-foreach="data" t-as="rec"> - <td width="90%"> - <table class="oe_view_editor_field"> - <tr> - <td width="16px" t-att-style="'background-position: ' + 20*rec.level + 'px; padding-left: ' + 20*rec.level + 'px'"> - <img t-if="rec.child_id.length" t-att-id="'parentimg-' + rec.id" - src="/web/static/src/img/collapse.gif" width="16" height="16" border="0"/> - </td> - <td style="cursor: pointer;"> - <a style="text-decoration:none" href="javascript:void(0);"> - <t t-esc="rec.name"/> - </a> - </td> - </tr> - </table> - </td> - <td width="2%"> - <img t-if="rec.att_list.length" - id="side-add" src="/web/static/src/img/icons/gtk-add.png" style="cursor: pointer;"/> - </td> - <td width="2%"> - <img id="side-remove" src="/web/static/src/img/icons/gtk-remove.png" style="cursor: pointer;"/> - </td> - <td width="2%"> - <img t-if="rec.att_list.length and !_.include(no_properties, rec.att_list[0])" - id="side-edit" src="/web/static/src/img/icons/gtk-edit.png" style="cursor: pointer;"/> - </td> - <td width="2%"> - <img t-if="rec.att_list.length" - id="side-up" src="/web/static/src/img/icons/gtk-go-up.png" style="cursor: pointer;"/> - </td> - <td width="2%"> - <img t-if="rec.att_list.length" - id="side-down" src="/web/static/src/img/icons/gtk-go-down.png" style="cursor: pointer;"/> - </td> - <t t-if="rec.child_id.length"> - <t t-set="data" t-value="rec.child_id"/> - <t t-call="view_editor.row"/> - </t> - </tr> -</t> -<t t-name="vieweditor_char"> - <input type="text" t-att-id="widget.name" class="field_char" size="50"/> -</t> -<t t-name="vieweditor_selection"> - <select t-att-id="widget.name" > - <t t-if="widget.selection" t-foreach="widget.selection" t-as="option"> - <option - t-att-value="typeof option === 'object' ? option[0] : option"> - <t t-esc="typeof option === 'object' ? option[1] : option"/> - </option> - </t> - </select> -</t> -<t t-name="vieweditor_boolean"> - <input type="checkbox" t-att-id="widget.name"/> -</t> <t t-name="ExportView"> <a id="exportview" href="javascript: void(0)" style="text-decoration: none;color: #3D3D3D;">Export</a> </t> -<table t-name="ExportTreeView" class="oe-export" +<table t-name="ExportTreeView" class="oe_export" style="background-color: #F3F3F3;"> <tr> <td colspan="3"> @@ -1656,7 +1583,7 @@ <td colspan="3"> <label for="import_compat">Export Type:</label> <select id="import_compat" name="import_compat"> - <option value="yes">Import Compatible Export</option> + <option value="yes">Import-Compatible Export</option> <option value="">Export all Data</option> </select> @@ -1749,7 +1676,7 @@ </t> <t t-name="ImportDataView"> <form name="import_data" id="import_data" action="" method="post" enctype="multipart/form-data" - class="oe-import oe-import-no-result"> + class="oe_import oe_import_no_result"> <input type="hidden" name="session_id" t-att-value="widget.session.session_id"/> <h2 class="separator horizontal">1. Import a .CSV file</h2> <p>Select a .CSV file to import. If you need a sample of file to import, @@ -1759,9 +1686,9 @@ <label for="csvfile">CSV File:</label> <input type="file" id="csvfile" size="50" name="csvfile"/> </p> - <h2 class="separator horizontal oe-import-result">2. Check your file format</h2> - <div id="result" class="oe-import-result"></div> - <fieldset class="oe-closed oe-import-result"> + <h2 class="separator horizontal oe_import_result">2. Check your file format</h2> + <div id="result" class="oe_import_result"></div> + <fieldset class="oe_closed oe_import_result"> <legend>Import Options</legend> <table> <tr> @@ -1801,7 +1728,7 @@ </tr> <tr> <td t-foreach="records[0]" t-as="column"> - <input class="sel_fields" placeholder="--- Don't Import ---"/><span class="oe-m2o-drop-down-button"> + <input class="sel_fields" placeholder="--- Don't Import ---"/><span class="oe_m2o_drop_down_button"> <img t-att-src='_s + "/web/static/src/img/down-arrow.png"' /></span> </td> </tr> @@ -1818,5 +1745,37 @@ <pre><t t-esc="error.preview"/></pre> </t> </t> - +<div t-name="Many2ManyKanban.quick_create" class="oe_kanban_quick_create"> + <input t-att-placeholder="_t('Type name to search')"/> +</div> +<t t-name="Throbber"> + <div> + <div class="oe_blockui_spin" style="height: 50px"> + </div> + <br /> + <div class="oe_throbber_message" style="color:white"></div> + </div> +</t> +<t t-name="M2ODialog"> + <div> + Name: <input class="oe_form_m2o_input_name" type="text"/> + </div> +</t> +<t t-name="M2ODialog.buttons"> + <button class="oe_form_m2o_qc_button oe_button oe_highlight">Quick Add</button> + <button class="oe_form_m2o_sc_button oe_button">Add All Info...</button> + <button class="oe_form_m2o_cancel_button oe_button">Cancel</button> +</t> +<t t-name="FieldMonetary" t-extend="FieldChar"> + <t t-jquery="t:first" t-operation="before"> + <t t-if="widget.get('currency_info') and widget.get('currency_info').position === 'before'"> + <t t-esc="widget.get('currency_info').symbol"/> + </t> + </t> + <t t-jquery="t:last" t-operation="after"> + <t t-if="widget.get('currency_info') and widget.get('currency_info').position === 'after'"> + <t t-esc="widget.get('currency_info').symbol"/> + </t> + </t> +</t> </templates> |
