diff options
| author | Xavier Morel <xmo@openerp.com> | 2011-08-04 10:55:16 +0200 |
|---|---|---|
| committer | Xavier Morel <xmo@openerp.com> | 2011-08-04 10:55:16 +0200 |
| commit | a66bbb20fcb57e1944ef29be23aa58aa1c2d1510 (patch) | |
| tree | 72ddf048b6d32fb6e51d42dadd5c144071b09041 /addons/base/static/src | |
| parent | 4b80238d1154b52f69125e5a702d1181c9c1a8e4 (diff) | |
| parent | 01aba99be5ddf9d481ef89dc3abfd978f0a4cbb8 (diff) | |
[MERGE] from trunk
bzr revid: xmo@openerp.com-20110804085516-nz1nelcutywq7gn4
Diffstat (limited to 'addons/base/static/src')
20 files changed, 2340 insertions, 854 deletions
diff --git a/addons/base/static/src/css/base.css b/addons/base/static/src/css/base.css index d19140a8a49..274553c1b5e 100644 --- a/addons/base/static/src/css/base.css +++ b/addons/base/static/src/css/base.css @@ -30,9 +30,13 @@ body.openerp { .openerp .oe-number { text-align: right !important; } +.openerp .oe_hide { + display: none !important; +} /* STATES */ -.openerp .on_logged { +.openerp .on_logged, +.openerp .db_options_row { display: none; } @@ -135,10 +139,84 @@ body.openerp { } .openerp.login-mode .menu, .openerp.login-mode .secondary_menu, -.openerp.login-mode .oe-application { +.openerp.login-mode .oe-application, +.openerp.login-mode .db_options_row { + display: none; +} + +/* Database */ +.openerp.database_block .db_options_row { + height: 100%; + display: table-row; +} + +.openerp.database_block .menu, +.openerp.database_block .secondary_menu, +.openerp.database_block .oe-application, +.openerp.database_block .login-container { display: none; } +.db_container { + width: 15%; + background: #666666; +} + +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: 0px; -webkit-border-radius: 0px; border-radius: 0px; + display: block; + font-weight: bold; + text-transform: uppercase; + margin: 1px; + color: #EEEEEE; + cursor: pointer; + width: 196px; + 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; +} + +.db_option_table input[type="text"], .db_option_table input[type="password"], .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%; @@ -158,14 +236,14 @@ body.openerp { .openerp .menu { height: 34px; -background: #cc4e45; /* Old browsers */ -background: -moz-linear-gradient(top, #cc4e45 0%, #b52d20 8%, #7a211a 100%); /* FF3.6+ */ -background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#cc4e45), color-stop(8%,#b52d20), color-stop(100%,#7a211a)); /* Chrome,Safari4+ */ -background: -webkit-linear-gradient(top, #cc4e45 0%,#b52d20 8%,#7a211a 100%); /* Chrome10+,Safari5.1+ */ -background: -o-linear-gradient(top, #cc4e45 0%,#b52d20 8%,#7a211a 100%); /* Opera11.10+ */ -background: -ms-linear-gradient(top, #cc4e45 0%,#b52d20 8%,#7a211a 100%); /* IE10+ */ -filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#CC4E45', endColorstr='#7A211A',GradientType=0 ); /* IE6-9 */ -background: linear-gradient(top, #cc4e45 0%,#b52d20 8%,#7a211a 100%); /* W3C */ + background: #cc4e45; /* Old browsers */ + background: -moz-linear-gradient(top, #cc4e45 0%, #b52d20 8%, #7a211a 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#cc4e45), color-stop(8%,#b52d20), color-stop(100%,#7a211a)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #cc4e45 0%,#b52d20 8%,#7a211a 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #cc4e45 0%,#b52d20 8%,#7a211a 100%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #cc4e45 0%,#b52d20 8%,#7a211a 100%); /* IE10+ */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#CC4E45', endColorstr='#7A211A',GradientType=0 ); /* IE6-9 */ + background: linear-gradient(top, #cc4e45 0%,#b52d20 8%,#7a211a 100%); /* W3C */ } .openerp .menu td { text-align: center; @@ -178,14 +256,14 @@ background: linear-gradient(top, #cc4e45 0%,#b52d20 8%,#7a211a 100%); /* W3C */ margin: 3px 2px; padding: 0 8px; -background: #bd5e54; /* Old browsers */ -background: -moz-linear-gradient(top, #bd5e54 0%, #90322a 60%); /* FF3.6+ */ -background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#bd5e54), color-stop(60%,#90322a)); /* Chrome,Safari4+ */ -background: -webkit-linear-gradient(top, #bd5e54 0%,#90322a 60%); /* Chrome10+,Safari5.1+ */ -background: -o-linear-gradient(top, #bd5e54 0%,#90322a 60%); /* Opera11.10+ */ -background: -ms-linear-gradient(top, #bd5e54 0%,#90322a 60%); /* IE10+ */ -filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#BD5E54', endColorstr='#90322A',GradientType=0 ); /* IE6-9 */ -background: linear-gradient(top, #bd5e54 0%,#90322a 60%); /* W3C */ + background: #bd5e54; /* Old browsers */ + background: -moz-linear-gradient(top, #bd5e54 0%, #90322a 60%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#bd5e54), color-stop(60%,#90322a)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #bd5e54 0%,#90322a 60%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #bd5e54 0%,#90322a 60%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #bd5e54 0%,#90322a 60%); /* IE10+ */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#BD5E54', endColorstr='#90322A',GradientType=0 ); /* IE6-9 */ + background: linear-gradient(top, #bd5e54 0%,#90322a 60%); /* W3C */ border: 1px solid #6E2A24; border-radius: 4px; @@ -205,16 +283,16 @@ background: linear-gradient(top, #bd5e54 0%,#90322a 60%); /* W3C */ .openerp .menu a:hover, .openerp .menu a:focus, .openerp .menu a.active { -background: #c6c6c6; /* Old browsers */ -background: -moz-linear-gradient(top, #c6c6c6 0%, #5c5c5c 7%, #969595 86%); /* FF3.6+ */ -background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#c6c6c6), color-stop(7%,#5c5c5c), color-stop(86%,#969595)); /* Chrome,Safari4+ */ -background: -webkit-linear-gradient(top, #c6c6c6 0%,#5c5c5c 7%,#969595 86%); /* Chrome10+,Safari5.1+ */ -background: -o-linear-gradient(top, #c6c6c6 0%,#5c5c5c 7%,#969595 86%); /* Opera11.10+ */ -background: -ms-linear-gradient(top, #c6c6c6 0%,#5c5c5c 7%,#969595 86%); /* IE10+ */ -filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#C6C6C6', endColorstr='#969595',GradientType=0 ); /* IE6-9 */ -background: linear-gradient(top, #c6c6c6 0%,#5c5c5c 7%,#969595 86%); /* W3C */ -/* for ie */ -filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#5c5c5c', endColorstr='#969595',GradientType=0 ); /* IE6-9 */ + background: #c6c6c6; /* Old browsers */ + background: -moz-linear-gradient(top, #c6c6c6 0%, #5c5c5c 7%, #969595 86%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#c6c6c6), color-stop(7%,#5c5c5c), color-stop(86%,#969595)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #c6c6c6 0%,#5c5c5c 7%,#969595 86%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #c6c6c6 0%,#5c5c5c 7%,#969595 86%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #c6c6c6 0%,#5c5c5c 7%,#969595 86%); /* IE10+ */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#C6C6C6', endColorstr='#969595',GradientType=0 ); /* IE6-9 */ + background: linear-gradient(top, #c6c6c6 0%,#5c5c5c 7%,#969595 86%); /* W3C */ + /* for ie */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#5c5c5c', endColorstr='#969595',GradientType=0 ); /* IE6-9 */ color: #fff; } @@ -236,19 +314,19 @@ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#5c5c5c', end } .openerp .secondary_menu h3 { padding: 0 0 2px; -background: #949292; /* Old browsers */ -background: -moz-linear-gradient(top, #949292 0%, #6d6b6b 87%, #282828 99%); /* FF3.6+ */ -background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#949292), color-stop(87%,#6d6b6b), color-stop(99%,#282828)); /* Chrome,Safari4+ */ -background: -webkit-linear-gradient(top, #949292 0%,#6d6b6b 87%,#282828 99%); /* Chrome10+,Safari5.1+ */ -background: -o-linear-gradient(top, #949292 0%,#6d6b6b 87%,#282828 99%); /* Opera11.10+ */ -background: -ms-linear-gradient(top, #949292 0%,#6d6b6b 87%,#282828 99%); /* IE10+ */ -filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#949292', endColorstr='#282828',GradientType=0 ); /* IE6-9 */ -background: linear-gradient(top, #949292 0%,#6d6b6b 87%,#282828 99%); /* W3C */ -/* for ie9 */ -filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#949292', endColorstr='#5B5A5A',GradientType=0 ); /* IE6-9 */ + background: #949292; /* Old browsers */ + background: -moz-linear-gradient(top, #949292 0%, #6d6b6b 87%, #282828 99%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#949292), color-stop(87%,#6d6b6b), color-stop(99%,#282828)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #949292 0%,#6d6b6b 87%,#282828 99%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #949292 0%,#6d6b6b 87%,#282828 99%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #949292 0%,#6d6b6b 87%,#282828 99%); /* IE10+ */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#949292', endColorstr='#282828',GradientType=0 ); /* IE6-9 */ + background: linear-gradient(top, #949292 0%,#6d6b6b 87%,#282828 99%); /* W3C */ + /* for ie9 */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#949292', endColorstr='#5B5A5A',GradientType=0 ); /* IE6-9 */ border: none; /* overriding jquery ui */ --moz-border-radius: 0px; -webkit-border-radius: 0px; border-radius: 0px; + -moz-border-radius: 0px; -webkit-border-radius: 0px; border-radius: 0px; } .openerp .secondary_menu h4 { padding: 0 0 2px 10px; @@ -274,17 +352,16 @@ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#949292', end .openerp .secondary_menu h4:hover, .openerp .secondary_menu h4:active, .openerp .secondary_menu h4.active { -background: #ffffff; /* Old browsers */ -background: -moz-linear-gradient(top, #ffffff 0%, #d8d8d8 11%, #afafaf 86%, #333333 91%, #5a5858 96%); /* FF3.6+ */ -background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ffffff), color-stop(11%,#d8d8d8), color-stop(86%,#afafaf), color-stop(91%,#333333), color-stop(96%,#5a5858)); /* Chrome,Safari4+ */ -background: -webkit-linear-gradient(top, #ffffff 0%,#d8d8d8 11%,#afafaf 86%,#333333 91%,#5a5858 96%); /* Chrome10+,Safari5.1+ */ -background: -o-linear-gradient(top, #ffffff 0%,#d8d8d8 11%,#afafaf 86%,#333333 91%,#5a5858 96%); /* Opera11.10+ */ -background: -ms-linear-gradient(top, #ffffff 0%,#d8d8d8 11%,#afafaf 86%,#333333 91%,#5a5858 96%); /* IE10+ */ -filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#FFFFFF', endColorstr='#5A5858',GradientType=0 ); /* IE6-9 */ -background: linear-gradient(top, #ffffff 0%,#d8d8d8 11%,#afafaf 86%,#333333 91%,#5a5858 96%); /* W3C */ + background: #ffffff; /* Old browsers */ + background: -moz-linear-gradient(top, #ffffff 0%, #d8d8d8 11%, #afafaf 86%, #333333 91%, #5a5858 96%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ffffff), color-stop(11%,#d8d8d8), color-stop(86%,#afafaf), color-stop(91%,#333333), color-stop(96%,#5a5858)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #ffffff 0%,#d8d8d8 11%,#afafaf 86%,#333333 91%,#5a5858 96%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #ffffff 0%,#d8d8d8 11%,#afafaf 86%,#333333 91%,#5a5858 96%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #ffffff 0%,#d8d8d8 11%,#afafaf 86%,#333333 91%,#5a5858 96%); /* IE10+ */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#FFFFFF', endColorstr='#5A5858',GradientType=0 ); /* IE6-9 */ + background: linear-gradient(top, #ffffff 0%,#d8d8d8 11%,#afafaf 86%,#333333 91%,#5a5858 96%); /* W3C */ /* overriding jquery ui */ --moz-border-radius-topright: 0; -webkit-border-top-right-radius: 0; border-top-right-radius: 0; - + -moz-border-radius-topright: 0; -webkit-border-top-right-radius: 0; border-top-right-radius: 0; color: #3f3d3d; text-shadow: #fff 0 1px 0; border: none !important; @@ -314,7 +391,6 @@ background: linear-gradient(top, #ffffff 0%,#d8d8d8 11%,#afafaf 86%,#333333 91%, float: left; height: 63px; width: 200px; - margin-right: 10px; border: 1px solid white; border-right-color: black; border-bottom-color: black; @@ -778,7 +854,7 @@ background: linear-gradient(top, #ffffff 0%,#d8d8d8 11%,#afafaf 86%,#333333 91%, } /* Inputs */ -.openerp input[type="text"], .openerp input[type="password"], .openerp select, .openerp textarea { +.openerp .oe_forms input[type="text"], .openerp .oe_forms input[type="password"], .openerp .oe_forms select, .openerp .oe_forms textarea { -moz-box-sizing: border-box; -webkit-box-sizing: border-box; -ms-box-sizing: border-box; @@ -792,17 +868,17 @@ background: linear-gradient(top, #ffffff 0%,#d8d8d8 11%,#afafaf 86%,#333333 91%, min-width: 90px; color: #1f1f1f; } -.openerp textarea { +.openerp .oe_forms textarea { resize:vertical; } -.openerp input[type="text"], .openerp input[type="password"], .openerp select, .openerp .button { +.openerp .oe_forms input[type="text"], .openerp .oe_forms input[type="password"], .openerp .oe_forms select, .openerp .oe_forms .button { height: 22px; } -.openerp .button { +.openerp .oe_forms .button { color: #4c4c4c; white-space: nowrap; } -.openerp .button span { +.openerp .oe_forms .button span { position: relative; vertical-align: top; } @@ -1021,6 +1097,10 @@ background: linear-gradient(top, #ffffff 0%,#ebe9e9 100%); /* W3C */ .openerp .closed-sidebar .toggle-sidebar { border-left: none; } +.openerp li.oe_sidebar_print { + padding-left: 20px; + background: 1px 3px url(../img/icons/gtk-print.png) no-repeat; +} .openerp.kitten-mode-activated .main_table { background: url(http://placekitten.com/g/1500/800) repeat; @@ -1093,3 +1173,10 @@ background: linear-gradient(top, #ffffff 0%,#ebe9e9 100%); /* W3C */ 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; +} diff --git a/addons/base/static/src/css/data_export.css b/addons/base/static/src/css/data_export.css new file mode 100644 index 00000000000..7b4b9707b39 --- /dev/null +++ b/addons/base/static/src/css/data_export.css @@ -0,0 +1,85 @@ +.openerp .oe_export_row tr{ + background-color: #FFFFFF; + font-size: 0.8em; + height: 22px; +} + +.openerp tr.ui-selected td { + background-color: #CCCCCC; +} + +.openerp .oe_export_requiredfield { + background-color: #D2D2FF; +} + +.openerp .oe_export_readonlyfield{ + background-color: #999999; +} + +.openerp .oe_export_row:hover{ + background-color: #F3F3F3; +} + +.openerp .oe_export_fields_selector_export { + width: 100%; + height: 400px; +} + +.openerp .oe_export_fields_selector_left { + width: 50%; +} + +.openerp div#left_field_panel { + overflow: scroll; + width: 100%; + height: 400px; + border: solid #999999 1px; +} + +.openerp .oe_export_fields_selector_center { + width: 102px; +} + +.openerp .oe_export_fields_selector_right { + width: 45%; + height: 400px; +} + +.openerp .oe_export_fields_selector_export select{ + width: 100%; + height: 100%; +} + +.openerp .oe_export_tree_header{ + border: 0.5px solid #E3E3E3; + text-align: left; + white-space: nowrap; + padding: 4px 5px; + background: url(/base/static/src/img/header.gif); +} + + +.openerp table.tree_grid{ + border: 1px solid #E3E3E3; + text-align: left; + white-space: nowrap; + background-color:#E3E3E3; + border-collapse: collapse; + width: 100%; +} + +.openerp table.tree_grid a:hover { + color: blue; + border: none; +} + +.openerp table.tree_grid a { + color: #5F5C5C; + border: none; + display: block; +} + +.openerp .oe_export_button_export { + border: 1px solid #006; + background-color: #F3F3F3; +} diff --git a/addons/base/static/src/img/collapse.gif b/addons/base/static/src/img/collapse.gif Binary files differnew file mode 100644 index 00000000000..759e72ebcfe --- /dev/null +++ b/addons/base/static/src/img/collapse.gif diff --git a/addons/base/static/src/img/db.png b/addons/base/static/src/img/db.png Binary files differnew file mode 100644 index 00000000000..83e42df26b3 --- /dev/null +++ b/addons/base/static/src/img/db.png diff --git a/addons/base/static/src/img/expand.gif b/addons/base/static/src/img/expand.gif Binary files differnew file mode 100644 index 00000000000..ca98d0b5f2b --- /dev/null +++ b/addons/base/static/src/img/expand.gif diff --git a/addons/base/static/src/img/header.gif b/addons/base/static/src/img/header.gif Binary files differnew file mode 100644 index 00000000000..f40bd9c0f6a --- /dev/null +++ b/addons/base/static/src/img/header.gif diff --git a/addons/base/static/src/js/base.js b/addons/base/static/src/js/boot.js index 28022f8b764..c16f2d261f5 100644 --- a/addons/base/static/src/js/base.js +++ b/addons/base/static/src/js/boot.js @@ -1,5 +1,5 @@ //--------------------------------------------------------- -// OpenERP initialisation and black magic about the pool +// OpenERP Web Boostrap //--------------------------------------------------------- /** @@ -57,9 +57,8 @@ // OpenERP base module split //--------------------------------------------------------- -/** @namespace */ openerp.base = function(instance) { - openerp.base.controller(instance); + openerp.base.core(instance); openerp.base.dates(instance); openerp.base.chrome(instance); openerp.base.data(instance); @@ -87,6 +86,9 @@ openerp.base = function(instance) { if (openerp.base.view_tree) { openerp.base.view_tree(instance); } + if (openerp.base.data_export) { + openerp.base.data_export(instance); + } }; // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: diff --git a/addons/base/static/src/js/chrome.js b/addons/base/static/src/js/chrome.js index a435850ce20..fd5fae7727d 100644 --- a/addons/base/static/src/js/chrome.js +++ b/addons/base/static/src/js/chrome.js @@ -4,241 +4,7 @@ openerp.base.chrome = function(openerp) { -/** - * Base error for lookup failure - * - * @class - */ -openerp.base.NotFound = openerp.base.Class.extend( /** @lends openerp.base.NotFound# */ { -}); -openerp.base.KeyNotFound = openerp.base.NotFound.extend( /** @lends openerp.base.KeyNotFound# */ { - /** - * Thrown when a key could not be found in a mapping - * - * @constructs - * @extends openerp.base.NotFound - * @param {String} key the key which could not be found - */ - init: function (key) { - this.key = key; - }, - toString: function () { - return "The key " + this.key + " was not found"; - } -}); -openerp.base.ObjectNotFound = openerp.base.NotFound.extend( /** @lends openerp.base.ObjectNotFound# */ { - /** - * Thrown when an object path does not designate a valid class or object - * in the openerp hierarchy. - * - * @constructs - * @extends openerp.base.NotFound - * @param {String} path the invalid object path - */ - init: function (path) { - this.path = path; - }, - toString: function () { - return "Could not find any object of path " + this.path; - } -}); -openerp.base.Registry = openerp.base.Class.extend( /** @lends openerp.base.Registry# */ { - /** - * Stores a mapping of arbitrary key (strings) to object paths (as strings - * as well). - * - * Resolves those paths at query time in order to always fetch the correct - * object, even if those objects have been overloaded/replaced after the - * registry was created. - * - * An object path is simply a dotted name from the openerp root to the - * object pointed to (e.g. ``"openerp.base.Session"`` for an OpenERP - * session object). - * - * @constructs - * @param {Object} mapping a mapping of keys to object-paths - */ - init: function (mapping) { - this.map = mapping || {}; - }, - /** - * Retrieves the object matching the provided key string. - * - * @param {String} key the key to fetch the object for - * @returns {Class} the stored class, to initialize - * - * @throws {openerp.base.KeyNotFound} if the object was not in the mapping - * @throws {openerp.base.ObjectNotFound} if the object path was invalid - */ - get_object: function (key) { - var path_string = this.map[key]; - if (path_string === undefined) { - throw new openerp.base.KeyNotFound(key); - } - - var object_match = openerp; - var path = path_string.split('.'); - // ignore first section - for(var i=1; i<path.length; ++i) { - object_match = object_match[path[i]]; - - if (object_match === undefined) { - throw new openerp.base.ObjectNotFound(path_string); - } - } - return object_match; - }, - /** - * Tries a number of keys, and returns the first object matching one of - * the keys. - * - * @param {Array} keys a sequence of keys to fetch the object for - * @returns {Class} the first class found matching an object - * - * @throws {openerp.base.KeyNotFound} if none of the keys was in the mapping - * @trows {openerp.base.ObjectNotFound} if a found object path was invalid - */ - get_any: function (keys) { - for (var i=0; i<keys.length; ++i) { - try { - return this.get_object(keys[i]); - } catch (e) { - if (e instanceof openerp.base.KeyNotFound) { - continue; - } - throw e; - } - } - throw new openerp.base.KeyNotFound(keys.join(',')); - }, - /** - * Adds a new key and value to the registry. - * - * This method can be chained. - * - * @param {String} key - * @param {String} object_path fully qualified dotted object path - * @returns {openerp.base.Registry} itself - */ - add: function (key, object_path) { - this.map[key] = object_path; - return this; - }, - /** - * Creates and returns a copy of the current mapping, with the provided - * mapping argument added in (replacing existing keys if needed) - * - * @param {Object} [mapping={}] a mapping of keys to object-paths - */ - clone: function (mapping) { - return new openerp.base.Registry( - _.extend({}, this.map, mapping || {})); - } -}); - -/** - * OpenERP session aware controller - * a controller takes an already existing dom element and manage it - */ -openerp.base.Controller = openerp.base.Controller.extend( /** @lends openerp.base.Controller# */{ - init: function(parent, element_id) { - this._super(parent, element_id); - if(this.controller_parent && this.controller_parent.session) { - this.session = this.controller_parent.session; - } - }, - /** - * Performs a JSON-RPC call - * - * @param {String} url endpoint url - * @param {Object} data RPC parameters - * @param {Function} success RPC call success callback - * @param {Function} error RPC call error callback - * @returns {jQuery.Deferred} deferred object for the RPC call - */ - rpc: function(url, data, success, error) { - return this.session.rpc(url, data, success, error); - }, - do_action: function(action, on_finished) { - return this.parent.do_action(action, on_finished); - } -}); - -/** - * OpenERP session aware widget - * A widget is a controller that doesnt take an element_id - * it render its own html render() that you should insert into the dom - * and bind it at start() - */ -openerp.base.BaseWidget = openerp.base.Controller.extend({ - /** - * The name of the QWeb template that will be used for rendering. Must be - * redefined in subclasses or the render() method can not be used. - * - * @type string - */ - template: null, - /** - * The prefix used to generate an id automatically. Should be redefined in - * subclasses. If it is not defined, a default identifier will be used. - * - * @type string - */ - identifier_prefix: 'generic-identifier', - /** - * Base class for widgets. Handle rendering (based on a QWeb template), - * identifier generation, parenting and destruction of the widget. - * Also initialize the identifier. - * - * @constructs - * @params {openerp.base.search.BaseWidget} parent The parent widget. - */ - init: function (parent) { - this._super(parent); - this.make_id(this.identifier_prefix); - }, - /** - * Sets and returns a globally unique identifier for the widget. - * - * If a prefix is appended, the identifier will be appended to it. - * - * @params sections prefix sections, empty/falsy sections will be removed - */ - make_id: function () { - this.element_id = _.uniqueId(_.toArray(arguments).join('_')); - return this.element_id; - }, - /** - * Render the widget. This.template must be defined. - * The content of the current object is passed as context to the template. - * - * @param {object} additional Additional context arguments to pass to the template. - */ - render: function (additional) { - return QWeb.render(this.template, _.extend({}, this, additional != null ? additional : {})); - }, - /** - * "Starts" the widgets. Called at the end of the rendering, this allows - * to get a jQuery object referring to the DOM ($element attribute). - */ - start: function () { - this._super(); - var tmp = document.getElementById(this.element_id); - this.$element = tmp ? $(tmp) : null; - }, - /** - * "Stops" the widgets. Called when the view destroys itself, this - * lets the widgets clean up after themselves. - */ - stop: function () { - if(this.$element != null) { - this.$element.remove(); - } - this._super(); - } -}); - -openerp.base.Session = openerp.base.Controller.extend( /** @lends openerp.base.Session# */{ +openerp.base.Session = openerp.base.Widget.extend( /** @lends openerp.base.Session# */{ /** * @constructs * @param element_id to use for exception reporting @@ -514,7 +280,7 @@ openerp.base.Session = openerp.base.Controller.extend( /** @lends openerp.base.S } }); -openerp.base.Notification = openerp.base.Controller.extend({ +openerp.base.Notification = openerp.base.Widget.extend({ init: function(parent, element_id) { this._super(parent, element_id); this.$element.notify({ @@ -536,7 +302,7 @@ openerp.base.Notification = openerp.base.Controller.extend({ } }); -openerp.base.Dialog = openerp.base.BaseWidget.extend({ +openerp.base.Dialog = openerp.base.OldWidget.extend({ dialog_title: "", identifier_prefix: 'dialog', init: function (parent, options) { @@ -552,8 +318,8 @@ openerp.base.Dialog = openerp.base.BaseWidget.extend({ max_height: '100%', autoOpen: false, buttons: {}, - close: function () { - self.stop(); + beforeClose: function () { + self.on_close(); } }; for (var f in this) { @@ -617,7 +383,15 @@ openerp.base.Dialog = openerp.base.BaseWidget.extend({ this.set_options(options); this.$dialog.dialog(this.options).dialog('open'); }, + close: function() { + // Closes the dialog but leave it in a state where it could be opened again. + this.$dialog.dialog('close'); + }, + on_close: function() { + }, stop: function () { + // Destroy widget + this.close(); this.$dialog.dialog('destroy'); } }); @@ -651,14 +425,14 @@ openerp.base.CrashManager = openerp.base.Dialog.extend({ this.dialog_title = "OpenERP Error"; this.template = 'DialogTraceback'; this.open({ - width: '80%', - height: '80%' + width: 'auto', + height: 'auto' }); } } }); -openerp.base.Loading = openerp.base.Controller.extend({ +openerp.base.Loading = openerp.base.Widget.extend({ init: function(parent, element_id) { this._super(parent, element_id); this.count = 0; @@ -677,16 +451,314 @@ openerp.base.Loading = openerp.base.Controller.extend({ } }); -openerp.base.Database = openerp.base.Controller.extend({ +openerp.base.Database = openerp.base.Widget.extend({ + init: function(parent, element_id, option_id) { + this._super(parent, element_id); + this.$option_id = $('#' + option_id); + }, + start: function() { + this.$element.html(QWeb.render("Database", this)); + this.$element.closest(".openerp") + .removeClass("login-mode") + .addClass("database_block"); + + var self = this; + + var fetch_db = this.rpc("/base/database/get_list", {}, function(result) { + self.db_list = result.db_list; + }); + var fetch_langs = this.rpc("/base/session/get_lang_list", {}, function(result) { + if (result.error) { + self.display_error(result); + return; + } + self.lang_list = result.lang_list; + }); + $.when(fetch_db, fetch_langs).then(function () {self.do_create();}); + + this.$element.find('#db-create').click(this.do_create); + this.$element.find('#db-drop').click(this.do_drop); + this.$element.find('#db-backup').click(this.do_backup); + this.$element.find('#db-restore').click(this.do_restore); + this.$element.find('#db-change-password').click(this.do_change_password); + this.$element.find('#back-to-login').click(function() { + self.stop(); + }); + }, + stop: function () { + this.$option_id.empty(); + + this.$element + .find('#db-create, #db-drop, #db-backup, #db-restore, #db-change-password, #back-to-login') + .unbind('click') + .end() + .closest(".openerp") + .addClass("login-mode") + .removeClass("database_block") + .end() + .empty(); + + }, + /** + * Converts a .serializeArray() result into a dict. Does not bother folding + * multiple identical keys into an array, last key wins. + * + * @param {Array} array + */ + to_object: function (array) { + var result = {}; + _(array).each(function (record) { + result[record.name] = record.value; + }); + return result; + }, + /** + * Waits until the new database is done creating, then unblocks the UI and + * logs the user in as admin + * + * @param {Number} db_creation_id identifier for the db-creation operation, used to fetch the current installation progress + * @param {Object} info info fields for this database creation + * @param {String} info.db name of the database being created + * @param {String} info.password super-admin password for the database + */ + wait_for_newdb: function (db_creation_id, info) { + var self = this; + self.rpc('/base/database/progress', { + id: db_creation_id, + password: info.password + }, function (result) { + var progress = result[0]; + // I'd display a progress bar, but turns out the progress status + // the server report kind-of blows goats: it's at 0 for ~75% of + // the installation, then jumps to 75%, then jumps down to either + // 0 or ~40%, then back up to 75%, then terminates. Let's keep that + // mess hidden behind a not-very-useful but not overly weird + // message instead. + if (progress < 1) { + setTimeout(function () { + self.wait_for_newdb(db_creation_id, info); + }, 500); + return; + } + + var admin = result[1][0]; + setTimeout(function () { + self.stop(); + self.widget_parent.do_login( + info.db, admin.login, admin.password); + $.unblockUI(); + }); + }); + }, + /** + * Displays an error dialog resulting from the various RPC communications + * failing over themselves + * + * @param {Object} error error description + * @param {String} error.title title of the error dialog + * @param {String} error.error message of the error dialog + */ + display_error: function (error) { + return $('<div>').dialog({ + modal: true, + title: error.title, + buttons: { + Ok: function() { + $(this).dialog("close"); + } + } + }).html(error.error); + }, + do_create: function() { + var self = this; + self.$option_id.html(QWeb.render("CreateDB", self)); + + self.$option_id.find("form[name=create_db_form]").validate({ + submitHandler: function (form) { + var fields = $(form).serializeArray(); + $.blockUI(); + self.rpc("/base/database/create", {'fields': fields}, function(result) { + if (result.error) { + $.unblockUI(); + self.display_error(result); + return; + } + self.db_list.push(self.to_object(fields)['db_name']); + self.db_list.sort(); + var form_obj = self.to_object(fields); + self.wait_for_newdb(result, { + password: form_obj['super_admin_pwd'], + db: form_obj['db_name'] + }); + }); + } + }); + }, + + do_drop: function() { + var self = this; + self.$option_id.html(QWeb.render("DropDB", self)); + + self.$option_id.find("form[name=drop_db_form]").validate({ + submitHandler: function (form) { + var $form = $(form), + fields = $form.serializeArray(), + $db_list = $form.find('select[name=drop_db]'), + db = $db_list.val(); + + if (!confirm("Do you really want to delete the database: " + db + " ?")) { + return; + } + self.rpc("/base/database/drop", {'fields': fields}, function(result) { + if (result.error) { + self.display_error(result); + return; + } + $db_list.find(':selected').remove(); + self.db_list.splice(_.indexOf(self.db_list, db, true), 1); + self.notification.notify("Dropping database", "The database '" + db + "' has been dropped"); + }); + } + }); + }, + + wait_for_file: function (token, cleanup) { + var self = this, + cookie_name = 'fileToken', + cookie_length = cookie_name.length; + this.backup_timer = setInterval(function () { + var cookies = document.cookie.split(';'); + for(var i=0; i<cookies.length; ++i) { + var cookie = cookies[i].replace(/^\s*/, ''); + if(!cookie.indexOf(cookie_name) === 0) { continue; } + var cookie_val = cookie.substring(cookie_length + 1); + if(parseInt(cookie_val, 10) !== token) { continue; } + + // clear waiter + clearInterval(self.backup_timer); + // clear cookie + document.cookie = _.sprintf("%s=;expires=%s;path=/", + cookie_name, new Date().toGMTString()); + + if (cleanup) { cleanup(); } + } + }, 100); + }, + do_backup: function() { + var self = this; + self.$option_id.html(QWeb.render("BackupDB", self)); + + self.$option_id.find("form[name=backup_db_form]").validate({ + submitHandler: function (form) { + $.blockUI(); + // need to detect when the file is done downloading (not used + // yet, but we'll need it to fix the UI e.g. with a throbber + // while dump is being generated), iframe load event only fires + // when the iframe content loads, so we need to go smarter: + // http://geekswithblogs.net/GruffCode/archive/2010/10/28/detecting-the-file-download-dialog-in-the-browser.aspx + var $target = $('#backup-target'), + token = new Date().getTime(); + if (!$target.length) { + $target = $('<iframe id="backup-target" style="display: none;">') + .appendTo(document.body) + .load(function () { + $.unblockUI(); + clearInterval(self.backup_timer); + var error = this.contentDocument.body + .firstChild.data + .split('|'); + self.display_error({ + title: error[0], + error: error[1] + }); + }); + } + $(form).find('input[name=token]').val(token); + form.submit(); + + self.wait_for_file(token, function () { + $.unblockUI(); + }); + } + }); + }, + + do_restore: function() { + var self = this; + self.$option_id.html(QWeb.render("RestoreDB", self)); + + self.$option_id.find("form[name=restore_db_form]").validate({ + submitHandler: function (form) { + $.blockUI(); + $(form).ajaxSubmit({ + url: '/base/database/restore', + type: 'POST', + resetForm: true, + success: function (body) { + // TODO: ui manipulations + // note: response objects don't work, but we have the + // HTTP body of the response~~ + + // If empty body, everything went fine + if (!body) { return; } + + if (body.indexOf('403 Forbidden') !== -1) { + self.display_error({ + title: 'Access Denied', + error: 'Incorrect super-administrator password' + }) + } else { + self.display_error({ + title: 'Restore Database', + error: 'Could not restore the database' + }) + } + }, + complete: function () { + $.unblockUI(); + } + }); + } + }); + }, + + do_change_password: function() { + var self = this; + self.$option_id.html(QWeb.render("Change_DB_Pwd", self)); + + self.$option_id.find("form[name=change_pwd_form]").validate({ + messages: { + old_pwd: "Please enter your previous password", + new_pwd: "Please enter your new password", + confirm_pwd: { + required: "Please confirm your new password", + equalTo: "The confirmation does not match the password" + } + }, + submitHandler: function (form) { + self.rpc("/base/database/change_password", { + 'fields': $(form).serializeArray() + }, function(result) { + if (result.error) { + self.display_error(result); + return; + } + self.notification.notify("Changed Password", "Password has been changed successfully"); + }); + } + }); + } }); -openerp.base.Login = openerp.base.Controller.extend({ +openerp.base.Login = openerp.base.Widget.extend({ remember_creditentials: true, + init: function(parent, element_id) { this._super(parent, element_id); this.has_local_storage = typeof(localStorage) != 'undefined'; this.selected_db = null; this.selected_login = null; + if (this.has_local_storage && this.remember_creditentials) { this.selected_db = localStorage.getItem('last_db_login_success'); this.selected_login = localStorage.getItem('last_login_login_success'); @@ -699,7 +771,7 @@ openerp.base.Login = openerp.base.Controller.extend({ }, start: function() { var self = this; - this.rpc("/base/database/get_databases_list", {}, function(result) { + this.rpc("/base/database/get_list", {}, function(result) { self.db_list = result.db_list; self.display(); }, function() { @@ -707,7 +779,16 @@ openerp.base.Login = openerp.base.Controller.extend({ }); }, display: function() { + var self = this; + this.$element.html(QWeb.render("Login", this)); + this.database = new openerp.base.Database( + this, "oe_database", "oe_db_options"); + + this.$element.find('#oe-db-config').click(function() { + self.database.start(); + }); + this.$element.find("form").submit(this.on_submit); }, on_login_invalid: function() { @@ -718,13 +799,22 @@ openerp.base.Login = openerp.base.Controller.extend({ }, on_submit: function(ev) { ev.preventDefault(); - var self = this; var $e = this.$element; var db = $e.find("form [name=db]").val(); var login = $e.find("form input[name=login]").val(); var password = $e.find("form input[name=password]").val(); - //$e.hide(); - // Should hide then call callback + + this.do_login(db, login, password); + }, + /** + * Performs actual login operation, and UI-related stuff + * + * @param {String} db database to log in + * @param {String} login user login + * @param {String} password user password + */ + do_login: function (db, login, password) { + var self = this; this.session.session_login(db, login, password, function() { if(self.session.session_is_valid()) { if (self.has_local_storage) { @@ -758,7 +848,7 @@ openerp.base.Login = openerp.base.Controller.extend({ } }); -openerp.base.Header = openerp.base.Controller.extend({ +openerp.base.Header = openerp.base.Widget.extend({ init: function(parent, element_id) { this._super(parent, element_id); }, @@ -772,7 +862,7 @@ openerp.base.Header = openerp.base.Controller.extend({ on_logout: function() {} }); -openerp.base.Menu = openerp.base.Controller.extend({ +openerp.base.Menu = openerp.base.Widget.extend({ init: function(parent, element_id, secondary_menu_id) { this._super(parent, element_id); this.secondary_menu_id = secondary_menu_id; @@ -853,16 +943,16 @@ openerp.base.Menu = openerp.base.Controller.extend({ } }); -openerp.base.Homepage = openerp.base.Controller.extend({ +openerp.base.Homepage = openerp.base.Widget.extend({ }); -openerp.base.Preferences = openerp.base.Controller.extend({ +openerp.base.Preferences = openerp.base.Widget.extend({ }); -openerp.base.ImportExport = openerp.base.Controller.extend({ +openerp.base.ImportExport = openerp.base.Widget.extend({ }); -openerp.base.WebClient = openerp.base.Controller.extend({ +openerp.base.WebClient = openerp.base.Widget.extend({ init: function(element_id) { this._super(null, element_id); @@ -879,7 +969,7 @@ openerp.base.WebClient = openerp.base.Controller.extend({ this.crashmanager.start(false); // Do you autorize this ? will be replaced by notify() in controller - openerp.base.Controller.prototype.notification = new openerp.base.Notification(this, "oe_notification"); + openerp.base.Widget.prototype.notification = new openerp.base.Notification(this, "oe_notification"); this.header = new openerp.base.Header(this, "oe_header"); this.login = new openerp.base.Login(this, "oe_login"); @@ -891,6 +981,7 @@ openerp.base.WebClient = openerp.base.Controller.extend({ this.menu = new openerp.base.Menu(this, "oe_menu", "oe_secondary_menu"); this.menu.on_action.add(this.on_menu_action); + }, start: function() { this.session.start(); diff --git a/addons/base/static/src/js/controller.js b/addons/base/static/src/js/controller.js deleted file mode 100644 index c9ca3193e70..00000000000 --- a/addons/base/static/src/js/controller.js +++ /dev/null @@ -1,248 +0,0 @@ -/*--------------------------------------------------------- - * OpenERP controller framework - *--------------------------------------------------------*/ - -openerp.base.controller = function(instance) { -/** - * John Resig Class with factory improvement - */ -(function() { - var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/; - // The base Class implementation (does nothing) - this.Class = function(){}; - - // Create a new Class that inherits from this class - Class.extend = function(prop) { - var _super = this.prototype; - - // Instantiate a base class (but only create the instance, - // don't run the init constructor) - initializing = true; - var prototype = new this(); - initializing = false; - - // Copy the properties over onto the new prototype - 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() { - var tmp = this._super; - - // Add a new ._super() method that is the same method - // but on the super-class - this._super = _super[name]; - - // The method only need to be bound temporarily, so we - // remove it when we're done executing - var ret = fn.apply(this, arguments); - this._super = tmp; - - return ret; - }; - })(name, prop[name]) : - prop[name]; - } - - // The dummy class constructor - function Class() { - // All construction is actually done in the init method - if ( !initializing && this.init ) { - var ret = this.init.apply(this, arguments); - if (ret) { return ret; } - } - return this; - } - - // Populate our constructed prototype object - Class.prototype = prototype; - - // Enforce the constructor to be what we expect - Class.constructor = Class; - - // And make this class extendable - Class.extend = arguments.callee; - - return Class; - }; -})(); - -// todo change john resig class to keep window clean -instance.base.Class = window.Class - -instance.base.callback = function(obj, method) { - var callback = function() { - var args = Array.prototype.slice.call(arguments); - 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; - } - r = c.callback.apply(c.self, c.args.concat(args)); - // TODO special value to stop the chain - // openerp.base.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" - }); - }; - - return callback.add({ - callback: method, - self:obj, - args:Array.prototype.slice.call(arguments, 2) - }); -}; - -/** - * Generates an inherited class that replaces all the methods by null methods (methods - * that does nothing and always return undefined). - * - * @param {Class} claz - * @param {dict} add Additional functions to override. - * @return {Class} - */ -instance.base.generate_null_object_class = function(claz, add) { - var newer = {}; - var copy_proto = function(prototype) { - for (var name in prototype) { - if(typeof prototype[name] == "function") { - newer[name] = function() {}; - } - } - if (prototype.prototype) - copy_proto(prototype.prototype); - }; - copy_proto(claz.prototype); - newer.init = instance.base.Controller.prototype.init; - var tmpclass = claz.extend(newer); - return tmpclass.extend(add || {}); -}; - -/** - * OpenERP Controller - * TODO merge BaseWidget with Controller - */ -instance.base.Controller = instance.base.Class.extend( /** @lends instance.base.Controller# */{ - /** - * @constructs - * rpc operations, event binding and callback calling should be done in - * start() instead of init so that events can be hooked in between. - */ - init: function(parent, element_id) { - this.element_id = element_id; - this.$element = $('#' + element_id); - if (element_id) { - instance.screen[element_id] = this; - } - // save the parent children relationship - this.controller_parent = parent; - this.controller_children = []; - if(parent && parent.controller_children) { - parent.controller_children.push(this); - } - // backward compatibility - this.parent = this.controller_parent; - this.children = this.controller_children; - - // Transform on_* method into openerp.base.callbacks - for (var name in this) { - if(typeof(this[name]) == "function") { - this[name].debug_name = name; - // bind ALL function to this not only on_and _do ? - if((/^on_|^do_/).test(name)) { - this[name] = instance.base.callback(this, this[name]); - } - } - } - }, - /** - * Event binding, rpc and callback calling required to initialize the - * object should happen here - * - * Returns a promise object letting callers (subclasses and direct callers) - * know when this component is done starting - * - * @returns {jQuery.Deferred} - */ - start: function() { - // returns an already fulfilled promise. Maybe we could return nothing? - // $.when can take non-deferred and in that case it simply considers - // them all as fulfilled promises. - // But in thise case we *have* to ensure callers use $.when and don't - // try to call deferred methods on this return value. - return $.Deferred().done().promise(); - }, - stop: function() { - if (this.parent && this.parent.children) { - this.parent.children = _.without(this.parent.children, this); - this.parent.controller_children = this.parent.children; - } - this.parent = null; - this.controller_parent = null; - }, - log: function() { - var args = Array.prototype.slice.call(arguments); - var caller = arguments.callee.caller; - // TODO add support for line number using - // https://github.com/emwendelin/javascript-stacktrace/blob/master/stacktrace.js - // args.unshift("" + caller.debug_name); - this.on_log.apply(this,args); - }, - on_log: function() { - if(window.openerp.debug || (window.location.search.indexOf('?debug') !== -1)) { - var notify = false; - var body = false; - if(window.console) { - console.log(arguments); - } else { - body = true; - } - var a = Array.prototype.slice.call(arguments, 0); - for(var i = 0; i < a.length; i++) { - var v = a[i]==null ? "null" : a[i].toString(); - if(i==0) { - notify = v.match(/^not/); - body = v.match(/^bod/); - } - if(body) { - $('<pre></pre>').text(v).appendTo($('body')); - } - if(notify && this.notification) { - this.notification.notify("Logging:",v); - } - } - } - - } -}); - -}; - -// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: diff --git a/addons/base/static/src/js/core.js b/addons/base/static/src/js/core.js new file mode 100644 index 00000000000..e57d79e40bb --- /dev/null +++ b/addons/base/static/src/js/core.js @@ -0,0 +1,572 @@ +/*--------------------------------------------------------- + * OpenERP controller framework + *--------------------------------------------------------*/ + +openerp.base.core = function(openerp) { +/** + * John Resig Class with factory improvement + */ +(function() { + var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/; + // The base Class implementation (does nothing) + openerp.base.Class = function(){}; + + // Create a new Class that inherits from this class + openerp.base.Class.extend = function(prop) { + var _super = this.prototype; + + // Instantiate a base class (but only create the instance, + // don't run the init constructor) + initializing = true; + var prototype = new this(); + initializing = false; + + // Copy the properties over onto the new prototype + 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() { + var tmp = this._super; + + // Add a new ._super() method that is the same method + // but on the super-class + this._super = _super[name]; + + // The method only need to be bound temporarily, so we + // remove it when we're done executing + var ret = fn.apply(this, arguments); + this._super = tmp; + + return ret; + }; + })(name, prop[name]) : + prop[name]; + } + + // The dummy class constructor + function Class() { + // All construction is actually done in the init method + if ( !initializing && this.init ) { + var ret = this.init.apply(this, arguments); + if (ret) { return ret; } + } + return this; + } + + // Populate our constructed prototype object + Class.prototype = prototype; + + // Enforce the constructor to be what we expect + Class.constructor = Class; + + // And make this class extendable + Class.extend = arguments.callee; + + return Class; + }; +})(); + +openerp.base.callback = function(obj, method) { + var callback = function() { + var args = Array.prototype.slice.call(arguments); + 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; + } + r = c.callback.apply(c.self, c.args.concat(args)); + // TODO special value to stop the chain + // openerp.base.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" + }); + }; + + return callback.add({ + callback: method, + self:obj, + args:Array.prototype.slice.call(arguments, 2) + }); +}; + +/** + * Generates an inherited class that replaces all the methods by null methods (methods + * that does nothing and always return undefined). + * + * @param {Class} claz + * @param {dict} add Additional functions to override. + * @return {Class} + */ +openerp.base.generate_null_object_class = function(claz, add) { + var newer = {}; + var copy_proto = function(prototype) { + for (var name in prototype) { + if(typeof prototype[name] == "function") { + newer[name] = function() {}; + } + } + if (prototype.prototype) + copy_proto(prototype.prototype); + }; + copy_proto(claz.prototype); + newer.init = openerp.base.Widget.prototype.init; + var tmpclass = claz.extend(newer); + return tmpclass.extend(add || {}); +}; + +/** + * Base error for lookup failure + * + * @class + */ +openerp.base.NotFound = openerp.base.Class.extend( /** @lends openerp.base.NotFound# */ { +}); +openerp.base.KeyNotFound = openerp.base.NotFound.extend( /** @lends openerp.base.KeyNotFound# */ { + /** + * Thrown when a key could not be found in a mapping + * + * @constructs + * @extends openerp.base.NotFound + * @param {String} key the key which could not be found + */ + init: function (key) { + this.key = key; + }, + toString: function () { + return "The key " + this.key + " was not found"; + } +}); +openerp.base.ObjectNotFound = openerp.base.NotFound.extend( /** @lends openerp.base.ObjectNotFound# */ { + /** + * Thrown when an object path does not designate a valid class or object + * in the openerp hierarchy. + * + * @constructs + * @extends openerp.base.NotFound + * @param {String} path the invalid object path + */ + init: function (path) { + this.path = path; + }, + toString: function () { + return "Could not find any object of path " + this.path; + } +}); +openerp.base.Registry = openerp.base.Class.extend( /** @lends openerp.base.Registry# */ { + /** + * Stores a mapping of arbitrary key (strings) to object paths (as strings + * as well). + * + * Resolves those paths at query time in order to always fetch the correct + * object, even if those objects have been overloaded/replaced after the + * registry was created. + * + * An object path is simply a dotted name from the openerp root to the + * object pointed to (e.g. ``"openerp.base.Session"`` for an OpenERP + * session object). + * + * @constructs + * @param {Object} mapping a mapping of keys to object-paths + */ + init: function (mapping) { + this.map = mapping || {}; + }, + /** + * Retrieves the object matching the provided key string. + * + * @param {String} key the key to fetch the object for + * @returns {Class} the stored class, to initialize + * + * @throws {openerp.base.KeyNotFound} if the object was not in the mapping + * @throws {openerp.base.ObjectNotFound} if the object path was invalid + */ + get_object: function (key) { + var path_string = this.map[key]; + if (path_string === undefined) { + throw new openerp.base.KeyNotFound(key); + } + + var object_match = openerp; + var path = path_string.split('.'); + // ignore first section + for(var i=1; i<path.length; ++i) { + object_match = object_match[path[i]]; + + if (object_match === undefined) { + throw new openerp.base.ObjectNotFound(path_string); + } + } + return object_match; + }, + /** + * Tries a number of keys, and returns the first object matching one of + * the keys. + * + * @param {Array} keys a sequence of keys to fetch the object for + * @returns {Class} the first class found matching an object + * + * @throws {openerp.base.KeyNotFound} if none of the keys was in the mapping + * @trows {openerp.base.ObjectNotFound} if a found object path was invalid + */ + get_any: function (keys) { + for (var i=0; i<keys.length; ++i) { + try { + return this.get_object(keys[i]); + } catch (e) { + if (e instanceof openerp.base.KeyNotFound) { + continue; + } + throw e; + } + } + throw new openerp.base.KeyNotFound(keys.join(',')); + }, + /** + * Adds a new key and value to the registry. + * + * This method can be chained. + * + * @param {String} key + * @param {String} object_path fully qualified dotted object path + * @returns {openerp.base.Registry} itself + */ + add: function (key, object_path) { + this.map[key] = object_path; + return this; + }, + /** + * Creates and returns a copy of the current mapping, with the provided + * mapping argument added in (replacing existing keys if needed) + * + * @param {Object} [mapping={}] a mapping of keys to object-paths + */ + clone: function (mapping) { + return new openerp.base.Registry( + _.extend({}, this.map, mapping || {})); + } +}); + +/** + * Utility class that any class is allowed to extend to easy common manipulations. + * + * It provides rpc calls, callback on all methods preceded by "on_" or "do_" and a + * logging facility. + */ +openerp.base.SessionAware = openerp.base.Class.extend({ + init: function(session) { + this.session = session; + + // Transform on_* method into openerp.base.callbacks + for (var name in this) { + if(typeof(this[name]) == "function") { + this[name].debug_name = name; + // bind ALL function to this not only on_and _do ? + if((/^on_|^do_/).test(name)) { + this[name] = openerp.base.callback(this, this[name]); + } + } + } + }, + /** + * Performs a JSON-RPC call + * + * @param {String} url endpoint url + * @param {Object} data RPC parameters + * @param {Function} success RPC call success callback + * @param {Function} error RPC call error callback + * @returns {jQuery.Deferred} deferred object for the RPC call + */ + rpc: function(url, data, success, error) { + return this.session.rpc(url, data, success, error); + }, + log: function() { + var args = Array.prototype.slice.call(arguments); + var caller = arguments.callee.caller; + // TODO add support for line number using + // https://github.com/emwendelin/javascript-stacktrace/blob/master/stacktrace.js + // args.unshift("" + caller.debug_name); + this.on_log.apply(this,args); + }, + on_log: function() { + if(window.openerp.debug || (window.location.search.indexOf('?debug') !== -1)) { + var notify = false; + var body = false; + if(window.console) { + console.log(arguments); + } else { + body = true; + } + var a = Array.prototype.slice.call(arguments, 0); + for(var i = 0; i < a.length; i++) { + var v = a[i]==null ? "null" : a[i].toString(); + if(i==0) { + notify = v.match(/^not/); + body = v.match(/^bod/); + } + if(body) { + $('<pre></pre>').text(v).appendTo($('body')); + } + if(notify && this.notification) { + this.notification.notify("Logging:",v); + } + } + } + } +}); + +/** + * Base class for all visual components. Provides a lot of functionalities helpful + * for the management of a part of the DOM. + * + * Widget handles: + * - Rendering with QWeb. + * - Life-cycle management and parenting (when a parent is destroyed, all its children are + * destroyed too). + * - Insertion in DOM. + * + * Widget also extends SessionAware for ease of use. + * + * Guide to create implementations of the Widget class: + * ============================================== + * + * Here is a sample child class: + * + * MyWidget = openerp.base.Widget.extend({ + * // the name of the QWeb template to use for rendering + * template: "MyQWebTemplate", + * // identifier prefix, it is useful to put an obvious one for debugging + * identifier_prefix: 'my-id-prefix-', + * + * init: function(parent) { + * this._super(parent); + * // stuff that you want to init before the rendering + * }, + * start: function() { + * this._super(); + * // 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 * /); + * + * // if you have some asynchronous operations, it's a good idea to return + * // a promise in start() + * var promise = this.rpc(...); + * return promise; + * } + * }); + * + * Now this class can simply be used with the following syntax: + * + * var my_widget = new MyWidget(this); + * my_widget.appendTo($(".some-div")); + * + * With these two lines, the MyWidget instance was inited, rendered, it was inserted into the + * DOM inside the ".some-div" div and its events were binded. + * + * And of course, when you don't need that widget anymore, just do: + * + * my_widget.stop(); + * + * That will kill the widget in a clean way and erase its content from the dom. + */ +openerp.base.Widget = openerp.base.SessionAware.extend({ + /** + * 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. + * + * @type string + */ + template: null, + /** + * The prefix used to generate an id automatically. Should be redefined in + * subclasses. If it is not defined, a generic identifier will be used. + * + * @type string + */ + identifier_prefix: 'generic-identifier-', + /** + * Construct the widget and set its parent if a parent is given. + * + * @constructs + * @param {openerp.base.Widget} parent Binds the current instance to the given Widget instance. + * When that widget is destroyed by calling stop(), 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, /** @deprecated */ element_id) { + this._super((parent || {}).session); + // if given an element_id, try to get the associated DOM element and save + // a reference in this.$element. Else just generate a unique identifier. + this.element_id = element_id; + this.element_id = this.element_id || _.uniqueId(this.identifier_prefix); + var tmp = document.getElementById(this.element_id); + this.$element = tmp ? $(tmp) : undefined; + + this.widget_parent = parent; + this.widget_children = []; + if(parent && parent.widget_children) { + parent.widget_children.push(this); + } + // useful to know if the widget was destroyed and should not be used anymore + this.widget_is_stopped = false; + }, + /** + * Render the current widget and appends it to the given jQuery object or Widget. + * + * @param target A jQuery object or a Widget instance. + */ + appendTo: function(target) { + var self = this; + return this._render_and_insert(function(t) { + self.$element.appendTo(t); + }, target); + }, + /** + * Render the current widget and prepends it to the given jQuery object or Widget. + * + * @param target A jQuery object or a Widget instance. + */ + prependTo: function(target) { + var self = this; + return this._render_and_insert(function(t) { + self.$element.prependTo(t); + }, target); + }, + /** + * Render the current widget and inserts it after to the given jQuery object or Widget. + * + * @param target A jQuery object or a Widget instance. + */ + insertAfter: function(target) { + var self = this; + return this._render_and_insert(function(t) { + self.$element.insertAfter(t); + }, target); + }, + /** + * Render the current widget and inserts it before to the given jQuery object or Widget. + * + * @param target A jQuery object or a Widget instance. + */ + insertBefore: function(target) { + var self = this; + return this._render_and_insert(function(t) { + self.$element.insertBefore(t); + }, target); + }, + _render_and_insert: function(insertion, target) { + var rendered = this.render(); + this.$element = $(rendered); + if (target instanceof openerp.base.Widget) + target = target.$element; + insertion(target); + return this.start(); + }, + /** + * Renders the widget using QWeb, `this.template` must be defined. + * The context given to QWeb contains the "widget" key that references `this`. + * + * @param {Object} additional Additional context arguments to pass to the template. + */ + render: function (additional) { + return QWeb.render(this.template, _.extend({widget: this}, additional || {})); + }, + /** + * Method called after rendering. Mostly used to bind actions, perform asynchronous + * calls, etc... + * + * By convention, the method should return a promise to inform the caller when + * this widget has been initialized. + * + * @returns {jQuery.Deferred} + */ + start: function() { + if (!this.$element) { + var tmp = document.getElementById(this.element_id); + this.$element = tmp ? $(tmp) : undefined; + } + return $.Deferred().done().promise(); + }, + /** + * Destroys the current widget, also destory all its children before destroying itself. + */ + stop: function() { + _.each(_.clone(this.widget_children), function(el) { + el.stop(); + }); + if(this.$element != null) { + this.$element.remove(); + } + if (this.widget_parent && this.widget_parent.widget_children) { + this.widget_parent.widget_children = _.without(this.widget_parent.widget_children, this); + } + this.widget_parent = null; + this.widget_is_stopped = true; + }, + /** + * Inform the action manager to do an action. Of course, this suppose 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.widget_parent) { + return this.widget_parent.do_action(action, on_finished); + } + return false; + }, + rpc: function(url, data, success, error) { + var def = $.Deferred().then(success, error); + var self = this; + this._super(url, data). then(function() { + if (!self.widget_is_stopped) + def.resolve.apply(def, arguments); + }, function() { + if (!self.widget_is_stopped) + def.reject.apply(def, arguments); + }); + return def.promise(); + } +}); + +/** + * @deprecated + * For retro compatibility only, the only difference with is that render() uses + * directly `this` instead of context with a "widget" key. + */ +openerp.base.OldWidget = openerp.base.Widget.extend({ + render: function (additional) { + return QWeb.render(this.template, _.extend(_.extend({}, this), additional || {})); + } +}); + +}; +// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: diff --git a/addons/base/static/src/js/data.js b/addons/base/static/src/js/data.js index 080f4409fa1..3b2c88acee1 100644 --- a/addons/base/static/src/js/data.js +++ b/addons/base/static/src/js/data.js @@ -18,7 +18,7 @@ openerp.base.serialize_sort = function (criterion) { }).join(', '); }; -openerp.base.DataGroup = openerp.base.Controller.extend( /** @lends openerp.base.DataGroup# */{ +openerp.base.DataGroup = openerp.base.Widget.extend( /** @lends openerp.base.DataGroup# */{ /** * Management interface between views and grouped collections of OpenERP * records. @@ -30,7 +30,7 @@ openerp.base.DataGroup = openerp.base.Controller.extend( /** @lends openerp.bas * content of the current grouping level. * * @constructs - * @extends openerp.base.Controller + * @extends openerp.base.Widget * * @param {openerp.base.Session} session Current OpenERP session * @param {String} model name of the model managed by this DataGroup @@ -232,13 +232,13 @@ openerp.base.StaticDataGroup = openerp.base.GrouplessDataGroup.extend( /** @lend } }); -openerp.base.DataSet = openerp.base.Controller.extend( /** @lends openerp.base.DataSet# */{ +openerp.base.DataSet = openerp.base.Widget.extend( /** @lends openerp.base.DataSet# */{ /** * DateaManagement interface between views and the collection of selected * OpenERP records (represents the view's state?) * * @constructs - * @extends openerp.base.Controller + * @extends openerp.base.Widget * * @param {String} model the OpenERP model this dataset will manage */ @@ -291,14 +291,18 @@ openerp.base.DataSet = openerp.base.Controller.extend( /** @lends openerp.base. * Read the indexed record. */ read_index: function (fields, callback) { + var def = $.Deferred().then(callback); if (_.isEmpty(this.ids)) { - return $.Deferred().reject().promise(); + def.reject(); } else { fields = fields || false; - return this.read_ids([this.ids[this.index]], fields, function(records) { - callback(records[0]); + return this.read_ids([this.ids[this.index]], fields).then(function(records) { + def.resolve(records[0]); + }, function() { + def.reject.apply(def, arguments); }); } + return def.promise(); }, default_get: function(fields, callback) { return this.rpc('/base/dataset/default_get', { @@ -500,24 +504,33 @@ openerp.base.BufferedDataSet = openerp.base.DataSetStatic.extend({ this.to_create.push(cached); this.cache.push(cached); var to_return = $.Deferred().then(callback); - setTimeout(function() {to_return.resolve({result: cached.id});}, 0); + to_return.resolve({result: cached.id}); return to_return.promise(); }, write: function (id, data, callback) { 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;}); + var dirty = false; if (record) { + for (k in data) { + if (record.values[k] === undefined || record.values[k] !== data[k]) { + dirty = true; + break; + } + } $.extend(record.values, data); } else { + dirty = true; record = {id: id, values: data}; self.to_write.push(record); } var cached = _.detect(this.cache, function(x) {return x.id === id;}); $.extend(cached.values, record.values); - this.on_change(); + if (dirty) + this.on_change(); var to_return = $.Deferred().then(callback); - setTimeout(function () {to_return.resolve({result: true});}, 0); + to_return.resolve({result: true}); return to_return.promise(); }, unlink: function(ids, callback, error_callback) { diff --git a/addons/base/static/src/js/data_export.js b/addons/base/static/src/js/data_export.js new file mode 100644 index 00000000000..ad440302c69 --- /dev/null +++ b/addons/base/static/src/js/data_export.js @@ -0,0 +1,402 @@ +openerp.base.data_export = function(openerp) { +openerp.base.DataExport = openerp.base.Dialog.extend({ + init: function(parent, dataset) { + this._super(parent); + this.dataset = dataset; + }, + start: function() { + var self = this; + self._super(false); + self.template = 'ExportTreeView'; + self.dialog_title = "Export Data"; + self.open({ + modal: true, + width: '55%', + height: 'auto', + position: 'top', + buttons : { + "Close" : function() { + self.close(); + }, + "Export To File" : function() { + self.on_click_export_data(); + } + }, + close: function(event, ui){ self.close();} + }); + self.on_show_exists_export_list(); + self.$element.removeClass('ui-dialog-content ui-widget-content'); + self.$element.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'); + 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.$element.find('#remove_field').click(function() { + self.$element.find('#fields_list option:selected').remove(); + }); + self.$element.find('#remove_all_field').click(function() { + self.$element.find('#fields_list option').remove(); + }); + self.$element.find('#export_new_list').click(function() { + self.on_show_save_list(); + }); + var import_comp = self.$element.find('#import_compat option:selected').val(), + params = { + import_compat: parseInt(import_comp) + }; + self.rpc('/base/export/get_fields', { model: self.dataset.model, params: params }, self.on_show_data); + + self.$element.find('#import_compat').change(function() { + self.$element.find('#fields_list option').remove(); + self.$element.find('#field-tree-structure').remove(); + var import_comp = self.$element.find("#import_compat option:selected").val(); + if (import_comp) { + var params = { + import_compat: parseInt(import_comp) + } + self.rpc("/base/export/get_fields", { model: self.dataset.model, params: params}, self.on_show_data); + } + }); + }, + on_show_exists_export_list: function() { + var self = this; + if (self.$element.find('#saved_export_list').is(':hidden')) { + self.$element.find('#ExistsExportList').show(); + } else { + this.rpc('/base/export/exist_export_lists', { 'model': this.dataset.model}, function(export_list) { + if (export_list.length) { + 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(); + if (export_id) { + self.rpc('/base/export/namelist', {'model': self.dataset.model, export_id: parseInt(export_id)}, self.do_load_export_field); + } + }); + self.$element.find('#delete_export_list').click(function() { + var select_exp = self.$element.find('#saved_export_list option:selected'); + if (select_exp.val()) { + self.rpc('/base/export/delete_export', { export_id: parseInt(select_exp.val())}, {}); + select_exp.remove(); + if (self.$element.find('#saved_export_list option').length <= 1) { + self.$element.find('#ExistsExportList').hide(); + } + } + }); + } + }); + } + }, + do_load_export_field: function(field_list) { + var export_node = this.$element.find("#fields_list"); + for (var key in field_list) { + export_node.append(new Option(field_list[key], key)); + } + }, + on_show_save_list: function() { + var self = this; + var current_node = self.$element.find("#savenewlist"); + if (!(current_node.find("label")).length) { + current_node.append(QWeb.render('ExportNewList')); + current_node.find("#add_export_list").click(function() { + var value = current_node.find("#savelist_name").val(); + if (value) { + self.do_save_export_list(value); + } else { + alert("Pleae Enter Save Field List Name"); + } + }); + } else { + if (current_node.is(':hidden')) { + current_node.show(); + current_node.find("#savelist_name").val(""); + } else { + current_node.hide(); + } + } + }, + do_save_export_list: function(value) { + var self = this; + var export_field = self.get_fields(); + if (export_field.length) { + self.rpc("/base/export/save_export_lists", {"model": self.dataset.model, "name":value, "field_list":export_field}, function(exp_id) { + if (exp_id) { + if (self.$element.find("#saved_export_list").length > 0) { + self.$element.find("#saved_export_list").append(new Option(value, exp_id)); + } else { + self.on_show_exists_export_list(); + } + if (self.$element.find("#saved_export_list").is(":hidden")) { + self.on_show_exists_export_list(); + } + } + }); + self.on_show_save_list(); + self.$element.find("#fields_list option").remove(); + } + }, + on_click: function(id, result) { + var self = this; + self.field_id = id.split("-")[1]; + var is_loaded = 0; + _.each(result, function(record) { + if (record['id'] == self.field_id && (record['children']).length >= 1) { + var model = record['params']['model'], + prefix = record['params']['prefix'], + name = record['params']['name']; + $(record['children']).each(function(e, childid) { + if (self.$element.find("tr[id='treerow-" + childid + "']").length > 0) { + if (self.$element.find("tr[id='treerow-" + childid + "']").is(':hidden')) { + is_loaded = -1; + } else { + is_loaded++; + } + } + }); + if (is_loaded == 0) { + if (self.$element.find("tr[id='treerow-" + self.field_id +"']").find('img').attr('src') === '/base/static/src/img/expand.gif') { + if (model) { + var import_comp = self.$element.find("#import_compat option:selected").val(); + var params = { + import_compat: parseInt(import_comp), + parent_field_type : record['field_type'] + } + self.rpc("/base/export/get_fields", { + model: model, + prefix: prefix, + name: name, + field_parent : self.field_id, + params: params + }, function(results) { + self.on_show_data(results); + }); + } + } + } else if (is_loaded > 0) { + self.showcontent(self.field_id, true); + } else { + self.showcontent(self.field_id, false); + } + } + }); + }, + on_show_data: function(result) { + var self = this; + var imp_cmpt = parseInt(self.$element.find("#import_compat option:selected").val()); + var current_tr = self.$element.find("tr[id='treerow-" + self.field_id + "']"); + if (current_tr.length >= 1) { + current_tr.find('img').attr('src','/base/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})); + } + _.each(result, function(record) { + if ((record.field_type == "one2many") && imp_cmpt) { + var o2m_fld = self.$element.find("tr[id='treerow-" + record.id + "']").find('#tree-column'); + o2m_fld.addClass("oe_export_readonlyfield"); + } + if ((record.required == true) || record.required == "True") { + var required_fld = self.$element.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.on_click(this.id, result); + }); + + self.$element.find("tr[id='treerow-" + record.id + "']").click(function(e) { + if (e.shiftKey == true) { + 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).addClass("ui-selected"); + } else { + if (this.rowIndex >=self.row_index) { + for (i = (self.row_index-1); i < this.rowIndex; i++) { + scnd_click = self.$element.find("tr[id^='treerow-']")[i]; + if (!$(scnd_click).find('#tree-column').hasClass("oe_export_readonlyfield")) { + $(scnd_click).addClass("ui-selected"); + } + } + } else { + for (i = (self.row_index-1); i >= (this.rowIndex-1); i--) { + scnd_click = self.$element.find("tr[id^='treerow-']")[i]; + if (!$(scnd_click).find('#tree-column').hasClass("oe_export_readonlyfield")) { + $(scnd_click).addClass("ui-selected"); + } + } + } + } + } + self.row_index = this.rowIndex; + + self.$element.find("tr[id='treerow-" + record.id + "']").keyup(function(e) { + self.row_index = 0; + }); + var o2m_selection = self.$element.find("tr[id='treerow-" + record.id + "']").find('#tree-column'); + if ($(o2m_selection).hasClass("oe_export_readonlyfield")) { + return false; + } + var selected = self.$element.find("tr.ui-selected"); + if ($(this).hasClass("ui-selected") && (e.ctrlKey == true)) { + $(this).find('a').blur(); + $(this).removeClass("ui-selected"); + } else if ($(this).hasClass("ui-selected") && (e.ctrlKey == false) && (e.shiftKey == false)) { + selected.find('a').blur(); + selected.removeClass("ui-selected"); + $(this).find('a').focus(); + $(this).addClass("ui-selected"); + } else if (!$(this).hasClass("ui-selected") && (e.ctrlKey == false) && (e.shiftKey == false)) { + selected.find('a').blur(); + selected.removeClass("ui-selected"); + $(this).find('a').focus(); + $(this).addClass("ui-selected"); + } else if (!$(this).hasClass("ui-selected") && (e.ctrlKey == true)) { + $(this).find('a').focus(); + $(this).addClass("ui-selected"); + } + return false; + }); + + self.$element.find("tr[id='treerow-" + record.id + "']").keydown(function(e) { + var keyCode = e.keyCode || e.which; + arrow = {left: 37, up: 38, right: 39, down: 40 }; + switch (keyCode) { + case arrow.left: + if ($(this).find('img').attr('src') === '/base/static/src/img/collapse.gif') { + self.on_click(this.id, result); + } + break; + case arrow.up: + var elem = this; + $(elem).removeClass("ui-selected"); + while ($(elem).prev().is(":visible") == false) { + elem = $(elem).prev(); + } + if (!$(elem).prev().find('#tree-column').hasClass("oe_export_readonlyfield")) { + $(elem).prev().addClass("ui-selected"); + } + $(elem).prev().find('a').focus(); + break; + case arrow.right: + if ($(this).find('img').attr('src') == '/base/static/src/img/expand.gif') { + self.on_click(this.id, result); + } + break; + case arrow.down: + var elem = this; + $(elem).removeClass("ui-selected"); + while($(elem).next().is(":visible") == false) { + elem = $(elem).next(); + } + if (!$(elem).next().find('#tree-column').hasClass("oe_export_readonlyfield")) { + $(elem).next().addClass("ui-selected"); + } + $(elem).next().find('a').focus(); + break; + } + }); + self.$element.find("tr[id='treerow-" + record.id + "']").dblclick(function(e) { + var $o2m_selection = self.$element.find("tr[id^='treerow-" + record.id + "']").find('#tree-column'); + if (!$o2m_selection.hasClass("oe_export_readonlyfield")) { + var field_id = $(this).find("a").attr("id"); + if (field_id) { + self.add_field(field_id.split('-')[1], $(this).find("a").attr("string")); + } + } + }); + }); + self.$element.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")) { + self.add_field(field_id.split("-")[1], event.relatedTarget.attributes["string"]["value"]); + } + } + } + } + }); + }, + showcontent: function(id, flag) { + // show & hide the contents + var first_child = this.$element.find("tr[id='treerow-" + id + "']").find('img'); + if (flag) { + first_child.attr('src', '/base/static/src/img/expand.gif'); + } + else { + first_child.attr('src', '/base/static/src/img/collapse.gif'); + } + var child_field = this.$element.find("tr[id^='treerow-" + id +"/']"); + var child_len = (id.split("/")).length + 1; + for (var i = 0; i < child_field.length; i++) { + if (flag) { + $(child_field[i]).hide(); + } else { + if (child_len == (child_field[i].id.split("/")).length) { + if ($(child_field[i]).find('img').attr('src') == '/base/static/src/img/collapse.gif') { + $(child_field[i]).find('img').attr('src', '/base/static/src/img/expand.gif'); + } + $(child_field[i]).show(); + } + } + } + }, + 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) { + field_list.append(new Option(string, field_id)); + } + }, + get_fields: function() { + var export_field = []; + this.$element.find("#fields_list option").each(function() { + export_field.push($(this).val()); + }); + if (!export_field.length) { + alert('Please select fields to save export list...'); + } + return export_field; + }, + on_click_export_data: function() { + var self = this; + var export_field = {}; + var flag = true; + self.$element.find("#fields_list option").each(function() { + export_field[$(this).val()] = $(this).text(); + flag = false; + }); + if (flag) { + alert('Please select fields to export...'); + return; + } + + var import_comp = self.$element.find("#import_compat option:selected").val(), + export_format = self.$element.find("#export_format").val(); + + self.rpc("/base/export/export_data", { + model: self.dataset.model, + fields: export_field, + ids: self.dataset.ids, + domain: self.dataset.domain, + import_compat: parseInt(import_comp), + export_format: export_format + }, function(data) { + window.location = "data:text/csv/excel;charset=utf8," + data; + self.close(); + }); + }, + close: function() { + $(this.$dialog).remove(); + this._super(); + } +}); + +}; diff --git a/addons/base/static/src/js/form.js b/addons/base/static/src/js/form.js index 7b86ce9f372..aea78502bc1 100644 --- a/addons/base/static/src/js/form.js +++ b/addons/base/static/src/js/form.js @@ -1,7 +1,7 @@ openerp.base.form = function (openerp) { openerp.base.views.add('form', 'openerp.base.FormView'); -openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormView# */{ +openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormView# */{ /** * Indicates that this view is not searchable, and thus that no search * view should be displayed (if there is one active). @@ -19,7 +19,7 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV */ init: function(parent, element_id, dataset, view_id, options) { this._super(parent, element_id); - this.view_manager = parent || new openerp.base.NullViewManager(); + this.set_default_options(); this.dataset = dataset; this.model = dataset.model; this.view_id = view_id; @@ -30,8 +30,7 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV this.datarecord = {}; this.ready = false; this.show_invalid = true; - this.touched = false; - this.flags = this.view_manager.flags || {}; + this.dirty = false; this.default_focus_field = null; this.default_focus_button = null; this.registry = openerp.base.form.widgets; @@ -49,14 +48,15 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV return def.promise(); } else { var context = new openerp.base.CompoundContext(this.dataset.get_context()); - if (this.view_manager.action && this.view_manager.action.context) { - context.add(this.view_manager.action.context); - } return this.rpc("/base/formview/load", {"model": this.model, "view_id": this.view_id, - toolbar:!!this.flags.sidebar, context: context}, this.on_loaded); + toolbar: this.options.sidebar, context: context}, this.on_loaded); } }, stop: function() { + if (this.sidebar) { + this.sidebar.attachments.stop(); + this.sidebar.stop(); + } _.each(this.widgets, function(w) { w.stop(); }); @@ -85,12 +85,17 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV $('<xmp>' + openerp.base.json_node_to_xml(self.fields_view.arch, true) + '</xmp>').dialog({ width: '95%', height: 600}); }); - if(this.view_manager.sidebar) - this.view_manager.sidebar.set_toolbar(data.fields_view.toolbar); + if (this.options.sidebar && this.options.sidebar_id) { + this.sidebar = new openerp.base.Sidebar(this, this.options.sidebar_id); + this.sidebar.start(); + this.sidebar.do_unfold(); + this.sidebar.attachments = new openerp.base.form.SidebarAttachments(this.sidebar, this.sidebar.add_section('attachments', "Attachments"), this); + this.sidebar.add_toolbar(data.fields_view.toolbar); + this.set_common_sidebar_sections(this.sidebar); + } this.has_been_loaded.resolve(); }, do_show: function () { - var self = this; var promise; if (this.dataset.index === null) { // null index means we should start a new record @@ -98,13 +103,17 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV } else { promise = this.dataset.read_index(_.keys(this.fields_view.fields), this.on_record_loaded); } - self.$element.show(); - if(this.view_manager.sidebar) - this.view_manager.sidebar.do_refresh(true); + this.$element.show(); + if (this.sidebar) { + this.sidebar.$element.show(); + } return promise; }, do_hide: function () { this.$element.hide(); + if (this.sidebar) { + this.sidebar.$element.hide(); + } }, on_record_loaded: function(record) { if (!record) { @@ -121,22 +130,22 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV this.$form_header.find('.oe_form_on_update').show(); this.$form_header.find('button.oe_form_button_new').show(); } - this.touched = false; + this.dirty = false; this.datarecord = record; for (var f in this.fields) { var field = this.fields[f]; - field.touched = false; + field.dirty = false; field.set_value(this.datarecord[f] || false); field.validate(); } if (!record.id) { // New record: Second pass in order to trigger the onchanges - this.touched = true; + this.dirty = true; this.show_invalid = false; for (var f in record) { var field = this.fields[f]; if (field) { - field.touched = true; + field.dirty = true; this.do_onchange(field); } } @@ -144,7 +153,9 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV this.on_form_changed(); this.show_invalid = this.ready = true; this.do_update_pager(record.id == null); - this.do_update_sidebar(); + if (this.sidebar) { + this.sidebar.attachments.do_update(); + } if (this.default_focus_field) { this.default_focus_field.focus(); } @@ -253,7 +264,7 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV processed.push(field.name); if (field.get_value() != value) { field.set_value(value); - field.touched = true; + field.dirty = true; if (_.indexOf(processed, field.name) < 0) { this.do_onchange(field, processed); } @@ -301,37 +312,40 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV if (!this.ready) { return false; } - var invalid = false, + var form_dirty = false, + form_invalid = false, values = {}, first_invalid_field = null; for (var f in this.fields) { f = this.fields[f]; - if (f.invalid) { - invalid = true; + if (!f.is_valid()) { + form_invalid = true; f.update_dom(); if (!first_invalid_field) { first_invalid_field = f; } - } else if (f.touched) { + } else if (f.is_dirty()) { + form_dirty = true; values[f.name] = f.get_value(); } } - if (invalid) { + if (form_invalid) { first_invalid_field.focus(); this.on_invalid(); return false; - } else { + } else if (form_dirty) { this.log("About to save", values); if (!this.datarecord.id) { - this.dataset.create(values, function(r) { + return this.dataset.create(values, function(r) { self.on_created(r, success, prepend_on_create); }); } else { - this.dataset.write(this.datarecord.id, values, function(r) { + return this.dataset.write(this.datarecord.id, values, function(r) { self.on_saved(r, success); }); } - return true; + } else { + return false; } }, do_save_edit: function() { @@ -345,7 +359,7 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV on_invalid: function() { var msg = "<ul>"; _.each(this.fields, function(f) { - if (f.invalid) { + if (!f.is_valid()) { msg += "<li>" + f.string + "</li>"; } }); @@ -389,7 +403,9 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV this.dataset.index = 0; } this.do_update_pager(); - this.do_update_sidebar(); + if (this.sidebar) { + this.sidebar.attachments.do_update(); + } this.notification.notify("Record created", "The record has been created with id #" + this.datarecord.id); if (success) { success(_.extend(r, {created: true})); @@ -406,34 +422,55 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV do_cancel: function () { this.notification.notify("Cancelling form"); }, - do_update_sidebar: function() { - if (this.flags.sidebar === false || this.view_manager.sidebar === undefined) { - return; + reload: function() { + if (this.datarecord.id) { + this.dataset.read_index(_.keys(this.fields_view.fields), this.on_record_loaded); + } else { + this.on_button_new(); } - if (!this.datarecord.id) { + }, + get_fields_values: function() { + var values = {}; + _.each(this.fields, function(value, key) { + var val = value.get_value(); + values[key] = val; + }); + return values; + } +}); + +/** @namespace */ +openerp.base.form = {}; + +openerp.base.form.SidebarAttachments = openerp.base.Widget.extend({ + init: function(parent, element_id, form_view) { + this._super(parent, element_id); + this.view = form_view; + }, + do_update: function() { + if (!this.view.datarecord.id) { this.on_attachments_loaded([]); } else { (new openerp.base.DataSetSearch( - this, 'ir.attachment', this.dataset.get_context(), - [['res_model', '=', this.dataset.model], - ['res_id', '=', this.datarecord.id], + this, 'ir.attachment', this.view.dataset.get_context(), + [['res_model', '=', this.view.dataset.model], + ['res_id', '=', this.view.datarecord.id], ['type', 'in', ['binary', 'url']]])).read_slice( ['name', 'url', 'type'], false, false, this.on_attachments_loaded); } }, on_attachments_loaded: function(attachments) { - this.$sidebar = this.view_manager.sidebar.$element.find('.sidebar-attachments'); this.attachments = attachments; - this.$sidebar.html(QWeb.render('FormView.sidebar.attachments', this)); - this.$sidebar.find('.oe-sidebar-attachment-delete').click(this.on_attachment_delete); - this.$sidebar.find('.oe-binary-file').change(this.on_attachment_changed); + this.$element.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); }, on_attachment_changed: function(e) { - window[this.element_id + '_iframe'] = this.do_update_sidebar; + window[this.element_id + '_iframe'] = this.do_update; var $e = $(e.target); if ($e.val() != '') { - this.$sidebar.find('form.oe-binary-form').submit(); + this.$element.find('form.oe-binary-form').submit(); $e.parent().find('input[type=file]').attr('disabled', 'true'); $e.parent().find('button').attr('disabled', 'true').find('img, span').toggle(); } @@ -450,27 +487,9 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV self.notification.notify("Delete an attachment", "The attachment '" + name + "' has been deleted"); }); } - }, - reload: function() { - if (this.datarecord.id) { - this.dataset.read_index(_.keys(this.fields_view.fields), this.on_record_loaded); - } else { - this.on_button_new(); - } - }, - get_fields_values: function() { - var values = {}; - _.each(this.fields, function(value, key) { - var val = value.get_value(); - values[key] = val; - }); - return values; } }); -/** @namespace */ -openerp.base.form = {}; - openerp.base.form.compute_domain = function(expr, fields) { var stack = []; for (var i = expr.length - 1; i >= 0; i--) { @@ -530,7 +549,7 @@ openerp.base.form.compute_domain = function(expr, fields) { return _.all(stack, _.identity); }; -openerp.base.form.Widget = openerp.base.Controller.extend({ +openerp.base.form.Widget = openerp.base.Widget.extend({ template: 'Widget', init: function(view, node) { this.view = view; @@ -696,6 +715,10 @@ openerp.base.form.WidgetButton = openerp.base.form.Widget.extend({ init: function(view, node) { this._super(view, node); this.template = "WidgetButton"; + if (this.string) { + // We don't have button key bindings in the webclient + this.string = this.string.replace(/_/g, ''); + } if (node.attrs.default_focus == '1') { // TODO fme: provide enter key binding to widgets this.view.default_focus_button = this; @@ -707,7 +730,7 @@ openerp.base.form.WidgetButton = openerp.base.form.Widget.extend({ }, on_click: function(saved) { var self = this; - if (!this.node.attrs.special && this.view.touched && saved !== true) { + if (!this.node.attrs.special && this.view.dirty && saved !== true) { this.view.do_save(function() { self.on_click(true); }); @@ -752,11 +775,12 @@ openerp.base.form.WidgetLabel = openerp.base.form.Widget.extend({ if (this.node.tag == 'label' && this.node.attrs.colspan) { this.is_field_label = false; this.template = "WidgetParagraph"; + this.colspan = this.node.attrs.colspan; } else { this.is_field_label = true; this.template = "WidgetLabel"; + this.colspan = 1; } - this.colspan = 1; }, render: function () { if (this['for'] && this.type !== 'label') { @@ -787,7 +811,7 @@ openerp.base.form.Field = openerp.base.form.Widget.extend({ this.readonly = this.modifiers['readonly'] === true; this.required = this.modifiers['required'] === true; this.invalid = false; - this.touched = false; + this.dirty = false; }, set_value: function(value) { this.value = value; @@ -800,21 +824,29 @@ openerp.base.form.Field = openerp.base.form.Widget.extend({ get_value: function() { return this.value; }, + is_valid: function() { + return !this.invalid; + }, + is_dirty: function() { + return this.dirty; + }, get_on_change_value: function() { return this.get_value(); }, update_dom: function() { this._super.apply(this, arguments); - this.$element.toggleClass('disabled', this.readonly); - this.$element.toggleClass('required', this.required); - if (this.view.show_invalid) { - this.$element.toggleClass('invalid', this.invalid); + if (!this.disable_utility_classes) { + this.$element.toggleClass('disabled', this.readonly); + this.$element.toggleClass('required', this.required); + if (this.view.show_invalid) { + this.$element.toggleClass('invalid', !this.is_valid()); + } } }, on_ui_change: function() { - this.touched = this.view.touched = true; + this.dirty = this.view.dirty = true; this.validate(); - if (!this.invalid) { + if (this.is_valid()) { this.set_value_from_ui(); this.view.do_onchange(this); this.view.on_form_changed(); @@ -912,7 +944,7 @@ openerp.base.form.FieldEmail = openerp.base.form.FieldChar.extend({ this.$element.find('button').click(this.on_button_clicked); }, on_button_clicked: function() { - if (!this.value || this.invalid) { + if (!this.value || !this.is_valid()) { this.notification.warn("E-mail error", "Can't send email to invalid e-mail address"); } else { location.href = 'mailto:' + this.value; @@ -953,7 +985,7 @@ openerp.base.form.FieldFloat = openerp.base.form.FieldChar.extend({ if (value === false || value === undefined) { // As in GTK client, floats default to 0 value = 0; - this.touched = true; + this.dirty = true; } var show_value = value.toFixed(2); this.$element.find('input').val(show_value); @@ -973,7 +1005,7 @@ openerp.base.form.FieldInteger = openerp.base.form.FieldFloat.extend({ if (value === false || value === undefined) { // TODO fme: check if GTK client default integers to 0 (like it does with floats) value = 0; - this.touched = true; + this.dirty = true; } var show_value = parseInt(value, 10); this.$element.find('input').val(show_value); @@ -1060,7 +1092,7 @@ openerp.base.form.FieldFloatTime = openerp.base.form.FieldChar.extend({ if (value === false || value === undefined) { // As in GTK client, floats default to 0 value = 0; - this.touched = true; + this.dirty = true; } var show_value = _.sprintf("%02d:%02d", Math.floor(value), Math.round((value % 1) * 60)); this.$element.find('input').val(show_value); @@ -1267,7 +1299,6 @@ openerp.base.form.dialog = function(content, options) { options.autoOpen = true; var dialog = new openerp.base.Dialog(null, options); dialog.$dialog = $(content).dialog(dialog.options); - console.log(dialog.options) return dialog.$dialog; } @@ -1450,7 +1481,7 @@ openerp.base.form.FieldMany2One = openerp.base.form.Field.extend({ }, self.build_domain(), new openerp.base.CompoundContext(self.build_context(), context || {})); pop.on_select_elements.add(function(element_ids) { - var dataset = new openerp.base.DataSetStatic(this, this.field.relation, self.build_context()); + var dataset = new openerp.base.DataSetStatic(self, self.field.relation, self.build_context()); dataset.name_get([element_ids[0]], function(data) { self._change_int_ext_value(data[0]); pop.stop(); @@ -1567,6 +1598,7 @@ openerp.base.form.FieldOne2Many = openerp.base.form.Field.extend({ this.template = "FieldOne2Many"; this.is_started = $.Deferred(); this.form_last_update = $.Deferred(); + this.disable_utility_classes = true; }, start: function() { this._super.apply(this, arguments); @@ -1612,9 +1644,16 @@ openerp.base.form.FieldOne2Many = openerp.base.form.Field.extend({ controller.on_record_loaded.add_last(function() { once.resolve(); }); + controller.on_pager_action.add_first(function() { + self.save_form_view(); + }); + controller.$element.find(".oe_form_button_save_edit").hide(); } self.is_started.resolve(); }); + this.viewmanager.on_mode_switch.add_first(function() { + self.save_form_view(); + }); setTimeout(function () { self.viewmanager.start(); }, 0); @@ -1684,6 +1723,9 @@ openerp.base.form.FieldOne2Many = openerp.base.form.Field.extend({ this._super(value); this.dataset.reset_ids(value); } + if (this.dataset.index === null && this.dataset.ids.length > 0) { + this.dataset.index = 0; + } $.when(this.is_started).then(function() { self.reload_current_view(); }); @@ -1708,9 +1750,50 @@ openerp.base.form.FieldOne2Many = openerp.base.form.Field.extend({ this.dataset.to_delete, function(x) { return commands['delete'](x.id);})); }, + save_form_view: 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") { + var res = view.do_save(); + if (res === false) { + // ignore + } else if (res.isRejected()) { + throw "Save or create on one2many dataset is not supposed to fail."; + } else if (!res.isResolved()) { + throw "Asynchronous get_value() is not supported in form view."; + } + return res; + } + } + return false; + }, + is_valid: function() { + this.validate(); + return this._super(); + }, validate: function() { this.invalid = false; - // TODO niv + var self = this; + var view = self.viewmanager.views[self.viewmanager.active_view].controller; + if (self.viewmanager.active_view === "form") { + for (var f in view.fields) { + f = view.fields[f]; + if (!f.is_valid()) { + this.invalid = true; + return; + } + } + } + }, + is_dirty: function() { + this.save_form_view(); + return this._super(); + }, + update_dom: function() { + this._super.apply(this, arguments); + this.$element.toggleClass('disabled', this.readonly); } }); @@ -1777,7 +1860,7 @@ openerp.base.form.FieldMany2Many = openerp.base.form.Field.extend({ var self = this; - this.dataset = new openerp.base.form.Many2ManyDataSet( this, this.field.relation); + this.dataset = new openerp.base.form.Many2ManyDataSet(this, this.field.relation); this.dataset.m2m = this; this.dataset.on_unlink.add_last(function(ids) { self.on_ui_change(); @@ -1852,7 +1935,7 @@ openerp.base.form.Many2ManyListView = openerp.base.ListView.extend({ } }); -openerp.base.form.SelectCreatePopup = openerp.base.BaseWidget.extend({ +openerp.base.form.SelectCreatePopup = openerp.base.OldWidget.extend({ identifier_prefix: "selectcreatepopup", template: "SelectCreatePopup", /** @@ -1992,7 +2075,7 @@ openerp.base.form.SelectCreateListView = openerp.base.ListView.extend({ } }); -openerp.base.form.FormOpenPopup = openerp.base.BaseWidget.extend({ +openerp.base.form.FormOpenPopup = openerp.base.OldWidget.extend({ identifier_prefix: "formopenpopup", template: "FormOpenPopup", /** diff --git a/addons/base/static/src/js/list-editable.js b/addons/base/static/src/js/list-editable.js index 9a48889d801..c3386136967 100644 --- a/addons/base/static/src/js/list-editable.js +++ b/addons/base/static/src/js/list-editable.js @@ -147,7 +147,7 @@ openerp.base.list.editable = function (openerp) { this.cancel_pending_edition().then(function () { var $new_row = $('<tr>', { id: _.uniqueId('oe-editable-row-'), - 'class': $(row).attr('class'), + 'class': $(row).attr('class') + ' oe_forms', click: function (e) {e.stopPropagation();} }) .delegate('button.oe-edit-row-save', 'click', function () { @@ -181,8 +181,7 @@ openerp.base.list.editable = function (openerp) { self.edition = true; self.edition_index = self.dataset.index; self.edition_form = _.extend(new openerp.base.FormView( - null, self.group.view.session, $new_row.attr('id'), - self.dataset, false), { + self, $new_row.attr('id'), self.dataset, false), { template: 'ListView.row.form', registry: openerp.base.list.form.widgets }); @@ -277,12 +276,13 @@ openerp.base.list.editable = function (openerp) { openerp.base.list.form[key] = (form_widgets.get_object(key)).extend({ update_dom: function () { this.$element.children().css('visibility', ''); - if (this.invisible) { - this.$element.children().css('visibility', 'hidden'); - } else { + if (this.modifiers.tree_invisible) { + var old_invisible = this.invisible; this.invisible = !!this.modifiers.tree_invisible; this._super(); - this.invisible = false; + this.invisible = old_invisible; + } else if (this.invisible) { + this.$element.children().css('visibility', 'hidden'); } } }); diff --git a/addons/base/static/src/js/list.js b/addons/base/static/src/js/list.js index a2fca3aa89e..04336e163c3 100644 --- a/addons/base/static/src/js/list.js +++ b/addons/base/static/src/js/list.js @@ -102,6 +102,7 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi */ init: function(parent, element_id, dataset, view_id, options) { this._super(parent, element_id); + this.set_default_options(); this.view_manager = parent || new openerp.base.NullViewManager(); this.dataset = dataset; this.model = dataset.model; @@ -110,7 +111,6 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi this.columns = []; this.options = _.extend({}, this.defaults, options || {}); - this.flags = this.view_manager.flags || {}; this.set_groups(new openerp.base.ListView.Groups(this)); @@ -267,9 +267,12 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi }) .val(self._limit || 'NaN'); }); - if(this.view_manager.sidebar) - this.view_manager.sidebar.set_toolbar(data.fields_view.toolbar); - + if (this.options.sidebar && this.options.sidebar_id) { + this.sidebar = new openerp.base.Sidebar(this, this.options.sidebar_id); + this.sidebar.start(); + this.sidebar.add_toolbar(data.fields_view.toolbar); + this.set_common_sidebar_sections(this.sidebar); + } }, /** * Configures the ListView pager based on the provided dataset's information @@ -393,16 +396,20 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi }, do_show: function () { this.$element.show(); + if (this.sidebar) { + this.sidebar.$element.show(); + } if (this.hidden) { this.$element.find('.oe-listview-content').append( this.groups.apoptosis().render()); this.hidden = false; } - if(this.view_manager.sidebar) - this.view_manager.sidebar.do_refresh(true); }, do_hide: function () { this.$element.hide(); + if (this.sidebar) { + this.sidebar.$element.hide(); + } this.hidden = true; }, /** @@ -422,7 +429,7 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi model: this.model, view_id: this.view_id, context: this.dataset.get_context(), - toolbar: !!this.flags.sidebar + toolbar: this.options.sidebar }, callback); } }, @@ -674,6 +681,7 @@ openerp.base.ListView.List = openerp.base.Class.extend( /** @lends openerp.base. var self = this; this.group = group; this.view = group.view; + this.session = this.view.session; this.options = opts.options; this.columns = opts.columns; @@ -1149,12 +1157,14 @@ openerp.base.ListView.Groups = openerp.base.Class.extend( /** @lends openerp.bas // if drag to 1st row (to = 0), start sequencing from 0 // (exclusive lower bound) seq = to ? list.rows[to - 1].data.sequence.value : 0; - while (++seq, data = list.rows[index++].data) { + while (++seq, list.rows[index]) { + data = list.rows[index].data; data.sequence.value = seq; // write are independent from one another, so we can just // launch them all at the same time and we don't really // give a fig about when they're done dataset.write(data.id.value, {sequence: seq}); + list.reload_record(index++); } } }); diff --git a/addons/base/static/src/js/search.js b/addons/base/static/src/js/search.js index 8a51abe1335..0a1f2396123 100644 --- a/addons/base/static/src/js/search.js +++ b/addons/base/static/src/js/search.js @@ -1,6 +1,6 @@ openerp.base.search = function(openerp) { -openerp.base.SearchView = openerp.base.Controller.extend({ +openerp.base.SearchView = openerp.base.Widget.extend({ init: function(parent, element_id, dataset, view_id, defaults) { this._super(parent, element_id); this.view_manager = parent || new openerp.base.NullViewManager(); @@ -140,7 +140,7 @@ openerp.base.SearchView = openerp.base.Controller.extend({ var widget_starts = _(lines).chain().flatten().map(function (widget) { return widget.start(); }).value(); - + $.when.apply(null, widget_starts).then(function () { self.ready.resolve(); }); @@ -203,10 +203,10 @@ openerp.base.SearchView = openerp.base.Controller.extend({ context_to_save: context, domain: domain, name: name - }).then(function(result) { + }).then(function() { self.reload_managed_filters(); }); - }, + } } }); } else { // manage_filters @@ -386,13 +386,13 @@ openerp.base.search.Invalid = openerp.base.Class.extend( /** @lends openerp.base ': [' + this.value + '] is ' + this.message); } }); -openerp.base.search.Widget = openerp.base.Controller.extend( /** @lends openerp.base.search.Widget# */{ +openerp.base.search.Widget = openerp.base.Widget.extend( /** @lends openerp.base.search.Widget# */{ template: null, /** * Root class of all search widgets * * @constructs - * @extends openerp.base.Controller + * @extends openerp.base.Widget * * @param view the ancestor view of this widget */ @@ -475,11 +475,11 @@ openerp.base.search.Group = openerp.base.search.Widget.extend({ }, start: function () { this._super(); - _(this.lines) - .chain() - .flatten() - .each(function (widget) { widget.start(); }); openerp.base.search.add_expand_listener(this.$element); + var widget_starts = _(this.lines).chain().flatten() + .map(function (widget) { return widget.start(); }) + .value(); + return $.when.apply(null, widget_starts); } }); @@ -779,7 +779,7 @@ openerp.base.search.OneToManyField = openerp.base.search.CharField.extend({ }); openerp.base.search.ManyToOneField = openerp.base.search.CharField.extend({ // TODO: @widget - // TODO: .relation, .selection, .context, .domain + // TODO: .selection, .context, .domain init: function (view_section, field, view) { this._super(view_section, field, view); var self = this; @@ -851,7 +851,7 @@ openerp.base.search.ManyToManyField = openerp.base.search.CharField.extend({ // TODO: .related_columns (Array), .context, .domain }); -openerp.base.search.ExtendedSearch = openerp.base.BaseWidget.extend({ +openerp.base.search.ExtendedSearch = openerp.base.OldWidget.extend({ template: 'SearchView.extended_search', identifier_prefix: 'extended-search', init: function (parent, model) { @@ -860,9 +860,7 @@ openerp.base.search.ExtendedSearch = openerp.base.BaseWidget.extend({ }, add_group: function() { var group = new openerp.base.search.ExtendedSearchGroup(this, this.fields); - var render = group.render(); - this.$element.find('.searchview_extended_groups_list').append(render); - group.start(); + group.appendTo(this.$element.find('.searchview_extended_groups_list')); this.check_last_element(); }, start: function () { @@ -891,7 +889,7 @@ openerp.base.search.ExtendedSearch = openerp.base.BaseWidget.extend({ if(this.$element.closest("table.oe-searchview-render-line").css("display") == "none") { return null; } - return _.reduce(this.children, + return _.reduce(this.widget_children, function(mem, x) { return mem.concat(x.get_domain());}, []); }, on_activate: function() { @@ -910,14 +908,14 @@ openerp.base.search.ExtendedSearch = openerp.base.BaseWidget.extend({ } }, check_last_element: function() { - _.each(this.children, function(x) {x.set_last_group(false);}); - if (this.children.length >= 1) { - this.children[this.children.length - 1].set_last_group(true); + _.each(this.widget_children, function(x) {x.set_last_group(false);}); + if (this.widget_children.length >= 1) { + this.widget_children[this.widget_children.length - 1].set_last_group(true); } } }); -openerp.base.search.ExtendedSearchGroup = openerp.base.BaseWidget.extend({ +openerp.base.search.ExtendedSearchGroup = openerp.base.OldWidget.extend({ template: 'SearchView.extended_search.group', identifier_prefix: 'extended-search-group', init: function (parent, fields) { @@ -926,7 +924,7 @@ openerp.base.search.ExtendedSearchGroup = openerp.base.BaseWidget.extend({ }, add_prop: function() { var prop = new openerp.base.search.ExtendedSearchProposition(this, this.fields); - var render = prop.render({'index': this.children.length - 1}); + var render = prop.render({'index': this.widget_children.length - 1}); this.$element.find('.searchview_extended_propositions_list').append(render); prop.start(); }, @@ -943,7 +941,7 @@ openerp.base.search.ExtendedSearchGroup = openerp.base.BaseWidget.extend({ }); }, get_domain: function() { - var props = _(this.children).chain().map(function(x) { + var props = _(this.widget_children).chain().map(function(x) { return x.get_proposition(); }).compact().value(); var choice = this.$element.find(".searchview_extended_group_choice").val(); @@ -953,9 +951,9 @@ openerp.base.search.ExtendedSearchGroup = openerp.base.BaseWidget.extend({ props); }, stop: function() { - var parent = this.parent; - if (this.parent.children.length == 1) - this.parent.hide(); + var parent = this.widget_parent; + if (this.widget_parent.widget_children.length == 1) + this.widget_parent.hide(); this._super(); parent.check_last_element(); }, @@ -967,7 +965,7 @@ openerp.base.search.ExtendedSearchGroup = openerp.base.BaseWidget.extend({ } }); -openerp.base.search.ExtendedSearchProposition = openerp.base.BaseWidget.extend({ +openerp.base.search.ExtendedSearchProposition = openerp.base.OldWidget.extend({ template: 'SearchView.extended_search.proposition', identifier_prefix: 'extended-search-proposition', init: function (parent, fields) { @@ -992,9 +990,12 @@ openerp.base.search.ExtendedSearchProposition = openerp.base.BaseWidget.extend({ }); }, stop: function() { - if (this.parent.children.length == 1) - this.parent.stop(); + var parent; + if (this.widget_parent.widget_children.length == 1) + parent = this.widget_parent; this._super(); + if (parent) + parent.stop(); }, changed: function() { var nval = this.$element.find(".searchview_extended_prop_field").val(); @@ -1054,7 +1055,7 @@ openerp.base.search.ExtendedSearchProposition = openerp.base.BaseWidget.extend({ } }); -openerp.base.search.ExtendedSearchProposition.Char = openerp.base.BaseWidget.extend({ +openerp.base.search.ExtendedSearchProposition.Char = openerp.base.OldWidget.extend({ template: 'SearchView.extended_search.proposition.char', identifier_prefix: 'extended-search-proposition-char', operators: [ @@ -1071,7 +1072,7 @@ openerp.base.search.ExtendedSearchProposition.Char = openerp.base.BaseWidget.ext return this.$element.val(); } }); -openerp.base.search.ExtendedSearchProposition.DateTime = openerp.base.BaseWidget.extend({ +openerp.base.search.ExtendedSearchProposition.DateTime = openerp.base.OldWidget.extend({ template: 'SearchView.extended_search.proposition.datetime', identifier_prefix: 'extended-search-proposition-datetime', operators: [ @@ -1093,7 +1094,7 @@ openerp.base.search.ExtendedSearchProposition.DateTime = openerp.base.BaseWidget }); } }); -openerp.base.search.ExtendedSearchProposition.Date = openerp.base.BaseWidget.extend({ +openerp.base.search.ExtendedSearchProposition.Date = openerp.base.OldWidget.extend({ template: 'SearchView.extended_search.proposition.date', identifier_prefix: 'extended-search-proposition-date', operators: [ @@ -1115,7 +1116,7 @@ openerp.base.search.ExtendedSearchProposition.Date = openerp.base.BaseWidget.ext }); } }); -openerp.base.search.ExtendedSearchProposition.Integer = openerp.base.BaseWidget.extend({ +openerp.base.search.ExtendedSearchProposition.Integer = openerp.base.OldWidget.extend({ template: 'SearchView.extended_search.proposition.integer', identifier_prefix: 'extended-search-proposition-integer', operators: [ @@ -1134,7 +1135,7 @@ openerp.base.search.ExtendedSearchProposition.Integer = openerp.base.BaseWidget. return Math.round(value); } }); -openerp.base.search.ExtendedSearchProposition.Float = openerp.base.BaseWidget.extend({ +openerp.base.search.ExtendedSearchProposition.Float = openerp.base.OldWidget.extend({ template: 'SearchView.extended_search.proposition.float', identifier_prefix: 'extended-search-proposition-float', operators: [ @@ -1153,7 +1154,7 @@ openerp.base.search.ExtendedSearchProposition.Float = openerp.base.BaseWidget.ex return value; } }); -openerp.base.search.ExtendedSearchProposition.Selection = openerp.base.BaseWidget.extend({ +openerp.base.search.ExtendedSearchProposition.Selection = openerp.base.OldWidget.extend({ template: 'SearchView.extended_search.proposition.selection', identifier_prefix: 'extended-search-proposition-selection', operators: [ @@ -1167,7 +1168,7 @@ openerp.base.search.ExtendedSearchProposition.Selection = openerp.base.BaseWidge return this.$element.val(); } }); -openerp.base.search.ExtendedSearchProposition.Boolean = openerp.base.BaseWidget.extend({ +openerp.base.search.ExtendedSearchProposition.Boolean = openerp.base.OldWidget.extend({ template: 'SearchView.extended_search.proposition.boolean', identifier_prefix: 'extended-search-proposition-boolean', operators: [ diff --git a/addons/base/static/src/js/view_help.js b/addons/base/static/src/js/view_help.js index 704c982eb3a..aa98f59c3ab 100644 --- a/addons/base/static/src/js/view_help.js +++ b/addons/base/static/src/js/view_help.js @@ -4,10 +4,10 @@ openerp.base.view_help = function(openerp) { -openerp.base.ProcessView = openerp.base.Controller.extend({ +openerp.base.ProcessView = openerp.base.Widget.extend({ }); -openerp.base.HelpView = openerp.base.Controller.extend({ +openerp.base.HelpView = openerp.base.Widget.extend({ }); }; diff --git a/addons/base/static/src/js/view_tree.js b/addons/base/static/src/js/view_tree.js index ceb5478a4a7..90972f84973 100644 --- a/addons/base/static/src/js/view_tree.js +++ b/addons/base/static/src/js/view_tree.js @@ -5,7 +5,7 @@ openerp.base.view_tree = function(openerp) { openerp.base.views.add('tree', 'openerp.base.TreeView'); -openerp.base.TreeView = openerp.base.Controller.extend({ +openerp.base.TreeView = openerp.base.Widget.extend({ /** * Genuine tree view (the one displayed as a tree, not the list) */ diff --git a/addons/base/static/src/js/views.js b/addons/base/static/src/js/views.js index e10add1c8c0..b9c8a01afc3 100644 --- a/addons/base/static/src/js/views.js +++ b/addons/base/static/src/js/views.js @@ -5,7 +5,7 @@ openerp.base.views = function(openerp) { openerp.base.client_actions = new openerp.base.Registry(); -openerp.base.ActionManager = openerp.base.Controller.extend({ +openerp.base.ActionManager = openerp.base.Widget.extend({ // process all kind of actions init: function(parent, element_id) { this._super(parent, element_id); @@ -15,22 +15,6 @@ openerp.base.ActionManager = openerp.base.Controller.extend({ // Will use parent to find it when implementation will be done. this.session.action_manager = this; }, - /** - * Process an action - * Supported actions: act_window - */ - action_window: function() { - }, - action_window_close: function() { - }, - action_server: function() { - }, - action_url: function() { - }, - action_report: function() { - }, - action_client: function() { - }, do_action: function(action, on_closed) { action.flags = _.extend({ sidebar : action.target != 'new', @@ -108,18 +92,21 @@ openerp.base.ActionManager = openerp.base.Controller.extend({ openerp.base.ActionDialog = openerp.base.Dialog.extend({ identifier_prefix: 'action_dialog', - stop: function() { + on_close: function() { this._super(this, arguments); if (this.close_callback) { this.close_callback(); } + }, + stop: function() { + this._super(this, arguments); if (this.viewmanager) { this.viewmanager.stop(); } } }); -openerp.base.ViewManager = openerp.base.Controller.extend({ +openerp.base.ViewManager = openerp.base.Widget.extend({ init: function(parent, element_id, dataset, views) { this._super(parent, element_id); this.model = dataset.model; @@ -130,13 +117,13 @@ openerp.base.ViewManager = openerp.base.Controller.extend({ {return x instanceof Array? {view_id: x[0], view_type: x[1]} : x;}); this.views = {}; this.flags = this.flags || {}; - this.sidebar = new openerp.base.NullSidebar(); this.registry = openerp.base.views; }, /** * @returns {jQuery.Deferred} initial view loading promise */ start: function() { + this._super(); var self = this; this.dataset.start(); this.$element.html(QWeb.render("ViewManager", {"prefix": this.element_id, views: this.views_src})); @@ -144,7 +131,12 @@ openerp.base.ViewManager = openerp.base.Controller.extend({ self.on_mode_switch($(this).data('view-type')); }); _.each(this.views_src, function(view) { - self.views[view.view_type] = $.extend({}, view, {controller: null}); + self.views[view.view_type] = $.extend({}, view, { + controller : null, + options : _.extend({ + sidebar_id : self.element_id + '_sidebar_' + view.view_type + }, self.flags) + }); }); if (this.flags.views_switcher === false) { this.$element.find('.oe_vm_switch').hide(); @@ -168,7 +160,7 @@ openerp.base.ViewManager = openerp.base.Controller.extend({ if (!view.controller) { // Lazy loading of views var controllerclass = this.registry.get_object(view_type); - var controller = new controllerclass( this, this.element_id + "_view_" + view_type, + var controller = new controllerclass(this, this.element_id + '_view_' + view_type, this.dataset, view.view_id, view.options); if (view.embedded_view) { controller.set_embedded_view(view.embedded_view); @@ -222,7 +214,7 @@ openerp.base.ViewManager = openerp.base.Controller.extend({ }, /** * Event launched when a controller has been inited. - * + * * @param {String} view_type type of view * @param {String} view the inited controller */ @@ -285,7 +277,6 @@ openerp.base.NullViewManager = openerp.base.generate_null_object_class(openerp.b if(parent) this.session = parent.session; this.action = {flags: {}}; - this.sidebar = new openerp.base.NullSidebar(); } }); @@ -310,19 +301,10 @@ openerp.base.ViewManagerAction = openerp.base.ViewManager.extend({ // Not elegant but allows to avoid flickering of SearchView#do_hide this.flags.search_view = this.flags.pager = this.flags.sidebar = this.flags.action_buttons = false; } - if (this.flags.sidebar) { - this.sidebar = new openerp.base.Sidebar(null, this); - } }, start: function() { var inital_view_loaded = this._super(); - // init sidebar - if (this.flags.sidebar) { - this.$element.find('.view-manager-main-sidebar').html(this.sidebar.render()); - this.sidebar.start(); - } - var search_defaults = {}; _.each(this.action.context, function (value, key) { var match = /^search_default_(.*)$/.exec(key); @@ -346,8 +328,7 @@ openerp.base.ViewManagerAction = openerp.base.ViewManager.extend({ } }, stop: function() { - // should be replaced by automatic destruction implemented in BaseWidget - this.sidebar.stop(); + // should be replaced by automatic destruction implemented in Widget this._super(); }, /** @@ -374,82 +355,101 @@ openerp.base.ViewManagerAction = openerp.base.ViewManager.extend({ } }); -openerp.base.Sidebar = openerp.base.BaseWidget.extend({ - template: "ViewManager.sidebar", - init: function(parent, view_manager) { - this._super(parent, view_manager.session); - this.view_manager = view_manager; - this.sections = []; - }, - set_toolbar: function(toolbar) { - this.sections = []; - var self = this; - _.each([["print", "Reports"], ["action", "Actions"], ["relate", "Links"]], function(type) { - if (toolbar[type[0]].length == 0) - return; - var section = {elements:toolbar[type[0]], label:type[1]}; - self.sections.push(section); - }); - this.do_refresh(true); +openerp.base.Sidebar = openerp.base.Widget.extend({ + init: function(parent, element_id) { + this._super(parent, element_id); + this.items = {}; + this.sections = {}; }, - do_refresh: function(new_view) { - var view = this.view_manager.active_view; - var the_condition = this.sections.length > 0 && _.detect(this.sections, - function(x) {return x.elements.length > 0;}) != undefined - && (!new_view || view != 'list'); - - this.$element.toggleClass('open-sidebar', the_condition) - .toggleClass('closed-sidebar', !the_condition); - - this.$element.html(QWeb.render("ViewManager.sidebar.internal", { sidebar: this, view: view })); - + start: function() { var self = this; + this._super(this, arguments); + this.$element.html(QWeb.render('Sidebar')); this.$element.find(".toggle-sidebar").click(function(e) { - self.$element.toggleClass('open-sidebar closed-sidebar'); - e.stopPropagation(); - e.preventDefault(); + self.do_toggle(); }); - - this.$element.find("a.oe_sidebar_action_a").click(function(e) { - var $this = jQuery(this); - var index = $this.attr("data-index").split('-'); - var action = self.sections[index[0]].elements[index[1]]; - action.flags = { - new_window : true - }; - self.session.action_manager.do_action(action); - e.stopPropagation(); - e.preventDefault(); + }, + add_toolbar: function(toolbar) { + var self = this; + _.each([['print', "Reports"], ['action', "Actions"], ['relate', "Links"]], function(type) { + var items = toolbar[type[0]]; + if (items.length) { + for (var i = 0; i < items.length; i++) { + items[i] = { + label: items[i]['name'], + action: items[i], + classname: 'oe_sidebar_' + type[0] + } + } + self.add_section(type[0], type[1], items); + } }); }, - start: function() { - this._super(); - this.do_refresh(false); - } -}); - -openerp.base.NullSidebar = openerp.base.generate_null_object_class(openerp.base.Sidebar); - -openerp.base.Export = openerp.base.Dialog.extend({ - dialog_title: "Export", - template: 'ExportDialog', - identifier_prefix: 'export_dialog', - init: function (session, model, domain) { - this._super(); + add_section: function(code, name, items) { + // For each section, we pass a name/label and optionally an array of items. + // If no items are passed, then the section will be created as a custom section + // returning back an element_id to be used by a custom controller. + // Else, the section is a standard section with items displayed as links. + // An item is a dictonary : { + // label: label to be displayed for the link, + // action: action to be launch when the link is clicked, + // callback: a function to be executed when the link is clicked, + // classname: optional dom class name for the line, + // title: optional title for the link + // } + // Note: The item should have one action or/and a callback + var self = this, + section_id = _.uniqueId(this.element_id + '_section_' + code + '_'); + if (items) { + for (var i = 0; i < items.length; i++) { + items[i].element_id = _.uniqueId(section_id + '_item_'); + this.items[items[i].element_id] = items[i]; + } + } + var $section = $(QWeb.render("Sidebar.section", { + section_id: section_id, + name: name, + classname: 'oe_sidebar_' + code, + items: items + })); + if (items) { + $section.find('a.oe_sidebar_action_a').click(function() { + var item = self.items[$(this).attr('id')]; + if (item.callback) { + item.callback(); + } + if (item.action) { + item.action.flags = item.action.flags || {}; + item.action.flags.new_window = true; + self.do_action(item.action); + } + return false; + }); + } + $section.appendTo(this.$element.find('div.sidebar-actions')); + this.sections[code] = $section; + return section_id; }, - start: function () { - this._super(); - this.$element.html(this.render()); + do_fold: function() { + this.$element.addClass('closed-sidebar').removeClass('open-sidebar'); }, - on_button_Export: function() { - console.log("Export") + do_unfold: function() { + this.$element.addClass('open-sidebar').removeClass('closed-sidebar'); }, - on_button_Cancel: function() { - this.$element.dialog("close"); + do_toggle: function() { + this.$element.toggleClass('open-sidebar closed-sidebar'); } }); -openerp.base.View = openerp.base.Controller.extend({ +openerp.base.View = openerp.base.Widget.extend({ + set_default_options: function(options) { + this.options = options || {}; + _.defaults(this.options, { + // All possible views options should be defaulted here + sidebar_id: null, + sidebar: true + }); + }, /** * Fetches and executes the action identified by ``action_data``. * @@ -505,11 +505,69 @@ openerp.base.View = openerp.base.Controller.extend({ * Directly set a view to use instead of calling fields_view_get. This method must * be called before start(). When an embedded view is set, underlying implementations * of openerp.base.View must use the provided view instead of any other one. - * + * * @param embedded_view A view. */ set_embedded_view: function(embedded_view) { this.embedded_view = embedded_view; + }, + set_common_sidebar_sections: function(sidebar) { + sidebar.add_section('customize', "Customize", [ + { + label: "Manage Views", + callback: this.on_sidebar_manage_view, + title: "Manage views of the current object" + }, { + label: "Edit Workflow", + callback: this.on_sidebar_edit_workflow, + title: "Manage views of the current object", + classname: 'oe_hide oe_sidebar_edit_workflow' + }, { + label: "Customize Object", + callback: this.on_sidebar_customize_object, + title: "Manage views of the current object" + } + ]); + sidebar.add_section('other', "Other Options", [ + { + label: "Import", + callback: this.on_sidebar_import + }, { + label: "Export", + callback: this.on_sidebar_export + }, { + label: "Translate", + callback: this.on_sidebar_translate, + classname: 'oe_hide oe_sidebar_translate' + }, { + label: "View Log", + callback: this.on_sidebar_view_log, + classname: 'oe_hide oe_sidebar_view_log' + } + ]); + }, + on_sidebar_manage_view: function() { + if (this.fields_view && this.fields_view.arch) { + $('<xmp>' + openerp.base.json_node_to_xml(this.fields_view.arch, true) + '</xmp>').dialog({ width: '95%', height: 600}); + } else { + this.notification.warn("Manage Views", "Could not find current view declaration"); + } + }, + on_sidebar_edit_workflow: function() { + console.log('Todo'); + }, + on_sidebar_customize_object: function() { + console.log('Todo'); + }, + on_sidebar_import: function() { + }, + on_sidebar_export: function() { + var export_view = new openerp.base.DataExport(this, this.dataset); + export_view.start(false); + }, + on_sidebar_translate: function() { + }, + on_sidebar_view_log: function() { } }); diff --git a/addons/base/static/src/xml/base.xml b/addons/base/static/src/xml/base.xml index 06e2b1b8e5c..db0fdad44fa 100644 --- a/addons/base/static/src/xml/base.xml +++ b/addons/base/static/src/xml/base.xml @@ -19,23 +19,31 @@ </div> <table border="0" cellpadding="0" cellspacing="0" width="100%" height="100%" class="main_table"> <tr> - <td colspan="2"> + <td colspan="2" valign="top"> <div id="oe_header" class="header"></div> <div id="oe_menu" class="menu"></div> </td> </tr> <tr> - <td valign="top" id="oe_secondary_menu" class="secondary_menu"> + <td valign="top" class="login-container" colspan="2"> + <div id="oe_login" class="login"></div> </td> - <td valign="top" class="oe-main-content"> - <div id="oe_app" class="oe-application"> - <div style="width:100%;">&nbsp;</div> - </div> + </tr> + <tr class="db_options_row"> + <td valign="top" class="db_container"> + <div id="oe_database" class="database"></div> + </td> + <td valign="top"> + <div id="oe_db_options"></div> </td> </tr> <tr> - <td valign="top" class="login-container" colspan="2"> - <div id="oe_login" class="login"></div> + <td valign="top" id="oe_secondary_menu" class="secondary_menu"> + </td> + <td valign="top"> + <div id="oe_app" class="oe-application"> + <div style="width: 100%;">&nbsp;</div> + </div> </td> </tr> <tr> @@ -50,8 +58,190 @@ <t t-name="Loading"> Loading... </t> +<t t-name="Database"> + <ul class="db_options" style="padding: 0px; display: inline;"> + <li id="db-create">Create</li> + <li id="db-drop">Drop</li> + <li id="db-backup">Backup</li> + <li id="db-restore">Restore</li> + <li id="db-change-password">Password</li> + <li id="back-to-login">Back to Login</li> + </ul> +</t> +<t t-name="CreateDB"> + <form name="create_db_form" class="oe_forms" method="POST"> + <table width="100%"> + <tr> + <td class="option_string"> + CREATE DATABASE + </td> + </tr> + </table> + <table align="center" class="db_option_table"> + <tr> + <td><label for="super_admin_pwd">Super admin password:</label></td> + <td><input type="password" name="super_admin_pwd" + class="required" autofocus="autofocus"/></td> + </tr> + <tr> + <td><label for="db_name">New database name:</label></td> + <td><input type="text" name="db_name" class="required"/></td> + </tr> + <tr> + <td><label for="demo_data">Load Demonstration data:</label></td> + <td><input type="checkbox" name="demo_data" checked='checked'/></td> + </tr> + <tr> + <td><label for="db_lang">Default language:</label></td> + <td> + <select name="db_lang" t-if="lang_list"> + <t t-foreach="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> + </t> + </select> + </td> + </tr> + <tr> + <td><label for="create_admin_pwd">Super admin password:</label></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"><input type="submit" value="Create"/></td> + </tr> + </table> + </form> +</t> +<t t-name="DropDB"> + <form name="drop_db_form" class="oe_forms" method="POST"> + <table width="100%"> + <tr> + <td class="option_string"> + DROP DATABASE + </td> + </tr> + </table> + <table align="center" class="db_option_table"> + <tr> + <td><label for="drop_db">Database:</label></td> + <td> + <select t-if="db_list" name="drop_db" autofocus="autofocus"> + <t t-foreach="db_list" t-as="db"> + <option t-att-value="db"><t t-esc="db"/></option> + </t> + </select> + </td> + </tr> + <tr> + <td><label for="drop_password">Password:</label></td> + <td><input type="password" name="drop_pwd" class="required"/></td> + </tr> + <tr> + <td colspan="2" align="right"><input type="submit" value="Drop"/></td> + </tr> + </table> + </form> +</t> +<t t-name="BackupDB"> + <form name="backup_db_form" class="oe_forms" method="POST" target="backup-target" + action="/base/database/backup"> + <input type="hidden" name="token"/> + <table width="100%"> + <tr> + <td class="option_string"> + BACKUP DATABASE + </td> + </tr> + </table> + <table align="center" class="db_option_table"> + <tr> + <td><label for="backup_db">Database:</label></td> + <td> + <select t-if="db_list" name="backup_db" autofocus="autofocus"> + <t t-foreach="db_list" t-as="db"> + <option t-att-value="db"><t t-esc="db"/></option> + </t> + </select> + </td> + </tr> + <tr> + <td><label for="backup_pwd">Password:</label></td> + <td><input type="password" name="backup_pwd" class="required"/></td> + </tr> + <tr> + <td colspan="2" align="right"><input type="submit" value="Backup"/></td> + </tr> + </table> + </form> +</t> +<t t-name="RestoreDB"> + <form name="restore_db_form" class="oe_forms" method="POST"> + <table width="100%"> + <tr> + <td class="option_string"> + RESTORE DATABASE + </td> + </tr> + </table> + <table align="center" class="db_option_table"> + <tr> + <td><label for="restore_db">File:</label></td> + <td><input type="file" name="db_file" class="required" + autofocus="autofocus"/></td> + </tr> + <tr> + <td><label for="restore_pwd">Password:</label></td> + <td><input type="password" name="restore_pwd" class="required"/></td> + </tr> + <tr> + <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"><input type="submit" value="Restore"/></td> + </tr> + </table> + </form> +</t> +<t t-name="Change_DB_Pwd"> + <form name="change_pwd_form" class="oe_forms" method="POST"> + <table width="100%"> + <tr> + <td class="option_string"> + CHANGE DATABASE PASSWORD + </td> + </tr> + </table> + <table align="center" class="db_option_table"> + <tr> + <td><label for="old_pwd">Old password:</label></td> + <td><input type="password" name="old_pwd" class="required" + minlength="1" autofocus="autofocus"/></td> + </tr> + <tr> + <td><label for="new_pwd">New password:</label></td> + <td><input type="password" name="new_pwd" class="required" + minlength="1"/></td> + </tr> + <tr> + <td><label for="confirm_pwd">Confirm 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"><input type="submit" value="Change Password"/></td> + </tr> + </table> + </form> +</t> <t t-name="Login"> - <form> + <form class="oe_forms"> <fieldset> <legend style=""> <img src="/base/static/src/img/stock_person.png" alt="" /> @@ -92,6 +282,7 @@ <tr> <td></td> <td> + <button type="button" id="oe-db-config">Database</button> <button type="submit" name="submit">Login</button> </td> </tr> @@ -103,7 +294,7 @@ <div class="oe_login_right_pane"> <p>We think that daily job activities can be more intuitive, efficient, automated, .. and even fun.</p> <h3>OpenERP's vision to be:</h3> - + <table cellpadding="0" cellspacing="0" width="100%" style="border:none;"> <tbody> <tr> @@ -135,7 +326,7 @@ </tr> </tbody> </table> - + </div> </t> <t t-name="Header"> @@ -232,18 +423,40 @@ </t> </td> <td class="view-manager-main-sidebar" height="100%"> + <t t-foreach="views" t-as="view"> + <div t-attf-id="#{prefix}_sidebar_#{view.view_type}" class="sidebar-main-div closed-sidebar" style="display: none"/> + </t> </td> </tr> </table> </t> +<t t-name="Sidebar"> + <a class="toggle-sidebar"></a> + <div class="sidebar-content"> + <div class="sidebar-actions"> + </div> + </div> +</t> +<t t-name="Sidebar.section"> + <h2><t t-esc="name"/></h2> + <div t-att-id="section_id" t-att-class="classname"> + <ul t-if="items"> + <li t-foreach="items" t-as="item" t-att-class="item.classname"> + <a class="oe_sidebar_action_a" t-att-id="item.element_id" t-att-title="item.title" href="#"> + <t t-esc="item.label"/> + </a> + </li> + </ul> + </div> +</t> <table t-name="ListView" class="oe-listview-content"> <t t-set="columns_count" t-value="visible_columns.length + (options.selectable ? 1 : 0) + (options.deletable ? 1 : 0)"/> <thead class="ui-widget-header"> - <tr t-if="flags.action_buttons !== false or flags.pager !== false"> + <tr t-if="options.action_buttons !== false or options.pager !== false"> <th t-att-colspan="columns_count"> <table> <tr> - <td t-if="flags.action_buttons !== false" class="oe-actions"> + <td t-if="options.action_buttons !== false" class="oe-actions"> <button type="button" class="oe-list-add" t-if="options.addable"> <t t-esc="options.addable"/> @@ -253,7 +466,7 @@ Delete </button> </td> - <th t-if="flags.pager !== false" class="oe-list-pager"> + <th t-if="options.pager !== false" class="oe-list-pager"> <button type="button" disabled="disabled" data-pager-action="first">First</button> <button type="button" disabled="disabled" @@ -311,7 +524,7 @@ t-att-data-index="row_index"> <t t-foreach="columns" t-as="column"> <td t-if="column.meta"> - + </td> </t> <th t-if="options.selectable" class="oe-record-selector" width="1"> @@ -335,7 +548,7 @@ <t t-name="FormView"> <div class="oe_form_header" t-att-id="view.element_id + '_header'"> <h2 class="oe_view_title"><t t-esc="view.fields_view.arch.attrs.string"/> <button class="oe_get_xml_view">xml</button></h2> - <div class="oe_form_buttons" t-if="view.flags.action_buttons !== false"> + <div class="oe_form_buttons" t-if="view.options.action_buttons !== false"> <!--<button type="button" class="oe_form_button_save"> <span class="oe_form_on_update">Save</span> <span class="oe_form_on_create">Create</span> @@ -347,7 +560,7 @@ <!--<button type="button" class="oe_form_button_cancel">Cancel</button>--> <button type="button" class="oe_form_button_new">New</button> </div> - <div class="oe_form_pager" t-if="view.flags.pager !== false"> + <div class="oe_form_pager" t-if="view.options.pager !== false"> <button type="button" data-pager-action="first">First</button> <button type="button" data-pager-action="previous"><<</button> @@ -360,21 +573,20 @@ <t t-raw="frame.render()"/> </t> <t t-name="FormView.sidebar.attachments"> - <h2>Attachments</h2> <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="/base/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="dataset.model"/> - <input type="hidden" name="id" t-att-value="datarecord.id"/> + <input type="hidden" name="model" t-att-value="view.dataset.model"/> + <input type="hidden" name="id" t-att-value="view.datarecord.id"/> <button class="button" type="button"> <img src="/base/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="datarecord.id ? null : 'alert(\'No record selected ! You can only attach to existing record.\'); return false;'"/> + 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"> </iframe> </div> @@ -384,7 +596,7 @@ <li t-foreach="attachments" t-as="attachment"> <t t-if="attachment.type == 'binary'" t-set="attachment.url" t-value="'/base/binary/saveas?session_id=' + session.session_id + '&model=ir.attachment&id=' + attachment.id - + '&field=datas' + '&fieldname=name' + '&t=' + (new Date().getTime())"/> + + '&field=datas&fieldname=name&t=' + (new Date().getTime())"/> <a class="oe-sidebar-attachments-link" t-att-href="attachment.url" target="_blank"> <t t-esc="attachment.name"/> </a> @@ -399,7 +611,7 @@ <t t-raw="console.log('Unhandled widget', widget)"/> </t> <t t-name="WidgetFrame"> - <table border="0" width="100%" cellpadding="0" cellspacing="0" class="oe_frame"> + <table border="0" width="100%" cellpadding="0" cellspacing="0" class="oe_frame oe_forms"> <tr t-foreach="widget.table" t-as="row"> <t t-foreach="row" t-as="td"> <td t-att-colspan="td.colspan gt 1 ? td.colspan : undefined" @@ -657,7 +869,7 @@ </t> <t t-name="SearchView"> <h2 class="oe_view_title"><t t-esc="view.attrs['string']"/></h2> - <form> + <form class="oe_forms"> <t t-call="SearchView.render_lines"/> <div class="oe_search-view-buttons" style="text-align: right;"> <input type="submit" value="Search"/> @@ -800,7 +1012,7 @@ <option value="all">All the following conditions must match</option> <option value="none">None of the following conditions must match</option> </select> - <a class="searchview_extended_delete_group" + <a class="searchview_extended_delete_group" href="javascript:void(0)"><span></span></a> <div class="searchview_extended_propositions_list"> </div> @@ -849,29 +1061,6 @@ </t> </select> </t> -<t t-name="ViewManager.sidebar"> - <div t-att-id="element_id" class="sidebar-main-div closed-sidebar"> - - </div> -</t> -<t t-name="ViewManager.sidebar.internal"> - <a class="toggle-sidebar"></a> - <div class="sidebar-content"> - <div class="sidebar-attachments" t-if="view == 'form'"> </div> - <div class="sidebar-actions"> - <t t-foreach="sidebar.sections" t-as="section" t-if="section.elements.length"> - <h2><t t-esc="section.label"/></h2> - <ul> - <li t-foreach="section.elements" t-as="element"> - <a class="oe_sidebar_action_a" t-attf-data-index="#{section_index}-#{element_index}" href="#"> - <t t-esc="element.name"/> - </a> - </li> - </ul> - </t> - </div> - </div> -</t> <t t-name="DialogWarning"> <table cellspacing="0" cellpadding="0" border="0" class="oe-dialog-warning"> <tr> @@ -946,4 +1135,145 @@ .unwrap(); </t> </t> + +<t t-name="ExportView"> + <a id="exportview" href="javascript: void(0)" style="text-decoration: none;color: #3D3D3D;">Export</a> +</t> + +<t t-name="ExportTreeView"> + <table class="view" style="background-color: #F3F3F3;"> + <tr> + <td align="left"> + This wizard will export all data that matches the current search criteria to a CSV file. + You can export all data or only the fields that can be reimported after modification. + </td> + </tr> + <tr> + <td> + <table> + <tr> + <td class="label"><label>Export Type:</label></td> + <td> + <select id="import_compat" name="import_compat"> + <option value="1">Import Compatible Export</option> + <option value="0">Export all Data</option> + </select> + </td> + <td class="label"><label>Export Format</label></td> + <td> + <select id="export_format" name="export_format"> + <option value="csv">CSV</option> + <option value="xls">Excel</option> + </select> + </td> + + </tr> + </table> + </td> + </tr> + + <tr> + <td> + <table class="oe_export_fields_selector_export"> + <tr> + <th class="oe_view_title" valign="bottom">Available fields</th> + <th class="oe_view_title"></th> + <th class="oe_view_title">Fields to export + <a style="color: blue; text-decoration: none;" href="#" id="export_new_list">Save fields list</a> + <div id="savenewlist"></div> + <div id="ExistsExportList"></div> + </th> + </tr> + <tr> + <td class="oe_export_fields_selector_left"> + <div id="left_field_panel"> + </div> + </td> + <td> + <table class="oe_export_fields_selector_center"> + <tr> + <td align="center"> + <button id="add_field" class="oe_export_button_export">Add</button> + </td> + </tr> + <tr> + <td align="center"> + <button id="remove_field" class="oe_export_button_export">Remove</button> + </td> + </tr> + <tr> + <td align="center"> + <button id="remove_all_field" class="oe_export_button_export">Remove All</button> + </td> + </tr> + </table> + </td> + <td class="oe_export_fields_selector_right"> + <select name="fields_list" id="fields_list" multiple="multiple"></select> + </td> + </tr> + </table> + </td> + </tr> + </table> +</t> + +<t t-name="ExportTreeView-Secondary"> + <table id="field-tree-structure" class="oe_export_fields_selector_export" cellspacing="0" cellpadding="0"> + <tr><th class="oe_export_tree_header"> Name </th></tr> + <t t-call="ExportTreeView-Secondary.children"/> + </table> +</t> +<t t-name="ExportTreeView-Secondary.children"> + <t t-foreach="fields" t-as="field" > + <tr t-att-id="'treerow-' + field.id" class="oe_export_row"> + <td> + <table class="tree_grid" border="0"> + <tr class="oe_export_row"> + <t t-foreach="(field.id).split('/')" t-as="level" > + <t t-if="(field.id).split('/')[0] != level"> + <td width="18">&nbsp;</td> + </t> + </t> + <td valign="top" align="left" style="cursor: pointer;" width="18"> + <t t-if="(field.children).length >= 1"> + <t t-if="(field.id).split('/').length != 3"> + <img t-att-id="'parentimg-' + field.id" src="/base/static/src/img/expand.gif" width="16" height="16" border="0"/> + </t> + </t> + </td> + <td id="tree-column" valign="middle" align="left" style="cursor: pointer;"> + <a t-att-id="'export-' + field.id" t-att-string="field.string" href="javascript: void(0);" style="text-decoration: none;"> + <t t-esc="field.string"/> + </a> + </td> + </tr> + </table> + </td> + </tr> + </t> +</t> + +<t t-name="ExportNewList"> + <tr> + <th><label>Save as:</label></th> + <td><input size="10" type="text" id="savelist_name"/></td> + <td><button class="oe_export_button_export" id="add_export_list">Ok</button></td> + </tr> +</t> + +<t t-name="Exists.ExportList"> + <tr><th align="right"><label >Saved exports:</label></th></tr> + <tr align="left"> + <td> + <select id="saved_export_list" style="width: 100%;"> + <option></option> + <t t-foreach="existing_exports" t-as="export"> + <option t-att-value="export.id"><t t-esc="export.name"/></option> + </t> + </select> + </td> + <td><button class="oe_export_button_export" id="delete_export_list">Delete</button></td> + </tr> +</t> </templates> |
