[MERGE] from 7.0
bzr revid: xmo@openerp.com-20130313094909-u21ee88l2lak9p2x
This commit is contained in:
commit
94c2c42be6
|
@ -26,6 +26,7 @@ This module provides the core of the OpenERP Web Client.
|
|||
"static/lib/spinjs/spin.js",
|
||||
"static/lib/jquery.autosize/jquery.autosize.js",
|
||||
"static/lib/jquery.blockUI/jquery.blockUI.js",
|
||||
"static/lib/jquery.placeholder/jquery.placeholder.js",
|
||||
"static/lib/jquery.ui/js/jquery-ui-1.9.1.custom.js",
|
||||
"static/lib/jquery.ui.timepicker/js/jquery-ui-timepicker-addon.js",
|
||||
"static/lib/jquery.ui.notify/js/jquery.notify.js",
|
||||
|
|
|
@ -15,6 +15,8 @@ import simplejson
|
|||
import time
|
||||
import urllib
|
||||
import urllib2
|
||||
import urlparse
|
||||
import xmlrpclib
|
||||
import zlib
|
||||
from xml.etree import ElementTree
|
||||
from cStringIO import StringIO
|
||||
|
@ -91,16 +93,50 @@ def db_list(req):
|
|||
dbs = [i for i in dbs if re.match(r, i)]
|
||||
return dbs
|
||||
|
||||
def db_monodb(req):
|
||||
# if only one db exists, return it else return False
|
||||
def db_monodb_redirect(req):
|
||||
db = False
|
||||
redirect = False
|
||||
|
||||
# 1 try the db in the url
|
||||
db_url = req.params.get('db')
|
||||
if db_url:
|
||||
return (db_url, False)
|
||||
|
||||
try:
|
||||
dbs = db_list(req)
|
||||
if len(dbs) == 1:
|
||||
return dbs[0]
|
||||
except Exception:
|
||||
# ignore access denied
|
||||
pass
|
||||
return False
|
||||
dbs = []
|
||||
|
||||
# 2 use the database from the cookie if it's listable and still listed
|
||||
cookie_db = req.httprequest.cookies.get('last_used_database')
|
||||
if cookie_db in dbs:
|
||||
db = cookie_db
|
||||
|
||||
# 3 use the first db
|
||||
if dbs and not db:
|
||||
db = dbs[0]
|
||||
|
||||
# redirect to the chosen db if multiple are available
|
||||
if db and len(dbs) > 1:
|
||||
query = dict(urlparse.parse_qsl(req.httprequest.query_string, keep_blank_values=True))
|
||||
query.update({ 'db': db })
|
||||
redirect = req.httprequest.path + '?' + urllib.urlencode(query)
|
||||
return (db, redirect)
|
||||
|
||||
def db_monodb(req):
|
||||
# if only one db exists, return it else return False
|
||||
return db_monodb_redirect(req)[0]
|
||||
|
||||
def redirect_with_hash(req, url, code=303):
|
||||
if req.httprequest.user_agent.browser == 'msie':
|
||||
try:
|
||||
version = float(req.httprequest.user_agent.version)
|
||||
if version < 10:
|
||||
return "<html><head><script>window.location = '%s#' + location.hash;</script></head></html>" % url
|
||||
except Exception:
|
||||
pass
|
||||
return werkzeug.utils.redirect(url, code)
|
||||
|
||||
def module_topological_sort(modules):
|
||||
""" Return a list of module names sorted so that their dependencies of the
|
||||
|
@ -291,6 +327,10 @@ def manifest_glob(req, extension, addons=None, db=None):
|
|||
return r
|
||||
|
||||
def manifest_list(req, extension, mods=None, db=None):
|
||||
""" list ressources to load specifying either:
|
||||
mods: a comma separated string listing modules
|
||||
db: a database name (return all installed modules in that database)
|
||||
"""
|
||||
if not req.debug:
|
||||
path = '/web/webclient/' + extension
|
||||
if mods is not None:
|
||||
|
@ -299,12 +339,7 @@ def manifest_list(req, extension, mods=None, db=None):
|
|||
path += '?' + urllib.urlencode({'db': db})
|
||||
return [path]
|
||||
files = manifest_glob(req, extension, addons=mods, db=db)
|
||||
i_am_diabetic = req.httprequest.environ["QUERY_STRING"].count("no_sugar") >= 1 or \
|
||||
req.httprequest.environ.get('HTTP_REFERER', '').count("no_sugar") >= 1
|
||||
if i_am_diabetic:
|
||||
return [wp for _fp, wp in files]
|
||||
else:
|
||||
return ['%s?debug=%s' % (wp, os.path.getmtime(fp)) for fp, wp in files]
|
||||
return [wp for _fp, wp in files]
|
||||
|
||||
def get_last_modified(files):
|
||||
""" Returns the modification time of the most recently modified
|
||||
|
@ -534,6 +569,10 @@ class Home(openerpweb.Controller):
|
|||
|
||||
@openerpweb.httprequest
|
||||
def index(self, req, s_action=None, db=None, **kw):
|
||||
db, redir = db_monodb_redirect(req)
|
||||
if redir:
|
||||
return redirect_with_hash(req, redir)
|
||||
|
||||
js = "\n ".join('<script type="text/javascript" src="%s"></script>' % i for i in manifest_list(req, 'js', db=db))
|
||||
css = "\n ".join('<link rel="stylesheet" href="%s">' % i for i in manifest_list(req, 'css', db=db))
|
||||
|
||||
|
@ -869,7 +908,7 @@ class Session(openerpweb.Controller):
|
|||
"""
|
||||
saved_actions = req.httpsession.get('saved_actions')
|
||||
if not saved_actions:
|
||||
saved_actions = {"next":0, "actions":{}}
|
||||
saved_actions = {"next":1, "actions":{}}
|
||||
req.httpsession['saved_actions'] = saved_actions
|
||||
# we don't allow more than 10 stored actions
|
||||
if len(saved_actions["actions"]) >= 10:
|
||||
|
|
|
@ -107,6 +107,12 @@ formatted differently). If an input *may* fetch multiple completion
|
|||
items, it *should* prefix those with a section title using its own
|
||||
name. This has no technical consequence but is clearer for users.
|
||||
|
||||
.. note::
|
||||
|
||||
If a field is :js:func:`invisible
|
||||
<openerp.web.search.Input.visible>`, its completion function will
|
||||
*not* be called.
|
||||
|
||||
Providing drawer/supplementary UI
|
||||
+++++++++++++++++++++++++++++++++
|
||||
|
||||
|
@ -145,6 +151,11 @@ started only once (per view).
|
|||
dynamically collects, lays out and renders filters? =>
|
||||
exercises drawer thingies
|
||||
|
||||
.. note::
|
||||
|
||||
An :js:func:`invisible <openerp.web.search.Input.visible>` input
|
||||
will not be inserted into the drawer.
|
||||
|
||||
Converting from facet objects
|
||||
+++++++++++++++++++++++++++++
|
||||
|
||||
|
|
|
@ -92,6 +92,14 @@ class WebRequest(object):
|
|||
if not self.session:
|
||||
self.session = session.OpenERPSession()
|
||||
self.httpsession[self.session_id] = self.session
|
||||
|
||||
# set db/uid trackers - they're cleaned up at the WSGI
|
||||
# dispatching phase in openerp.service.wsgi_server.application
|
||||
if self.session._db:
|
||||
threading.current_thread().dbname = self.session._db
|
||||
if self.session._uid:
|
||||
threading.current_thread().uid = self.session._uid
|
||||
|
||||
self.context = self.params.pop('context', {})
|
||||
self.debug = self.params.pop('debug', False) is not False
|
||||
# Determine self.lang
|
||||
|
|
|
@ -11,8 +11,8 @@ Date.CultureInfo = {
|
|||
firstLetterDayNames: ["أ", "ا", "ث", "أ", "خ", "ج", "س"],
|
||||
|
||||
/* Month Name Strings */
|
||||
monthNames: ["كانون الثاني", "شباط", "آذار", "نيسان", "أيار", "حزيران", "تموز", "آب", "أيلول", "تشرين الأول", "تشرين الثاني", "كانون الأول"],
|
||||
abbreviatedMonthNames: ["كانون الثاني", "شباط", "آذار", "نيسان", "أيار", "حزيران", "تموز", "آب", "أيلول", "تشرين الأول", "تشرين الثاني", "كانون الأول"],
|
||||
monthNames: ["يناير", "فبراير", "مارس", "أبريل", "مايو", "يونيو", "يوليو", "أغسطس", "سبتمبر", "أكتوبر", "نوفمبر", "ديسمبر"],
|
||||
abbreviatedMonthNames: ["يناير", "فبراير", "مارس", "أبريل", "مايو", "يونيو", "يوليو", "أغسطس", "سبتمبر", "أكتوبر", "نوفمبر", "ديسمبر"],
|
||||
|
||||
/* AM/PM Designators */
|
||||
amDesignator: "ص",
|
||||
|
@ -82,18 +82,18 @@ Date.CultureInfo = {
|
|||
* As well, please review the list of "Future Strings" section below.
|
||||
*/
|
||||
regexPatterns: {
|
||||
jan: /^كانون الثاني/i,
|
||||
feb: /^شباط/i,
|
||||
mar: /^آذار/i,
|
||||
apr: /^نيسان/i,
|
||||
may: /^أيار/i,
|
||||
jun: /^حزيران/i,
|
||||
jul: /^تموز/i,
|
||||
aug: /^آب/i,
|
||||
sep: /^أيلول/i,
|
||||
oct: /^تشرين الأول/i,
|
||||
nov: /^تشرين الثاني/i,
|
||||
dec: /^كانون الأول/i,
|
||||
jan: /^يناير/i,
|
||||
feb: /^فبراير/i,
|
||||
mar: /^مارس/i,
|
||||
apr: /^أبريل/i,
|
||||
may: /^مايو/i,
|
||||
jun: /^يونيو/i,
|
||||
jul: /^يوليو/i,
|
||||
aug: /^أغسطس/i,
|
||||
sep: /^سبتمبر/i,
|
||||
oct: /^أكتوبر/i,
|
||||
nov: /^نوفمبر/i,
|
||||
dec: /^ديسمبر/i,
|
||||
|
||||
sun: /^الاحد/i,
|
||||
mon: /^ا(1)?/i,
|
||||
|
@ -192,4 +192,4 @@ Date.CultureInfo = {
|
|||
* end end
|
||||
* long long
|
||||
* short short
|
||||
*/
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,157 @@
|
|||
/*! http://mths.be/placeholder v2.0.7 by @mathias */
|
||||
;(function(window, document, $) {
|
||||
|
||||
var isInputSupported = 'placeholder' in document.createElement('input'),
|
||||
isTextareaSupported = 'placeholder' in document.createElement('textarea'),
|
||||
prototype = $.fn,
|
||||
valHooks = $.valHooks,
|
||||
hooks,
|
||||
placeholder;
|
||||
|
||||
if (isInputSupported && isTextareaSupported) {
|
||||
|
||||
placeholder = prototype.placeholder = function() {
|
||||
return this;
|
||||
};
|
||||
|
||||
placeholder.input = placeholder.textarea = true;
|
||||
|
||||
} else {
|
||||
|
||||
placeholder = prototype.placeholder = function() {
|
||||
var $this = this;
|
||||
$this
|
||||
.filter((isInputSupported ? 'textarea' : ':input') + '[placeholder]')
|
||||
.not('.placeholder')
|
||||
.bind({
|
||||
'focus.placeholder': clearPlaceholder,
|
||||
'blur.placeholder': setPlaceholder
|
||||
})
|
||||
.data('placeholder-enabled', true)
|
||||
.trigger('blur.placeholder');
|
||||
return $this;
|
||||
};
|
||||
|
||||
placeholder.input = isInputSupported;
|
||||
placeholder.textarea = isTextareaSupported;
|
||||
|
||||
hooks = {
|
||||
'get': function(element) {
|
||||
var $element = $(element);
|
||||
return $element.data('placeholder-enabled') && $element.hasClass('placeholder') ? '' : element.value;
|
||||
},
|
||||
'set': function(element, value) {
|
||||
var $element = $(element);
|
||||
if (!$element.data('placeholder-enabled')) {
|
||||
return element.value = value;
|
||||
}
|
||||
if (value == '') {
|
||||
element.value = value;
|
||||
// Issue #56: Setting the placeholder causes problems if the element continues to have focus.
|
||||
if (element != document.activeElement) {
|
||||
// We can't use `triggerHandler` here because of dummy text/password inputs :(
|
||||
setPlaceholder.call(element);
|
||||
}
|
||||
} else if ($element.hasClass('placeholder')) {
|
||||
clearPlaceholder.call(element, true, value) || (element.value = value);
|
||||
} else {
|
||||
element.value = value;
|
||||
}
|
||||
// `set` can not return `undefined`; see http://jsapi.info/jquery/1.7.1/val#L2363
|
||||
return $element;
|
||||
}
|
||||
};
|
||||
|
||||
isInputSupported || (valHooks.input = hooks);
|
||||
isTextareaSupported || (valHooks.textarea = hooks);
|
||||
|
||||
$(function() {
|
||||
// Look for forms
|
||||
$(document).delegate('form', 'submit.placeholder', function() {
|
||||
// Clear the placeholder values so they don't get submitted
|
||||
var $inputs = $('.placeholder', this).each(clearPlaceholder);
|
||||
setTimeout(function() {
|
||||
$inputs.each(setPlaceholder);
|
||||
}, 10);
|
||||
});
|
||||
});
|
||||
|
||||
// Clear placeholder values upon page reload
|
||||
$(window).bind('beforeunload.placeholder', function() {
|
||||
$('.placeholder').each(function() {
|
||||
this.value = '';
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function args(elem) {
|
||||
// Return an object of element attributes
|
||||
var newAttrs = {},
|
||||
rinlinejQuery = /^jQuery\d+$/;
|
||||
$.each(elem.attributes, function(i, attr) {
|
||||
if (attr.specified && !rinlinejQuery.test(attr.name)) {
|
||||
newAttrs[attr.name] = attr.value;
|
||||
}
|
||||
});
|
||||
return newAttrs;
|
||||
}
|
||||
|
||||
function clearPlaceholder(event, value) {
|
||||
var input = this,
|
||||
$input = $(input);
|
||||
if (input.value == $input.attr('placeholder') && $input.hasClass('placeholder')) {
|
||||
if ($input.data('placeholder-password')) {
|
||||
$input = $input.hide().next().show().attr('id', $input.removeAttr('id').data('placeholder-id'));
|
||||
// If `clearPlaceholder` was called from `$.valHooks.input.set`
|
||||
if (event === true) {
|
||||
return $input[0].value = value;
|
||||
}
|
||||
$input.focus();
|
||||
} else {
|
||||
input.value = '';
|
||||
$input.removeClass('placeholder');
|
||||
input == document.activeElement && input.select();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setPlaceholder() {
|
||||
var $replacement,
|
||||
input = this,
|
||||
$input = $(input),
|
||||
$origInput = $input,
|
||||
id = this.id;
|
||||
if (input.value == '') {
|
||||
if (input.type == 'password') {
|
||||
if (!$input.data('placeholder-textinput')) {
|
||||
try {
|
||||
$replacement = $input.clone().attr({ 'type': 'text' });
|
||||
} catch(e) {
|
||||
$replacement = $('<input>').attr($.extend(args(this), { 'type': 'text' }));
|
||||
}
|
||||
$replacement
|
||||
.removeAttr('name')
|
||||
.data({
|
||||
'placeholder-password': true,
|
||||
'placeholder-id': id
|
||||
})
|
||||
.bind('focus.placeholder', clearPlaceholder);
|
||||
$input
|
||||
.data({
|
||||
'placeholder-textinput': $replacement,
|
||||
'placeholder-id': id
|
||||
})
|
||||
.before($replacement);
|
||||
}
|
||||
$input = $input.removeAttr('id').hide().prev().attr('id', id).show();
|
||||
// Note: `$input[0] != input` now!
|
||||
}
|
||||
$input.addClass('placeholder');
|
||||
$input[0].value = $input.attr('placeholder');
|
||||
} else {
|
||||
$input.removeClass('placeholder');
|
||||
}
|
||||
}
|
||||
|
||||
}(this, document, jQuery));
|
|
@ -1436,7 +1436,7 @@ $sheet-padding: 16px
|
|||
|
||||
.oe_searchview_drawer
|
||||
position: absolute
|
||||
z-index: 100
|
||||
z-index: 2
|
||||
// detach drawer from field slightly
|
||||
margin-top: 4px
|
||||
top: 100%
|
||||
|
@ -1832,6 +1832,12 @@ $sheet-padding: 16px
|
|||
.oe_form
|
||||
.oe_form_field_text
|
||||
width: 100%
|
||||
.oe_form_text_content
|
||||
text-overflow: ellipsis
|
||||
display: inline-block
|
||||
white-space: pre-wrap
|
||||
overflow-x: hidden
|
||||
width: 100%
|
||||
.oe_form_field_char input,
|
||||
.oe_form_field_url input,
|
||||
.oe_form_field_email input,
|
||||
|
@ -2502,6 +2508,9 @@ div.ui-widget-overlay
|
|||
|
||||
// Internet Explorer 9+ specifics {{{
|
||||
.openerp_ie
|
||||
.placeholder
|
||||
color: $tag-border !important
|
||||
font-style: italic !important
|
||||
.oe_form_field_boolean input
|
||||
background: #fff
|
||||
.db_option_table .oe_form_field_selection
|
||||
|
@ -2517,6 +2526,8 @@ div.ui-widget-overlay
|
|||
button.oe_highlight
|
||||
padding-top: 0
|
||||
padding-bottom: 0
|
||||
.oe_view_manager_view_kanban
|
||||
display: table-cell
|
||||
.oe_view_manager_buttons
|
||||
button.oe_write_full
|
||||
padding-top: 0
|
||||
|
|
|
@ -530,7 +530,16 @@ instance.web.DatabaseManager = instance.web.Widget.extend({
|
|||
'login': 'admin',
|
||||
'password': form_obj['create_admin_pwd'],
|
||||
'login_successful': function() {
|
||||
self.do_action("reload");
|
||||
var action = {
|
||||
type: "ir.actions.client",
|
||||
tag: 'reload',
|
||||
params: {
|
||||
url_search : {
|
||||
db: form_obj['db_name'],
|
||||
},
|
||||
}
|
||||
};
|
||||
self.do_action(action);
|
||||
},
|
||||
},
|
||||
_push_me: false,
|
||||
|
@ -641,6 +650,11 @@ instance.web.client_actions.add("database_manager", "instance.web.DatabaseManage
|
|||
instance.web.Login = instance.web.Widget.extend({
|
||||
template: "Login",
|
||||
remember_credentials: true,
|
||||
events: {
|
||||
'change input[name=db],select[name=db]': function(ev) {
|
||||
this.set('database_selector', $(ev.currentTarget).val());
|
||||
},
|
||||
},
|
||||
|
||||
init: function(parent, action) {
|
||||
this._super(parent);
|
||||
|
@ -652,18 +666,18 @@ instance.web.Login = instance.web.Widget.extend({
|
|||
if (_.isEmpty(this.params)) {
|
||||
this.params = $.bbq.getState(true);
|
||||
}
|
||||
if (action && action.params && action.params.db) {
|
||||
this.params.db = action.params.db;
|
||||
} else if ($.deparam.querystring().db) {
|
||||
this.params.db = $.deparam.querystring().db;
|
||||
}
|
||||
if (this.params.db) {
|
||||
this.selected_db = this.params.db;
|
||||
}
|
||||
|
||||
if (this.params.login_successful) {
|
||||
this.on('login_successful', this, this.params.login_successful);
|
||||
}
|
||||
|
||||
if (this.has_local_storage && this.remember_credentials) {
|
||||
this.selected_db = localStorage.getItem('last_db_login_success');
|
||||
this.selected_login = localStorage.getItem('last_login_login_success');
|
||||
if (jQuery.deparam(jQuery.param.querystring()).debug !== undefined) {
|
||||
this.selected_password = localStorage.getItem('last_password_login_success');
|
||||
}
|
||||
}
|
||||
},
|
||||
start: function() {
|
||||
var self = this;
|
||||
|
@ -671,10 +685,10 @@ instance.web.Login = instance.web.Widget.extend({
|
|||
self.$el.find('.oe_login_manage_db').click(function() {
|
||||
self.do_action("database_manager");
|
||||
});
|
||||
self.on('change:database_selector', this, function() {
|
||||
this.database_selected(this.get('database_selector'));
|
||||
});
|
||||
var d = $.when();
|
||||
if ($.deparam.querystring().db) {
|
||||
self.params.db = $.deparam.querystring().db;
|
||||
}
|
||||
if ($.param.fragment().token) {
|
||||
self.params.token = $.param.fragment().token;
|
||||
}
|
||||
|
@ -682,16 +696,44 @@ instance.web.Login = instance.web.Widget.extend({
|
|||
if (self.params.db && self.params.login && self.params.password) {
|
||||
d = self.do_login(self.params.db, self.params.login, self.params.password);
|
||||
} else {
|
||||
if (self.params.db) {
|
||||
self.on_db_loaded([self.params.db])
|
||||
} else {
|
||||
d = self.rpc("/web/database/get_list", {}).done(self.on_db_loaded).fail(self.on_db_failed);
|
||||
}
|
||||
d = self.rpc("/web/database/get_list", {})
|
||||
.done(self.on_db_loaded)
|
||||
.fail(self.on_db_failed)
|
||||
.always(function() {
|
||||
if (self.selected_db && self.has_local_storage && self.remember_credentials) {
|
||||
self.$("[name=login]").val(localStorage.getItem(self.selected_db + '|last_login') || '');
|
||||
if (self.session.debug) {
|
||||
self.$("[name=password]").val(localStorage.getItem(self.selected_db + '|last_password') || '');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return d;
|
||||
},
|
||||
remember_last_used_database: function(db) {
|
||||
// This cookie will be used server side in order to avoid db reloading on first visit
|
||||
var ttl = 24 * 60 * 60 * 365;
|
||||
document.cookie = [
|
||||
'last_used_database=' + db,
|
||||
'path=/',
|
||||
'max-age=' + ttl,
|
||||
'expires=' + new Date(new Date().getTime() + ttl * 1000).toGMTString()
|
||||
].join(';');
|
||||
},
|
||||
database_selected: function(db) {
|
||||
var params = $.deparam.querystring();
|
||||
params.db = db;
|
||||
this.remember_last_used_database(db);
|
||||
this.$('.oe_login_dbpane').empty().text(_t('Loading...'));
|
||||
this.$('[name=login], [name=password]').prop('readonly', true);
|
||||
instance.web.redirect('/?' + $.param(params));
|
||||
},
|
||||
on_db_loaded: function (result) {
|
||||
var self = this;
|
||||
this.db_list = result;
|
||||
if (!this.selected_db) {
|
||||
this.selected_db = result[0];
|
||||
}
|
||||
this.$("[name=db]").replaceWith(QWeb.render('Login.dblist', { db_list: this.db_list, selected_db: this.selected_db}));
|
||||
if(this.db_list.length === 0) {
|
||||
this.do_action("database_manager");
|
||||
|
@ -732,17 +774,11 @@ instance.web.Login = instance.web.Widget.extend({
|
|||
self.hide_error();
|
||||
self.$(".oe_login_pane").fadeOut("slow");
|
||||
return this.session.session_authenticate(db, login, password).then(function() {
|
||||
if (self.has_local_storage) {
|
||||
if(self.remember_credentials) {
|
||||
localStorage.setItem('last_db_login_success', db);
|
||||
localStorage.setItem('last_login_login_success', login);
|
||||
if (jQuery.deparam(jQuery.param.querystring()).debug !== undefined) {
|
||||
localStorage.setItem('last_password_login_success', password);
|
||||
}
|
||||
} else {
|
||||
localStorage.setItem('last_db_login_success', '');
|
||||
localStorage.setItem('last_login_login_success', '');
|
||||
localStorage.setItem('last_password_login_success', '');
|
||||
self.remember_last_used_database(db);
|
||||
if (self.has_local_storage && self.remember_credentials) {
|
||||
localStorage.setItem(db + '|last_login', login);
|
||||
if (self.session.debug) {
|
||||
localStorage.setItem(db + '|last_password', password);
|
||||
}
|
||||
}
|
||||
self.trigger('login_successful');
|
||||
|
@ -800,6 +836,9 @@ instance.web.Reload = function(parent, action) {
|
|||
|
||||
var sobj = $.deparam(l.search.substr(1));
|
||||
sobj.ts = new Date().getTime();
|
||||
if (params.url_search) {
|
||||
sobj = _.extend(sobj, params.url_search);
|
||||
}
|
||||
var search = '?' + $.param(sobj);
|
||||
|
||||
var hash = l.hash;
|
||||
|
@ -1303,7 +1342,7 @@ instance.web.WebClient = instance.web.Client.extend({
|
|||
},
|
||||
logo_edit: function(ev) {
|
||||
var self = this;
|
||||
new self.alive(instance.web.Model("res.users").get_func("read")(this.session.uid, ["company_id"])).then(function(res) {
|
||||
self.alive(new instance.web.Model("res.users").get_func("read")(this.session.uid, ["company_id"])).then(function(res) {
|
||||
self.rpc("/web/action/load", { action_id: "base.action_res_company_form" }).done(function(result) {
|
||||
result.res_id = res['company_id'][0];
|
||||
result.target = "new";
|
||||
|
|
|
@ -232,13 +232,17 @@ instance.web.ParentedMixin = {
|
|||
Utility method to only execute asynchronous actions if the current
|
||||
object has not been destroyed.
|
||||
|
||||
@param {Promise} promise The promise representing the asynchronous action.
|
||||
@param {bool} reject Defaults to false. If true, the returned promise will be
|
||||
rejected with no arguments if the current object is destroyed. If false,
|
||||
the returned promise will never be resolved nor rejected.
|
||||
@returns {Promise} A promise that will mirror the given promise if everything goes
|
||||
fine but will either be rejected with no arguments or never resolved if the
|
||||
current object is destroyed.
|
||||
@param {$.Deferred} promise The promise representing the asynchronous
|
||||
action.
|
||||
@param {bool} [reject=false] If true, the returned promise will be
|
||||
rejected with no arguments if the current
|
||||
object is destroyed. If false, the
|
||||
returned promise will never be resolved
|
||||
or rejected.
|
||||
@returns {$.Deferred} A promise that will mirror the given promise if
|
||||
everything goes fine but will either be rejected
|
||||
with no arguments or never resolved if the
|
||||
current object is destroyed.
|
||||
*/
|
||||
alive: function(promise, reject) {
|
||||
var def = $.Deferred();
|
||||
|
|
|
@ -89,6 +89,10 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess
|
|||
});
|
||||
},
|
||||
session_is_valid: function() {
|
||||
var db = $.deparam.querystring().db;
|
||||
if (db && this.db !== db) {
|
||||
return false;
|
||||
}
|
||||
return !!this.uid;
|
||||
},
|
||||
/**
|
||||
|
|
|
@ -481,9 +481,12 @@ instance.web.DataSet = instance.web.Class.extend(instance.web.PropertiesMixin,
|
|||
* Creates a new record in db
|
||||
*
|
||||
* @param {Object} data field values to set on the new record
|
||||
* @param {Object} options Dictionary that can contain the following keys:
|
||||
* - readonly_fields: Values from readonly fields that were updated by
|
||||
* on_changes. Only used by the BufferedDataSet to make the o2m work correctly.
|
||||
* @returns {$.Deferred}
|
||||
*/
|
||||
create: function(data) {
|
||||
create: function(data, options) {
|
||||
return this._model.call('create', [data], {context: this.get_context()});
|
||||
},
|
||||
/**
|
||||
|
@ -491,8 +494,10 @@ instance.web.DataSet = instance.web.Class.extend(instance.web.PropertiesMixin,
|
|||
*
|
||||
* @param {Number|String} id identifier for the record to alter
|
||||
* @param {Object} data field values to write into the record
|
||||
* @param {Function} callback function called with operation result
|
||||
* @param {Function} error_callback function called in case of write error
|
||||
* @param {Object} options Dictionary that can contain the following keys:
|
||||
* - context: The context to use in the server-side call.
|
||||
* - readonly_fields: Values from readonly fields that were updated by
|
||||
* on_changes. Only used by the BufferedDataSet to make the o2m work correctly.
|
||||
* @returns {$.Deferred}
|
||||
*/
|
||||
write: function (id, data, options) {
|
||||
|
@ -732,10 +737,13 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({
|
|||
self.last_default_get = res;
|
||||
});
|
||||
},
|
||||
create: function(data) {
|
||||
var cached = {id:_.uniqueId(this.virtual_id_prefix), values: data,
|
||||
defaults: this.last_default_get};
|
||||
this.to_create.push(_.extend(_.clone(cached), {values: _.clone(cached.values)}));
|
||||
create: function(data, options) {
|
||||
var cached = {
|
||||
id:_.uniqueId(this.virtual_id_prefix),
|
||||
values: _.extend({}, data, (options || {}).readonly_fields || {}),
|
||||
defaults: this.last_default_get
|
||||
};
|
||||
this.to_create.push(_.extend(_.clone(cached), {values: _.clone(data)}));
|
||||
this.cache.push(cached);
|
||||
return $.Deferred().resolve(cached.id).promise();
|
||||
},
|
||||
|
@ -762,7 +770,7 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({
|
|||
cached = {id: id, values: {}};
|
||||
this.cache.push(cached);
|
||||
}
|
||||
$.extend(cached.values, record.values);
|
||||
$.extend(cached.values, _.extend({}, record.values, (options || {}).readonly_fields || {}));
|
||||
if (dirty)
|
||||
this.trigger("dataset_changed", id, data, options);
|
||||
return $.Deferred().resolve(true).promise();
|
||||
|
@ -860,8 +868,16 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({
|
|||
}
|
||||
return completion.promise();
|
||||
},
|
||||
call_button: function (method, args) {
|
||||
var id = args[0][0], index;
|
||||
/**
|
||||
* Invalidates caching of a record in the dataset to ensure the next read
|
||||
* of that record will hit the server.
|
||||
*
|
||||
* Of use when an action is going to remote-alter a record which will then
|
||||
* need to be reloaded, e.g. action button.
|
||||
*
|
||||
* @param {Object} id record to remove from the BDS's cache
|
||||
*/
|
||||
evict_record: function (id) {
|
||||
for(var i=0, len=this.cache.length; i<len; ++i) {
|
||||
var record = this.cache[i];
|
||||
// if record we call the button upon is in the cache
|
||||
|
@ -871,8 +887,15 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({
|
|||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
call_button: function (method, args) {
|
||||
this.evict_record(args[0][0]);
|
||||
return this._super(method, args);
|
||||
},
|
||||
exec_workflow: function (id, signal) {
|
||||
this.evict_record(id);
|
||||
return this._super(id, signal);
|
||||
},
|
||||
alter_ids: function(n_ids) {
|
||||
this._super(n_ids);
|
||||
this.trigger("dataset_changed", n_ids);
|
||||
|
@ -903,9 +926,9 @@ instance.web.ProxyDataSet = instance.web.DataSetSearch.extend({
|
|||
return this._super.apply(this, arguments);
|
||||
}
|
||||
},
|
||||
create: function(data) {
|
||||
create: function(data, options) {
|
||||
if (this.create_function) {
|
||||
return this.create_function(data, this._super);
|
||||
return this.create_function(data, options, this._super);
|
||||
} else {
|
||||
return this._super.apply(this, arguments);
|
||||
}
|
||||
|
|
|
@ -324,7 +324,10 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
|
|||
e.preventDefault();
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
'autocompleteopen': function () {
|
||||
this.$el.autocomplete('widget').css('z-index', 3);
|
||||
},
|
||||
},
|
||||
/**
|
||||
* @constructs instance.web.SearchView
|
||||
|
@ -352,7 +355,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
|
|||
this.has_defaults = !_.isEmpty(this.defaults);
|
||||
|
||||
this.inputs = [];
|
||||
this.controls = {};
|
||||
this.controls = [];
|
||||
|
||||
this.headless = this.options.hidden && !this.has_defaults;
|
||||
|
||||
|
@ -492,6 +495,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
|
|||
*/
|
||||
complete_global_search: function (req, resp) {
|
||||
$.when.apply(null, _(this.inputs).chain()
|
||||
.filter(function (input) { return input.visible(); })
|
||||
.invoke('complete', req.term)
|
||||
.value()).then(function () {
|
||||
resp(_(_(arguments).compact()).flatten(true));
|
||||
|
@ -580,18 +584,18 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
|
|||
*
|
||||
* @param {Array} items a list of nodes to convert to widgets
|
||||
* @param {Object} fields a mapping of field names to (ORM) field attributes
|
||||
* @param {String} [group_name] name of the group to put the new controls in
|
||||
* @param {Object} [group] group to put the new controls in
|
||||
*/
|
||||
make_widgets: function (items, fields, group_name) {
|
||||
group_name = group_name || null;
|
||||
if (!(group_name in this.controls)) {
|
||||
this.controls[group_name] = [];
|
||||
make_widgets: function (items, fields, group) {
|
||||
if (!group) {
|
||||
group = new instance.web.search.Group(
|
||||
this, 'q', {attrs: {string: _t("Filters")}});
|
||||
}
|
||||
var self = this, group = this.controls[group_name];
|
||||
var self = this;
|
||||
var filters = [];
|
||||
_.each(items, function (item) {
|
||||
if (filters.length && item.tag !== 'filter') {
|
||||
group.push(new instance.web.search.FilterGroup(filters, this));
|
||||
group.push(new instance.web.search.FilterGroup(filters, group));
|
||||
filters = [];
|
||||
}
|
||||
|
||||
|
@ -599,15 +603,18 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
|
|||
case 'separator': case 'newline':
|
||||
break;
|
||||
case 'filter':
|
||||
filters.push(new instance.web.search.Filter(item, this));
|
||||
filters.push(new instance.web.search.Filter(item, group));
|
||||
break;
|
||||
case 'group':
|
||||
self.make_widgets(item.children, fields, item.attrs.string);
|
||||
self.make_widgets(item.children, fields,
|
||||
new instance.web.search.Group(group, 'w', item));
|
||||
break;
|
||||
case 'field':
|
||||
group.push(this.make_field(item, fields[item['attrs'].name]));
|
||||
var field = this.make_field(
|
||||
item, fields[item['attrs'].name], group);
|
||||
group.push(field);
|
||||
// filters
|
||||
self.make_widgets(item.children, fields, group_name);
|
||||
self.make_widgets(item.children, fields, group);
|
||||
break;
|
||||
}
|
||||
}, this);
|
||||
|
@ -622,12 +629,13 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
|
|||
*
|
||||
* @param {Object} item fields_view_get node for the field
|
||||
* @param {Object} field fields_get result for the field
|
||||
* @param {Object} [parent]
|
||||
* @returns instance.web.search.Field
|
||||
*/
|
||||
make_field: function (item, field) {
|
||||
make_field: function (item, field, parent) {
|
||||
var obj = instance.web.search.fields.get_any( [item.attrs.widget, field.type]);
|
||||
if(obj) {
|
||||
return new (obj) (item, field, this);
|
||||
return new (obj) (item, field, parent || this);
|
||||
} else {
|
||||
console.group('Unknown field type ' + field.type);
|
||||
console.error('View node', item);
|
||||
|
@ -862,13 +870,18 @@ instance.web.search.Widget = instance.web.Widget.extend( /** @lends instance.web
|
|||
* @constructs instance.web.search.Widget
|
||||
* @extends instance.web.Widget
|
||||
*
|
||||
* @param view the ancestor view of this widget
|
||||
* @param parent parent of this widget
|
||||
*/
|
||||
init: function (view) {
|
||||
this._super(view);
|
||||
this.view = view;
|
||||
init: function (parent) {
|
||||
this._super(parent);
|
||||
var ancestor = parent;
|
||||
do {
|
||||
this.view = ancestor;
|
||||
} while (!(ancestor instanceof instance.web.SearchView)
|
||||
&& (ancestor = (ancestor.getParent && ancestor.getParent())));
|
||||
}
|
||||
});
|
||||
|
||||
instance.web.search.add_expand_listener = function($root) {
|
||||
$root.find('a.searchview_group_string').click(function (e) {
|
||||
$root.toggleClass('folded expanded');
|
||||
|
@ -877,13 +890,24 @@ instance.web.search.add_expand_listener = function($root) {
|
|||
});
|
||||
};
|
||||
instance.web.search.Group = instance.web.search.Widget.extend({
|
||||
template: 'SearchView.group',
|
||||
init: function (view_section, view, fields) {
|
||||
this._super(view);
|
||||
this.attrs = view_section.attrs;
|
||||
this.lines = view.make_widgets(
|
||||
view_section.children, fields);
|
||||
}
|
||||
init: function (parent, icon, node) {
|
||||
this._super(parent);
|
||||
var attrs = node.attrs;
|
||||
this.modifiers = attrs.modifiers =
|
||||
attrs.modifiers ? JSON.parse(attrs.modifiers) : {};
|
||||
this.attrs = attrs;
|
||||
this.icon = icon;
|
||||
this.name = attrs.string;
|
||||
this.children = [];
|
||||
|
||||
this.view.controls.push(this);
|
||||
},
|
||||
push: function (input) {
|
||||
this.children.push(input);
|
||||
},
|
||||
visible: function () {
|
||||
return !this.modifiers.invisible;
|
||||
},
|
||||
});
|
||||
|
||||
instance.web.search.Input = instance.web.search.Widget.extend( /** @lends instance.web.search.Input# */{
|
||||
|
@ -892,12 +916,12 @@ instance.web.search.Input = instance.web.search.Widget.extend( /** @lends instan
|
|||
* @constructs instance.web.search.Input
|
||||
* @extends instance.web.search.Widget
|
||||
*
|
||||
* @param view
|
||||
* @param parent
|
||||
*/
|
||||
init: function (view) {
|
||||
this._super(view);
|
||||
init: function (parent) {
|
||||
this._super(parent);
|
||||
this.load_attrs({});
|
||||
this.view.inputs.push(this);
|
||||
this.style = undefined;
|
||||
},
|
||||
/**
|
||||
* Fetch auto-completion values for the widget.
|
||||
|
@ -945,15 +969,30 @@ instance.web.search.Input = instance.web.search.Widget.extend( /** @lends instan
|
|||
"get_domain not implemented for widget " + this.attrs.type);
|
||||
},
|
||||
load_attrs: function (attrs) {
|
||||
if (attrs.modifiers) {
|
||||
attrs.modifiers = JSON.parse(attrs.modifiers);
|
||||
attrs.invisible = attrs.modifiers.invisible || false;
|
||||
if (attrs.invisible) {
|
||||
this.style = 'display: none;'
|
||||
attrs.modifiers = attrs.modifiers ? JSON.parse(attrs.modifiers) : {};
|
||||
this.attrs = attrs;
|
||||
},
|
||||
/**
|
||||
* Returns whether the input is "visible". The default behavior is to
|
||||
* query the ``modifiers.invisible`` flag on the input's description or
|
||||
* view node.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
visible: function () {
|
||||
if (this.attrs.modifiers.invisible) {
|
||||
return false;
|
||||
}
|
||||
var parent = this;
|
||||
while ((parent = parent.getParent()) &&
|
||||
( (parent instanceof instance.web.search.Group)
|
||||
|| (parent instanceof instance.web.search.Input))) {
|
||||
if (!parent.visible()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
this.attrs = attrs;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends instance.web.search.FilterGroup# */{
|
||||
template: 'SearchView.filters',
|
||||
|
@ -967,17 +1006,19 @@ instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends in
|
|||
* @extends instance.web.search.Input
|
||||
*
|
||||
* @param {Array<instance.web.search.Filter>} filters elements of the group
|
||||
* @param {instance.web.SearchView} view view in which the filters are contained
|
||||
* @param {instance.web.SearchView} parent parent in which the filters are contained
|
||||
*/
|
||||
init: function (filters, view) {
|
||||
init: function (filters, parent) {
|
||||
// If all filters are group_by and we're not initializing a GroupbyGroup,
|
||||
// create a GroupbyGroup instead of the current FilterGroup
|
||||
if (!(this instanceof instance.web.search.GroupbyGroup) &&
|
||||
_(filters).all(function (f) {
|
||||
return f.attrs.context && f.attrs.context.group_by; })) {
|
||||
return new instance.web.search.GroupbyGroup(filters, view);
|
||||
if (!f.attrs.context) { return false; }
|
||||
var c = instance.web.pyeval.eval('context', f.attrs.context);
|
||||
return !_.isEmpty(c.group_by);})) {
|
||||
return new instance.web.search.GroupbyGroup(filters, parent);
|
||||
}
|
||||
this._super(view);
|
||||
this._super(parent);
|
||||
this.filters = filters;
|
||||
this.view.query.on('add remove change reset', this.proxy('search_change'));
|
||||
},
|
||||
|
@ -1096,6 +1137,7 @@ instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends in
|
|||
var self = this;
|
||||
item = item.toLowerCase();
|
||||
var facet_values = _(this.filters).chain()
|
||||
.filter(function (filter) { return filter.visible(); })
|
||||
.filter(function (filter) {
|
||||
var at = {
|
||||
string: filter.attrs.string || '',
|
||||
|
@ -1122,8 +1164,8 @@ instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends in
|
|||
instance.web.search.GroupbyGroup = instance.web.search.FilterGroup.extend({
|
||||
icon: 'w',
|
||||
completion_label: _lt("Group by: %s"),
|
||||
init: function (filters, view) {
|
||||
this._super(filters, view);
|
||||
init: function (filters, parent) {
|
||||
this._super(filters, parent);
|
||||
// Not flanders: facet unicity is handled through the
|
||||
// (category, field) pair of facet attributes. This is all well and
|
||||
// good for regular filter groups where a group matches a facet, but for
|
||||
|
@ -1131,8 +1173,8 @@ instance.web.search.GroupbyGroup = instance.web.search.FilterGroup.extend({
|
|||
// view which proxies to the first GroupbyGroup, so it can be used
|
||||
// for every GroupbyGroup and still provides the various methods needed
|
||||
// by the search view. Use weirdo name to avoid risks of conflicts
|
||||
if (!this.getParent()._s_groupby) {
|
||||
this.getParent()._s_groupby = {
|
||||
if (!this.view._s_groupby) {
|
||||
this.view._s_groupby = {
|
||||
help: "See GroupbyGroup#init",
|
||||
get_context: this.proxy('get_context'),
|
||||
get_domain: this.proxy('get_domain'),
|
||||
|
@ -1141,14 +1183,14 @@ instance.web.search.GroupbyGroup = instance.web.search.FilterGroup.extend({
|
|||
}
|
||||
},
|
||||
match_facet: function (facet) {
|
||||
return facet.get('field') === this.getParent()._s_groupby;
|
||||
return facet.get('field') === this.view._s_groupby;
|
||||
},
|
||||
make_facet: function (values) {
|
||||
return {
|
||||
category: _t("GroupBy"),
|
||||
icon: this.icon,
|
||||
values: values,
|
||||
field: this.getParent()._s_groupby
|
||||
field: this.view._s_groupby
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -1166,10 +1208,10 @@ instance.web.search.Filter = instance.web.search.Input.extend(/** @lends instanc
|
|||
* @extends instance.web.search.Input
|
||||
*
|
||||
* @param node
|
||||
* @param view
|
||||
* @param parent
|
||||
*/
|
||||
init: function (node, view) {
|
||||
this._super(view);
|
||||
init: function (node, parent) {
|
||||
this._super(parent);
|
||||
this.load_attrs(node.attrs);
|
||||
},
|
||||
facet_for: function () { return $.when(null); },
|
||||
|
@ -1185,10 +1227,10 @@ instance.web.search.Field = instance.web.search.Input.extend( /** @lends instanc
|
|||
*
|
||||
* @param view_section
|
||||
* @param field
|
||||
* @param view
|
||||
* @param parent
|
||||
*/
|
||||
init: function (view_section, field, view) {
|
||||
this._super(view);
|
||||
init: function (view_section, field, parent) {
|
||||
this._super(parent);
|
||||
this.load_attrs(_.extend({}, field, view_section.attrs));
|
||||
},
|
||||
facet_for: function (value) {
|
||||
|
@ -1228,7 +1270,7 @@ instance.web.search.Field = instance.web.search.Input.extend( /** @lends instanc
|
|||
*
|
||||
* @param {String} name the field's name
|
||||
* @param {String} operator the field's operator (either attribute-specified or default operator for the field
|
||||
* @param {Number|String} value parsed value for the field
|
||||
* @param {Number|String} facet parsed value for the field
|
||||
* @returns {Array<Array>} domain to include in the resulting search
|
||||
*/
|
||||
make_domain: function (name, operator, facet) {
|
||||
|
@ -1460,8 +1502,8 @@ instance.web.search.DateTimeField = instance.web.search.DateField.extend(/** @le
|
|||
});
|
||||
instance.web.search.ManyToOneField = instance.web.search.CharField.extend({
|
||||
default_operator: {},
|
||||
init: function (view_section, field, view) {
|
||||
this._super(view_section, field, view);
|
||||
init: function (view_section, field, parent) {
|
||||
this._super(view_section, field, parent);
|
||||
this.model = new instance.web.Model(this.attrs.relation);
|
||||
},
|
||||
complete: function (needle) {
|
||||
|
@ -1673,6 +1715,13 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({
|
|||
if (!_.isEmpty(results.group_by)) {
|
||||
results.context.group_by = results.group_by;
|
||||
}
|
||||
// Don't save user_context keys in the custom filter, otherwise end
|
||||
// up with e.g. wrong uid or lang stored *and used in subsequent
|
||||
// reqs*
|
||||
var ctx = results.context;
|
||||
_(_.keys(instance.session.user_context)).each(function (key) {
|
||||
delete ctx[key];
|
||||
});
|
||||
var filter = {
|
||||
name: $name.val(),
|
||||
user_id: private_filter ? instance.session.uid : false,
|
||||
|
@ -1702,22 +1751,28 @@ instance.web.search.Filters = instance.web.search.Input.extend({
|
|||
var running_count = 0;
|
||||
// get total filters count
|
||||
var is_group = function (i) { return i instanceof instance.web.search.FilterGroup; };
|
||||
var filters_count = _(this.view.controls).chain()
|
||||
var visible_filters = _(this.view.controls).chain().reject(function (group) {
|
||||
return _(_(group.children).filter(is_group)).isEmpty()
|
||||
|| group.modifiers.invisible;
|
||||
});
|
||||
var filters_count = visible_filters
|
||||
.pluck('children')
|
||||
.flatten()
|
||||
.filter(is_group)
|
||||
.map(function (i) { return i.filters.length; })
|
||||
.sum()
|
||||
.value();
|
||||
|
||||
var col1 = [], col2 = _(this.view.controls).map(function (inputs, group) {
|
||||
var filters = _(inputs).filter(is_group);
|
||||
return {
|
||||
name: group === 'null' ? "<span class='oe_i'>q</span> " + _t("Filters") : "<span class='oe_i'>w</span> " + group,
|
||||
filters: filters,
|
||||
length: _(filters).chain().map(function (i) {
|
||||
return i.filters.length; }).sum().value()
|
||||
};
|
||||
});
|
||||
var col1 = [], col2 = visible_filters.map(function (group) {
|
||||
var filters = _(group.children).filter(is_group);
|
||||
return {
|
||||
name: _.str.sprintf("<span class='oe_i'>%s</span> %s",
|
||||
group.icon, group.name),
|
||||
filters: filters,
|
||||
length: _(filters).chain().map(function (i) {
|
||||
return i.filters.length; }).sum().value()
|
||||
};
|
||||
}).value();
|
||||
|
||||
while (col2.length) {
|
||||
// col1 + group should be smaller than col2 + group
|
||||
|
|
|
@ -810,7 +810,8 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
|
|||
try {
|
||||
var form_invalid = false,
|
||||
values = {},
|
||||
first_invalid_field = null;
|
||||
first_invalid_field = null,
|
||||
readonly_values = {};
|
||||
for (var f in self.fields) {
|
||||
if (!self.fields.hasOwnProperty(f)) { continue; }
|
||||
f = self.fields[f];
|
||||
|
@ -819,11 +820,15 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
|
|||
if (!first_invalid_field) {
|
||||
first_invalid_field = f;
|
||||
}
|
||||
} else if (f.name !== 'id' && !f.get("readonly") && (!self.datarecord.id || f._dirty_flag)) {
|
||||
} else if (f.name !== 'id' && (!self.datarecord.id || f._dirty_flag)) {
|
||||
// Special case 'id' field, do not save this field
|
||||
// on 'create' : save all non readonly fields
|
||||
// on 'edit' : save non readonly modified fields
|
||||
values[f.name] = f.get_value();
|
||||
if (!f.get("readonly")) {
|
||||
values[f.name] = f.get_value();
|
||||
} else {
|
||||
readonly_values[f.name] = f.get_value();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (form_invalid) {
|
||||
|
@ -836,7 +841,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
|
|||
var save_deferral;
|
||||
if (!self.datarecord.id) {
|
||||
// Creation save
|
||||
save_deferral = self.dataset.create(values).then(function(r) {
|
||||
save_deferral = self.dataset.create(values, {readonly_fields: readonly_values}).then(function(r) {
|
||||
return self.record_created(r, prepend_on_create);
|
||||
}, null);
|
||||
} else if (_.isEmpty(values)) {
|
||||
|
@ -844,7 +849,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
|
|||
save_deferral = $.Deferred().resolve({}).promise();
|
||||
} else {
|
||||
// Write save
|
||||
save_deferral = self.dataset.write(self.datarecord.id, values, {}).then(function(r) {
|
||||
save_deferral = self.dataset.write(self.datarecord.id, values, {readonly_fields: readonly_values}).then(function(r) {
|
||||
return self.record_saved(r);
|
||||
}, null);
|
||||
}
|
||||
|
@ -1887,10 +1892,11 @@ instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({
|
|||
if (this.node.attrs.icon && (! /\//.test(this.node.attrs.icon))) {
|
||||
this.node.attrs.icon = '/web/static/src/img/icons/' + this.node.attrs.icon + '.png';
|
||||
}
|
||||
this.view.on('view_content_has_changed', this, this.check_disable);
|
||||
},
|
||||
start: function() {
|
||||
this._super.apply(this, arguments);
|
||||
this.view.on('view_content_has_changed', this, this.check_disable);
|
||||
this.check_disable();
|
||||
this.$el.click(this.on_click);
|
||||
if (this.node.attrs.help || instance.session.debug) {
|
||||
this.do_attach_tooltip();
|
||||
|
@ -1916,18 +1922,20 @@ instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({
|
|||
modal: true,
|
||||
buttons: [
|
||||
{text: _t("Cancel"), click: function() {
|
||||
def.resolve();
|
||||
$(this).dialog("close");
|
||||
}
|
||||
},
|
||||
{text: _t("Ok"), click: function() {
|
||||
self.on_confirmed().done(function() {
|
||||
def.resolve();
|
||||
var self2 = this;
|
||||
self.on_confirmed().always(function() {
|
||||
$(self2).dialog("close");
|
||||
});
|
||||
$(this).dialog("close");
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
beforeClose: function() {
|
||||
def.resolve();
|
||||
},
|
||||
});
|
||||
return def.promise();
|
||||
} else {
|
||||
|
@ -2218,6 +2226,18 @@ instance.web.form.ReinitializeFieldMixin = _.extend({}, instance.web.form.Reini
|
|||
},
|
||||
});
|
||||
|
||||
/**
|
||||
Some hack to make placeholders work in ie9.
|
||||
*/
|
||||
if ($.browser.msie && $.browser.version === "9.0") {
|
||||
document.addEventListener("DOMNodeInserted",function(event){
|
||||
var nodename = event.target.nodeName.toLowerCase();
|
||||
if ( nodename === "input" || nodename == "textarea" ) {
|
||||
$(event.target).placeholder();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
instance.web.form.FieldChar = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
|
||||
template: 'FieldChar',
|
||||
widget_class: 'oe_form_field_char',
|
||||
|
@ -2382,7 +2402,6 @@ instance.web.DateTimeWidget = instance.web.Widget.extend({
|
|||
type_of_date: "datetime",
|
||||
events: {
|
||||
'change .oe_datepicker_master': 'change_datetime',
|
||||
'dragstart img.oe_datepicker_trigger': function () { return false; },
|
||||
},
|
||||
init: function(parent) {
|
||||
this._super(parent);
|
||||
|
@ -2546,42 +2565,45 @@ instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.we
|
|||
},
|
||||
'change textarea': 'store_dom_value',
|
||||
},
|
||||
init: function (field_manager, node) {
|
||||
this._super(field_manager, node);
|
||||
},
|
||||
initialize_content: function() {
|
||||
var self = this;
|
||||
this.$textarea = this.$el.find('textarea');
|
||||
this.auto_sized = false;
|
||||
this.default_height = this.$textarea.css('height');
|
||||
if (this.get("effective_readonly")) {
|
||||
this.$textarea.attr('disabled', 'disabled');
|
||||
if (! this.get("effective_readonly")) {
|
||||
this.$textarea = this.$el.find('textarea');
|
||||
this.auto_sized = false;
|
||||
this.default_height = this.$textarea.css('height');
|
||||
if (this.get("effective_readonly")) {
|
||||
this.$textarea.attr('disabled', 'disabled');
|
||||
}
|
||||
this.setupFocus(this.$textarea);
|
||||
} else {
|
||||
this.$textarea = undefined;
|
||||
}
|
||||
this.setupFocus(this.$textarea);
|
||||
},
|
||||
commit_value: function () {
|
||||
this.store_dom_value();
|
||||
if (! this.get("effective_readonly") && this.$textarea) {
|
||||
this.store_dom_value();
|
||||
}
|
||||
return this._super();
|
||||
},
|
||||
store_dom_value: function () {
|
||||
if (!this.get('effective_readonly') && this.$('textarea').length) {
|
||||
this.internal_set_value(
|
||||
instance.web.parse_value(
|
||||
this.$textarea.val(),
|
||||
this));
|
||||
}
|
||||
this.internal_set_value(instance.web.parse_value(this.$textarea.val(), this));
|
||||
},
|
||||
render_value: function() {
|
||||
var show_value = instance.web.format_value(this.get('value'), this, '');
|
||||
if (show_value === '') {
|
||||
this.$textarea.css('height', parseInt(this.default_height)+"px");
|
||||
}
|
||||
this.$textarea.val(show_value);
|
||||
if (! this.auto_sized) {
|
||||
this.auto_sized = true;
|
||||
this.$textarea.autosize();
|
||||
if (! this.get("effective_readonly")) {
|
||||
var show_value = instance.web.format_value(this.get('value'), this, '');
|
||||
if (show_value === '') {
|
||||
this.$textarea.css('height', parseInt(this.default_height)+"px");
|
||||
}
|
||||
this.$textarea.val(show_value);
|
||||
if (! this.auto_sized) {
|
||||
this.auto_sized = true;
|
||||
this.$textarea.autosize();
|
||||
} else {
|
||||
this.$textarea.trigger("autosize");
|
||||
}
|
||||
} else {
|
||||
this.$textarea.trigger("autosize");
|
||||
var txt = this.get("value");
|
||||
this.$(".oe_form_text_content").text(txt);
|
||||
}
|
||||
},
|
||||
is_syntax_valid: function() {
|
||||
|
@ -2599,14 +2621,18 @@ instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.we
|
|||
return this.get('value') === '' || this._super();
|
||||
},
|
||||
focus: function($el) {
|
||||
this.$textarea[0].focus();
|
||||
if (!this.get("effective_readonly") && this.$textarea) {
|
||||
this.$textarea[0].focus();
|
||||
}
|
||||
},
|
||||
set_dimensions: function (height, width) {
|
||||
this._super(height, width);
|
||||
this.$textarea.css({
|
||||
width: width,
|
||||
minHeight: height
|
||||
});
|
||||
if (!this.get("effective_readonly") && this.$textarea) {
|
||||
this.$textarea.css({
|
||||
width: width,
|
||||
minHeight: height
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -2970,8 +2996,6 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc
|
|||
e.stopPropagation();
|
||||
}
|
||||
},
|
||||
'dragstart .oe_m2o_drop_down_button img': function () { return false; },
|
||||
'dragstart .oe_m2o_cm_button': function () { return false; }
|
||||
},
|
||||
init: function(field_manager, node) {
|
||||
this._super(field_manager, node);
|
||||
|
@ -3002,6 +3026,26 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc
|
|||
if (!this.get("effective_readonly"))
|
||||
this.render_editable();
|
||||
},
|
||||
destroy_content: function () {
|
||||
if (this.$drop_down) {
|
||||
this.$drop_down.off('click');
|
||||
delete this.$drop_down;
|
||||
}
|
||||
if (this.$input) {
|
||||
this.$input.closest(".ui-dialog .ui-dialog-content").off('scroll');
|
||||
this.$input.off('keyup blur autocompleteclose autocompleteopen ' +
|
||||
'focus focusout change keydown');
|
||||
delete this.$input;
|
||||
}
|
||||
if (this.$follow_button) {
|
||||
this.$follow_button.off('blur focus click');
|
||||
delete this.$follow_button;
|
||||
}
|
||||
},
|
||||
destroy: function () {
|
||||
this.destroy_content();
|
||||
return this._super();
|
||||
},
|
||||
init_error_displayer: function() {
|
||||
// nothing
|
||||
},
|
||||
|
@ -3263,7 +3307,7 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc
|
|||
},
|
||||
focus: function () {
|
||||
if (!this.get('effective_readonly')) {
|
||||
this.$input[0].focus();
|
||||
this.$input && this.$input[0].focus();
|
||||
}
|
||||
},
|
||||
_quick_create: function() {
|
||||
|
@ -3682,8 +3726,8 @@ instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({
|
|||
var pop = new instance.web.form.FormOpenPopup(this);
|
||||
pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
|
||||
title: _t("Open: ") + self.o2m.string,
|
||||
create_function: function(data) {
|
||||
return self.o2m.dataset.create(data).done(function(r) {
|
||||
create_function: function(data, options) {
|
||||
return self.o2m.dataset.create(data, options).done(function(r) {
|
||||
self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
|
||||
self.o2m.dataset.trigger("dataset_changed", r);
|
||||
});
|
||||
|
@ -3742,8 +3786,12 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({
|
|||
this.o2m.trigger_on_change();
|
||||
},
|
||||
is_valid: function () {
|
||||
var form = this.editor.form;
|
||||
|
||||
var editor = this.editor;
|
||||
var form = editor.form;
|
||||
// If no edition is pending, the listview can not be invalid (?)
|
||||
if (!editor.record) {
|
||||
return true
|
||||
}
|
||||
// If the form has not been modified, the view can only be valid
|
||||
// NB: is_dirty will also be set on defaults/onchanges/whatever?
|
||||
// oe_form_dirty seems to only be set on actual user actions
|
||||
|
@ -3773,11 +3821,11 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({
|
|||
title: _t("Create: ") + self.o2m.string,
|
||||
initial_view: "form",
|
||||
alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
|
||||
create_function: function(data, callback, error_callback) {
|
||||
return self.o2m.dataset.create(data).done(function(r) {
|
||||
create_function: function(data, options) {
|
||||
return self.o2m.dataset.create(data, options).done(function(r) {
|
||||
self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
|
||||
self.o2m.dataset.trigger("dataset_changed", r);
|
||||
}).done(callback).fail(error_callback);
|
||||
});
|
||||
},
|
||||
read_function: function() {
|
||||
return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
|
||||
|
@ -3990,6 +4038,7 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(in
|
|||
if (this.get("effective_readonly"))
|
||||
return;
|
||||
var self = this;
|
||||
var ignore_blur = false;
|
||||
self.$text = this.$("textarea");
|
||||
self.$text.textext({
|
||||
plugins : 'tags arrow autocomplete',
|
||||
|
@ -4008,6 +4057,7 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(in
|
|||
if (data.id) {
|
||||
self.add_id(data.id);
|
||||
} else {
|
||||
ignore_blur = true;
|
||||
data.action();
|
||||
}
|
||||
},
|
||||
|
@ -4058,10 +4108,13 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(in
|
|||
self.$text
|
||||
.focusin(function () {
|
||||
self.trigger('focused');
|
||||
ignore_blur = false;
|
||||
})
|
||||
.focusout(function() {
|
||||
self.$text.trigger("setInputData", "");
|
||||
self.trigger('blurred');
|
||||
if (!ignore_blur) {
|
||||
self.trigger('blurred');
|
||||
}
|
||||
}).keydown(function(e) {
|
||||
if (e.which === $.ui.keyCode.TAB && self._drop_shown) {
|
||||
self.$text.textext()[0].autocomplete().selectFromDropdown();
|
||||
|
@ -4268,6 +4321,7 @@ instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends in
|
|||
});
|
||||
}
|
||||
},
|
||||
is_action_enabled: function () { return true; },
|
||||
});
|
||||
|
||||
instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, {
|
||||
|
@ -4502,9 +4556,9 @@ instance.web.form.AbstractFormPopup = instance.web.Widget.extend({
|
|||
this.created_elements = [];
|
||||
this.dataset = new instance.web.ProxyDataSet(this, this.model, this.context);
|
||||
this.dataset.read_function = this.options.read_function;
|
||||
this.dataset.create_function = function(data, sup) {
|
||||
this.dataset.create_function = function(data, options, sup) {
|
||||
var fct = self.options.create_function || sup;
|
||||
return fct.call(this, data).done(function(r) {
|
||||
return fct.call(this, data, options).done(function(r) {
|
||||
self.trigger('create_completed saved', r);
|
||||
self.created_elements.push(r);
|
||||
});
|
||||
|
@ -5216,11 +5270,11 @@ instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
|
|||
this.calc_domain();
|
||||
this.on("change:value", this, this.get_selection);
|
||||
this.on("change:evaluated_selection_domain", this, this.get_selection);
|
||||
this.get_selection();
|
||||
this.on("change:selection", this, function() {
|
||||
this.selection = this.get("selection");
|
||||
this.render_value();
|
||||
});
|
||||
this.get_selection();
|
||||
if (this.options.clickable) {
|
||||
this.$el.on('click','li',this.on_click_stage);
|
||||
}
|
||||
|
@ -5339,10 +5393,10 @@ instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({
|
|||
});
|
||||
},
|
||||
parse_value: function(val, def) {
|
||||
return instance.web.parse_value(val, {type: "float"}, def);
|
||||
return instance.web.parse_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
|
||||
},
|
||||
format_value: function(val, def) {
|
||||
return instance.web.format_value(val, {type: "float"}, def);
|
||||
return instance.web.format_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -321,9 +321,9 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
|
|||
.appendTo($this.empty())
|
||||
.click(function (e) {e.stopPropagation();})
|
||||
.append('<option value="80">80</option>' +
|
||||
'<option value="100">100</option>' +
|
||||
'<option value="200">200</option>' +
|
||||
'<option value="500">500</option>' +
|
||||
'<option value="2000">2000</option>' +
|
||||
'<option value="NaN">' + _t("Unlimited") + '</option>')
|
||||
.change(function () {
|
||||
var val = parseInt($select.val(), 10);
|
||||
|
|
|
@ -96,7 +96,8 @@ openerp.web.list_editable = function (instance) {
|
|||
});
|
||||
},
|
||||
editable: function () {
|
||||
return !this.options.disable_editable_mode
|
||||
return !this.grouped
|
||||
&& !this.options.disable_editable_mode
|
||||
&& (this.fields_view.arch.attrs.editable
|
||||
|| this._context_editable
|
||||
|| this.options.editable);
|
||||
|
|
|
@ -1446,11 +1446,11 @@ instance.web.View = instance.web.Widget.extend({
|
|||
* Performs a fields_view_get and apply postprocessing.
|
||||
* return a {$.Deferred} resolved with the fvg
|
||||
*
|
||||
* @param {Object} [args]
|
||||
* @param {Object} args
|
||||
* @param {String|Object} args.model instance.web.Model instance or string repr of the model
|
||||
* @param {null|Object} args.context context if args.model is a string
|
||||
* @param {null|Number} args.view_id id of the view to be loaded, default view if null
|
||||
* @param {null|String} args.view_type type of view to be loaded if view_id is null
|
||||
* @param {Object} [args.context] context if args.model is a string
|
||||
* @param {Number} [args.view_id] id of the view to be loaded, default view if null
|
||||
* @param {String} [args.view_type] type of view to be loaded if view_id is null
|
||||
* @param {Boolean} [args.toolbar=false] get the toolbar definition
|
||||
*/
|
||||
instance.web.fields_view_get = function(args) {
|
||||
|
@ -1555,12 +1555,17 @@ instance.web.xml_to_str = function(node) {
|
|||
} else {
|
||||
throw new Error(_t("Could not serialize XML"));
|
||||
}
|
||||
// Browsers won't deal with self closing tags except br, hr, input, ...
|
||||
// http://stackoverflow.com/questions/97522/what-are-all-the-valid-self-closing-elements-in-xhtml-as-implemented-by-the-maj
|
||||
//
|
||||
// Browsers won't deal with self closing tags except void elements:
|
||||
// http://www.w3.org/TR/html-markup/syntax.html
|
||||
var void_elements = 'area base br col command embed hr img input keygen link meta param source track wbr'.split(' ');
|
||||
|
||||
// The following regex is a bit naive but it's ok for the xmlserializer output
|
||||
str = str.replace(/<([a-z]+)([^<>]*)\s*\/\s*>/g, function(match, tag, attrs) {
|
||||
return "<" + tag + attrs + "></" + tag + ">";
|
||||
if (void_elements.indexOf(tag) < 0) {
|
||||
return "<" + tag + attrs + "></" + tag + ">";
|
||||
} else {
|
||||
return match;
|
||||
}
|
||||
});
|
||||
return str;
|
||||
};
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
<td>
|
||||
<p>
|
||||
<t t-js="d">
|
||||
var message = d.message ? d.message : d.error.data.message;
|
||||
var message = d.message ? d.message : d.error.data.fault_code;
|
||||
d.html_error = context.engine.tools.html_escape(message)
|
||||
.replace(/\n/g, '<br/>');
|
||||
</t>
|
||||
|
@ -71,9 +71,9 @@
|
|||
</div>
|
||||
<ul>
|
||||
<li>Username</li>
|
||||
<li><input name="login" type="text" t-att-value="widget.selected_login || ''" autofocus="autofocus"/></li>
|
||||
<li><input name="login" type="text" value="" autofocus="autofocus"/></li>
|
||||
<li>Password</li>
|
||||
<li><input name="password" type="password" t-att-value="widget.selected_password || ''"/></li>
|
||||
<li><input name="password" type="password" value=""/></li>
|
||||
<li><button name="submit">Log in</button></li>
|
||||
</ul>
|
||||
</form>
|
||||
|
@ -118,7 +118,8 @@
|
|||
you will be able to install your first application.
|
||||
</p>
|
||||
<p class="oe_grey" style="margin: 10px">
|
||||
By default, the master password is 'admin' if you did not changed it.
|
||||
By default, the master password is 'admin'. This password
|
||||
is required to created, delete dump or restore databases.
|
||||
</p>
|
||||
<table class="db_option_table" style="margin: 10px">
|
||||
<tr>
|
||||
|
@ -129,7 +130,9 @@
|
|||
</tr>
|
||||
<tr>
|
||||
<td><label for="db_name">Select a database name:</label></td>
|
||||
<td><input type="text" name="db_name" class="required" matches="^[a-zA-Z0-9][a-zA-Z0-9_-]+$" autofocus="true" placeholder="e.g. mycompany"/></td>
|
||||
<td>
|
||||
<input type="text" name="db_name" class="required" matches="^[a-zA-Z0-9][a-zA-Z0-9_-]+$" autofocus="true" placeholder="e.g. mycompany"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="demo_data">Load demonstration data:</label></td>
|
||||
|
@ -1047,17 +1050,22 @@
|
|||
</t>
|
||||
<t t-name="FieldText">
|
||||
<div class="oe_form_field oe_form_field_text" t-att-style="widget.node.attrs.style">
|
||||
<textarea rows="6"
|
||||
t-att-name="widget.name"
|
||||
class="field_text"
|
||||
t-att-tabindex="widget.node.attrs.tabindex"
|
||||
t-att-autofocus="widget.node.attrs.autofocus"
|
||||
t-att-placeholder="! widget.get('effective_readonly') ? widget.node.attrs.placeholder : ''"
|
||||
t-att-maxlength="widget.field.size"
|
||||
></textarea><img class="oe_field_translate oe_input_icon"
|
||||
t-if="widget.field.translate and !widget.get('effective_readonly')"
|
||||
t-att-src='_s + "/web/static/src/img/icons/terp-translate.png"' width="16" height="16" border="0"
|
||||
/>
|
||||
<t t-if="!widget.get('effective_readonly')">
|
||||
<textarea rows="6"
|
||||
t-att-name="widget.name"
|
||||
class="field_text"
|
||||
t-att-tabindex="widget.node.attrs.tabindex"
|
||||
t-att-autofocus="widget.node.attrs.autofocus"
|
||||
t-att-placeholder="! widget.get('effective_readonly') ? widget.node.attrs.placeholder : ''"
|
||||
t-att-maxlength="widget.field.size"
|
||||
></textarea><img class="oe_field_translate oe_input_icon"
|
||||
t-if="widget.field.translate and !widget.get('effective_readonly')"
|
||||
t-att-src='_s + "/web/static/src/img/icons/terp-translate.png"' width="16" height="16" border="0"
|
||||
/>
|
||||
</t>
|
||||
<t t-if="widget.get('effective_readonly')">
|
||||
<span class="oe_form_text_content"></span>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
<t t-name="FieldTextHtml">
|
||||
|
@ -1076,8 +1084,9 @@
|
|||
t-att-name="widget.name"
|
||||
t-att-placeholder="placeholder"
|
||||
class="oe_datepicker_master"
|
||||
/><img class="oe_input_icon oe_datepicker_trigger" t-att-src='_s + "/web/static/src/img/ui/field_calendar.png"'
|
||||
title="Select date" width="16" height="16" border="0"/>
|
||||
/><img class="oe_input_icon oe_datepicker_trigger" draggable="false"
|
||||
t-att-src='_s + "/web/static/src/img/ui/field_calendar.png"'
|
||||
title="Select date" width="16" height="16" border="0"/>
|
||||
</span>
|
||||
</t>
|
||||
<t t-name="FieldDate">
|
||||
|
@ -1111,7 +1120,7 @@
|
|||
</t>
|
||||
<t t-if="!widget.get('effective_readonly')">
|
||||
<a t-if="! widget.options.no_open" href="#" tabindex="-1"
|
||||
class="oe_m2o_cm_button oe_e">/</a>
|
||||
class="oe_m2o_cm_button oe_e" draggable="false">/</a>
|
||||
<div>
|
||||
<input type="text"
|
||||
t-att-id="widget.id_for_label"
|
||||
|
@ -1120,7 +1129,7 @@
|
|||
t-att-placeholder="widget.node.attrs.placeholder"
|
||||
/>
|
||||
<span class="oe_m2o_drop_down_button">
|
||||
<img t-att-src='_s + "/web/static/src/img/down-arrow.png"'/>
|
||||
<img t-att-src='_s + "/web/static/src/img/down-arrow.png"' draggable="false"/>
|
||||
</span>
|
||||
</div>
|
||||
</t>
|
||||
|
@ -1515,7 +1524,7 @@
|
|||
<t t-esc="attrs.string"/>
|
||||
</button>
|
||||
<ul t-name="SearchView.filters">
|
||||
<li t-foreach="widget.filters" t-as="filter"
|
||||
<li t-foreach="widget.filters" t-as="filter" t-if="filter.visible()"
|
||||
t-att-title="filter.attrs.string ? filter.attrs.help : undefined">
|
||||
<t t-esc="filter.attrs.string or filter.attrs.help or filter.attrs.name or 'Ω'"/>
|
||||
</li>
|
||||
|
@ -1603,15 +1612,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<t t-name="SearchView.group">
|
||||
<t t-call="SearchView.util.expand">
|
||||
<t t-set="expand" t-value="attrs.expand"/>
|
||||
<t t-set="label" t-value="attrs.string"/>
|
||||
<t t-set="content">
|
||||
<t t-call="SearchView.render_lines"/>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
<div t-name="SearchView.Filters" class="oe_searchview_filters oe_searchview_section">
|
||||
|
||||
</div>
|
||||
|
@ -1842,7 +1842,7 @@
|
|||
<tr>
|
||||
<td t-foreach="records[0]" t-as="column">
|
||||
<input class="sel_fields" placeholder="--- Don't Import ---"/><span class="oe_m2o_drop_down_button">
|
||||
<img t-att-src='_s + "/web/static/src/img/down-arrow.png"' /></span>
|
||||
<img t-att-src='_s + "/web/static/src/img/down-arrow.png"' draggable="false"/></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr t-foreach="records" t-as="record" class="oe_import_grid-row">
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
openerp.testing.section('query', {
|
||||
openerp.testing.section('search.query', {
|
||||
dependencies: ['web.search']
|
||||
}, function (test) {
|
||||
test('Adding a facet to the query creates a facet and a value', function (instance) {
|
||||
|
@ -180,7 +180,7 @@ var makeSearchView = function (instance, dummy_widget_attributes, defaults) {
|
|||
});
|
||||
return view;
|
||||
};
|
||||
openerp.testing.section('defaults', {
|
||||
openerp.testing.section('search.defaults', {
|
||||
dependencies: ['web.search'],
|
||||
rpc: 'mock',
|
||||
templates: true,
|
||||
|
@ -362,7 +362,7 @@ openerp.testing.section('defaults', {
|
|||
"should not accept multiple default values");
|
||||
})
|
||||
});
|
||||
openerp.testing.section('completions', {
|
||||
openerp.testing.section('search.completions', {
|
||||
dependencies: ['web.search'],
|
||||
rpc: 'mock',
|
||||
templates: true
|
||||
|
@ -615,7 +615,7 @@ openerp.testing.section('completions', {
|
|||
return f.complete("bob");
|
||||
});
|
||||
});
|
||||
openerp.testing.section('search-serialization', {
|
||||
openerp.testing.section('search.serialization', {
|
||||
dependencies: ['web.search'],
|
||||
rpc: 'mock',
|
||||
templates: true
|
||||
|
@ -855,11 +855,11 @@ openerp.testing.section('search-serialization', {
|
|||
test('FilterGroup', {asserts: 6}, function (instance) {
|
||||
var view = {inputs: [], query: {on: function () {}}};
|
||||
var filter_a = new instance.web.search.Filter(
|
||||
{attrs: {name: 'a', context: 'c1', domain: 'd1'}}, view);
|
||||
{attrs: {name: 'a', context: '{"c1": True}', domain: 'd1'}}, view);
|
||||
var filter_b = new instance.web.search.Filter(
|
||||
{attrs: {name: 'b', context: 'c2', domain: 'd2'}}, view);
|
||||
{attrs: {name: 'b', context: '{"c2": True}', domain: 'd2'}}, view);
|
||||
var filter_c = new instance.web.search.Filter(
|
||||
{attrs: {name: 'c', context: 'c3', domain: 'd3'}}, view);
|
||||
{attrs: {name: 'c', context: '{"c3": True}', domain: 'd3'}}, view);
|
||||
var group = new instance.web.search.FilterGroup(
|
||||
[filter_a, filter_b, filter_c], view);
|
||||
return group.facet_for_defaults({a: true, c: true})
|
||||
|
@ -880,7 +880,7 @@ openerp.testing.section('search-serialization', {
|
|||
equal(context.__ref, 'compound_context',
|
||||
"context should be compound");
|
||||
deepEqual(context.__contexts, [
|
||||
'c1', 'c3'
|
||||
'{"c1": True}', '{"c3": True}'
|
||||
], "context should merge all filter contexts");
|
||||
ok(!context.get_eval_context(), "context should have no evaluation context");
|
||||
});
|
||||
|
@ -922,7 +922,7 @@ openerp.testing.section('search-serialization', {
|
|||
return $.when(t1, t2);
|
||||
});
|
||||
});
|
||||
openerp.testing.section('removal', {
|
||||
openerp.testing.section('search.removal', {
|
||||
dependencies: ['web.search'],
|
||||
rpc: 'mock',
|
||||
templates: true
|
||||
|
@ -945,7 +945,7 @@ openerp.testing.section('removal', {
|
|||
});
|
||||
});
|
||||
});
|
||||
openerp.testing.section('drawer', {
|
||||
openerp.testing.section('search.drawer', {
|
||||
dependencies: ['web.search'],
|
||||
rpc: 'mock',
|
||||
templates: true
|
||||
|
@ -961,7 +961,7 @@ openerp.testing.section('drawer', {
|
|||
});
|
||||
});
|
||||
});
|
||||
openerp.testing.section('filters', {
|
||||
openerp.testing.section('search.filters', {
|
||||
dependencies: ['web.search'],
|
||||
rpc: 'mock',
|
||||
templates: true,
|
||||
|
@ -1046,7 +1046,104 @@ openerp.testing.section('filters', {
|
|||
});
|
||||
});
|
||||
});
|
||||
openerp.testing.section('saved_filters', {
|
||||
openerp.testing.section('search.groupby', {
|
||||
dependencies: ['web.search'],
|
||||
rpc: 'mock',
|
||||
templates: true,
|
||||
}, function (test) {
|
||||
test('basic', {
|
||||
asserts: 7,
|
||||
setup: function (instance, $s, mock) {
|
||||
mock('dummy.model:fields_view_get', function () {
|
||||
return {
|
||||
type: 'search',
|
||||
fields: {},
|
||||
arch: [
|
||||
'<search>',
|
||||
'<filter string="Foo" context="{\'group_by\': \'foo\'}"/>',
|
||||
'<filter string="Bar" context="{\'group_by\': \'bar\'}"/>',
|
||||
'<filter string="Baz" context="{\'group_by\': \'baz\'}"/>',
|
||||
'</search>'
|
||||
].join(''),
|
||||
}
|
||||
});
|
||||
}
|
||||
}, function (instance, $fix) {
|
||||
var view = makeSearchView(instance);
|
||||
return view.appendTo($fix)
|
||||
.done(function () {
|
||||
// 3 filters, 1 filtergroup group, 1 custom filter, 1 advanced, 1 Filters
|
||||
equal(view.inputs.length, 7,
|
||||
'should have 7 inputs total');
|
||||
var group = _.find(view.inputs, function (f) {
|
||||
return f instanceof instance.web.search.GroupbyGroup
|
||||
});
|
||||
ok(group, "should have a GroupbyGroup input");
|
||||
strictEqual(group.getParent(), view,
|
||||
"group's parent should be view");
|
||||
|
||||
group.toggle(group.filters[0]);
|
||||
group.toggle(group.filters[2]);
|
||||
|
||||
var results = view.build_search_data();
|
||||
deepEqual(results.errors, [], "should have no errors");
|
||||
deepEqual(results.domains, [], "should have no domain");
|
||||
deepEqual(results.contexts, [
|
||||
new instance.web.CompoundContext(
|
||||
"{'group_by': 'foo'}", "{'group_by': 'baz'}")
|
||||
], "should have compound contexts");
|
||||
deepEqual(results.groupbys, [
|
||||
"{'group_by': 'foo'}",
|
||||
"{'group_by': 'baz'}"
|
||||
], "should have sequence of contexts")
|
||||
});
|
||||
});
|
||||
test('unified multiple groupby groups', {
|
||||
asserts: 4,
|
||||
setup: function (instance, $s, mock) {
|
||||
mock('dummy.model:fields_view_get', function () {
|
||||
return {
|
||||
type: 'search',
|
||||
fields: {},
|
||||
arch: [
|
||||
'<search>',
|
||||
'<filter string="Foo" context="{\'group_by\': \'foo\'}"/>',
|
||||
'<separator/>',
|
||||
'<filter string="Bar" context="{\'group_by\': \'bar\'}"/>',
|
||||
'<separator/>',
|
||||
'<filter string="Baz" context="{\'group_by\': \'baz\'}"/>',
|
||||
'</search>'
|
||||
].join(''),
|
||||
}
|
||||
});
|
||||
}
|
||||
}, function (instance, $fix) {
|
||||
var view = makeSearchView(instance);
|
||||
return view.appendTo($fix)
|
||||
.done(function () {
|
||||
// 3 filters, 3 filtergroups, 1 custom filter, 1 advanced, 1 Filters
|
||||
equal(view.inputs.length, 9, "should have 9 inputs total");
|
||||
|
||||
var groups = _.filter(view.inputs, function (f) {
|
||||
return f instanceof instance.web.search.GroupbyGroup
|
||||
});
|
||||
equal(groups.length, 3, "should have 3 GroupbyGroups");
|
||||
|
||||
groups[0].toggle(groups[0].filters[0]);
|
||||
groups[2].toggle(groups[2].filters[0]);
|
||||
equal(view.query.length, 1,
|
||||
"should have unified groupby groups in single facet");
|
||||
deepEqual(view.build_search_data(), {
|
||||
errors: [],
|
||||
domains: [],
|
||||
contexts: [new instance.web.CompoundContext(
|
||||
"{'group_by': 'foo'}", "{'group_by': 'baz'}")],
|
||||
groupbys: [ "{'group_by': 'foo'}", "{'group_by': 'baz'}" ],
|
||||
}, "should only have contexts & groupbys in search data");
|
||||
});
|
||||
});
|
||||
});
|
||||
openerp.testing.section('search.filters.saved', {
|
||||
dependencies: ['web.search'],
|
||||
rpc: 'mock',
|
||||
templates: true
|
||||
|
@ -1127,8 +1224,30 @@ openerp.testing.section('saved_filters', {
|
|||
"should have selected second filter");
|
||||
});
|
||||
});
|
||||
test('creation', {asserts: 2}, function (instance, $fix, mock) {
|
||||
// force a user context
|
||||
instance.session.user_context = {foo: 'bar'};
|
||||
|
||||
var view = makeSearchView(instance);
|
||||
var done = $.Deferred();
|
||||
mock('ir.filters:get_filters', function () { return []; });
|
||||
mock('ir.filters:create_or_replace', function (args) {
|
||||
var filter = args[0];
|
||||
deepEqual(filter.context, {}, "should have empty context");
|
||||
deepEqual(filter.domain, [], "should have empty domain");
|
||||
done.resolve();
|
||||
});
|
||||
return view.appendTo($fix)
|
||||
.then(function () {
|
||||
$fix.find('.oe_searchview_custom input#oe_searchview_custom_input')
|
||||
.text("filter name")
|
||||
.end()
|
||||
.find('.oe_searchview_custom button').click();
|
||||
return done.promise();
|
||||
});
|
||||
});
|
||||
});
|
||||
openerp.testing.section('advanced', {
|
||||
openerp.testing.section('search.advanced', {
|
||||
dependencies: ['web.search'],
|
||||
rpc: 'mock',
|
||||
templates: true
|
||||
|
@ -1209,3 +1328,159 @@ openerp.testing.section('advanced', {
|
|||
});
|
||||
// TODO: UI tests?
|
||||
});
|
||||
openerp.testing.section('search.invisible', {
|
||||
dependencies: ['web.search'],
|
||||
rpc: 'mock',
|
||||
templates: true,
|
||||
}, function (test) {
|
||||
var registerTestField = function (instance, methods) {
|
||||
instance.web.search.fields.add('test', 'instance.testing.TestWidget');
|
||||
instance.testing = {
|
||||
TestWidget: instance.web.search.Field.extend(methods),
|
||||
};
|
||||
};
|
||||
var makeView = function (instance, mock, fields, arch, defaults) {
|
||||
mock('ir.filters:get_filters', function () { return []; });
|
||||
mock('test.model:fields_get', function () { return fields; });
|
||||
mock('test.model:fields_view_get', function () {
|
||||
return { type: 'search', fields: fields, arch: arch };
|
||||
});
|
||||
var ds = new instance.web.DataSet(null, 'test.model');
|
||||
return new instance.web.SearchView(null, ds, false, defaults);
|
||||
};
|
||||
// Invisible fields should not auto-complete
|
||||
test('invisible-field-no-autocomplete', {asserts: 1}, function (instance, $fix, mock) {
|
||||
registerTestField(instance, {
|
||||
complete: function () {
|
||||
return $.when([{label: this.attrs.string}]);
|
||||
},
|
||||
});
|
||||
var view = makeView(instance, mock, {
|
||||
field0: {type: 'test', string: 'Field 0'},
|
||||
field1: {type: 'test', string: 'Field 1'},
|
||||
}, ['<search>',
|
||||
'<field name="field0"/>',
|
||||
'<field name="field1" modifiers="{"invisible": true}"/>',
|
||||
'</search>'].join());
|
||||
return view.appendTo($fix)
|
||||
.then(function () {
|
||||
var done = $.Deferred();
|
||||
view.complete_global_search({term: 'test'}, function (comps) {
|
||||
done.resolve(comps);
|
||||
});
|
||||
return done;
|
||||
}).then(function (completions) {
|
||||
deepEqual(completions, [{label: 'Field 0'}],
|
||||
"should only complete the visible field");
|
||||
});
|
||||
});
|
||||
// Invisible filters should not appear in the drawer
|
||||
test('invisible-filter-no-drawer', {asserts: 4}, function (instance, $fix, mock) {
|
||||
var view = makeView(instance, mock, {}, [
|
||||
'<search>',
|
||||
'<filter string="filter 0"/>',
|
||||
'<filter string="filter 1" modifiers="{"invisible": true}"/>',
|
||||
'</search>'].join());
|
||||
return view.appendTo($fix)
|
||||
.then(function () {
|
||||
var $fs = $fix.find('.oe_searchview_filters ul');
|
||||
strictEqual($fs.children().length,
|
||||
1,
|
||||
"should only display one filter");
|
||||
strictEqual(_.str.trim($fs.children().text()),
|
||||
"filter 0",
|
||||
"should only display filter 0");
|
||||
var done = $.Deferred();
|
||||
view.complete_global_search({term: 'filter'}, function (comps) {
|
||||
done.resolve();
|
||||
strictEqual(comps.length, 1, "should only complete visible filter");
|
||||
strictEqual(comps[0].label, "Filter on: filter 0",
|
||||
"should complete filter 0");
|
||||
});
|
||||
return done;
|
||||
});
|
||||
});
|
||||
// Invisible filter groups should not appear in the drawer
|
||||
// Group invisibility should be inherited by children
|
||||
test('group-invisibility', {asserts: 6}, function (instance, $fix, mock) {
|
||||
registerTestField(instance, {
|
||||
complete: function () {
|
||||
return $.when([{label: this.attrs.string}]);
|
||||
},
|
||||
});
|
||||
var view = makeView(instance, mock, {
|
||||
field0: {type: 'test', string: 'Field 0'},
|
||||
field1: {type: 'test', string: 'Field 1'},
|
||||
}, [
|
||||
'<search>',
|
||||
'<group string="Visibles">',
|
||||
'<field name="field0"/>',
|
||||
'<filter string="Filter 0"/>',
|
||||
'</group>',
|
||||
'<group string="Invisibles" modifiers="{"invisible": true}">',
|
||||
'<field name="field1"/>',
|
||||
'<filter string="Filter 1"/>',
|
||||
'</group>',
|
||||
'</search>'
|
||||
].join(''));
|
||||
return view.appendTo($fix)
|
||||
.then(function () {
|
||||
strictEqual($fix.find('.oe_searchview_filters h3').length,
|
||||
1,
|
||||
"should only display one group");
|
||||
strictEqual($fix.find('.oe_searchview_filters h3').text(),
|
||||
'w Visibles',
|
||||
"should only display the Visibles group (and its icon char)");
|
||||
|
||||
var $fs = $fix.find('.oe_searchview_filters ul');
|
||||
strictEqual($fs.children().length, 1,
|
||||
"should only have one filter in the drawer");
|
||||
strictEqual(_.str.trim($fs.text()), "Filter 0",
|
||||
"should have filter 0 as sole filter");
|
||||
|
||||
var done = $.Deferred();
|
||||
view.complete_global_search({term: 'filter'}, function (compls) {
|
||||
done.resolve();
|
||||
strictEqual(compls.length, 2,
|
||||
"should have 2 completions");
|
||||
deepEqual(_.pluck(compls, 'label'),
|
||||
['Field 0', 'Filter on: Filter 0'],
|
||||
"should complete on field 0 and filter 0");
|
||||
});
|
||||
return done;
|
||||
});
|
||||
});
|
||||
// Default on invisible fields should still work, for fields and filters both
|
||||
test('invisible-defaults', {asserts: 1}, function (instance, $fix, mock) {
|
||||
var view = makeView(instance, mock, {
|
||||
field: {type: 'char', string: "Field"},
|
||||
field2: {type: 'char', string: "Field 2"},
|
||||
}, [
|
||||
'<search>',
|
||||
'<field name="field2"/>',
|
||||
'<filter name="filter2" string="Filter"',
|
||||
' domain="[[\'qwa\', \'=\', 42]]"/>',
|
||||
'<group string="Invisibles" modifiers="{"invisible": true}">',
|
||||
'<field name="field"/>',
|
||||
'<filter name="filter" string="Filter"',
|
||||
' domain="[[\'whee\', \'=\', \'42\']]"/>',
|
||||
'</group>',
|
||||
'</search>'
|
||||
].join(''), {field: "foo", filter: true});
|
||||
|
||||
return view.appendTo($fix)
|
||||
.then(function () {
|
||||
deepEqual(view.build_search_data(), {
|
||||
errors: [],
|
||||
groupbys: [],
|
||||
contexts: [],
|
||||
domains: [
|
||||
// Generated from field
|
||||
[['field', 'ilike', 'foo']],
|
||||
// generated from filter
|
||||
"[['whee', '=', '42']]"
|
||||
],
|
||||
}, "should yield invisible fields selected by defaults");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
.openerp a.dropdown-menu-icon {
|
||||
z-index: 10;
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
color: #4c4c4c;
|
||||
right: 8px;
|
||||
|
@ -23,7 +23,7 @@
|
|||
padding: 8px;
|
||||
border: 1px solid #afafb6;
|
||||
background: white;
|
||||
z-index: 900;
|
||||
z-index: 1;
|
||||
min-width: 160px;
|
||||
overflow-x: hidden;
|
||||
-moz-border-radius: 3px;
|
||||
|
|
|
@ -246,7 +246,7 @@ instance.web_graph.GraphView = instance.web.View.extend({
|
|||
var result = [];
|
||||
var ticks = {};
|
||||
|
||||
return this.alive(obj.call("fields_view_get", [view_id, 'graph']).then(function(tmp) {
|
||||
return this.alive(obj.call("fields_view_get", [view_id, 'graph', context]).then(function(tmp) {
|
||||
view_get = tmp;
|
||||
fields = view_get['fields'];
|
||||
var toload = _.select(group_by, function(x) { return fields[x] === undefined });
|
||||
|
|
|
@ -7,6 +7,15 @@
|
|||
background: url(/web/static/src/img/form_sheetbg.png);
|
||||
width: 100%;
|
||||
}
|
||||
.openerp .oe_kanban_view .oe_kanban_group_length {
|
||||
text-align: center;
|
||||
display: none;
|
||||
}
|
||||
.openerp .oe_kanban_view .oe_kanban_group_length .oe_tag {
|
||||
position: relative;
|
||||
top: 8px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.openerp .oe_kanban_view .ui-sortable-placeholder {
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
visibility: visible !important;
|
||||
|
@ -92,15 +101,6 @@
|
|||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.openerp .oe_kanban_view .oe_kanban_group_length {
|
||||
text-align: center;
|
||||
display: none;
|
||||
}
|
||||
.openerp .oe_kanban_view .oe_kanban_group_length .oe_tag {
|
||||
position: relative;
|
||||
top: +8px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.openerp .oe_kanban_view .oe_fold_column .oe_kanban_group_length {
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
|
@ -108,9 +108,6 @@
|
|||
float: right;
|
||||
display: block;
|
||||
}
|
||||
.openerp .oe_kanban_view .oe_kanban_header:hover .oe_kanban_group_length {
|
||||
display: none;
|
||||
}
|
||||
.openerp .oe_kanban_view.oe_kanban_grouped .oe_kanban_column, .openerp .oe_kanban_view.oe_kanban_grouped .oe_kanban_group_header {
|
||||
width: 185px;
|
||||
min-width: 185px;
|
||||
|
@ -150,8 +147,7 @@
|
|||
.openerp .oe_kanban_view .oe_kanban_group_folded .oe_kanban_group_title, .openerp .oe_kanban_view .oe_kanban_group_folded.oe_kanban_column *, .openerp .oe_kanban_view .oe_kanban_group_folded .oe_kanban_aggregates, .openerp .oe_kanban_view .oe_kanban_group_folded .oe_kanban_add {
|
||||
display: none;
|
||||
}
|
||||
.openerp .oe_kanban_view .oe_kanban_group_folded .oe_kanban_group_title_vertical,
|
||||
.openerp .oe_kanban_view .oe_kanban_group_folded .oe_kanban_group_length {
|
||||
.openerp .oe_kanban_view .oe_kanban_group_folded .oe_kanban_group_title_vertical, .openerp .oe_kanban_view .oe_kanban_group_folded .oe_kanban_group_length {
|
||||
display: block;
|
||||
}
|
||||
.openerp .oe_kanban_view .oe_kanban_group_folded .oe_dropdown_kanban {
|
||||
|
|
|
@ -130,8 +130,8 @@
|
|||
position: absolute
|
||||
top: -1px
|
||||
right: -14px
|
||||
display: block
|
||||
float: right
|
||||
display: block
|
||||
&.oe_kanban_grouped
|
||||
.oe_kanban_column, .oe_kanban_group_header
|
||||
width: 185px
|
||||
|
@ -197,7 +197,7 @@
|
|||
top: -8px
|
||||
.oe_kanban_header .oe_dropdown_toggle
|
||||
top: -2px
|
||||
height: 14px;
|
||||
height: 14px
|
||||
.oe_kanban_card, .oe_dropdown_toggle
|
||||
cursor: pointer
|
||||
display: inline-block
|
||||
|
|
Loading…
Reference in New Issue