summaryrefslogtreecommitdiff
path: root/addons/web/static/src
diff options
context:
space:
mode:
authorAntony Lesuisse <al@openerp.com>2012-07-25 10:26:46 +0200
committerAntony Lesuisse <al@openerp.com>2012-07-25 10:26:46 +0200
commit3235ed8a1896fbf24e61982e4c06b7d7f0f1815b (patch)
treec563cc1ae571e10d95804cb99ff0e7e51a452986 /addons/web/static/src
parent974d4f4f106762dafb82b6d6f7a7a43fd6cdacdb (diff)
parenta4f1ce69ab01818fce1a23fc3744865bcc4fd85c (diff)
[MERGE] trunk
bzr revid: al@openerp.com-20120725082646-ff6ljt0pquvm0dup
Diffstat (limited to 'addons/web/static/src')
-rw-r--r--addons/web/static/src/css/base.css122
-rw-r--r--addons/web/static/src/css/base.sass106
-rw-r--r--addons/web/static/src/img/pattern.pngbin98 -> 0 bytes
-rw-r--r--addons/web/static/src/img/throbber.gifbin673 -> 7625 bytes
-rw-r--r--addons/web/static/src/img/throbber2.gifbin7195 -> 0 bytes
-rw-r--r--addons/web/static/src/js/chrome.js35
-rw-r--r--addons/web/static/src/js/coresetup.js10
-rw-r--r--addons/web/static/src/js/data_export.js4
-rw-r--r--addons/web/static/src/js/search.js19
-rw-r--r--addons/web/static/src/js/view_form.js310
-rw-r--r--addons/web/static/src/js/view_list.js158
-rw-r--r--addons/web/static/src/js/view_list_editable.js929
-rw-r--r--addons/web/static/src/js/views.js16
-rw-r--r--addons/web/static/src/xml/base.xml65
14 files changed, 1182 insertions, 592 deletions
diff --git a/addons/web/static/src/css/base.css b/addons/web/static/src/css/base.css
index 0a3d24d41f4..30c0b5ea0b3 100644
--- a/addons/web/static/src/css/base.css
+++ b/addons/web/static/src/css/base.css
@@ -1,4 +1,4 @@
-@charset "UTF-8";
+@charset "utf-8";
@font-face {
font-family: "mnmliconsRegular";
src: url("/web/static/src/font/mnmliconsv21-webfont.eot") format("eot");
@@ -267,6 +267,45 @@
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: #8a89ba;
+}
+.openerp.ui-dialog .oe_about a:hover {
+ text-decoration: underline;
+}
+.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;
}
@@ -584,7 +623,7 @@
z-index: 1;
border: 1px solid #afafb6;
background: white;
- padding: 6px 0;
+ padding: 4px 0;
min-width: 140px;
text-align: left;
-moz-border-radius: 3px;
@@ -599,6 +638,7 @@
float: none;
display: block;
position: relative;
+ padding: 2px 8px;
}
.openerp .oe_dropdown_menu > li:hover {
background-color: #f0f0fa;
@@ -615,7 +655,6 @@
.openerp .oe_dropdown_menu > li > a {
white-space: nowrap;
display: block;
- padding: 4px 15px;
color: #4c4c4c;
text-decoration: none;
}
@@ -711,7 +750,7 @@
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%;
@@ -945,11 +984,12 @@
}
.openerp .oe_topbar .oe_dropdown_menu li {
float: none;
+ padding: 3px 12px;
}
.openerp .oe_topbar .oe_dropdown_menu li a {
color: #eeeeee;
}
-.openerp .oe_topbar .oe_dropdown_menu 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);
@@ -1408,20 +1448,31 @@
filter: alpha(opacity=50);
opacity: 0.5;
}
-.openerp .oe_searchview .oe_searchview_facets {
- min-height: 22px;
+.openerp .oe_searchview .oe_searchview_search {
+ font-size: 1px;
+ letter-spacing: -1px;
+ color: transparent;
+ -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_facets:before {
- color: #cccccc;
- font-family: "mnmliconsRegular";
+.openerp .oe_searchview .oe_searchview_search:before {
+ font: 21px "mnmliconsRegular";
content: "r";
- font-size: 130%;
- display: inline;
- position: relative;
- left: 6px;
- top: 2px;
color: #a3a3a3;
- padding-right: 4px;
+}
+.openerp .oe_searchview .oe_searchview_facets {
+ min-height: 22px;
+ margin-left: 15px;
}
.openerp .oe_searchview .oe_searchview_facets * {
vertical-align: top;
@@ -1437,7 +1488,7 @@
outline: none;
}
.openerp .oe_searchview .oe_searchview_facets .oe_searchview_input {
- padding: 0 3px;
+ padding: 0 0 0 6px;
}
.openerp .oe_searchview .oe_searchview_facets .oe_searchview_facet {
position: relative;
@@ -1600,14 +1651,14 @@
.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_section li:hover {
background-color: #f0f0fa;
}
-.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_section form {
+.openerp .oe_searchview .oe_searchview_drawer form {
margin-left: 12px;
}
-.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_section form p {
+.openerp .oe_searchview .oe_searchview_drawer form p {
margin: 4px 0;
line-height: 18px;
}
-.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_section form button {
+.openerp .oe_searchview .oe_searchview_drawer form button {
margin: 0 0 8px 0;
}
.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_custom {
@@ -2180,9 +2231,33 @@
height: auto;
line-height: 16px;
}
+.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_field_many2many > .oe_list .oe_list_pager_single_page {
display: none;
}
+.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, .openerp .oe_list_buttons.oe_editing .oe_list_button_import {
+ display: none;
+}
+.openerp .oe_list_buttons.oe_editing .oe_list_save {
+ display: inline-block;
+}
+.openerp .oe_list_buttons.oe_editing .oe_list_discard {
+ display: inline;
+}
+.openerp .oe_list {
+ position: relative;
+}
+.openerp .oe_list .oe_form .oe_form_field {
+ width: auto;
+ position: absolute;
+ margin: 0 !important;
+ padding: 0;
+}
.openerp .oe_list_content {
width: 100%;
}
@@ -2240,9 +2315,7 @@
}
.openerp .oe_list_content > tbody > tr > td.oe_list_field_cell {
padding: 3px 6px;
-}
-.openerp .oe_list_content > tbody > tr > td.oe_list_field_cell progress {
- width: 100%;
+ white-space: pre-line;
}
.openerp .oe_list_content > tbody > tr > td > button, .openerp .oe_list_content > tbody > tr > th > button {
border: none;
@@ -2290,9 +2363,6 @@
.openerp .oe_list_content .numeric input {
text-align: right;
}
-.openerp .oe_list_content .oe_list_edit_row_save:before {
- content: "S";
-}
.openerp .oe_trad_field.touched {
border: 1px solid green !important;
}
diff --git a/addons/web/static/src/css/base.sass b/addons/web/static/src/css/base.sass
index fb67945a02d..7fbe5c1c336 100644
--- a/addons/web/static/src/css/base.sass
+++ b/addons/web/static/src/css/base.sass
@@ -92,9 +92,8 @@ $sheet-max-width: 860px
letter-spacing: -1px
color: transparent
&:before
- font-family: "mnmliconsRegular"
+ font: 21px "mnmliconsRegular"
content: $icon-name
- font-size: 20px
color: $color
// }}}
@@ -256,6 +255,29 @@ $sheet-max-width: 860px
&: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: #8A89BA
+ &:hover
+ text-decoration: underline
+ .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
&.ui-dialog.oe_act_window
.ui-dialog-content
@@ -470,7 +492,7 @@ $sheet-max-width: 860px
z-index: 1
border: 1px solid #afafb6
background: white
- padding: 6px 0
+ padding: 4px 0
min-width: 140px
text-align: left
@include radius(3px)
@@ -483,10 +505,10 @@ $sheet-max-width: 860px
float: none
display: block
position: relative
+ padding: 2px 8px
> a
white-space: nowrap
display: block
- padding: 4px 15px
color: #4c4c4c
text-decoration: none
&:hover
@@ -567,7 +589,7 @@ $sheet-max-width: 860px
// }}}
// 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%
@@ -735,11 +757,12 @@ $sheet-max-width: 860px
@include background-clip()
li
float: none
+ padding: 3px 12px
a
color: #eee
- &:hover
- @include vertical-gradient(#292929, #191919)
- @include box-shadow(none)
+ &:hover
+ @include vertical-gradient(#292929, #191919)
+ @include box-shadow(none)
// }}}
// Webclient.leftbar {{{
@@ -1102,19 +1125,20 @@ $sheet-max-width: 860px
border-right: 5px solid transparent
@include opacity()
+ .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
- &:before
- color: #ccc
- font-family: "mnmliconsRegular"
- content: "r"
- font-size: 130%
- display: inline
- position: relative
- left: 6px
- top: 2px
- color: #a3a3a3
- padding-right: 4px
+ margin-left: 15px
*
vertical-align: top
display: inline-block
@@ -1126,7 +1150,7 @@ $sheet-max-width: 860px
&:focus
outline: none
.oe_searchview_input
- padding: 0 3px
+ padding: 0 0 0 6px
.oe_searchview_facet
position: relative
cursor: pointer
@@ -1248,13 +1272,13 @@ $sheet-max-width: 860px
// 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
+ 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
@@ -1721,6 +1745,9 @@ $sheet-max-width: 860px
li
height: auto
line-height: 16px
+ .oe_list_buttons.oe_editing .oe_list_save
+ // keep "save row" button hidden in o2m
+ visibility: hidden
// }}}
// FormView.many2many {{{
.oe_form .oe_form_field_many2many > .oe_list
@@ -1728,6 +1755,25 @@ $sheet-max-width: 860px
display: none
// }}}
// ListView {{{
+ .oe_list_buttons
+ .oe_list_save, .oe_list_discard
+ display: none
+ &.oe_editing
+ .oe_list_add, .oe_list_button_import
+ display: none
+ .oe_list_save
+ display: inline-block
+ .oe_list_discard
+ display: inline
+
+ .oe_list
+ position: relative
+ .oe_form .oe_form_field
+ width: auto
+ position: absolute
+ margin: 0 !important // dammit
+ padding: 0
+
.oe_list_content
width: 100%
td:first-child, th:first-child
@@ -1771,8 +1817,7 @@ $sheet-max-width: 860px
border-top: 1px solid #ddd
> td.oe_list_field_cell
padding: 3px 6px
- progress
- width: 100%
+ white-space: pre-line
> td, > th
> button
border: none
@@ -1800,8 +1845,6 @@ $sheet-max-width: 860px
width: 82px
input
text-align: right
- .oe_list_edit_row_save:before
- content: "S"
// }}}
// Translation {{{
.oe_trad_field.touched
@@ -1910,6 +1953,7 @@ $sheet-max-width: 860px
background-attachment: fixed
>*
opacity: 0.70
+
// }}}
div.ui-widget-overlay
diff --git a/addons/web/static/src/img/pattern.png b/addons/web/static/src/img/pattern.png
deleted file mode 100644
index 7e3a2b37834..00000000000
--- a/addons/web/static/src/img/pattern.png
+++ /dev/null
Binary files differ
diff --git a/addons/web/static/src/img/throbber.gif b/addons/web/static/src/img/throbber.gif
index ded8d36d486..7ab8346ce09 100644
--- a/addons/web/static/src/img/throbber.gif
+++ b/addons/web/static/src/img/throbber.gif
Binary files differ
diff --git a/addons/web/static/src/img/throbber2.gif b/addons/web/static/src/img/throbber2.gif
deleted file mode 100644
index 5d2057ae0e7..00000000000
--- a/addons/web/static/src/img/throbber2.gif
+++ /dev/null
Binary files differ
diff --git a/addons/web/static/src/js/chrome.js b/addons/web/static/src/js/chrome.js
index 40cff06e66a..b5c69f5c82f 100644
--- a/addons/web/static/src/js/chrome.js
+++ b/addons/web/static/src/js/chrome.js
@@ -135,8 +135,12 @@ instance.web.Dialog = instance.web.Widget.extend({
this.$element.dialog('close');
},
on_close: function() {
+ if (this.__tmp_dialog_destroying)
+ return;
if (this.dialog_options.destroy_on_close) {
+ this.__tmp_dialog_closing = true;
this.destroy();
+ this.__tmp_dialog_closing = undefined;
}
},
on_resized: function() {
@@ -145,6 +149,11 @@ instance.web.Dialog = instance.web.Widget.extend({
_.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.$element.dialog('destroy');
}
@@ -248,13 +257,17 @@ instance.web.Loading = instance.web.Widget.extend({
// 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));
+ if (instance.connection.debug) {
+ this.$element.text(_.str.sprintf( _t("Loading (%d)"), this.count));
+ } else {
+ this.$element.text(_t("Loading"));
+ }
this.$element.show();
this.getParent().$element.addClass('oe_wait');
} else {
@@ -263,7 +276,7 @@ instance.web.Loading = instance.web.Widget.extend({
// 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');
@@ -274,7 +287,7 @@ 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"));
@@ -341,16 +354,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
@@ -851,7 +864,7 @@ instance.web.UserMenu = instance.web.Widget.extend({
window.location.href, 'debug');
});
instance.web.dialog($help, {autoOpen: true,
- modal: true, width: 960, title: _t("About")});
+ modal: true, width: 580, height: 290, resizable: false, title: _t("About")});
});
},
});
@@ -890,7 +903,7 @@ instance.web.Client = instance.web.Widget.extend({
var doc_width = $(document).width();
var offset = $menu.offset();
var menu_width = $menu.width();
- var x = doc_width - offset.left - menu_width - 15;
+ var x = doc_width - offset.left - menu_width - 2;
if (x < 0) {
$menu.offset({ left: offset.left + x }).width(menu_width);
}
@@ -957,10 +970,12 @@ instance.web.WebClient = instance.web.Client.extend({
},
show_login: function() {
var self = this;
+ self.$('.oe_topbar').hide();
self.login.appendTo(self.$element);
},
show_application: function() {
var self = this;
+ self.$('.oe_topbar').show();
self.login.$element.hide();
self.menu = new instance.web.Menu(self);
self.menu.replace(this.$element.find('.oe_menu_placeholder'));
diff --git a/addons/web/static/src/js/coresetup.js b/addons/web/static/src/js/coresetup.js
index 7d101874c07..c9015220ba7 100644
--- a/addons/web/static/src/js/coresetup.js
+++ b/addons/web/static/src/js/coresetup.js
@@ -550,7 +550,15 @@ $.async_when = function() {
/** Setup blockui */
if ($.blockUI) {
$.blockUI.defaults.baseZ = 1100;
- $.blockUI.defaults.message = '<img src="/web/static/src/img/throbber2.gif">';
+ $.blockUI.defaults.message = '<img src="/web/static/src/img/throbber.gif">';
+ $.blockUI.defaults.css.border = '0';
+ $.blockUI.defaults.css["background-color"] = '';
+}
+instance.web.blockUI = function() {
+ $.blockUI.apply($, arguments);
+}
+instance.web.unblockUI = function() {
+ return $.unblockUI.apply($, arguments);
}
/** Setup default session */
diff --git a/addons/web/static/src/js/data_export.js b/addons/web/static/src/js/data_export.js
index ebe82ddb4b4..f8e669e1b4a 100644
--- a/addons/web/static/src/js/data_export.js
+++ b/addons/web/static/src/js/data_export.js
@@ -376,7 +376,7 @@ 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();
+ instance.web.blockUI();
this.session.get_file({
url: '/web/export/' + export_format,
data: {data: JSON.stringify({
@@ -387,7 +387,7 @@ instance.web.DataExport = instance.web.Dialog.extend({
import_compat: Boolean(
this.$element.find("#import_compat").val())
})},
- complete: $.unblockUI
+ complete: instance.web.unblockUI
});
},
close: function() {
diff --git a/addons/web/static/src/js/search.js b/addons/web/static/src/js/search.js
index 165e47d1f20..7e070e406f8 100644
--- a/addons/web/static/src/js/search.js
+++ b/addons/web/static/src/js/search.js
@@ -330,6 +330,12 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
});
}
+ // Launch a search on clicking the oe_searchview_search button
+ this.$element.on('click', 'button.oe_searchview_search', function (e) {
+ e.stopImmediatePropagation();
+ self.do_search();
+ });
+
this.$element.on('keydown',
'.oe_searchview_input, .oe_searchview_facet', function (e) {
switch(e.which) {
@@ -1475,7 +1481,17 @@ 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));
+ }
+ if (value.length > 1) {
+ // more than one search_default m2o id? Should we OR them?
+ throw new Error(
+ _("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) {
if (_(names).isEmpty()) { return null; }
@@ -1669,6 +1685,7 @@ instance.web.search.AddToDashboard = instance.web.Widget.extend({
return $.when(this.load_data(),this.data_loaded).pipe(this.proxy("render_data"));
},
load_data:function(){
+ if (!instance.webclient) { return $.Deferred().reject(); }
var self = this,dashboard_menu = instance.webclient.menu.data.data.children;
var ir_model_data = new instance.web.Model('ir.model.data',{},[['name','=','menu_reporting_dashboard']]).query(['res_id']);
var map_data = function(result){
diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js
index d6ed85fedad..835527b3135 100644
--- a/addons/web/static/src/js/view_form.js
+++ b/addons/web/static/src/js/view_form.js
@@ -239,6 +239,13 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
}
}
},
+ /**
+ *
+ * @param {Object} [options]
+ * @param {Boolean} [editable=false] whether the form should be switched to edition mode. A value of ``false`` will keep the current mode.
+ * @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 || {};
@@ -253,23 +260,24 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
}
this.$element.show().css('visibility', 'hidden');
this.$element.add(this.$buttons).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() {
- if (options.editable) {
- self.set({mode: "edit"});
+
+ 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();
}
- self.$element.css('visibility', 'visible');
+ return self.dataset.read_index(_.keys(self.fields_view.fields), {
+ context: { 'bin_size': true }
+ }).pipe(self.on_record_loaded);
});
- return result;
+ }
+ return shown.pipe(function() {
+ if (options.editable) {
+ self.set({mode: "edit"});
+ }
+ self.$element.css('visibility', 'visible');
});
},
do_hide: function () {
@@ -330,6 +338,20 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
self.$element.add(self.$buttons).removeClass('oe_form_dirty');
});
},
+ /**
+ * Loads and sets up the default values for the model as the current
+ * record
+ *
+ * @return {$.Deferred}
+ */
+ load_defaults: function () {
+ var keys = _.keys(this.fields_view.fields);
+ if (keys.length) {
+ return this.dataset.default_get(keys)
+ .pipe(this.on_record_loaded);
+ }
+ return this.on_record_loaded({});
+ },
on_form_changed: function() {
this.trigger("view_content_has_changed");
},
@@ -600,22 +622,11 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
on_button_new: function() {
var self = this;
this.set({mode: "edit"});
- var def = $.Deferred();
- $.when(this.has_been_loaded).then(function() {
+ 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"});
@@ -674,6 +685,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
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;
@@ -689,8 +701,9 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
}
if (form_invalid) {
self.set({'display_invalid_fields': true});
- for (var f in self.fields) {
- self.fields[f]._check_css_flags();
+ for (var g in self.fields) {
+ if (!self.fields.hasOwnProperty(g)) { continue; }
+ self.fields[g]._check_css_flags();
}
first_invalid_field.focus();
self.on_invalid();
@@ -722,14 +735,15 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
});});
},
on_invalid: function() {
- var msg = "<ul>";
- _.each(this.fields, function(f) {
- if (!f.is_valid()) {
- msg += "<li>" + f.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) {
@@ -807,10 +821,10 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
var ids = this.get_selected_ids();
values["id"] = ids.length > 0 ? ids[0] : false;
_.each(this.fields, function(value_, key) {
- if (_.include(blacklist, key))
+ if (_.include(blacklist, key)) {
return;
- var val = value_.get_value();
- values[key] = val;
+ }
+ values[key] = value_.get_value();
});
return values;
},
@@ -945,6 +959,9 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
is_create_mode: function() {
return !this.datarecord.id;
},
+ open_translate_dialog: function(field) {
+ return this._super(field);
+ },
});
/**
@@ -988,7 +1005,6 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt
}
});
}
- selector = 'form[version!="7.0"] page,form[version!="7.0"]';
},
render_to: function($target) {
var self = this;
@@ -1122,7 +1138,7 @@ 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'),
@@ -1336,7 +1352,7 @@ 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)
@@ -1455,24 +1471,27 @@ instance.web.form.compute_domain = function(expr, fields) {
*/
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 :
+ instance.web.form.compute_domain(
+ self._ic_invisible_modifier,
+ self._ic_field_manager.fields);
+ 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() {
this.on("change:effective_invisible", this, this._check_visibility);
@@ -1536,6 +1555,7 @@ instance.web.form.FormWidget = instance.web.Widget.extend(instance.web.form.Invi
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);
to_set[a] = val;
@@ -1682,11 +1702,7 @@ 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}),
@@ -1739,18 +1755,18 @@ instance.web.form.FieldInterface = {
/**
* 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() {},
@@ -1765,7 +1781,7 @@ instance.web.form.FieldInterface = {
*/
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() {},
@@ -1795,6 +1811,7 @@ instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.w
* @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;
@@ -1809,12 +1826,11 @@ instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.w
this.set({"readonly": this.modifiers['readonly'] === true});
this.set({"force_readonly": false});
var test_effective_readonly = function() {
- this.set({"effective_readonly": this.get("readonly") || !!this.get("force_readonly")});
+ self.set({"effective_readonly": self.get("readonly") || !!self.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)();
-
+ test_effective_readonly.call(this);
this.on("change:value", this, function() {
if (! this._inhibit_on_change)
this.trigger('changed_value');
@@ -1891,7 +1907,7 @@ instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.w
*/
delay_focus: function($elem) {
setTimeout(function() {
- $elem.focus();
+ $elem[0].focus();
}, 50);
},
/**
@@ -2234,6 +2250,11 @@ instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.we
} else {
this.$textarea.attr('disabled', 'disabled');
}
+ this.$element.keyup(function (e) {
+ if (e.which === $.ui.keyCode.ENTER) {
+ e.stopPropagation();
+ }
+ });
this.setupFocus(this.$textarea);
},
set_value: function(value_) {
@@ -2337,6 +2358,7 @@ instance.web.form.FieldTextHtml = instance.web.form.FieldText.extend({
instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({
template: 'FieldBoolean',
start: function() {
+ var self = this;
this._super.apply(this, arguments);
this.$checkbox = $("input", this.$element);
this.setupFocus(this.$checkbox);
@@ -2344,10 +2366,10 @@ instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({
this.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)();
+ check_readonly.call(this);
},
set_value: function(value_) {
this._super.apply(this, arguments);
@@ -2960,6 +2982,7 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
selectable: self.multi_selection,
sortable: false,
import_enabled: false,
+ deletable: true
});
if (self.get("effective_readonly")) {
_.extend(view.options, {
@@ -3004,8 +3027,11 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
this.viewmanager.on_controller_inited.add_last(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;
+ });
+ }
} else if (view_type === "form") {
if (self.get("effective_readonly")) {
$(".oe_form_buttons", controller.$element).children().remove();
@@ -3189,6 +3215,7 @@ 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) {
if (mode !== 'form') {
@@ -3238,19 +3265,34 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({
this._super(parent, dataset, view_id, _.extend(options || {}, {
ListType: instance.web.form.One2ManyList
}));
+ this.on('edit:before', this, this.proxy('_before_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.$element
+ .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;
- // A list not being edited is always valid
- if (!(form = this.first_edition_form())) {
- return true;
- }
+ 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.$element.is('.oe_form_dirty')) {
return true;
}
+ this.o2m._dirty_flag = true;
// Otherwise validate internal form
return _(form.fields).chain()
@@ -3261,21 +3303,8 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({
.all(_.identity)
.value();
},
- first_edition_form: function () {
- var get_form = function (group_or_list) {
- if (group_or_list.edition) {
- return group_or_list.edition_form;
- }
- return _(group_or_list.children).chain()
- .map(get_form)
- .compact()
- .first()
- .value();
- };
- return get_form(this.groups);
- },
do_add_record: function () {
- if (this.options.editable) {
+ if (this.editable()) {
this._super.apply(this, arguments);
} else {
var self = this;
@@ -3328,55 +3357,58 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({
});
},
do_button_action: function (name, id, callback) {
- var _super = _.bind(this._super, this);
-
- this.o2m.view.do_save().then(function () {
- _super(name, id, callback);
- });
- }
-});
-instance.web.form.One2ManyList = instance.web.ListView.List.extend({
- KEY_RETURN: 13,
- // blurring caused by hitting the [Return] key, should skip the
- // autosave-on-blur and let the handler for [Return] do its thing
- __return_blur: false,
- render_row_as_form: function () {
+ 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;
- return this._super.apply(this, arguments).then(function () {
- // Replace the "Save Row" button with "Cancel Edition"
- self.edition_form.$element
- .undelegate('button.oe_list_edit_row_save', 'click')
- .delegate('button.oe_list_edit_row_save', 'click', function () {
- self.cancel_pending_edition();
- });
-
- // Overload execute_action on the edition form to perform a simple
- // reload_record after the action is done, rather than fully
- // reload the parent view (or something)
- var _execute_action = self.edition_form.do_execute_action;
- self.edition_form.do_execute_action = function (action, dataset, record_id, _callback) {
- return _execute_action.call(this, action, dataset, record_id, function () {
- self.view.reload_record(
- self.view.records.get(record_id));
- });
- };
-
- self.edition_form.on('blurred', null, function () {
- if (self.__return_blur) {
- delete self.__return_blur;
- return;
- }
- if (!self.edition_form.widget_is_stopped) {
- self.view.ensure_saved();
- }
- });
+ this.ensure_saved().pipe(function () {
+ return parent_form.do_save();
+ }).then(function () {
+ self.handle_button(name, id, callback);
});
},
- on_row_keyup: function (e) {
- if (e.which === this.KEY_RETURN) {
- this.__return_blur = true;
+
+ _before_edit: function () {
+ this.__ignore_blur = false;
+ this.editor.form.on('blurred', this, this._on_form_blur);
+ },
+ _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;
}
- this._super(e);
+ // FIXME: why isn't there an API for this?
+ if (this.editor.form.$element.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);
}
});
@@ -3545,7 +3577,7 @@ instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({
},
start: function() {
this._super.apply(this, arguments);
- this.$element.addClass('oe_form_field_many2many');
+ this.$element.addClass('oe_form_field oe_form_field_many2many');
var self = this;
@@ -4079,10 +4111,12 @@ instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend
self.dataset, false,
_.extend({'deletable': false,
'selectable': !self.options.disable_multiple_selection,
- 'read_only': true,
'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_popup_list", self.$element)).pipe(function() {
self.view_list.do_show();
@@ -4314,7 +4348,7 @@ instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.
//link.target = '_blank';
link.href = "data:application/octet-stream;base64," + value;
} else {
- $.blockUI();
+ instance.web.blockUI();
this.session.get_file({
url: '/web/binary/saveas_ajax',
data: {data: JSON.stringify({
@@ -4324,7 +4358,7 @@ instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.
filename_field: (this.node.attrs.filename || ''),
context: this.view.dataset.get_context()
})},
- complete: $.unblockUI,
+ complete: instance.web.unblockUI,
error: instance.webclient.crashmanager.on_rpc_error
});
ev.stopPropagation();
diff --git a/addons/web/static/src/js/view_list.js b/addons/web/static/src/js/view_list.js
index edc53f82042..53a8c0126b2 100644
--- a/addons/web/static/src/js/view_list.js
+++ b/addons/web/static/src/js/view_list.js
@@ -21,9 +21,6 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
// whether the view rows can be reordered (via vertical drag & drop)
'reorderable': true,
'action_buttons': true,
- // if true, the view can't be editable, ignoring the view's and the context's
- // instructions
- 'read_only': false,
// if true, the 'Import', 'Export', etc... buttons will be shown
'import_enabled': true,
},
@@ -142,7 +139,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
});
},
/**
- * View startup method, the default behavior is to set the ``oe_listw``
+ * 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
@@ -288,7 +285,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
}
this.$buttons.find('.oe_list_add')
.click(this.proxy('do_add_record'))
- .prop('disabled', grouped && this.options.editable);
+ .prop('disabled', grouped);
this.$buttons.on('click', '.oe_list_button_import', function() {
self.on_sidebar_import();
return false;
@@ -411,20 +408,27 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
if (column.modifiers) {
var modifiers = JSON.parse(column.modifiers);
column.modifiers_for = function (fields) {
- if (!modifiers.invisible) {
- return {};
+ var out = {};
+
+ for (var attr in modifiers) {
+ if (!modifiers.hasOwnProperty(attr)) { continue; }
+ var modifier = modifiers[attr];
+ out[attr] = _.isBoolean(modifier)
+ ? modifier
+ : domain_computer(modifier, fields);
}
- return {
- 'invisible': domain_computer(modifiers.invisible, fields)
- };
+
+ return out;
};
if (modifiers['tree_invisible']) {
column.invisible = '1';
} else {
delete column.invisible;
}
+ column.modifiers = modifiers;
} else {
column.modifiers_for = noop;
+ column.modifiers = {};
}
return column;
};
@@ -436,10 +440,12 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
if (grouped) {
this.columns.unshift({
id: '_group', tag: '', string: _t("Group"), meta: true,
- modifiers_for: function () { return {}; }
+ modifiers_for: function () { return {}; },
+ modifiers: {}
}, {
id: '_count', tag: '', string: '#', meta: true,
- modifiers_for: function () { return {}; }
+ modifiers_for: function () { return {}; },
+ modifiers: {}
});
}
@@ -667,6 +673,19 @@ 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.
+ *
+ * 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;
});
@@ -911,27 +930,42 @@ 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);
}
@@ -975,11 +1009,11 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.
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);
}
@@ -1139,6 +1173,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,
@@ -1147,7 +1182,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); }
+ return self.render_cell.apply(self, arguments); }
});
},
/**
@@ -1831,7 +1866,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);
@@ -1847,13 +1882,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)) {
@@ -1878,6 +1942,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);
diff --git a/addons/web/static/src/js/view_list_editable.js b/addons/web/static/src/js/view_list_editable.js
index c370aeaf7fd..c7654479864 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,29 @@ 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.$element.add(self.$buttons).addClass('oe_editing');
+ });
+ this.on('save:after cancel:after', this, function () {
+ self.$element.add(self.$buttons).removeClass('oe_editing');
+ });
+ },
+ destroy: function () {
+ instance.web.bus.off('resize', this, this.resize_fields);
+ this._super();
},
/**
* Handles the activation of a record in editable mode (making a record
@@ -42,25 +69,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 = (
- ! this.options.read_only && ((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);
},
/**
@@ -68,18 +86,62 @@ 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) {
+ if (this.editable()) {
this.$element.find('table:first').show();
this.$element.find('.oe_view_nocontent').remove();
- this.groups.new_record();
+ this.start_edition();
} else {
this._super();
}
},
on_loaded: function (data, grouped) {
+ var self = this;
+ if (this.editor) {
+ this.editor.destroy();
+ }
// tree/@editable takes priority on everything else if present.
- this.options.editable = ! this.options.read_only && (data.arch.attrs.editable || this.options.editable);
- return this._super(data, grouped);
+ var result = this._super(data, grouped);
+ if (this.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.$element
+ .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();
+ }
+ });
+ // Editor is not restartable due to formview not being
+ // restartable
+ this.editor = this.make_editor();
+ var editor_ready = this.editor.prependTo(this.$element)
+ .then(this.proxy('setup_events'));
+
+ return $.when(result, editor_ready);
+ }
+
+ 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 () {
+ var self = this, args = arguments;
+ this.ensure_saved().then(function () {
+ self.handle_button.apply(self, args);
+ });
},
/**
* Ensures the editable list is saved (saves any pending edition if
@@ -90,354 +152,581 @@ 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'));
+ var $recordRow = this.groups.get_row_for(record);
+ var cells = this.get_cells_for($recordRow);
+
+ return this.ensure_saved().pipe(function () {
+ 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 || field.get('effective_readonly')) {
+ // Readonly fields can just remain the list's,
+ // form's usually don't have backgrounds &al
+ field.set({invisible: true});
+ return;
+ }
+
+ // FIXME: need better way to get the field back from bubbling (delegated) DOM events somehow
+ field.$element.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;
- if (!this.edition) {
- return $.when();
+ 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);
}
-
- if (this.edition_id) {
- cancelled = this.reload_record(this.records.get(this.edition_id));
- } else {
- cancelled = $.when();
- }
- cancelled.then(function () {
- self.view.unpad_columns();
- self.edition_form.destroy();
- self.edition_form.$element.remove();
- delete self.edition_form;
- self.dataset.index = null;
- delete self.edition_id;
- delete self.edition;
- });
- this.pad_table_to(5);
- return cancelled;
},
/**
- * 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.$element.css({
+ top: position.top,
+ left: position.left,
+ width: $cell.outerWidth(),
+ minHeight: $cell.outerHeight()
});
- 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:
- $(e.target).blur();
- e.preventDefault();
- //e.stopImmediatePropagation();
- setTimeout(function () {
- self.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');
- }
- self.edit_record(next_record_id);
- }, 0);
+ 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);
+ }
+ // 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) {
+ /**
+ * @return {jQuery.Deferred}
+ */
+ cancel_edition: function () {
var self = this;
- return this.ensure_saved().pipe(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') : ''),
- click: function (e) {e.stopPropagation();}
- })
- .addClass('oe_form oe_form_container')
- .delegate('button.oe_list_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(); })
- .keypress(function (e) {
- if (e.which === KEY_RETURN) {
- return false;
- }
- });
-
- 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().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);
+ return self.reload_record(record);
}
- if ($last_child.is(':not([data-id])')) {
- $last_child.remove();
- }
- }
- self.edition = true;
- self.edition_id = record_id;
- self.dataset.index = _(self.dataset.ids).indexOf(record_id);
- if (self.dataset.index === -1) {
- self.dataset.index = null;
- }
- self.edition_form = _.extend(new instance.web.ListEditableFormView(self.view, self.dataset, false), {
- $element: $new_row,
- editable_list: self
- });
- // put in $.when just in case FormView.on_loaded becomes asynchronous
- return $.when(self.edition_form.on_loaded(self.get_form_fields_view())).then(function () {
- $new_row.find('> td')
- .end()
- .find('td:last').removeClass('oe_list_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});
- }
-
- self.edition_form.do_show();
- });
- });
- },
- handle_onwrite: function (source_record_id) {
- 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);
+ if (to_delete) {
+ self.records.remove(to_delete);
}
- self.reload_record(record);
});
});
},
/**
- * Saves the current row, and returns a Deferred resolving to an object
- * with the following properties:
+ * Executes an action on the view's editor bracketed by a cancellable
+ * event of the name provided.
*
- * ``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.
+ * 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
*
- * @returns {$.Deferred<{created: Boolean, edited_record: Record}>}
+ * @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}
*/
- save_row: function () {
- //noinspection JSPotentiallyInvalidConstructorUsage
+ with_event: function (event_name, event, action) {
var self = this;
- 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
- };
- });
+ 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).each(function (widget) {
+ var modifiers = JSON.parse(widget.attrs.modifiers || '{}');
+ widget.attrs.nolabel = true;
+ if (modifiers['tree_invisible'] || widget.tag === 'button') {
+ modifiers.invisible = true;
+ }
+ 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;
+ this.editor.$element.on('keyup keydown', function (e) {
+ if (!self.editor.is_editing()) { return; }
+ 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; }
+ var method = e.type + '_' + key.name;
+ if (!(method in self)) { return; }
+ self[method](e);
+ });
+ },
/**
- * If the current list is being edited, ensures it's saved
+ * Saves the current record, and goes to the next one (creation or
+ * edition)
+ *
+ * @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 {*}
*/
- ensure_saved: function () {
- if (this.edition) {
- // kinda-hack-ish: if the user has entered data in a field,
- // oe_form_dirty will be set on the form so save, otherwise
- // discard the current (entirely empty) line
- if (this.edition_form.$element.is('.oe_form_dirty')) {
- return this.save_row();
+ _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();
+ },
+ keyup_ESCAPE: function () {
+ return this.cancel_edition();
+ },
+ _text_selection_range: function (el) {
+ if (el.selectionStart !== undefined) {
+ return {
+ start: el.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) {
+
}
- return this.cancel_pending_edition();
}
- //noinspection JSPotentiallyInvalidConstructorUsage
- return $.when();
},
- /**
- * Cancels the edition of the row for the current dataset index
- */
- cancel_edition: function () {
- this.cancel_pending_edition();
+ _text_cursor: function (el) {
+ var selection = this._text_selection_range(el);
+ if (selection.start !== selection.end) {
+ return null;
+ }
+ return selection.start;
},
/**
- * 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.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(); }
+ // FIXME: assumes editable widgets are input-type elements
+ var index = this._text_cursor(event.target);
+ // If selecting or not at the start of the input
+ if (!is_valid_move(event.target, index)) { 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) {
+ return this._key_move_record(e, 'pred', function (el, index) {
+ return index === 0;
+ });
+ },
+ keydown_DOWN: function (e) {
+ return this._key_move_record(e, 'succ', function (el, index) {
+ return index === el.value.length;
});
+ },
+
+ 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 index = this._text_cursor(e.target);
+ if (index !== 0) { 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.$element.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 index = this._text_cursor(e.target);
+ if (index !== e.target.value.length) { 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.$element.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.$element.is(':visible'); })
+ .last()
+ .value();
+ // tabbed from last field in form
+ if (last_field && last_field.$element.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',
+ $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.$element).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.$element.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.$element.is(':visible')) {
+ return false;
+ }
+ field.focus();
+ // Stop as soon as a field got focused
+ return true;
+ });
},
- 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.on_record_loaded(_.extend({}, record))
+ : form.load_defaults();
+
+ return 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;
+ });
+ },
+ save: function () {
+ var self = this;
+ return this.form
+ .do_save(null, 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 () {
+ var record = this.record;
+ this.record = null;
+ if (!this.form.can_be_discarded()) {
+ return $.Deferred().reject({
+ message: "The form's data can not be discarded"}).promise();
+ }
+ 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()) {
+ return this._super.apply(this, arguments);
+ }
+ var record_id = $(event.currentTarget).data('id');
+ this.view.start_edition(
+ record_id ? this.records.get(record_id) : null, {
+ focus_field: $(event.target).data('field')
});
- $(QWeb.render('ListView.row.save')).appendTo($element);
},
+ /**
+ * 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/views.js b/addons/web/static/src/js/views.js
index bd102d867de..ddf4050fa38 100644
--- a/addons/web/static/src/js/views.js
+++ b/addons/web/static/src/js/views.js
@@ -274,7 +274,7 @@ instance.web.ActionManager = instance.web.Widget.extend({
},
ir_actions_report_xml: function(action, on_closed) {
var self = this;
- $.blockUI();
+ instance.web.blockUI();
self.rpc("/web/session/eval_domain_and_context", {
contexts: [action.context],
domains: []
@@ -284,7 +284,7 @@ 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();
@@ -841,7 +841,7 @@ instance.web.Sidebar = instance.web.Widget.extend({
} else {
self.do_attachement_update(self.dataset, self.model_id);
}
- $.unblockUI();
+ instance.web.unblockUI();
});
},
start: function() {
@@ -980,7 +980,7 @@ instance.web.Sidebar = instance.web.Widget.extend({
$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...'));
- $.blockUI();
+ instance.web.blockUI();
}
},
on_attachment_delete: function(e) {
@@ -1074,11 +1074,13 @@ instance.web.TranslateDialog = instance.web.Dialog.extend({
if (self.view.translatable_fields && self.view.translatable_fields.length) {
self.do_load_fields_values(function() {
sup.call(self);
+ // desactivated because it created an exception, plus it does not seem very useful
+ /*
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);
@@ -1346,7 +1348,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 55280f6badb..5d55a0ece15 100644
--- a/addons/web/static/src/xml/base.xml
+++ b/addons/web/static/src/xml/base.xml
@@ -329,21 +329,20 @@
</span>
</t>
<t t-name="UserMenu.about">
- <div>
- <a class="oe_activate_debug_mode" href="?debug" style="float:right; font-size: 80%;">Activate the developer 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">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">
@@ -630,9 +629,9 @@
<button type="button" class="oe_button oe_list_add oe_highlight">
<t t-esc="widget.options.addable"/>
</button>
- <t t-if="widget.options.import_enabled">
+ <span class="oe_alternative" t-if="widget.options.import_enabled">
<span class="oe_fade">or</span> <a href="#" class="oe_bold oe_list_button_import">Import</a>
- </t>
+ </span>
</t>
</div>
<t t-name="ListView.pager">
@@ -652,6 +651,7 @@
<tr t-name="ListView.row" t-att-class="row_parity"
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">
@@ -663,23 +663,26 @@
<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_list_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' : ''}"
+ t-att-data-field="column.id"
+ ><t t-raw="render_cell(record, column)"/></td>
</t>
<td t-if="options.deletable" class='oe_list_record_delete' width="1">
<button type="button" name="delete" class="oe_i">d</button>
</td>
</tr>
-<t t-name="ListView.row.save">
- <td>
- <button class='oe_i oe_list_edit_row_save' type='button' name='save'/>
- </td>
+<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>
+ </t>
+ <t t-jquery="a.oe_list_button_import" t-operation="after">
+ <a href="#" class="oe_bold oe_list_discard">discard</a>
+ </t>
</t>
<t t-name="FormView">
@@ -956,7 +959,7 @@
<span class="oe_form_m2o_follow"/>
</t>
<t t-if="!widget.get('effective_readonly')">
- <a href="#" class="oe_m2o_cm_button oe_e oe_right">/</a>
+ <a href="#" tabindex="-1" class="oe_m2o_cm_button oe_e oe_right">/</a>
<div>
<input type="text"
t-att-id="widget.id_for_label"
@@ -1216,6 +1219,8 @@
<div class="oe_searchview_clear"/>
<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"
@@ -1403,8 +1408,8 @@
<div t-name="SearchView.addtodashboard" class="oe_searchview_dashboard">
<h4>Add to Dashboard</h4>
<form>
- <input placeholder ="Title of new Dashboard item" title = "Title of new Dashboard item" type="text"/>
- <button class="oe_apply" type="submit">save</button>
+ <p><input placeholder ="Title of new Dashboard item" title = "Title of new Dashboard item" type="text"/></p>
+ <button class="oe_apply" type="submit">Save</button>
</form>
</div>
<t t-name="SearchView.addtodashboard.selection">
@@ -1498,7 +1503,7 @@
missing columns
-->
<t t-jquery="&gt; :last" t-operation="after">
- <td t-if="edited and !options.deletable" class="oe_list_padding"/>
+ <td t-if="edited and !options.deletable" class="oe-listview-padding"/>
</t>
</t>