[REF] web_kanban: new redesigned kanban view

This commit introduces an almost completely rewritten kanban view. It
was necessary in order to be able to render a different DOM to match
the new design.

It basically has the same features as before, but will render a
different DOM.  Three main differences, API wise, should be noted:
* the way the kanban view components communicate has been changed.  It now
  uses events, and event bubbling.
* kanban records do not have access to the view, so every call to this.view
  should be updated
* kanban templates are no longer wrapped in a div, so they should have a
  single root element for better result.
This commit is contained in:
Géry Debongnie
2015-06-15 16:30:34 +02:00
parent 5cb10d4fc9
commit ae583ba173
13 changed files with 2140 additions and 1589 deletions
+4 -4
View File
@@ -577,11 +577,11 @@
background: #ffffff;
}
.openerp .oe_popup_form .oe_kanban_buttons .oe_highlight {
.openerp .o_popup_form .oe_kanban_buttons .oe_highlight {
color: #404040;
background: none;
}
.openerp .oe_popup_form .oe_kanban_buttons button.oe_highlight {
.openerp .o_popup_form .oe_kanban_buttons button.oe_highlight {
background-color: #efefef;
background-image: -webkit-gradient(linear, left top, left bottom, from(#efefef), to(#d8d8d8));
background-image: -webkit-linear-gradient(top, #efefef, #d8d8d8);
@@ -593,7 +593,7 @@
-webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 1px rgba(255, 255, 255, 0.8) inset;
-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 1px rgba(255, 255, 255, 0.8) inset;
}
.openerp .oe_popup_form .oe_kanban_buttons button.oe_highlight:active {
.openerp .o_popup_form .oe_kanban_buttons button.oe_highlight:active {
background-color: #e3e3e3;
background-image: -webkit-gradient(linear, left top, left bottom, from(#e3e3e3), to(#f6f6f6));
background-image: -webkit-linear-gradient(top, #e3e3e3, #f6f6f6);
@@ -605,7 +605,7 @@
-webkit-box-shadow: none;
-box-shadow: none;
}
.openerp .oe_popup_form .oe_kanban_buttons button.oe_highlight:hover {
.openerp .o_popup_form .oe_kanban_buttons button.oe_highlight:hover {
background-color: #f6f6f6;
background-image: -webkit-gradient(linear, left top, left bottom, from(#f6f6f6), to(#e3e3e3));
background-image: -webkit-linear-gradient(top, #f6f6f6, #e3e3e3);
+1 -1
View File
@@ -514,7 +514,7 @@
padding: 0px
background: #ffffff
.openerp
.oe_popup_form
.o_popup_form
.oe_kanban_buttons
.oe_highlight
color: #404040
@@ -1,15 +1,17 @@
odoo.define('web_kanban.compatibility', function (require) {
"use strict";
var common = require('web_kanban.common');
var kanban_widgets = require('web_kanban.widgets');
var KanbanRecord = require('web_kanban.Record');
var KanbanColumn = require('web_kanban.Column');
var KanbanView = require('web_kanban.KanbanView');
return;
openerp = window.openerp || {};
openerp.web_kanban = openerp.web_kanban || {};
openerp.web_kanban.AbstractField = common.AbstractField;
openerp.web_kanban.KanbanGroup = common.KanbanGroup;
openerp.web_kanban.KanbanRecord = common.KanbanRecord;
openerp.web_kanban.AbstractField = kanban_widgets.AbstractField;
openerp.web_kanban.KanbanGroup = KanbanColumn;
openerp.web_kanban.KanbanRecord = KanbanRecord;
openerp.web_kanban.KanbanView = KanbanView;
});
@@ -0,0 +1,245 @@
odoo.define('web_kanban.Column', function (require) {
"use strict";
var core = require('web.core');
var Dialog = require('web.Dialog');
var form_common = require('web.form_common');
var Widget = require('web.Widget');
var quick_create = require('web_kanban.quick_create');
var KanbanRecord = require('web_kanban.Record');
var _t = core._t;
var QWeb = core.qweb;
var RecordQuickCreate = quick_create.RecordQuickCreate;
var KanbanColumn = Widget.extend({
template: "KanbanView.Group",
events: {
'click .o_kanban_toggle_fold': function (e) {
e.preventDefault();
e.stopPropagation();
this.folded = true;
this.update_column();
},
'click .o_column_edit': 'edit_column',
'click .o_column_delete': 'delete_column',
'click .o_kanban_quick_add': 'add_quick_create',
'click .o_kanban_load_more': 'load_more',
},
custom_events: {
'kanban_record_delete': 'delete_record',
'cancel_quick_create': 'cancel_quick_create',
'quick_create_add_record': 'quick_create_add_record',
},
init: function(parent, group_data, options, record_options) {
this._super(parent);
this.data_records = group_data.records;
this.dataset = group_data.dataset;
this.records = [];
this.title = group_data.title;
this.id = group_data.id;
this.folded = group_data.attributes.folded;
this.fields = options.fields;
this.values = group_data.values;
this.quick_create = options.quick_create;
this.grouped_by_m2o = options.grouped_by_m2o;
this.editable = options.editable;
this.deletable = options.deletable;
this.records_editable = options.records_editable;
this.records_deletable = options.records_deletable;
this.relation = options.relation;
this.offset = 0;
this.remaining = this.dataset.size() - this.data_records.length;
this.record_options = _.extend(_.clone(record_options), {
group_info: this.values,
});
},
start: function() {
var self = this;
this.$header = this.$('.o_kanban_header');
for (var i = 0; i < this.data_records.length; i++) {
this.add_record(this.data_records[i], {no_update: true});
}
this.$header.tooltip();
this.update_column();
this.$el.sortable({
connectWith: '.o_kanban_group',
revert: 150,
delay: 100,
items: '> .o_kanban_record',
helper: 'clone',
cursor: 'move',
over: function () {
self.folded = false;
self.update_column();
},
update: function (event, ui) {
var record = ui.item.data('record');
var index = self.records.indexOf(record);
var test2 = $.contains(self.$el[0], record.$el[0]);
if (index >= 0 && test2) {
// resequencing records
record.$el.removeAttr('style'); // jqueryui sortable add display:block inline
self.trigger_up('kanban_column_resequence');
} else if (index >= 0 && !test2) {
// removing record from this column
self.records.splice(self.records.indexOf(record), 1);
self.dataset.remove_ids([record.id]);
} else {
// adding record to this column
self.records.push(record);
record.setParent(self);
self.trigger_up('kanban_column_add_record', {record: record});
}
self.update_column();
}
});
this.$el.click(function (event) {
if (self.$el.hasClass('o_column_folded')) {
event.preventDefault();
self.folded = false;
self.update_column();
}
});
},
is_empty: function () {
return !this.records.length;
},
add_record: function (data, options) {
var record = new KanbanRecord(this, data, this.record_options);
this.records.push(record);
this.dataset.ids = _.uniq(this.dataset.ids.concat(record.id));
if (options.position === 'before') {
record.insertAfter(this.quick_create_widget ? this.quick_create_widget.$el : this.$header);
} else {
var $load_more = this.$('.o_kanban_load_more');
if ($load_more.length) {
record.insertBefore($load_more);
} else {
record.appendTo(this.$el);
}
}
if (!options.no_update) {
this.update_column();
}
},
get_ids: function () {
var ids = [];
this.$('.o_kanban_record').each(function (index, r) {
ids.push($(r).data('record').id);
});
return ids;
},
update_column: function () {
var title = this.folded ? this.title + ' (' + this.records.length + ')' : this.title;
this.$header.find('.o_column_title').text(title);
this.$header.find('.o-kanban-count').text(this.records.length);
this.$el.toggleClass('o_column_folded', this.folded);
var tooltip;
if (this.remaining) {
tooltip = this.records.length + '/' + this.dataset.size() + _t(' records');
} else {
tooltip = this.records.length + _t(' records');
}
this.$header.tooltip().attr('data-original-title', tooltip);
if (!this.remaining) {
this.$('.o_kanban_load_more').remove();
} else {
this.$('.o_kanban_load_more').html(QWeb.render('KanbanView.LoadMore', {widget:this}))
}
},
delete_column: function (event) {
event.preventDefault();
var buttons = [
{
text: _t("Ok"),
classes: 'btn-primary',
close: true,
click: this.trigger_up.bind(this, 'kanban_column_delete')
},
{text: _t("Cancel"), close: true}
];
new Dialog(this, {
size: 'medium',
buttons: buttons,
$content: $('<div>', {
text: _t("Are you sure that you want to remove this column ?")
}),
}).open();
},
edit_column: function (event) {
event.preventDefault();
var dialog = new form_common.FormViewDialog(this, {
res_model: this.relation,
res_id: this.id,
title: _t("Edit Column"),
}).open();
dialog.on('record_saved', this, function () {
this.trigger_up('kanban_reload');
});
},
delete_record: function (event) {
event.stopped = false;
var self = this;
event.data.after = function cleanup () {
var index = self.records.indexOf(event.data.record);
self.records.splice(index, 1);
self.update_column();
};
},
add_quick_create: function () {
if (this.quick_create_widget) {
return;
}
var self = this;
var width = this.records.length ? this.records[0].$el.innerWidth() : this.$el.width() - 8;
this.quick_create_widget = new RecordQuickCreate(this, width);
this.quick_create_widget.insertAfter(this.$header);
this.quick_create_widget.$el.focusout(function () {
setTimeout(function() {
var hasFocus = !! (self.quick_create_widget.$(':focus').length > 0);
if (! hasFocus && self.quick_create_widget) {
self.cancel_quick_create();
}
}, 10);
});
},
cancel_quick_create: function () {
this.quick_create_widget.destroy();
this.quick_create_widget = undefined;
},
quick_create_add_record: function (event) {
this.trigger_up('column_add_record', event.data);
},
load_more: function (event) {
event.preventDefault();
this.trigger_up('kanban_load_more');
},
});
return KanbanColumn;
});
@@ -1,928 +0,0 @@
odoo.define('web_kanban.common', function (require) {
"use strict";
var core = require('web.core');
var data = require('web.data');
var form_common = require('web.form_common');
var formats = require('web.formats');
var framework = require('web.framework');
var ProgressBar = require('web.ProgressBar');
var pyeval = require('web.pyeval');
var Registry = require('web.Registry');
var session = require('web.session');
var time = require('web.time');
var utils = require('web.utils');
var web_client = require('web.web_client');
var Widget = require('web.Widget');
var QWeb = core.qweb;
var _t = core._t;
/**
* Interface to be implemented by kanban fields.
*
*/
var FieldInterface = {
/**
Constructor.
- parent: The widget's parent.
- field: A dictionary giving details about the field, including the current field's value in the
raw_value field.
- $node: The field <field> tag as it appears in the view, encapsulated in a jQuery object.
*/
init: function(parent, field, $node) {},
};
/**
* Abstract class for classes implementing FieldInterface.
*
* Properties:
* - value: useful property to hold the value of the field. By default, the constructor
* sets value property.
*
*/
var AbstractField = Widget.extend(FieldInterface, {
/**
Constructor that saves the field and $node parameters and sets the "value" property.
*/
init: function(parent, field, $node) {
this._super(parent);
this.field = field;
this.$node = $node;
this.options = pyeval.py_eval(this.$node.attr("options") || '{}');
this.set("value", field.raw_value);
},
});
var Priority = AbstractField.extend({
init: function(parent, field, $node) {
this._super.apply(this, arguments);
this.name = $node.attr('name');
this.parent = parent;
},
prepare_priority: function() {
var self = this;
var selection = this.field.selection || [];
var init_value = selection && selection[0][0] || 0;
var data = _.map(selection.slice(1), function(element, index) {
var value = {
'value': element[0],
'name': element[1],
'click_value': element[0],
};
if (index === 0 && self.get('value') == element[0]) {
value.click_value = init_value;
}
return value;
});
return data;
},
renderElement: function() {
this.record_id = this.parent.id;
this.priorities = this.prepare_priority();
var readonly = this.field && this.field.readonly;
if (readonly){
this.set('readonly', true);
}
this.$el = $(QWeb.render("Priority", {'widget': this}));
if (!readonly){
this.$el.find('li').click(this.do_action.bind(this));
}
},
do_action: function(e) {
var self = this;
var li = $(e.target).closest( "li" );
if (li.length) {
var value = {};
value[self.name] = String(li.data('value'));
return self.parent.view.dataset._model.call('write', [[self.record_id], value, self.parent.view.dataset.get_context()]).done(self.reload_record.bind(self.parent));
}
},
reload_record: function() {
this.do_reload();
},
});
var KanbanSelection = AbstractField.extend({
init: function(parent, field, $node) {
this._super.apply(this, arguments);
this.name = $node.attr('name');
this.parent = parent;
},
prepare_dropdown_selection: function() {
var self = this;
var data = [];
_.map(this.field.selection || [], function(res) {
var value = {
'name': res[0],
'tooltip': res[1],
'state_name': res[1],
};
var leg_opt = self.options && self.options.states_legend || null;
if (leg_opt && leg_opt[res[0]] && self.parent.group.values && self.parent.group.values[leg_opt[res[0]]]) {
value['state_name'] = self.parent.group.values[leg_opt[res[0]]];
value['tooltip'] = self.parent.group.values[leg_opt[res[0]]];
}
if (res[0] == 'normal') { value['state_class'] = 'oe_kanban_status'; }
else if (res[0] == 'done') { value['state_class'] = 'oe_kanban_status oe_kanban_status_green'; }
else { value['state_class'] = 'oe_kanban_status oe_kanban_status_red'; }
data.push(value);
});
return data;
},
renderElement: function() {
var self = this;
this.record_id = self.parent.id;
this.states = self.prepare_dropdown_selection();
this.$el = $(QWeb.render("KanbanSelection", {'widget': self}));
this.$el.find('li').click(self.do_action.bind(self));
},
do_action: function(e) {
var self = this;
var li = $(e.target).closest( "li" );
if (li.length) {
var value = {};
value[self.name] = String(li.data('value'));
return self.parent.view.dataset._model.call('write', [[self.record_id], value, self.parent.view.dataset.get_context()]).done(self.reload_record.bind(self.parent));
}
},
reload_record: function() {
this.do_reload();
},
});
/**
* Quick creation view.
*
* Triggers a single event "added" with a single parameter "name", which is the
* name entered by the user
*
* @class
* @type {*}
*/
var QuickCreate = Widget.extend({
template: 'KanbanView.quick_create',
/**
* close_btn: If true, the widget will display a "Close" button able to trigger
* a "close" event.
*/
init: function(parent, dataset, context, buttons) {
this._super(parent);
this._dataset = dataset;
this._buttons = buttons || false;
this._context = context || {};
},
start: function () {
var self = this;
self.$input = this.$el.find('input');
self.$input.keyup(function(event){
if(event.keyCode == 13){
self.quick_add();
}
});
$(".oe_kanban_quick_create").focusout(function (e) {
var val = self.$el.find('input').val();
if (/^\s*$/.test(val)) { self.trigger('close'); }
e.stopImmediatePropagation();
});
$(".oe_kanban_quick_create_add", this.$el).click(function () {
self.quick_add();
self.focus();
});
$(".oe_kanban_quick_create_close", this.$el).click(function (ev) {
ev.preventDefault();
self.trigger('close');
});
self.$input.keyup(function(e) {
if (e.keyCode == 27 && self._buttons) {
self.trigger('close');
}
});
},
focus: function() {
this.$el.find('input').focus();
},
/**
* Handles user event from nested quick creation view
*/
quick_add: function () {
var self = this;
var val = this.$input.val();
if (/^\s*$/.test(val)) { this.$el.remove(); return; }
this._dataset.call(
'name_create', [val, new data.CompoundContext(
this._dataset.get_context(), this._context)])
.then(function(record) {
self.$input.val("");
self.trigger('added', record[0]);
}, function(error, event) {
event.preventDefault();
return self.slow_create();
});
},
slow_create: function() {
var self = this;
var popup = new form_common.SelectCreatePopup(this);
popup.select_element(
self._dataset.model,
{
title: _t("Create: ") + (this.string || this.name),
initial_view: "form",
disable_multiple_selection: true
},
[],
{"default_name": self.$input.val()}
);
popup.on("elements_selected", self, function(element_ids) {
self.$input.val("");
self.trigger('added', element_ids[0]);
});
}
});
var KanbanRecord = Widget.extend({
template: 'KanbanView.record',
events: {
'click .o_kanban_manage_toggle_button': 'toggle_manage_pane',
},
init: function (parent, record) {
this._super(parent);
this.group = parent;
this.view = parent.view;
this.id = null;
this.set_record(record);
if (!this.view.state.records[this.id]) {
this.view.state.records[this.id] = {
folded: false
};
}
this.state = this.view.state.records[this.id];
this.fields = {};
},
set_record: function(record) {
var self = this;
this.id = record.id;
this.values = {};
_.each(record, function(v, k) {
self.values[k] = {
value: v
};
});
this.record = this.transform_record(record);
},
start: function() {
var self = this;
this._super();
this.init_content();
},
init_content: function() {
var self = this;
self.sub_widgets = [];
this.$("[data-field_id]").each(function() {
self.add_widget($(this));
});
this.$el.data('widget', this);
this.bind_events();
},
transform_record: function(record) {
var self = this,
new_record = {};
_.each(record, function(value, name) {
var r = _.clone(self.view.fields_view.fields[name] || {});
if ((r.type === 'date' || r.type === 'datetime') && value) {
r.raw_value = time.auto_str_to_date(value);
} else {
r.raw_value = value;
}
r.value = formats.format_value(value, r);
new_record[name] = r;
});
return new_record;
},
renderElement: function() {
this.qweb_context = {
record: this.record,
widget: this,
read_only_mode: this.view.options.read_only_mode,
};
for (var p in this) {
if (_.str.startsWith(p, 'kanban_')) {
this.qweb_context[p] = _.bind(this[p], this);
}
}
var $el = QWeb.render(this.template, {
'widget': this,
'content': this.view.qweb.render('kanban-box', this.qweb_context)
});
this.replaceElement($el);
this.replace_fields();
},
replace_fields: function() {
var self = this;
this.$("field").each(function() {
var $field = $(this);
var $nfield = $("<span></span");
var id = _.uniqueId("kanbanfield");
self.fields[id] = $field;
$nfield.attr("data-field_id", id);
$field.replaceWith($nfield);
});
},
add_widget: function($node) {
var $orig = this.fields[$node.data("field_id")];
var field = this.record[$orig.attr("name")];
var type = field.type;
type = $orig.attr("widget") ? $orig.attr("widget") : type;
var obj = fields_registry.get(type);
var widget = new obj(this, field, $orig);
this.sub_widgets.push(widget);
widget.replace($node);
},
bind_events: function() {
var self = this;
this.setup_color_picker();
this.$el.find('[title]').each(function(){
$(this).tooltip({
delay: { show: 500, hide: 0},
title: function() {
var template = $(this).attr('tooltip');
if (!self.view.qweb.has_template(template)) {
return false;
}
return self.view.qweb.render(template, self.qweb_context);
},
});
});
// If no draghandle is found, make the whole card as draghandle (provided one can edit)
if (!this.$el.find('.oe_kanban_draghandle').length) {
this.$el.children(':first')
.toggleClass('oe_kanban_draghandle', this.view.is_action_enabled('edit'));
}
this.$el.find('.oe_kanban_action').click(function(ev) {
ev.preventDefault();
var $action = $(this),
type = $action.data('type') || 'button',
method = 'do_action_' + (type === 'action' ? 'object' : type);
if ((type === 'edit' || type === 'delete') && ! self.view.is_action_enabled(type)) {
self.view.open_record(self.id, true);
} else if (_.str.startsWith(type, 'switch_')) {
self.view.do_switch_view(type.substr(7));
} else if (typeof self[method] === 'function') {
self[method]($action);
} else {
self.do_warn("Kanban: no action for type : " + type);
}
});
if (this.$el.find('.oe_kanban_global_click,.oe_kanban_global_click_edit').length) {
this.$el.on('click', function(ev) {
if (!ev.isTrigger && !$._data(ev.target, 'events')) {
var trigger = true;
var elem = ev.target;
var ischild = true;
var children = [];
while (elem) {
var events = $._data(elem, 'events');
if (elem == ev.currentTarget) {
ischild = false;
}
if (ischild) {
children.push(elem);
if (events && events.click) {
// do not trigger global click if one child has a click event registered
trigger = false;
}
}
if (trigger && events && events.click) {
_.each(events.click, function(click_event) {
if (click_event.selector) {
// For each parent of original target, check if a
// delegated click is bound to any previously found children
_.each(children, function(child) {
if ($(child).is(click_event.selector)) {
trigger = false;
}
});
}
});
}
elem = elem.parentElement;
}
if (trigger) {
self.on_card_clicked(ev);
}
}
});
}
},
/* actions when user click on the block with a specific class
* open on normal view : oe_kanban_global_click
* open on form/edit view : oe_kanban_global_click_edit
*/
on_card_clicked: function(ev) {
if (this.$el.find('.oe_kanban_global_click').size() > 0 && this.$el.find('.oe_kanban_global_click').data('routing')) {
framework.redirect(this.$el.find('.oe_kanban_global_click').data('routing') + "/" + this.id);
}
else if (this.$el.find('.oe_kanban_global_click_edit').size()>0)
this.do_action_edit();
else
this.do_action_open();
},
setup_color_picker: function() {
var self = this;
var $el = this.$el.find('ul.oe_kanban_colorpicker');
if ($el.length) {
$el.html(QWeb.render('KanbanColorPicker', {
widget: this
}));
$el.on('click', 'a', function(ev) {
ev.preventDefault();
var color_field = $(this).parents('.oe_kanban_colorpicker').first().data('field') || 'color';
var data = {};
data[color_field] = $(this).data('color');
self.view.dataset.write(self.id, data, {}).done(function() {
self.record[color_field] = $(this).data('color');
self.do_reload();
});
});
}
},
toggle_manage_pane: function(){
this.$('.o_kanban_card_content').toggleClass('o_visible o_invisible');
this.$('.o_kanban_card_manage_pane').toggleClass('o_visible o_invisible');
this.$('.o_kanban_manage_button_section').toggleClass(this.kanban_color((this.values['color'] && this.values['color'].value) || 0));
},
do_action_delete: function($action) {
var self = this;
function do_it() {
return $.when(self.view.dataset.unlink([self.id])).done(function() {
self.group.remove_record(self.id);
self.destroy();
});
}
if (this.view.options.confirm_on_delete) {
if (confirm(_t("Are you sure you want to delete this record ?"))) {
return do_it();
}
} else
return do_it();
},
do_action_edit: function($action) {
this.view.open_record(this.id, true);
},
do_action_open: function($action) {
this.view.open_record(this.id);
},
do_action_object: function ($action) {
var button_attrs = $action.data();
var node_context = button_attrs.context || {};
var context = new data.CompoundContext(node_context);
context.set_eval_context({
active_id: this.id,
active_ids: [this.id],
active_model: this.view.dataset.model
});
var action_data = _.extend({}, button_attrs, {context: context});
this.view.do_execute_action(action_data, this.view.dataset, this.id, this.do_reload);
},
do_action_url: function($action) {
return framework.redirect($action.attr("href"));
},
do_reload: function() {
var self = this;
this.view.dataset.read_ids([this.id], this.view.fields_keys.concat(['__last_update'])).done(function(records) {
_.each(self.sub_widgets, function(el) {
el.destroy();
});
self.sub_widgets = [];
if (records.length) {
self.set_record(records[0]);
self.renderElement();
self.init_content();
self.group.compute_cards_auto_height();
self.view.postprocess_m2m_tags();
} else {
self.destroy();
}
});
},
kanban_getcolor: function(variable) {
var index = 0;
switch (typeof(variable)) {
case 'string':
for (var i=0, ii=variable.length; i<ii; i++) {
index += variable.charCodeAt(i);
}
break;
case 'number':
index = Math.round(variable);
break;
default:
return '';
}
var color = (index % this.view.number_of_color_schemes);
return color;
},
kanban_color: function(variable) {
var color = this.kanban_getcolor(variable);
return color === '' ? '' : 'oe_kanban_color_' + color;
},
kanban_image: function(model, field, id, cache, options) {
options = options || {};
var url;
if (this.record[field] && this.record[field].value && !utils.is_bin_size(this.record[field].value)) {
url = 'data:image/png;base64,' + this.record[field].value;
} else if (this.record[field] && ! this.record[field].value) {
url = "/web/static/src/img/placeholder.png";
} else {
id = JSON.stringify(id);
if (options.preview_image)
field = options.preview_image;
url = session.url('/web/binary/image', {model: model, field: field, id: id});
if (cache !== undefined) {
// Set the cache duration in seconds.
url += '&cache=' + parseInt(cache, 10);
}
}
return url;
},
kanban_text_ellipsis: function(s, size) {
size = size || 160;
if (!s) {
return '';
} else if (s.length <= size) {
return s;
} else {
return s.substr(0, size) + '...';
}
},
kanban_compute_domain: function(domain) {
return data.compute_domain(domain, this.values);
}
});
var KanbanGroup = Widget.extend({
template: 'KanbanView.group_header',
init: function (parent, records, group, dataset) {
var self = this;
this._super(parent);
this.$has_been_started = $.Deferred();
this.view = parent;
this.group = group;
this.dataset = dataset;
this.dataset_offset = 0;
this.value = this.title = this.values = null;
if (this.group) {
this.values = group.values;
this.value = group.get('value');
this.title = group.get('value');
if (this.value instanceof Array) {
this.title = this.value[1];
this.value = this.value[0];
}
var field = this.view.group_by_field;
if (!_.isEmpty(field)) {
try {
this.title = formats.format_value(group.get('value'), field, false);
} catch(e) {}
}
}
if (this.title === false) {
this.title = _t('Undefined');
this.undefined_title = true;
}
var key = this.view.group_by + '-' + this.value;
if (!this.view.state.groups[key]) {
this.view.state.groups[key] = {
folded: group ? group.get('folded') : false
};
}
this.state = this.view.state.groups[key];
this.$records = null;
this.records = [];
this.$has_been_started.done(function() {
self.do_add_records(records);
});
},
start: function() {
var self = this,
def = this._super();
if (! self.view.group_by) {
self.$el.addClass("oe_kanban_no_group");
var QuickCreateClass = self.view.get_quick_create_class();
self.quick = (new QuickCreateClass(this, self.dataset, {}, false))
.on('added', self, self.proxy('quick_created'));
self.quick.replace($(".oe_kanban_no_group_qc_placeholder"));
}
this.$records = $(QWeb.render('KanbanView.group_records_container', { widget : this}));
this.$records.insertBefore(this.view.$el.find('.oe_kanban_groups_records td:last'));
this.$el.on('click', '.oe_kanban_group_dropdown li a', function(ev) {
var fn = 'do_action_' + $(ev.target).data().action;
if (typeof(self[fn]) === 'function') {
self[fn]($(ev.target));
}
});
this.$el.find('.oe_kanban_add').click(function () {
if (self.view.quick) {
self.view.quick.trigger('close');
}
if (self.quick) {
return false;
}
self.view.$el.find('.oe_view_nocontent').hide();
var ctx = {};
ctx['default_' + self.view.group_by] = self.value;
self.quick = new (self.view.get_quick_create_class())(this, self.dataset, ctx, true)
.on('added', self, self.proxy('quick_created'))
.on('close', self, function() {
self.view.$el.find('.oe_view_nocontent').show();
this.quick.destroy();
delete self.view.quick;
delete this.quick;
});
self.quick.appendTo($(".oe_kanban_group_list_header", self.$records));
self.quick.focus();
self.view.quick = self.quick;
});
// Add bounce effect on image '+' of kanban header when click on empty space of kanban grouped column.
this.$records.on('click', '.oe_kanban_show_more', this.do_show_more);
if (this.state.folded) {
this.do_toggle_fold();
}
this.$el.data('widget', this);
this.$records.data('widget', this);
this.$has_been_started.resolve();
var add_btn = this.$el.find('.oe_kanban_add');
add_btn.tooltip({delay: { show: 500, hide:1000 }});
this.$records.find(".oe_kanban_column_cards").click(function (ev) {
if (ev.target == ev.currentTarget) {
if (!self.state.folded) {
add_btn.openerpBounce();
}
}
});
this.is_started = true;
this.fetch_tooltip();
return def;
},
/*
* Form the tooltip, based on optional group_by_tooltip on the grouping field.
* This function goes through the arch of the view, finding the declaration
* of the field used to group. If group_by_tooltip is defined, use the previously
* computed values of the group to form the tooltip. */
fetch_tooltip: function() {
var self = this;
if (! this.group)
return;
var options = null;
var recurse = function(node) {
if (node.tag === "field" && node.attrs.name == self.view.group_by) {
options = pyeval.py_eval(node.attrs.options || '{}');
return;
}
_.each(node.children, function(child) {
recurse(child);
});
};
recurse(this.view.fields_view.arch);
if (options && options.group_by_tooltip) {
this.tooltip = _.compact(
_.union(
[this.title],
_.map(
options.group_by_tooltip,
function (key, value, list) { return self.values && self.values[value] && (key + "\n" + self.values[value]) || ''; }))
).join('\n\n');
this.$(".oe_kanban_group_title_text").attr("title", this.tooltip || this.title || "");
}
},
compute_cards_auto_height: function() {
// oe_kanban_no_auto_height is an empty class used to disable this feature
if (!this.view.group_by) {
var min_height = 0;
var els = [];
_.each(this.records, function(r) {
var $e = r.$el.children(':first:not(.oe_kanban_no_auto_height)').css('min-height', 0);
if ($e.length) {
els.push($e[0]);
min_height = Math.max(min_height, $e.outerHeight());
}
});
$(els).css('min-height', min_height);
}
},
destroy: function() {
this._super();
if (this.$records) {
this.$records.remove();
}
},
do_show_more: function(evt) {
var self = this;
var ids = self.view.dataset.ids.slice(0);
return this.dataset.read_slice(this.view.fields_keys.concat(['__last_update']), {
'limit': self.view.limit,
'offset': self.dataset_offset += self.view.limit
}).then(function(records) {
self.view.dataset.ids = ids.concat(_.difference(self.dataset.ids, ids));
self.do_add_records(records);
self.compute_cards_auto_height();
self.view.postprocess_m2m_tags();
return records;
});
},
do_add_records: function(records, prepend) {
var self = this;
var $list_header = this.$records.find('.oe_kanban_group_list_header');
var $show_more = this.$records.find('.oe_kanban_show_more');
var $cards = this.$records.find('.oe_kanban_column_cards');
_.each(records, function(record) {
var rec = new KanbanRecord(self, record);
if (!prepend) {
rec.appendTo($cards);
self.records.push(rec);
} else {
rec.prependTo($cards);
self.records.unshift(rec);
}
});
if ($show_more.length) {
var size = this.dataset.size();
$show_more.toggle(this.records.length < size).find('.oe_kanban_remaining').text(size - this.records.length);
}
},
remove_record: function(id, remove_from_dataset) {
for (var i = 0; i < this.records.length; i++) {
if (this.records[i]['id'] === id) {
this.records.splice(i, 1);
i--;
}
}
},
do_toggle_fold: function(compute_width) {
this.$el.add(this.$records).toggleClass('oe_kanban_group_folded');
this.state.folded = this.$el.is('.oe_kanban_group_folded');
this.$("ul.oe_kanban_group_dropdown li a[data-action=toggle_fold]").text((this.state.folded) ? _t("Unfold") : _t("Fold"));
},
do_action_toggle_fold: function() {
this.do_toggle_fold();
},
do_action_edit: function() {
var self = this;
self.do_action({
res_id: this.value,
name: _t("Edit column"),
res_model: self.view.group_by_field.relation,
views: [[false, 'form']],
type: 'ir.actions.act_window',
target: "new",
flags: {
action_buttons: true,
headless: true,
}
});
// to do : remove this horrible hack
var am = web_client.action_manager;
var form = am.dialog_widget.views.form.controller;
form.on("on_button_cancel", am.dialog, function() { return am.dialog.$dialog_box.modal('hide'); });
form.on('record_saved', self, function() {
am.dialog.$dialog_box.modal('hide');
self.view.do_reload();
});
},
do_action_delete: function() {
var self = this;
if (confirm(_t("Are you sure to remove this column ?"))) {
(new data.DataSet(self, self.view.group_by_field.relation)).unlink([self.value]).done(function(r) {
self.view.do_reload();
});
}
},
do_save_sequences: function() {
var self = this;
if (_.indexOf(this.view.fields_keys, 'sequence') > -1) {
var new_sequence = _.pluck(this.records, 'id');
self.view.dataset.resequence(new_sequence);
}
},
/**
* Handles a newly created record
*
* @param {id} id of the newly created record
*/
quick_created: function (record) {
var id = record, self = this;
self.view.remove_no_result();
self.trigger("add_record");
this.dataset.read_ids([id], this.view.fields_keys)
.done(function (records) {
self.view.dataset.ids.push(id);
self.do_add_records(records, true);
});
},
highlight: function(show){
if(show){
this.$el.addClass('oe_kanban_column_higlight');
this.$records.addClass('oe_kanban_column_higlight');
}else{
this.$el.removeClass('oe_kanban_column_higlight');
this.$records.removeClass('oe_kanban_column_higlight');
}
}
});
/**
* Kanban widgets: ProgressBar
* options
* - editable: boolean if current_value is editable
* - current_value: get the current_value from the field that must be present in the view
* - max_value: get the max_value from the field that must be present in the view
* - title: title of the gauge, displayed on top of the gauge
* - on_change: action to call when cliking and setting a value
*/
var KanbanProgressBar = AbstractField.extend({
events: {
'click': function() {
if(!this.readonly && this.progressbar.readonly) {
this.toggle_progressbar();
}
}
},
init: function (parent, field, node) {
this._super(parent, field, node);
var record = this.getParent().record;
this.progressbar = new ProgressBar(this, {
readonly: true,
value: record[this.options.current_value].raw_value,
max_value: record[this.options.max_value].raw_value,
title: this.options.title,
edit_max_value: this.options.edit_max_value,
});
this.readonly = !this.options.editable;
this.on_change = this.options.on_change;
},
start: function () {
var self = this;
var def = this.progressbar.appendTo('<div>').done(function() {
self.progressbar.$el.addClass(self.$el.attr('class'));
self.replaceElement(self.progressbar.$el);
});
return $.when(this._super(), def).then(function() {
if(!self.readonly) {
var parent = self.getParent();
self.progressbar.on('change:value change:max_value', self, function(e) {
var value = this.progressbar.get(this.progressbar.edit_max_value ? 'max_value' : 'value') || 0;
if(!isNaN(value)) {
parent.view.dataset.call(this.on_change, [parent.id, value]).then(function() {
self.toggle_progressbar();
});
}
});
}
});
},
toggle_progressbar: function() {
this.progressbar.readonly = !this.progressbar.readonly;
var $div = $('<div/>').insertAfter(this.$el);
this.progressbar.replace($div);
this.setElement(this.progressbar.$el);
},
});
var fields_registry = new Registry();
fields_registry
.add('priority', Priority)
.add('kanban_state_selection', KanbanSelection)
.add("progress", KanbanProgressBar);
return {
AbstractField: AbstractField,
registry: fields_registry,
QuickCreate: QuickCreate,
KanbanRecord: KanbanRecord,
KanbanGroup: KanbanGroup,
};
});
@@ -0,0 +1,127 @@
odoo.define('web_kanban.quick_create', function (require) {
"use strict";
var Widget = require('web.Widget');
var RecordQuickCreate = Widget.extend({
template: "KanbanView.QuickCreate",
events: {
'click .o_kanban_cancel': 'cancel',
'click .o_kanban_add': 'add_record',
'keypress input': function (event) {
if (event.keyCode === 13) {
this.add_record();
}
},
'keydown': function (event) {
if (event.keyCode === 27) {
this.trigger_up('cancel_quick_create');
}
},
},
init: function (parent, width) {
this._super(parent);
this.width = width;
},
start: function () {
this.$el.css({width: this.width});
this.$input = this.$('input');
this.$input.focus();
},
cancel: function () {
this.trigger_up('cancel_quick_create');
},
add_record: function () {
var value = this.$input.val();
this.$input.val('');
if (/^\s*$/.test(value)) { return; }
this.trigger_up('quick_create_add_record', {value: value});
this.$input.focus();
},
});
var ColumnQuickCreate = Widget.extend({
template: 'KanbanView.ColumnQuickCreate',
events: {
'click .o_kanban_add': function (event) {
console.log('click okanbanadd');
event.stopPropagation();
this.add_column();
},
'click .o_kanban_quick_create': function (event) {
console.log('click quickcreate');
event.stopPropagation();
},
'keypress input': function (event) {
if (event.keyCode === 13) {
this.add_column();
}
},
'click .o_kanban_cancel': function (event) {
this.folded = true;
this.update();
},
'keydown': function (event) {
if (event.keyCode === 27) {
this.folded = true;
this.update();
}
},
},
init: function (parent) {
this._super(parent);
this.folded = true;
},
start: function () {
this.$header = this.$('.o_column_header');
this.$quick_create = this.$('.o_kanban_quick_create');
this.$input = this.$('input');
this.$el.click(this.proxy('toggle'));
var self = this;
this.$el.focusout(function () {
setTimeout(function() {
var hasFocus = !! (self.$(':focus').length > 0);
if (! hasFocus) {
self.folded = true;
self.update();
}
}, 10);
});
},
toggle: function () {
this.folded = !this.folded;
this.update();
if (!this.folded) {
this.$input.focus();
}
},
update: function () {
this.$header.toggle(this.folded);
this.$quick_create.toggle(!this.folded);
},
add_column: function () {
var name = this.$input.val();
this.$input.val('');
if (/^\s*$/.test(name)) { return; }
this.trigger_up('quick_create_add_column', {value: name});
this.$input.focus();
}
});
return {
RecordQuickCreate: RecordQuickCreate,
ColumnQuickCreate: ColumnQuickCreate,
};
});
@@ -0,0 +1,275 @@
odoo.define('web_kanban.Record', function (require) {
"use strict";
var core = require('web.core');
var data = require('web.data');
var formats = require('web.formats');
var framework = require('web.framework');
var session = require('web.session');
var time = require('web.time');
var utils = require('web.utils');
var Widget = require('web.Widget');
var kanban_widgets = require('web_kanban.widgets');
var QWeb = core.qweb;
var fields_registry = kanban_widgets.registry;
var KanbanRecord = Widget.extend({
template: 'KanbanView.record',
events: {
'click .oe_kanban_action': 'on_kanban_action_clicked',
'click .o_kanban_manage_toggle_button': 'toggle_manage_pane',
},
custom_events: {
'kanban_update_record': 'update_record',
},
init: function (parent, record, options) {
this._super(parent);
this.fields = options.fields;
this.editable = options.editable;
this.deletable = options.deletable;
this.draggable = options.draggable;
this.read_only_mode = options.read_only_mode;
this.model = options.model;
this.group_info = options.group_info;
this.qweb = options.qweb;
this.sub_widgets = [];
this.init_content(record);
},
init_content: function (record) {
var self = this;
this.id = record.id;
this.values = {};
_.each(record, function(v, k) {
self.values[k] = {
value: v
};
});
this.record = this.transform_record(record);
var qweb_context = {
record: this.record,
widget: this,
read_only_mode: this.read_only_mode,
};
for (var p in this) {
if (_.str.startsWith(p, 'kanban_')) {
qweb_context[p] = _.bind(this[p], this);
}
}
this.content = this.qweb.render('kanban-box', qweb_context);
},
add_widgets: function () {
var self = this;
this.$("field").each(function() {
var $field = $(this);
var field = self.record[$field.attr("name")];
var type = $field.attr("widget") || field.type;
var Widget = fields_registry.get(type);
var widget = new Widget(self, field, $field);
widget.replace($field);
self.sub_widgets.push(widget);
});
},
start: function() {
this.add_widgets();
},
renderElement: function () {
this._super();
this.setup_color_picker();
this.$el.addClass('o_kanban_record');
this.$el.data('record', this);
if (this.$el.hasClass('oe_kanban_global_click') || this.$el.hasClass('oe_kanban_global_click_edit')) {
this.$el.on('click', this.proxy('on_global_click'));
}
},
transform_record: function(record) {
var self = this;
var new_record = {};
_.each(record, function(value, name) {
var r = _.clone(self.fields[name] || {});
if ((r.type === 'date' || r.type === 'datetime') && value) {
r.raw_value = time.auto_str_to_date(value);
} else {
r.raw_value = value;
}
r.value = formats.format_value(value, r);
new_record[name] = r;
});
return new_record;
},
update: function (record) {
_.invoke(this.sub_widgets, 'destroy');
this.sub_widgets = [];
this.init_content(record);
this.renderElement();
this.add_widgets();
},
kanban_image: function(model, field, id, cache, options) {
options = options || {};
var url;
if (this.record[field] && this.record[field].value && !utils.is_bin_size(this.record[field].value)) {
url = 'data:image/png;base64,' + this.record[field].value;
} else if (this.record[field] && ! this.record[field].value) {
url = "/web/static/src/img/placeholder.png";
} else {
id = JSON.stringify(id);
if (options.preview_image)
field = options.preview_image;
url = session.url('/web/binary/image', {model: model, field: field, id: id});
if (cache !== undefined) {
// Set the cache duration in seconds.
url += '&cache=' + parseInt(cache, 10);
}
}
return url;
},
kanban_getcolor: function(variable) {
if (typeof(variable) === 'number') {
return Math.round(variable) % 10;
}
if (typeof(variable) === 'string') {
var index = 0;
for (var i=0; i<variable.length; i++) {
index += variable.charCodeAt(i);
}
return index % 10;
}
return 0;
},
kanban_color: function(variable) {
var color = this.kanban_getcolor(variable);
return 'oe_kanban_color_' + color;
},
on_global_click: function (ev) {
if ($(ev.target).parents('.o_dropdown_kanban').length) {
return;
}
if (!ev.isTrigger) {
var trigger = true;
var elem = ev.target;
var ischild = true;
var children = [];
while (elem) {
var events = $._data(elem, 'events');
if (elem == ev.currentTarget) {
ischild = false;
}
if (ischild) {
children.push(elem);
if (events && events.click) {
// do not trigger global click if one child has a click event registered
trigger = false;
}
}
if (trigger && events && events.click) {
_.each(events.click, function(click_event) {
if (click_event.selector) {
// For each parent of original target, check if a
// delegated click is bound to any previously found children
_.each(children, function(child) {
if ($(child).is(click_event.selector)) {
trigger = false;
}
});
}
});
}
elem = elem.parentElement;
}
if (trigger) {
this.on_card_clicked(ev);
}
}
},
/* actions when user click on the block with a specific class
* open on normal view : oe_kanban_global_click
* open on form/edit view : oe_kanban_global_click_edit
*/
on_card_clicked: function() {
if (this.$el.hasClass('oe_kanban_global_click_edit') && this.$el.data('routing')) {
framework.redirect(this.$el.data('routing') + "/" + this.id);
} else if (this.$el.hasClass('oe_kanban_global_click_edit')) {
this.trigger_up('kanban_record_edit', {id: this.id});
} else {
this.trigger_up('kanban_record_open', {id: this.id});
}
},
toggle_manage_pane: function(event){
event.preventDefault();
this.$('.o_kanban_card_content').toggleClass('o_visible o_invisible');
this.$('.o_kanban_card_manage_pane').toggleClass('o_visible o_invisible');
this.$('.o_kanban_manage_button_section').toggleClass(this.kanban_color((this.values['color'] && this.values['color'].value) || 0));
},
on_kanban_action_clicked: function (ev) {
ev.preventDefault();
var $action = $(ev.currentTarget);
var type = $action.data('type') || 'button';
switch (type) {
case 'edit':
this.trigger_up('kanban_record_edit', {id: this.id});
break;
case 'open':
this.trigger_up('kanban_record_open', {id: this.id});
break;
case 'delete':
this.trigger_up('kanban_record_delete', {record: this});
break;
case 'action':
case 'object':
this.trigger_up('kanban_do_action', $action.data());
break;
default:
this.do_warn("Kanban: no action for type : " + type);
}
},
kanban_compute_domain: function(domain) {
return data.compute_domain(domain, this.values);
},
update_record: function (event) {
this.trigger_up('kanban_record_update', event.data);
},
setup_color_picker: function() {
var self = this;
var $colorpicker = this.$('ul.oe_kanban_colorpicker');
if (!$colorpicker.length) {
return;
}
$colorpicker.html(QWeb.render('KanbanColorPicker', {
widget: this
}));
$colorpicker.on('click', 'a', function(ev) {
ev.preventDefault();
var color_field = $colorpicker.data('field') || 'color';
var data = {};
data[color_field] = $(this).data('color');
self.trigger_up('kanban_record_update', data);
});
},
});
return KanbanRecord;
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,247 @@
odoo.define('web_kanban.widgets', function (require) {
"use strict";
var core = require('web.core');
var ProgressBar = require('web.ProgressBar');
var pyeval = require('web.pyeval');
var Registry = require('web.Registry');
var session = require('web.session');
var Widget = require('web.Widget');
var QWeb = core.qweb;
/**
* Interface to be implemented by kanban fields.
*
*/
var FieldInterface = {
/**
Constructor.
- parent: The widget's parent.
- field: A dictionary giving details about the field, including the current field's value in the
raw_value field.
- $node: The field <field> tag as it appears in the view, encapsulated in a jQuery object.
*/
init: function(parent, field, $node) {},
};
/**
* Abstract class for classes implementing FieldInterface.
*
* Properties:
* - value: useful property to hold the value of the field. By default, the constructor
* sets value property.
*
*/
var AbstractField = Widget.extend(FieldInterface, {
/**
Constructor that saves the field and $node parameters and sets the "value" property.
*/
init: function(parent, field, $node) {
this._super(parent);
this.field = field;
this.$node = $node;
this.options = pyeval.py_eval(this.$node.attr("options") || '{}');
this.set("value", field.raw_value);
},
});
var Priority = AbstractField.extend({
init: function(parent, field, $node) {
this._super.apply(this, arguments);
this.name = $node.attr('name');
},
prepare_priority: function() {
var self = this;
var selection = this.field.selection || [];
var init_value = selection && selection[0][0] || 0;
var data = _.map(selection.slice(1), function(element, index) {
var value = {
'value': element[0],
'name': element[1],
'click_value': element[0],
};
if (index === 0 && self.get('value') == element[0]) {
value.click_value = init_value;
}
return value;
});
return data;
},
renderElement: function() {
this.priorities = this.prepare_priority();
var readonly = this.field && this.field.readonly;
if (readonly){
this.set('readonly', true);
}
this.$el = $(QWeb.render("Priority", {'widget': this}));
if (!readonly){
this.$el.on('click', 'a', this.do_action.bind(this));
}
},
do_action: function(event) {
event.preventDefault();
var $a = $(event.currentTarget);
var data = {};
data[this.name] = String($a.data('value'));
this.trigger_up('kanban_update_record', data);
},
});
var KanbanSelection = AbstractField.extend({
init: function(parent, field, $node) {
this._super.apply(this, arguments);
this.name = $node.attr('name');
this.parent = parent;
},
prepare_dropdown_selection: function() {
var self = this;
var data = [];
_.map(this.field.selection || [], function(res) {
var value = {
'name': res[0],
'tooltip': res[1],
'state_name': res[1],
};
var leg_opt = self.options && self.options.states_legend || null;
if (leg_opt) {
var key = leg_opt[value.name];
var legend = self.parent.group_info[key];
if (legend) {
value.state_name = legend;
value.tooltip = legend;
}
}
if (res[0] == 'normal') { value.state_class = 'oe_kanban_status'; }
else if (res[0] == 'done') { value.state_class = 'oe_kanban_status oe_kanban_status_green'; }
else { value.state_class = 'oe_kanban_status oe_kanban_status_red'; }
data.push(value);
});
return data;
},
renderElement: function() {
var self = this;
this.states = this.prepare_dropdown_selection();
var state;
for(var i = 0 ; i < this.states.length ; i++) {
if(this.states[i].name === this.get('value')) {
state = this.states[i];
}
}
this.$el = $(QWeb.render("KanbanSelection", {'widget': this}));
this.$('a').first().find('span').removeClass().addClass(state.state_class);
this.$('ul li').show().filter(function() {
return ($(this).data('value') === state.name);
}).hide();
this.$('a').click(function (ev) {
ev.preventDefault();
});
this.$('li').click(this.set_kanban_selection.bind(this));
},
set_kanban_selection: function(e) {
e.preventDefault();
var self = this;
var $li = $(e.target).closest( "li" );
if ($li.length) {
var value = {};
value[self.name] = String($li.data('value'));
this.trigger_up('kanban_update_record', value, this);
}
},
});
var KanbanAttachmentImage = AbstractField.extend({
template: 'KanbanAttachmentImage',
});
/**
* Kanban widgets: ProgressBar
* options
* - editable: boolean if current_value is editable
* - current_value: get the current_value from the field that must be present in the view
* - max_value: get the max_value from the field that must be present in the view
* - title: title of the gauge, displayed on top of the gauge
* - on_change: action to call when cliking and setting a value
*/
var KanbanProgressBar = AbstractField.extend({
events: {
'click': function() {
if(!this.readonly && this.progressbar.readonly) {
this.toggle_progressbar();
}
}
},
init: function (parent, field, node) {
this._super(parent, field, node);
var record = this.getParent().record;
this.progressbar = new ProgressBar(this, {
readonly: true,
value: record[this.options.current_value].raw_value,
max_value: record[this.options.max_value].raw_value,
title: this.options.title,
edit_max_value: this.options.edit_max_value,
});
this.readonly = !this.options.editable;
this.on_change = this.options.on_change;
},
start: function () {
var self = this;
var def = this.progressbar.appendTo('<div>').done(function() {
self.progressbar.$el.addClass(self.$el.attr('class'));
self.replaceElement(self.progressbar.$el);
});
return $.when(this._super(), def).then(function() {
if(!self.readonly) {
var parent = self.getParent();
self.progressbar.on('change:value change:max_value', self, function(e) {
var value = this.progressbar.get(this.progressbar.edit_max_value ? 'max_value' : 'value') || 0;
if(!isNaN(value)) {
var data = {
method: this.on_change,
params: [parent.id, value],
callback: self.proxy('toggle_progressbar'),
};
self.trigger_up('kanban_call_method', data);
// parent.view.dataset.call(this.on_change, [parent.id, value]).then(function() {
// self.toggle_progressbar();
// });
}
});
}
});
},
toggle_progressbar: function() {
this.progressbar.readonly = !this.progressbar.readonly;
var $div = $('<div/>').insertAfter(this.$el);
this.progressbar.replace($div);
this.setElement(this.progressbar.$el);
},
});
var fields_registry = new Registry();
fields_registry
.add('priority', Priority)
.add('kanban_state_selection', KanbanSelection)
.add("attachment_image", KanbanAttachmentImage)
.add('progress', KanbanProgressBar);
return {
AbstractField: AbstractField,
registry: fields_registry,
};
});
@@ -1,7 +1,7 @@
.oe_kanban_view {
.o_kanban_view {
&.o_kanban_mobile {
.oe_kanban_record {
.o_kanban_record {
width: 400px;
}
span {
@@ -14,8 +14,6 @@
&.o_kanban_dashboard {
@odoo-brand-optional: #21b799;
.o_visible {
width: 100%;
}
@@ -43,8 +41,8 @@
margin-right: 0px !important;
}
.oe_kanban_colorpicker {
min-height: 21px;
li {
height: 0px;
@@ -52,45 +50,37 @@
}
}
.oe_kanban_record {
padding: 0;
position: relative;
margin: 10px;
// @media (max-width: @grid-float-breakpoint) {
// margin: 10px !important;
// }
}
// if invisible, it will no take more than its original height
// If invisible, elem will no take more than its original height
a, span {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
position: relative;
width: 100%;
display: block;
}
.o_kanban_record {
padding: 0;
position: relative;
margin: 4px 8px;
}
a {
// color: @odoo-brand-optional;
color: @odoo-brand-optional;
}
.o_kanban_card_header {
display: flex;
display: -webkit-flex;
.o-flex-display();
background-color: lightgrey;
position: relative;
.o_kanban_card_header_title {
-webkit-flex: 3 0 0;
flex: 3 0 0;
padding: 8px;
.o-flex(3, 0, 0);
padding: 8px 16px;
.o_primary {
// color: @odoo-brand-optional;
font-variant: small-caps;
font-size: large;
color: @odoo-brand-optional;
font-weight: 700;
}
.o_secondary {
@@ -98,12 +88,11 @@
}
.o_kanban_manage_button_section {
-webkit-flex: 1 0 0;
flex: 1 0 0;
.o-flex(1, 0, 0);
padding: 16px;
.o_kanban_manage_toggle_button {
text-align: center;
text-align: right;
}
}
}
@@ -129,6 +118,7 @@
.o_kanban_card_manage_pane {
display: inline-block;
vertical-align: top;
padding: 0 8px;
}
.o_kanban_card_manage_section {
@@ -145,6 +135,7 @@
}
.o_kanban_card_manage_settings {
text-align: right;
padding: 8px;
border-color: lightgrey;
@@ -0,0 +1,500 @@
@o-kanban-record-margin: 4px;
.o_kanban_view {
.o-flex-display();
.o-align-content(stretch);
@media (max-width: @grid-float-breakpoint) {
padding: 0px;
}
.o_kanban_quick_create {
background-color: white;
margin: @o-kanban-record-margin;
box-shadow: 0px 0px 1px 1px lighten(@odoo-main-text-color, 10%);
padding: 3px;
input {
width: 100%;
margin: 4px 0;
}
}
.o_kanban_record {
min-width: 150px;
background-color: white;
margin: 4px 4px 8px 4px;
box-shadow: 0px 0px 1px 1px lighten(@odoo-main-text-color, 10%);
padding: 8px;
&.o_record_dragged {
background-color: @odoo-main-text-color;
}
&.oe_kanban_global_click, &.oe_kanban_global_click_edit {
cursor: pointer;
}
ul {
list-style-type: none;
padding: 0;
}
&.o_kanban_two_column {
.o-flex-display();
.oe_kanban_details {
position: relative;
display: inline-block;
.o-flex(1, 1, auto);
width: 0;
}
}
&.o_kanban_two_column_tags {
.o-flex-display();
.o-flex-flow(row, wrap);
.o_kanban_tags_section {
.o-flex(1, 1, 100%);
}
.o_kanban_image {
.o-flex(1, 1, 25%);
margin-right: 0;
> img {
box-shadow: none;
}
}
.oe_kanban_details {
position: relative;
.o-flex(1, 1, 75%);
ul {
margin-bottom: 0;
}
}
}
.o_kanban_title {
font-weight: bold;
margin-bottom: 3px;
}
.o_kanban_image {
.o-flex(0, 0, auto);
margin-right: 8px;
text-align: center;
img {
width: 64px;
height: 64px;
box-shadow: none;
}
}
.o_kanban_button {
margin-top: 15px;
> button, > a {
position: absolute;
bottom: 0;
right: 0;
}
}
&.ui-sortable-helper {
transform: rotate(3deg);
opacity: 0.9,
}
.o_dropdown_kanban {
float: right;
// Open the dropdown a the left side of the dropdown toggle
> .dropdown-menu {
left: auto;
right: 0;
padding: 3px;
}
> a {
padding: 3px;
color: @odoo-main-text-color;
}
&.open {
visibility: visible;
}
}
.o_kanban_footer {
.o-flex-display();
.o-justify-content(space-between);
> * {
flex: none;
}
}
&:hover {
.o_dropdown_kanban {
visibility: visible;
}
}
.o_dropdown_kanban {
@media (max-width: @grid-float-breakpoint) {
visibility: visible;
}
}
}
&.o_kanban_ungrouped {
.o-flex-flow(row, wrap);
.o-justify-content(flex-start);
padding: @o-kanban-record-margin @odoo-horizontal-padding - @o-kanban-record-margin;
@media (max-width: @grid-float-breakpoint) {
padding: 0;
}
.o_kanban_record {
.o-flex(1, 1, auto);
width: 320px;
@media (max-width: @grid-float-breakpoint) {
margin: 0 0 1px 0;
width: 100vw;
box-shadow: none;
}
}
.o_kanban_ghost {
height: 0;
min-height: 0 !important; // to prevent view writers to override this height
visibility: hidden;
margin-top: 0;
margin-bottom: 0;
padding: 0;
}
}
&.o_kanban_grouped {
min-height: 100%;
padding: 0 @odoo-horizontal-padding - @o-kanban-record-margin;
.o_kanban_record {
width: 300px;
&.o_kanban_small {
width: 240px;
}
}
}
.o_kanban_group {
.o-flex(0, 0, auto);
margin: 0 4px;
&.o_kanban_dragged {
background-color: @odoo-main-text-color;
.o_kanban_record {
visibility: hidden;
}
}
.o_kanban_header + .o_kanban_record {
margin-top: 0; // no margin top from the first card after kanban_header
}
.o_kanban_header {
color: white;
padding: 0 4px;
height: 50px;
line-height: 2.2;
cursor: move;
.o-flex-display();
.o_column_unfold {
display: none;
}
.o_column_title {
font-size: 18px;
font-weight: 300;
.o-flex(1, 1, auto);
white-space: nowrap;
padding-top: 5px;
}
.o-kanban-icon() {
// Share the style of `.o_kanban_quick_add` and `o_kanban_config`.
height: 50px;
width: 50px;
line-height: 1.8;
text-align: center;
color: white;
padding: 8px;
font-size: 22px;
cursor: pointer;
}
.o_kanban_quick_add i {
.o-kanban-icon;
&:hover {
background-color: @odoo-brand-optional;
}
}
.o_kanban_config i {
.o-kanban-icon;
&:hover {
background-color: darken(@gray-light, 10%);
}
}
.o_kanban_number {
color: @odoo-main-text-color;
text-align: center;
}
}
.o_kanban_record {
&.o_kanban_dragged {
> * {
visibility: hidden;
}
background-color: @odoo-main-text-color;
}
}
&.o_column_folded {
position: relative;
border-right: 1px solid @odoo-main-text-color;
margin: 0;
.o_column_title {
line-height: 1.3;
text-transform: uppercase;
transform-origin: left bottom;
transform: rotate(90deg);
top: 40px;
left: 15px;
position: absolute;
}
> .o_kanban_record, .o_kanban_quick_add, .o_kanban_config, .o_kanban_load_more {
display: none;
}
&:hover {
cursor: pointer;
background-color: darken(@gray-light, 10%);
}
.o_column_unfold {
display: block;
padding: 0 10px;
> i {
font-size: 22px;
padding-top: 15px;
}
}
background-color: @gray-light;
}
.o_kanban_load_more {
text-align: center;
a {
color: white;
font-weight: 300;
}
}
}
.oe_kanban_color_0 {
background-color: #FFFFFF;
color: #5a5a5a;
}
.oe_kanban_color_1 {
background-color: #d8d8d8;
color: #5e5e5e;
}
.oe_kanban_color_2 {
background-color: #e5d8e3;
color: #7a3737;
}
.oe_kanban_color_3 {
background-color: #ece9c2;
color: #756832;
}
.oe_kanban_color_4 {
background-color: #dee180;
color: #5d6937;
}
.oe_kanban_color_5 {
background-color: #abd4cc;
color: #1a7759;
}
.oe_kanban_color_6 {
background-color: #b8e3e8;
color: #1a5d83;
}
.oe_kanban_color_7 {
background-color: #f4ddab;
color: #5e5e5e;
}
.oe_kanban_color_8 {
background-color: #7f637d;
color: #a5a5a5;
}
.oe_kanban_color_9 {
background-color: #d8c5d5;
color: #5e5e5e;
}
.oe_kanban_color_10 {
background-color: #46C265;
color: #639B89;
}
.oe_tags {
display: block;
}
.o_kanban_tags {
margin-left: -8px;
.o_tag{
width: 50px;
height: 9px;
display: inline-block;
vertical-align: top;
}
}
.o_kanban_footer {
width: 100%;
}
.oe_kanban_avatar {
border-radius: 12px;
border: 1px solid @odoo-main-color-muted;
}
.o_progressbar {
.o_progressbar_title {
width: 20%;
}
.o_progress {
width: 60%;
height: 15px;
}
.o_progressbar_value {
width: 20%;
text-align: right;
}
}
.o_kanban_attachment_image {
// The image should take the hole card
margin: -8px;
> img {
width: 100%;
}
}
}
.oe_kanban_colorpicker {
li {
float: left;
&:first-child a {
border: 1px solid #ccc;
}
a {
width: 15px;
height: 16px;
display: inline-block;
border: 1px solid white;
}
}
}
.o_dropdown_kanban {
visibility: hidden;
}
.oe_kanban_avatar_smallbox {
height: 40px;
width: 40px;
box-shadow: 0 1px 4px @gray-light;
// border: 1px solid
// border-color: #e5e5e5 #dbdbdb #d2d2d2
// @include radius(3px)
// @include box-shadow(0 1px 2px rgba(0,0,0,0.2))
}
.o_kanban_shadow {
background-color: @odoo-main-text-color;
miw-width: 100px;
}
.o_column_quick_create {
.o-flex(0, 0, auto);
margin: 0;
padding: 8px;
position: relative;
background-color: @odoo-main-text-color;
cursor: pointer;
&:hover {
background-color: darken(@gray-light, 10%);
}
.o_column_header {
.o_kanban_add_column {
padding: 0 10px;
color: white;
> i {
font-size: 22px;
padding-top: 7px;
}
}
.o_kanban_title {
font-size: 18px;
font-weight: 300;
white-space: nowrap;
color: @gray-lighter;
text-transform: uppercase;
transform-origin: left bottom;
transform: rotate(90deg);
top: 40px;
left: 15px;
position: absolute;
}
}
.o_kanban_quick_create {
display: none;
width: 180px;
}
}
.oe_kanban_footer_left {
float: left;
}
.oe_kanban_bottom_right {
float: right;
}
.oe_kanban_bottom_left {
float: left;
}
.oe_kanban_status {
display: inline-block;
height: 12px;
width: 12px;
border-radius: 6px;
background-color: @odoo-brand-secondary;
&.oe_kanban_status_green {
background-color: @brand-success;
}
&.oe_kanban_status_red {
background-color: @brand-danger;
}
}
.o_form_field_one2many .oe_view_nocontent {
display: none;
}
+64 -80
View File
@@ -1,90 +1,72 @@
<template>
<t t-name="KanbanView">
<div class="oe_kanban_view">
<div class="oe_kanban_buttons"/>
<table class="oe_kanban_groups">
<tr class="oe_kanban_groups_headers">
<td class="oe_kanban_dummy_cell"><div/></td>
</tr>
<tr class="oe_kanban_groups_records">
<td class="oe_kanban_dummy_cell"><div/></td>
</tr>
</table>
</div>
</t>
<t t-name="KanbanView.buttons">
<t t-if="display">
<div class="oe_kanban_buttons">
<button type="button" class="oe_kanban_button_new btn btn-primary btn-sm">
<div>
<button type="button" class="btn btn-primary btn-sm o-kanban-button-new">
<t t-esc="widget.options.create_text || _t('Create')"/>
</button>
<span t-if="widget.is_action_enabled('group_create')" class="oe_alternative" style="display: none">
<span class="oe_fade">or</span>
<div class="oe_bounce_container">
<a href="#" class="oe_kanban_add_column text-info">Add a new column</a>
</div>
</span>
</div>
</t>
</t>
<t t-name="KanbanView.group_header">
<td class="oe_kanban_group_header">
<t t-if="widget.view.group_by">
<t t-set="parent" t-value="(widget.getParent())"/>
<div class="oe_kanban_header oe_semantic_html_override">
<t t-if="widget.view._is_quick_create_enabled()">
<div><i class="oe_kanban_add fa fa-plus" title="Quick create"/></div>
</t>
<div class="oe_dropdown_toggle oe_dropdown_kanban">
<div class="oe_kanban_group_length">
<span class="oe_tag">
<t t-if="widget.group.get('length') &gt; 99"> 99+ </t><t t-if="widget.group.get('length') &lt;= 99"> <t t-esc="widget.group.get('length')"/> </t>
</span>
</div>
<i class="fa fa-bars fa-lg"/>
<ul class="oe_dropdown_menu oe_kanban_group_dropdown">
<li><a data-action="toggle_fold" href="#">Fold</a></li>
<t t-if="widget.view.grouped_by_m2o and widget.value">
<li t-if="parent &amp;&amp; parent.is_action_enabled('group_edit')"><a data-action="edit" href="#">Edit</a></li>
<li t-if="parent &amp;&amp; parent.is_action_enabled('group_delete')"><a data-action="delete" href="#">Delete</a></li>
</t>
</ul>
</div>
<div class="oe_fold_column">
<div t-attf-class="oe_kanban_group_title #{widget.undefined_title ? 'oe_kanban_group_title_undefined' : ''}">
<div class="oe_kanban_group_title_text" t-att-title="widget.title"><t t-esc="widget.title"/></div>
</div>
<div class="oe_kanban_group_length oe_tag">
<t t-if="widget.group.get('length') &gt; 99"> 99+ </t><t t-if="widget.group.get('length') &lt;= 99"> <t t-esc="widget.group.get('length')"/> </t>
</div>
</div>
<span t-if="widget.title" class="oe_kanban_group_title_vertical">
<t t-esc="widget.title"/>
</span>
</div>
</t>
<t t-if="! widget.view.group_by &amp;&amp; widget.view._is_quick_create_enabled()">
<span class="oe_kanban_no_group_qc_placeholder"/>
</t>
</td>
</t>
<t t-name="KanbanView.group_records_container">
<td class="oe_kanban_column">
<div class="oe_kanban_group_list_header"/>
<div class="oe_kanban_column_cards"/>
<div class="oe_kanban_folded_column_cards"/>
<div class="oe_kanban_show_more">
<button class="oe_button">Show more... (<span class="oe_kanban_remaining"></span> remaining)</button>
<t t-name="KanbanView.Group">
<div class="o_kanban_group" t-att-data-id="widget.id">
<div class="o_kanban_header" t-att-title="widget.data_records.length + ' records'">
<span class="o_column_title"><t t-esc="widget.title"/></span>
<span class="o_column_unfold"><i class="fa fa-arrows-h"/></span>
<span class="o_kanban_config dropdown">
<a class="dropdown-toggle" data-toggle="dropdown" href="#"><i class="fa fa-gear"/></a>
<ul class="dropdown-menu" role="menu">
<li><a class="o_kanban_toggle_fold" href="#">Fold</a></li>
<t t-if="widget.grouped_by_m2o">
<li t-if="widget.editable and widget.id"><a class="o_column_edit" href="#">Edit</a></li>
<li t-if="widget.deletable and widget.id"><a class="o_column_delete" href="#">Delete</a></li>
</t>
</ul>
</span>
<t t-if="widget.quick_create">
<span class="o_kanban_quick_add"><i class="fa fa-plus"/></span>
</t>
</div>
<div t-if="widget.remaining > 0" class="o_kanban_load_more">
<t t-call="KanbanView.LoadMore"/>
</div>
</td>
</t>
<t t-name="KanbanView.record">
<div class="oe_fold_column oe_kanban_record">
<t t-raw="content"/>
</div>
</t>
<t t-name="KanbanView.LoadMore">
<a href="#">Load more... (<t t-esc="widget.remaining"/> remaining)</a>
</t>
<t t-name="KanbanView.ColumnQuickCreate">
<div class="o_column_quick_create">
<div class="o_column_header">
<span class="o_kanban_add_column"><i class="fa fa-chevron-right"/></span>
<span class="o_kanban_title">Add new Column</span>
</div>
<div class="o_kanban_quick_create">
<input type="text" placeholder="Add"/>
<button class="btn btn-primary o_kanban_add">Add</button>
<button class="btn btn-default o_kanban_cancel">Cancel</button>
</div>
</div>
</t>
<t t-name="KanbanView.record">
<t t-raw="widget.content"/>
</t>
<t t-name="KanbanView.QuickCreate">
<div class="o_kanban_quick_create">
<input type="text" placeholder="Add"/>
<button class="btn btn-primary o_kanban_add">Add</button>
<button class="btn btn-default o_kanban_cancel">Cancel</button>
</div>
</t>
<t t-name="KanbanColorPicker">
<li t-foreach="widget.view.number_of_color_schemes" t-as="color">
<li t-foreach="10" t-as="color">
<a href="#" t-att-data-color="color_index" t-attf-class="oe_kanban_color_#{color}"/>
</li>
</t>
@@ -98,11 +80,13 @@
</t>
</div>
<t t-name="KanbanView.nocontent">
<div class="oe_view_nocontent" style="position:absolute;">
<div class="oe_view_nocontent_bg"></div>
<div class="oe_view_nocontent_content">
<t t-raw="content"/>
</div>
<div class="oe_view_nocontent">
<t t-raw="content"/>
</div>
</t>
<t t-name="KanbanAttachmentImage">
<div class="o_kanban_attachment_image">
<img t-att-src="'/web/binary/image?model=ir.attachment&amp;field=datas&amp;id=' + widget.field.raw_value[0]"/>
</div>
</t>
</template>
+5 -2
View File
@@ -3,11 +3,14 @@
<data>
<template id="assets_backend" name="web_kanban assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<link rel="stylesheet" href="/web_kanban/static/src/css/kanban.css"/>
<link rel="stylesheet" href="/web_kanban/static/src/less/kanban_dashboard.less"/>
<link rel="stylesheet" href="/web_kanban/static/src/less/kanban_view.less"/>
<script type="text/javascript" src="/web_kanban/static/src/js/kanban_record.js"></script>
<script type="text/javascript" src="/web_kanban/static/src/js/kanban_quick_create.js"></script>
<script type="text/javascript" src="/web_kanban/static/src/js/kanban_column.js"></script>
<script type="text/javascript" src="/web_kanban/static/src/js/kanban_view.js"></script>
<script type="text/javascript" src="/web_kanban/static/src/js/kanban_common.js"></script>
<script type="text/javascript" src="/web_kanban/static/src/js/kanban_widgets.js"></script>
<script type="text/javascript" src="/web_kanban/static/src/js/compatibility.js"></script>
</xpath>
</template>