[MERGE] web,*: Kanban QuickCreate widgets improvement

*: mainly crm, hr_recruitment and project

This branch improves the RecordQuickCreate and ColumnQuickCreate
widgets of the Kanban view, in order to ease user onboarding.

The RecordQuickCreate is no longer limited to the display_name: it
instantiates a real form view, whose xmlid has to be referenced in the
root node of the Kanban arch (attribute 'quick_create_view'). This
branch already defines quick create form views for models 'crm.lead',
'hr.applicant' and 'project.task'.

The ColumnQuickCreate's main change is that it may now contain a link
leading to a dialog with examples of how to configure the columns (by
specifying a JS registry key under attribute 'examples' in the root node
of the Kanban arch, and by defining the examples to display in the
dialog in that registry). Examples have already been specified for the
'project.task' Kanban view. Several other tweaks have been done on the
ColumnQuickCreate (e.g. it now opens directly in grouped Kanban views
with no column, its style has been improved...)

Tasks #27475 and #38740.
This commit is contained in:
Aaron Bohy
2018-01-26 20:24:55 +01:00
41 changed files with 1943 additions and 615 deletions
+6 -6
View File
@@ -18,13 +18,13 @@ tour.register('crm_tour', {
content: _t("Click here to <b>create your first opportunity</b> and add it to your pipeline."),
position: "bottom",
}, {
trigger: ".modal-body input:first",
auto: true
trigger: ".o_kanban_quick_create input:first",
content: _t("<b>Choose a name</b> for your opportunity."),
position: "right",
}, {
trigger: ".modal-footer button:first",
extra_trigger: '.o_opportunity_kanban',
content: _t("Create the opportunity."),
position: "bottom",
trigger: ".o_kanban_quick_create .o_kanban_add",
content: _t("Click here to <b>add your opportunity</b>."),
position: "right",
}, {
trigger: ".o_opportunity_kanban .o_kanban_group:first-child .o_kanban_record:last-child",
content: _t("<b>Drag &amp; drop opportunities</b> between columns as you progress in your sales cycle."),
+17 -2
View File
@@ -281,12 +281,28 @@
<field name="target">new</field>
</record>
<record id="quick_create_opportunity_form" model="ir.ui.view">
<field name="name">crm.lead.form.quick_create</field>
<field name="model">crm.lead</field>
<field name="priority">1000</field>
<field name="arch" type="xml">
<form>
<group>
<field name="name"/>
<field name="partner_id" domain="[('customer', '=', True)]" context="{'search_default_customer': 1}"/>
<field name="planned_revenue"/>
<field name="priority" nolabel="1" widget="priority"/>
</group>
</form>
</field>
</record>
<record id="crm_case_kanban_view_leads" model="ir.ui.view">
<field name="name">crm.lead.kanban.lead</field>
<field name="model">crm.lead</field>
<field name="priority" eval="1"/>
<field name="arch" type="xml">
<kanban default_group_by="stage_id" class="o_kanban_small_column o_opportunity_kanban" on_create="crm.create_opportunity_simplified">
<kanban default_group_by="stage_id" class="o_kanban_small_column o_opportunity_kanban" on_create="quick_create" quick_create_view="crm.quick_create_opportunity_form">
<field name="stage_id" options='{"group_by_tooltip": {"requirements": "Description", "legend_priority": "Use of stars"}}'/>
<field name="color"/>
<field name="priority"/>
@@ -305,7 +321,6 @@
<field name="activity_ids" />
<progressbar field="activity_state" colors='{"planned": "success", "today": "warning", "overdue": "danger"}' sum_field="planned_revenue" help="This bar allows to filter the opportunities based on scheduled activities."/>
<templates>
<field name="date_deadline"/>
<t t-name="kanban-box">
<div t-attf-class="#{kanban_color(record.color.raw_value)} oe_kanban_global_click">
<div class="o_dropdown_kanban dropdown">
@@ -240,12 +240,28 @@
</field>
</record>
<record id="quick_create_applicant_form" model="ir.ui.view">
<field name="name">hr.applicant.form.quick_create</field>
<field name="model">hr.applicant</field>
<field name="priority">1000</field>
<field name="arch" type="xml">
<form>
<group>
<field name="name"/>
<field name="partner_name"/>
<field name="email_from"/>
<field name="job_id" options="{'no_open': True}"/>
</group>
</form>
</field>
</record>
<!-- Hr Applicant Kanban View -->
<record model="ir.ui.view" id="hr_kanban_view_applicant">
<field name="name">Hr Applicants kanban</field>
<field name="model">hr.applicant</field>
<field name="arch" type="xml">
<kanban default_group_by="stage_id" class="o_kanban_applicant">
<kanban default_group_by="stage_id" class="o_kanban_applicant" quick_create_view="hr_recruitment.quick_create_applicant_form">
<field name="stage_id" options='{"group_by_tooltip": {"requirements": "Requirements"}}'/>
<field name="color"/>
<field name="priority"/>
@@ -78,11 +78,10 @@ var LivechatButton = Widget.extend({
start: function () {
this.$el.text(this.options.button_text);
var small_screen = config.device.size_class === config.device.SIZES.XS;
if (this.history) {
_.each(this.history.reverse(), this.add_message.bind(this));
this.open_chat();
} else if (!small_screen && this.rule.action === 'auto_popup') {
} else if (!config.device.isMobile && this.rule.action === 'auto_popup') {
var auto_popup_cookie = utils.get_cookie('im_livechat_auto_popup');
if (!auto_popup_cookie || JSON.parse(auto_popup_cookie)) {
this.auto_popup_timeout = setTimeout(this.open_chat.bind(this), this.rule.auto_popup_timer*1000);
+1 -1
View File
@@ -150,7 +150,7 @@ return Widget.extend({
this.trigger("close_chat_session");
},
on_click_fold: function () {
if (config.device.size_class !== config.device.SIZES.XS) {
if (!config.device.isMobile) {
this.toggle_fold();
this.trigger("fold_channel", this.channel_id, this.folded);
}
+1 -1
View File
@@ -193,7 +193,7 @@ function toggle_fold_chat (channel) {
}
function compute_available_slots (nb_windows) {
if (config.device.size_class === config.device.SIZES.XS) {
if (config.device.isMobile) {
display_state.nb_slots = 1; // one chat window full screen in mobile
return;
}
@@ -0,0 +1,73 @@
odoo.define('project.task_kanban_examples', function (require) {
'use strict';
var core = require('web.core');
var kanbanExamplesRegistry = require('web.kanban_examples_registry');
var _t = core._t;
var greenBullet = '<span class="o_status o_status_green"></span>';
var redBullet = '<span class="o_status o_status_red"></span>';
var star = '<a style="color: gold;" class="fa fa-star"/>';
/**
* Helper function to escape a text before formatting it.
*
* First argument is the string to format and the other arguments are the values
* to inject into the string.
*
* @returns {string} the formatted and escaped string
*/
function escFormat() {
arguments[0] = _.escape(arguments[0]);
return _.str.sprintf.apply(_.str, arguments);
}
kanbanExamplesRegistry.add('project', [{
name: _t('Software Development'),
columns: [_t('Backlog'), _t('Specifications'), _t('Development'), _t('Tests'), _t('Delivered')],
description: escFormat(_t('Once a task is specified, set it %s in the Specifications ' +
'column, so that developers know they can pull it. If you work in sprints, use %s to ' +
'mark tasks of the current sprint.'), greenBullet, star),
}, {
name: _t('Agile'),
columns: [_t('Backlog'), _t('Analysis'), _t('Development'), _t('Testing'), _t('Done')],
description: escFormat(_t('Waiting for the next stage: use %s and %s bullets.'), greenBullet, redBullet),
}, {
name: _t('Digital Marketing'),
columns: [_t('Ideas'), _t('Researching'), _t('Writing'), _t('Editing'), _t('Done')],
description: escFormat(_t('Everyone can propose ideas, and the Editor marks the best ones ' +
'as %s. Attach all documents or links to the task directly, to have all information about ' +
'a research centralized.'), greenBullet),
}, {
name: _t('Customer Feedback'),
columns: [_t('New'), _t('In development'), _t('Done'), _t('Refused')],
description: escFormat(_t('Customers propose feedbacks by email; Odoo creates tasks ' +
'automatically, and you can communicate on the task directly. Your managers decide which ' +
'feedback is accepted %s and which feedback is moved to the "Refused" column.'), greenBullet),
}, {
name: _t('Getting Things Done (GTD)'),
columns: [_t('Inbox'), _t('Today'), _t('This Week'), _t('This Month'), _t('Long Term')],
description: _t('Fill your Inbox easily with the email gateway. Periodically review your ' +
'Inbox and schedule tasks by moving them to others columns. Every day, you review the ' +
'"This Week" column to move important tasks "Today". Every Monday, you review the "This ' +
'Month" column.'),
}, {
name: _t('Consulting'),
columns: [_t('New Projects'), _t('Resources Allocation'), _t('In Progress'), _t('Done')],
}, {
name: _t('Research Project'),
columns: [_t('Brainstorm'), _t('Research'), _t('Draft'), _t('Final Document')],
}, {
name: _t('Website Redesign'),
columns: [_t('Page Ideas'), _t('Copywriting'), _t('Design'), _t('Live')],
}, {
name: _t('T-shirt Printing'),
columns: [_t('New Orders'), _t('Logo Design'), _t('To Print'), _t('Done')],
description: escFormat(_t('Communicate with customers on the task using the email gateway. ' +
'Attach logo designs to the task, so that information flow from designers to the workers ' +
'who print the t-shirt. Organize priorities amongst orders %s using the icon.'), star),
}]);
});
+7 -18
View File
@@ -22,9 +22,6 @@ tour.register('project_tour', {
trigger: 'input.o_project_name',
content: _t('Choose a <b>project name</b>. (e.g. Website Launch, Product Development, Office Party, etc.)'),
position: 'right',
run: function (actions) {
actions.auto();
},
}, {
trigger: '.o_open_tasks',
content: _t('This will create new project and redirect us to its tasks.'),
@@ -33,14 +30,12 @@ tour.register('project_tour', {
actions.auto(".modal:visible .btn.btn-primary");
},
}, {
trigger: ".o_kanban_project_tasks .o_column_quick_create",
trigger: ".o_kanban_project_tasks .o_column_quick_create input",
content: _t("Add columns to configure <b>stages for your tasks</b>.<br/><i>e.g. Specification &gt; Development &gt; Done</i>"),
position: "right"
}, {
trigger: ".o-kanban-button-new",
extra_trigger: '.o_kanban_project_tasks',
content: _t("Now that the project is set up, <b>create a few tasks</b>."),
position: "right"
trigger: ".o_kanban_project_tasks .o_column_quick_create .o_kanban_add",
auto: true,
}, {
trigger: '.o-kanban-button-new',
extra_trigger: '.o_kanban_project_tasks',
@@ -48,21 +43,15 @@ tour.register('project_tour', {
position: 'right',
width: 200,
}, {
trigger: 'input.o_task_name',
trigger: '.o_kanban_quick_create input.o_field_char[name=name]',
extra_trigger: '.o_kanban_project_tasks',
content: _t('Choose a <b>task name</b>. (e.g. Website Design, Purchase Goods etc.)'),
position: 'right',
run: function (actions) {
actions.auto();
},
}, {
trigger: '.o_form_button_save',
trigger: '.o_kanban_quick_create .o_kanban_add',
extra_trigger: '.o_kanban_project_tasks',
content: _t("<p>Once your task is ready, you can save it.</p>"),
position: 'bottom',
}, {
trigger: ".breadcrumb li:not(.active):last",
extra_trigger: '.o_form_project_tasks.o_form_readonly',
content: _t("Use the breadcrumbs to <b>go back to your tasks pipeline</b>."),
position: "bottom"
}, {
trigger: ".o_kanban_record .o_priority_star",
extra_trigger: '.o_kanban_project_tasks',
+1
View File
@@ -4,6 +4,7 @@
<xpath expr="." position="inside">
<link rel="stylesheet" href="/project/static/src/css/project.css"/>
<script type="text/javascript" src="/project/static/src/js/project.js"></script>
<script type="text/javascript" src="/project/static/src/js/project_task_kanban_examples.js"></script>
<script type="text/javascript" src="/project/static/src/js/tour.js"></script>
<link rel="stylesheet" href="/project/static/src/less/project_dashboard.less"/>
</xpath>
+15 -1
View File
@@ -502,12 +502,26 @@
</field>
</record>
<record id="quick_create_task_form" model="ir.ui.view">
<field name="name">project.task.form.quick_create</field>
<field name="model">project.task</field>
<field name="priority">1000</field>
<field name="arch" type="xml">
<form>
<group>
<field name="name"/>
<field name="user_id" options="{'no_open': True}"/>
</group>
</form>
</field>
</record>
<!-- Project Task Kanban View -->
<record model="ir.ui.view" id="view_task_kanban">
<field name="name">project.task.kanban</field>
<field name="model">project.task</field>
<field name="arch" type="xml">
<kanban default_group_by="stage_id" class="o_kanban_small_column o_kanban_project_tasks" on_create="quick_create">
<kanban default_group_by="stage_id" class="o_kanban_small_column o_kanban_project_tasks" on_create="quick_create" quick_create_view="project.quick_create_task_form" examples="project">
<field name="color"/>
<field name="priority"/>
<field name="stage_id" options='{"group_by_tooltip": {"description": "Stage Description", "legend_priority": "Use of stars"}}'/>
@@ -1037,7 +1037,7 @@ var FieldPhone = FieldEmail.extend({
* @private
*/
_canCall: function () {
return config.device.size_class <= config.device.SIZES.XS;
return config.device.isMobile;
}
});
+7 -6
View File
@@ -1,4 +1,4 @@
odoo.define('web.config', function (require) {
odoo.define('web.config', function () {
"use strict";
/**
@@ -31,7 +31,7 @@ var config = {
*/
touch: 'ontouchstart' in window || 'onmsgesturechange' in window,
/**
* size_class is an integer: 0, 1, 2 or 3, depending on the (current)
* size_class is an integer: 0, 1, 2, 3 or 4, depending on the (current)
* size of the device. This is a dynamic property, updated whenever the
* browser is resized
*
@@ -41,21 +41,22 @@ var config = {
/**
* A frequent use case is to have a different render in 'mobile' mode,
* meaning when the screen is small. This flag (boolean) is true when
* the size is not 3. It is also updated dynamically.
* the size is XXS or XS. It is also updated dynamically.
*
* @type Boolean
*/
isMobile: null,
/**
* Mapping between the numbers 0,1,2,3 and some descriptions
* Mapping between the numbers 0,1,2,3,4 and some descriptions
*/
SIZES: { XS: 0, SM: 1, MD: 2, LG: 3 },
SIZES: { XXS: 0, XS: 1, SM: 2, MD: 3, LG: 4 },
},
};
var medias = [
window.matchMedia('(max-width: 767px)'),
window.matchMedia('(max-width: 474px)'),
window.matchMedia('(min-width: 475px) and (max-width: 767px)'),
window.matchMedia('(min-width: 768px) and (max-width: 991px)'),
window.matchMedia('(min-width: 992px) and (max-width: 1199px)'),
window.matchMedia('(min-width: 1200px)')
@@ -68,7 +68,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
* rejected otherwise
*/
canBeDiscarded: function (recordID) {
if (!this.model.isDirty(recordID || this.handle)) {
if (!this.isDirty(recordID)) {
return $.when(false);
}
@@ -120,6 +120,15 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
getSelectedIds: function () {
return [];
},
/**
* Returns true iff the given recordID (or the main recordID) is dirty.
*
* @param {string} [recordID] - default to main recordID
* @returns {boolean}
*/
isDirty: function (recordID) {
return this.model.isDirty(recordID || this.handle);
},
/**
* @override
*/
@@ -246,7 +246,7 @@ var BasicView = AbstractView.extend({
}
}
if (mode.indexOf(',') !== -1) {
mode = config.device.size_class !== config.device.SIZES.XS ? 'tree' : 'kanban';
mode = config.device.isMobile ? 'kanban' : 'tree';
}
if (mode === 'tree') {
mode = 'list';
@@ -18,10 +18,19 @@ var FormRenderer = BasicRenderer.extend({
/**
* @override
*/
init: function (parent, state, params) {
init: function () {
this._super.apply(this, arguments);
this.idsForLabels = {};
},
/**
* @override
*/
start: function () {
if (config.device.size_class <= config.device.SIZES.XXS) {
this.$el.addClass('o_xxs_form_view');
}
return this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
// Public
@@ -279,7 +288,7 @@ var FormRenderer = BasicRenderer.extend({
var visible_buttons = buttons_partition[1];
// Get the unfolded buttons according to window size
var nb_buttons = [2, 4, 6, 7][config.device.size_class];
var nb_buttons = [2, 2, 4, 6, 7][config.device.size_class];
var unfolded_buttons = visible_buttons.slice(0, nb_buttons).concat(invisible_buttons);
// Get the folded buttons
@@ -20,7 +20,7 @@ var qweb = core.qweb;
var CHART_TYPES = ['pie', 'bar', 'line'];
// hide top legend when too many items for device size
var MAX_LEGEND_LENGTH = 25 * (1 + config.device.size_class);
var MAX_LEGEND_LENGTH = 25 * (Math.max(1, config.device.size_class));
return AbstractRenderer.extend({
className: "o_graph_svg_container",
@@ -4,15 +4,14 @@ odoo.define('web.KanbanColumn', function (require) {
var config = require('web.config');
var core = require('web.core');
var Dialog = require('web.Dialog');
var kanban_quick_create = require('web.kanban_quick_create');
var KanbanRecord = require('web.KanbanRecord');
var RecordQuickCreate = require('web.kanban_record_quick_create');
var view_dialogs = require('web.view_dialogs');
var Widget = require('web.Widget');
var KanbanColumnProgressBar = require('web.KanbanColumnProgressBar');
var _t = core._t;
var QWeb = core.qweb;
var RecordQuickCreate = kanban_quick_create.RecordQuickCreate;
var KanbanColumn = Widget.extend({
template: 'KanbanView.Group',
@@ -47,6 +46,8 @@ var KanbanColumn = Widget.extend({
this.modelName = data.model;
this.quick_create = options.quick_create;
this.quickCreateView = options.quickCreateView;
this.groupedBy = options.groupedBy;
this.grouped_by_m2o = options.grouped_by_m2o;
this.editable = options.editable;
this.deletable = options.deletable;
@@ -156,6 +157,18 @@ var KanbanColumn = Widget.extend({
return $.when.apply($, defs);
},
/**
* Called when a record has been quick created, as a new column is rendered
* and appended into a fragment, before replacing the old column in the DOM.
* When this happens, the quick create widget is inserted into the new
* column directly, and it should be focused. However, as it is rendered
* into a fragment, the focus has to be set manually once in the DOM.
*/
on_attach_callback: function () {
if (this.quickCreateWidget) {
this.quickCreateWidget.on_attach_callback();
}
},
//--------------------------------------------------------------------------
// Public
@@ -163,6 +176,8 @@ var KanbanColumn = Widget.extend({
/**
* Adds the quick create record to the top of the column.
*
* @returns {Deferred}
*/
addQuickCreate: function () {
if (this.folded) {
@@ -173,11 +188,25 @@ var KanbanColumn = Widget.extend({
return;
}
if (this.quickCreateWidget) {
return;
return $.Deferred().reject();
}
this.trigger_up('close_quick_create'); // close other quick create widgets
var context = this.data.getContext();
context['default_' + this.groupedBy] = this.id;
this.quickCreateWidget = new RecordQuickCreate(this, {
context: context,
formViewRef: this.quickCreateView,
model: this.modelName,
});
return this.quickCreateWidget.insertAfter(this.$header);
},
/**
* Closes the quick create widget if it isn't dirty.
*/
cancelQuickCreate: function () {
if (this.quickCreateWidget) {
this.quickCreateWidget.cancel();
}
var width = this.records.length ? this.records[0].$el.innerWidth() : this.$el.width() - 8;
this.quickCreateWidget = new RecordQuickCreate(this, width);
this.quickCreateWidget.insertAfter(this.$header);
},
/**
* @returns {Boolean} true iff the column is empty
@@ -0,0 +1,208 @@
odoo.define('web.kanban_column_quick_create', function (require) {
"use strict";
/**
* This file defines the ColumnQuickCreate widget for Kanban. It allows to
* create kanban columns directly from the Kanban view.
*/
var core = require('web.core');
var Dialog = require('web.Dialog');
var Widget = require('web.Widget');
var _t = core._t;
var QWeb = core.qweb;
var ColumnQuickCreate = Widget.extend({
template: 'KanbanView.ColumnQuickCreate',
events: {
'click .o_quick_create_folded': '_onUnfold',
'click .o_kanban_add': '_onAddClicked',
'click .o_kanban_examples': '_onShowExamples',
'keydown': '_onKeydown',
'keypress input': '_onKeypress',
},
/**
* @override
* @param {Object} [options]
* @param {Object} [options.examples]
*/
init: function (parent, options) {
this._super.apply(this, arguments);
this.examples = options.examples;
this.folded = true;
},
/**
* @override
*/
start: function () {
this.$quickCreateFolded = this.$('.o_quick_create_folded');
this.$quickCreateUnfolded = this.$('.o_quick_create_unfolded');
this.$input = this.$('input');
// destroy the quick create when the user clicks outside
core.bus.on('click', this, this._onWindowClicked);
this._update();
return this._super.apply(this, arguments);
},
/**
* Called each time the quick create is attached into the DOM
*/
on_attach_callback: function () {
if (!this.folded) {
this.$input.focus();
}
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Folds/unfolds the Column quick create widget
*/
toggleFold: function () {
this.folded = !this.folded;
this._update();
if (!this.folded) {
this.$input.focus();
this.trigger_up('scrollTo', {selector: '.o_column_quick_create'});
}
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Clears the input value and notify the environment to create a column
*
* @private
*/
_add: function () {
var value = this.$input.val().trim();
if (!value.length) {
this._cancel();
return;
}
this.$input.val('');
this.trigger_up('quick_create_add_column', {value: value});
this.$input.focus();
},
/**
* Cancels the quick creation
*
* @private
*/
_cancel: function () {
this.folded = true;
this.$input.val('');
this._update();
},
/**
* Updates the rendering according to the current state (folded/unfolded)
*
* @private
*/
_update: function () {
this.$quickCreateFolded.toggle(this.folded);
this.$quickCreateUnfolded.toggle(!this.folded);
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
* @param {MouseEvent} event
*/
_onAddClicked: function (event) {
event.stopPropagation();
this._add();
},
/**
* Cancels quick creation on escape keydown event
*
* @private
* @param {KeyEvent} event
*/
_onKeydown: function (event) {
if (event.keyCode === $.ui.keyCode.ESCAPE) {
this._cancel();
}
},
/**
* Validates quick creation on enter keypress event
*
* @private
* @param {KeyEvent} event
*/
_onKeypress: function (event) {
if (event.keyCode === $.ui.keyCode.ENTER) {
this._add();
}
},
/**
* Opens a dialog containing examples of Kanban processes
*
* @private
*/
_onShowExamples: function () {
var self = this;
var dialog = new Dialog(this, {
$content: $(QWeb.render('KanbanView.ExamplesDialog', {
examples: this.examples,
})),
buttons: [{
classes: 'btn-primary pull-right',
close: true,
text: _t('Got it'),
}],
size: "large",
title: "Kanban Examples",
}).open();
dialog.on('closed', this, function () {
self.$input.focus();
});
},
/**
* @private
*/
_onUnfold: function () {
if (this.folded) {
this.toggleFold();
}
},
/**
* When a click happens outside the quick create, we want to close it.
*
* @private
* @param {MouseEvent} event
*/
_onWindowClicked: function (event) {
// ignore clicks if the quick create is not in the dom
if (!document.contains(this.el)) {
return;
}
// ignore clicks in modals
if ($(event.target).closest('.modal').length) {
return;
}
// ignore clicks if target is inside the quick create
if (this.el.contains(event.target)) {
return;
}
this._cancel();
},
});
return ColumnQuickCreate;
});
@@ -39,8 +39,12 @@ var KanbanController = BasicController.extend({
this.on_create = params.on_create;
this.hasButtons = params.hasButtons;
this.createColumnEnabled = this._isCreateColumnEnabled();
},
/**
* Called each time the kanban view is attached into the DOM.
*/
on_attach_callback: function () {
this.renderer.on_attach_callback();
},
//--------------------------------------------------------------------------
@@ -58,15 +62,6 @@ var KanbanController = BasicController.extend({
this.$buttons.appendTo($node);
}
},
/**
* Override update method to recompute createColumnEnabled.
*
* @returns {Deferred}
*/
update: function () {
this.createColumnEnabled = this._isCreateColumnEnabled();
return this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
// Private
@@ -92,18 +87,18 @@ var KanbanController = BasicController.extend({
* m2o field and group_create action enabled.
*
* @private
* @param {Object} state the current state
* @returns {boolean}
*/
_isCreateColumnEnabled: function () {
_isCreateColumnEnabled: function (state) {
var groupCreate = this.is_action_enabled('group_create');
if (!groupCreate) {
// pre-return to avoid a lot of the following processing
return false;
}
var state = this.model.get(this.handle, {raw: true});
var groupByField = state.fields[state.groupedBy[0]];
var groupedByM2o = groupByField && (groupByField.type === 'many2one');
return groupedByM2o;
return !!groupedByM2o;
},
/**
* This method calls the server to ask for a resequence. Note that this
@@ -121,6 +116,16 @@ var KanbanController = BasicController.extend({
self._updateEnv();
});
},
/**
* Overrides to update the control panel buttons when the state is updated.
*
* @override
* @private
*/
_update: function () {
this._updateButtons();
return this._super.apply(this, arguments);
},
/**
* In grouped mode, set 'Create' button as btn-default if there is no column
* (except if we can't create new columns)
@@ -130,12 +135,9 @@ var KanbanController = BasicController.extend({
*/
_updateButtons: function () {
if (this.$buttons) {
var data = this.model.get(this.handle, {raw: true});
var grouped = data.groupedBy.length;
var createMuted = grouped && data.data.length === 0 && this.createColumnEnabled;
this.$buttons.find('.o-kanban-button-new')
.toggleClass('btn-primary', !createMuted)
.toggleClass('btn-default', createMuted);
var state = this.model.get(this.handle, {raw: true});
var createHidden = state.data.length === 0 && this._isCreateColumnEnabled(state);
this.$buttons.find('.o-kanban-button-new').toggleClass('o_hidden', createHidden);
}
},
@@ -297,52 +299,51 @@ var KanbanController = BasicController.extend({
/**
* @private
* @param {OdooEvent} event
* @param {KanbanColumn} event.target the column in which the record should
* be added
* @param {Object} event.data.values the field values of the record to
* create; if values only contains the value of the 'display_name', a
* 'name_create' is performed instead of 'create'
*/
_onQuickCreateRecord: function (event) {
var self = this;
var values = event.data.values;
var column = event.target;
var name = event.data.value;
var state = this.model.get(this.handle, {raw: true});
var columnState = this.model.get(column.db_id, {raw: true});
var context = columnState.getContext();
context['default_' + state.groupedBy[0]] = columnState.res_id;
this._rpc({
model: state.model,
method: 'name_create',
args: [name],
context: context,
})
.then(add_record)
// function that updates the kanban view once the record has been added
// it receives the local id of the created record in arguments
var update = function (db_id) {
self._updateEnv();
var columnState = self.model.getColumn(db_id);
return self.renderer
.updateColumn(columnState.id, columnState, {openQuickCreate: true})
.then(function () {
if (event.data.openRecord) {
self.trigger_up('open_record', {id: db_id, mode: 'edit'});
}
});
};
this.model.createRecordInGroup(column.db_id, values)
.then(update)
.fail(function (error, event) {
event.preventDefault();
var columnState = self.model.get(column.db_id, {raw: true});
var context = columnState.getContext();
var state = self.model.get(self.handle, {raw: true});
context['default_' + state.groupedBy[0]] = columnState.res_id;
new view_dialogs.FormViewDialog(self, {
res_model: state.model,
context: _.extend({default_name: name}, context),
title: _t("Create"),
disable_multiple_selection: true,
on_saved: function (record) {
add_record([record.res_id]);
self.model.addRecordToGroup(column.db_id, record.res_id)
.then(update);
},
}).open();
});
function add_record(records) {
return self.model
.addRecordToGroup(columnState.id, records[0])
.then(function (db_id) {
self._updateEnv();
var columnState = self.model.getColumn(db_id);
return self.renderer
.updateColumn(columnState.id, columnState, {openQuickCreate: true})
.then(function () {
if (event.data.openRecord) {
self.trigger_up('open_record', {id: db_id, mode: 'edit'});
}
});
});
}
},
/**
* @private
@@ -0,0 +1,27 @@
odoo.define('web.kanban_examples_registry', function (require) {
"use strict";
/**
* This file instantiates and exports a registry. The purpose of this registry
* is to store static data displayed in a dialog to help the end user to
* configure its columns in the grouped Kanban view.
*
* To activate a link on the ColumnQuickCreate widget on open such a dialog, the
* attribute 'examples' on the root arch node must be set to a valid key in this
* registry.
*
* Each value in this registry must be an array of Objects containing the
* following keys:
* - name (string)
* - columns (Array[string])
* - description (string, optional) BE CAREFUL [*]
*
* [*] The description is added with a t-raw so the translated texts must be
* properly escaped.
*/
var Registry = require('web.Registry');
return new Registry();
});
@@ -33,7 +33,7 @@ var KanbanModel = BasicModel.extend({
parentID: groupID,
});
return this._fetchRecord(new_record).then(function (result) {
var def = this._fetchRecord(new_record).then(function (result) {
group.data.unshift(new_record.id);
group.res_ids.unshift(resId);
group.count++;
@@ -44,6 +44,7 @@ var KanbanModel = BasicModel.extend({
return result.id;
});
return this._reloadProgressBarGroupFromRecord(new_record.id, def);
},
/**
* Creates a new group from a name (performs a name_create).
@@ -93,6 +94,43 @@ var KanbanModel = BasicModel.extend({
return newGroup.id;
});
},
/**
* Creates a new record from the given value, and add it to the given group.
*
* @param {string} groupID
* @param {Object} values
* @returns {Deferred} resolved with the local id of the created record
*/
createRecordInGroup: function (groupID, values) {
var self = this;
var group = this.localData[groupID];
var context = this._getContext(group);
var parent = this.localData[group.parentID];
context['default_' + parent.groupedBy[0]] = group.res_id;
var def;
if (Object.keys(values).length === 1 && 'display_name' in values) {
// only 'display_name is given, perform a 'name_create'
def = this._rpc({
model: parent.model,
method: 'name_create',
args: [values.display_name],
context: context,
}).then(function (records) {
return records[0];
});
} else {
// other fields are specified, perform a classical 'create'
def = this._rpc({
model: parent.model,
method: 'create',
args: [values],
context: context,
});
}
return def.then(function (resID) {
return self.addRecordToGroup(group.id, resID);
});
},
/**
* Add the key `tooltipData` (kanban specific) when performing a `geŧ`.
*
@@ -1,322 +0,0 @@
odoo.define('web.kanban_quick_create', function (require) {
"use strict";
/**
* This file defines two Kanban 'quick create' widgets: one to quick create
* kanban records, and one to quick create kanban columns.
*/
var Widget = require('web.Widget');
var AbstractQuickCreate = Widget.extend({
events: {
'click .o_kanban_add': '_onAddClicked',
'click .o_kanban_cancel': '_onCancelClicked',
'keydown': '_onKeydown',
'keypress input': '_onKeypress',
'mousedown .o_kanban_add': '_onMousedown',
'mousedown .o_kanban_cancel': '_onMousedown',
'focusout': '_onFocusOut',
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Clears the input value and notify the environment that a quick create
* has been done
*
* @private
* @param {Object} [options] dict of options to pass to call to '_notifyAdd'
*/
_add: function (options) {
var value = this.$input.val();
this.$input.val('');
if (/^\s*$/.test(value)) {
return;
}
this._notifyAdd(value, options);
this.$input.focus();
},
/**
* Should implement what to do when the quick creation has been cancelled
*
* @abstract
* @private
*/
_cancel: function () {
},
/**
* Should implement what to do to notify the environment that a quick
* create has been done
*
* @abstract
* @private
*/
_notifyAdd: function (name, options) {
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
* @param {MouseEvent} event
*/
_onAddClicked: function (event) {
event.stopPropagation();
this._add();
},
/**
* @private
*/
_onCancelClicked: function () {
this._cancel();
},
/**
* Cancels quick creation on focusout input event
*
* @private
* @param {KeyEvent} ev
*/
_onFocusOut: function (ev) {
if (!this.$input.val()) {
this._cancel();
}
},
/**
* Cancels quick creation on escape keydown event
*
* @private
* @param {KeyEvent} event
*/
_onKeydown: function (event) {
if (event.keyCode === 27) { // escape
this._cancel();
}
},
/**
* Validates quick creation on enter keypress event
*
* @private
* @param {KeyEvent} event
*/
_onKeypress: function (event) {
if (event.keyCode === 13) { // enter
this._add();
}
},
/**
* In all browsers the 'focus/blur' event is triggered before a button's
* 'click' (it's actually triggered by mousedown).
* The quick create is destroyed on blur of its input but still needs
* to create the data when the "Add" button is clicked.
* This problem is adressed by suppressing the 'focus/blur' event from the
* relevant buttons (Add and Cancel) entirely by preventing the browser's
* default mousedown handler.
*
* @private
* @param {MouseEvent} event
*/
_onMousedown: function (event) {
event.preventDefault();
},
});
var RecordQuickCreate = AbstractQuickCreate.extend({
template: "KanbanView.QuickCreate",
events: _.extend({}, AbstractQuickCreate.prototype.events, {
'click .o_kanban_edit': '_onEditClicked',
'mousedown .o_kanban_edit': '_onMousedown',
}),
/**
* @override
* @param {Widget} parent
* @param {Object} options
* @param {string|number} options.width defines the element's width
* @param {string} [options.defaultName] the record's default name
*/
init: function (parent, options) {
this._super.apply(this, arguments);
this.width = options.width;
this.defaultName = options.defaultName;
},
/**
* @override
*/
start: function () {
this.$el.css({width: this.width});
this.$input = this.$('input');
this._addDefaultName();
this.$input.focus();
return this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Set the default value for the record name.
*
* @private
*/
_addDefaultName: function () {
if (this.defaultName) {
this.$input.val(this.defaultName);
}
},
/**
* Triggers up an event to cancel the quick creation
*
* @override
* @private
*/
_cancel: function () {
this.trigger_up('cancel_quick_create');
},
/**
* Triggers up an event to quick create a record with the given value
*
* @override
* @private
* @param {string} value
* @param {Object} [options]
* @param {boolean} [options.openRecord] set to true to directly open the
* newly created record in a form view (in edit mode)
*/
_notifyAdd: function (value, options) {
this.trigger_up('quick_create_add_record', {
value: value,
openRecord: options && options.openRecord || false,
});
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Add the default value for the record name again after creating
* the previous record.
*
* @override
* @private
*/
_onAddClicked: function () {
this._super.apply(this, arguments);
this._addDefaultName();
},
/**
* Validates the quick creation and directly opens the record in a form
* view in edit mode.
*
* @private
* @param {MouseEvent} event
*/
_onEditClicked: function (event) {
event.stopPropagation();
this._add({openRecord: true});
},
});
var ColumnQuickCreate = AbstractQuickCreate.extend({
template: 'KanbanView.ColumnQuickCreate',
events: _.extend({}, AbstractQuickCreate.prototype.events, {
'click': 'toggleFold',
'click input': '_onInputClicked',
'focusout': '_onFocusout',
}),
/**
* @override
*/
init: function () {
this._super.apply(this, arguments);
this.folded = true;
},
/**
* @override
*/
start: function () {
this.$header = this.$('.o_column_header');
this.$quick_create = this.$('.o_kanban_quick_create');
this.$input = this.$('input');
return this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Toggle fold/unfold the Column quick create widget
*/
toggleFold: function () {
this.folded = !this.folded;
this._update();
if (!this.folded) {
this.$input.focus();
this.trigger_up('scrollTo', {selector: '.o_column_quick_create'});
}
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Triggers up an event to quick create a column with the given value
*
* @override
* @private
* @param {string} value
*/
_notifyAdd: function (value) {
this.trigger_up('quick_create_add_column', {value: value});
},
/**
* Updates the rendering according to the current state
*
* @private
*/
_update: function () {
this.$header.toggle(this.folded);
this.$quick_create.toggle(!this.folded);
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
*/
_onFocusout: function () {
var hasFocus = this.$(':focus').length > 0;
if (hasFocus) {
return;
}
this.folded = true;
this.$input.val('');
this._update();
},
/**
* Stops the propagation of the event to prevent the quick create from
* toggling when the user clicks in the input
*
* @private
* @param {MouseEvent} event
*/
_onInputClicked: function (event) {
event.stopPropagation();
},
});
return {
RecordQuickCreate: RecordQuickCreate,
ColumnQuickCreate: ColumnQuickCreate,
};
});
@@ -0,0 +1,256 @@
odoo.define('web.kanban_record_quick_create', function (require) {
"use strict";
/**
* This file defines the RecordQuickCreate widget for Kanban. It allows to
* create kanban records directly from the Kanban view.
*/
var core = require('web.core');
var QuickCreateFormView = require('web.QuickCreateFormView');
var Widget = require('web.Widget');
var qweb = core.qweb;
var RecordQuickCreate = Widget.extend({
className: 'o_kanban_quick_create',
custom_events: {
add: '_onAdd',
cancel: '_onCancel',
},
events: {
'click .o_kanban_add': '_onAddClicked',
'click .o_kanban_edit': '_onEditClicked',
'click .o_kanban_cancel': '_onCancelClicked',
},
/**
* @override
* @param {Widget} parent
* @param {Object} options
* @param {Object} options.context
* @param {string|null} options.formViewRef
* @param {string} options.model
*/
init: function (parent, options) {
this._super.apply(this, arguments);
this.context = options.context;
this.formViewRef = options.formViewRef;
this.model = options.model;
},
/**
* Loads the form fieldsView (if not provided), instantiates the form view
* and starts the form controller.
*
* @override
*/
willStart: function () {
var self = this;
var def1 = this._super.apply(this, arguments);
var def2;
if (this.formViewRef) {
var views = [[false, 'form']];
var context = _.extend({}, this.context, {
form_view_ref: this.formViewRef,
});
def2 = this.loadViews(this.model, context, views);
} else {
var fieldsView = {};
fieldsView.arch = '<form>' +
'<field name="display_name" placeholder="Title" modifiers=\'{"required": true}\'/>' +
'</form>';
var fields = {
display_name: {string: 'Display name', type: 'char'},
};
fieldsView.fields = fields;
fieldsView.viewFields = fields;
def2 = $.when({form: fieldsView});
}
def2 = def2.then(function (fieldsViews) {
var formView = new QuickCreateFormView(fieldsViews.form, {
context: self.context,
modelName: self.model,
userContext: self.getSession().user_context,
});
return formView.getController(self).then(function (controller) {
self.controller = controller;
return self.controller.appendTo(document.createDocumentFragment());
});
});
return $.when(def1, def2);
},
/**
* @override
*/
start: function () {
this.$el.append(this.controller.$el);
this.$el.append(qweb.render('KanbanView.RecordQuickCreate.buttons'));
// focus the first field
this.controller.autofocus();
// destroy the quick create when the user clicks outside
core.bus.on('click', this, this._onWindowClicked);
return this._super.apply(this, arguments);
},
/**
* Called when the quick create is appended into the DOM.
*/
on_attach_callback: function () {
if (this.controller) {
this.controller.autofocus();
}
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Cancels the quick creation if the record isn't dirty, i.e. if no changes
* have been made yet
*
* @private
* @returns {Deferred}
*/
cancel: function () {
var self = this;
return this.controller.commitChanges().then(function () {
if (!self.controller.isDirty()) {
self._cancel();
}
});
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @override
* @private
* @param {Object} [options]
* @param {boolean} [options.openRecord] set to true to directly open the
* newly created record in a form view (in edit mode)
* @returns {Deferred}
*/
_add: function (options) {
var self = this;
return this.controller.commitChanges().then(function () {
var canBeSaved = self.controller.canBeSaved();
if (canBeSaved) {
self.trigger_up('quick_create_add_record', {
openRecord: options && options.openRecord || false,
values: self.controller.getChanges(),
});
}
});
},
/**
* Notifies the environment that the quick creation must be cancelled
*
* @private
* @returns {Deferred}
*/
_cancel: function () {
this.trigger_up('cancel_quick_create');
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
* @param {OdooEvent} ev
*/
_onAdd: function (ev) {
ev.stopPropagation();
this._add();
},
/**
* @private
* @param {MouseEvent} ev
*/
_onAddClicked: function (ev) {
ev.stopPropagation();
this._add();
},
/**
* @private
* @param {OdooEvent} ev
*/
_onCancel: function (ev) {
ev.stopPropagation();
this._cancel();
},
/**
* @private
* @param {MouseEvent} ev
*/
_onCancelClicked: function (ev) {
ev.stopPropagation();
this._cancel();
},
/**
* Validates the quick creation and directly opens the record in a form
* view in edit mode.
*
* @private
* @param {MouseEvent} ev
*/
_onEditClicked: function (ev) {
ev.stopPropagation();
this._add({openRecord: true});
},
/**
* When a click happens outside the quick create, we want to close the quick
* create.
*
* This is quite tricky, because in some cases a click is performed outside
* the quick create, but is still related to it (e.g. click in a dialog
* opened from the quick create).
*
* @param {MouseEvent} ev
*/
_onWindowClicked: function (ev) {
// ignore clicks if the quick create is not in the dom
if (!document.contains(this.el)) {
return;
}
// ignore clicks on elements that open the quick create widget, to
// prevent from closing quick create widget that has just been opened
if ($(ev.target).closest('.o-kanban-button-new, .o_kanban_quick_add').length) {
return;
}
// ignore clicks in autocomplete dropdowns
if ($(ev.target).parents('.ui-autocomplete').length) {
return;
}
// ignore clicks in modals
if ($(ev.target).closest('.modal').length) {
return;
}
// ignore clicks if target is no longer in dom (e.g., a click on the
// 'delete' trash icon of a m2m tag)
if (!document.contains(ev.target)) {
return;
}
// ignore clicks if target is inside the quick create
if (this.el.contains(ev.target) && this.el !== ev.target) {
return;
}
this.cancel();
},
});
return RecordQuickCreate;
});
@@ -5,13 +5,11 @@ var BasicRenderer = require('web.BasicRenderer');
var core = require('web.core');
var KanbanColumn = require('web.KanbanColumn');
var KanbanRecord = require('web.KanbanRecord');
var quick_create = require('web.kanban_quick_create');
var ColumnQuickCreate = require('web.kanban_column_quick_create');
var QWeb = require('web.QWeb');
var session = require('web.session');
var utils = require('web.utils');
var ColumnQuickCreate = quick_create.ColumnQuickCreate;
var qweb = core.qweb;
function findInNode(node, predicate) {
@@ -84,7 +82,8 @@ function transformQwebTemplate(node, fields) {
var KanbanRenderer = BasicRenderer.extend({
className: 'o_kanban_view',
custom_events: _.extend({}, BasicRenderer.prototype.custom_events || {}, {
'set_progress_bar_state': '_onSetProgressBarState',
close_quick_create: '_onCloseQuickCreate',
set_progress_bar_state: '_onSetProgressBarState',
}),
/**
@@ -98,18 +97,25 @@ var KanbanRenderer = BasicRenderer.extend({
var templates = findInNode(this.arch, function (n) { return n.tag === 'templates';});
transformQwebTemplate(templates, state.fields);
this.qweb.add_template(utils.json_node_to_xml(templates));
this.examples = params.examples;
this.recordOptions = _.extend({}, params.record_options, {
qweb: this.qweb,
viewType: 'kanban',
});
this.columnOptions = _.extend({}, params.column_options, { qweb: this.qweb });
this.columnOptions = _.extend({}, params.column_options);
if (this.columnOptions.hasProgressBar) {
this.columnOptions.progressBarStates = {};
}
this._setState(state);
},
/**
* Called each time the renderer is attached into the DOM.
*/
on_attach_callback: function () {
if (this.quickCreate) {
this.quickCreate.on_attach_callback();
}
},
//--------------------------------------------------------------------------
// Public
@@ -117,9 +123,11 @@ var KanbanRenderer = BasicRenderer.extend({
/**
* Displays the quick create record in the first column.
*
* @returns {Deferred}
*/
addQuickCreate: function () {
this.widgets[0].addQuickCreate();
return this.widgets[0].addQuickCreate();
},
/**
* Toggle fold/unfold the Column quick create widget
@@ -152,11 +160,21 @@ var KanbanRenderer = BasicRenderer.extend({
var index = _.findIndex(this.widgets, {db_id: localID});
var column = this.widgets[index];
this.widgets[index] = newColumn;
return newColumn.insertAfter(column.$el).then(function () {
return newColumn.appendTo(document.createDocumentFragment()).then(function () {
var def;
if (options && options.openQuickCreate) {
newColumn.addQuickCreate();
def = newColumn.addQuickCreate();
}
column.destroy();
return $.when(def).then(function () {
newColumn.$el.insertAfter(column.$el);
// When a record has been quick created, the new column directly
// renders the quick create widget (to allow quick creating several
// records in a row). However, as we render this column in a
// fragment, the quick create widget can't be correctly focused. So
// we manually call on_attach_callback to focus it once in the DOM.
newColumn.on_attach_callback();
column.destroy();
});
});
},
/**
@@ -272,11 +290,18 @@ var KanbanRenderer = BasicRenderer.extend({
// Enable column quickcreate
if (this.createColumnEnabled) {
this.quickCreate = new ColumnQuickCreate(this);
this.quickCreate.appendTo(fragment);
this.quickCreate = new ColumnQuickCreate(this, {
examples: this.examples,
});
this.quickCreate.appendTo(fragment).then(function () {
// Open it directly if there is no column yet
if (!self.state.data.length) {
self.quickCreate.toggleFold();
}
});
}
}
},
/**
* Renders an ungrouped kanban view in a fragment.
@@ -334,8 +359,9 @@ var KanbanRenderer = BasicRenderer.extend({
_setState: function (state) {
this.state = state;
var groupByFieldAttrs = state.fields[state.groupedBy[0]];
var groupByFieldInfo = state.fieldsInfo.kanban[state.groupedBy[0]];
var groupByField = state.groupedBy[0];
var groupByFieldAttrs = state.fields[groupByField];
var groupByFieldInfo = state.fieldsInfo.kanban[groupByField];
// Deactivate the drag'n'drop if the groupedBy field:
// - is a date or datetime since we group by month or
// - is readonly (on the field attrs or in the view)
@@ -353,13 +379,14 @@ var KanbanRenderer = BasicRenderer.extend({
}
}
this.groupedByM2O = groupByFieldAttrs && (groupByFieldAttrs.type === 'many2one');
var grouped_by_field = this.groupedByM2O && groupByFieldAttrs.relation;
var relation = this.groupedByM2O && groupByFieldAttrs.relation;
var groupByTooltip = groupByFieldInfo && groupByFieldInfo.options.group_by_tooltip;
this.columnOptions = _.extend(this.columnOptions, {
draggable: draggable,
group_by_tooltip: groupByTooltip,
groupedBy: groupByField,
grouped_by_m2o: this.groupedByM2O,
relation: grouped_by_field,
relation: relation,
});
this.createColumnEnabled = this.groupedByM2O && this.columnOptions.group_creatable;
},
@@ -368,6 +395,16 @@ var KanbanRenderer = BasicRenderer.extend({
// Handlers
//--------------------------------------------------------------------------
/**
* Closes the opened quick create widgets in columns
*
* @private
*/
_onCloseQuickCreate: function () {
if (this.state.groupedBy.length) {
_.invoke(this.widgets, 'cancelQuickCreate');
}
},
/**
* Updates progressbar internal states (necessary for animations) with
* received data.
@@ -38,9 +38,11 @@ KanbanRenderer.include({
/**
* Displays the quick create record in the active column
*
* @returns {Deferred}
*/
addQuickCreate: function () {
this.widgets[this.activeColumnIndex].addQuickCreate();
return this.widgets[this.activeColumnIndex].addQuickCreate();
},
/**
* Overrides to restore the left property and the scrollTop on the updated
@@ -7,6 +7,7 @@ var config = require('web.config');
var KanbanModel = require('web.KanbanModel');
var KanbanRenderer = require('web.KanbanRenderer');
var KanbanController = require('web.KanbanController');
var kanbanExamplesRegistry = require('web.kanban_examples_registry');
var utils = require('web.utils');
var _lt = core._lt;
@@ -62,6 +63,7 @@ var KanbanView = BasicView.extend({
deletable: activeActions.group_delete,
group_creatable: activeActions.group_create && !config.device.isMobile,
quick_create: params.isQuickCreateEnabled || this._isQuickCreateEnabled(),
quickCreateView: this.arch.attrs.quick_create_view || null,
hasProgressBar: !!progressBar,
};
this.rendererParams.record_options = {
@@ -69,9 +71,12 @@ var KanbanView = BasicView.extend({
deletable: activeActions.delete,
read_only_mode: params.readOnlyMode,
};
var examples = this.arch.attrs.examples;
if (examples) {
this.rendererParams.examples = kanbanExamplesRegistry.get(examples);
}
this.controllerParams.on_create = this.arch.attrs.on_create;
this.controllerParams.readOnlyMode = false;
this.controllerParams.hasButtons = true;
@@ -0,0 +1,110 @@
odoo.define('web.QuickCreateFormView', function (require) {
"use strict";
/**
* This file defines the QuickCreateFormView, an extension of the FormView that
* is used by the RecordQuickCreate in Kanban views.
*/
var FormController = require('web.FormController');
var BasicModel = require('web.BasicModel');
var FormRenderer = require('web.FormRenderer');
var FormView = require('web.FormView');
var QuickCreateFormRenderer = FormRenderer.extend({
/**
* @override
*/
start: function () {
this.$el.addClass('o_xxs_form_view');
return this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @override
* @private
* @param {OdooEvent} ev
*/
_onNavigationMove: function (ev) {
var direction = ev.data.direction;
if (direction === 'cancel' || direction === 'next_line') {
ev.stopPropagation();
this.trigger_up(direction === 'cancel' ? 'cancel' : 'add');
} else {
this._super.apply(this, arguments);
}
},
});
var QuickCreateFormModel = BasicModel.extend({
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @returns {Object} the changes of the given resource (server commands for
* x2manys)
*/
getChanges: function (localID) {
var record = this.localData[localID];
return this._generateChanges(record, {changesOnly: false});
},
});
var QuickCreateFormController = FormController.extend({
custom_events: _.extend({}, FormController.prototype.custom_events, {
env_updated: '_onEnvUpdated',
}),
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Asks all field widgets to notify the environment with their current value
* (useful for instance for input fields that still have the focus and that
* could have not notified the environment of their changes yet).
*
* @return {Deferred}
*/
commitChanges: function () {
return this.mutex
.exec(this.renderer.commitChanges.bind(this.renderer, this.handle));
},
/**
* @returns {Object} the changes done on the current record
*/
getChanges: function () {
return this.model.getChanges(this.handle);
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Stops the propagation of the 'env_updated' event to prevent interferences
* with the kanban controller.
*
* @private
*/
_onEnvUpdated: function (ev) {
ev.stopPropagation();
},
});
var QuickCreateFormView = FormView.extend({
config: _.extend({}, FormView.prototype.config, {
Model: QuickCreateFormModel,
Renderer: QuickCreateFormRenderer,
Controller: QuickCreateFormController,
}),
});
return QuickCreateFormView;
});
+51
View File
@@ -649,6 +649,57 @@ label.o_light_label, .o_light_label label {
font-weight: normal;
}
// XXS form view specific rules
.form-break-table() {
display: block;
margin-bottom: @odoo-form-spacing-unit * 4;
> tbody {
display: block;
> tr {
.o-flex-display();
.o-flex-flow(row, wrap);
> td {
.o-flex(1, 0, auto);
display: block;
max-width: 100%;
padding: 0;
width: auto!important; // !important is required to override the width computed in JS
&.o_td_label {
width: 94%!important; // The label must be on its own line except if the form field is small enough (checkbox)
line-height: @odoo-label-font-size-factor;
}
.o_field_widget {
margin-bottom: @odoo-form-spacing-unit * 2;
&.o_field_boolean {
margin-right: 0;
}
}
.o_input_dropdown {
width: auto;
max-width: 100%;
}
}
}
}
}
.o_form_view.o_xxs_form_view {
.oe_title {
word-break: break-all;
}
.o_group {
&.o_inner_group {
.form-break-table();
}
}
}
// Settings form views
.o_settings_container {
.o-flex-display();
.o-flex(0, 1, auto);
@@ -181,3 +181,10 @@
}
}
}
// XXS form view specific rules
.o_form_view.o_xxs_form_view {
.o_group .o_td_label {
border: none;
}
}
@@ -0,0 +1,71 @@
.o_kanban_examples_dialog {
.o-flex-display();
padding: 0px;
background: @odoo-color-silver;
// sidebar
.o_kanban_examples_dialog_nav {
.o-flex(0, 0, auto);
border-right: 1px solid @gray-lighter-dark;
background: white;
> ul > li {
margin: 0;
border-bottom: 1px solid @gray-lighter-dark;
> a {
padding: 12px 15px;
color: @odoo-main-text-color;
border-left: 3px solid transparent;
&:focus {
outline: none;
}
}
&.active > a {
color: @headings-color;
border-left-color: @odoo-brand-optional;
font-weight: bold;
}
}
}
// content
.o_kanban_examples_dialog_content {
.o-flex(1, 1, 100%);
.o_kanban_examples_description {
padding: 16px 16px 0;
text-align: justify;
}
.o_kanban_examples {
.o-flex-display();
padding: 0 6px;
.o_kanban_examples_group {
.o-flex(1, 1, 100%);
margin: 10px;
.o_kanban_examples_ghost {
padding: 5px;
margin-top: -1px;
border: thin solid @gray-lighter-dark;
background-color: white;
.o_ghost_content {
height: 12px;
background: @gray-lighter-dark;
&.o_ghost_tag {
display: inline-block;
width: 40%;
margin-top: 10px;
}
}
.o_ghost_avatar {
height: 20px;
width: 20px;
margin-top: 6px;
}
}
}
}
}
}
+44 -32
View File
@@ -161,12 +161,11 @@
box-shadow: 0 0 20px -10px;
margin: 0 @o-kanban-record-margin -1px @o-kanban-record-margin;
input {
width: 100%;
margin: 2px 0 8px;
padding: 3px 6px;
color: @headings-color;
font-weight: 500;
.o_form_view {
padding: 0;
.o_group.o_inner_group {
margin: 0;
}
}
}
@@ -548,45 +547,58 @@
// Kanban Grouped Layout - "Create new column" column
.o_column_quick_create {
.o-kanban-slim-col();
.o_column_header {
.o-kanban-header-title();
position: relative;
opacity: 0.5;
.o_kanban_title {
.o-kanban-v-title();
.o_quick_create_folded {
cursor: pointer;
padding: 12px 16px;
white-space: nowrap;
color: @text-muted;
opacity: 0.6;
&:hover {
opacity: 1;
}
.o_kanban_add_column {
.o-kanban-icon(1);
display: inline-block;
padding: 10px 14px;
background-color: @odoo-color-silver-dark;
}
.o_kanban_title {
margin-left: 10px;
}
}
.o_kanban_quick_create {
display: none;
width: @o-kanban-default-record-width;
.o_quick_create_unfolded {
padding: @o-kanban-inside-vgutter @o-kanban-inside-hgutter;
width: @o-kanban-small-record-width;
height: 100%;
margin: 0 (-@o-kanban-group-padding/2);
border: none;
box-shadow: 0 0 20px -10px;
background-color: @gray-lighter;
input, input:focus, input:hover {
font-size: 16px;
background: @gray-lighter;
outline: none;
box-shadow: none;
font-weight: 400;
margin: 0;
}
}
.o_kanban_header {
height: 50px;
&:hover {
.o_column_header, .o_kanban_add_column {
opacity: 1;
.o_kanban_examples {
cursor: pointer;
}
input, input:focus, input:hover {
margin-top: 3px;
font-size: 16px;
background: transparent;
}
.o_kanban_add {
margin-left: 10px;
}
}
.o_kanban_muted_record {
background: @gray-lighter-dark;
height: 70px;
margin: 10px 0px;
}
}
}
+59 -12
View File
@@ -39,24 +39,31 @@
<t t-name="KanbanView.ColumnQuickCreate">
<div class="o_column_quick_create">
<div class="o_column_header">
<div class="o_quick_create_folded">
<span class="o_kanban_add_column"><i class="fa fa-plus"/></span>
<span class="o_kanban_title">Add new Column</span>
<span class="o_kanban_title">Add a new Column</span>
</div>
<div class="o_kanban_quick_create">
<div class="input-group">
<input type="text" class="form-control o_input" placeholder="Column title"/>
<span class="input-group-btn">
<button class="btn btn-primary o_kanban_add" type="button">Add</button>
</span>
<div class="o_quick_create_unfolded">
<div class="o_kanban_header">
<div class="input-group">
<input type="text" class="o_input" placeholder="Column title"/>
<span class="input-group-btn">
<button class="btn btn-sm btn-primary o_kanban_add" type="button">Add</button>
</span>
</div>
<t t-if="widget.examples">
<span class="btn-link o_kanban_examples">See Examples</span>
</t>
</div>
<div class="o_kanban_muted_record"/>
<div class="o_kanban_muted_record"/>
<div class="o_kanban_muted_record"/>
</div>
</div>
</t>
<t t-name="KanbanView.QuickCreate">
<div class="o_kanban_quick_create">
<input type="text" class="o_input" placeholder="Title"/>
<t t-name="KanbanView.RecordQuickCreate.buttons">
<div>
<button class="btn btn-sm btn-primary o_kanban_add">Add</button>
<button class="btn btn-sm btn-primary o_kanban_edit">Edit</button>
<button class="btn btn-sm btn-default o_kanban_cancel ml8">Discard</button>
@@ -98,8 +105,48 @@
</div>
</t>
<!-- Mobile templates -->
<!-- Note: this dialog isn't responsive, but it is not accessible on mobile -->
<div t-name="KanbanView.ExamplesDialog" class="o_kanban_examples_dialog">
<div class="o_kanban_examples_dialog_nav">
<ul class="nav nav-pills nav-stacked">
<t t-foreach="examples" t-as="example">
<li t-att-class="example_index == 0 and 'active' or None">
<a data-toggle="tab" t-attf-href="#example_#{example_index}"> <t t-esc="example.name"/> </a>
</li>
</t>
</ul>
</div>
<div class="o_kanban_examples_dialog_content">
<div class="tab-content">
<t t-foreach="examples" t-as="example">
<div t-attf-id="example_#{example_index}" t-attf-class="tab-pane fade #{example_index == 0 and 'active in' or ''}">
<div t-if="example.description" class="o_kanban_examples_description">
<t t-raw="example.description"/> <!-- description is supposed to be properly escaped -->
</div>
<div class="o_kanban_examples">
<t t-foreach="example.columns" t-as="column">
<div class="o_kanban_examples_group">
<h6><b><t t-esc="column"/></b></h6>
<t t-foreach="_.random(1,4)">
<t t-call="KanbanView.ExamplesDialog.RecordGhost"/>
</t>
</div>
</t>
</div>
</div>
</t>
</div>
</div>
</div>
<t t-name="KanbanView.ExamplesDialog.RecordGhost">
<div class="o_kanban_examples_ghost">
<div class="o_ghost_content"/>
<div class="o_ghost_content o_ghost_tag"/>
<img class="pull-right o_ghost_avatar" src="/base/static/img/avatar.png"/>
</div>
</t>
<!-- Mobile templates -->
<t t-name="KanbanView.MobileTabs">
<div class="o_kanban_mobile_tabs">
<t t-foreach="data" t-as="group">
@@ -3,6 +3,7 @@ odoo.define('web.basic_fields_tests', function (require) {
var basicFields = require('web.basic_fields');
var concurrency = require('web.concurrency');
var config = require('web.config');
var core = require('web.core');
var FormView = require('web.FormView');
var KanbanView = require('web.KanbanView');
@@ -2987,9 +2988,8 @@ QUnit.module('basic_fields', {
res_id: 1,
config: {
device: {
size_class: 0, // Screen XS
SIZES: { XS: 0, SM: 1, MD: 2, LG: 3 },
}
size_class: config.device.SIZES.XS,
},
},
});
@@ -3032,9 +3032,8 @@ QUnit.module('basic_fields', {
arch: '<tree editable="bottom"><field name="foo" widget="phone"/></tree>',
config: {
device: {
size_class: 0, // Screen XS
SIZES: { XS: 0, SM: 1, MD: 2, LG: 3 },
}
size_class: config.device.SIZES.XS,
},
},
});
@@ -3097,9 +3096,8 @@ QUnit.module('basic_fields', {
res_id: 1,
config: {
device: {
size_class: 1, // Screen SM
SIZES: { XS: 0, SM: 1, MD: 2, LG: 3 },
}
size_class: config.device.SIZES.SM,
},
},
});
@@ -3145,9 +3143,8 @@ QUnit.module('basic_fields', {
arch: '<tree editable="bottom"><field name="foo" widget="phone"/></tree>',
config: {
device: {
size_class: 1, // Screen SM
SIZES: { XS: 0, SM: 1, MD: 2, LG: 3 },
}
size_class: config.device.SIZES.SM,
},
},
});
@@ -3199,9 +3196,8 @@ QUnit.module('basic_fields', {
},
config: {
device: {
size_class: 0,
SIZES: { XS: 0, SM: 1, MD: 2, LG: 3 },
}
size_class: config.device.SIZES.XS,
},
},
});
@@ -485,6 +485,12 @@ var MockServer = Class.extend({
_.each(kwargs.views, function (view_descr) {
var viewID = view_descr[0] || false;
var viewType = view_descr[1];
if (!viewID) {
var contextKey = viewType + '_view_ref';
if (contextKey in kwargs.context) {
viewID = kwargs.context[contextKey];
}
}
var key = [model, viewID, viewType].join(',');
var arch = self.archs[key];
if (!arch) {
@@ -346,6 +346,7 @@ function addMockEnvironment(widget, params) {
initialConfig.device = _.clone(config.device);
if ('device' in params.config) {
_.extend(config.device, params.config.device);
config.device.isMobile = config.device.size_class <= config.device.SIZES.XS;
}
if ('debug' in params.config) {
config.debug = params.config.debug;
+716 -98
View File
@@ -1,15 +1,14 @@
odoo.define('web.kanban_tests', function (require) {
"use strict";
var core = require('web.core');
var KanbanColumnProgressBar = require('web.KanbanColumnProgressBar');
var kanbanExamplesRegistry = require('web.kanban_examples_registry');
var KanbanRenderer = require('web.KanbanRenderer');
var KanbanView = require('web.KanbanView');
var testUtils = require('web.test_utils');
var Widget = require('web.Widget');
var widgetRegistry = require('web.widget_registry');
var createAsyncView = testUtils.createAsyncView;
var createView = testUtils.createView;
QUnit.module('Views', {
@@ -331,7 +330,367 @@ QUnit.module('Views', {
kanban.destroy();
});
QUnit.test('quick create and change state in grouped mode', function (assert) {
QUnit.test('quick create record without quick_create_view', function (assert) {
assert.expect(16);
var kanban = createView({
View: KanbanView,
model: 'partner',
data: this.data,
arch: '<kanban on_create="quick_create">' +
'<field name="bar"/>' +
'<templates><t t-name="kanban-box">' +
'<div><field name="foo"/></div>' +
'</t></templates></kanban>',
groupBy: ['bar'],
mockRPC: function (route, args) {
assert.step(args.method || route);
if (args.method === 'name_create') {
assert.strictEqual(args.args[0], 'new partner',
"should send the correct value");
}
return this._super.apply(this, arguments);
},
});
assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 1,
"first column should contain one record");
// click on 'Create' -> should open the quick create in the first column
kanban.$buttons.find('.o-kanban-button-new').click();
var $quickCreate = kanban.$('.o_kanban_group:first .o_kanban_quick_create');
assert.strictEqual($quickCreate.length, 1,
"should have a quick create element in the first column");
assert.strictEqual($quickCreate.find('.o_form_view.o_xxs_form_view').length, 1,
"should have rendered an XXS form view");
assert.strictEqual($quickCreate.find('input').length, 1,
"should have only one input");
assert.ok($quickCreate.find('input').hasClass('o_required_modifier'),
"the field should be required");
assert.strictEqual($quickCreate.find('input[placeholder=Title]').length, 1,
"input placeholder should be 'Title'");
// fill the quick create and validate
$quickCreate.find('input').val('new partner').trigger('input');
$quickCreate.find('button.o_kanban_add').click();
assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 2,
"first column should contain two records");
assert.verifySteps([
'read_group', // initial read_group
'/web/dataset/search_read', // initial search_read (first column)
'/web/dataset/search_read', // initial search_read (second column)
'default_get', // quick create
'name_create', // should perform a name_create to create the record
'read', // read the created record
'default_get', // reopen the quick create automatically
]);
kanban.destroy();
});
QUnit.test('quick create record with quick_create_view', function (assert) {
assert.expect(17);
var kanban = createView({
View: KanbanView,
model: 'partner',
data: this.data,
arch: '<kanban on_create="quick_create" quick_create_view="some_view_ref">' +
'<field name="bar"/>' +
'<templates><t t-name="kanban-box">' +
'<div><field name="foo"/></div>' +
'</t></templates></kanban>',
archs: {
'partner,some_view_ref,form': '<form>' +
'<field name="foo"/>' +
'<field name="int_field"/>' +
'<field name="state" widget="priority"/>' +
'</form>',
},
groupBy: ['bar'],
mockRPC: function (route, args) {
assert.step(args.method || route);
if (args.method === 'create') {
assert.deepEqual(args.args[0], {
foo: 'new partner',
int_field: 4,
state: 'def',
}, "should send the correct values");
}
return this._super.apply(this, arguments);
},
});
assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 1,
"first column should contain one record");
// click on 'Create' -> should open the quick create in the first column
kanban.$buttons.find('.o-kanban-button-new').click();
var $quickCreate = kanban.$('.o_kanban_group:first .o_kanban_quick_create');
assert.strictEqual($quickCreate.length, 1,
"should have a quick create element in the first column");
assert.strictEqual($quickCreate.find('.o_form_view.o_xxs_form_view').length, 1,
"should have rendered an XXS form view");
assert.strictEqual($quickCreate.find('input').length, 2,
"should have two inputs");
assert.strictEqual($quickCreate.find('.o_field_widget').length, 3,
"should have rendered three widgets");
// fill the quick create and validate
$quickCreate.find('.o_field_widget[name=foo]').val('new partner').trigger('input');
$quickCreate.find('.o_field_widget[name=int_field]').val('4').trigger('input');
$quickCreate.find('.o_field_widget[name=state] .o_priority_star:first').click();
$quickCreate.find('button.o_kanban_add').click();
assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 2,
"first column should contain two records");
assert.verifySteps([
'read_group', // initial read_group
'/web/dataset/search_read', // initial search_read (first column)
'/web/dataset/search_read', // initial search_read (second column)
'load_views', // form view in quick create
'default_get', // quick create
'create', // should perform a create to create the record
'read', // read the created record
'load_views', // form view in quick create (is actually in cache)
'default_get', // reopen the quick create automatically
]);
kanban.destroy();
});
QUnit.test('quick create record in grouped on m2o (no quick_create_view)', function (assert) {
assert.expect(12);
var kanban = createView({
View: KanbanView,
model: 'partner',
data: this.data,
arch: '<kanban on_create="quick_create">' +
'<field name="product_id"/>' +
'<templates><t t-name="kanban-box">' +
'<div><field name="foo"/></div>' +
'</t></templates>' +
'</kanban>',
groupBy: ['product_id'],
mockRPC: function (route, args) {
assert.step(args.method || route);
if (args.method === 'name_create') {
assert.strictEqual(args.args[0], 'new partner',
"should send the correct value");
assert.deepEqual(args.kwargs.context, {
default_product_id: 3,
default_qux: 2.5,
}, "should send the correct context");
}
return this._super.apply(this, arguments);
},
viewOptions: {
context: {default_qux: 2.5},
},
});
assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 2,
"first column should contain two records");
// click on 'Create', fill the quick create and validate
kanban.$buttons.find('.o-kanban-button-new').click();
var $quickCreate = kanban.$('.o_kanban_group:first .o_kanban_quick_create');
$quickCreate.find('input').val('new partner').trigger('input');
$quickCreate.find('button.o_kanban_add').click();
assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 3,
"first column should contain three records");
assert.verifySteps([
'read_group', // initial read_group
'/web/dataset/search_read', // initial search_read (first column)
'/web/dataset/search_read', // initial search_read (second column)
'default_get', // quick create
'name_create', // should perform a name_create to create the record
'read', // read the created record
'default_get', // reopen the quick create automatically
]);
kanban.destroy();
});
QUnit.test('quick create record in grouped on m2o (with quick_create_view)', function (assert) {
assert.expect(14);
var kanban = createView({
View: KanbanView,
model: 'partner',
data: this.data,
arch: '<kanban on_create="quick_create" quick_create_view="some_view_ref">' +
'<field name="product_id"/>' +
'<templates><t t-name="kanban-box">' +
'<div><field name="foo"/></div>' +
'</t></templates></kanban>',
archs: {
'partner,some_view_ref,form': '<form>' +
'<field name="foo"/>' +
'<field name="int_field"/>' +
'<field name="state" widget="priority"/>' +
'</form>',
},
groupBy: ['product_id'],
mockRPC: function (route, args) {
assert.step(args.method || route);
if (args.method === 'create') {
assert.deepEqual(args.args[0], {
foo: 'new partner',
int_field: 4,
state: 'def',
}, "should send the correct values");
assert.deepEqual(args.kwargs.context, {
default_product_id: 3,
default_qux: 2.5,
}, "should send the correct context");
}
return this._super.apply(this, arguments);
},
viewOptions: {
context: {default_qux: 2.5},
},
});
assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 2,
"first column should contain two records");
// click on 'Create', fill the quick create and validate
kanban.$buttons.find('.o-kanban-button-new').click();
var $quickCreate = kanban.$('.o_kanban_group:first .o_kanban_quick_create');
$quickCreate.find('.o_field_widget[name=foo]').val('new partner').trigger('input');
$quickCreate.find('.o_field_widget[name=int_field]').val('4').trigger('input');
$quickCreate.find('.o_field_widget[name=state] .o_priority_star:first').click();
$quickCreate.find('button.o_kanban_add').click();
assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 3,
"first column should contain three records");
assert.verifySteps([
'read_group', // initial read_group
'/web/dataset/search_read', // initial search_read (first column)
'/web/dataset/search_read', // initial search_read (second column)
'load_views', // form view in quick create
'default_get', // quick create
'create', // should perform a create to create the record
'read', // read the created record
'load_views', // form view in quick create (is actually in cache)
'default_get', // reopen the quick create automatically
]);
kanban.destroy();
});
QUnit.test('quick create record with default values and onchanges', function (assert) {
assert.expect(11);
this.data.partner.fields.int_field.default = 4;
this.data.partner.onchanges = {
foo: function (obj) {
if (obj.foo) {
obj.int_field = 8;
}
},
};
var kanban = createView({
View: KanbanView,
model: 'partner',
data: this.data,
arch: '<kanban on_create="quick_create" quick_create_view="some_view_ref">' +
'<field name="bar"/>' +
'<templates><t t-name="kanban-box">' +
'<div><field name="foo"/></div>' +
'</t></templates></kanban>',
archs: {
'partner,some_view_ref,form': '<form>' +
'<field name="foo"/>' +
'<field name="int_field"/>' +
'</form>',
},
groupBy: ['bar'],
mockRPC: function (route, args) {
assert.step(args.method || route);
return this._super.apply(this, arguments);
},
});
// click on 'Create' -> should open the quick create in the first column
kanban.$buttons.find('.o-kanban-button-new').click();
var $quickCreate = kanban.$('.o_kanban_group:first .o_kanban_quick_create');
assert.strictEqual($quickCreate.length, 1,
"should have a quick create element in the first column");
assert.strictEqual($quickCreate.find('.o_field_widget[name=int_field]').val(), '4',
"default value should be set");
// fill the 'foo' field -> should trigger the onchange
$quickCreate.find('.o_field_widget[name=foo]').val('new partner').trigger('input');
assert.strictEqual($quickCreate.find('.o_field_widget[name=int_field]').val(), '8',
"onchange should have been triggered");
assert.verifySteps([
'read_group', // initial read_group
'/web/dataset/search_read', // initial search_read (first column)
'/web/dataset/search_read', // initial search_read (second column)
'load_views', // form view in quick create
'default_get', // quick create
'onchange', // default_get's onchange
'onchange', // onchange due to 'foo' field change
]);
kanban.destroy();
});
QUnit.test('quick create record with quick_create_view: modifiers', function (assert) {
assert.expect(3);
var kanban = createView({
View: KanbanView,
model: 'partner',
data: this.data,
arch: '<kanban quick_create_view="some_view_ref">' +
'<field name="bar"/>' +
'<templates><t t-name="kanban-box">' +
'<div><field name="foo"/></div>' +
'</t></templates></kanban>',
archs: {
'partner,some_view_ref,form': '<form>' +
'<field name="foo" required="1"/>' +
'<field name="int_field" attrs=\'{"invisible": [["foo", "=", false]]}\'/>' +
'</form>',
},
groupBy: ['bar'],
});
// create a new record
kanban.$('.o_kanban_group:first .o_kanban_quick_add').click();
var $quickCreate = kanban.$('.o_kanban_group:first .o_kanban_quick_create');
assert.ok($quickCreate.find('.o_field_widget[name=foo]').hasClass('o_required_modifier'),
"foo field should be required");
assert.ok($quickCreate.find('.o_field_widget[name=int_field]').hasClass('o_invisible_modifier'),
"int_field should be invisible");
// fill 'foo' field
$quickCreate.find('.o_field_widget[name=foo]').val('new partner').trigger('input');
assert.notOk($quickCreate.find('.o_field_widget[name=int_field]').hasClass('o_invisible_modifier'),
"int_field should now be visible");
kanban.destroy();
});
QUnit.test('quick create record and change state in grouped mode', function (assert) {
assert.expect(1);
this.data.partner.fields.kanban_state = {
@@ -369,7 +728,7 @@ QUnit.module('Views', {
kanban.destroy();
});
QUnit.test('quick create in grouped mode', function (assert) {
QUnit.test('quick create record: cancel and validate without using the buttons', function (assert) {
assert.expect(8);
var nbRecords = 4;
@@ -377,7 +736,7 @@ QUnit.module('Views', {
View: KanbanView,
model: 'partner',
data: this.data,
arch: '<kanban class="o_kanban_test" on_create="quick_create">' +
arch: '<kanban on_create="quick_create">' +
'<field name="bar"/>' +
'<templates><t t-name="kanban-box">' +
'<div><field name="foo"/></div>' +
@@ -391,34 +750,41 @@ QUnit.module('Views', {
}
});
// click to add an element and cancel the quick creation
// click to add an element and cancel the quick creation by pressing ESC
kanban.$('.o_kanban_header .o_kanban_quick_add i').first().click();
var $quickCreate = kanban.$('.o_kanban_quick_create');
assert.strictEqual($quickCreate.length, 1, "should have a quick create element");
$quickCreate.find('input').trigger($.Event('keydown', {keyCode: $.ui.keyCode.ESCAPE}));
$quickCreate.find('input').trigger($.Event('keydown', {
keyCode: $.ui.keyCode.ESCAPE,
which: $.ui.keyCode.ESCAPE,
}));
assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 0,
"should have destroyed the quick create element");
//click to add and element and focus out the blank input, should cancel the quick creation
// click to add and element and click outside, should cancel the quick creation
kanban.$('.o_kanban_header .o_kanban_quick_add i').first().click();
$quickCreate = kanban.$('.o_kanban_quick_create');
$quickCreate.find('input').blur();
kanban.$('.o_kanban_group .o_kanban_record:first').click();
assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 0,
"Blur should have destroyed the quick create element");
"the quick create should be destroyed when the user clicks outside");
// click to really add an element
kanban.$('.o_kanban_header .o_kanban_quick_add i').first().click();
$quickCreate = kanban.$('.o_kanban_quick_create');
$quickCreate.find('input').val('new partner');
$quickCreate.find('input').blur();
$quickCreate.find('input').val('new partner').trigger('input');
// When focus out the input containing value, should not delete the Quick Creation
// clicking outside should no longer destroy the quick create as it is dirty
kanban.$('.o_kanban_group .o_kanban_record:first').click();
assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 1,
"Blur should not have destroyed the quick create element");
"the quick create should not have been destroyed");
// confirm by pressing ENTER
nbRecords = 5;
$quickCreate.find('button.o_kanban_add').click();
$quickCreate.find('input').trigger($.Event('keydown', {
keyCode: $.ui.keyCode.ENTER,
which: $.ui.keyCode.ENTER,
}));
assert.strictEqual(this.data.partner.records.length, 5,
"should have created a partner");
@@ -472,7 +838,128 @@ QUnit.module('Views', {
kanban.destroy();
});
QUnit.test('quick create and edit in grouped mode', function (assert) {
QUnit.test('quick create record: cancel when not dirty', function (assert) {
assert.expect(9);
var kanban = createView({
View: KanbanView,
model: 'partner',
data: this.data,
arch: '<kanban>' +
'<field name="bar"/>' +
'<templates><t t-name="kanban-box">' +
'<div><field name="foo"/></div>' +
'</t></templates></kanban>',
groupBy: ['bar'],
});
assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 1,
"first column should contain one record");
// click to add an element
kanban.$('.o_kanban_header .o_kanban_quick_add i').first().click();
assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 1,
"should have open the quick create widget");
// click again to add an element -> should have kept the quick create open
kanban.$('.o_kanban_header .o_kanban_quick_add i').first().click();
assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 1,
"should have kept the quick create open");
// click outside: should remove the quick create
kanban.$('.o_kanban_group .o_kanban_record:first').click();
assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 0,
"the quick create should not have been destroyed");
// click to reopen the quick create
kanban.$('.o_kanban_header .o_kanban_quick_add i').first().click();
assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 1,
"should have open the quick create widget");
// press ESC: should remove the quick create
kanban.$('.o_kanban_quick_create input').trigger($.Event('keydown', {
keyCode: $.ui.keyCode.ESCAPE,
which: $.ui.keyCode.ESCAPE,
}));
assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 0,
"quick create widget should have been removed");
// click to reopen the quick create
kanban.$('.o_kanban_header .o_kanban_quick_add i').first().click();
assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 1,
"should have open the quick create widget");
// click on 'Discard': should remove the quick create
kanban.$('.o_kanban_header .o_kanban_quick_add i').first().click();
kanban.$('.o_kanban_group .o_kanban_record:first').click();
assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 0,
"the quick create should be destroyed when the user clicks outside");
assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 1,
"first column should still contain one record");
kanban.destroy();
});
QUnit.test('quick create record: cancel when dirty', function (assert) {
assert.expect(7);
var kanban = createView({
View: KanbanView,
model: 'partner',
data: this.data,
arch: '<kanban>' +
'<field name="bar"/>' +
'<templates><t t-name="kanban-box">' +
'<div><field name="foo"/></div>' +
'</t></templates></kanban>',
groupBy: ['bar'],
});
assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 1,
"first column should contain one record");
// click to add an element and edit it
kanban.$('.o_kanban_header .o_kanban_quick_add i').first().click();
assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 1,
"should have open the quick create widget");
var $quickCreate = kanban.$('.o_kanban_quick_create');
$quickCreate.find('input').val('some value').trigger('input');
// click outside: should not remove the quick create
kanban.$('.o_kanban_group .o_kanban_record:first').click();
assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 1,
"the quick create should not have been destroyed");
// press ESC: should remove the quick create
$quickCreate.find('input').trigger($.Event('keydown', {
keyCode: $.ui.keyCode.ESCAPE,
which: $.ui.keyCode.ESCAPE,
}));
assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 0,
"quick create widget should have been removed");
// click to reopen quick create and edit it
kanban.$('.o_kanban_header .o_kanban_quick_add i').first().click();
assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 1,
"should have open the quick create widget");
$quickCreate = kanban.$('.o_kanban_quick_create');
$quickCreate.find('input').val('some value').trigger('input');
// click on 'Discard': should remove the quick create
kanban.$('.o_kanban_quick_create .o_kanban_cancel').click();
assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 0,
"the quick create should be destroyed when the user clicks outside");
assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 1,
"first column should still contain one record");
kanban.destroy();
});
QUnit.test('quick create record and edit in grouped mode', function (assert) {
assert.expect(6);
var newRecordID;
@@ -512,7 +999,7 @@ QUnit.module('Views', {
var $quickCreate = kanban.$('.o_kanban_quick_create');
kanban.$('.o_kanban_header .o_kanban_quick_add i').first().click();
$quickCreate = kanban.$('.o_kanban_quick_create');
$quickCreate.find('input').val('new partner');
$quickCreate.find('input').val('new partner').trigger('input');
$quickCreate.find('button.o_kanban_edit').click();
assert.strictEqual(this.data.partner.records.length, 5,
@@ -551,7 +1038,8 @@ QUnit.module('Views', {
kanban.$('.o_kanban_quick_create input')
.val('new partner 1')
.trigger($.Event('keypress', {
.trigger('input')
.trigger($.Event('keydown', {
which: $.ui.keyCode.ENTER,
keyCode: $.ui.keyCode.ENTER,
}));
@@ -564,7 +1052,8 @@ QUnit.module('Views', {
// create a second element in a row
kanban.$('.o_kanban_quick_create input')
.val('new partner 2')
.trigger($.Event('keypress', {
.trigger('input')
.trigger($.Event('keydown', {
which: $.ui.keyCode.ENTER,
keyCode: $.ui.keyCode.ENTER,
}));
@@ -623,7 +1112,8 @@ QUnit.module('Views', {
def = $.Deferred();
kanban.$('.o_kanban_quick_create input')
.val('new partner 1')
.trigger($.Event('keypress', {
.trigger('input')
.trigger($.Event('keydown', {
which: $.ui.keyCode.ENTER,
keyCode: $.ui.keyCode.ENTER,
}));
@@ -631,7 +1121,8 @@ QUnit.module('Views', {
def = null;
kanban.$('.o_kanban_quick_create input')
.val('new partner 2')
.trigger($.Event('keypress', {
.trigger('input')
.trigger($.Event('keydown', {
which: $.ui.keyCode.ENTER,
keyCode: $.ui.keyCode.ENTER,
}));
@@ -651,7 +1142,7 @@ QUnit.module('Views', {
kanban.destroy();
});
QUnit.test('quick create fail in grouped', function (assert) {
QUnit.test('quick create record fail in grouped', function (assert) {
assert.expect(7);
var kanban = createView({
@@ -692,7 +1183,11 @@ QUnit.module('Views', {
kanban.$('.o_kanban_quick_create input')
.val('test')
.trigger($.Event('keypress', {keyCode: $.ui.keyCode.ENTER}));
.trigger('input')
.trigger($.Event('keydown', {
keyCode: $.ui.keyCode.ENTER,
which: $.ui.keyCode.ENTER,
}));
assert.strictEqual($('.modal .o_form_view.o_form_editable').length, 1,
"a form view dialog should have been opened (in edit)");
@@ -1142,17 +1637,20 @@ QUnit.module('Views', {
assert.notOk(kanban.$('.o_column_quick_create input').is(':visible'),
"the input should not be visible");
kanban.$('.o_column_quick_create').click();
kanban.$('.o_quick_create_folded').click();
assert.ok(kanban.$('.o_column_quick_create input').is(':visible'),
"the input should be visible");
// discard the column creation and click it again
kanban.$('.o_column_quick_create').click();
kanban.$('.o_column_quick_create input').trigger($.Event('keydown', {
keyCode: $.ui.keyCode.ESCAPE,
which: $.ui.keyCode.ESCAPE,
}));
assert.notOk(kanban.$('.o_column_quick_create input').is(':visible'),
"the input should not be visible after discard");
kanban.$('.o_column_quick_create').click();
kanban.$('.o_quick_create_folded').click();
assert.ok(kanban.$('.o_column_quick_create input').is(':visible'),
"the input should be visible");
@@ -1183,37 +1681,6 @@ QUnit.module('Views', {
kanban.destroy();
});
QUnit.test('quick create record & column in grouped on m2o', function (assert) {
assert.expect(2);
var kanban = createView({
View: KanbanView,
model: 'partner',
data: this.data,
arch: '<kanban on_create="quick_create">' +
'<field name="product_id"/>' +
'<templates><t t-name="kanban-box">' +
'<div><field name="foo"/></div>' +
'</t></templates>' +
'</kanban>',
groupBy: ['product_id'],
});
kanban.$('.o_kanban_group:first .o_kanban_quick_add').click();
var $quickCreate = kanban.$('.o_kanban_quick_create');
$quickCreate.find('input').val('new partner');
$quickCreate.find('button.o_kanban_add').click();
assert.strictEqual(this.data.partner.records.length, 5,
"should have created a partner");
kanban.$('.o_column_quick_create').click();
kanban.$('.o_column_quick_create input').val('new column');
kanban.$('.o_column_quick_create button.o_kanban_add').click();
assert.strictEqual(kanban.$('.o_kanban_group:last span:contains(new column)').length, 1,
"the last column should be the newly created one");
kanban.destroy();
});
QUnit.test('delete a column in grouped on m2o', function (assert) {
assert.expect(28);
@@ -1425,6 +1892,147 @@ QUnit.module('Views', {
kanban.destroy();
});
QUnit.test('quick create column should be opened if there is no column', function (assert) {
assert.expect(3);
var kanban = createView({
View: KanbanView,
model: 'partner',
data: this.data,
arch: '<kanban class="o_kanban_test">' +
'<field name="product_id"/>' +
'<templates><t t-name="kanban-box">' +
'<div><field name="foo"/></div>' +
'</t></templates>' +
'</kanban>',
groupBy: ['product_id'],
domain: [['foo', '=', 'norecord']],
});
assert.strictEqual(kanban.$('.o_kanban_group').length, 0,
"there should be no column");
assert.strictEqual(kanban.$('.o_column_quick_create').length, 1,
"should have a quick create column");
assert.ok(kanban.$('.o_column_quick_create input').is(':visible'),
"the quick create should be opened");
kanban.destroy();
});
QUnit.test('quick create several columns in a row', function (assert) {
assert.expect(10);
var kanban = createView({
View: KanbanView,
model: 'partner',
data: this.data,
arch: '<kanban>' +
'<field name="product_id"/>' +
'<templates><t t-name="kanban-box">' +
'<div><field name="foo"/></div>' +
'</t></templates>' +
'</kanban>',
groupBy: ['product_id'],
});
assert.strictEqual(kanban.$('.o_kanban_group').length, 2,
"should have two columns");
assert.strictEqual(kanban.$('.o_column_quick_create').length, 1,
"should have a ColumnQuickCreate widget");
assert.strictEqual(kanban.$('.o_column_quick_create .o_quick_create_folded:visible').length, 1,
"the ColumnQuickCreate should be folded");
assert.strictEqual(kanban.$('.o_column_quick_create .o_quick_create_unfolded:visible').length, 0,
"the ColumnQuickCreate should be folded");
// add a new column
kanban.$('.o_column_quick_create .o_quick_create_folded').click();
assert.strictEqual(kanban.$('.o_column_quick_create .o_quick_create_folded:visible').length, 0,
"the ColumnQuickCreate should be unfolded");
assert.strictEqual(kanban.$('.o_column_quick_create .o_quick_create_unfolded:visible').length, 1,
"the ColumnQuickCreate should be unfolded");
kanban.$('.o_column_quick_create input').val('New Column 1');
kanban.$('.o_column_quick_create .btn-primary').click();
assert.strictEqual(kanban.$('.o_kanban_group').length, 3,
"should now have three columns");
// add another column
assert.strictEqual(kanban.$('.o_column_quick_create .o_quick_create_folded:visible').length, 0,
"the ColumnQuickCreate should still be unfolded");
assert.strictEqual(kanban.$('.o_column_quick_create .o_quick_create_unfolded:visible').length, 1,
"the ColumnQuickCreate should still be unfolded");
kanban.$('.o_column_quick_create input').val('New Column 2');
kanban.$('.o_column_quick_create .btn-primary').click();
assert.strictEqual(kanban.$('.o_kanban_group').length, 4,
"should now have four columns");
kanban.destroy();
});
QUnit.test('quick create column and examples', function (assert) {
assert.expect(12);
kanbanExamplesRegistry.add('test', [{
name: "A first example",
columns: ["Column 1", "Column 2", "Column 3"],
description: "Some description",
}, {
name: "A second example",
columns: ["Col 1", "Col 2"],
}]);
var kanban = createView({
View: KanbanView,
model: 'partner',
data: this.data,
arch: '<kanban examples="test">' +
'<field name="product_id"/>' +
'<templates><t t-name="kanban-box">' +
'<div><field name="foo"/></div>' +
'</t></templates>' +
'</kanban>',
groupBy: ['product_id'],
});
assert.strictEqual(kanban.$('.o_column_quick_create').length, 1,
"should have a ColumnQuickCreate widget");
// open the quick create
kanban.$('.o_column_quick_create .o_quick_create_folded').click();
assert.strictEqual(kanban.$('.o_column_quick_create .o_kanban_examples:visible').length, 1,
"should have a link to see examples");
// click to see the examples
kanban.$('.o_column_quick_create .o_kanban_examples').click();
assert.strictEqual($('.modal .o_kanban_examples_dialog').length, 1,
"should have open the examples dialog");
assert.strictEqual($('.modal .o_kanban_examples_dialog_nav li').length, 2,
"should have two examples (in the menu)");
assert.strictEqual($('.modal .o_kanban_examples_dialog_nav a').text(),
' A first example A second example ', "example names should be correct");
assert.strictEqual($('.modal .o_kanban_examples_dialog_content .tab-pane').length, 2,
"should have two examples");
var $firstPane = $('.modal .o_kanban_examples_dialog_content .tab-pane:first');
assert.strictEqual($firstPane.find('.o_kanban_examples_group').length, 3,
"there should be 3 stages in the first example");
assert.strictEqual($firstPane.find('h6').text(), 'Column 1Column 2Column 3',
"column titles should be correct");
assert.strictEqual($firstPane.find('.o_kanban_examples_description').text().trim(),
"Some description", "the correct description should be displayed");
var $secondPane = $('.modal .o_kanban_examples_dialog_content .tab-pane:nth(1)');
assert.strictEqual($secondPane.find('.o_kanban_examples_group').length, 2,
"there should be 2 stages in the second example");
assert.strictEqual($secondPane.find('h6').text(), 'Col 1Col 2',
"column titles should be correct");
assert.strictEqual($secondPane.find('.o_kanban_examples_description').text().trim(),
"", "there should be no description for the second example");
kanban.destroy();
});
QUnit.test('if view was grouped at start, it stays grouped', function (assert) {
assert.expect(1);
@@ -2045,15 +2653,15 @@ QUnit.module('Views', {
groupBy: ['product_id'],
});
assert.ok(kanban.$buttons.find('.o-kanban-button-new').hasClass('btn-default'),
"Create button shouldn't be highlighted");
assert.ok(kanban.$buttons.find('.o-kanban-button-new').hasClass('o_hidden'),
"Create button should be hidden");
kanban.$('.o_column_quick_create').click();
kanban.$('.o_column_quick_create input').val('new column');
kanban.$('.o_column_quick_create button.o_kanban_add').click();
assert.ok(kanban.$buttons.find('.o-kanban-button-new').hasClass('btn-primary'),
"Create button should now be highlighted");
assert.notOk(kanban.$buttons.find('.o-kanban-button-new').hasClass('o_hidden'),
"Create button should now be visible");
kanban.destroy();
});
@@ -2137,7 +2745,7 @@ QUnit.module('Views', {
kanban.$('.o_kanban_group:eq(1) .o_kanban_quick_add i').click();
var $quickCreate = kanban.$('.o_kanban_group:eq(1) .o_kanban_quick_create');
$quickCreate.find('input').val('new partner');
$quickCreate.find('input').val('new partner').trigger('input');
$quickCreate.find('button.o_kanban_add').click();
assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_record').length, 0,
@@ -2197,12 +2805,12 @@ QUnit.module('Views', {
kanban.$('.o_kanban_group:eq(0) .o_kanban_quick_add i').click();
var $quickCreate = kanban.$('.o_kanban_group:eq(0) .o_kanban_quick_create');
$quickCreate.find('input').val('record1');
$quickCreate.find('input').val('record1').trigger('input');
$quickCreate.find('button.o_kanban_add').click();
kanban.$('.o_kanban_group:eq(0) .o_kanban_quick_add i').click();
$quickCreate = kanban.$('.o_kanban_group:eq(0) .o_kanban_quick_create');
$quickCreate.find('input').val('record2');
$quickCreate.find('input').val('record2').trigger('input');
$quickCreate.find('button.o_kanban_add').click();
assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_record').length, 2,
@@ -2235,35 +2843,6 @@ QUnit.module('Views', {
kanban.destroy();
});
QUnit.test('don\'t fold column quick create after creation', function (assert) {
assert.expect(2);
var kanban = createView({
View: KanbanView,
model: 'partner',
data: this.data,
arch: '<kanban on_create="quick_create">' +
'<field name="product_id"/>' +
'<templates><t t-name="kanban-box">' +
'<div><field name="foo"/></div>' +
'</t></templates>' +
'</kanban>',
groupBy: ['product_id'],
});
// add a new column
kanban.$('.o_kanban_group:first .o_kanban_quick_add').click();
var $quickCreate = kanban.$('.o_kanban_quick_create');
$quickCreate.find('input').val('new partner');
$quickCreate.find('button.o_kanban_add').click();
assert.strictEqual(this.data.partner.records.length, 5,
"should have created a 'new partner' column");
assert.strictEqual(kanban.$('.o_kanban_add:visible').length, 1,
"the add button should still be visible");
kanban.destroy();
});
QUnit.test('basic support for widgets', function (assert) {
assert.expect(1);
@@ -2379,11 +2958,11 @@ QUnit.module('Views', {
groupBy: ['bar'],
});
var initialCount = parseInt(kanban.$('.o_kanban_counter_side').eq(1).text());
kanban.$('.o_kanban_quick_add').eq(1).click();
kanban.$('.o_input').val('Test');
var initialCount = parseInt(kanban.$('.o_kanban_counter_side:first').text());
kanban.$('.o_kanban_quick_add:first').click();
kanban.$('.o_kanban_quick_create input').val('Test').trigger('input');
kanban.$('.o_kanban_add').click();
var lastCount = parseInt(kanban.$('.o_kanban_counter_side').eq(1).text());
var lastCount = parseInt(kanban.$('.o_kanban_counter_side:first').text());
assert.strictEqual(lastCount, initialCount + 1,
"kanban counters should have updated on quick create");
@@ -2420,6 +2999,45 @@ QUnit.module('Views', {
kanban.destroy();
});
QUnit.test('column progressbars on quick create with quick_create_view are updated', function (assert) {
assert.expect(1);
var kanban = createView({
View: KanbanView,
model: 'partner',
data: this.data,
arch: '<kanban on_create="quick_create" quick_create_view="some_view_ref">' +
'<field name="int_field"/>' +
'<progressbar field="foo" colors=\'{"yop": "success", "gnap": "warning", "blip": "danger"}\' sum_field="int_field"/>' +
'<templates><t t-name="kanban-box">' +
'<div>' +
'<field name="name"/>' +
'</div>' +
'</t></templates>' +
'</kanban>',
archs: {
'partner,some_view_ref,form': '<form>' +
'<field name="int_field"/>' +
'</form>',
},
groupBy: ['bar'],
});
var initialCount = parseInt(kanban.$('.o_kanban_counter_side:first').text());
// click on 'Create', fill the quick create and validate
kanban.$buttons.find('.o-kanban-button-new').click();
var $quickCreate = kanban.$('.o_kanban_group:first .o_kanban_quick_create');
$quickCreate.find('.o_field_widget[name=int_field]').val('44').trigger('input');
$quickCreate.find('button.o_kanban_add').click();
var lastCount = parseInt(kanban.$('.o_kanban_counter_side:first').text());
assert.strictEqual(lastCount, initialCount + 44,
"kanban counters should have been updated on quick create");
kanban.destroy();
});
QUnit.test('keep adding quickcreate in first column after a record from this column was moved', function (assert) {
assert.expect(2);
+5 -1
View File
@@ -122,6 +122,7 @@
<link rel="stylesheet" type="text/less" href="/web/static/src/less/list_view.less"/>
<link rel="stylesheet" type="text/less" href="/web/static/src/less/list_view_extra.less"/>
<link rel="stylesheet" type="text/less" href="/web/static/src/less/kanban_dashboard.less"/>
<link rel="stylesheet" type="text/less" href="/web/static/src/less/kanban_examples_dialog.less"/>
<link rel="stylesheet" type="text/less" href="/web/static/src/less/kanban_column_progressbar.less"/>
<link rel="stylesheet" type="text/less" href="/web/static/src/less/kanban_view.less"/>
<link rel="stylesheet" type="text/less" href="/web/static/src/less/kanban_view_mobile.less"/>
@@ -213,14 +214,17 @@
<script type="text/javascript" src="/web/static/src/js/views/graph/graph_renderer.js"></script>
<script type="text/javascript" src="/web/static/src/js/views/graph/graph_view.js"></script>
<script type="text/javascript" src="/web/static/src/js/views/kanban/kanban_column.js"></script>
<script type="text/javascript" src="/web/static/src/js/views/kanban/kanban_quick_create.js"></script>
<script type="text/javascript" src="/web/static/src/js/views/kanban/kanban_column_progressbar.js"></script>
<script type="text/javascript" src="/web/static/src/js/views/kanban/kanban_column_quick_create.js"></script>
<script type="text/javascript" src="/web/static/src/js/views/kanban/kanban_model.js"></script>
<script type="text/javascript" src="/web/static/src/js/views/kanban/kanban_controller.js"></script>
<script type="text/javascript" src="/web/static/src/js/views/kanban/kanban_examples_registry.js"></script>
<script type="text/javascript" src="/web/static/src/js/views/kanban/kanban_record.js"></script>
<script type="text/javascript" src="/web/static/src/js/views/kanban/kanban_record_quick_create.js"></script>
<script type="text/javascript" src="/web/static/src/js/views/kanban/kanban_renderer.js"></script>
<script type="text/javascript" src="/web/static/src/js/views/kanban/kanban_renderer_mobile.js"></script>
<script type="text/javascript" src="/web/static/src/js/views/kanban/kanban_view.js"></script>
<script type="text/javascript" src="/web/static/src/js/views/kanban/quick_create_form_view.js"></script>
<script type="text/javascript" src="/web/static/src/js/views/list/list_editable_renderer.js"></script>
<script type="text/javascript" src="/web/static/src/js/views/list/list_renderer.js"></script>
<script type="text/javascript" src="/web/static/src/js/views/list/list_view.js"></script>
@@ -12,7 +12,7 @@ var TourManager = require('web_tour.TourManager');
var QWeb = core.qweb;
if (config.device.size_class <= config.device.SIZES.XS) {
if (config.device.isMobile) {
return $.Deferred().reject();
}
+1 -1
View File
@@ -113,7 +113,7 @@ sAnimation.registry.autohideMenu = sAnimation.Class.extend({
*/
_adapt: function () {
this._restore();
if (config.device.size_class < config.device.SIZES.SM) {
if (config.device.isMobile) {
return;
}
@@ -480,7 +480,7 @@ odoo.define('website_sale.website_sale', function (require) {
});
// Deactivate image zoom for mobile devices, since it might prevent users to scroll
if (config.device.size_class > config.device.SIZES.XS) {
if (!config.device.isMobile) {
$('.ecom-zoomable img[data-zoom]').zoomOdoo({ attach: '#o-carousel-product'});
}
});
@@ -248,32 +248,30 @@ tour.register('main_flow_tour', {
content: _t("Click here to <b>create your first opportunity</b> and add it to your pipeline."),
position: "bottom"
}, {
trigger: ".modal-body input:first",
content: _t("Enter the opportunity title."),
trigger: ".o_kanban_quick_create input:first",
content: _t("<b>Choose a name</b> for your opportunity."),
position: "right",
run: "text the_flow.opportunity",
}, {
trigger: ".o_field_widget[name=partner_id] input",
trigger: ".o_kanban_quick_create .o_field_widget[name=partner_id] input",
content: _t("Write the name of your customer to create one on the fly, or select an existing one."),
position: "left",
run: "text the_flow.customer",
}, {
trigger: ".ui-menu-item > a:contains('the_flow.customer')",
in_modal: false,
auto: true,
}, {
trigger: ".modal-footer .btn-primary",
extra_trigger: ".o_field_widget[name=partner_id] > .o_external_button", // Wait name_create
content: _t("Create"),
position: "bottom",
trigger: ".o_kanban_quick_create .o_kanban_add",
extra_trigger: ".o_kanban_quick_create .o_field_widget[name=partner_id] > .o_external_button", // Wait name_create
content: _t("Click here to <b>add your opportunity</b>."),
position: "right",
}, {
trigger: ".o_kanban_group:first-child .o_kanban_record:last-child",
trigger: ".o_kanban_group:first .o_kanban_record:has(span:contains('the_flow.opportunity'))",
content: _t("<b>Drag &amp; drop opportunities</b> between columns as you progress in your sales cycle."),
position: "right",
run: "drag_and_drop .o_opportunity_kanban .o_kanban_group:eq(2) ",
}, {
trigger: ".o_kanban_record:has(span:contains('the_flow.opportunity'))",
extra_trigger: ".o_kanban_group:eq(2) > .o_kanban_record:has(span:contains('the_flow.opportunity'))", // FIXME: this is required due to an issue in tour_manager (see [*])
trigger: ".o_kanban_group:eq(2) > .o_kanban_record:has(span:contains('the_flow.opportunity'))",
content: _t("Click on an opportunity to zoom to it."),
position: "bottom",
}, {