diff options
| author | sri-odoo <sri@odoo.com> | 2018-12-13 12:16:19 +0000 |
|---|---|---|
| committer | Jigar Patel <jat@openerp.com> | 2019-08-07 12:30:55 +0000 |
| commit | 369c20a99bd25172f72f4efde870172a4729aadc (patch) | |
| tree | d6d7a2f4a75348c235e96659a8bc6701808aa18c /addons/web/static/src | |
| parent | aca0e700ac6b3cdb63d6de3ef021cb6fce1cbb47 (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')
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 && 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 && (filter.value && 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> |
