[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, {
changeDate: '_onChangeDate',
changeFilter: '_onChangeFilter',
deleteRecord: '_onDeleteRecord',
dropRecord: '_onDropRecord',
next: '_onNext',
openCreate: '_onOpenCreate',
openEvent: '_onOpenEvent',
prev: '_onPrev',
quickCreate: '_onQuickCreate',
toggleFullWidth: '_onToggleFullWidth',
updateRecord: '_onUpdateRecord',
viewUpdated: '_onViewUpdated',
}),
@@ -200,6 +200,20 @@ var CalendarController = AbstractController.extend({
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
* @param {OdooEvent} event
@@ -323,53 +337,22 @@ var CalendarController = AbstractController.extend({
return;
}
var open_dialog = function (readonly) {
var options = {
res_model: self.modelName,
res_id: id || null,
context: event.context || self.context,
readonly: readonly,
title: _t("Open: ") + event.data.title,
on_saved: function () {
if (event.data.on_save) {
event.data.on_save();
}
self.reload();
},
};
if (readonly) {
if (self.readonlyFormViewId) {
options.view_id = parseInt(self.readonlyFormViewId);
var options = {
res_model: self.modelName,
res_id: id || null,
context: event.context || self.context,
title: _t("Open: ") + event.data.title,
on_saved: function () {
if (event.data.on_save) {
event.data.on_save();
}
options.buttons = [
{
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();
self.reload();
},
};
open_dialog(true);
if (this.formViewId) {
options.view_id = parseInt(this.formViewId);
}
new dialogs.FormViewDialog(this, options).open();
},
/**
* @private
@@ -410,15 +393,6 @@ var CalendarController = AbstractController.extend({
self.quickCreating = false;
})
},
/**
* Called when we want to open or close the sidebar.
*
* @private
*/
_onToggleFullWidth: function () {
this.model.toggleFullWidth();
this.reload();
},
/**
* @private
* @param {OdooEvent} event
@@ -308,13 +308,6 @@ return AbstractModel.extend({
today: function () {
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 {integer} record.id
@@ -410,8 +403,10 @@ return AbstractModel.extend({
snapMinutes: 15,
longPressDelay: 500,
eventResizableFromStart: true,
nowIndicator: true,
weekNumbers: true,
weekNumberTitle: _t("W"),
weekNumbersWithinDays: true,
weekNumberTitle: _t("Week") + " ",
allDayText: _t("All day"),
monthNames: moment.months(),
monthNamesShort: moment.monthsShort(),
@@ -444,7 +439,6 @@ return AbstractModel.extend({
*/
_loadCalendar: function () {
var self = this;
this.data.fullWidth = this.call('local_storage', 'getItem', 'calendar_fullWidth') === true;
this.data.fc_options = this._getFullCalendarOptions();
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";
var AbstractRenderer = require('web.AbstractRenderer');
var CalendarPopover = require('web.CalendarPopover');
var config = require('web.config');
var core = require('web.core');
var Dialog = require('web.Dialog');
var field_utils = require('web.field_utils');
var FieldManagerMixin = require('web.FieldManagerMixin');
var QWeb = require('web.QWeb');
var relational_fields = require('web.relational_fields');
var session = require('web.session');
var utils = require('web.utils');
var Widget = require('web.Widget');
var _t = core._t;
@@ -78,7 +77,7 @@ var SidebarFilter = Widget.extend(FieldManagerMixin, {
{
mode: 'edit',
attrs: {
placeholder: _.str.sprintf(_t("Add %s"), self.title),
placeholder: "+ " + _.str.sprintf(_t("Add %s"), self.title),
can_create: false
},
});
@@ -98,7 +97,7 @@ var SidebarFilter = Widget.extend(FieldManagerMixin, {
this.many2one.filter_ids = _.without(_.pluck(this.filters, 'value'), 'all');
}
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({
template: "CalendarView",
events: _.extend({}, AbstractRenderer.prototype.events, {
'click .o_calendar_sidebar_toggler': '_onToggleSidebar',
config: {
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.filters = [];
this.color_map = {};
if (params.eventTemplate) {
this.qweb = new QWeb(config.isDebug(), {_s: session.origin});
this.qweb.add_template(utils.json_node_to_xml(params.eventTemplate));
}
this.hideDate = params.hideDate;
this.hideTime = params.hideTime;
},
/**
* @override
@@ -203,6 +203,20 @@ return AbstractRenderer.extend({
}
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
*/
@@ -325,19 +339,14 @@ return AbstractRenderer.extend({
_eventRender: function (event) {
var qweb_context = {
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,
user_context: session.user_context,
widget: this,
color: this.getColor(event.color_index),
};
this.qweb_context = qweb_context;
if (_.isEmpty(qweb_context.record)) {
return '';
} 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);
},
eventResize: function (event) {
self._unselectEvent();
self.trigger_up('updateRecord', event);
},
eventClick: function (event) {
self.trigger_up('openEvent', event);
self.$calendar.fullCalendar('unselect');
eventClick: function (eventData, ev) {
self._unselectEvent();
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) {
var data = {'start': target_date, 'end': end_date};
if (self.state.context.default_name) {
data.title = self.state.context.default_name;
select: function (startDate, endDate) {
// Clicking on the view, dispose any visible popover. Otherwise create a new event.
if (self.$('.o_cw_popover').length) {
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');
},
eventRender: function (event, element) {
eventRender: function (event, element, view) {
var $render = $(self._eventRender(event));
event.title = $render.find('.o_field_type_char:first').text();
element.find('.fc-content').html($render.html());
element.addClass($render.attr('class'));
var display_hour = '';
if (!event.allDay) {
element.attr('data-event-id', event.id);
// 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 end = event.r_end || event.end;
var timeFormat = _t.database.parameters.time_format.search("%H") != -1 ? 'HH:mm': 'h:mma';
display_hour = start.format(timeFormat) + ' - ' + end.format(timeFormat);
if (display_hour === '00:00 - 00:00') {
display_hour = _t('All day');
// Detect if the event occurs in just one day
// note: add & remove 1 min to avoid issues with 00:00
var isSameDayEvent = start.clone().add(1, 'minute').isSame(end.clone().subtract(1, 'minute'), 'day');
if (!event.record.allday && isSameDayEvent) {
element.addClass('o_cw_nobg');
}
}
element.find('.fc-content .fc-time').text(display_hour);
},
// Dirty hack to ensure a correct first render
eventAfterAllRender: function () {
$(window).trigger('resize');
// On double click, edit the event
element.on('dblclick', function () {
self.trigger_up('edit_event', {id: event.id});
});
},
viewRender: function (view) {
// compute mode from view.name which is either 'month', 'agendaWeek' or 'agendaDay'
@@ -418,6 +441,39 @@ return AbstractRenderer.extend({
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',
unselectAuto: false,
isRTL: _t.database.parameters.direction === "rtl",
@@ -440,6 +496,7 @@ return AbstractRenderer.extend({
date: moment(new Date(+obj.currentYear , +obj.currentMonth, +obj.currentDay))
});
},
'showOtherMonths': true,
'dayNamesMin' : this.state.fc_options.dayNamesShort,
'monthNames': this.state.fc_options.monthNamesShort,
'firstDay': this.state.fc_options.firstDay,
@@ -466,7 +523,6 @@ return AbstractRenderer.extend({
var $calendar = this.$calendar;
var $fc_view = $calendar.find('.fc-view');
var scrollPosition = $fc_view.scrollLeft();
var scrollTop = this.$calendar.find('.fc-scroller').scrollTop();
$fc_view.scrollLeft(0);
$calendar.fullCalendar('unselect');
@@ -485,8 +541,8 @@ return AbstractRenderer.extend({
.removeClass('o_color o_selected_range');
var $a;
switch (this.state.scale) {
case 'month': $a = this.$small_calendar.find('td a'); break;
case 'week': $a = this.$small_calendar.find('tr:has(.ui-state-active) a'); break;
case 'month': $a = this.$small_calendar.find('td'); 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;
}
$a.addClass('o_selected_range');
@@ -496,21 +552,8 @@ return AbstractRenderer.extend({
$fc_view.scrollLeft(scrollPosition);
var fullWidth = this.state.fullWidth;
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);
this._unselectEvent();
var filterProm = this._renderFilters();
this.$calendar.appendTo('body');
if (scrollTop) {
this.$calendar.fullCalendar('reinitView');
} else {
this.$calendar.fullCalendar('render');
}
this._renderEvents();
this.$calendar.prependTo(this.$('.o_calendar_view'));
@@ -532,6 +575,8 @@ return AbstractRenderer.extend({
* @returns {Promise} resolved when all filters have been rendered
*/
_renderFilters: function () {
// Dispose of filter popover
this.$('.o_calendar_filter_item').popover('dispose');
_.each(this.filters || (this.filters = []), function (filter) {
filter.destroy();
});
@@ -565,24 +610,152 @@ return AbstractRenderer.extend({
options.fields = this.state.fields;
var filter = new SidebarFilter(self, options);
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);
});
this.filters.push(filter);
}
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
//--------------------------------------------------------------------------
/**
* Toggle the sidebar
* Remove highlight classes and dispose of popovers
*
* @private
*/
_onToggleSidebar: function () {
this.trigger_up('toggleFullWidth');
_unselectEvent: function () {
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 CalendarRenderer = require('web.CalendarRenderer');
var core = require('web.core');
var pyUtils = require('web.py_utils');
var utils = require('web.utils');
var _lt = core._lt;
@@ -66,7 +67,8 @@ var CalendarView = AbstractView.extend({
var fieldName = child.attrs.name;
fieldNames.push(fieldName);
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"
@@ -102,17 +104,13 @@ var CalendarView = AbstractView.extend({
'filters': [],
};
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_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 = 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'
@@ -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.mapping = mapping;
this.controllerParams.context = params.context || {};
this.controllerParams.displayName = params.action && params.action.name;
this.rendererParams.displayFields = displayFields;
this.rendererParams.eventTemplate = _.findWhere(arch.children, {'tag': 'templates'});
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.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 {
from {background-color: rgba($o-brand-odoo, 0.5);}
to {background-color: #f5f5f5;}
from { background-color: rgba($info, 0.5); }
to { background-color: rgba($info, 0.1); }
}
.o_calendar_container {
height: 100%;
@include o-webclient-padding($top: $o-horizontal-padding/2, $bottom: $o-horizontal-padding/2);
display: flex;
}
@for $index from 1 through $o-nb-calendar-colors {
.o_calendar_color_#{$index} {
color: #0D0D0D;
background-color: adjust-hue(rgb(255, 192, 192), (360/($o-nb-calendar-colors+1) * $index) * 1deg);
border-color: adjust-hue(rgb(255, 192, 192), (360/($o-nb-calendar-colors+1) * $index) * 1deg);
opacity: 0.7;
&.o_event_hightlight {
font-weight: bold;
opacity: 0.9;
}
&:hover {
color: #666 !important;
}
.o_calendar_view {
flex: 1 1 auto;
min-width: 0;
height: 100%;
background-color: gray('100');
background: linear-gradient(-45deg, gray('100'), white);
.fc-event {
margin: 0 1px;
border-style: solid;
border-width: 0 0 0 3px;
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 {
flex: 1 1 auto;
min-width: 0;
.fc-content {
white-space: normal;
margin: 8px 4px;
font-size: 1.1em;
font-weight: 500;
}
.o_calendar_widget {
height: 100%;
background-color: $o-view-background-color;
* {
z-index: 0; // FIXME
}
.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_cw_custom_highlight {
z-index: 10!important;
.fc-bg{
opacity: 0.95;
}
}
}
.o_calendar_sidebar_container {
flex: 0 0 $o-horizontal-padding;
position: relative;
.o_calendar_widget {
height: 100%;
&.o_sidebar_hidden {
margin-right: -$o-horizontal-padding; // hack to display the toggler in the gutter
.o_calendar_sidebar_toggler {
padding: 4px;
color: darken($o-webclient-background-color, 30%);
// === Adapt calendar table borders ===
// =====================================
td {
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 {
@include o-position-absolute($top: 0, $right: 0);
cursor: pointer;
padding: 6px;
z-index: 1;
&:hover {
color: black;
// ====== Day only
.fc-agendaDay-view .fc-event {
padding: 10px;
font-size: 14px;
}
// ====== 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;
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 {
width: 100%;
font-size: 8pt;
border-radius: 0;
border: none;
th {
padding: .7em .2em;
}
td {
padding: 0;
.ui-widget-header {
display: flex;
a, span {
padding: .2em .2em .1em;
float: right;
width: 22px;
background: none;
background-color: lighten($o-brand-secondary, 30%);
border-radius: 0;
text-align: center;
color: gray('700');
}
.ui-datepicker-prev, .ui-datepicker-next {
flex: 0 0 auto;
position: static;
cursor: pointer;
padding: 2px;
text-align: center;
color: $o-main-text-color;
&.ui-state-hover {
background: none;
border: none;
color: black;
}
.ui-icon {
display: none;
}
&.ui-datepicker-current-day a {
background: $info;
color: color-yiq($info);
font-weight: bold;
}
&.ui-datepicker-today a {
border-radius: 100%;
padding-top: 0.3em;
width: 25px;
background: mix($o-cw-color-today-accent, white, 80%);
font-weight: bold;
color: white;
}
&.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 {
order: 1;
&: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;
&.ui-icon-circle-triangle-e:before {
content: "\f054"
}
}
table {
td {
padding: 0 2px 2px 0;
background-color: $o-view-background-color;
a {
border: none;
background: none;
background-color: lighten($o-brand-secondary, 40%);
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;
.ui-state-hover.ui-datepicker-next-hover, .ui-state-hover.ui-datepicker-prev-hover {
background: none;
border: none;
cursor: pointer;
span:before {
color: gray('800');
}
}
}
.o_calendar_filter {
padding: 8px;
border-top: 1px solid lightgray;
.o_field_many2one {
width: 100%;
.o_selected_range.o_color:not(.ui-datepicker-unselectable) {
background-color: $info;
animation: backgroundfade 2s forwards;
}
}
.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 {
content: '';
.o_cw_filter_color_#{$i} {
.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) {
.o_calendar_container {
padding: 0;
}
.o_control_panel {
.o_calendar_button_today {
font-size: 0.7em;
+67 -45
View File
@@ -5,7 +5,6 @@
<div class="o_calendar_widget" />
</div>
<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_mini"/>
</div>
@@ -13,58 +12,81 @@
</div>
<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 &amp;&amp; record.is_highlighted.value ? 'o_event_hightlight' : ''} #{typeof color === 'number' ? 'o_calendar_color_'+color : ''}">
<div class="fc-time"/>
<div class="o_fields">
<t t-foreach="widget.displayFields" t-as="name">
<div t-attf-class="o_field_#{name} o_field_type_#{fields[name].type}">
<t t-if="widget.displayFields[name].avatar_field">
<t t-if="!isMobile"><t t-esc="fields[name].string"/>:</t>
<div class="o_calendar_avatars float-right">
<t t-foreach="widget.getAvatars(record, name, widget.displayFields[name].avatar_field).slice(0,3)" t-as="image"><t t-raw="image"/></t>
<span t-if="record[name].length - 3 > 0">+<t t-esc="record[name].length - 3"/></span>
</div>
</t>
<t t-else="">
<t t-esc="format(record, name)"/>
</t>
</div>
</t>
<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 class="o_event_title" t-esc="record.display_name"/>
</div>
</t>
<t t-name="CalendarView.event.popover.placeholder">
<div t-attf-class="o_cw_popover popover card shadow #{typeof color === 'number' ? _.str.sprintf('o_calendar_color_%s', color) : ''}" role="tooltip">
<div class="arrow"/>
<div class="card-header d-flex justify-content-between py-2 pr-2">
<h4 class="popover-header border-0 p-0 pt-1"/>
<span class="o_cw_popover_close ml-4"><i class="fa fa-close small"/></span>
</div>
<div class="o_cw_body">
<div class="py-4 text-center">
<i class="fa fa-spinner fa-spin fa-2x"></i>
</div>
</div>
</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>
</t>
<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">
<h3 t-if="widget.title"><t t-esc="widget.title"/></h3>
<div class="o_calendar_filter_items">
<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">
<t t-set="id_for_label" t-value="_.uniqueId('o_calendar_filter_item_')"/>
<div class="custom-control custom-control-inline custom-checkbox">
<input type="checkbox"
t-att-id="id_for_label"
name="selection"
class="custom-control-input"
t-att-checked="(filter.active ? true : undefined)"/>
<label t-att-for="id_for_label"
class="custom-control-label">
<t t-if="filter.value == 'all'">
<span><i class="fa fa-users fa-fw o_cal_avatar" role="img" aria-label="Avatar" title="Avatar"/></span>
</t>
<t t-if="widget.avatar_field &amp;&amp; (filter.value &amp;&amp; filter.value != 'all')">
<img t-attf-src="/web/image/#{widget.avatar_model}/#{filter.value}/#{widget.avatar_field}" class="o_cal_avatar" alt="Avatar"/>
</t>
<t t-set="color" t-value="widget.getColor(filter.color_index)"/>
<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>
<span t-elif="color" t-attf-style="border-bottom: 4px solid #{color};"><t t-esc="filter.label"/></span>
<span t-else=""><t t-esc="filter.label"/></span>
<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">
<t t-esc="widget.title"/><i class="o_cw_filter_collapse_icon fa fa-chevron-down"/>
</h5>
<h5 t-elif="widget.title" t-esc="widget.title"/>
<div t-attf-class="o_calendar_filter_items #{needCollapse ? 'collapse show' : 0}" aria-expanded="true" t-attf-id="o_cw_filter_collapse_#{widget.title}">
<t t-foreach="widget.filters" t-as="filter" t-if="filter.display == null || filter.display">
<t t-set="idForLabel" t-value="_.uniqueId('o_calendar_filter_item_')"/>
<t t-set="color" t-value="widget.getColor(filter.color_index)"/>
<t t-set="isColorNumber" t-value="typeof color === 'number'"/>
<div t-attf-class="o_calendar_filter_item w-100 position-relative mb-2 #{isColorNumber ? _.str.sprintf('o_cw_filter_color_%s', color) : ''}"
t-att-data-value="filter.value" t-att-data-id="filter.id">
<input type="checkbox" t-att-id="idForLabel" name="selection" class="position-absolute" t-att-checked="filter.active ? true : undefined"/>
<label t-att-for="idForLabel" class="d-flex align-items-center m-0">
<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' : ''}"
t-att-style="color and !isColorNumber ? _.str.sprintf('border-color:%s;background-color:%s', color, color) : ''">
<i class="fa fa-check position-relative"/>
</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>
<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>
<t t-if="filter.id">
<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>
</t>
</div>
</div>
</t>
+181 -184
View File
@@ -161,7 +161,6 @@ QUnit.module('Views', {
'mode="week" '+
'attendee="partner_ids" '+
'color="partner_id">'+
'<field name="name"/>'+
'<filter name="user_id" avatar_field="image"/>'+
'<field name="partner_ids" write_model="filter_partner" write_field="partner_id"/>'+
'</calendar>',
@@ -179,7 +178,7 @@ QUnit.module('Views', {
// test view scales
assert.containsN(calendar, '.fc-event', 9,
"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");
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
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')");
assert.containsN($sidebar, '.o_selected_range', 31,
assert.containsN($sidebar, 'td a', 31,
"month scale should highlight all days in mini calendar");
// test 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.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
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");
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.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.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)");
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");
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");
// test search bar in filter
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
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"]'));
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});
assert.ok($('.modal-footer button.btn:contains(Ok)').length, "should display the confirm message");
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();
});
@@ -235,9 +234,7 @@ QUnit.module('Views', {
assert.expect(3);
var archs = {
'event,1,calendar': '<calendar date_start="start" date_stop="stop" all_day="allday">' +
'<field name="name"/>' +
'</calendar>',
'event,1,calendar': '<calendar date_start="start" date_stop="stop" all_day="allday"/>',
'event,false,search': '<search></search>',
};
@@ -290,11 +287,14 @@ QUnit.module('Views', {
'date_start="start" '+
'date_stop="stop" '+
'all_day="allday" '+
'mode="month" '+
'readonly_form_view_id="1">'+
'<field name="name"/>'+
'</calendar>',
'mode="month"/>',
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: {
initialDate: initialDate,
},
@@ -306,20 +306,19 @@ QUnit.module('Views', {
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-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.dom.click($('.modal-footer button.btn:contains(Save)'));
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
@@ -406,7 +405,7 @@ QUnit.module('Views', {
// delete the a record
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");
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");
@@ -435,10 +434,7 @@ QUnit.module('Views', {
'date_start="start" '+
'date_stop="stop" '+
'all_day="allday" '+
'mode="month" '+
'readonly_form_view_id="1">'+
'<field name="name"/>'+
'</calendar>',
'mode="month"/>',
archs: archs,
viewOptions: {
initialDate: initialDate,
@@ -497,8 +493,7 @@ QUnit.module('Views', {
'date_start="start" '+
'date_stop="stop" '+
'all_day="allday" '+
'mode="week" '+
'readonly_form_view_id="1">'+
'mode="week">'+
'<field name="name"/>'+
'<field name="start"/>'+
'<field name="allday"/>'+
@@ -541,7 +536,7 @@ QUnit.module('Views', {
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");
await testUtils.dom.triggerPositionalMouseEvent(left, top + 60, "mouseup");
@@ -550,8 +545,8 @@ QUnit.module('Views', {
await testUtils.dom.click($('.modal button.btn:contains(Create)'));
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",
"should display the new event with time, title and additional fields");
assert.strictEqual($newevent.find('.o_event_title').text(), "new event",
"should display the new event with title");
assert.deepEqual($newevent.data('fcSeg').event.record,
{
@@ -567,7 +562,7 @@ QUnit.module('Views', {
// delete record
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)'));
assert.containsNone(calendar, '.fc-content', "should delete the record");
@@ -595,7 +590,7 @@ QUnit.module('Views', {
initialDate: initialDate,
},
}).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");
calendar.destroy();
done();
@@ -626,7 +621,7 @@ QUnit.module('Views', {
week_start: 1,
},
}).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");
calendar.destroy();
done();
@@ -659,15 +654,89 @@ QUnit.module('Views', {
week_start: 7,
},
}).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");
calendar.destroy();
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) {
assert.expect(8);
assert.expect(7);
this.data.event.records = [];
this.data.event.onchanges = {
@@ -692,8 +761,7 @@ QUnit.module('Views', {
'date_start="start" '+
'date_stop="stop" '+
'all_day="allday" '+
'mode="week" '+
'readonly_form_view_id="1">'+
'mode="week">'+
'<field name="name"/>'+
'</calendar>',
archs: archs,
@@ -771,8 +839,8 @@ QUnit.module('Views', {
await testUtils.dom.click($('.modal-lg button.btn:contains(Save)'));
var $newevent = calendar.$('.fc-event:contains(new event)');
assert.strictEqual($newevent.text().replace(/[\s\n\r]+/g, ''), "08:00-10:00newevent",
"should display the new event with time and title");
assert.strictEqual($newevent.find('.o_event_title').text(), "new event",
"should display the new event with title");
assert.deepEqual($newevent.data('fcSeg').event.record,
{
@@ -831,8 +899,7 @@ QUnit.module('Views', {
'date_start="start" '+
'date_stop="stop" '+
'all_day="allday" '+
'mode="week" '+
'readonly_form_view_id="1">'+
'mode="week">'+
'<field name="name"/>'+
'<field name="start"/>'+
'<field name="allday"/>'+
@@ -875,7 +942,7 @@ QUnit.module('Views', {
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");
testUtils.dom.triggerPositionalMouseEvent(left, top + 60, "mouseup");
@@ -884,8 +951,8 @@ QUnit.module('Views', {
await testUtils.dom.click($('.modal button.btn:contains(Create)'));
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",
"should display the new event with time, title and additional fields");
assert.strictEqual($newevent.find('.o_event_title').text(), "new event",
"should display the new event with title");
assert.deepEqual($newevent.data('fcSeg').event.record,
{
@@ -901,7 +968,7 @@ QUnit.module('Views', {
// delete record
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)'));
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) {
assert.expect(8);
assert.expect(7);
this.data.event.records = [];
this.data.event.onchanges = {
@@ -934,8 +1001,7 @@ QUnit.module('Views', {
'date_start="start" '+
'date_stop="stop" '+
'all_day="allday" '+
'mode="week" '+
'readonly_form_view_id="1">'+
'mode="week">'+
'<field name="name"/>'+
'</calendar>',
archs: archs,
@@ -1013,8 +1079,8 @@ QUnit.module('Views', {
await testUtils.dom.click($('.modal-lg button.btn:contains(Save)'));
var $newevent = calendar.$('.fc-event:contains(new event)');
assert.strictEqual($newevent.text().replace(/[\s\n\r]+/g, ''), "8:00am-10:00amnewevent",
"should display the new event with time and title");
assert.strictEqual($newevent.find('.o_event_title').text(), "new event",
"should display the new event with title");
assert.deepEqual($newevent.data('fcSeg').event.record,
{
@@ -1058,7 +1124,7 @@ QUnit.module('Views', {
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);
var calendar = await createCalendarView({
@@ -1066,9 +1132,7 @@ QUnit.module('Views', {
model: 'event',
data: this.data,
arch:
'<calendar date_start="start">'+
'<field name="name"/>'+
'</calendar>',
'<calendar date_start="start"/>',
archs: archs,
viewOptions: {
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-event:first:contains(12:00am)').length, 1,
assert.strictEqual(calendar.$('.fc-axis:contains(11pm)').length, 1,
"event time format should 12 hour");
calendar.destroy();
@@ -1100,8 +1164,7 @@ QUnit.module('Views', {
'date_start="start" '+
'date_stop="stop" '+
'all_day="allday" '+
'mode="week" '+
'readonly_form_view_id="1">'+
'mode="week">'+
'<field name="name"/>'+
'</calendar>',
archs: archs,
@@ -1164,10 +1227,7 @@ QUnit.module('Views', {
'date_start="start" '+
'date_stop="stop" '+
'mode="week" '+
'quick_add="False" '+
'readonly_form_view_id="1">'+
'<field name="name"/>'+
'</calendar>',
'quick_add="False"/>',
archs: archs,
viewOptions: {
initialDate: initialDate,
@@ -1217,8 +1277,7 @@ QUnit.module('Views', {
'event_open_popup="true" '+
'date_start="start" '+
'date_stop="stop" '+
'mode="month" '+
'readonly_form_view_id="1">'+
'mode="month">'+
'<field name="name"/>'+
'</calendar>',
archs: archs,
@@ -1243,14 +1302,14 @@ QUnit.module('Views', {
},
}, {positionalClicks: true});
var pos = calendar.$('.fc-bg td:eq(20)').offset();
var pos = calendar.$('.fc-bg td:eq(17)').offset();
try {
testUtils.dom.triggerPositionalMouseEvent(pos.left+15, pos.top+15, "mousedown");
} catch (e) {
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.');
}
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, "mouseup");
await testUtils.nextTick();
@@ -1288,10 +1347,7 @@ QUnit.module('Views', {
'date_start="start" '+
'date_stop="stop" '+
'all_day="allday" '+
'mode="week" '+
'readonly_form_view_id="1">'+
'<field name="name"/>'+
'</calendar>',
'mode="week"/>',
archs: archs,
viewOptions: {
initialDate: initialDate,
@@ -1344,7 +1400,7 @@ QUnit.module('Views', {
'date_start="start" '+
'date_stop="stop" '+
'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>',
archs: archs,
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");
var $event1Avatars = calendar.$('.fc-event .o_calendar_avatars').first();
assert.containsOnce($event1Avatars, 'img', "should have 1 avatar");
assert.containsNone($event1Avatars, 'span',
"should not have a span for more attendees since there is only one");
// Event 1
await testUtils.dom.click(calendar.$('.fc-event:first'));
assert.ok(calendar.$('.o_cw_popover').length, "should open a popover clicking on event");
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();
});
@@ -1380,10 +1437,7 @@ QUnit.module('Views', {
'date_start="start" '+
'date_stop="stop" '+
'all_day="allday" '+
'mode="month" '+
'readonly_form_view_id="1">'+
'<field name="name"/>'+
'</calendar>',
'mode="month"/>',
archs: archs,
viewOptions: {
initialDate: initialDate,
@@ -1411,6 +1465,7 @@ QUnit.module('Views', {
"should open the form view");
});
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
@@ -1456,10 +1511,7 @@ QUnit.module('Views', {
'string="Events" ' +
'date_start="start" '+
'date_stop="stop" '+
'mode="month" '+
'readonly_form_view_id="1">'+
'<field name="name"/>'+
'</calendar>',
'mode="month"/>',
archs: archs,
viewOptions: {
initialDate: initialDate,
@@ -1515,10 +1567,7 @@ QUnit.module('Views', {
'date_start="start" '+
'date_stop="stop" '+
'all_day="allday" '+
'mode="month" '+
'readonly_form_view_id="1">'+
'<field name="name"/>'+
'</calendar>',
'mode="month"/>',
archs: archs,
viewOptions: {
initialDate: initialDate,
@@ -1548,6 +1597,7 @@ QUnit.module('Views', {
"should open the form view");
});
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
@@ -1610,7 +1660,6 @@ QUnit.module('Views', {
'mode="week" '+
'attendee="partner_ids" '+
'color="partner_id">'+
'<field name="name"/>'+
'<filter name="user_id" avatar_field="image"/>'+
'<field name="partner_ids" write_model="filter_partner" write_field="partner_id"/>'+
'</calendar>',
@@ -1662,7 +1711,6 @@ QUnit.module('Views', {
'mode="week" '+
'attendee="partner_ids" '+
'color="partner_id">'+
'<field name="name"/>'+
'<filter name="user_id" avatar_field="image"/>'+
'<field name="partner_ids" write_model="filter_partner" write_field="partner_id"/>'+
'</calendar>',
@@ -1745,7 +1793,6 @@ QUnit.module('Views', {
'mode="week" '+
'attendee="partner_ids" '+
'color="partner_id">'+
'<field name="name"/>'+
'<filter name="user_id" avatar_field="image"/>'+
'<field name="partner_ids" write_model="filter_partner" write_field="partner_id"/>'+
'</calendar>',
@@ -1797,7 +1844,7 @@ QUnit.module('Views', {
});
QUnit.test('Update event with filters', async function (assert) {
assert.expect(4);
assert.expect(5);
var records = this.data.user.records;
records.push({
@@ -1825,7 +1872,6 @@ QUnit.module('Views', {
'mode="week" '+
'attendee="partner_ids" '+
'color="partner_id">'+
'<field name="name"/>'+
'<filter name="user_id" avatar_field="image"/>'+
'<field name="partner_ids" write_model="filter_partner" write_field="partner_id"/>'+
'</calendar>',
@@ -1852,7 +1898,8 @@ QUnit.module('Views', {
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($('.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($('.ui-menu-item a:contains(user 5)').trigger('mouseenter'));
await testUtils.dom.click($('.modal button.btn:contains(Save)'));
@@ -1916,7 +1963,6 @@ QUnit.module('Views', {
'mode="week" '+
'attendee="partner_ids" '+
'color="partner_id">'+
'<field name="name"/>'+
'<filter name="user_id" avatar_field="image"/>'+
'<field name="partner_ids" write_model="filter_partner" write_field="partner_id"/>'+
'</calendar>',
@@ -1930,7 +1976,7 @@ QUnit.module('Views', {
assert.containsN(calendar, '.o_calendar_filter_item', 6, "should display 6 filter items");
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");
calendar.destroy();
@@ -2021,7 +2067,6 @@ QUnit.module('Views', {
'mode="week" '+
'attendee="partner_ids" '+
'color="partner_id">'+
'<field name="name"/>'+
'<filter name="user_id" avatar_field="image"/>'+
'<field name="partner_ids" write_model="filter_partner" write_field="partner_id"/>'+
'</calendar>',
@@ -2056,10 +2101,7 @@ QUnit.module('Views', {
'date_start="start" '+
'date_stop="stop" '+
'all_day="allday" '+
'mode="month" '+
'readonly_form_view_id="1">'+
'<field name="name"/>'+
'</calendar>',
'mode="month"/>',
archs: archs,
viewOptions: {
initialDate: initialDate,
@@ -2126,7 +2168,6 @@ QUnit.module('Views', {
'mode="week" '+
'attendee="partner_ids" '+
'color="partner_id">'+
'<field name="name"/>'+
'<filter name="user_id" avatar_field="image"/>'+
'<field name="partner_ids" write_model="filter_partner" write_field="partner_id"/>'+
'</calendar>',
@@ -2178,10 +2219,7 @@ QUnit.module('Views', {
'date_start="start" '+
'date_stop="stop" '+
'all_day="allday" '+
'mode="month" '+
'readonly_form_view_id="1">'+
'<field name="name"/>'+
'</calendar>',
'mode="month"/>',
archs: archs,
viewOptions: {
initialDate: initialDate,
@@ -2216,9 +2254,7 @@ QUnit.module('Views', {
'event,1,calendar': '<calendar class="o_calendar_test" '+
'date_start="start" '+
'date_stop="stop" '+
'all_day="allday"> '+
'<field name="name"/>'+
'</calendar>',
'all_day="allday"/>',
'event,false,search': '<search></search>',
};
@@ -2250,9 +2286,7 @@ QUnit.module('Views', {
model: 'event',
data: this.data,
arch:
'<calendar date_start="start_date">'+
'<field name="name"/>'+
'</calendar>',
'<calendar date_start="start_date"/>',
archs: archs,
viewOptions: {
initialDate: initialDate,
@@ -2278,7 +2312,7 @@ QUnit.module('Views', {
});
QUnit.test('timezone does not affect drag and drop', async function (assert) {
assert.expect(6);
assert.expect(10);
var calendar = await createCalendarView({
View: CalendarView,
@@ -2308,12 +2342,13 @@ QUnit.module('Views', {
},
});
var events = calendar.$('.fc-event').map(function () {
return $(this).text().trim().replace(/\s+/g, '|');
});
assert.strictEqual(calendar.$('.fc-event:eq(0)').text().trim(), "event 1");
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(events[5], "event|6|12/16/2016|16:00:00");
assert.strictEqual(calendar.$('.fc-event:eq(5)').text().trim(), "event 6");
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)
await testUtils.dragAndDrop(
@@ -2322,18 +2357,19 @@ QUnit.module('Views', {
);
await testUtils.nextTick();
events = calendar.$('.fc-event').map(function () {
return $(this).text().trim().replace(/\s+/g, '|');
});
assert.strictEqual(calendar.$('.fc-event:eq(0)').text().trim(), "event 6");
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(events[1], "event|1|12/09/2016|08:00:00");
assert.strictEqual(calendar.$('.fc-event:eq(1)').text().trim(), "event 1");
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();
});
QUnit.test('timzeone does not affect calendar with date field', async function (assert) {
assert.expect(8);
assert.expect(11);
var calendar = await createCalendarView({
View: CalendarView,
@@ -2374,7 +2410,9 @@ QUnit.module('Views', {
await testUtils.dom.click($('.modal button.btn:contains(Create)'));
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)
await testUtils.dragAndDrop(
@@ -2383,7 +2421,9 @@ QUnit.module('Views', {
);
await testUtils.nextTick();
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)
await testUtils.dragAndDrop(
@@ -2392,7 +2432,9 @@ QUnit.module('Views', {
);
await testUtils.nextTick();
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();
});
@@ -2452,9 +2494,7 @@ QUnit.module('Views', {
'date_start="start" '+
'date_stop="stop" '+
'mode="month" '+
'form_view_id="42">'+
'<field name="name"/>'+
'</calendar>',
'form_view_id="42"/>',
archs: archs,
viewOptions: {
initialDate: initialDate,
@@ -2536,9 +2576,7 @@ QUnit.module('Views', {
arch: '<calendar class="o_calendar_test" '+
'date_start="start" '+
'date_stop="stop" '+
'mode="month"> '+
'<field name="name"/>'+
'</calendar>',
'mode="month"/>',
archs: archs,
viewOptions: {
initialDate: initialDate,
@@ -2593,9 +2631,7 @@ QUnit.module('Views', {
arch: '<calendar class="o_calendar_test" '+
'date_start="start" '+
'date_stop="stop" '+
'mode="week"> '+
'<field name="name"/>'+
'</calendar>',
'mode="week"/>',
archs: archs,
viewOptions: {
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');
moment.locale(initialLocale);
@@ -2612,45 +2648,6 @@ QUnit.module('Views', {
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_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_renderer.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,
colors are allocated semi-randomly.
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``
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
+2 -8
View File
@@ -13,13 +13,14 @@
<rng:optional><rng:attribute name="date_stop" /></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="readonly_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="quick_add" /></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="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:attribute name="mode">
<rng:choice>
@@ -32,13 +33,6 @@
<rng:zeroOrMore>
<rng:ref name="field"/>
</rng:zeroOrMore>
<rng:optional>
<rng:element name="templates">
<rng:oneOrMore>
<rng:ref name="any"/>
</rng:oneOrMore>
</rng:element>
</rng:optional>
</rng:element>
</rng:define>
<rng:start>