[REF] web: refactor the calendar view

Improve design and introducing several usability improvements:
- Removed customization of an event card, just display event title.
- Added popover, popover consist of two sections:
    1) Show the title, date and time of an event. Date and time are
       automatically computed according to the user timing preferences,
       language and event duration. Can be hidden with hide_date/hide_time
       attribute respectively.
    2) Display all the fields specified inside the <calendar/> element.
       fields can be specified with a widget and its options.
       e.g. <field name="name" string="Custom Name" widget="char" options="{}"/>
- Show avatar in popover for filters.
- Removed hide sidebar feature.
- Increase readability using a better color selection.
- Better UI in mobile.

And, Adapted test cases

Task 1919926

Co-authored-by: jat-odoo <jat@openerp.com>
This commit is contained in:
sri-odoo
2019-08-07 12:30:55 +00:00
committed by Jigar Patel
co-authored by jat-odoo
parent aca0e700ac
commit 369c20a99b
12 changed files with 1277 additions and 519 deletions
@@ -27,13 +27,13 @@ var CalendarController = AbstractController.extend({
custom_events: _.extend({}, AbstractController.prototype.custom_events, { custom_events: _.extend({}, AbstractController.prototype.custom_events, {
changeDate: '_onChangeDate', changeDate: '_onChangeDate',
changeFilter: '_onChangeFilter', changeFilter: '_onChangeFilter',
deleteRecord: '_onDeleteRecord',
dropRecord: '_onDropRecord', dropRecord: '_onDropRecord',
next: '_onNext', next: '_onNext',
openCreate: '_onOpenCreate', openCreate: '_onOpenCreate',
openEvent: '_onOpenEvent', openEvent: '_onOpenEvent',
prev: '_onPrev', prev: '_onPrev',
quickCreate: '_onQuickCreate', quickCreate: '_onQuickCreate',
toggleFullWidth: '_onToggleFullWidth',
updateRecord: '_onUpdateRecord', updateRecord: '_onUpdateRecord',
viewUpdated: '_onViewUpdated', viewUpdated: '_onViewUpdated',
}), }),
@@ -200,6 +200,20 @@ var CalendarController = AbstractController.extend({
this.reload(); this.reload();
} }
}, },
/**
* @private
* @param {OdooEvent} event
*/
_onDeleteRecord: function (event) {
var self = this;
Dialog.confirm(this, _t("Are you sure you want to delete this record ?"), {
confirm_callback: function () {
self.model.deleteRecords([event.data.id], self.modelName).then(function () {
self.reload();
});
}
});
},
/** /**
* @private * @private
* @param {OdooEvent} event * @param {OdooEvent} event
@@ -323,53 +337,22 @@ var CalendarController = AbstractController.extend({
return; return;
} }
var open_dialog = function (readonly) { var options = {
var options = { res_model: self.modelName,
res_model: self.modelName, res_id: id || null,
res_id: id || null, context: event.context || self.context,
context: event.context || self.context, title: _t("Open: ") + event.data.title,
readonly: readonly, on_saved: function () {
title: _t("Open: ") + event.data.title, if (event.data.on_save) {
on_saved: function () { event.data.on_save();
if (event.data.on_save) {
event.data.on_save();
}
self.reload();
},
};
if (readonly) {
if (self.readonlyFormViewId) {
options.view_id = parseInt(self.readonlyFormViewId);
} }
options.buttons = [ self.reload();
{ },
text: _t("Edit"),
classes: 'btn-primary',
close: true,
click: function () { open_dialog(false); }
},
{
text: _t("Delete"),
click: function () {
Dialog.confirm(this, _t("Are you sure you want to delete this record ?"), {
confirm_callback: function () {
self.model.deleteRecords([id], self.modelName)
.then(function () {
self.dialog.destroy();
self.reload();
});
}
});
},
},
{text: _t("Close"), close: true}
];
} else if (self.formViewId) {
options.view_id = parseInt(self.formViewId);
}
self.dialog = new dialogs.FormViewDialog(self, options).open();
}; };
open_dialog(true); if (this.formViewId) {
options.view_id = parseInt(this.formViewId);
}
new dialogs.FormViewDialog(this, options).open();
}, },
/** /**
* @private * @private
@@ -410,15 +393,6 @@ var CalendarController = AbstractController.extend({
self.quickCreating = false; self.quickCreating = false;
}) })
}, },
/**
* Called when we want to open or close the sidebar.
*
* @private
*/
_onToggleFullWidth: function () {
this.model.toggleFullWidth();
this.reload();
},
/** /**
* @private * @private
* @param {OdooEvent} event * @param {OdooEvent} event
@@ -308,13 +308,6 @@ return AbstractModel.extend({
today: function () { today: function () {
this.setDate(moment(new Date())); this.setDate(moment(new Date()));
}, },
/**
* Toggle the sidebar (containing the mini calendar)
*/
toggleFullWidth: function () {
var fullWidth = this.call('local_storage', 'getItem', 'calendar_fullWidth') !== true;
this.call('local_storage', 'setItem', 'calendar_fullWidth', fullWidth);
},
/** /**
* @param {Object} record * @param {Object} record
* @param {integer} record.id * @param {integer} record.id
@@ -410,8 +403,10 @@ return AbstractModel.extend({
snapMinutes: 15, snapMinutes: 15,
longPressDelay: 500, longPressDelay: 500,
eventResizableFromStart: true, eventResizableFromStart: true,
nowIndicator: true,
weekNumbers: true, weekNumbers: true,
weekNumberTitle: _t("W"), weekNumbersWithinDays: true,
weekNumberTitle: _t("Week") + " ",
allDayText: _t("All day"), allDayText: _t("All day"),
monthNames: moment.months(), monthNames: moment.months(),
monthNamesShort: moment.monthsShort(), monthNamesShort: moment.monthsShort(),
@@ -444,7 +439,6 @@ return AbstractModel.extend({
*/ */
_loadCalendar: function () { _loadCalendar: function () {
var self = this; var self = this;
this.data.fullWidth = this.call('local_storage', 'getItem', 'calendar_fullWidth') === true;
this.data.fc_options = this._getFullCalendarOptions(); this.data.fc_options = this._getFullCalendarOptions();
var defs = _.map(this.data.filters, this._loadFilter.bind(this)); var defs = _.map(this.data.filters, this._loadFilter.bind(this));
@@ -0,0 +1,140 @@
odoo.define('web.CalendarPopover', function (require) {
"use strict";
var fieldRegistry = require('web.field_registry');
var StandaloneFieldManagerMixin = require('web.StandaloneFieldManagerMixin');
var Widget = require('web.Widget');
var CalendarPopover = Widget.extend(StandaloneFieldManagerMixin, {
template: 'CalendarView.event.popover',
events: {
'click .o_cw_popover_edit': '_onClickPopoverEdit',
'click .o_cw_popover_delete': '_onClickPopoverDelete',
},
/**
* @constructor
* @param {Widget} parent
* @param {Object} eventInfo
*/
init: function (parent, eventInfo) {
this._super.apply(this, arguments);
StandaloneFieldManagerMixin.init.call(this);
this.hideDate = eventInfo.hideDate;
this.hideTime = eventInfo.hideTime;
this.eventTime = eventInfo.eventTime;
this.eventDate = eventInfo.eventDate;
this.displayFields = eventInfo.displayFields;
this.fields = eventInfo.fields;
this.event = eventInfo.event;
this.modelName = eventInfo.modelName;
},
/**
* @override
*/
willStart: function () {
return Promise.all([this._super.apply(this, arguments), this._processFields()]);
},
/**
* @override
*/
start: function () {
var self = this;
_.each(this.$fieldsList, function ($field) {
$field.appendTo(self.$('.o_cw_popover_fields_secondary'));
});
return this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Generate fields to render into popover
*
* @private
* @returns {Promise}
*/
_processFields: function () {
var self = this;
var fieldsToGenerate = [];
_.each(this.displayFields, function (displayFieldInfo, fieldName) {
var fieldInfo = self.fields[fieldName];
var field = {
name: fieldName,
string: displayFieldInfo.attrs.string || fieldInfo.string,
value: self.event.record[fieldName],
type: fieldInfo.type,
};
if (field.type === 'selection') {
field.selection = fieldInfo.selection;
}
if (fieldInfo.relation) {
field.relation = fieldInfo.relation;
}
if (displayFieldInfo.attrs.widget) {
field.widget = displayFieldInfo.attrs.widget;
} else if (_.contains(['many2many', 'one2many'], field.type)) {
field.widget = 'many2many_tags';
}
if (_.contains(['many2many', 'one2many'], field.type)) {
field.fields = [{
name: 'id',
type: 'integer',
}, {
name: 'display_name',
type: 'char',
}];
}
fieldsToGenerate.push(field);
});
this.$fieldsList = [];
return this.model.makeRecord(this.modelName, fieldsToGenerate).then(function (recordID) {
var defs = [];
var record = self.model.get(recordID);
_.each(fieldsToGenerate, function (field) {
var FieldClass = fieldRegistry.getAny([field.widget, field.type]);
var fieldWidget = new FieldClass(self, field.name, record, self.displayFields[field.name]);
self._registerWidget(recordID, field.name, fieldWidget);
var $field = $('<li>', {class: 'list-group-item flex-shrink-0 d-flex flex-wrap'});
var $fieldLabel = $('<strong>', {class: 'mr-2', text: _.str.sprintf('%s : ', field.string)});
$fieldLabel.appendTo($field);
var $fieldContainer = $('<div>', {class: 'flex-grow-1'});
$fieldContainer.appendTo($field);
defs.push(fieldWidget.appendTo($fieldContainer).then(function () {
self.$fieldsList.push($field);
}));
});
return Promise.all(defs);
});
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
* @param {jQueryEvent} ev
*/
_onClickPopoverEdit: function (ev) {
ev.preventDefault();
this.trigger_up('edit_event', {id: this.event.id});
},
/**
* @private
* @param {jQueryEvent} ev
*/
_onClickPopoverDelete: function (ev) {
ev.preventDefault();
this.trigger_up('delete_event', {id: this.event.id});
},
});
return CalendarPopover;
});
@@ -2,15 +2,14 @@ odoo.define('web.CalendarRenderer', function (require) {
"use strict"; "use strict";
var AbstractRenderer = require('web.AbstractRenderer'); var AbstractRenderer = require('web.AbstractRenderer');
var CalendarPopover = require('web.CalendarPopover');
var config = require('web.config'); var config = require('web.config');
var core = require('web.core'); var core = require('web.core');
var Dialog = require('web.Dialog'); var Dialog = require('web.Dialog');
var field_utils = require('web.field_utils'); var field_utils = require('web.field_utils');
var FieldManagerMixin = require('web.FieldManagerMixin'); var FieldManagerMixin = require('web.FieldManagerMixin');
var QWeb = require('web.QWeb');
var relational_fields = require('web.relational_fields'); var relational_fields = require('web.relational_fields');
var session = require('web.session'); var session = require('web.session');
var utils = require('web.utils');
var Widget = require('web.Widget'); var Widget = require('web.Widget');
var _t = core._t; var _t = core._t;
@@ -78,7 +77,7 @@ var SidebarFilter = Widget.extend(FieldManagerMixin, {
{ {
mode: 'edit', mode: 'edit',
attrs: { attrs: {
placeholder: _.str.sprintf(_t("Add %s"), self.title), placeholder: "+ " + _.str.sprintf(_t("Add %s"), self.title),
can_create: false can_create: false
}, },
}); });
@@ -98,7 +97,7 @@ var SidebarFilter = Widget.extend(FieldManagerMixin, {
this.many2one.filter_ids = _.without(_.pluck(this.filters, 'value'), 'all'); this.many2one.filter_ids = _.without(_.pluck(this.filters, 'value'), 'all');
} }
this.$el.on('click', '.o_remove', this._onFilterRemove.bind(this)); this.$el.on('click', '.o_remove', this._onFilterRemove.bind(this));
this.$el.on('click', '.custom-checkbox input', this._onFilterActive.bind(this)); this.$el.on('click', '.o_calendar_filter_items input', this._onFilterActive.bind(this));
}, },
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
@@ -169,8 +168,12 @@ var SidebarFilter = Widget.extend(FieldManagerMixin, {
return AbstractRenderer.extend({ return AbstractRenderer.extend({
template: "CalendarView", template: "CalendarView",
events: _.extend({}, AbstractRenderer.prototype.events, { config: {
'click .o_calendar_sidebar_toggler': '_onToggleSidebar', CalendarPopover: CalendarPopover,
},
custom_events: _.extend({}, AbstractRenderer.prototype.custom_events || {}, {
edit_event: '_onEditEvent',
delete_event: '_onDeleteEvent',
}), }),
/** /**
@@ -185,11 +188,8 @@ return AbstractRenderer.extend({
this.model = params.model; this.model = params.model;
this.filters = []; this.filters = [];
this.color_map = {}; this.color_map = {};
this.hideDate = params.hideDate;
if (params.eventTemplate) { this.hideTime = params.hideTime;
this.qweb = new QWeb(config.isDebug(), {_s: session.origin});
this.qweb.add_template(utils.json_node_to_xml(params.eventTemplate));
}
}, },
/** /**
* @override * @override
@@ -203,6 +203,20 @@ return AbstractRenderer.extend({
} }
return this._super(); return this._super();
}, },
/**
* @override
*/
on_attach_callback: function () {
if (config.device.isMobile) {
this.$el.height($(window).height() - this.$el.offset().top);
}
var scrollTop = this.$calendar.find('.fc-scroller').scrollTop();
if (scrollTop) {
this.$calendar.fullCalendar('reinitView');
} else {
this.$calendar.fullCalendar('render');
}
},
/** /**
* @override * @override
*/ */
@@ -325,19 +339,14 @@ return AbstractRenderer.extend({
_eventRender: function (event) { _eventRender: function (event) {
var qweb_context = { var qweb_context = {
event: event, event: event,
fields: this.state.fields,
format: this._format.bind(this),
isMobile: config.device.isMobile,
read_only_mode: this.read_only_mode,
record: event.record, record: event.record,
user_context: session.user_context, color: this.getColor(event.color_index),
widget: this,
}; };
this.qweb_context = qweb_context; this.qweb_context = qweb_context;
if (_.isEmpty(qweb_context.record)) { if (_.isEmpty(qweb_context.record)) {
return ''; return '';
} else { } else {
return (this.qweb || qweb).render("calendar-box", qweb_context); return qweb.render("calendar-box", qweb_context);
} }
}, },
/** /**
@@ -375,40 +384,54 @@ return AbstractRenderer.extend({
self.trigger_up('dropRecord', event); self.trigger_up('dropRecord', event);
}, },
eventResize: function (event) { eventResize: function (event) {
self._unselectEvent();
self.trigger_up('updateRecord', event); self.trigger_up('updateRecord', event);
}, },
eventClick: function (event) { eventClick: function (eventData, ev) {
self.trigger_up('openEvent', event); self._unselectEvent();
self.$calendar.fullCalendar('unselect'); self.$calendar.find(_.str.sprintf('[data-event-id=%s]', eventData.id)).addClass('o_cw_custom_highlight');
self._renderEventPopover(eventData, $(ev.currentTarget));
}, },
select: function (target_date, end_date, event, _js_event, _view) { select: function (startDate, endDate) {
var data = {'start': target_date, 'end': end_date}; // Clicking on the view, dispose any visible popover. Otherwise create a new event.
if (self.state.context.default_name) { if (self.$('.o_cw_popover').length) {
data.title = self.state.context.default_name; self._unselectEvent();
} else {
var data = {start: startDate, end: endDate};
if (self.state.context.default_name) {
data.title = self.state.context.default_name;
}
self.trigger_up('openCreate', data);
} }
self.trigger_up('openCreate', data);
self.$calendar.fullCalendar('unselect'); self.$calendar.fullCalendar('unselect');
}, },
eventRender: function (event, element) { eventRender: function (event, element, view) {
var $render = $(self._eventRender(event)); var $render = $(self._eventRender(event));
event.title = $render.find('.o_field_type_char:first').text();
element.find('.fc-content').html($render.html()); element.find('.fc-content').html($render.html());
element.addClass($render.attr('class')); element.addClass($render.attr('class'));
var display_hour = ''; element.attr('data-event-id', event.id);
if (!event.allDay) {
// Add background if doesn't exist
if (!element.find('.fc-bg').length) {
element.find('.fc-content').after($('<div/>', {class: 'fc-bg'}));
}
// For month view: Show background for all-day/multidate events only
if (view.name === 'month' && event.record) {
var start = event.r_start || event.start; var start = event.r_start || event.start;
var end = event.r_end || event.end; var end = event.r_end || event.end;
var timeFormat = _t.database.parameters.time_format.search("%H") != -1 ? 'HH:mm': 'h:mma'; // Detect if the event occurs in just one day
display_hour = start.format(timeFormat) + ' - ' + end.format(timeFormat); // note: add & remove 1 min to avoid issues with 00:00
if (display_hour === '00:00 - 00:00') { var isSameDayEvent = start.clone().add(1, 'minute').isSame(end.clone().subtract(1, 'minute'), 'day');
display_hour = _t('All day'); if (!event.record.allday && isSameDayEvent) {
element.addClass('o_cw_nobg');
} }
} }
element.find('.fc-content .fc-time').text(display_hour);
}, // On double click, edit the event
// Dirty hack to ensure a correct first render element.on('dblclick', function () {
eventAfterAllRender: function () { self.trigger_up('edit_event', {id: event.id});
$(window).trigger('resize'); });
}, },
viewRender: function (view) { viewRender: function (view) {
// compute mode from view.name which is either 'month', 'agendaWeek' or 'agendaDay' // compute mode from view.name which is either 'month', 'agendaWeek' or 'agendaDay'
@@ -418,6 +441,39 @@ return AbstractRenderer.extend({
title: view.title, title: view.title,
}); });
}, },
// Add/Remove a class on hover to style multiple days events.
// The css ":hover" selector can't be used because these events
// are rendered using multiple elements.
eventMouseover: function (eventData) {
self.$calendar.find(_.str.sprintf('[data-event-id=%s]', eventData.id)).addClass('o_cw_custom_hover');
},
eventMouseout: function (eventData) {
self.$calendar.find(_.str.sprintf('[data-event-id=%s]', eventData.id)).removeClass('o_cw_custom_hover');
},
eventDragStart: function () {
self._unselectEvent();
},
eventResizeStart: function () {
self._unselectEvent();
},
eventLimitClick: function () {
self._unselectEvent();
return 'popover';
},
windowResize: function () {
self._render();
},
views: {
day: {
columnFormat: 'LL'
},
week: {
columnFormat: 'ddd D'
},
month: {
columnFormat: config.device.isMobile ? 'ddd' : 'dddd'
}
},
height: 'parent', height: 'parent',
unselectAuto: false, unselectAuto: false,
isRTL: _t.database.parameters.direction === "rtl", isRTL: _t.database.parameters.direction === "rtl",
@@ -440,6 +496,7 @@ return AbstractRenderer.extend({
date: moment(new Date(+obj.currentYear , +obj.currentMonth, +obj.currentDay)) date: moment(new Date(+obj.currentYear , +obj.currentMonth, +obj.currentDay))
}); });
}, },
'showOtherMonths': true,
'dayNamesMin' : this.state.fc_options.dayNamesShort, 'dayNamesMin' : this.state.fc_options.dayNamesShort,
'monthNames': this.state.fc_options.monthNamesShort, 'monthNames': this.state.fc_options.monthNamesShort,
'firstDay': this.state.fc_options.firstDay, 'firstDay': this.state.fc_options.firstDay,
@@ -466,7 +523,6 @@ return AbstractRenderer.extend({
var $calendar = this.$calendar; var $calendar = this.$calendar;
var $fc_view = $calendar.find('.fc-view'); var $fc_view = $calendar.find('.fc-view');
var scrollPosition = $fc_view.scrollLeft(); var scrollPosition = $fc_view.scrollLeft();
var scrollTop = this.$calendar.find('.fc-scroller').scrollTop();
$fc_view.scrollLeft(0); $fc_view.scrollLeft(0);
$calendar.fullCalendar('unselect'); $calendar.fullCalendar('unselect');
@@ -485,8 +541,8 @@ return AbstractRenderer.extend({
.removeClass('o_color o_selected_range'); .removeClass('o_color o_selected_range');
var $a; var $a;
switch (this.state.scale) { switch (this.state.scale) {
case 'month': $a = this.$small_calendar.find('td a'); break; case 'month': $a = this.$small_calendar.find('td'); break;
case 'week': $a = this.$small_calendar.find('tr:has(.ui-state-active) a'); break; case 'week': $a = this.$small_calendar.find('tr:has(.ui-state-active)'); break;
case 'day': $a = this.$small_calendar.find('a.ui-state-active'); break; case 'day': $a = this.$small_calendar.find('a.ui-state-active'); break;
} }
$a.addClass('o_selected_range'); $a.addClass('o_selected_range');
@@ -496,21 +552,8 @@ return AbstractRenderer.extend({
$fc_view.scrollLeft(scrollPosition); $fc_view.scrollLeft(scrollPosition);
var fullWidth = this.state.fullWidth; this._unselectEvent();
this.$('.o_calendar_sidebar_toggler')
.toggleClass('fa-close', !fullWidth)
.toggleClass('fa-chevron-left', fullWidth)
.attr('title', !fullWidth ? _('Close Sidebar') : _('Open Sidebar'));
this.$sidebar_container.toggleClass('o_sidebar_hidden', fullWidth);
this.$sidebar.toggleClass('o_hidden', fullWidth);
var filterProm = this._renderFilters(); var filterProm = this._renderFilters();
this.$calendar.appendTo('body');
if (scrollTop) {
this.$calendar.fullCalendar('reinitView');
} else {
this.$calendar.fullCalendar('render');
}
this._renderEvents(); this._renderEvents();
this.$calendar.prependTo(this.$('.o_calendar_view')); this.$calendar.prependTo(this.$('.o_calendar_view'));
@@ -532,6 +575,8 @@ return AbstractRenderer.extend({
* @returns {Promise} resolved when all filters have been rendered * @returns {Promise} resolved when all filters have been rendered
*/ */
_renderFilters: function () { _renderFilters: function () {
// Dispose of filter popover
this.$('.o_calendar_filter_item').popover('dispose');
_.each(this.filters || (this.filters = []), function (filter) { _.each(this.filters || (this.filters = []), function (filter) {
filter.destroy(); filter.destroy();
}); });
@@ -565,24 +610,152 @@ return AbstractRenderer.extend({
options.fields = this.state.fields; options.fields = this.state.fields;
var filter = new SidebarFilter(self, options); var filter = new SidebarFilter(self, options);
prom = filter.appendTo(this.$sidebar).then(function () { prom = filter.appendTo(this.$sidebar).then(function () {
// Show filter popover
if (options.avatar_field) {
_.each(options.filters, function (filter) {
if (filter.value !== 'all') {
var selector = _.str.sprintf('.o_calendar_filter_item[data-value=%s]', filter.value);
self.$sidebar.find(selector).popover({
animation: false,
trigger: 'hover',
html: true,
placement: 'top',
title: filter.label,
delay: {show: 300, hide: 0},
content: function () {
return $('<img>', {
src: _.str.sprintf('/web/image/%s/%s/%s', options.avatar_model, filter.value, options.avatar_field),
class: 'mx-auto',
});
},
});
}
});
}
return self._renderFiltersOneByOne(filterIndex + 1); return self._renderFiltersOneByOne(filterIndex + 1);
}); });
this.filters.push(filter); this.filters.push(filter);
} }
return Promise.resolve(prom); return Promise.resolve(prom);
}, },
/**
* Prepare context to display in the popover.
*
* @private
* @param {Object} eventData
* @returns {Object} context
*/
_getPopoverContext: function (eventData) {
var context = {
hideDate: this.hideDate,
hideTime: this.hideTime,
eventTime: {},
eventDate: {},
fields: this.state.fields,
displayFields: this.displayFields,
event: eventData,
modelName: this.model,
};
var start = moment(eventData.r_start || eventData.start);
var end = moment(eventData.r_end || eventData.end);
var isSameDayEvent = start.clone().add(1, 'minute').isSame(end.clone().subtract(1, 'minute'), 'day');
// Do not display timing if the event occur across multiple days. Otherwise use user's timing preferences
if (!this.hideTime && !eventData.record.allday && isSameDayEvent) {
// Fetch user's preferences
var dbTimeFormat = _t.database.parameters.time_format.search('%H') != -1 ? 'HH:mm': 'hh:mm a';
context.eventTime.time = start.clone().format(dbTimeFormat) + ' - ' + end.clone().format(dbTimeFormat);
// Calculate duration and format text
var durationHours = moment.duration(end.diff(start)).hours();
var durationHoursKey = (durationHours === 1) ? 'h' : 'hh';
var durationMinutes = moment.duration(end.diff(start)).minutes();
var durationMinutesKey = (durationMinutes === 1) ? 'm' : 'mm';
var localeData = moment.localeData(); // i18n for 'hours' and "minutes" strings
context.eventTime.duration = (durationHours > 0 ? localeData.relativeTime(durationHours, true, durationHoursKey) : '')
+ (durationHours > 0 && durationMinutes > 0 ? ', ' : '')
+ (durationMinutes > 0 ? localeData.relativeTime(durationMinutes, true, durationMinutesKey) : '');
}
if (!this.hideDate) {
if (!isSameDayEvent && start.isSame(end, 'month')) {
// Simplify date-range if an event occurs into the same month (eg. '4-5 August 2019')
context.eventDate.date = start.clone().format('MMMM D') + '-' + end.clone().format('D, YYYY');
} else {
context.eventDate.date = isSameDayEvent ? start.clone().format('dddd, LL') : start.clone().format('LL') + ' - ' + end.clone().format('LL');
}
if (eventData.record.allday && isSameDayEvent) {
context.eventDate.duration = _t("All day");
} else if (eventData.record.allday && !isSameDayEvent) {
var daysLocaleData = moment.localeData();
var days = moment.duration(end.diff(start)).days();
context.eventDate.duration = daysLocaleData.relativeTime(days, true, 'dd');
}
}
return context;
},
/**
* Render event popover
*
* @private
* @param {Object} eventData
* @param {jQueryElement} $eventElement
*/
_renderEventPopover: function (eventData, $eventElement) {
var self = this;
$eventElement.popover({
animation: false,
delay: {
show: 50,
hide: 100
},
trigger: 'manual',
html: true,
title: eventData.record.display_name,
template: qweb.render('CalendarView.event.popover.placeholder', {color: this.getColor(eventData.color_index)}),
container: eventData.allDay ? '.fc-view' : '.fc-scroller',
}).on('shown.bs.popover', function () {
var $popover = $($(this).data('bs.popover').tip);
$popover.find('.o_cw_popover_close').on('click', self._unselectEvent.bind(self));
// Initialize popover widget
var calendarPopover = new self.config.CalendarPopover(self, self._getPopoverContext(eventData));
calendarPopover.replace($popover.find('.o_cw_body'));
}).popover('show');
},
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
// Handlers // Handlers
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
/** /**
* Toggle the sidebar * Remove highlight classes and dispose of popovers
* *
* @private * @private
*/ */
_onToggleSidebar: function () { _unselectEvent: function () {
this.trigger_up('toggleFullWidth'); this.$('.fc-event').removeClass('o_cw_custom_highlight');
this.$('.o_cw_popover').popover('dispose');
},
/**
* @private
* @param {OdooEvent} event
*/
_onEditEvent: function (event) {
this._unselectEvent();
this.trigger_up('openEvent', {_id: event.data.id});
},
/**
* @private
* @param {OdooEvent} event
*/
_onDeleteEvent: function (event) {
this._unselectEvent();
this.trigger_up('deleteRecord', {id: event.data.id});
}, },
}); });
@@ -6,6 +6,7 @@ var CalendarModel = require('web.CalendarModel');
var CalendarController = require('web.CalendarController'); var CalendarController = require('web.CalendarController');
var CalendarRenderer = require('web.CalendarRenderer'); var CalendarRenderer = require('web.CalendarRenderer');
var core = require('web.core'); var core = require('web.core');
var pyUtils = require('web.py_utils');
var utils = require('web.utils'); var utils = require('web.utils');
var _lt = core._lt; var _lt = core._lt;
@@ -66,7 +67,8 @@ var CalendarView = AbstractView.extend({
var fieldName = child.attrs.name; var fieldName = child.attrs.name;
fieldNames.push(fieldName); fieldNames.push(fieldName);
if (!child.attrs.invisible) { if (!child.attrs.invisible) {
displayFields[fieldName] = child.attrs; child.attrs.options = child.attrs.options ? pyUtils.py_eval(child.attrs.options) : {};
displayFields[fieldName] = {attrs: child.attrs};
if (params.sidebar === false) return; // if we have not sidebar, (eg: Dashboard), we don't use the filter "coworkers" if (params.sidebar === false) return; // if we have not sidebar, (eg: Dashboard), we don't use the filter "coworkers"
@@ -102,17 +104,13 @@ var CalendarView = AbstractView.extend({
'filters': [], 'filters': [],
}; };
if (fields[fieldName].relation) { if (fields[fieldName].relation) {
if (['res.users', 'res.partner'].indexOf(fields[fieldName].relation) !== -1) { if (['res.users', 'res.partner', 'hr.employee'].indexOf(fields[fieldName].relation) !== -1) {
filters[fieldName].avatar_field = 'image_64'; filters[fieldName].avatar_field = 'image_64';
} }
filters[fieldName].avatar_model = fields[fieldName].relation; filters[fieldName].avatar_model = fields[fieldName].relation;
} }
} }
if (_.isEmpty(displayFields)) {
displayFields = fields.display_name ? {'display_name': {}} : [];
}
//if quick_add = False, we don't allow quick_add //if quick_add = False, we don't allow quick_add
//if quick_add = not specified in view, we use the default widgets.QuickCreate //if quick_add = not specified in view, we use the default widgets.QuickCreate
//if quick_add = is NOT False and IS specified in view, we this one for widgets.QuickCreate' //if quick_add = is NOT False and IS specified in view, we this one for widgets.QuickCreate'
@@ -132,15 +130,15 @@ var CalendarView = AbstractView.extend({
} }
} }
this.controllerParams.readonlyFormViewId = !attrs.readonly_form_view_id || !utils.toBoolElse(attrs.readonly_form_view_id, true) ? false : attrs.readonly_form_view_id;
this.controllerParams.eventOpenPopup = utils.toBoolElse(attrs.event_open_popup || '', false); this.controllerParams.eventOpenPopup = utils.toBoolElse(attrs.event_open_popup || '', false);
this.controllerParams.mapping = mapping; this.controllerParams.mapping = mapping;
this.controllerParams.context = params.context || {}; this.controllerParams.context = params.context || {};
this.controllerParams.displayName = params.action && params.action.name; this.controllerParams.displayName = params.action && params.action.name;
this.rendererParams.displayFields = displayFields; this.rendererParams.displayFields = displayFields;
this.rendererParams.eventTemplate = _.findWhere(arch.children, {'tag': 'templates'});
this.rendererParams.model = viewInfo.model; this.rendererParams.model = viewInfo.model;
this.rendererParams.hideDate = utils.toBoolElse(attrs.hide_date || '', false);
this.rendererParams.hideTime = utils.toBoolElse(attrs.hide_time || '', false);
this.loadParams.fieldNames = _.uniq(fieldNames); this.loadParams.fieldNames = _.uniq(fieldNames);
this.loadParams.mapping = mapping; this.loadParams.mapping = mapping;
+616 -145
View File
@@ -1,187 +1,658 @@
// Variables
$o-cw-color-today: nth($o-colors, 3);
$o-cw-color-today-accent: #FC3D39;
$o-cw-popup-avatar-size: 16px;
$o-cw-filter-avatar-size: 20px;
// Animations
@keyframes backgroundfade { @keyframes backgroundfade {
from {background-color: rgba($o-brand-odoo, 0.5);} from { background-color: rgba($info, 0.5); }
to {background-color: #f5f5f5;} to { background-color: rgba($info, 0.1); }
} }
.o_calendar_container { .o_calendar_container {
height: 100%; height: 100%;
@include o-webclient-padding($top: $o-horizontal-padding/2, $bottom: $o-horizontal-padding/2);
display: flex; display: flex;
}
@for $index from 1 through $o-nb-calendar-colors { .o_calendar_view {
.o_calendar_color_#{$index} { flex: 1 1 auto;
color: #0D0D0D; min-width: 0;
background-color: adjust-hue(rgb(255, 192, 192), (360/($o-nb-calendar-colors+1) * $index) * 1deg); height: 100%;
border-color: adjust-hue(rgb(255, 192, 192), (360/($o-nb-calendar-colors+1) * $index) * 1deg); background-color: gray('100');
opacity: 0.7; background: linear-gradient(-45deg, gray('100'), white);
&.o_event_hightlight {
font-weight: bold; .fc-event {
opacity: 0.9; margin: 0 1px;
} border-style: solid;
&:hover { border-width: 0 0 0 3px;
color: #666 !important; border-radius: 0;
} box-sizing: border-box;
overflow: hidden;
background: none;
font-size: 11px;
line-height: 1;
&:not([href]):not([tabindex]) {
color: $body-color;
} }
.o_underline_color_#{$index} {
border-bottom: 4px solid adjust-hue(rgba(255, 192, 192, 0.7), (360/($o-nb-calendar-colors+1) * $index) * 1deg); .fc-bg {
background-color: mix(theme-color('primary'), white); // Used for placeholder events only (on creation)
@include size(101%); // Compensate border
opacity: 0.9;
transition: opacity 0.2s;
} }
}
.o_calendar_view { .fc-content {
flex: 1 1 auto; white-space: normal;
min-width: 0; margin: 8px 4px;
font-size: 1.1em;
font-weight: 500;
}
.o_calendar_widget { &.o_cw_custom_highlight {
height: 100%; z-index: 10!important;
background-color: $o-view-background-color;
* { .fc-bg{
z-index: 0; // FIXME opacity: 0.95;
}
.fc-week-number, .fc-widget-header {
background-color: lighten($o-brand-secondary, 30%);
}
.o_avatar_square {
border-radius: 5px;
display: inline-block;
opacity: 1;
border: 1px solid #999;
}
.o_fields img, .o_avatar_square {
vertical-align: middle;
margin-left: 1px;
width: 18px;
height: 18px;
}
.o_field_type_char:first-child {
font-weight: bold;
}
.fc-view {
overflow-x: auto;
} }
} }
} }
.o_calendar_sidebar_container { .o_calendar_widget {
flex: 0 0 $o-horizontal-padding; height: 100%;
position: relative;
&.o_sidebar_hidden { // === Adapt calendar table borders ===
margin-right: -$o-horizontal-padding; // hack to display the toggler in the gutter // =====================================
.o_calendar_sidebar_toggler {
padding: 4px; td {
color: darken($o-webclient-background-color, 30%); border-color: gray('200');
}
.fc-time-grid .fc-slats .fc-minor td {
border-top-color: gray('300');
}
.fc-widget-content {
border-left-color: transparent;
}
.fc-widget-header {
border-color: transparent;
border-bottom-color: $gray-200;
padding: 3px 0 5px;
}
hr.fc-widget-header {
padding: 1px;
border: 0;
background: gray('400');
}
.fc-agenda-view .fc-day-grid .fc-row .fc-content-skeleton {
padding: .5em;
}
.fc-event-container {
color: white;
}
// ====== Specific agenda types ======
// ====================================
// ====== Both Day and Week agenda
.fc-agendaDay-view, .fc-agendaWeek-view {
.fc-axis {
padding-left: $o-horizontal-padding;
}
// Hide unnecessary borders
table td, div.fc-row.fc-week.fc-widget-content {
border-left-color: transparent;
border-right-color: transparent;
}
// Reinfornce default border color
tbody td {
border-top-color: gray('400');
}
// Remove dotted borders (half-hours)
.fc-time-grid .fc-slats .fc-minor td {
border-top-style: none;
}
// Align labels and timelines
.fc-axis.fc-time {
border-top-color: transparent;
span {
max-width: 45px;
margin-top: -19px;
position: relative;
display: block;
}
}
// Add a small gap on top to show the first time label (0:00)
.fc-scroller .fc-time-grid > .fc-slats,
.fc-scroller .fc-time-grid > .fc-bg {
padding-top: 15px;
}
// Row containing "all day" events
div.fc-day-grid {
background-color: $o-view-background-color;
box-shadow: 0 6px 12px -6px rgba(black, 0.16);
+ hr.fc-widget-header {
padding: 1px 0 0;
background: gray('300');
}
.fc-content-skeleton tr:not(:first-child) .fc-h-event{
margin-top: 3px
}
}
// Create a 'preudo-border' for the first row. The actual border
// it's hidden because of border-collapse settings.
.fc-slats tr:first-child td.fc-widget-content:last-child {
box-shadow: inset 0 1px 0 gray('400');
}
.fc-day.fc-widget-content.fc-today.fc-state-highlight {
border-left-color: rgba($o-cw-color-today, 0.3);
border-right-color: rgba($o-cw-color-today, 0.3);
background: rgba($o-cw-color-today, 0.05 );
}
.fc-event {
// Prevent events with similar color to visually overlap each other
box-shadow: 0 0 0 1px white;
&.fc-v-event {
border-width: 3px 0 0;
&.fc-not-start {
border-width: 0 0 3px;
&.fc-not-end {
border-width: 0;
}
}
}
}
// Reset position to keep the "nowIndicator" line visible
.fc-content-col {
position: initial;
} }
} }
.o_calendar_sidebar_toggler { // ====== Day only
@include o-position-absolute($top: 0, $right: 0); .fc-agendaDay-view .fc-event {
cursor: pointer; padding: 10px;
padding: 6px; font-size: 14px;
z-index: 1; }
&:hover {
color: black; // ====== Week only
.fc-agendaWeek-view {
.fc-now-indicator {
left: $o-horizontal-padding;
}
// Expand tiny events on hover/select
.fc-v-event.o_cw_custom_highlight, .fc-v-event.o_cw_custom_hover {
transition: margin .1s .3s, left .1s .3s, right .1s .3s;
margin: 0!important;
right: 1px!important;
left: 1px!important;
} }
} }
.o_calendar_sidebar { // ====== Month only
.fc-month-view {
padding-left: $o-horizontal-padding;
.fc-widget-header {
padding: 3px 0;
}
.fc-week-number {
background: none;
}
.fc-day-number {
margin: 5px;
padding: 0.1rem 0.3rem 0.1rem 0;
font-size: 1.2rem;
color: gray('900');
font-weight: 400;
line-height: 1;
}
.fc-day-top.fc-other-month {
opacity: 0.8;
.fc-day-number {
color: gray('500');
}
}
td:last-child {
border-right-color: transparent;
}
.fc-bg .fc-today {
background: $o-view-background-color;
border-color: gray('300');
}
.fc-content-skeleton .fc-today .fc-day-number {
margin-top: 3px;
padding: 0.4em 0.4em 0.35em;
border-radius: 100%;
min-width: 1.1em;
background: $o-cw-color-today-accent;
text-align: center;
color: white;
font-size: 1.1rem;
}
.fc-more-cell {
> div, .fc-more {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
}
.fc-event {
margin: 0 3px 2px;
.o_event_title {
@include text-truncate();
}
.fc-content {
margin: 4px 4px 3px;
}
&.o_cw_nobg {
.fc-bg {
visibility: hidden;
}
&.o_cw_custom_hover, &.o_cw_custom_highlight, &:hover {
.fc-bg {
visibility: visible;
}
}
}
&.fc-not-start {
border-right-width: 3px;
.fc-content {
padding-left: 6px;
}
}
&.fc-not-end {
margin-right: 0;
.fc-content {
padding-right: 6px;
}
}
}
}
// ====== RTL layout(s)
&.fc-rtl {
.fc-agendaWeek-view .fc-event {
border-width: 0 3px 0 0;
}
.fc-month-view .fc-event {
border-width: 0 3px 0 0;
&.fc-not-start {
margin: 0 0 1px 5px;
border-width: 0 0 0 3px;
.fc-content {
padding-right: 6px;
padding-left: 0;
}
}
&.fc-not-end {
margin: 0 5px 1px 0;
.fc-content {
padding-left: 6px;
padding-right: 0;
}
}
}
}
}
}
.o_calendar_sidebar_container {
flex: 0 0 auto;
position: relative;
@include o-webclient-padding($top: $o-horizontal-padding/2);
background-color: $o-view-background-color;
border-left: 1px solid darken($o-control-panel-background-color, 20%);
overflow-y:auto;
.o_calendar_sidebar {
width: 200px;
font-size: 14px;
@include media-breakpoint-up('xl') {
width: 250px; width: 250px;
margin-left: 8px; }
background: $o-view-background-color; }
.ui-datepicker {
margin: 0;
width: 100%;
padding: 0;
&, td, .ui-datepicker-header, td a, td span {
border: 0;
} }
.ui-datepicker { th {
width: 100%; padding: .7em .2em;
font-size: 8pt; }
border-radius: 0;
border: none; td {
padding: 0; padding: 0;
.ui-widget-header { a, span {
display: flex; padding: .2em .2em .1em;
float: right;
width: 22px;
background: none; background: none;
background-color: lighten($o-brand-secondary, 30%); text-align: center;
border-radius: 0; color: gray('700');
}
.ui-datepicker-prev, .ui-datepicker-next { &.ui-datepicker-current-day a {
flex: 0 0 auto; background: $info;
position: static; color: color-yiq($info);
cursor: pointer; font-weight: bold;
padding: 2px; }
text-align: center;
color: $o-main-text-color; &.ui-datepicker-today a {
&.ui-state-hover { border-radius: 100%;
background: none; padding-top: 0.3em;
border: none; width: 25px;
color: black; background: mix($o-cw-color-today-accent, white, 80%);
} font-weight: bold;
.ui-icon { color: white;
display: none; }
}
&.ui-datepicker-current-day.ui-datepicker-today a {
background: $o-cw-color-today-accent;
}
}
.ui-datepicker-header {
background: none;
}
.ui-datepicker-header {
border-radius: 0;
.ui-icon {
background-image: none;
text-indent: 0;
color: transparent;
&:before {
font: normal normal normal 13px/1 FontAwesome;
content: "\f053";
color: gray('400');
} }
.ui-datepicker-prev { &.ui-icon-circle-triangle-e:before {
order: 1; content: "\f054"
&:after {
font-family: FontAwesome;
content: "";
}
}
.ui-datepicker-next {
order: 3;
&:before {
font-family: FontAwesome;
content: "";
}
}
.ui-datepicker-title {
flex: 0 0 auto;
order: 2;
display: inline-block;
margin: 0 10px;
} }
} }
table {
td { .ui-state-hover.ui-datepicker-next-hover, .ui-state-hover.ui-datepicker-prev-hover {
padding: 0 2px 2px 0; background: none;
background-color: $o-view-background-color; border: none;
a { cursor: pointer;
border: none;
background: none; span:before {
background-color: lighten($o-brand-secondary, 40%); color: gray('800');
text-align: center;
}
}
.ui-state-default {
color: $o-brand-odoo;
&:hover {
background: none;
background-color: rgba($o-brand-odoo, 0.5);
border-color: $o-brand-odoo;
color: $o-view-background-color;
};
}
.ui-state-active {
background: none;
background-color: $o-brand-odoo;
color: $o-view-background-color;
}
.o_selected_range.o_color {
background: none;
background-color: $o-brand-odoo;
animation: backgroundfade 3s forwards;
} }
} }
} }
.o_calendar_filter {
padding: 8px; .o_selected_range.o_color:not(.ui-datepicker-unselectable) {
border-top: 1px solid lightgray; background-color: $info;
.o_field_many2one { animation: backgroundfade 2s forwards;
width: 100%; }
}
.o_calendar_filter {
font-size: 0.9em;
padding: 2em 0 1em;
.o_cw_filter_collapse_icon {
transition: all 0.3s ease;
@include o-hover-opacity();
font-size: 0.7em;
}
.collapsed .o_cw_filter_collapse_icon {
transform: rotate(90deg);
opacity: 1;
}
.o_calendar_filter_item {
cursor: pointer;
overflow: hidden;
input {
z-index: -1;
opacity: 0;
}
.o_cw_filter_input_bg {
@include size(1.3em);
border-width: 2px;
border-style: solid;
border-radius: 1px;
overflow: hidden;
&.o_beside_avatar {
@include size($o-cw-filter-avatar-size);
border-radius: 2px;
}
}
input:not(:checked) + label .o_cw_filter_input_bg {
background: transparent!important;
i.fa {
visibility: hidden;
}
}
.o_cw_filter_avatar {
@include size($o-cw-filter-avatar-size);
border-radius: 2px;
&.fa {
padding: 4px 3px;
}
}
.o_cw_filter_title {
line-height: 1;
flex-grow: 1;
}
button.o_remove {
@include o-position-absolute(0,0,0);
transform: translateX(100%);
transition: transform 0.2s;
}
&:hover {
button.o_remove {
transform: translateX(0%);
}
}
}
.o_field_many2one {
margin-top: 1rem;
width: 100%;
}
}
}
.o_cw_popover {
min-width: 256px;
max-width: 328px;
font-size: $font-size-base;
.card-header, .card-header .popover-header {
font-size: 1.05em;
font-weight: 500;
line-height: 1;
}
.o_cw_popover_close {
cursor: pointer;
}
.o_calendar_avatars {
line-height: 1;
}
.o_calendar_avatars img {
margin-right: 0.4rem;
@include size($o-cw-popup-avatar-size);
border-radius: 100%;
}
.list-group-item {
padding: 0.5rem 1rem;
}
.o_cw_popover_fields_secondary {
max-height: 170px; // Fallback for old browsers
max-height: 25vh;
overflow-y: auto;
&::-webkit-scrollbar {
background: gray('200');
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: gray('500');
}
}
.fc-rtl & {
text-align: right;
.o_calendar_avatars {
> div {
justify-content: flex-end;
}
img {
order: 2;
margin: 0 0 0 0.4rem;
}
}
}
}
// =============== Generate color classes ===============
// These colors are specifically designed for calendar
// =============== =============== ======================
$o-cw-colors: (
#5e0231, // 1:
#97743a, // 2:
#F4A460, // 3:
#F7CD1F, // 4:
#6CC1ED, // 5:
#4285F4, // 6: Default User
#8E24AA, // 7:
#2C8397, // 8:
#475577, // 9:
#D6145F, // 10:
#30C381, // 11:
#9365B8, // 12:
#aa4b6b, // 13:
#348F50, // 14:
#AA3A38, // 15:
#173e43, // 16:
#134E5E, // 17:
#795548, // 18:
#6be585, // 19:
#999966, // 20:
#e9d362, // 21:
#b56969, // 22:
#bdc3c7, // 23:
#649173, // 24:
);
@for $i from 1 to length($o-cw-colors) + 1 {
$color: nth($o-cw-colors, $i);
.o_calendar_view .fc-view {
.fc-event.o_calendar_color_#{$i} {
border-color: $color;
color: darken($color, 35%);
.fc-bg {
background: mix($color, white);
}
&.o_cw_custom_hover, &.o_cw_custom_highlight {
box-shadow: 0 12px 12px -5px rgba($color, 0.3);
color: color-yiq($color);
.fc-bg {
background: $color;
}
} }
} }
} }
.fc-time-grid-event.fc-short .fc-time:before { .o_cw_filter_color_#{$i} {
content: ''; .o_cw_filter_input_bg {
border-color: $color;
background: $color;
color: color-yiq($color);
}
}
.o_cw_popover.o_calendar_color_#{$i} {
$color-subdle: mix(white, $color, 90%);
.card-header, .card-header .popover-header {
background-color: $color-subdle;
color: color-yiq($color-subdle);
}
.card-header {
border-color: mix($card-border-color, mix(white, $color));
}
} }
} }
@@ -1,8 +1,4 @@
@include media-breakpoint-down(sm) { @include media-breakpoint-down(sm) {
.o_calendar_container {
padding: 0;
}
.o_control_panel { .o_control_panel {
.o_calendar_button_today { .o_calendar_button_today {
font-size: 0.7em; font-size: 0.7em;
+67 -45
View File
@@ -5,7 +5,6 @@
<div class="o_calendar_widget" /> <div class="o_calendar_widget" />
</div> </div>
<div class="o_calendar_sidebar_container d-none d-md-block"> <div class="o_calendar_sidebar_container d-none d-md-block">
<i class="o_calendar_sidebar_toggler fa"/>
<div class="o_calendar_sidebar"> <div class="o_calendar_sidebar">
<div class="o_calendar_mini"/> <div class="o_calendar_mini"/>
</div> </div>
@@ -13,58 +12,81 @@
</div> </div>
<t t-name="calendar-box"> <t t-name="calendar-box">
<t t-set="color" t-value="widget.getColor(event.color_index)"/> <div t-att-style="typeof color === 'string' ? ('background-color:' + color) + ';' : ''" t-attf-class="#{!event.color_index ? 'text-white' : ''} #{record.is_highlighted and record.is_highlighted.value ? 'o_event_hightlight' : ''} #{typeof color === 'number' ? _.str.sprintf('o_calendar_color_%s', color) : ''}">
<div t-att-style="typeof color === 'string' ? ('background-color:'+color)+';' : ''" t-attf-class="#{!event.color_index ? 'text-white' : ''} #{record.is_highlighted &amp;&amp; record.is_highlighted.value ? 'o_event_hightlight' : ''} #{typeof color === 'number' ? 'o_calendar_color_'+color : ''}"> <div class="o_event_title" t-esc="record.display_name"/>
<div class="fc-time"/> </div>
<div class="o_fields"> </t>
<t t-foreach="widget.displayFields" t-as="name">
<div t-attf-class="o_field_#{name} o_field_type_#{fields[name].type}"> <t t-name="CalendarView.event.popover.placeholder">
<t t-if="widget.displayFields[name].avatar_field"> <div t-attf-class="o_cw_popover popover card shadow #{typeof color === 'number' ? _.str.sprintf('o_calendar_color_%s', color) : ''}" role="tooltip">
<t t-if="!isMobile"><t t-esc="fields[name].string"/>:</t> <div class="arrow"/>
<div class="o_calendar_avatars float-right"> <div class="card-header d-flex justify-content-between py-2 pr-2">
<t t-foreach="widget.getAvatars(record, name, widget.displayFields[name].avatar_field).slice(0,3)" t-as="image"><t t-raw="image"/></t> <h4 class="popover-header border-0 p-0 pt-1"/>
<span t-if="record[name].length - 3 > 0">+<t t-esc="record[name].length - 3"/></span> <span class="o_cw_popover_close ml-4"><i class="fa fa-close small"/></span>
</div> </div>
</t> <div class="o_cw_body">
<t t-else=""> <div class="py-4 text-center">
<t t-esc="format(record, name)"/> <i class="fa fa-spinner fa-spin fa-2x"></i>
</t> </div>
</div> </div>
</t> </div>
</t>
<t t-name="CalendarView.event.popover">
<div class="o_cw_body">
<ul class="list-group list-group-flush">
<li t-if="!widget.hideDate and widget.eventDate.date" class="list-group-item">
<i class="fa fa-fw fa-calendar-o"/>
<b class="text-capitalize" t-esc="widget.eventDate.date"/> <small t-if="widget.eventDate.duration"><b t-esc="_.str.sprintf('(%s)', widget.eventDate.duration)"/></small>
</li>
<li t-if="!widget.hideTime and widget.eventTime.time" class="list-group-item">
<i class="fa fa-fw fa-clock-o"/>
<b t-esc="widget.eventTime.time"/> <small t-if="widget.eventTime.duration"><b t-esc="_.str.sprintf('(%s)', widget.eventTime.duration)"/></small>
</li>
</ul>
<ul class="list-group list-group-flush o_cw_popover_fields_secondary"/>
<div class="card-footer border-top">
<a href="#" class="btn btn-primary o_cw_popover_edit">Edit</a>
<a href="#" class="btn btn-secondary o_cw_popover_delete ml-2">Delete</a>
</div> </div>
</div> </div>
</t> </t>
<t t-name="CalendarView.sidebar.filter"> <t t-name="CalendarView.sidebar.filter">
<t t-set="needCollapse" t-value="widget.filters.length > 2 and widget.title"></t>
<div class="o_calendar_filter"> <div class="o_calendar_filter">
<h3 t-if="widget.title"><t t-esc="widget.title"/></h3> <h5 t-if="needCollapse" type="button" data-toggle="collapse" t-attf-data-target="#o_cw_filter_collapse_#{widget.title}" class="d-flex justify-content-between align-items-center">
<div class="o_calendar_filter_items"> <t t-esc="widget.title"/><i class="o_cw_filter_collapse_icon fa fa-chevron-down"/>
<div t-foreach="widget.filters" t-as="filter" t-if="filter.display == null || filter.display" class="o_calendar_filter_item" t-att-data-value="filter.value" t-att-data-id="filter.id"> </h5>
<t t-set="id_for_label" t-value="_.uniqueId('o_calendar_filter_item_')"/>
<div class="custom-control custom-control-inline custom-checkbox"> <h5 t-elif="widget.title" t-esc="widget.title"/>
<input type="checkbox"
t-att-id="id_for_label" <div t-attf-class="o_calendar_filter_items #{needCollapse ? 'collapse show' : 0}" aria-expanded="true" t-attf-id="o_cw_filter_collapse_#{widget.title}">
name="selection" <t t-foreach="widget.filters" t-as="filter" t-if="filter.display == null || filter.display">
class="custom-control-input" <t t-set="idForLabel" t-value="_.uniqueId('o_calendar_filter_item_')"/>
t-att-checked="(filter.active ? true : undefined)"/> <t t-set="color" t-value="widget.getColor(filter.color_index)"/>
<label t-att-for="id_for_label" <t t-set="isColorNumber" t-value="typeof color === 'number'"/>
class="custom-control-label">
<t t-if="filter.value == 'all'"> <div t-attf-class="o_calendar_filter_item w-100 position-relative mb-2 #{isColorNumber ? _.str.sprintf('o_cw_filter_color_%s', color) : ''}"
<span><i class="fa fa-users fa-fw o_cal_avatar" role="img" aria-label="Avatar" title="Avatar"/></span> t-att-data-value="filter.value" t-att-data-id="filter.id">
</t>
<t t-if="widget.avatar_field &amp;&amp; (filter.value &amp;&amp; filter.value != 'all')"> <input type="checkbox" t-att-id="idForLabel" name="selection" class="position-absolute" t-att-checked="filter.active ? true : undefined"/>
<img t-attf-src="/web/image/#{widget.avatar_model}/#{filter.value}/#{widget.avatar_field}" class="o_cal_avatar" alt="Avatar"/>
</t> <label t-att-for="idForLabel" class="d-flex align-items-center m-0">
<t t-set="color" t-value="widget.getColor(filter.color_index)"/> <span t-attf-class="o_cw_filter_input_bg align-items-center d-flex flex-shrink-0 justify-content-center position-relative mr-1 #{widget.avatar_field ? 'o_beside_avatar' : ''}"
<span t-if="typeof color === 'number'" t-attf-class="color_filter o_underline_color_#{widget.getColor(filter.color_index)}"><t t-esc="filter.label"/></span> t-att-style="color and !isColorNumber ? _.str.sprintf('border-color:%s;background-color:%s', color, color) : ''">
<span t-elif="color" t-attf-style="border-bottom: 4px solid #{color};"><t t-esc="filter.label"/></span> <i class="fa fa-check position-relative"/>
<span t-else=""><t t-esc="filter.label"/></span> </span>
<i t-if="filter.value == 'all'" class="o_cw_filter_avatar fa fa-users fa-fw flex-shrink-0 mr-1" role="img" aria-label="Avatar" title="Avatar"/>
<img t-elif="widget.avatar_field and filter.value" t-attf-src="/web/image/#{widget.avatar_model}/#{filter.value}/#{widget.avatar_field}" class="o_cw_filter_avatar flex-shrink-0 mr-1" alt="Avatar"/>
<span class="o_cw_filter_title text-truncate flex-grow" t-esc="filter.label" t-att-title="filter.label"/>
</label> </label>
<button t-if="filter.id" class="o_remove btn bg-white text-700 py-0 px-2" title="Remove this favorite from the list" role="img" aria-label="Remove this favorite from the list">
<i class="fa fa-times"/>
</button>
</div> </div>
<t t-if="filter.id"> </t>
<span class="o_remove fa fa-times" title="Remove this favorite from the list" role="img" aria-label="Remove this favorite from the list"/>
</t>
</div>
</div> </div>
</div> </div>
</t> </t>
+181 -184
View File
@@ -161,7 +161,6 @@ QUnit.module('Views', {
'mode="week" '+ 'mode="week" '+
'attendee="partner_ids" '+ 'attendee="partner_ids" '+
'color="partner_id">'+ 'color="partner_id">'+
'<field name="name"/>'+
'<filter name="user_id" avatar_field="image"/>'+ '<filter name="user_id" avatar_field="image"/>'+
'<field name="partner_ids" write_model="filter_partner" write_field="partner_id"/>'+ '<field name="partner_ids" write_model="filter_partner" write_field="partner_id"/>'+
'</calendar>', '</calendar>',
@@ -179,7 +178,7 @@ QUnit.module('Views', {
// test view scales // test view scales
assert.containsN(calendar, '.fc-event', 9, assert.containsN(calendar, '.fc-event', 9,
"should display 9 events on the week (4 event + 5 days event)"); "should display 9 events on the week (4 event + 5 days event)");
assert.containsN($sidebar, '.o_selected_range', 7, assert.containsN($sidebar, 'tr:has(.ui-state-active) td', 7,
"week scale should highlight 7 days in mini calendar"); "week scale should highlight 7 days in mini calendar");
await testUtils.dom.click(calendar.$buttons.find('.o_calendar_button_day')); // display only one day await testUtils.dom.click(calendar.$buttons.find('.o_calendar_button_day')); // display only one day
@@ -190,44 +189,44 @@ QUnit.module('Views', {
await testUtils.dom.click(calendar.$buttons.find('.o_calendar_button_month')); // display all the month await testUtils.dom.click(calendar.$buttons.find('.o_calendar_button_month')); // display all the month
assert.containsN(calendar, '.fc-event', 7, assert.containsN(calendar, '.fc-event', 7,
"should display 7 events on the month (5 events + 2 week event - 1 'event 6' is filtered + 1 'Undefined event')"); "should display 7 events on the month (5 events + 2 week event - 1 'event 6' is filtered + 1 'Undefined event')");
assert.containsN($sidebar, '.o_selected_range', 31, assert.containsN($sidebar, 'td a', 31,
"month scale should highlight all days in mini calendar"); "month scale should highlight all days in mini calendar");
// test filters // test filters
assert.containsN($sidebar, '.o_calendar_filter', 2, "should display 2 filters"); assert.containsN($sidebar, '.o_calendar_filter', 2, "should display 2 filters");
var $typeFilter = $sidebar.find('.o_calendar_filter:has(h3:contains(user))'); var $typeFilter = $sidebar.find('.o_calendar_filter:has(h5:contains(user))');
assert.ok($typeFilter.length, "should display 'user' filter"); assert.ok($typeFilter.length, "should display 'user' filter");
assert.containsN($typeFilter, '.o_calendar_filter_item', 3, "should display 3 filter items for 'user'"); assert.containsN($typeFilter, '.o_calendar_filter_item', 3, "should display 3 filter items for 'user'");
// filters which has no value should show with string "Undefined", should not have any user image and should show at the last // filters which has no value should show with string "Undefined", should not have any user image and should show at the last
assert.strictEqual($typeFilter.find('.o_calendar_filter_item:last').data('value'), false, "filters having false value should be displayed at last in filter items"); assert.strictEqual($typeFilter.find('.o_calendar_filter_item:last').data('value'), false, "filters having false value should be displayed at last in filter items");
assert.strictEqual($typeFilter.find('.o_calendar_filter_item:last span').text(), "Undefined", "filters having false value should display 'Undefined' string"); assert.strictEqual($typeFilter.find('.o_calendar_filter_item:last .o_cw_filter_title').text(), "Undefined", "filters having false value should display 'Undefined' string");
assert.strictEqual($typeFilter.find('.o_calendar_filter_item:last label img').length, 0, "filters having false value should not have any user image"); assert.strictEqual($typeFilter.find('.o_calendar_filter_item:last label img').length, 0, "filters having false value should not have any user image");
var $attendeesFilter = $sidebar.find('.o_calendar_filter:has(h3:contains(attendees))'); var $attendeesFilter = $sidebar.find('.o_calendar_filter:has(h5:contains(attendees))');
assert.ok($attendeesFilter.length, "should display 'attendees' filter"); assert.ok($attendeesFilter.length, "should display 'attendees' filter");
assert.containsN($attendeesFilter, '.o_calendar_filter_item', 3, "should display 3 filter items for 'attendees' who use write_model (2 saved + Everything)"); assert.containsN($attendeesFilter, '.o_calendar_filter_item', 3, "should display 3 filter items for 'attendees' who use write_model (2 saved + Everything)");
assert.ok($attendeesFilter.find('.o_field_many2one').length, "should display one2many search bar for 'attendees' filter"); assert.ok($attendeesFilter.find('.o_field_many2one').length, "should display one2many search bar for 'attendees' filter");
assert.containsN(calendar, '.fc-event', 7, assert.containsN(calendar, '.fc-event', 7,
"should display 7 events ('event 5' counts for 2 because it spans two weeks and thus generate two fc-event elements)"); "should display 7 events ('event 5' counts for 2 because it spans two weeks and thus generate two fc-event elements)");
await testUtils.dom.click(calendar.$('.o_calendar_filter .custom-checkbox input').first()); await testUtils.dom.click(calendar.$('.o_calendar_filter input[type="checkbox"]').first());
assert.containsN(calendar, '.fc-event', 4, "should now only display 4 event"); assert.containsN(calendar, '.fc-event', 4, "should now only display 4 event");
await testUtils.dom.click(calendar.$('.o_calendar_filter .custom-checkbox input').eq(1)); await testUtils.dom.click(calendar.$('.o_calendar_filter input[type="checkbox"]').eq(1));
assert.containsNone(calendar, '.fc-event', "should not display any event anymore"); assert.containsNone(calendar, '.fc-event', "should not display any event anymore");
// test search bar in filter // test search bar in filter
await testUtils.dom.click($sidebar.find('input[type="text"]')); await testUtils.dom.click($sidebar.find('input[type="text"]'));
assert.strictEqual($('ul.ui-autocomplete li:not(.o_m2o_dropdown_option)').length, 2,"should display 2 choices in one2many autocomplete"); // TODO: remove :not(.o_m2o_dropdown_option) because can't have "create & edit" choice assert.strictEqual($('ul.ui-autocomplete li:not(.o_m2o_dropdown_option)').length, 2,"should display 2 choices in one2many autocomplete"); // TODO: remove :not(.o_m2o_dropdown_option) because can't have "create & edit" choice
await testUtils.dom.click($('ul.ui-autocomplete li:first')); await testUtils.dom.click($('ul.ui-autocomplete li:first'));
assert.containsN($sidebar, '.o_calendar_filter:has(h3:contains(attendees)) .o_calendar_filter_item', 4, "should display 4 filter items for 'attendees'"); assert.containsN($sidebar, '.o_calendar_filter:has(h5:contains(attendees)) .o_calendar_filter_item', 4, "should display 4 filter items for 'attendees'");
await testUtils.dom.click($sidebar.find('input[type="text"]')); await testUtils.dom.click($sidebar.find('input[type="text"]'));
assert.strictEqual($('ul.ui-autocomplete li:not(.o_m2o_dropdown_option)').text(), "partner 4", "should display the last choice in one2many autocomplete"); // TODO: remove :not(.o_m2o_dropdown_option) because can't have "create & edit" choice assert.strictEqual($('ul.ui-autocomplete li:not(.o_m2o_dropdown_option)').text(), "partner 4", "should display the last choice in one2many autocomplete"); // TODO: remove :not(.o_m2o_dropdown_option) because can't have "create & edit" choice
await testUtils.dom.click($sidebar.find('.o_calendar_filter_item .o_remove').first(), {allowInvisible: true}); await testUtils.dom.click($sidebar.find('.o_calendar_filter_item .o_remove').first(), {allowInvisible: true});
assert.ok($('.modal-footer button.btn:contains(Ok)').length, "should display the confirm message"); assert.ok($('.modal-footer button.btn:contains(Ok)').length, "should display the confirm message");
await testUtils.dom.click($('.modal-footer button.btn:contains(Ok)')); await testUtils.dom.click($('.modal-footer button.btn:contains(Ok)'));
assert.containsN($sidebar, '.o_calendar_filter:has(h3:contains(attendees)) .o_calendar_filter_item', 3, "click on remove then should display 3 filter items for 'attendees'"); assert.containsN($sidebar, '.o_calendar_filter:has(h5:contains(attendees)) .o_calendar_filter_item', 3, "click on remove then should display 3 filter items for 'attendees'");
calendar.destroy(); calendar.destroy();
}); });
@@ -235,9 +234,7 @@ QUnit.module('Views', {
assert.expect(3); assert.expect(3);
var archs = { var archs = {
'event,1,calendar': '<calendar date_start="start" date_stop="stop" all_day="allday">' + 'event,1,calendar': '<calendar date_start="start" date_stop="stop" all_day="allday"/>',
'<field name="name"/>' +
'</calendar>',
'event,false,search': '<search></search>', 'event,false,search': '<search></search>',
}; };
@@ -290,11 +287,14 @@ QUnit.module('Views', {
'date_start="start" '+ 'date_start="start" '+
'date_stop="stop" '+ 'date_stop="stop" '+
'all_day="allday" '+ 'all_day="allday" '+
'mode="month" '+ 'mode="month"/>',
'readonly_form_view_id="1">'+
'<field name="name"/>'+
'</calendar>',
archs: archs, archs: archs,
mockRPC: function (route, args) {
if (args.method === 'write') {
assert.deepEqual(args.args[1], {name: 'event 4 modified'}, "should update the record");
}
return this._super.apply(this, arguments);
},
viewOptions: { viewOptions: {
initialDate: initialDate, initialDate: initialDate,
}, },
@@ -306,20 +306,19 @@ QUnit.module('Views', {
await testUtils.dom.click(calendar.$('.fc-event:contains(event 4) .fc-content')); await testUtils.dom.click(calendar.$('.fc-event:contains(event 4) .fc-content'));
assert.ok(calendar.$('.o_cw_popover').length, "should open a popover clicking on event");
assert.ok(calendar.$('.o_cw_popover .o_cw_popover_edit').length, "popover should have an edit button");
assert.ok(calendar.$('.o_cw_popover .o_cw_popover_delete').length, "popover should have a delete button");
assert.ok(calendar.$('.o_cw_popover .o_cw_popover_close').length, "popover should have a close button");
await testUtils.dom.click(calendar.$('.o_cw_popover .o_cw_popover_edit'));
assert.ok($('.modal-body').length, "should open the form view in dialog when click on event"); assert.ok($('.modal-body').length, "should open the form view in dialog when click on event");
assert.ok($('.modal-footer button.btn:contains(Edit)').length, "formViewDialog should be in readonly mode");
assert.ok($('.modal-footer button.btn:contains(Delete)').length, "formViewDialog should have a delete button");
await testUtils.dom.click($('.modal-footer button.btn:contains(Edit)'));
assert.ok($('.modal-body').length, "should switch the modal in edit mode");
assert.notOk($('.modal-footer button.btn:contains(Delete)').length, "formViewDialog should not have a delete button in edit mode");
await testUtils.fields.editInput($('.modal-body input:first'), 'event 4 modified'); await testUtils.fields.editInput($('.modal-body input:first'), 'event 4 modified');
await testUtils.dom.click($('.modal-footer button.btn:contains(Save)')); await testUtils.dom.click($('.modal-footer button.btn:contains(Save)'));
assert.notOk($('.modal-body').length, "save button should close the modal"); assert.notOk($('.modal-body').length, "save button should close the modal");
assert.ok(calendar.$('.fc-event:contains(event 4 modified)').length, "should display the updated records");
// create a new event, quick create only // create a new event, quick create only
@@ -406,7 +405,7 @@ QUnit.module('Views', {
// delete the a record // delete the a record
await testUtils.dom.click(calendar.$('.fc-event:contains(event 4) .fc-content')); await testUtils.dom.click(calendar.$('.fc-event:contains(event 4) .fc-content'));
await testUtils.dom.click($('.modal-footer button.btn:contains(Delete)')); await testUtils.dom.click(calendar.$('.o_cw_popover .o_cw_popover_delete'));
assert.ok($('.modal-footer button.btn:contains(Ok)').length, "should display the confirm message"); assert.ok($('.modal-footer button.btn:contains(Ok)').length, "should display the confirm message");
await testUtils.dom.click($('.modal-footer button.btn:contains(Ok)')); await testUtils.dom.click($('.modal-footer button.btn:contains(Ok)'));
assert.notOk(calendar.$('.fc-event:contains(event 4) .fc-content').length, "the record should be deleted"); assert.notOk(calendar.$('.fc-event:contains(event 4) .fc-content').length, "the record should be deleted");
@@ -435,10 +434,7 @@ QUnit.module('Views', {
'date_start="start" '+ 'date_start="start" '+
'date_stop="stop" '+ 'date_stop="stop" '+
'all_day="allday" '+ 'all_day="allday" '+
'mode="month" '+ 'mode="month"/>',
'readonly_form_view_id="1">'+
'<field name="name"/>'+
'</calendar>',
archs: archs, archs: archs,
viewOptions: { viewOptions: {
initialDate: initialDate, initialDate: initialDate,
@@ -497,8 +493,7 @@ QUnit.module('Views', {
'date_start="start" '+ 'date_start="start" '+
'date_stop="stop" '+ 'date_stop="stop" '+
'all_day="allday" '+ 'all_day="allday" '+
'mode="week" '+ 'mode="week">'+
'readonly_form_view_id="1">'+
'<field name="name"/>'+ '<field name="name"/>'+
'<field name="start"/>'+ '<field name="start"/>'+
'<field name="allday"/>'+ '<field name="allday"/>'+
@@ -541,7 +536,7 @@ QUnit.module('Views', {
testUtils.dom.triggerPositionalMouseEvent(left, top + 60, "mousemove"); testUtils.dom.triggerPositionalMouseEvent(left, top + 60, "mousemove");
assert.strictEqual(calendar.$('.fc-content .fc-time').text(), "08:00 - 10:00", assert.strictEqual(calendar.$('.fc-content .fc-time').text(), "8:00 - 10:00",
"should display the time in the calendar sticker"); "should display the time in the calendar sticker");
await testUtils.dom.triggerPositionalMouseEvent(left, top + 60, "mouseup"); await testUtils.dom.triggerPositionalMouseEvent(left, top + 60, "mouseup");
@@ -550,8 +545,8 @@ QUnit.module('Views', {
await testUtils.dom.click($('.modal button.btn:contains(Create)')); await testUtils.dom.click($('.modal button.btn:contains(Create)'));
var $newevent = calendar.$('.fc-event:contains(new event)'); var $newevent = calendar.$('.fc-event:contains(new event)');
assert.strictEqual($newevent.text().replace(/[\s\n\r]+/g, ''), "08:00-10:00newevent12/13/201608:00:00False", assert.strictEqual($newevent.find('.o_event_title').text(), "new event",
"should display the new event with time, title and additional fields"); "should display the new event with title");
assert.deepEqual($newevent.data('fcSeg').event.record, assert.deepEqual($newevent.data('fcSeg').event.record,
{ {
@@ -567,7 +562,7 @@ QUnit.module('Views', {
// delete record // delete record
await testUtils.dom.click($newevent); await testUtils.dom.click($newevent);
await testUtils.dom.click($('.modal button.btn-secondary:contains(Delete)')); await testUtils.dom.click(calendar.$('.o_cw_popover .o_cw_popover_delete'));
await testUtils.dom.click($('.modal button.btn-primary:contains(Ok)')); await testUtils.dom.click($('.modal button.btn-primary:contains(Ok)'));
assert.containsNone(calendar, '.fc-content', "should delete the record"); assert.containsNone(calendar, '.fc-content', "should delete the record");
@@ -595,7 +590,7 @@ QUnit.module('Views', {
initialDate: initialDate, initialDate: initialDate,
}, },
}).then(function (calendar) { }).then(function (calendar) {
assert.strictEqual(calendar.$('.fc-day-header').first().text(), "Sun 12/11", assert.strictEqual(calendar.$('.fc-day-header').first().text(), "Sun 11",
"The first day of the week should be Sunday"); "The first day of the week should be Sunday");
calendar.destroy(); calendar.destroy();
done(); done();
@@ -626,7 +621,7 @@ QUnit.module('Views', {
week_start: 1, week_start: 1,
}, },
}).then(function (calendar) { }).then(function (calendar) {
assert.strictEqual(calendar.$('.fc-day-header').first().text(), "Mon 12/12", assert.strictEqual(calendar.$('.fc-day-header').first().text(), "Mon 12",
"The first day of the week should be Monday"); "The first day of the week should be Monday");
calendar.destroy(); calendar.destroy();
done(); done();
@@ -659,15 +654,89 @@ QUnit.module('Views', {
week_start: 7, week_start: 7,
}, },
}).then(function (calendar) { }).then(function (calendar) {
assert.strictEqual(calendar.$('.fc-week-number').text(), "W51", assert.strictEqual(calendar.$('.fc-week-number').text(), "Week 51",
"We should be on the 51st week"); "We should be on the 51st week");
calendar.destroy(); calendar.destroy();
done(); done();
}); });
}); });
QUnit.test('render popover', async function (assert) {
assert.expect(14);
var calendar = await createCalendarView({
View: CalendarView,
model: 'event',
data: this.data,
arch:
'<calendar class="o_calendar_test" '+
'date_start="start" '+
'date_stop="stop" '+
'all_day="allday" '+
'mode="week">'+
'<field name="name" string="Custom Name"/>'+
'<field name="partner_id"/>'+
'</calendar>',
archs: archs,
viewOptions: {
initialDate: initialDate,
},
});
await testUtils.dom.click($('.fc-event:contains(event 4)'));
assert.containsOnce(calendar, '.o_cw_popover', "should open a popover clicking on event");
assert.strictEqual(calendar.$('.o_cw_popover .popover-header').text(), 'event 4', "popover should have a title 'event 4'");
assert.containsOnce(calendar, '.o_cw_popover .o_cw_popover_edit', "popover should have an edit button");
assert.containsOnce(calendar, '.o_cw_popover .o_cw_popover_delete', "popover should have a delete button");
assert.containsOnce(calendar, '.o_cw_popover .o_cw_popover_close', "popover should have a close button");
assert.strictEqual(calendar.$('.o_cw_popover .list-group-item:first b.text-capitalize').text(), 'Wednesday, December 14, 2016', "should display date 'Wednesday, December 14, 2016'");
assert.containsN(calendar, '.o_cw_popover .o_cw_popover_fields_secondary .list-group-item', 2, "popover should have a two fields");
assert.containsOnce(calendar, '.o_cw_popover .o_cw_popover_fields_secondary .list-group-item:first .o_field_char', "should apply char widget");
assert.strictEqual(calendar.$('.o_cw_popover .o_cw_popover_fields_secondary .list-group-item:first strong').text(), 'Custom Name : ', "label should be a 'Custom Name'");
assert.strictEqual(calendar.$('.o_cw_popover .o_cw_popover_fields_secondary .list-group-item:first .o_field_char').text(), 'event 4', "value should be a 'event 4'");
assert.containsOnce(calendar, '.o_cw_popover .o_cw_popover_fields_secondary .list-group-item:last .o_form_uri', "should apply m20 widget");
assert.strictEqual(calendar.$('.o_cw_popover .o_cw_popover_fields_secondary .list-group-item:last strong').text(), 'user : ', "label should be a 'user'");
assert.strictEqual(calendar.$('.o_cw_popover .o_cw_popover_fields_secondary .list-group-item:last .o_form_uri').text(), 'partner 1', "value should be a 'partner 1'");
await testUtils.dom.click($('.o_cw_popover .o_cw_popover_close'));
assert.containsNone(calendar, '.o_cw_popover', "should close a popover");
calendar.destroy();
});
QUnit.test('attributes hide_date and hide_time', async function (assert) {
assert.expect(1);
var calendar = await createCalendarView({
View: CalendarView,
model: 'event',
data: this.data,
arch:
'<calendar class="o_calendar_test" '+
'date_start="start" '+
'date_stop="stop" '+
'hide_date="true" '+
'hide_time="true" '+
'mode="month">'+
'</calendar>',
archs: archs,
viewOptions: {
initialDate: initialDate,
},
});
await testUtils.dom.click($('.fc-event:contains(event 4)'));
assert.containsNone(calendar, '.o_cw_popover .list-group-item', "popover should not contain date/time");
calendar.destroy();
});
QUnit.test('create event with timezone in week mode with formViewDialog European locale', async function (assert) { QUnit.test('create event with timezone in week mode with formViewDialog European locale', async function (assert) {
assert.expect(8); assert.expect(7);
this.data.event.records = []; this.data.event.records = [];
this.data.event.onchanges = { this.data.event.onchanges = {
@@ -692,8 +761,7 @@ QUnit.module('Views', {
'date_start="start" '+ 'date_start="start" '+
'date_stop="stop" '+ 'date_stop="stop" '+
'all_day="allday" '+ 'all_day="allday" '+
'mode="week" '+ 'mode="week">'+
'readonly_form_view_id="1">'+
'<field name="name"/>'+ '<field name="name"/>'+
'</calendar>', '</calendar>',
archs: archs, archs: archs,
@@ -771,8 +839,8 @@ QUnit.module('Views', {
await testUtils.dom.click($('.modal-lg button.btn:contains(Save)')); await testUtils.dom.click($('.modal-lg button.btn:contains(Save)'));
var $newevent = calendar.$('.fc-event:contains(new event)'); var $newevent = calendar.$('.fc-event:contains(new event)');
assert.strictEqual($newevent.text().replace(/[\s\n\r]+/g, ''), "08:00-10:00newevent", assert.strictEqual($newevent.find('.o_event_title').text(), "new event",
"should display the new event with time and title"); "should display the new event with title");
assert.deepEqual($newevent.data('fcSeg').event.record, assert.deepEqual($newevent.data('fcSeg').event.record,
{ {
@@ -831,8 +899,7 @@ QUnit.module('Views', {
'date_start="start" '+ 'date_start="start" '+
'date_stop="stop" '+ 'date_stop="stop" '+
'all_day="allday" '+ 'all_day="allday" '+
'mode="week" '+ 'mode="week">'+
'readonly_form_view_id="1">'+
'<field name="name"/>'+ '<field name="name"/>'+
'<field name="start"/>'+ '<field name="start"/>'+
'<field name="allday"/>'+ '<field name="allday"/>'+
@@ -875,7 +942,7 @@ QUnit.module('Views', {
testUtils.dom.triggerPositionalMouseEvent(left, top + 60, "mousemove"); testUtils.dom.triggerPositionalMouseEvent(left, top + 60, "mousemove");
assert.strictEqual(calendar.$('.fc-content .fc-time').text(), "8:00am - 10:00am", assert.strictEqual(calendar.$('.fc-content .fc-time').text(), "8:00 - 10:00",
"should display the time in the calendar sticker"); "should display the time in the calendar sticker");
testUtils.dom.triggerPositionalMouseEvent(left, top + 60, "mouseup"); testUtils.dom.triggerPositionalMouseEvent(left, top + 60, "mouseup");
@@ -884,8 +951,8 @@ QUnit.module('Views', {
await testUtils.dom.click($('.modal button.btn:contains(Create)')); await testUtils.dom.click($('.modal button.btn:contains(Create)'));
var $newevent = calendar.$('.fc-event:contains(new event)'); var $newevent = calendar.$('.fc-event:contains(new event)');
assert.strictEqual($newevent.text().replace(/[\s\n\r]+/g, ''), "8:00am-10:00amnewevent12/13/201608:00:00False", assert.strictEqual($newevent.find('.o_event_title').text(), "new event",
"should display the new event with time, title and additional fields"); "should display the new event with title");
assert.deepEqual($newevent.data('fcSeg').event.record, assert.deepEqual($newevent.data('fcSeg').event.record,
{ {
@@ -901,7 +968,7 @@ QUnit.module('Views', {
// delete record // delete record
await testUtils.dom.click($newevent); await testUtils.dom.click($newevent);
await testUtils.dom.click($('.modal button.btn-secondary:contains(Delete)')); await testUtils.dom.click(calendar.$('.o_cw_popover .o_cw_popover_delete'));
await testUtils.dom.click($('.modal button.btn-primary:contains(Ok)')); await testUtils.dom.click($('.modal button.btn-primary:contains(Ok)'));
assert.containsNone(calendar, '.fc-content', "should delete the record"); assert.containsNone(calendar, '.fc-content', "should delete the record");
@@ -909,7 +976,7 @@ QUnit.module('Views', {
}); });
QUnit.test('create event with timezone in week mode with formViewDialog American locale', async function (assert) { QUnit.test('create event with timezone in week mode with formViewDialog American locale', async function (assert) {
assert.expect(8); assert.expect(7);
this.data.event.records = []; this.data.event.records = [];
this.data.event.onchanges = { this.data.event.onchanges = {
@@ -934,8 +1001,7 @@ QUnit.module('Views', {
'date_start="start" '+ 'date_start="start" '+
'date_stop="stop" '+ 'date_stop="stop" '+
'all_day="allday" '+ 'all_day="allday" '+
'mode="week" '+ 'mode="week">'+
'readonly_form_view_id="1">'+
'<field name="name"/>'+ '<field name="name"/>'+
'</calendar>', '</calendar>',
archs: archs, archs: archs,
@@ -1013,8 +1079,8 @@ QUnit.module('Views', {
await testUtils.dom.click($('.modal-lg button.btn:contains(Save)')); await testUtils.dom.click($('.modal-lg button.btn:contains(Save)'));
var $newevent = calendar.$('.fc-event:contains(new event)'); var $newevent = calendar.$('.fc-event:contains(new event)');
assert.strictEqual($newevent.text().replace(/[\s\n\r]+/g, ''), "8:00am-10:00amnewevent", assert.strictEqual($newevent.find('.o_event_title').text(), "new event",
"should display the new event with time and title"); "should display the new event with title");
assert.deepEqual($newevent.data('fcSeg').event.record, assert.deepEqual($newevent.data('fcSeg').event.record,
{ {
@@ -1058,7 +1124,7 @@ QUnit.module('Views', {
calendar.destroy(); calendar.destroy();
}); });
QUnit.test('check calendar week column timeformat and event content timeformat', async function (assert) { QUnit.test('check calendar week column timeformat', async function (assert) {
assert.expect(2); assert.expect(2);
var calendar = await createCalendarView({ var calendar = await createCalendarView({
@@ -1066,9 +1132,7 @@ QUnit.module('Views', {
model: 'event', model: 'event',
data: this.data, data: this.data,
arch: arch:
'<calendar date_start="start">'+ '<calendar date_start="start"/>',
'<field name="name"/>'+
'</calendar>',
archs: archs, archs: archs,
viewOptions: { viewOptions: {
initialDate: initialDate, initialDate: initialDate,
@@ -1079,7 +1143,7 @@ QUnit.module('Views', {
}); });
assert.strictEqual(calendar.$('.fc-axis:contains(8am)').length, 1, "calendar should show according to timeformat"); assert.strictEqual(calendar.$('.fc-axis:contains(8am)').length, 1, "calendar should show according to timeformat");
assert.strictEqual(calendar.$('.fc-event:first:contains(12:00am)').length, 1, assert.strictEqual(calendar.$('.fc-axis:contains(11pm)').length, 1,
"event time format should 12 hour"); "event time format should 12 hour");
calendar.destroy(); calendar.destroy();
@@ -1100,8 +1164,7 @@ QUnit.module('Views', {
'date_start="start" '+ 'date_start="start" '+
'date_stop="stop" '+ 'date_stop="stop" '+
'all_day="allday" '+ 'all_day="allday" '+
'mode="week" '+ 'mode="week">'+
'readonly_form_view_id="1">'+
'<field name="name"/>'+ '<field name="name"/>'+
'</calendar>', '</calendar>',
archs: archs, archs: archs,
@@ -1164,10 +1227,7 @@ QUnit.module('Views', {
'date_start="start" '+ 'date_start="start" '+
'date_stop="stop" '+ 'date_stop="stop" '+
'mode="week" '+ 'mode="week" '+
'quick_add="False" '+ 'quick_add="False"/>',
'readonly_form_view_id="1">'+
'<field name="name"/>'+
'</calendar>',
archs: archs, archs: archs,
viewOptions: { viewOptions: {
initialDate: initialDate, initialDate: initialDate,
@@ -1217,8 +1277,7 @@ QUnit.module('Views', {
'event_open_popup="true" '+ 'event_open_popup="true" '+
'date_start="start" '+ 'date_start="start" '+
'date_stop="stop" '+ 'date_stop="stop" '+
'mode="month" '+ 'mode="month">'+
'readonly_form_view_id="1">'+
'<field name="name"/>'+ '<field name="name"/>'+
'</calendar>', '</calendar>',
archs: archs, archs: archs,
@@ -1243,14 +1302,14 @@ QUnit.module('Views', {
}, },
}, {positionalClicks: true}); }, {positionalClicks: true});
var pos = calendar.$('.fc-bg td:eq(20)').offset(); var pos = calendar.$('.fc-bg td:eq(17)').offset();
try { try {
testUtils.dom.triggerPositionalMouseEvent(pos.left+15, pos.top+15, "mousedown"); testUtils.dom.triggerPositionalMouseEvent(pos.left+15, pos.top+15, "mousedown");
} catch (e) { } catch (e) {
calendar.destroy(); calendar.destroy();
throw new Error('The test fails to simulate a click in the screen. Your screen is probably too small or your dev tools is open.'); throw new Error('The test fails to simulate a click in the screen. Your screen is probably too small or your dev tools is open.');
} }
pos = calendar.$('.fc-bg td:eq(21)').offset(); pos = calendar.$('.fc-bg td:eq(18)').offset();
testUtils.dom.triggerPositionalMouseEvent(pos.left+15, pos.top+15, "mousemove"); testUtils.dom.triggerPositionalMouseEvent(pos.left+15, pos.top+15, "mousemove");
testUtils.dom.triggerPositionalMouseEvent(pos.left+15, pos.top+15, "mouseup"); testUtils.dom.triggerPositionalMouseEvent(pos.left+15, pos.top+15, "mouseup");
await testUtils.nextTick(); await testUtils.nextTick();
@@ -1288,10 +1347,7 @@ QUnit.module('Views', {
'date_start="start" '+ 'date_start="start" '+
'date_stop="stop" '+ 'date_stop="stop" '+
'all_day="allday" '+ 'all_day="allday" '+
'mode="week" '+ 'mode="week"/>',
'readonly_form_view_id="1">'+
'<field name="name"/>'+
'</calendar>',
archs: archs, archs: archs,
viewOptions: { viewOptions: {
initialDate: initialDate, initialDate: initialDate,
@@ -1344,7 +1400,7 @@ QUnit.module('Views', {
'date_start="start" '+ 'date_start="start" '+
'date_stop="stop" '+ 'date_stop="stop" '+
'all_day="allday"> '+ 'all_day="allday"> '+
'<field name="partner_ids" avatar_field="image" write_model="filter_partner" write_field="partner_id"/>'+ '<field name="partner_ids" widget="many2many_tags_avatar" avatar_field="image" write_model="filter_partner" write_field="partner_id"/>'+
'</calendar>', '</calendar>',
archs: archs, archs: archs,
viewOptions: { viewOptions: {
@@ -1352,18 +1408,19 @@ QUnit.module('Views', {
}, },
}); });
assert.containsN(calendar, '.o_calendar_filter_items .o_cal_avatar', 3, assert.containsN(calendar, '.o_calendar_filter_items .o_cw_filter_avatar', 3,
"should have 3 avatars in the side bar"); "should have 3 avatars in the side bar");
var $event1Avatars = calendar.$('.fc-event .o_calendar_avatars').first(); // Event 1
assert.containsOnce($event1Avatars, 'img', "should have 1 avatar"); await testUtils.dom.click(calendar.$('.fc-event:first'));
assert.containsNone($event1Avatars, 'span', assert.ok(calendar.$('.o_cw_popover').length, "should open a popover clicking on event");
"should not have a span for more attendees since there is only one"); assert.strictEqual(calendar.$('.o_cw_popover').find('img').length, 1, "should have 1 avatar");
// Event 2
await testUtils.dom.click(calendar.$('.fc-event:eq(1)'));
assert.ok(calendar.$('.o_cw_popover').length, "should open a popover clicking on event");
assert.strictEqual(calendar.$('.o_cw_popover').find('img').length, 5, "should have 5 avatar");
var $event2Avatars = calendar.$('.fc-event:contains(All day) .o_calendar_avatars');
assert.containsN($event2Avatars, 'img', 3, "should have 3 avatars");
assert.strictEqual($event2Avatars.find('span').text(), '+2',
"should indicate there are 2 more attendees that we don't show");
calendar.destroy(); calendar.destroy();
}); });
@@ -1380,10 +1437,7 @@ QUnit.module('Views', {
'date_start="start" '+ 'date_start="start" '+
'date_stop="stop" '+ 'date_stop="stop" '+
'all_day="allday" '+ 'all_day="allday" '+
'mode="month" '+ 'mode="month"/>',
'readonly_form_view_id="1">'+
'<field name="name"/>'+
'</calendar>',
archs: archs, archs: archs,
viewOptions: { viewOptions: {
initialDate: initialDate, initialDate: initialDate,
@@ -1411,6 +1465,7 @@ QUnit.module('Views', {
"should open the form view"); "should open the form view");
}); });
await testUtils.dom.click(calendar.$('.fc-event:contains(event 4) .fc-content')); await testUtils.dom.click(calendar.$('.fc-event:contains(event 4) .fc-content'));
await testUtils.dom.click(calendar.$('.o_cw_popover .o_cw_popover_edit'));
// create a new event and edit it // create a new event and edit it
@@ -1456,10 +1511,7 @@ QUnit.module('Views', {
'string="Events" ' + 'string="Events" ' +
'date_start="start" '+ 'date_start="start" '+
'date_stop="stop" '+ 'date_stop="stop" '+
'mode="month" '+ 'mode="month"/>',
'readonly_form_view_id="1">'+
'<field name="name"/>'+
'</calendar>',
archs: archs, archs: archs,
viewOptions: { viewOptions: {
initialDate: initialDate, initialDate: initialDate,
@@ -1515,10 +1567,7 @@ QUnit.module('Views', {
'date_start="start" '+ 'date_start="start" '+
'date_stop="stop" '+ 'date_stop="stop" '+
'all_day="allday" '+ 'all_day="allday" '+
'mode="month" '+ 'mode="month"/>',
'readonly_form_view_id="1">'+
'<field name="name"/>'+
'</calendar>',
archs: archs, archs: archs,
viewOptions: { viewOptions: {
initialDate: initialDate, initialDate: initialDate,
@@ -1548,6 +1597,7 @@ QUnit.module('Views', {
"should open the form view"); "should open the form view");
}); });
await testUtils.dom.click(calendar.$('.fc-event:contains(event 4) .fc-content')); await testUtils.dom.click(calendar.$('.fc-event:contains(event 4) .fc-content'));
await testUtils.dom.click(calendar.$('.o_cw_popover .o_cw_popover_edit'));
// create a new event and edit it // create a new event and edit it
@@ -1610,7 +1660,6 @@ QUnit.module('Views', {
'mode="week" '+ 'mode="week" '+
'attendee="partner_ids" '+ 'attendee="partner_ids" '+
'color="partner_id">'+ 'color="partner_id">'+
'<field name="name"/>'+
'<filter name="user_id" avatar_field="image"/>'+ '<filter name="user_id" avatar_field="image"/>'+
'<field name="partner_ids" write_model="filter_partner" write_field="partner_id"/>'+ '<field name="partner_ids" write_model="filter_partner" write_field="partner_id"/>'+
'</calendar>', '</calendar>',
@@ -1662,7 +1711,6 @@ QUnit.module('Views', {
'mode="week" '+ 'mode="week" '+
'attendee="partner_ids" '+ 'attendee="partner_ids" '+
'color="partner_id">'+ 'color="partner_id">'+
'<field name="name"/>'+
'<filter name="user_id" avatar_field="image"/>'+ '<filter name="user_id" avatar_field="image"/>'+
'<field name="partner_ids" write_model="filter_partner" write_field="partner_id"/>'+ '<field name="partner_ids" write_model="filter_partner" write_field="partner_id"/>'+
'</calendar>', '</calendar>',
@@ -1745,7 +1793,6 @@ QUnit.module('Views', {
'mode="week" '+ 'mode="week" '+
'attendee="partner_ids" '+ 'attendee="partner_ids" '+
'color="partner_id">'+ 'color="partner_id">'+
'<field name="name"/>'+
'<filter name="user_id" avatar_field="image"/>'+ '<filter name="user_id" avatar_field="image"/>'+
'<field name="partner_ids" write_model="filter_partner" write_field="partner_id"/>'+ '<field name="partner_ids" write_model="filter_partner" write_field="partner_id"/>'+
'</calendar>', '</calendar>',
@@ -1797,7 +1844,7 @@ QUnit.module('Views', {
}); });
QUnit.test('Update event with filters', async function (assert) { QUnit.test('Update event with filters', async function (assert) {
assert.expect(4); assert.expect(5);
var records = this.data.user.records; var records = this.data.user.records;
records.push({ records.push({
@@ -1825,7 +1872,6 @@ QUnit.module('Views', {
'mode="week" '+ 'mode="week" '+
'attendee="partner_ids" '+ 'attendee="partner_ids" '+
'color="partner_id">'+ 'color="partner_id">'+
'<field name="name"/>'+
'<filter name="user_id" avatar_field="image"/>'+ '<filter name="user_id" avatar_field="image"/>'+
'<field name="partner_ids" write_model="filter_partner" write_field="partner_id"/>'+ '<field name="partner_ids" write_model="filter_partner" write_field="partner_id"/>'+
'</calendar>', '</calendar>',
@@ -1852,7 +1898,8 @@ QUnit.module('Views', {
assert.containsN(calendar, '.fc-event', 3, "should display 3 events"); assert.containsN(calendar, '.fc-event', 3, "should display 3 events");
await testUtils.dom.click(calendar.$('.fc-event:contains(event 2) .fc-content')); await testUtils.dom.click(calendar.$('.fc-event:contains(event 2) .fc-content'));
await testUtils.dom.click($('.modal button.btn:contains(Edit)')); assert.ok(calendar.$('.o_cw_popover').length, "should open a popover clicking on event");
await testUtils.dom.click(calendar.$('.o_cw_popover .o_cw_popover_edit'));
await testUtils.dom.click($('.modal .o_field_widget[name="user_id"] input')); await testUtils.dom.click($('.modal .o_field_widget[name="user_id"] input'));
await testUtils.dom.click($('.ui-menu-item a:contains(user 5)').trigger('mouseenter')); await testUtils.dom.click($('.ui-menu-item a:contains(user 5)').trigger('mouseenter'));
await testUtils.dom.click($('.modal button.btn:contains(Save)')); await testUtils.dom.click($('.modal button.btn:contains(Save)'));
@@ -1916,7 +1963,6 @@ QUnit.module('Views', {
'mode="week" '+ 'mode="week" '+
'attendee="partner_ids" '+ 'attendee="partner_ids" '+
'color="partner_id">'+ 'color="partner_id">'+
'<field name="name"/>'+
'<filter name="user_id" avatar_field="image"/>'+ '<filter name="user_id" avatar_field="image"/>'+
'<field name="partner_ids" write_model="filter_partner" write_field="partner_id"/>'+ '<field name="partner_ids" write_model="filter_partner" write_field="partner_id"/>'+
'</calendar>', '</calendar>',
@@ -1930,7 +1976,7 @@ QUnit.module('Views', {
assert.containsN(calendar, '.o_calendar_filter_item', 6, "should display 6 filter items"); assert.containsN(calendar, '.o_calendar_filter_item', 6, "should display 6 filter items");
assert.containsN(calendar, '.fc-event', 2, "should display 2 events"); assert.containsN(calendar, '.fc-event', 2, "should display 2 events");
assert.strictEqual(calendar.$('.fc-event .o_field_name').text().replace(/\s/g, ''), "event7event8", assert.strictEqual(calendar.$('.fc-event .o_event_title').text().replace(/\s/g, ''), "event7event8",
"should display 2 events"); "should display 2 events");
calendar.destroy(); calendar.destroy();
@@ -2021,7 +2067,6 @@ QUnit.module('Views', {
'mode="week" '+ 'mode="week" '+
'attendee="partner_ids" '+ 'attendee="partner_ids" '+
'color="partner_id">'+ 'color="partner_id">'+
'<field name="name"/>'+
'<filter name="user_id" avatar_field="image"/>'+ '<filter name="user_id" avatar_field="image"/>'+
'<field name="partner_ids" write_model="filter_partner" write_field="partner_id"/>'+ '<field name="partner_ids" write_model="filter_partner" write_field="partner_id"/>'+
'</calendar>', '</calendar>',
@@ -2056,10 +2101,7 @@ QUnit.module('Views', {
'date_start="start" '+ 'date_start="start" '+
'date_stop="stop" '+ 'date_stop="stop" '+
'all_day="allday" '+ 'all_day="allday" '+
'mode="month" '+ 'mode="month"/>',
'readonly_form_view_id="1">'+
'<field name="name"/>'+
'</calendar>',
archs: archs, archs: archs,
viewOptions: { viewOptions: {
initialDate: initialDate, initialDate: initialDate,
@@ -2126,7 +2168,6 @@ QUnit.module('Views', {
'mode="week" '+ 'mode="week" '+
'attendee="partner_ids" '+ 'attendee="partner_ids" '+
'color="partner_id">'+ 'color="partner_id">'+
'<field name="name"/>'+
'<filter name="user_id" avatar_field="image"/>'+ '<filter name="user_id" avatar_field="image"/>'+
'<field name="partner_ids" write_model="filter_partner" write_field="partner_id"/>'+ '<field name="partner_ids" write_model="filter_partner" write_field="partner_id"/>'+
'</calendar>', '</calendar>',
@@ -2178,10 +2219,7 @@ QUnit.module('Views', {
'date_start="start" '+ 'date_start="start" '+
'date_stop="stop" '+ 'date_stop="stop" '+
'all_day="allday" '+ 'all_day="allday" '+
'mode="month" '+ 'mode="month"/>',
'readonly_form_view_id="1">'+
'<field name="name"/>'+
'</calendar>',
archs: archs, archs: archs,
viewOptions: { viewOptions: {
initialDate: initialDate, initialDate: initialDate,
@@ -2216,9 +2254,7 @@ QUnit.module('Views', {
'event,1,calendar': '<calendar class="o_calendar_test" '+ 'event,1,calendar': '<calendar class="o_calendar_test" '+
'date_start="start" '+ 'date_start="start" '+
'date_stop="stop" '+ 'date_stop="stop" '+
'all_day="allday"> '+ 'all_day="allday"/>',
'<field name="name"/>'+
'</calendar>',
'event,false,search': '<search></search>', 'event,false,search': '<search></search>',
}; };
@@ -2250,9 +2286,7 @@ QUnit.module('Views', {
model: 'event', model: 'event',
data: this.data, data: this.data,
arch: arch:
'<calendar date_start="start_date">'+ '<calendar date_start="start_date"/>',
'<field name="name"/>'+
'</calendar>',
archs: archs, archs: archs,
viewOptions: { viewOptions: {
initialDate: initialDate, initialDate: initialDate,
@@ -2278,7 +2312,7 @@ QUnit.module('Views', {
}); });
QUnit.test('timezone does not affect drag and drop', async function (assert) { QUnit.test('timezone does not affect drag and drop', async function (assert) {
assert.expect(6); assert.expect(10);
var calendar = await createCalendarView({ var calendar = await createCalendarView({
View: CalendarView, View: CalendarView,
@@ -2308,12 +2342,13 @@ QUnit.module('Views', {
}, },
}); });
var events = calendar.$('.fc-event').map(function () { assert.strictEqual(calendar.$('.fc-event:eq(0)').text().trim(), "event 1");
return $(this).text().trim().replace(/\s+/g, '|'); await testUtils.dom.click(calendar.$('.fc-event:eq(0)'));
}); assert.strictEqual(calendar.$('.o_field_widget[name="start"]').text(), "12/09/2016 08:00:00");
assert.strictEqual(events[0], "event|1|12/09/2016|08:00:00"); assert.strictEqual(calendar.$('.fc-event:eq(5)').text().trim(), "event 6");
assert.strictEqual(events[5], "event|6|12/16/2016|16:00:00"); await testUtils.dom.click(calendar.$('.fc-event:eq(5)'));
assert.strictEqual(calendar.$('.o_field_widget[name="start"]').text(), "12/16/2016 16:00:00");
// Move event 6 as on first day of month view (27th november 2016) // Move event 6 as on first day of month view (27th november 2016)
await testUtils.dragAndDrop( await testUtils.dragAndDrop(
@@ -2322,18 +2357,19 @@ QUnit.module('Views', {
); );
await testUtils.nextTick(); await testUtils.nextTick();
events = calendar.$('.fc-event').map(function () { assert.strictEqual(calendar.$('.fc-event:eq(0)').text().trim(), "event 6");
return $(this).text().trim().replace(/\s+/g, '|'); await testUtils.dom.click(calendar.$('.fc-event:eq(0)'));
}); assert.strictEqual(calendar.$('.o_field_widget[name="start"]').text(), "11/27/2016 16:00:00");
assert.strictEqual(events[0], "event|6|11/27/2016|16:00:00"); assert.strictEqual(calendar.$('.fc-event:eq(1)').text().trim(), "event 1");
assert.strictEqual(events[1], "event|1|12/09/2016|08:00:00"); await testUtils.dom.click(calendar.$('.fc-event:eq(1)'));
assert.strictEqual(calendar.$('.o_field_widget[name="start"]').text(), "12/09/2016 08:00:00");
calendar.destroy(); calendar.destroy();
}); });
QUnit.test('timzeone does not affect calendar with date field', async function (assert) { QUnit.test('timzeone does not affect calendar with date field', async function (assert) {
assert.expect(8); assert.expect(11);
var calendar = await createCalendarView({ var calendar = await createCalendarView({
View: CalendarView, View: CalendarView,
@@ -2374,7 +2410,9 @@ QUnit.module('Views', {
await testUtils.dom.click($('.modal button.btn:contains(Create)')); await testUtils.dom.click($('.modal button.btn:contains(Create)'));
await testUtils.nextTick(); await testUtils.nextTick();
assert.strictEqual(calendar.$('.o_field_start_date').text().trim(), "12/20/2016"); await testUtils.dom.click(calendar.$('.fc-event:contains(An event)'));
assert.ok(calendar.$('.o_cw_popover').length, "should open a popover clicking on event");
assert.strictEqual(calendar.$('.o_cw_popover .o_cw_popover_fields_secondary .list-group-item:last .o_field_date').text(), '12/20/2016', "should have correct start date");
// Move event to another day (on 27 november) // Move event to another day (on 27 november)
await testUtils.dragAndDrop( await testUtils.dragAndDrop(
@@ -2383,7 +2421,9 @@ QUnit.module('Views', {
); );
await testUtils.nextTick(); await testUtils.nextTick();
assert.verifySteps(["2016-11-27 00:00:00"]); assert.verifySteps(["2016-11-27 00:00:00"]);
assert.strictEqual(calendar.$('.o_field_start_date').text().trim(), "11/27/2016"); await testUtils.dom.click(calendar.$('.fc-event:contains(An event)'));
assert.ok(calendar.$('.o_cw_popover').length, "should open a popover clicking on event");
assert.strictEqual(calendar.$('.o_cw_popover .o_cw_popover_fields_secondary .list-group-item:last .o_field_date').text(), '11/27/2016', "should have correct start date");
// Move event to last day (on 7 january) // Move event to last day (on 7 january)
await testUtils.dragAndDrop( await testUtils.dragAndDrop(
@@ -2392,7 +2432,9 @@ QUnit.module('Views', {
); );
await testUtils.nextTick(); await testUtils.nextTick();
assert.verifySteps(["2017-01-07 00:00:00"]); assert.verifySteps(["2017-01-07 00:00:00"]);
assert.strictEqual(calendar.$('.o_field_start_date').text().trim(), "01/07/2017"); await testUtils.dom.click(calendar.$('.fc-event:contains(An event)'));
assert.ok(calendar.$('.o_cw_popover').length, "should open a popover clicking on event");
assert.strictEqual(calendar.$('.o_cw_popover .o_cw_popover_fields_secondary .list-group-item:last .o_field_date').text(), '01/07/2017', "should have correct start date");
calendar.destroy(); calendar.destroy();
}); });
@@ -2452,9 +2494,7 @@ QUnit.module('Views', {
'date_start="start" '+ 'date_start="start" '+
'date_stop="stop" '+ 'date_stop="stop" '+
'mode="month" '+ 'mode="month" '+
'form_view_id="42">'+ 'form_view_id="42"/>',
'<field name="name"/>'+
'</calendar>',
archs: archs, archs: archs,
viewOptions: { viewOptions: {
initialDate: initialDate, initialDate: initialDate,
@@ -2536,9 +2576,7 @@ QUnit.module('Views', {
arch: '<calendar class="o_calendar_test" '+ arch: '<calendar class="o_calendar_test" '+
'date_start="start" '+ 'date_start="start" '+
'date_stop="stop" '+ 'date_stop="stop" '+
'mode="month"> '+ 'mode="month"/>',
'<field name="name"/>'+
'</calendar>',
archs: archs, archs: archs,
viewOptions: { viewOptions: {
initialDate: initialDate, initialDate: initialDate,
@@ -2593,9 +2631,7 @@ QUnit.module('Views', {
arch: '<calendar class="o_calendar_test" '+ arch: '<calendar class="o_calendar_test" '+
'date_start="start" '+ 'date_start="start" '+
'date_stop="stop" '+ 'date_stop="stop" '+
'mode="week"> '+ 'mode="week"/>',
'<field name="name"/>'+
'</calendar>',
archs: archs, archs: archs,
viewOptions: { viewOptions: {
initialDate: initialDate, initialDate: initialDate,
@@ -2604,7 +2640,7 @@ QUnit.module('Views', {
}); });
assert.strictEqual(calendar.$('.fc-day-header:first').text(), "zz1. 11/12", assert.strictEqual(calendar.$('.fc-day-header:first').text(), "zz1. 11",
'The day should be in the given locale specific format'); 'The day should be in the given locale specific format');
moment.locale(initialLocale); moment.locale(initialLocale);
@@ -2612,45 +2648,6 @@ QUnit.module('Views', {
calendar.destroy(); calendar.destroy();
}); });
QUnit.test('calendar sidebar toggle', async function (assert) {
assert.expect(5);
var calendar = await createCalendarView({
View: CalendarView,
model: 'event',
data: this.data,
arch:
'<calendar date_start="start_date">'+
'<field name="name"/>'+
'</calendar>',
services: {
local_storage: AbstractStorageService.extend({storage: new RamStorage()}),
},
});
var fullWidth = calendar.call('local_storage', 'getItem', 'calendar_fullWidth');
assert.strictEqual(fullWidth, undefined,
"calendar_fullWidth should be undefined in local storage");
assert.containsNone(calendar, '.o_calendar_sidebar_container.o_sidebar_hidden',
"sidebar should be opened");
// click on close
await testUtils.dom.click(calendar.$('.o_calendar_sidebar_toggler'));
fullWidth = calendar.call('local_storage', 'getItem', 'calendar_fullWidth');
assert.strictEqual(fullWidth, true,
"calendar_fullWidth should be true in local storage");
assert.containsOnce(calendar, '.o_calendar_sidebar_container.o_sidebar_hidden',
"sidebar should be hidden");
// reload calendar view and check calendar view has still full width
calendar.reload();
await testUtils.nextTick();
assert.containsOnce(calendar, '.o_calendar_sidebar_container.o_sidebar_hidden',
"sidebar should be hidden");
calendar.destroy();
});
}); });
}); });
+1
View File
@@ -355,6 +355,7 @@
<script type="text/javascript" src="/web/static/src/js/views/calendar/calendar_controller.js"></script> <script type="text/javascript" src="/web/static/src/js/views/calendar/calendar_controller.js"></script>
<script type="text/javascript" src="/web/static/src/js/views/calendar/calendar_model.js"></script> <script type="text/javascript" src="/web/static/src/js/views/calendar/calendar_model.js"></script>
<script type="text/javascript" src="/web/static/src/js/views/calendar/calendar_popover.js"></script>
<script type="text/javascript" src="/web/static/src/js/views/calendar/calendar_quick_create.js"></script> <script type="text/javascript" src="/web/static/src/js/views/calendar/calendar_quick_create.js"></script>
<script type="text/javascript" src="/web/static/src/js/views/calendar/calendar_renderer.js"></script> <script type="text/javascript" src="/web/static/src/js/views/calendar/calendar_renderer.js"></script>
<script type="text/javascript" src="/web/static/src/js/views/calendar/calendar_view.js"></script> <script type="text/javascript" src="/web/static/src/js/views/calendar/calendar_view.js"></script>
-2
View File
@@ -1168,8 +1168,6 @@ calendar view are:
same color segment are allocated the same highlight color in the calendar, same color segment are allocated the same highlight color in the calendar,
colors are allocated semi-randomly. colors are allocated semi-randomly.
Displayed the display_name/avatar of the visible record in the sidebar Displayed the display_name/avatar of the visible record in the sidebar
``readonly_form_view_id``
view to open in readonly mode
``form_view_id`` ``form_view_id``
view to open when the user create or edit an event. Note that if this attribute view to open when the user create or edit an event. Note that if this attribute
is not set, the calendar view will fall back to the id of the form view in the is not set, the calendar view will fall back to the id of the form view in the
+2 -8
View File
@@ -13,13 +13,14 @@
<rng:optional><rng:attribute name="date_stop" /></rng:optional> <rng:optional><rng:attribute name="date_stop" /></rng:optional>
<rng:optional><rng:attribute name="date_delay" /></rng:optional> <rng:optional><rng:attribute name="date_delay" /></rng:optional>
<rng:optional><rng:attribute name="all_day" /></rng:optional> <rng:optional><rng:attribute name="all_day" /></rng:optional>
<rng:optional><rng:attribute name="readonly_form_view_id" /></rng:optional>
<rng:optional><rng:attribute name="form_view_id" /></rng:optional> <rng:optional><rng:attribute name="form_view_id" /></rng:optional>
<rng:optional><rng:attribute name="event_limit" /></rng:optional> <rng:optional><rng:attribute name="event_limit" /></rng:optional>
<rng:optional><rng:attribute name="quick_add" /></rng:optional> <rng:optional><rng:attribute name="quick_add" /></rng:optional>
<rng:optional><rng:attribute name="color" /></rng:optional> <rng:optional><rng:attribute name="color" /></rng:optional>
<rng:optional><rng:attribute name="event_open_popup" /></rng:optional> <rng:optional><rng:attribute name="event_open_popup" /></rng:optional>
<rng:optional><rng:attribute name="js_class"/></rng:optional> <rng:optional><rng:attribute name="js_class"/></rng:optional>
<rng:optional><rng:attribute name="hide_time"/></rng:optional>
<rng:optional><rng:attribute name="hide_date"/></rng:optional>
<rng:optional> <rng:optional>
<rng:attribute name="mode"> <rng:attribute name="mode">
<rng:choice> <rng:choice>
@@ -32,13 +33,6 @@
<rng:zeroOrMore> <rng:zeroOrMore>
<rng:ref name="field"/> <rng:ref name="field"/>
</rng:zeroOrMore> </rng:zeroOrMore>
<rng:optional>
<rng:element name="templates">
<rng:oneOrMore>
<rng:ref name="any"/>
</rng:oneOrMore>
</rng:element>
</rng:optional>
</rng:element> </rng:element>
</rng:define> </rng:define>
<rng:start> <rng:start>