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', }), @@ -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 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 @@ -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)); 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 = $('
  • ', {class: 'list-group-item flex-shrink-0 d-flex flex-wrap'}); + var $fieldLabel = $('', {class: 'mr-2', text: _.str.sprintf('%s : ', field.string)}); + $fieldLabel.appendTo($field); + var $fieldContainer = $('
    ', {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 @@ -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($('
    ', {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 $('', { + 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; } - .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)); + } } } 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 @@
    -
    @@ -13,58 +12,81 @@
    - -
    -
    -
    - -
    - - : -
    - - + -
    -
    - - - -
    -
    +
    +
    +
    + + + +
    +
    +
    +

    + +

    +
    +
    + +
    +
    +
    + + + +
    +
      +
    • + + +
    • +
    • + + +
    • +
    +
    +
    -

    -
    -
    - -
    - -
    diff --git a/addons/web/static/tests/views/calendar_tests.js b/addons/web/static/tests/views/calendar_tests.js index 16e304c14a6..afc1799446c 100644 --- a/addons/web/static/tests/views/calendar_tests.js +++ b/addons/web/static/tests/views/calendar_tests.js @@ -161,7 +161,6 @@ QUnit.module('Views', { 'mode="week" '+ 'attendee="partner_ids" '+ 'color="partner_id">'+ - ''+ ''+ ''+ '', @@ -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': '' + - '' + - '', + 'event,1,calendar': '', 'event,false,search': '', }; @@ -290,11 +287,14 @@ QUnit.module('Views', { 'date_start="start" '+ 'date_stop="stop" '+ 'all_day="allday" '+ - 'mode="month" '+ - 'readonly_form_view_id="1">'+ - ''+ - '', + '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">'+ - ''+ - '', + '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">'+ ''+ ''+ ''+ @@ -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: + ''+ + ''+ + ''+ + '', + 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: + ''+ + '', + 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">'+ ''+ '', 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">'+ ''+ ''+ ''+ @@ -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">'+ ''+ '', 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: - ''+ - ''+ - '', + '', 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">'+ ''+ '', 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">'+ - ''+ - '', + '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">'+ ''+ '', 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">'+ - ''+ - '', + 'mode="week"/>', archs: archs, viewOptions: { initialDate: initialDate, @@ -1344,7 +1400,7 @@ QUnit.module('Views', { 'date_start="start" '+ 'date_stop="stop" '+ 'all_day="allday"> '+ - ''+ + ''+ '', 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">'+ - ''+ - '', + '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">'+ - ''+ - '', + '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">'+ - ''+ - '', + '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">'+ - ''+ ''+ ''+ '', @@ -1662,7 +1711,6 @@ QUnit.module('Views', { 'mode="week" '+ 'attendee="partner_ids" '+ 'color="partner_id">'+ - ''+ ''+ ''+ '', @@ -1745,7 +1793,6 @@ QUnit.module('Views', { 'mode="week" '+ 'attendee="partner_ids" '+ 'color="partner_id">'+ - ''+ ''+ ''+ '', @@ -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">'+ - ''+ ''+ ''+ '', @@ -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">'+ - ''+ ''+ ''+ '', @@ -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">'+ - ''+ ''+ ''+ '', @@ -2056,10 +2101,7 @@ QUnit.module('Views', { 'date_start="start" '+ 'date_stop="stop" '+ 'all_day="allday" '+ - 'mode="month" '+ - 'readonly_form_view_id="1">'+ - ''+ - '', + 'mode="month"/>', archs: archs, viewOptions: { initialDate: initialDate, @@ -2126,7 +2168,6 @@ QUnit.module('Views', { 'mode="week" '+ 'attendee="partner_ids" '+ 'color="partner_id">'+ - ''+ ''+ ''+ '', @@ -2178,10 +2219,7 @@ QUnit.module('Views', { 'date_start="start" '+ 'date_stop="stop" '+ 'all_day="allday" '+ - 'mode="month" '+ - 'readonly_form_view_id="1">'+ - ''+ - '', + 'mode="month"/>', archs: archs, viewOptions: { initialDate: initialDate, @@ -2216,9 +2254,7 @@ QUnit.module('Views', { 'event,1,calendar': ' '+ - ''+ - '', + 'all_day="allday"/>', 'event,false,search': '', }; @@ -2250,9 +2286,7 @@ QUnit.module('Views', { model: 'event', data: this.data, arch: - ''+ - ''+ - '', + '', 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">'+ - ''+ - '', + 'form_view_id="42"/>', archs: archs, viewOptions: { initialDate: initialDate, @@ -2536,9 +2576,7 @@ QUnit.module('Views', { arch: ' '+ - ''+ - '', + 'mode="month"/>', archs: archs, viewOptions: { initialDate: initialDate, @@ -2593,9 +2631,7 @@ QUnit.module('Views', { arch: ' '+ - ''+ - '', + '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: - ''+ - ''+ - '', - 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(); - }); - }); }); diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml index e9640dd512d..51aa1423584 100644 --- a/addons/web/views/webclient_templates.xml +++ b/addons/web/views/webclient_templates.xml @@ -355,6 +355,7 @@ + diff --git a/doc/reference/views.rst b/doc/reference/views.rst index 08bc5e6313e..f3bc35fa286 100644 --- a/doc/reference/views.rst +++ b/doc/reference/views.rst @@ -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 diff --git a/odoo/addons/base/rng/calendar_view.rng b/odoo/addons/base/rng/calendar_view.rng index 656445e449e..a6737fbcfa2 100644 --- a/odoo/addons/base/rng/calendar_view.rng +++ b/odoo/addons/base/rng/calendar_view.rng @@ -13,13 +13,14 @@ - + + @@ -32,13 +33,6 @@ - - - - - - -