summaryrefslogtreecommitdiff
path: root/addons/web/static/src
diff options
context:
space:
mode:
authorsri-odoo <sri@odoo.com>2018-12-13 12:16:19 +0000
committerJigar Patel <jat@openerp.com>2019-08-07 12:30:55 +0000
commit369c20a99bd25172f72f4efde870172a4729aadc (patch)
treed6d7a2f4a75348c235e96659a8bc6701808aa18c /addons/web/static/src
parentaca0e700ac6b3cdb63d6de3ef021cb6fce1cbb47 (diff)
[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>
Diffstat (limited to 'addons/web/static/src')
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_controller.js84
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_model.js12
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_popover.js140
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_renderer.js291
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_view.js14
-rw-r--r--addons/web/static/src/scss/web_calendar.scss745
-rw-r--r--addons/web/static/src/scss/web_calendar_mobile.scss4
-rw-r--r--addons/web/static/src/xml/web_calendar.xml112
8 files changed, 1085 insertions, 317 deletions
diff --git a/addons/web/static/src/js/views/calendar/calendar_controller.js b/addons/web/static/src/js/views/calendar/calendar_controller.js
index b3a04ec9a2b..a0de1c665f5 100644
--- a/addons/web/static/src/js/views/calendar/calendar_controller.js
+++ b/addons/web/static/src/js/views/calendar/calendar_controller.js
@@ -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',
}),
@@ -204,6 +204,20 @@ var CalendarController = AbstractController.extend({
* @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
+ */
_onDropRecord: function (event) {
this._updateRecord(_.extend({}, event.data, {
'drop': true,
@@ -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
@@ -411,15 +394,6 @@ var CalendarController = AbstractController.extend({
})
},
/**
- * Called when we want to open or close the sidebar.
- *
- * @private
- */
- _onToggleFullWidth: function () {
- this.model.toggleFullWidth();
- this.reload();
- },
- /**
* @private
* @param {OdooEvent} event
*/
diff --git a/addons/web/static/src/js/views/calendar/calendar_model.js b/addons/web/static/src/js/views/calendar/calendar_model.js
index 57e7a36c403..22ff7bb211d 100644
--- a/addons/web/static/src/js/views/calendar/calendar_model.js
+++ b/addons/web/static/src/js/views/calendar/calendar_model.js
@@ -309,13 +309,6 @@ return AbstractModel.extend({
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
* @returns {Promise}
@@ -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));
diff --git a/addons/web/static/src/js/views/calendar/calendar_popover.js b/addons/web/static/src/js/views/calendar/calendar_popover.js
new file mode 100644
index 00000000000..8c34ed87ba5
--- /dev/null
+++ b/addons/web/static/src/js/views/calendar/calendar_popover.js
@@ -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;
+
+});
diff --git a/addons/web/static/src/js/views/calendar/calendar_renderer.js b/addons/web/static/src/js/views/calendar/calendar_renderer.js
index 192f8da21fe..8999d84bf34 100644
--- a/addons/web/static/src/js/views/calendar/calendar_renderer.js
+++ b/addons/web/static/src/js/views/calendar/calendar_renderer.js
@@ -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
@@ -206,6 +206,20 @@ return AbstractRenderer.extend({
/**
* @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
+ */
destroy: function () {
if (this.$calendar) {
this.$calendar.fullCalendar('destroy');
@@ -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});
},
});
diff --git a/addons/web/static/src/js/views/calendar/calendar_view.js b/addons/web/static/src/js/views/calendar/calendar_view.js
index af4a0baa2ec..232a7316fa5 100644
--- a/addons/web/static/src/js/views/calendar/calendar_view.js
+++ b/addons/web/static/src/js/views/calendar/calendar_view.js
@@ -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;
diff --git a/addons/web/static/src/scss/web_calendar.scss b/addons/web/static/src/scss/web_calendar.scss
index 540b8710df3..cf0da7c8abd 100644
--- a/addons/web/static/src/scss/web_calendar.scss
+++ b/addons/web/static/src/scss/web_calendar.scss
@@ -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;
+ }
+
+ .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_underline_color_#{$index} {
- border-bottom: 4px solid adjust-hue(rgba(255, 192, 192, 0.7), (360/($o-nb-calendar-colors+1) * $index) * 1deg);
+
+ .fc-content {
+ white-space: normal;
+ margin: 8px 4px;
+ font-size: 1.1em;
+ font-weight: 500;
+ }
+
+ &.o_cw_custom_highlight {
+ z-index: 10!important;
+
+ .fc-bg{
+ opacity: 0.95;
+ }
}
}
- .o_calendar_view {
- flex: 1 1 auto;
- min-width: 0;
+ .o_calendar_widget {
+ height: 100%;
+
+ // === 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;
+ }
+
- .o_calendar_widget {
- height: 100%;
- background-color: $o-view-background-color;
- * {
- z-index: 0; // FIXME
+ // ====== Specific agenda types ======
+ // ====================================
+
+ // ====== Both Day and Week agenda
+ .fc-agendaDay-view, .fc-agendaWeek-view {
+ .fc-axis {
+ padding-left: $o-horizontal-padding;
}
- .fc-week-number, .fc-widget-header {
- background-color: lighten($o-brand-secondary, 30%);
+
+ // Hide unnecessary borders
+ table td, div.fc-row.fc-week.fc-widget-content {
+ border-left-color: transparent;
+ border-right-color: transparent;
}
- .o_avatar_square {
- border-radius: 5px;
- display: inline-block;
- opacity: 1;
- border: 1px solid #999;
+
+ // Reinfornce default border color
+ tbody td {
+ border-top-color: gray('400');
}
- .o_fields img, .o_avatar_square {
- vertical-align: middle;
- margin-left: 1px;
- width: 18px;
- height: 18px;
+
+ // Remove dotted borders (half-hours)
+ .fc-time-grid .fc-slats .fc-minor td {
+ border-top-style: none;
}
- .o_field_type_char:first-child {
- font-weight: bold;
+
+ // Align labels and timelines
+ .fc-axis.fc-time {
+ border-top-color: transparent;
+
+ span {
+ max-width: 45px;
+ margin-top: -19px;
+ position: relative;
+ display: block;
+ }
}
- .fc-view {
- overflow-x: auto;
+
+ // 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;
}
- }
- }
- .o_calendar_sidebar_container {
- flex: 0 0 $o-horizontal-padding;
- position: relative;
+ // 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');
+ }
- &.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%);
+ .fc-content-skeleton tr:not(:first-child) .fc-h-event{
+ margin-top: 3px
+ }
}
- }
- .o_calendar_sidebar_toggler {
- @include o-position-absolute($top: 0, $right: 0);
- cursor: pointer;
- padding: 6px;
- z-index: 1;
- &:hover {
- color: black;
+ // 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 {
- width: 250px;
- margin-left: 8px;
- background: $o-view-background-color;
+ // ====== Day only
+ .fc-agendaDay-view .fc-event {
+ padding: 10px;
+ font-size: 14px;
}
- .ui-datepicker {
- width: 100%;
- font-size: 8pt;
- border-radius: 0;
- border: none;
- padding: 0;
+ // ====== 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;
+ }
+ }
+
+ // ====== Month only
+ .fc-month-view {
+ padding-left: $o-horizontal-padding;
+
+ .fc-widget-header {
+ padding: 3px 0;
+ }
- .ui-widget-header {
- display: flex;
+ .fc-week-number {
background: none;
- background-color: lighten($o-brand-secondary, 30%);
- border-radius: 0;
-
- .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;
+ }
+
+ .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;
}
- .ui-icon {
- display: none;
+
+ &.o_cw_custom_hover, &.o_cw_custom_highlight, &:hover {
+ .fc-bg {
+ visibility: visible;
+ }
}
}
- .ui-datepicker-prev {
- order: 1;
- &:after {
- font-family: FontAwesome;
- content: "";
+
+ &.fc-not-start {
+ border-right-width: 3px;
+
+ .fc-content {
+ padding-left: 6px;
}
}
- .ui-datepicker-next {
- order: 3;
- &:before {
- font-family: FontAwesome;
- content: "";
+
+ &.fc-not-end {
+ margin-right: 0;
+
+ .fc-content {
+ padding-right: 6px;
}
}
- .ui-datepicker-title {
- flex: 0 0 auto;
- order: 2;
- display: inline-block;
- margin: 0 10px;
- }
- }
- 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;
+ }
+ }
+ // ====== 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;
}
}
- .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;
- };
+
+ &.fc-not-end {
+ margin: 0 5px 1px 0;
+
+ .fc-content {
+ padding-left: 6px;
+ padding-right: 0;
+ }
}
- .ui-state-active {
- background: none;
- background-color: $o-brand-odoo;
- color: $o-view-background-color;
+ }
+ }
+ }
+}
+
+.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;
+ }
+ }
+
+ .ui-datepicker {
+ margin: 0;
+ width: 100%;
+ padding: 0;
+
+ &, td, .ui-datepicker-header, td a, td span {
+ border: 0;
+ }
+
+ th {
+ padding: .7em .2em;
+ }
+
+ td {
+ padding: 0;
+
+ a, span {
+ padding: .2em .2em .1em;
+ float: right;
+ width: 22px;
+ background: none;
+ text-align: center;
+ color: gray('700');
+ }
+
+ &.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');
}
- .o_selected_range.o_color {
- background: none;
- background-color: $o-brand-odoo;
- animation: backgroundfade 3s forwards;
+ &.ui-icon-circle-triangle-e:before {
+ content: "\f054"
}
}
+
+ .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_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 {
- padding: 8px;
- border-top: 1px solid lightgray;
- .o_field_many2one {
- width: 100%;
+
+ .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;
}
- .fc-time-grid-event.fc-short .fc-time:before {
- content: '';
+ .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;
+ }
+ }
+ }
+ }
+
+ .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));
+ }
}
}
diff --git a/addons/web/static/src/scss/web_calendar_mobile.scss b/addons/web/static/src/scss/web_calendar_mobile.scss
index af1aa3d7145..de4c10fa12f 100644
--- a/addons/web/static/src/scss/web_calendar_mobile.scss
+++ b/addons/web/static/src/scss/web_calendar_mobile.scss
@@ -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;
diff --git a/addons/web/static/src/xml/web_calendar.xml b/addons/web/static/src/xml/web_calendar.xml
index 9a64c7258d8..fd4f7b3a532 100644
--- a/addons/web/static/src/xml/web_calendar.xml
+++ b/addons/web/static/src/xml/web_calendar.xml
@@ -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>